<commit_before>'use strict'

//  ▄▄ •             ▄▄▄ . ▄· ▄▌
// ▐█ ▀ ▪▪     ▪     ▀▄.▀·▐█▪██▌
// ▄█ ▀█▄ ▄█▀▄  ▄█▀▄ ▐▀▀▪▄▐█▌▐█▪
// ▐█▄▪▐█▐█▌.▐▌▐█▌.▐▌▐█▄▄▌ ▐█▀·.
// ·▀▀▀▀  ▀█▄▀▪ ▀█▄▀▪ ▀▀▀   ▀ •

import jsonPath from 'jsonpath'

import * as traversals from './traverse'
import * as topic from './topic'
import * as _util from './util'

/**
 * Flat map of all registered services, indexed by name
 */
var _services = {}

/**
 * Default service configuration object
 */
const _config = {
  strict: true,

  data: {
    matching: true,

    publish: {
      ignore: {
        falsy: true
      }
    }
  }
}

/**
 * A canonical, hierarchical, and composable data source that can publish and receive updates bi-directionally with other services
 * Forms a full k-ary tree that exists in a global forest
 */
export class Service {

  /**
   * Creates and registers a new gooey service with the current module
   *
   * @param {String} name
   * @param {?Function} model
   * @param {?Service} parent
   * @param {?Array} children
   * @param {?Object} config
   */
  constructor(name: String, model?: Function, parent?: Service, children?: Array = [], config?: Object = _config) {
    if (Object.is(name, undefined) || Service.isRegistered(name)) {
      throw `Services must have unique names: ${name}`
    }

    this.name = name
    this.model = model
    this.state = {}
    this.parent = parent instanceof Service ? parent.relateTo(this) : null
    this.children = this.relateToAll(children)
    this.subscriptions = []
    this.config = config
    this.symbol = Symbol(name)

    _services[name] = this

    if (this.model instanceof Function) {
      this.model(this.state)
    }
  }

  /**
   * Getter alias for `data`
   *
   * @returns {Object}
   */
  get data() {
    return this.state
  }

  /**
   * Setter for `data` that automatically publishes changes with default traversal settings
   *
   * @param {Object} data
   */
  set data(data: Object) {
    this.update(data)
  }

  /**
   * Traverses service tree via a conflict-free path and matches subscribers against the published data
   *
   * @param {Object} data
   * @param {?String} traversal
   * @param {?String} direction
   * @returns {Promise} deferred service tree traversal(s)
   */
  // TODO - Allow users to publish data with a certain key
  // - that way you aren't forced to always write a JsonPath or matcher function for each subscribe / publish
  publish(data, traversal: String = 'breadth', direction: String = 'down'): Promise {
    return new Promise((resolve, reject) => {
      // ensure data is pure
      data = data instanceof Object ? Object.assign({}, data) : data

      // process data against matching subscribers
      const matches = this.subscriptions.map(subscrip => subscrip.process(data, false))

      // final result is either converged conflict-resolved subscriber match or cloned source data
      const result = matches[0] || data

      // traverse service node tree and publish result on each "next" node
      return this.traverse(traversal, direction,
        next => next.publish(result, traversal, direction)
      )
    })
  }

  /**
   * Creates and registers a publish subsubscription with the Service
   *
   * @param {String} path
   * @param {Function} on
   */
  subscribe(topic: Object = '$', on: Function): Subscription {
    const subscrip = new Subscription(this, topic, on)

    this.subscriptions.push(subscrip)

    return subscrip
  }

  /**
   * Deregisters a subsubscription from the Service
   *
   * @param {Subscription} subscrip
   */
  unsubscribe(subscrip: Subscription) {
    this.subscriptions.splice(this.subscriptions.indexOf(subscrip), 1)
  }

  /**
   * Updates the Service's canonical data source with new data and publishs the change
   *
   * @param {Object} data
   * @returns {Promise}
   */
  update(data: Object, ...rest): Promise {
    this.state = data

    return this.publish(data, ...rest)
  }

  /**
   * Merges and updates the Service's canonical date source with a new (cloned) data object and publishs the change
   *
   * @param {Object} data
   * @param {?Function} error
   * @returns {Promise}
   */
  merge(data: Object, ...rest): Promise {
    const merged = data instanceof Object ? Object.assign({}, this.state, data) : this.state

    return this.update(merged, ...rest)
  }

  /**
   * Alias for subscribe
   *
   * @param {String} path
   * @param {Function} on
   * @returns {Subscription}
   */
  on(path: String, on: Function): Subscription {
    return this.subscribe(path, on)
  }

  /**
   * Alias for update
   *
   * @param {Object} data
   * @returns {Promise}
   */
  use(data, ...rest): Promise {
    return this.update(data, ...rest)
  }

  /**
   * Alias for merge
   *
   * @param {Object} data
   * @returns {Promise}
   */
  up(data, ...rest): Promise {
    return this.merge(data, ...rest)
  }

  /**
   * Determines set of data that matches the provided subsubscription's path/pattern
   *
   * @param {Object} data
   * @param {Subscription} subscrip
   * @returns {Set}
   */
  matches(data, subscrip: Subscription): Set {
    return subscrip.matches(data)
  }

  /**
   * Recursively traverses service tree via provided `next` function
   *
   * @param {String} traversal supported values defined by gooey.traverse.patterns
   * @param {String} direction up, down or bi
   * @param {Function} next
   * @returns {Promise}
   */
  traverse(traversal: String, direction: String, next: Function): Promise {
    return traversals.step.call(this, traversal, direction, next)
  }

  /**
   * Establishes strong acyclic child relationship with provided service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   * Silently fails if a cyclic relationship is proposed.
   *
   * @param {Service} child service to relate to
   * @returns {Service} modified service with new child relationship
   */
  relateTo(child: Service): Service {
    this.children.push(child)

    if (Service.cycleExists()) {
      this.children.pop() // FIXME - bleh, needs improvement to say the least
    }

    return this
  }

  /**
   * Establishes service as parent to each provided child service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   *
   * @param {Array} children services to relate to
   * @returns {Array} modified children services with new parent relationship
   */
  relateToAll(children: Array): Array {
    return children.map(c => {
      c.parent = this

      // this.relateTo(c) // TODO/FIXME

      return c
    })
  }

  /**
   * Determines a provided service's depth in the service tree
   *
   * @param {Service} node relative/starting service
   * @returns {Number} depth of service
   */
  depth(node: Service = this): Number {
    let nodeDepth = 0

    while (node.parent) {
      node = node.parent
      nodeDepth += 1
    }

    return nodeDepth
  }

  /**
   * Searches for and returns all siblings of the provided service
   *
   * @param {Service} node relative/starting service
   * @param {?Boolean} global return siblings across disjoint trees (true) or siblings in connected hierarchy (false - UNSUPPORTED)
   * @returns {Array} siblings of service
   */
  siblings(node = this, global?: Boolean = false): Array {
    const roots = Service.findRoots()
    const depth = node.depth()

    return Service.findAtDepth(depth, roots).filter(svc => svc !== node)
  }

  /**
   * Determines if the service is a root node in the local service tree
   *
   * @returns {Boolean}
   */
  isRoot(): Boolean {
    return !this.parent
  }

  /**
   * Determines if the service is a leaf node in the local service tree
   *
   * @returns {Boolean}
   */
  isLeaf(): Boolean {
    return !this.children
  }

  /**
   * Determines and returns all root node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findRoots(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isRoot())
  }

  /**
   * Determines and returns all leaf node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findLeafs(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isLeaf())
  }

  /**
   * Determines and returns the nodes (out of the provided service tree) at the target depth
   *
   * @param {Number} targetDepth
   * @param {Array} nodes service tree to search through (default is global)
   * @returns {Array}
   */
  static findAtDepth(targetDepth: Number, nodes: Array = []): Array {
    const found  = []
    let curDepth = 0

    nodes.forEach(node => {
      (node.children || []).forEach(child => {
        curDepth = child.depth()

        if (curDepth < targetDepth) {
          found.push(...this.findAtDepth(targetDepth, [child]))
        } else if (curDepth === targetDepth) {
          found.push(child)
        }
      })
    })

    return found
  }

  /**
   * Determines if a cyclic relationship exists anywhere in the provided service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Boolean}
   */
  static cycleExists(services = _services): Boolean {
    const roots = Service.findRoots(services) || []
    const found = !util.isEmpty(roots) ? roots.map(r => r.name) : []

    let curNode = null
    let cyclic  = false

    roots.forEach(root => {
      curNode = root

      while (!cyclic && !util.isEmpty(curNode.children)) {
        (curNode.children || []).forEach(child => {
          if (!found.includes(child.name)) {
            found.push(child.name)

            curNode = child
          } else {
            cyclic = true
          }
        })
      }
    })

    return cyclic
  }

  /**
   * Determines if a service name is already registered in the global service tree
   *
   * @returns {Boolean}
   */
  static isRegistered(name: String): Boolean {
    return Array.from(_services).map(serv => serv.name).includes(name)
  }
}

/**
 * A topic-based data matcher that reacts to a service's publications
 */
export class Subscription {

  /**
   * A topic-based data matcher that reacts to a service's publications
   *
   * @param {Service} service
   * @param {String} topic topic/pattern to react to (* is wildcard)
   * @param {Function} on functionality to be triggered on successful match
   */
  constructor(service: Service, topic: String, on: Function) {
    // this.key = key // TODO -> will allow subscriptions to be triggered via simple keys
    this.service = service
    this.topic = topic
    this.on = on
    this.active = true
  }

  /**
   * Determines data or a subset of data that matches subscription topic
   *
   * @param {Object} data
   * @returns {Set} data matching subsubscription
   */
  matches(data): Set {
    const matchSet = new Set()

    if (this.active && this.service.config.data.matching) {
      const topicMatches = topic.identify(this.topic).matches(data)

      if (!util.isEmpty(topicMatches)) {
        matchSet.add(...topicMatches)
      }
    }

    return matchSet
  }

  /**
   * Determines if data matches the subsubscription and, if so, allows
   * the subsubscription to mutate and return the data.
   *
   * @param {Object} data
   * @param {Boolean} passive return either untouched data on mismatch (true) or null on mismatch (false)
   * @returns {Object} subsubscription modified data
   */
  process(data, passive: Boolean = true): Object {
    return this.matches(data).size ? this.on(data) : (passive ? data : null)
  }

  /**
   * Unsubscribes a subsubscription from its service and mark it as inactive.
   * Subscription will not react to any messages from service until activated again.
   */
  end() {
    service.unsubscribe(this)

    this.active = false
  }

  /**
   * Activates the subsubscription, permitting it to react to topic-based messages
   */
  activate() {
    this.active = true
  }

  /**
   * For sane debugging
   */
  toString() {
    return `[gooey.${this.name}]: ${JSON.stringify(this)}`
  }

}

/**
 * Alternative destructured alias or Service constructor
 *
 * @param {String} name
 * @param {?Function} model
 * @param {?Service} parent
 * @param {?Array} children
 * @param {?Object} config
 * @returns {Service}
 */
export var service = ({name, model, parent, children, config}) => new Service(name, model, parent, children, config)

/**
 * Exported flat map of module services - to be used with caution
 */
export var services = _services

/**
 * Convenience reference to utility module
 */
export const util = _util

/**
 * Detaches services from module
 */
export const clear = () => { _services = new Set() }

/**
 * Logger for establishing a consistent message format
 */
export const log = (msg: String, level: String) => `[gooey:${level || 'INFO'}] ${msg}`
<commit_msg>:scroll: docstring fixes and renamed Subscription.activate -> start<commit_after>'use strict'

//  ▄▄ •             ▄▄▄ . ▄· ▄▌
// ▐█ ▀ ▪▪     ▪     ▀▄.▀·▐█▪██▌
// ▄█ ▀█▄ ▄█▀▄  ▄█▀▄ ▐▀▀▪▄▐█▌▐█▪
// ▐█▄▪▐█▐█▌.▐▌▐█▌.▐▌▐█▄▄▌ ▐█▀·.
// ·▀▀▀▀  ▀█▄▀▪ ▀█▄▀▪ ▀▀▀   ▀ •

import jsonPath from 'jsonpath'

import * as traversals from './traverse'
import * as topic from './topic'
import * as _util from './util'

/**
 * Flat map of all registered services, indexed by name
 */
var _services = {}

/**
 * Default service configuration object
 */
const _config = {
  strict: true,

  data: {
    matching: true,

    publish: {
      ignore: {
        falsy: true
      }
    }
  }
}

/**
 * A canonical, hierarchical, and composable data source that can publish and receive updates bi-directionally with other services
 * Forms a full k-ary tree that exists in a global forest
 */
export class Service {

  /**
   * Creates and registers a new gooey service with the current module
   *
   * @param {String} name
   * @param {?Function} model
   * @param {?Service} parent
   * @param {?Array} children
   * @param {?Object} config
   */
  constructor(name: String, model?: Function, parent?: Service, children?: Array = [], config?: Object = _config) {
    if (Object.is(name, undefined) || Service.isRegistered(name)) {
      throw `Services must have unique names: ${name}`
    }

    this.name = name
    this.model = model
    this.state = {}
    this.parent = parent instanceof Service ? parent.relateTo(this) : null
    this.children = this.relateToAll(children)
    this.subscriptions = []
    this.config = config
    this.symbol = Symbol(name)

    _services[name] = this

    if (this.model instanceof Function) {
      this.model(this.state)
    }
  }

  /**
   * Getter alias for `data`
   *
   * @returns {Object}
   */
  get data() {
    return this.state
  }

  /**
   * Setter for `data` that automatically publishes changes with default traversal settings
   *
   * @param {Object} data
   */
  set data(data: Object) {
    this.update(data)
  }

  /**
   * Traverses service tree via a conflict-free path and matches subscribers against the published data
   *
   * @param {Object} data
   * @param {?String} traversal
   * @param {?String} direction
   * @returns {Promise} deferred service tree traversal(s)
   */
  // TODO - Allow users to publish data with a certain key
  // - that way you aren't forced to always write a JsonPath or matcher function for each subscribe / publish
  publish(data, traversal: String = 'breadth', direction: String = 'down'): Promise {
    return new Promise((resolve, reject) => {
      // ensure data is pure
      data = data instanceof Object ? Object.assign({}, data) : data

      // process data against matching subscribers
      const matches = this.subscriptions.map(subscrip => subscrip.process(data, false))

      // final result is either converged conflict-resolved subscriber match or cloned source data
      const result = matches[0] || data

      // traverse service node tree and publish result on each "next" node
      return this.traverse(traversal, direction,
        next => next.publish(result, traversal, direction)
      )
    })
  }

  /**
   * Creates and registers a publish subsubscription with the Service
   *
   * @param {Topic|String} topic
   * @param {Function} on
   */
  subscribe(topic = '$', on: Function): Subscription {
    const subscrip = new Subscription(this, topic, on)

    this.subscriptions.push(subscrip)

    return subscrip
  }

  /**
   * Deregisters a subsubscription from the Service
   *
   * @param {Subscription} subscrip
   */
  unsubscribe(subscrip: Subscription) {
    this.subscriptions.splice(this.subscriptions.indexOf(subscrip), 1)
  }

  /**
   * Updates the Service's canonical data source with new data and publishs the change
   *
   * @param {Object} data
   * @returns {Promise}
   */
  update(data: Object, ...rest): Promise {
    this.state = data

    return this.publish(data, ...rest)
  }

  /**
   * Merges and updates the Service's canonical date source with a new (cloned) data object and publishs the change
   *
   * @param {Object} data
   * @param {?Function} error
   * @returns {Promise}
   */
  merge(data: Object, ...rest): Promise {
    const merged = data instanceof Object ? Object.assign({}, this.state, data) : this.state

    return this.update(merged, ...rest)
  }

  /**
   * Alias for subscribe
   *
   * @param {Topic|String} topic
   * @param {Function} on
   * @returns {Subscription}
   */
  on(topic, on: Function): Subscription {
    return this.subscribe(topic, on)
  }

  /**
   * Alias for update
   *
   * @param {Object} data
   * @returns {Promise}
   */
  use(data, ...rest): Promise {
    return this.update(data, ...rest)
  }

  /**
   * Alias for merge
   *
   * @param {Object} data
   * @returns {Promise}
   */
  up(data, ...rest): Promise {
    return this.merge(data, ...rest)
  }

  /**
   * Determines set of data that matches the provided subsubscription's topic
   *
   * @param {Object} data
   * @param {Subscription} subscrip
   * @returns {Set}
   */
  matches(data, subscrip: Subscription): Set {
    return subscrip.matches(data)
  }

  /**
   * Recursively traverses service tree via provided `next` function
   *
   * @param {String} traversal supported values defined by gooey.traverse.patterns
   * @param {String} direction up, down or bi
   * @param {Function} next
   * @returns {Promise}
   */
  traverse(traversal: String, direction: String, next: Function): Promise {
    return traversals.step.call(this, traversal, direction, next)
  }

  /**
   * Establishes strong acyclic child relationship with provided service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   * Silently fails if a cyclic relationship is proposed.
   *
   * @param {Service} child service to relate to
   * @returns {Service} modified service with new child relationship
   */
  relateTo(child: Service): Service {
    this.children.push(child)

    if (Service.cycleExists()) {
      this.children.pop() // FIXME - bleh, needs improvement to say the least
    }

    return this
  }

  /**
   * Establishes service as parent to each provided child service.
   * Child services inherit publications from their parent.
   * The opposite is also supported via `up` traversals.
   *
   * @param {Array} children services to relate to
   * @returns {Array} modified children services with new parent relationship
   */
  relateToAll(children: Array): Array {
    return children.map(c => {
      c.parent = this

      // this.relateTo(c) // TODO/FIXME

      return c
    })
  }

  /**
   * Determines a provided service's depth in the service tree
   *
   * @param {Service} node relative/starting service
   * @returns {Number} depth of service
   */
  depth(node: Service = this): Number {
    let nodeDepth = 0

    while (node.parent) {
      node = node.parent
      nodeDepth += 1
    }

    return nodeDepth
  }

  /**
   * Searches for and returns all siblings of the provided service
   *
   * @param {Service} node relative/starting service
   * @param {?Boolean} global return siblings across disjoint trees (true) or siblings in connected hierarchy (false - UNSUPPORTED)
   * @returns {Array} siblings of service
   */
  siblings(node = this, global?: Boolean = false): Array {
    const roots = Service.findRoots()
    const depth = node.depth()

    return Service.findAtDepth(depth, roots).filter(svc => svc !== node)
  }

  /**
   * Determines if the service is a root node in the local service tree
   *
   * @returns {Boolean}
   */
  isRoot(): Boolean {
    return !this.parent
  }

  /**
   * Determines if the service is a leaf node in the local service tree
   *
   * @returns {Boolean}
   */
  isLeaf(): Boolean {
    return !this.children
  }

  /**
   * Determines and returns all root node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findRoots(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isRoot())
  }

  /**
   * Determines and returns all leaf node services in the global service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Array}
   */
  static findLeafs(services: Array = _services): Array {
    return Object.values(services).filter(svc => svc instanceof Service && svc.isLeaf())
  }

  /**
   * Determines and returns the nodes (out of the provided service tree) at the target depth
   *
   * @param {Number} targetDepth
   * @param {Array} nodes service tree to search through (default is global)
   * @returns {Array}
   */
  static findAtDepth(targetDepth: Number, nodes: Array = []): Array {
    const found  = []
    let curDepth = 0

    nodes.forEach(node => {
      (node.children || []).forEach(child => {
        curDepth = child.depth()

        if (curDepth < targetDepth) {
          found.push(...this.findAtDepth(targetDepth, [child]))
        } else if (curDepth === targetDepth) {
          found.push(child)
        }
      })
    })

    return found
  }

  /**
   * Determines if a cyclic relationship exists anywhere in the provided service tree
   *
   * @param {Array} services service tree to search through (default is global)
   * @returns {Boolean}
   */
  static cycleExists(services = _services): Boolean {
    const roots = Service.findRoots(services) || []
    const found = !util.isEmpty(roots) ? roots.map(r => r.name) : []

    let curNode = null
    let cyclic  = false

    roots.forEach(root => {
      curNode = root

      while (!cyclic && !util.isEmpty(curNode.children)) {
        (curNode.children || []).forEach(child => {
          if (!found.includes(child.name)) {
            found.push(child.name)

            curNode = child
          } else {
            cyclic = true
          }
        })
      }
    })

    return cyclic
  }

  /**
   * Determines if a service name is already registered in the global service tree
   *
   * @returns {Boolean}
   */
  static isRegistered(name: String): Boolean {
    return Array.from(_services).map(serv => serv.name).includes(name)
  }
}

/**
 * A topic-based data matcher that reacts to a service's publications
 */
export class Subscription {

  /**
   * A topic-based data matcher that reacts to a service's publications
   *
   * @param {Service} service
   * @param {String} topic topic/pattern to react to (* is wildcard)
   * @param {Function} on functionality to be triggered on successful match
   */
  constructor(service: Service, topic: String, on: Function) {
    // this.key = key // TODO -> will allow subscriptions to be triggered via simple keys
    this.service = service
    this.topic = topic
    this.on = on
    this.active = true
  }

  /**
   * Determines data or a subset of data that matches subscription topic
   *
   * @param {Object} data
   * @returns {Set} data matching subsubscription
   */
  matches(data): Set {
    const matchSet = new Set()

    if (this.active && this.service.config.data.matching) {
      const topicMatches = topic.identify(this.topic).matches(data)

      if (!util.isEmpty(topicMatches)) {
        matchSet.add(...topicMatches)
      }
    }

    return matchSet
  }

  /**
   * Determines if data matches the subsubscription and, if so, allows
   * the subsubscription to mutate and return the data.
   *
   * @param {Object} data
   * @param {Boolean} passive return either untouched data on mismatch (true) or null on mismatch (false)
   * @returns {Object} subsubscription modified data
   */
  process(data, passive: Boolean = true): Object {
    return this.matches(data).size ? this.on(data) : (passive ? data : null)
  }

  /**
   * Unsubscribes a subsubscription from its service and mark it as inactive.
   * Subscription will not react to any messages from service until activated again.
   */
  end() {
    service.unsubscribe(this)

    this.active = false
  }

  /**
   * Activates the subsubscription, permitting it to react to topic-based messages
   */
  start() {
    this.active = true
  }

  /**
   * For sane debugging
   */
  toString() {
    return `[gooey.${this.name}]: ${JSON.stringify(this)}`
  }

}

/**
 * Alternative destructured alias or Service constructor
 *
 * @param {String} name
 * @param {?Function} model
 * @param {?Service} parent
 * @param {?Array} children
 * @param {?Object} config
 * @returns {Service}
 */
export var service = ({name, model, parent, children, config}) => new Service(name, model, parent, children, config)

/**
 * Exported flat map of module services - to be used with caution
 */
export var services = _services

/**
 * Convenience reference to utility module
 */
export const util = _util

/**
 * Detaches services from module
 */
export const clear = () => { _services = new Set() }

/**
 * Logger for establishing a consistent message format
 */
export const log = (msg: String, level: String) => `[gooey:${level || 'INFO'}] ${msg}`
<|endoftext|>


<commit_before>/**
 *
 * Abstract Om-style cursors that may be adapted to data structures of any type.
 * This intended to be an alternative to immutable-js Cursors.
 *
 * By default, Providence cursors will be unboxed to an Immutable object type.
 * However, Providence will not provide the default initial root data, which
 * should be provided by the caller.
 *
 */
const Immutable = require('immutable');
const { Map, Iterable } = Immutable;

const isPlainObject = require('lodash.isplainobject');
const objGet = require('lodash.get');
const objSet = require('lodash.set');
const objHas = require('lodash.has');

const utils = require('./utils');
// TODO: make these overridable externally
const { valToPath, newPath } = utils;

/* constants */
const NOT_SET = {}; // sentinel value
const IDENTITY = (x) => x;
const INITIAL_PATH = [];
// paths
const UNBOXER_PATH = ['root', 'unbox'];
const BOXER_PATH = ['root', 'box'];
const DATA_PATH = ['root', 'data'];
const PATH_PATH = ['path'];
const GETIN_PATH = ['getIn'];
const SETIN_PATH = ['setIn'];
const DELETEIN_PATH = ['deleteIn'];
const ONUPDATE_PATH = ['onUpdate'];
// Internal _onUpdate function; useful for when Providence is inherited.
// Whereas, onUpdate function is used by users.
const _ONUPDATE_PATH = ['_onUpdate'];

// DEFAULTS is an array of 2-tuple arrays where the first element of the tuple is the key path
// to be checked, and the second element is the function that will be called to generate the value
// to be used to set on the key path in the case that there is no value set in the key path.
const DEFAULTS = [
    [UNBOXER_PATH, IDENTITY],
    [BOXER_PATH, IDENTITY],
    [PATH_PATH, INITIAL_PATH],
    [GETIN_PATH, _defaultGetIn],
    [SETIN_PATH, _defaultSetIn],
    [DELETEIN_PATH, _defaultDeleteIn]
];
const PATH = 0;
const VALUE = 1;

module.exports = Providence;

/**
 * Create a Providence cursor given options.
 * Options may either be plain object or an Immutable Map.
 *
 * @param {Object | Immutable Map} options Defines the character of the providence cursor.
 */
function Providence(options = NOT_SET, skipDataCheck = false, skipProcessOptions = false) {

    if(options === NOT_SET) {
        throw new Error('Expected options to be a plain object or an Immutable Map');
    }

    // This will not support constructors that are extending Providence.
    // They should provide their own setup in their constructor function.
    if(!(this instanceof Providence)) {
        return new Providence(options, skipDataCheck, skipProcessOptions);
    }

    this._options = skipProcessOptions ? options : processOptions(options);

    // Used for caching value of the Providence object.
    // When the unboxed root data and this._refUnboxedRootData are equal,
    // then unboxed root data hasn't changed since the previous look up, and thus
    // this._cachedValue and value at path also hasn't changed.
    this._refUnboxedRootData = NOT_SET;
    this._cachedValue = NOT_SET;

    if(!skipDataCheck && this._options.getIn(DATA_PATH, NOT_SET) === NOT_SET) {
        throw new Error("value at path ['root', 'data'] is required!")
    }
}

Providence.prototype.constructor = Providence;
Providence.prototype.__utils = utils;

/**
 * Returns string representation of `this.deref()`.
 *
 * @return {String}
 */
Providence.prototype.toString = function() {
    return String(this.deref());
}

/**
 * Dereference by unboxing the root data and getting the value at path.
 * If path happens to not exist, notSetValue is, instead, returned.
 * If notSetValue is not provided, it becomes value: void 0.
 *
 * @param  {any} notSetValue
 * @return {any}             The sub-structure value at path relative to
 *                           root data.
 */
Providence.prototype.valueOf =
Providence.prototype.deref = function(notSetValue) {

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    // check if value was cached
    if(this._refUnboxedRootData === unboxed) {
        const resolvedValue = this._cachedValue;
        return resolvedValue === NOT_SET ? notSetValue : resolvedValue;
    }

    const path = options.getIn(PATH_PATH);
    const fetchGetIn = options.getIn(GETIN_PATH);
    const getIn = fetchGetIn(unboxed);

    const resolvedValue = getIn(path, NOT_SET);

    // cache
    this._refUnboxedRootData = unboxed;
    this._cachedValue = resolvedValue;

    if(this._firstValue === NOT_SET) {
        this._firstValue = resolvedValue;
    }

    return resolvedValue === NOT_SET ? notSetValue : resolvedValue;
}

/**
 * Return true if a path exists within the unboxed root data.
 *
 * @return {Bool}
 */
Providence.prototype.exists = function() {
    return(this.deref(NOT_SET) !== NOT_SET);
}

/**
 * Returns the array representation of the path.
 *
 * @type {Array}
 */
Providence.prototype.path = function() {
    return this._options.getIn(PATH_PATH).slice();
}

/**
 * Returns providence cursor's options. It is safe to modify this object since
 * it is an Immutable Map object; and any changes will not reflect back to the
 * originating cursor, unless it is used as the new options.
 *
 * @return {Immutable Map}
 */
Providence.prototype.options = function() {
    return this._options;
}

/**
 * Create a new Providence object with options via this instance.
 *
 * @param  {Immutable Map | Object} newOptions
 * @return {Providence}
 */
Providence.prototype.new = function(newOptions) {
    return new (this.constructor)(newOptions);
}

/**
 * When given no arguments, return itself.
 *
 * By default, this is the same behaviour as cursor() method for immutable-js
 * cursors:
 * - Returns a sub-cursor following the path keyValue starting from this cursor.
 * - If keyValue is not an array, an array containing keyValue is instead used.
 *
 * @param  {any | array } keyValue
 * @return {Providence}
 */
Providence.prototype.cursor = function(keyValue) {

    if(arguments.length === 0) {
        return this;
    }

    // TODO: overridable valToPath; expect this to be a pure function
    // valToPath converts keyValue to path
    const subpath = valToPath(keyValue);

    // TODO: validateKeyPath that returns bool; expect this to be a pure function
    // be able to abort cursor procedure. aborting returns this instead.
    if(subpath.length === 0) {
        return this;
    }

    const options = this._options;

    const path = options.getIn(PATH_PATH);
    const newOptions = options.setIn(PATH_PATH, newPath(path, subpath));

    return new (this.constructor)(newOptions, true, true);
}

/**
 * Update value in the unboxed root data at path using the updater function.
 * If the path exists, updater is called using:
 * - the value at path
 * - unboxed root data
 * - boxed root data
 *
 * If path doesn't exist, notSetValue is used as the initial value.
 * If notSetValue is not defined, it has value void 0.
 *
 * If updater returns the same value the value at path (or notSetValue),
 * then no changes has truly occured, and the current cursor is instead returned.
 *
 * Otherwise, the new value is replaced at path of the unboxed root data,
 * and a new providence cursor is returned with the new boxed root data.
 * In addition, any defined functions at onUpdate and/or _onUpdate within options
 * will be called with the following:
 * - options
 * - cursor path
 * - new unboxed root data with the new value
 * - previous unboxed root data
 *
 * @param  {any} notSetValue
 * @param  {Function} updater
 * @return {Providence}
 */
Providence.prototype.update = function(notSetValue, updater) {

    if(!updater) {
        updater = notSetValue;
        notSetValue = void 0;
    }

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    // fetch state at path
    const fetchGetIn = options.getIn(GETIN_PATH);
    const getIn = fetchGetIn(unboxed);
    const path = options.getIn(PATH_PATH);
    const state = getIn(path, notSetValue);

    // get new state
    //
    // call updater with:
    // - state      the object to be updated
    // - unboxed    unboxed root data
    // - rootData   boxed root data
    const newState = updater.call(null, state, unboxed, rootData);

    // TODO: delegate to an overridable: confirmChange(prev, next)
    if(state === newState) {
        return this;
    }

    // merge new state at path into root data
    const fetchSetIn = options.getIn(SETIN_PATH);
    const setIn = fetchSetIn(unboxed);
    const newRootData = setIn(path, newState);

    // box new root data
    const boxer = options.getIn(BOXER_PATH);
    const boxed = boxer(newRootData, rootData);

    callOnUpdate(options, path, newRootData, unboxed);

    const newOptions = options.setIn(DATA_PATH, boxed);

    return new (this.constructor)(newOptions, true, true);
}

/**
 * Delete value at path.
 *
 * If the new unboxed root data is the same as the previous, original unboxed root data,
 * then the current cursor is returned.
 *
 * Otherwise, any defined functions at onUpdate and/or _onUpdate within options
 * will be called with the following:
 * - options
 * - cursor path
 * - new unboxed root data with the new value
 * - previous unboxed root data
 *
 * In addition, the new providence cursor containing the new unboxed root data
 * will be returned.
 *
 * @type {Providence}
 */
Providence.prototype.remove =
Providence.prototype.delete = function() {

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    const fetchDeleteIn = options.getIn(DELETEIN_PATH);
    const deleteIn = fetchDeleteIn(unboxed);
    const path = options.getIn(PATH_PATH);
    const newRootData = deleteIn(path);

    // TODO: delegate to an overridable: confirmChange(prev, next)
    if(unboxed === newRootData) {
        return this;
    }

    // box new root data
    const boxer = options.getIn(BOXER_PATH);
    const boxed = boxer(newRootData, rootData);

    callOnUpdate(options, path, newRootData, unboxed);

    const newOptions = options.setIn(DATA_PATH, boxed);

    return new (this.constructor)(newOptions, true, true);
}

Providence.prototype.unboxRootData = function() {
    const options = this._options;

    const unboxer = options.getIn(UNBOXER_PATH);
    const rootData = options.getIn(DATA_PATH);
    return {
        rootData: rootData,
        unboxed: unboxer(rootData)
    };
}

/* helpers */

// Process and convert options to an Immutable Map if it isn't one already.
//
// processOptions is called whenever a new Providence object is created.
//
// We take advantage of structural sharing (a la flyweight) when a child
// Providence object inherit options from its parent.
function processOptions(options) {
    options = validateOptions(options);

    return options;
}

const __immutableSetIfNotSet = setIfNotSet.bind(null, _ImmutableHasIn, _ImmutableSetIn);
const immutableValidateOptions = __validateOptions.bind(null, __immutableSetIfNotSet);

const __plainSetIfNotSet = setIfNotSet.bind(null, _plainHasIn, _plainSetIn);
const plainValidateOptions = __validateOptions.bind(null, __plainSetIfNotSet);

function validateOptions(options) {

    if(Map.isMap(options)) {
        return immutableValidateOptions(options);
    }

    if(Iterable.isIterable(options)) {
        throw new Error('Expected options to be an Immutable Map!');
    }

    if(!isPlainObject(options)) {
        throw new Error('Expected options to be a plain object');
    }

    options = plainValidateOptions(options);

    // preserve values that Immutable.fromJS may transform; which is undesirable.
    const getIn = _plainGetIn(options);
    const setIn = _plainSetIn(options);

    const rootData = getIn(DATA_PATH, NOT_SET);
    const path = getIn(PATH_PATH, NOT_SET);

    // Set null values for paths so that they're not transformed by Immutable.fromJS().
    // We do this in case that rootData is a deeply nested plain object.
    //
    // NOTE: This is a side-effect; we restore the values later.
    //
    // TODO: document this side-effect and potential for unintended effect
    // with Object.observe(...)
    if(rootData !== NOT_SET) {
        setIn(DATA_PATH, null);
    }

    if(path !== NOT_SET) {
        setIn(PATH_PATH, null);
    }

    options = Immutable.fromJS(options);

    // restore values
    if(rootData !== NOT_SET) {
        options = options.setIn(DATA_PATH, rootData);
        setIn(DATA_PATH, rootData);
    }

    if(path !== NOT_SET) {
        options = options.setIn(PATH_PATH, path);
        setIn(PATH_PATH, path);
    }

    return options;
}

function __validateOptions(_setIfNotSet, _options) {

    let options = _options;
    let n = DEFAULTS.length;

    while(n-- > 0) {
        const current = DEFAULTS[n];
        options = _setIfNotSet(options, current[PATH], current[VALUE]);
    }

    return options;
}

function setIfNotSet(_fetchHasIn, _fetchSetIn, options, path, value) {

    const _hasIn = _fetchHasIn(options);

    if(!_hasIn(path)) {
        const _setIn = _fetchSetIn(options);
        return _setIn(path, value);
    }

    return options;
}

function callOnUpdate(options, path, newRoot, oldRoot) {
    const _onUpdate = options.getIn(_ONUPDATE_PATH, NOT_SET);
    if(_onUpdate !== NOT_SET) {
        _onUpdate.call(null, options, path.slice(), newRoot, oldRoot);
    }

    const onUpdate = options.getIn(ONUPDATE_PATH, NOT_SET);
    if(onUpdate !== NOT_SET) {
        onUpdate.call(null, options, path.slice(), newRoot, oldRoot);
    }
}

function _plainHasIn(obj) {
    return objHas.bind(objHas, obj);
}

function _plainSetIn(obj) {
    return objSet.bind(objSet, obj);
}

function _plainGetIn(obj) {
    return objGet.bind(objGet, obj);
}

function _ImmutableHasIn(obj) {
    return obj.hasIn.bind(obj);
}

function _ImmutableSetIn(obj) {
    return obj.setIn.bind(obj);
}

function _defaultGetIn(rootData) {
    return rootData.getIn.bind(rootData);
}

function _defaultSetIn(rootData) {
    return rootData.setIn.bind(rootData);
}

function _defaultDeleteIn(rootData) {
    return rootData.deleteIn.bind(rootData);
}
<commit_msg>dead code<commit_after>/**
 *
 * Abstract Om-style cursors that may be adapted to data structures of any type.
 * This intended to be an alternative to immutable-js Cursors.
 *
 * By default, Providence cursors will be unboxed to an Immutable object type.
 * However, Providence will not provide the default initial root data, which
 * should be provided by the caller.
 *
 */
const Immutable = require('immutable');
const { Map, Iterable } = Immutable;

const isPlainObject = require('lodash.isplainobject');
const objGet = require('lodash.get');
const objSet = require('lodash.set');
const objHas = require('lodash.has');

const utils = require('./utils');
// TODO: make these overridable externally
const { valToPath, newPath } = utils;

/* constants */
const NOT_SET = {}; // sentinel value
const IDENTITY = (x) => x;
const INITIAL_PATH = [];
// paths
const UNBOXER_PATH = ['root', 'unbox'];
const BOXER_PATH = ['root', 'box'];
const DATA_PATH = ['root', 'data'];
const PATH_PATH = ['path'];
const GETIN_PATH = ['getIn'];
const SETIN_PATH = ['setIn'];
const DELETEIN_PATH = ['deleteIn'];
const ONUPDATE_PATH = ['onUpdate'];
// Internal _onUpdate function; useful for when Providence is inherited.
// Whereas, onUpdate function is used by users.
const _ONUPDATE_PATH = ['_onUpdate'];

// DEFAULTS is an array of 2-tuple arrays where the first element of the tuple is the key path
// to be checked, and the second element is the function that will be called to generate the value
// to be used to set on the key path in the case that there is no value set in the key path.
const DEFAULTS = [
    [UNBOXER_PATH, IDENTITY],
    [BOXER_PATH, IDENTITY],
    [PATH_PATH, INITIAL_PATH],
    [GETIN_PATH, _defaultGetIn],
    [SETIN_PATH, _defaultSetIn],
    [DELETEIN_PATH, _defaultDeleteIn]
];
const PATH = 0;
const VALUE = 1;

module.exports = Providence;

/**
 * Create a Providence cursor given options.
 * Options may either be plain object or an Immutable Map.
 *
 * @param {Object | Immutable Map} options Defines the character of the providence cursor.
 */
function Providence(options = NOT_SET, skipDataCheck = false, skipProcessOptions = false) {

    if(options === NOT_SET) {
        throw new Error('Expected options to be a plain object or an Immutable Map');
    }

    // This will not support constructors that are extending Providence.
    // They should provide their own setup in their constructor function.
    if(!(this instanceof Providence)) {
        return new Providence(options, skipDataCheck, skipProcessOptions);
    }

    this._options = skipProcessOptions ? options : processOptions(options);

    // Used for caching value of the Providence object.
    // When the unboxed root data and this._refUnboxedRootData are equal,
    // then unboxed root data hasn't changed since the previous look up, and thus
    // this._cachedValue and value at path also hasn't changed.
    this._refUnboxedRootData = NOT_SET;
    this._cachedValue = NOT_SET;

    if(!skipDataCheck && this._options.getIn(DATA_PATH, NOT_SET) === NOT_SET) {
        throw new Error("value at path ['root', 'data'] is required!")
    }
}

Providence.prototype.constructor = Providence;
Providence.prototype.__utils = utils;

/**
 * Returns string representation of `this.deref()`.
 *
 * @return {String}
 */
Providence.prototype.toString = function() {
    return String(this.deref());
}

/**
 * Dereference by unboxing the root data and getting the value at path.
 * If path happens to not exist, notSetValue is, instead, returned.
 * If notSetValue is not provided, it becomes value: void 0.
 *
 * @param  {any} notSetValue
 * @return {any}             The sub-structure value at path relative to
 *                           root data.
 */
Providence.prototype.valueOf =
Providence.prototype.deref = function(notSetValue) {

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    // check if value was cached
    if(this._refUnboxedRootData === unboxed) {
        const resolvedValue = this._cachedValue;
        return resolvedValue === NOT_SET ? notSetValue : resolvedValue;
    }

    const path = options.getIn(PATH_PATH);
    const fetchGetIn = options.getIn(GETIN_PATH);
    const getIn = fetchGetIn(unboxed);

    const resolvedValue = getIn(path, NOT_SET);

    // cache
    this._refUnboxedRootData = unboxed;
    this._cachedValue = resolvedValue;

    return resolvedValue === NOT_SET ? notSetValue : resolvedValue;
}

/**
 * Return true if a path exists within the unboxed root data.
 *
 * @return {Bool}
 */
Providence.prototype.exists = function() {
    return(this.deref(NOT_SET) !== NOT_SET);
}

/**
 * Returns the array representation of the path.
 *
 * @type {Array}
 */
Providence.prototype.path = function() {
    return this._options.getIn(PATH_PATH).slice();
}

/**
 * Returns providence cursor's options. It is safe to modify this object since
 * it is an Immutable Map object; and any changes will not reflect back to the
 * originating cursor, unless it is used as the new options.
 *
 * @return {Immutable Map}
 */
Providence.prototype.options = function() {
    return this._options;
}

/**
 * Create a new Providence object with options via this instance.
 *
 * @param  {Immutable Map | Object} newOptions
 * @return {Providence}
 */
Providence.prototype.new = function(newOptions) {
    return new (this.constructor)(newOptions);
}

/**
 * When given no arguments, return itself.
 *
 * By default, this is the same behaviour as cursor() method for immutable-js
 * cursors:
 * - Returns a sub-cursor following the path keyValue starting from this cursor.
 * - If keyValue is not an array, an array containing keyValue is instead used.
 *
 * @param  {any | array } keyValue
 * @return {Providence}
 */
Providence.prototype.cursor = function(keyValue) {

    if(arguments.length === 0) {
        return this;
    }

    // TODO: overridable valToPath; expect this to be a pure function
    // valToPath converts keyValue to path
    const subpath = valToPath(keyValue);

    // TODO: validateKeyPath that returns bool; expect this to be a pure function
    // be able to abort cursor procedure. aborting returns this instead.
    if(subpath.length === 0) {
        return this;
    }

    const options = this._options;

    const path = options.getIn(PATH_PATH);
    const newOptions = options.setIn(PATH_PATH, newPath(path, subpath));

    return new (this.constructor)(newOptions, true, true);
}

/**
 * Update value in the unboxed root data at path using the updater function.
 * If the path exists, updater is called using:
 * - the value at path
 * - unboxed root data
 * - boxed root data
 *
 * If path doesn't exist, notSetValue is used as the initial value.
 * If notSetValue is not defined, it has value void 0.
 *
 * If updater returns the same value the value at path (or notSetValue),
 * then no changes has truly occured, and the current cursor is instead returned.
 *
 * Otherwise, the new value is replaced at path of the unboxed root data,
 * and a new providence cursor is returned with the new boxed root data.
 * In addition, any defined functions at onUpdate and/or _onUpdate within options
 * will be called with the following:
 * - options
 * - cursor path
 * - new unboxed root data with the new value
 * - previous unboxed root data
 *
 * @param  {any} notSetValue
 * @param  {Function} updater
 * @return {Providence}
 */
Providence.prototype.update = function(notSetValue, updater) {

    if(!updater) {
        updater = notSetValue;
        notSetValue = void 0;
    }

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    // fetch state at path
    const fetchGetIn = options.getIn(GETIN_PATH);
    const getIn = fetchGetIn(unboxed);
    const path = options.getIn(PATH_PATH);
    const state = getIn(path, notSetValue);

    // get new state
    //
    // call updater with:
    // - state      the object to be updated
    // - unboxed    unboxed root data
    // - rootData   boxed root data
    const newState = updater.call(null, state, unboxed, rootData);

    // TODO: delegate to an overridable: confirmChange(prev, next)
    if(state === newState) {
        return this;
    }

    // merge new state at path into root data
    const fetchSetIn = options.getIn(SETIN_PATH);
    const setIn = fetchSetIn(unboxed);
    const newRootData = setIn(path, newState);

    // box new root data
    const boxer = options.getIn(BOXER_PATH);
    const boxed = boxer(newRootData, rootData);

    callOnUpdate(options, path, newRootData, unboxed);

    const newOptions = options.setIn(DATA_PATH, boxed);

    return new (this.constructor)(newOptions, true, true);
}

/**
 * Delete value at path.
 *
 * If the new unboxed root data is the same as the previous, original unboxed root data,
 * then the current cursor is returned.
 *
 * Otherwise, any defined functions at onUpdate and/or _onUpdate within options
 * will be called with the following:
 * - options
 * - cursor path
 * - new unboxed root data with the new value
 * - previous unboxed root data
 *
 * In addition, the new providence cursor containing the new unboxed root data
 * will be returned.
 *
 * @type {Providence}
 */
Providence.prototype.remove =
Providence.prototype.delete = function() {

    const options = this._options;

    // unbox root data
    const { rootData, unboxed } = this.unboxRootData();

    const fetchDeleteIn = options.getIn(DELETEIN_PATH);
    const deleteIn = fetchDeleteIn(unboxed);
    const path = options.getIn(PATH_PATH);
    const newRootData = deleteIn(path);

    // TODO: delegate to an overridable: confirmChange(prev, next)
    if(unboxed === newRootData) {
        return this;
    }

    // box new root data
    const boxer = options.getIn(BOXER_PATH);
    const boxed = boxer(newRootData, rootData);

    callOnUpdate(options, path, newRootData, unboxed);

    const newOptions = options.setIn(DATA_PATH, boxed);

    return new (this.constructor)(newOptions, true, true);
}

Providence.prototype.unboxRootData = function() {
    const options = this._options;

    const unboxer = options.getIn(UNBOXER_PATH);
    const rootData = options.getIn(DATA_PATH);
    return {
        rootData: rootData,
        unboxed: unboxer(rootData)
    };
}

/* helpers */

// Process and convert options to an Immutable Map if it isn't one already.
//
// processOptions is called whenever a new Providence object is created.
//
// We take advantage of structural sharing (a la flyweight) when a child
// Providence object inherit options from its parent.
function processOptions(options) {
    options = validateOptions(options);

    return options;
}

const __immutableSetIfNotSet = setIfNotSet.bind(null, _ImmutableHasIn, _ImmutableSetIn);
const immutableValidateOptions = __validateOptions.bind(null, __immutableSetIfNotSet);

const __plainSetIfNotSet = setIfNotSet.bind(null, _plainHasIn, _plainSetIn);
const plainValidateOptions = __validateOptions.bind(null, __plainSetIfNotSet);

function validateOptions(options) {

    if(Map.isMap(options)) {
        return immutableValidateOptions(options);
    }

    if(Iterable.isIterable(options)) {
        throw new Error('Expected options to be an Immutable Map!');
    }

    if(!isPlainObject(options)) {
        throw new Error('Expected options to be a plain object');
    }

    options = plainValidateOptions(options);

    // preserve values that Immutable.fromJS may transform; which is undesirable.
    const getIn = _plainGetIn(options);
    const setIn = _plainSetIn(options);

    const rootData = getIn(DATA_PATH, NOT_SET);
    const path = getIn(PATH_PATH, NOT_SET);

    // Set null values for paths so that they're not transformed by Immutable.fromJS().
    // We do this in case that rootData is a deeply nested plain object.
    //
    // NOTE: This is a side-effect; we restore the values later.
    //
    // TODO: document this side-effect and potential for unintended effect
    // with Object.observe(...)
    if(rootData !== NOT_SET) {
        setIn(DATA_PATH, null);
    }

    if(path !== NOT_SET) {
        setIn(PATH_PATH, null);
    }

    options = Immutable.fromJS(options);

    // restore values
    if(rootData !== NOT_SET) {
        options = options.setIn(DATA_PATH, rootData);
        setIn(DATA_PATH, rootData);
    }

    if(path !== NOT_SET) {
        options = options.setIn(PATH_PATH, path);
        setIn(PATH_PATH, path);
    }

    return options;
}

function __validateOptions(_setIfNotSet, _options) {

    let options = _options;
    let n = DEFAULTS.length;

    while(n-- > 0) {
        const current = DEFAULTS[n];
        options = _setIfNotSet(options, current[PATH], current[VALUE]);
    }

    return options;
}

function setIfNotSet(_fetchHasIn, _fetchSetIn, options, path, value) {

    const _hasIn = _fetchHasIn(options);

    if(!_hasIn(path)) {
        const _setIn = _fetchSetIn(options);
        return _setIn(path, value);
    }

    return options;
}

function callOnUpdate(options, path, newRoot, oldRoot) {
    const _onUpdate = options.getIn(_ONUPDATE_PATH, NOT_SET);
    if(_onUpdate !== NOT_SET) {
        _onUpdate.call(null, options, path.slice(), newRoot, oldRoot);
    }

    const onUpdate = options.getIn(ONUPDATE_PATH, NOT_SET);
    if(onUpdate !== NOT_SET) {
        onUpdate.call(null, options, path.slice(), newRoot, oldRoot);
    }
}

function _plainHasIn(obj) {
    return objHas.bind(objHas, obj);
}

function _plainSetIn(obj) {
    return objSet.bind(objSet, obj);
}

function _plainGetIn(obj) {
    return objGet.bind(objGet, obj);
}

function _ImmutableHasIn(obj) {
    return obj.hasIn.bind(obj);
}

function _ImmutableSetIn(obj) {
    return obj.setIn.bind(obj);
}

function _defaultGetIn(rootData) {
    return rootData.getIn.bind(rootData);
}

function _defaultSetIn(rootData) {
    return rootData.setIn.bind(rootData);
}

function _defaultDeleteIn(rootData) {
    return rootData.deleteIn.bind(rootData);
}
<|endoftext|>


<commit_before>import { parse } from 'dotenv'
import fs from 'fs'
import { DefinePlugin } from 'webpack'

class Dotenv {
  constructor (options) {
    options = options || {}
    if (!options.path) options.path = './.env'

    this.env = {}
    try {
      this.env = parse(fs.readFileSync(options.path))
    } catch (err) {}
  }

  apply (compiler) {
    const plugin = Object.keys(this.env).reduce((definitions, key) => {
      const value = process.env[key] || this.env[key]
      definitions[`process.env.${key}`] = JSON.stringify(value)
      return definitions
    }, {})

    compiler.apply(new DefinePlugin(plugin))
  }
}

export default Dotenv
<commit_msg>feat: Adding support for safe dotenv compilation.<commit_after>import dotenv from 'dotenv-safe'
import fs from 'fs'
import { DefinePlugin } from 'webpack'

class Dotenv {
  constructor (options) {
    this.options = Object.assign({
      path: './.env',
      safe: false,
      sample: './env.example'
    }, options)

    this.blueprint = (this.safe) ? this.loadFile(this.options.sample) : this.loadFile(this.options.path)
    this.env = this.loadFile(this.options.path)
  }

  loadFile (file) {
    try {
      return dotenv.parse(fs.readFileSync(file))
    } catch (err) {
      return {}
    }
  }

  apply (compiler) {
    const plugin = Object.keys(this.blueprint).reduce((definitions, key) => {
      const value = process.env[key] || this.env[key]
      if (!value) {
        throw new Error(`Missing environment variable: ${key}`)
      }
      definitions[`process.env.${key}`] = JSON.stringify(value)
      return definitions
    }, {})

    compiler.apply(new DefinePlugin(plugin))
  }
}

export default Dotenv
<|endoftext|>


<commit_before>/* eslint no-unused-expressions: 0 */
const reUnit = /width|height|top|left|right|bottom|margin|padding/i;
let _amId = 1;
const _amDisplay = {};

let requestAnimationFrame;
if (typeof window !== 'undefined') {
  requestAnimationFrame = window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function(callback) {
      window.setTimeout(callback, 1000 / 60);
    };
} else {
  requestAnimationFrame = function() {
    throw new Error('requestAnimationFrame is not supported, maybe you are running in the server side');
  };
}

function getAmId(obj) {
  return obj._amId || (obj._amId = _amId++);
}

function setAmDisplay(elem, display) {
  const id = getAmId(elem);
  _amDisplay[`_am_${id}`] = display;
}

function getAmDisplay(elem) {
  const id = getAmId(elem);
  return _amDisplay[`_am_${id}`];
}

export default {
  // el can be an Element, NodeList or selector
  addClass(el, className) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];

    els.forEach(e => {
      if (this.hasClass(e, className)) { return; }

      if (e.classList) {
        e.classList.add(className);
      } else {
        e.className += ' ' + className;
      }
    });
  },

  // el can be an Element, NodeList or selector
  removeClass(el, className) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];

    els.forEach(e => {
      if (this.hasClass(e, className)) {
        if (e.classList) {
          e.classList.remove(className);
        } else {
          e.className = e.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
        }
      }
    });
  },

  // el can be an Element or selector
  hasClass(el, className) {
    if (typeof el === 'string') el = document.querySelector(el);
    if (el.classList) {
      return el.classList.contains(className);
    }
    return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
  },

  // el can be an Element or selector
  toggleClass(el, className) {
    if (typeof el === 'string') el = document.querySelector(el);
    const flag = this.hasClass(el, className);
    if (flag) {
      this.removeClass(el, className);
    } else {
      this.addClass(el, className);
    }
    return flag;
  },

  insertAfter(newEl, targetEl) {
    const parent = targetEl.parentNode;

    if (parent.lastChild === targetEl) {
      parent.appendChild(newEl);
    } else {
      parent.insertBefore(newEl, targetEl.nextSibling);
    }
  },

  // el can be an Element, NodeList or query string
  remove(el) {
    if (typeof el === 'string') {
      [].forEach.call(document.querySelectorAll(el), node => {
        node.parentNode.removeChild(node);
      });
    } else if (el.parentNode) {
      // it's an Element
      el.parentNode.removeChild(el);
    } else if (el instanceof NodeList) {
      // it's an array of elements
      [].forEach.call(el, node => {
        node.parentNode.removeChild(node);
      });
    } else {
      throw new Error('you can only pass Element, array of Elements or query string as argument');
    }
  },

  forceReflow(el) {
    el.offsetHeight;
  },

  getDocumentScrollTop() {
    // IE8 used `document.documentElement`
    return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
  },

  // Set the current vertical position of the scroll bar for document
  // Note: do not support fixed position of body
  setDocumentScrollTop(value) {
    window.scrollTo(0, value);
    return value;
  },

  outerHeight(el) {
    return el.offsetHeight;
  },

  outerHeightWithMargin(el) {
    let height = el.offsetHeight;
    const style = getComputedStyle(el);

    height += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
    return height;
  },

  outerWidth(el) {
    return el.offsetWidth;
  },

  outerWidthWithMargin(el) {
    let width = el.offsetWidth;
    const style = getComputedStyle(el);

    width += (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0);
    return width;
  },

  getComputedStyles(el) {
    return el.ownerDocument.defaultView.getComputedStyle(el, null);
  },

  getOffset(el) {
    const html = el.ownerDocument.documentElement;
    let box = { top: 0, left: 0 };

    // If we don't have gBCR, just use 0,0 rather than error
    // BlackBerry 5, iOS 3 (original iPhone)
    if ( typeof el.getBoundingClientRect !== 'undefined' ) {
      box = el.getBoundingClientRect();
    }

    return {
      top: box.top + window.pageYOffset - html.clientTop,
      left: box.left + window.pageXOffset - html.clientLeft
    };
  },

  getPosition(el) {
    if (!el) {
      return {
        left: 0,
        top: 0
      };
    }

    return {
      left: el.offsetLeft,
      top: el.offsetTop
    };
  },

  setStyle(node, att, val, style) {
    style = style || node.style;

    if (style) {
      if (val === null || val === '') { // normalize unsetting
        val = '';
      } else if (!isNaN(Number(val)) && reUnit.test(att)) { // number values may need a unit
        val += 'px';
      }

      if (att === '') {
        att = 'cssText';
        val = '';
      }

      style[att] = val;
    }
  },

  setStyles(el, hash) {
    const HAS_CSSTEXT_FEATURE = typeof(el.style.cssText) !== 'undefined';
    function trim(str) {
      return str.replace(/^\s+|\s+$/g, '');
    }
    let originStyleText;
    const originStyleObj = {};
    if (!!HAS_CSSTEXT_FEATURE) {
      originStyleText = el.style.cssText;
    } else {
      originStyleText = el.getAttribute('style');
    }
    originStyleText.split(';').forEach(item => {
      if (item.indexOf(':') !== -1) {
        const obj = item.split(':');
        originStyleObj[trim(obj[0])] = trim(obj[1]);
      }
    });

    const styleObj = {};
    Object.keys(hash).forEach(item => {
      this.setStyle(el, item, hash[item], styleObj);
    });
    const mergedStyleObj = Object.assign({}, originStyleObj, styleObj);
    const styleText = Object.keys(mergedStyleObj)
        .map(item => item + ': ' + mergedStyleObj[item] + ';')
        .join(' ');

    if (!!HAS_CSSTEXT_FEATURE) {
      el.style.cssText = styleText;
    } else {
      el.setAttribute('style', styleText);
    }
  },

  getStyle(el, att, style) {
    style = style || el.style;

    let val = '';

    if (style) {
      val = style[att];

      if (val === '') {
        val = this.getComputedStyle(el, att);
      }
    }

    return val;
  },

  // NOTE: Known bug, will return 'auto' if style value is 'auto'
  getComputedStyle(el, att) {
    const win = el.ownerDocument.defaultView;
    // null means not return presudo styles
    const computed = win.getComputedStyle(el, null);

    return att ? computed.att : computed;
  },

  getPageSize() {
    let xScroll, yScroll;

    if (window.innerHeight && window.scrollMaxY) {
      xScroll = window.innerWidth + window.scrollMaxX;
      yScroll = window.innerHeight + window.scrollMaxY;
    } else {
      if (document.body.scrollHeight > document.body.offsetHeight) { // all but Explorer Mac
        xScroll = document.body.scrollWidth;
        yScroll = document.body.scrollHeight;
      } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
        xScroll = document.body.offsetWidth;
        yScroll = document.body.offsetHeight;
      }
    }

    let windowWidth, windowHeight;

    if (self.innerHeight) { // all except Explorer
      if (document.documentElement.clientWidth) {
        windowWidth = document.documentElement.clientWidth;
      } else {
        windowWidth = self.innerWidth;
      }
      windowHeight = self.innerHeight;
    } else {
      if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
        windowWidth = document.documentElement.clientWidth;
        windowHeight = document.documentElement.clientHeight;
      } else {
        if (document.body) { // other Explorers
          windowWidth = document.body.clientWidth;
          windowHeight = document.body.clientHeight;
        }
      }
    }

    let pageHeight, pageWidth;

    // for small pages with total height less then height of the viewport
    if (yScroll < windowHeight) {
      pageHeight = windowHeight;
    } else {
      pageHeight = yScroll;
    }
    // for small pages with total width less then width of the viewport
    if (xScroll < windowWidth) {
      pageWidth = xScroll;
    } else {
      pageWidth = windowWidth;
    }

    return {
      pageWidth: pageWidth,
      pageHeight: pageHeight,
      windowWidth: windowWidth,
      windowHeight: windowHeight
    };
  },

  get(selector) {
    return document.querySelector(selector) || {};
  },

  getAll(selector) {
    return document.querySelectorAll(selector);
  },

  // selector 可选。字符串值，规定在何处停止对祖先元素进行匹配的选择器表达式。
  // filter   可选。字符串值，包含用于匹配元素的选择器表达式。
  parentsUntil(el, selector, filter) {
    const result = [];
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
    // match start from parent
    el = el.parentElement;
    while (el && !matchesSelector.call(el, selector)) {
      if (!filter) {
        result.push(el);
      } else {
        if (matchesSelector.call(el, filter)) {
          result.push(el);
        }
      }
      el = el.parentElement;
    }
    return result;
  },

  // 获得匹配选择器的第一个祖先元素，从当前元素开始沿 DOM 树向上
  closest(el, selector) {
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    while (el) {
      if (matchesSelector.call(el, selector)) {
        return el;
      }

      el = el.parentElement;
    }
    return null;
  },

  // el can be an Element, NodeList or selector
  _showHide(el, show ) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];
    let display;
    const values = [];
    if (els.length === 0) {
      return;
    }
    els.forEach((e, index) => {
      if (e.style) {
        display = e.style.display;
        if (show) {
          if (display === 'none') {
            values[index] = getAmDisplay(e) || '';
          }
        } else {
          if (display !== 'none') {
            values[index] = 'none';
            setAmDisplay(e, display);
          }
        }
      }
    });

    els.forEach((e, index) => {
      if ( values[index] !== null ) {
        els[index].style.display = values[index];
      }
    });
  },

  show(elements) {
    this._showHide(elements, true);
  },

  hide(elements) {
    this._showHide(elements, false);
  },

  toggle(element) {
    if (element.style.display === 'none') {
      this.show(element);
    } else {
      this.hide(element);
    }
  },

  /**
   * scroll to location with animation
   * @param  {Number} to       to assign the scrollTop value
   * @param  {Number} duration assign the animate duration
   * @return {Null}            return null
   */
  scrollTo(to = 0, duration = 16) {
    if (duration < 0) {
      return;
    }
    const diff = to - this.getDocumentScrollTop();
    if (diff === 0) {
      return;
    }
    const perTick = diff / duration * 10;
    requestAnimationFrame(() => {
      if (Math.abs(perTick) > Math.abs(diff)) {
        this.setDocumentScrollTop(this.getDocumentScrollTop() + diff);
        return;
      }
      this.setDocumentScrollTop(this.getDocumentScrollTop() + perTick);
      if (diff > 0 && this.getDocumentScrollTop() >= to || diff < 0 && this.getDocumentScrollTop() <= to) {
        return;
      }
      this.scrollTo(to, duration - 16);
    });
  },

  // matches(el, '.my-class'); 这里不能使用伪类选择器
  is(el, selector) {
    return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
  },

  width(el) {
    const styles = this.getComputedStyles(el);
    const width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0);

    const boxSizing = styles.boxSizing || 'content-box';
    if (boxSizing === 'border-box') {
      return width;
    }

    const borderLeftWidth = parseFloat(styles.borderLeftWidth);
    const borderRightWidth = parseFloat(styles.borderRightWidth);
    const paddingLeft = parseFloat(styles.paddingLeft);
    const paddingRight = parseFloat(styles.paddingRight);
    return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight;
  },

  height(el) {
    const styles = this.getComputedStyles(el);
    const height = parseFloat(styles.height.indexOf('px') !== -1 ? styles.height : 0);

    const boxSizing = styles.boxSizing || 'content-box';
    if (boxSizing === 'border-box') {
      return height;
    }

    const borderTopWidth = parseFloat(styles.borderTopWidth);
    const borderBottomWidth = parseFloat(styles.borderBottomWidth);
    const paddingTop = parseFloat(styles.paddingTop);
    const paddingBottom = parseFloat(styles.paddingBottom);
    return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom;
  }
};
<commit_msg>fix getComputedStyle<commit_after>/* eslint no-unused-expressions: 0 */
const reUnit = /width|height|top|left|right|bottom|margin|padding/i;
let _amId = 1;
const _amDisplay = {};

let requestAnimationFrame;
if (typeof window !== 'undefined') {
  requestAnimationFrame = window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function(callback) {
      window.setTimeout(callback, 1000 / 60);
    };
} else {
  requestAnimationFrame = function() {
    throw new Error('requestAnimationFrame is not supported, maybe you are running in the server side');
  };
}

function getAmId(obj) {
  return obj._amId || (obj._amId = _amId++);
}

function setAmDisplay(elem, display) {
  const id = getAmId(elem);
  _amDisplay[`_am_${id}`] = display;
}

function getAmDisplay(elem) {
  const id = getAmId(elem);
  return _amDisplay[`_am_${id}`];
}

export default {
  // el can be an Element, NodeList or selector
  addClass(el, className) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];

    els.forEach(e => {
      if (this.hasClass(e, className)) { return; }

      if (e.classList) {
        e.classList.add(className);
      } else {
        e.className += ' ' + className;
      }
    });
  },

  // el can be an Element, NodeList or selector
  removeClass(el, className) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];

    els.forEach(e => {
      if (this.hasClass(e, className)) {
        if (e.classList) {
          e.classList.remove(className);
        } else {
          e.className = e.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
        }
      }
    });
  },

  // el can be an Element or selector
  hasClass(el, className) {
    if (typeof el === 'string') el = document.querySelector(el);
    if (el.classList) {
      return el.classList.contains(className);
    }
    return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
  },

  // el can be an Element or selector
  toggleClass(el, className) {
    if (typeof el === 'string') el = document.querySelector(el);
    const flag = this.hasClass(el, className);
    if (flag) {
      this.removeClass(el, className);
    } else {
      this.addClass(el, className);
    }
    return flag;
  },

  insertAfter(newEl, targetEl) {
    const parent = targetEl.parentNode;

    if (parent.lastChild === targetEl) {
      parent.appendChild(newEl);
    } else {
      parent.insertBefore(newEl, targetEl.nextSibling);
    }
  },

  // el can be an Element, NodeList or query string
  remove(el) {
    if (typeof el === 'string') {
      [].forEach.call(document.querySelectorAll(el), node => {
        node.parentNode.removeChild(node);
      });
    } else if (el.parentNode) {
      // it's an Element
      el.parentNode.removeChild(el);
    } else if (el instanceof NodeList) {
      // it's an array of elements
      [].forEach.call(el, node => {
        node.parentNode.removeChild(node);
      });
    } else {
      throw new Error('you can only pass Element, array of Elements or query string as argument');
    }
  },

  forceReflow(el) {
    el.offsetHeight;
  },

  getDocumentScrollTop() {
    // IE8 used `document.documentElement`
    return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
  },

  // Set the current vertical position of the scroll bar for document
  // Note: do not support fixed position of body
  setDocumentScrollTop(value) {
    window.scrollTo(0, value);
    return value;
  },

  outerHeight(el) {
    return el.offsetHeight;
  },

  outerHeightWithMargin(el) {
    let height = el.offsetHeight;
    const style = getComputedStyle(el);

    height += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
    return height;
  },

  outerWidth(el) {
    return el.offsetWidth;
  },

  outerWidthWithMargin(el) {
    let width = el.offsetWidth;
    const style = getComputedStyle(el);

    width += (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0);
    return width;
  },

  getComputedStyles(el) {
    return el.ownerDocument.defaultView.getComputedStyle(el, null);
  },

  getOffset(el) {
    const html = el.ownerDocument.documentElement;
    let box = { top: 0, left: 0 };

    // If we don't have gBCR, just use 0,0 rather than error
    // BlackBerry 5, iOS 3 (original iPhone)
    if ( typeof el.getBoundingClientRect !== 'undefined' ) {
      box = el.getBoundingClientRect();
    }

    return {
      top: box.top + window.pageYOffset - html.clientTop,
      left: box.left + window.pageXOffset - html.clientLeft
    };
  },

  getPosition(el) {
    if (!el) {
      return {
        left: 0,
        top: 0
      };
    }

    return {
      left: el.offsetLeft,
      top: el.offsetTop
    };
  },

  setStyle(node, att, val, style) {
    style = style || node.style;

    if (style) {
      if (val === null || val === '') { // normalize unsetting
        val = '';
      } else if (!isNaN(Number(val)) && reUnit.test(att)) { // number values may need a unit
        val += 'px';
      }

      if (att === '') {
        att = 'cssText';
        val = '';
      }

      style[att] = val;
    }
  },

  setStyles(el, hash) {
    const HAS_CSSTEXT_FEATURE = typeof(el.style.cssText) !== 'undefined';
    function trim(str) {
      return str.replace(/^\s+|\s+$/g, '');
    }
    let originStyleText;
    const originStyleObj = {};
    if (!!HAS_CSSTEXT_FEATURE) {
      originStyleText = el.style.cssText;
    } else {
      originStyleText = el.getAttribute('style');
    }
    originStyleText.split(';').forEach(item => {
      if (item.indexOf(':') !== -1) {
        const obj = item.split(':');
        originStyleObj[trim(obj[0])] = trim(obj[1]);
      }
    });

    const styleObj = {};
    Object.keys(hash).forEach(item => {
      this.setStyle(el, item, hash[item], styleObj);
    });
    const mergedStyleObj = Object.assign({}, originStyleObj, styleObj);
    const styleText = Object.keys(mergedStyleObj)
        .map(item => item + ': ' + mergedStyleObj[item] + ';')
        .join(' ');

    if (!!HAS_CSSTEXT_FEATURE) {
      el.style.cssText = styleText;
    } else {
      el.setAttribute('style', styleText);
    }
  },

  getStyle(el, att, style) {
    style = style || el.style;

    let val = '';

    if (style) {
      val = style[att];

      if (val === '') {
        val = this.getComputedStyle(el, att);
      }
    }

    return val;
  },

  // NOTE: Known bug, will return 'auto' if style value is 'auto'
  getComputedStyle(el, att) {
    const win = el.ownerDocument.defaultView;
    // null means not return presudo styles
    const computed = win.getComputedStyle(el, null);

    return att ? computed[att] : computed;
  },

  getPageSize() {
    let xScroll, yScroll;

    if (window.innerHeight && window.scrollMaxY) {
      xScroll = window.innerWidth + window.scrollMaxX;
      yScroll = window.innerHeight + window.scrollMaxY;
    } else {
      if (document.body.scrollHeight > document.body.offsetHeight) { // all but Explorer Mac
        xScroll = document.body.scrollWidth;
        yScroll = document.body.scrollHeight;
      } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
        xScroll = document.body.offsetWidth;
        yScroll = document.body.offsetHeight;
      }
    }

    let windowWidth, windowHeight;

    if (self.innerHeight) { // all except Explorer
      if (document.documentElement.clientWidth) {
        windowWidth = document.documentElement.clientWidth;
      } else {
        windowWidth = self.innerWidth;
      }
      windowHeight = self.innerHeight;
    } else {
      if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
        windowWidth = document.documentElement.clientWidth;
        windowHeight = document.documentElement.clientHeight;
      } else {
        if (document.body) { // other Explorers
          windowWidth = document.body.clientWidth;
          windowHeight = document.body.clientHeight;
        }
      }
    }

    let pageHeight, pageWidth;

    // for small pages with total height less then height of the viewport
    if (yScroll < windowHeight) {
      pageHeight = windowHeight;
    } else {
      pageHeight = yScroll;
    }
    // for small pages with total width less then width of the viewport
    if (xScroll < windowWidth) {
      pageWidth = xScroll;
    } else {
      pageWidth = windowWidth;
    }

    return {
      pageWidth: pageWidth,
      pageHeight: pageHeight,
      windowWidth: windowWidth,
      windowHeight: windowHeight
    };
  },

  get(selector) {
    return document.querySelector(selector) || {};
  },

  getAll(selector) {
    return document.querySelectorAll(selector);
  },

  // selector 可选。字符串值，规定在何处停止对祖先元素进行匹配的选择器表达式。
  // filter   可选。字符串值，包含用于匹配元素的选择器表达式。
  parentsUntil(el, selector, filter) {
    const result = [];
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
    // match start from parent
    el = el.parentElement;
    while (el && !matchesSelector.call(el, selector)) {
      if (!filter) {
        result.push(el);
      } else {
        if (matchesSelector.call(el, filter)) {
          result.push(el);
        }
      }
      el = el.parentElement;
    }
    return result;
  },

  // 获得匹配选择器的第一个祖先元素，从当前元素开始沿 DOM 树向上
  closest(el, selector) {
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    while (el) {
      if (matchesSelector.call(el, selector)) {
        return el;
      }

      el = el.parentElement;
    }
    return null;
  },

  // el can be an Element, NodeList or selector
  _showHide(el, show ) {
    if (typeof el === 'string') el = document.querySelectorAll(el);
    const els = (el instanceof NodeList) ? [].slice.call(el) : [el];
    let display;
    const values = [];
    if (els.length === 0) {
      return;
    }
    els.forEach((e, index) => {
      if (e.style) {
        display = e.style.display;
        if (show) {
          if (display === 'none') {
            values[index] = getAmDisplay(e) || '';
          }
        } else {
          if (display !== 'none') {
            values[index] = 'none';
            setAmDisplay(e, display);
          }
        }
      }
    });

    els.forEach((e, index) => {
      if ( values[index] !== null ) {
        els[index].style.display = values[index];
      }
    });
  },

  show(elements) {
    this._showHide(elements, true);
  },

  hide(elements) {
    this._showHide(elements, false);
  },

  toggle(element) {
    if (element.style.display === 'none') {
      this.show(element);
    } else {
      this.hide(element);
    }
  },

  /**
   * scroll to location with animation
   * @param  {Number} to       to assign the scrollTop value
   * @param  {Number} duration assign the animate duration
   * @return {Null}            return null
   */
  scrollTo(to = 0, duration = 16) {
    if (duration < 0) {
      return;
    }
    const diff = to - this.getDocumentScrollTop();
    if (diff === 0) {
      return;
    }
    const perTick = diff / duration * 10;
    requestAnimationFrame(() => {
      if (Math.abs(perTick) > Math.abs(diff)) {
        this.setDocumentScrollTop(this.getDocumentScrollTop() + diff);
        return;
      }
      this.setDocumentScrollTop(this.getDocumentScrollTop() + perTick);
      if (diff > 0 && this.getDocumentScrollTop() >= to || diff < 0 && this.getDocumentScrollTop() <= to) {
        return;
      }
      this.scrollTo(to, duration - 16);
    });
  },

  // matches(el, '.my-class'); 这里不能使用伪类选择器
  is(el, selector) {
    return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
  },

  width(el) {
    const styles = this.getComputedStyles(el);
    const width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0);

    const boxSizing = styles.boxSizing || 'content-box';
    if (boxSizing === 'border-box') {
      return width;
    }

    const borderLeftWidth = parseFloat(styles.borderLeftWidth);
    const borderRightWidth = parseFloat(styles.borderRightWidth);
    const paddingLeft = parseFloat(styles.paddingLeft);
    const paddingRight = parseFloat(styles.paddingRight);
    return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight;
  },

  height(el) {
    const styles = this.getComputedStyles(el);
    const height = parseFloat(styles.height.indexOf('px') !== -1 ? styles.height : 0);

    const boxSizing = styles.boxSizing || 'content-box';
    if (boxSizing === 'border-box') {
      return height;
    }

    const borderTopWidth = parseFloat(styles.borderTopWidth);
    const borderBottomWidth = parseFloat(styles.borderBottomWidth);
    const paddingTop = parseFloat(styles.paddingTop);
    const paddingBottom = parseFloat(styles.paddingBottom);
    return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom;
  }
};
<|endoftext|>


<commit_before>import React from 'react';
import { TAG_NAMES } from './tag-names.js';
export { TAG_NAMES };

const isArray    = x => Array.isArray(x);
const isString   = x => typeof x === 'string' && x.length > 0;
const isSelector = x => isString(x) && (startsWith(x, '.') || startsWith(x, '#'));
const isChildren = x => /string|number|boolean/.test(typeof x) || isArray(x);
const startsWith = (string, start) => string.indexOf(start) === 0;
const split      = (string, separator) => string.split(separator);
const subString  = (string, start, end) => string.substring(start, end);
const hasKey     = x => x && x.some(child => child.hasOwnProperty('key'));

const parseSelector = selector => {
  const classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/;
  const parts        = split(selector, classIdSplit);

  return parts.reduce((acc, part) => {

    if (startsWith(part, '#')) {
      acc.id = subString(part, 1);
    } else if (startsWith(part, '.')) {
      acc.className = `${acc.className} ${subString(part, 1)}`.trim();
    }

    return acc;
  }, { className: '' });

};

const createElement = (nameOrType, props, children) => {
  const args = [ nameOrType ];

  if (props) args.push(props);

  if (children) {
    if (!isArray(children) || hasKey(children)) {
      args.push(children);
    } else {
      args.push(...children);
    }
  }

  return React.createElement.apply(React, args);
};

export const hh = nameOrType => (first, second, third) => {

  if (isSelector(first)) {
    const selector = parseSelector(first);

    // selector, children
    if (isChildren(second)) {
      return createElement(nameOrType, selector, second);
    }

    // selector, props, children
    let { className = '' } = second || {};
    className = `${selector.className} ${className} `.trim();
    const props = { ...second, ...selector, className };

    if (isChildren(third)) {
      return createElement(nameOrType, props, third);
    }

    return createElement(nameOrType, props);
  }

  // children
  if (isChildren(first)) {
    return createElement(nameOrType, {}, first);
  }

  // props, children
  if (isChildren(second)) {
    return createElement(nameOrType, first, second);
  }

  return createElement(nameOrType, first);
};

const h = (nameOrType, ...rest) => hh(nameOrType)(...rest);

module.exports = TAG_NAMES.reduce((exported, type) => {
  exported[type] = hh(type);
  return exported;
}, { h, hh });
<commit_msg>I don't really think the 'hasKey' stuff is necessary<commit_after>import React from 'react';
import { TAG_NAMES } from './tag-names.js';
export { TAG_NAMES };

const isArray    = x => Array.isArray(x);
const isString   = x => typeof x === 'string' && x.length > 0;
const isSelector = x => isString(x) && (startsWith(x, '.') || startsWith(x, '#'));
const isChildren = x => /string|number|boolean/.test(typeof x) || isArray(x);
const startsWith = (string, start) => string.indexOf(start) === 0;
const split      = (string, separator) => string.split(separator);
const subString  = (string, start, end) => string.substring(start, end);

const parseSelector = selector => {
  const classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/;
  const parts        = split(selector, classIdSplit);

  return parts.reduce((acc, part) => {

    if (startsWith(part, '#')) {
      acc.id = subString(part, 1);
    } else if (startsWith(part, '.')) {
      acc.className = `${acc.className} ${subString(part, 1)}`.trim();
    }

    return acc;
  }, { className: '' });

};

const createElement = (nameOrType, props = {}, children = []) => {
  const args = [ nameOrType, props ];

  if (!isArray(children)) {
    args.push(children);
  } else {
    args.push(...children);
  }

  return React.createElement.apply(React, args);
};

export const hh = nameOrType => (first, second, third) => {

  if (isSelector(first)) {
    const selector = parseSelector(first);

    // selector, children
    if (isChildren(second)) {
      return createElement(nameOrType, selector, second);
    }

    // selector, props, children
    let { className = '' } = second || {};
    className = `${selector.className} ${className} `.trim();
    const props = { ...second, ...selector, className };

    if (isChildren(third)) {
      return createElement(nameOrType, props, third);
    }

    return createElement(nameOrType, props);
  }

  // children
  if (isChildren(first)) {
    return createElement(nameOrType, {}, first);
  }

  // props, children
  if (isChildren(second)) {
    return createElement(nameOrType, first, second);
  }

  return createElement(nameOrType, first);
};

const h = (nameOrType, ...rest) => hh(nameOrType)(...rest);

module.exports = TAG_NAMES.reduce((exported, type) => {
  exported[type] = hh(type);
  return exported;
}, { h, hh });
<|endoftext|>


<commit_before>import { app, BrowserWindow, shell } from 'electron';
import fs from 'fs-extra';
import path from 'path';

/* eslint-disable */
if (require('electron-squirrel-startup')) app.quit();

import windowStateKeeper from 'electron-window-state';

import { isDevMode, isWindows } from './environment';
import ipcApi from './electron/ipc-api';
import Tray from './lib/Tray';
import Settings from './electron/Settings';
import { appId } from './package.json';
import './electron/exception';
/* eslint-enable */

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
const settings = new Settings();
let willQuitApp = false;

// Ensure that the recipe directory exists
fs.ensureDir(path.join(app.getPath('userData'), 'recipes'));

// Set App ID for Windows
if (isWindows) {
  app.setAppUserModelId(appId);
}

const createWindow = async () => {
  // Remember window size
  const mainWindowState = windowStateKeeper({
    defaultWidth: 800,
    defaultHeight: 600,
  });

  // Create the browser window.
  mainWindow = new BrowserWindow({
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    minWidth: 800,
    minHeight: 600,
    titleBarStyle: 'hidden',
    backgroundColor: '#3498db',
    autoHideMenuBar: true,
  });

  // Initialize System Tray
  const trayIcon = new Tray(mainWindow);

  // Initialize ipcApi
  ipcApi({ mainWindow, settings, trayIcon });

  // Manage Window State
  mainWindowState.manage(mainWindow);

  // and load the index.html of the app.
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // Open the DevTools.
  if (isDevMode) {
    mainWindow.webContents.openDevTools();
  }

  // Emitted when the window is closed.
  mainWindow.on('close', (e) => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    if (!willQuitApp && (settings.get('runInBackground') === undefined || settings.get('runInBackground'))) {
      e.preventDefault();
      mainWindow.hide();

      if (process.platform === 'win32') {
        mainWindow.setSkipTaskbar(true);
      }
    } else {
      app.quit();
    }
  });

  // For Windows we need to store a flag to properly restore the window
  // if the window was maximized before minimizing it so system tray
  mainWindow.on('minimize', () => {
    app.wasMaximized = app.isMaximized;

    if (settings.get('minimizeToSystemTray')) {
      mainWindow.setSkipTaskbar(true);
    }
  });

  mainWindow.on('maximize', () => {
    app.isMaximized = true;
  });

  mainWindow.on('unmaximize', () => {
    app.isMaximized = false;
  });

  mainWindow.on('restore', () => {
    mainWindow.setSkipTaskbar(false);

    if (app.wasMaximized) {
      mainWindow.maximize();
    }

    if (!settings.get('enableSystemTray')) {
      trayIcon.hide();
    }
  });

  mainWindow.on('show', () => {
    mainWindow.setSkipTaskbar(false);
  });

  app.mainWindow = mainWindow;
  app.isMaximized = mainWindow.isMaximized();

  mainWindow.webContents.on('new-window', (e, url) => {
    e.preventDefault();
    shell.openExternal(url);
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (settings.get('runInBackground') === undefined
    || settings.get('runInBackground')) {
    app.quit();
  }
});

app.on('before-quit', () => {
  willQuitApp = true;
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  } else {
    mainWindow.show();
  }
});
<commit_msg>Display system tray icon when minimizing to system tray<commit_after>import { app, BrowserWindow, shell } from 'electron';
import fs from 'fs-extra';
import path from 'path';

/* eslint-disable */
if (require('electron-squirrel-startup')) app.quit();

import windowStateKeeper from 'electron-window-state';

import { isDevMode, isWindows } from './environment';
import ipcApi from './electron/ipc-api';
import Tray from './lib/Tray';
import Settings from './electron/Settings';
import { appId } from './package.json';
import './electron/exception';
/* eslint-enable */

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
const settings = new Settings();
let willQuitApp = false;

// Ensure that the recipe directory exists
fs.ensureDir(path.join(app.getPath('userData'), 'recipes'));

// Set App ID for Windows
if (isWindows) {
  app.setAppUserModelId(appId);
}

const createWindow = async () => {
  // Remember window size
  const mainWindowState = windowStateKeeper({
    defaultWidth: 800,
    defaultHeight: 600,
  });

  // Create the browser window.
  mainWindow = new BrowserWindow({
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    minWidth: 800,
    minHeight: 600,
    titleBarStyle: 'hidden',
    backgroundColor: '#3498db',
    autoHideMenuBar: true,
  });

  // Initialize System Tray
  const trayIcon = new Tray(mainWindow);

  // Initialize ipcApi
  ipcApi({ mainWindow, settings, trayIcon });

  // Manage Window State
  mainWindowState.manage(mainWindow);

  // and load the index.html of the app.
  mainWindow.loadURL(`file://${__dirname}/index.html`);

  // Open the DevTools.
  if (isDevMode) {
    mainWindow.webContents.openDevTools();
  }

  // Emitted when the window is closed.
  mainWindow.on('close', (e) => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    if (!willQuitApp && (settings.get('runInBackground') === undefined || settings.get('runInBackground'))) {
      e.preventDefault();
      mainWindow.hide();

      if (process.platform === 'win32') {
        mainWindow.setSkipTaskbar(true);
      }
    } else {
      app.quit();
    }
  });

  // For Windows we need to store a flag to properly restore the window
  // if the window was maximized before minimizing it so system tray
  mainWindow.on('minimize', () => {
    app.wasMaximized = app.isMaximized;

    if (settings.get('minimizeToSystemTray')) {
      mainWindow.setSkipTaskbar(true);
      trayIcon.show();
    }
  });

  mainWindow.on('maximize', () => {
    app.isMaximized = true;
  });

  mainWindow.on('unmaximize', () => {
    app.isMaximized = false;
  });

  mainWindow.on('restore', () => {
    mainWindow.setSkipTaskbar(false);

    if (app.wasMaximized) {
      mainWindow.maximize();
    }

    if (!settings.get('enableSystemTray')) {
      trayIcon.hide();
    }
  });

  mainWindow.on('show', () => {
    mainWindow.setSkipTaskbar(false);
  });

  app.mainWindow = mainWindow;
  app.isMaximized = mainWindow.isMaximized();

  mainWindow.webContents.on('new-window', (e, url) => {
    e.preventDefault();
    shell.openExternal(url);
  });
};

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (settings.get('runInBackground') === undefined
    || settings.get('runInBackground')) {
    app.quit();
  }
});

app.on('before-quit', () => {
  willQuitApp = true;
});

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow();
  } else {
    mainWindow.show();
  }
});
<|endoftext|>