traversal.js

'use strict'


const destr =(c)=> {
  return typeof(c) == 'string' ? {id: c} : c
}

/** A config object that a Query may be constructed from
 * @typedef {Object} QueryConf
 * @property {string} id - match against the id of the node
 * @property {string|string[]} tag - expect the node to have all the given tags
 * @property {boolean} leaf - match only leaves
 * @property {boolean} stem - match only stems
 * @property {boolean} root - match only root
 */

/** A literal that a Query may be constructed from.
 * Either a configuration object or a string representing a node id.
 * @typedef {(string|QueryConf)} QueryLit
 */

/** A Query object or QueryLiteral
 * @typedef {(Query|QueryLit)} QueryLike
 */

/** The Query class matches against the basic properties of a node
 * @constructor
 * @param {QueryLit} conf - a QueryConf object or a string representing a node id
 */
class Query {
  static make(conf) {
    return conf?.isQuery ? conf : new this(conf || {})
  }
  constructor(conf={}) {
    conf = destr(conf)
    Object.assign(this, {
      id: null,
      tag: null,
      leaf: false,
      stem: false,
      root: false,
    }, conf)
    if (typeof(this.tag) == 'string') this.tag = this.tag.split(' ')
  }
  get isQuery() { return true }

  // Match against a node
  match(node) {
    return (!this.id || node.id == this.id)
      && (!this.tag || node.hasTag(...this.tag))
      && (!this.leaf || node.isLeaf)
      && (!this.stem || node.isStem)
      && (!this.root || node.isRoot)
  }
}
 
/** A config object that a Traverse may be constructed from
 * @typedef {Object} TraverseConf
 * @property {string} id - match against the id of the node
 * @property {string|string[]} tag - expect the node to have all the given tags
 * @property {boolean} leaf - match only leaves
 * @property {boolean} stem - match only stems
 * @property {boolean} root - match only root
 * @property {number} depth - limit depth of traversal. a negative number means no limit
 * @property {boolean} self - include self in traversal
 * @property {boolean} first - stop traversal on first match and return it, or null if no match
 * @property {boolean} last - return only the last matched node, or null if no match
 * @property {boolean} up - traverse upwards through this nodes ancestors
 */

/** A literal that a Traverse may be constructed from.
 * Either a configuration object or a string representing a node id.
 * @typedef {(string|TraverseConf)} TraverseLit
 */

/** A Traverse object or TraverseLiteral
 * @typedef {(Traverse|TraverseLit)} TraverseLike
 */

/** The Traverse class matches against nodes according to configurable constraints
 * @constructor
 * @param {TraverseLit} conf - a TraverseConf object or a string representing a node id
 */
class Traverse extends Query {
  static make(conf) {
    return conf?.isTraverse ? conf : new this(conf || {})
  }
  constructor(conf={}) {
    conf = destr(conf)
    super(Object.assign({
      depth: -1,
      self: false,
      first: false,
      last: false,
      up: false,
      top: true,
    }, conf))
  }
  get isTraverse() { return true }

  next() {
    return new Traverse({...this, depth: this.depth - 1, top: false})
  }
}

class Inquiry {
  static make(...atoms) {
    if (atoms[0]?.isInquiry) return atoms[0]
    else return new this(...atoms)
  }
  constructor(...atoms) {
    const last = atoms[atoms.length-1]
    let cb = null
    if (typeof(last) == 'function') {
      cb = last
      atoms = atoms.slice(0,-1)
    }
    this.traverse = Traverse.make(atoms[0])
    this.cb = cb
  }
  get isInquiry() { return true }
}

class Selector extends Array {
  static make(...atoms) {
    if (atoms[0]?.isSelector) return atoms[0]
    else return new this(...atoms)
  }
  constructor(...atoms) {
    const last = atoms[atoms.length-1]
    let cb = null
    if (typeof(last) == 'function') {
      cb = last
      atoms = atoms.slice(0,-1)
    }
    atoms = atoms.map(c=> Traverse.make(c))
    if (!atoms.length) atoms = [Traverse.make()]
    super(...atoms)
    this.cb = cb
  }
  get isSelector() { return true }
}

/** A literal that a Visitor may be constructed from.
 * @typedef {Array<QueryLike|eachNode>} VisitorLit
 */

/** A Visitor object or VisitorLit
 * @typedef {(Visitor|VisitorLit)} VisitorLike
 */

/** A set of Query objects and a callback to be called on nodes that match them.
 * @constructor
 * @param {...QueryLike} query - one or more QueryLike objects to match against nodes
 * @param {eachNode} callback - called on nodes matching any of the visitor's queries
 */
class Visitor extends Array {
  static make(...atoms) {
    if (atoms[0]?.isVisitor) return atoms[0]
    else return new this(...atoms)
  }
  constructor(...atoms) {
    const last = atoms[atoms.length-1]
    let cb = null
    if (typeof(last) == 'function') {
      cb = last
      atoms = atoms.slice(0,-1)
    }
    atoms = atoms.map(c=> Query.make(c))
    if (!atoms.length) atoms = [Query.make()]
    super(...atoms)
    this.cb = cb
  }
  get isVisitor() { return true }
}


module.exports = { Visitor, Selector, Inquiry, Query, Traverse }