Source: core/src/plugins/input/parser.js

import { type, typeMatcher } from './type.js'

/**
 * @memberof module:@citation-js/core.plugins.input.util
 * @param {module:@citation-js/core.plugins.input~typeParser} data
 */
class TypeParser {
  /**
   * @access protected
   * @type {Array<module:@citation-js/core.plugins.input~dataType>}
   */
  validDataTypes = ['String', 'Array', 'SimpleObject', 'ComplexObject', 'Primitive']

  constructor (data) {
    this.data = data
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  /**
   * @access protected
   * @throws {RangeError} if dataType is not valid
   */
  validateDataType () {
    const dataType = this.data.dataType
    if (dataType && !this.validDataTypes.includes(dataType)) {
      throw new RangeError(`dataType was ${dataType}; expected one of ${this.validDataTypes}`)
    }
  }

  /**
   * @access protected
   * @throws {TypeError} if predicate is not valid
   */
  validateParseType () {
    const predicate = this.data.predicate
    if (predicate && !(predicate instanceof RegExp || typeof predicate === 'function')) {
      throw new TypeError(`predicate was ${typeof predicate}; expected RegExp or function`)
    }
  }

  /**
   * @access protected
   * @throws {TypeError} if predicate is not valid
   */
  validateTokenList () {
    const tokenList = this.data.tokenList
    if (tokenList && typeof tokenList !== 'object') {
      throw new TypeError(`tokenList was ${typeof tokenList}; expected object or RegExp`)
    }
  }

  /**
   * @access protected
   * @throws {TypeError} if propertyConstraint is not valid
   */
  validatePropertyConstraint () {
    const propertyConstraint = this.data.propertyConstraint
    if (propertyConstraint && typeof propertyConstraint !== 'object') {
      throw new TypeError(`propertyConstraint was ${typeof propertyConstraint}; expected array or object`)
    }
  }

  /**
   * @access protected
   * @throws {TypeError} if elementConstraint is not valid
   */
  validateElementConstraint () {
    const elementConstraint = this.data.elementConstraint
    if (elementConstraint && typeof elementConstraint !== 'string') {
      throw new TypeError(`elementConstraint was ${typeof elementConstraint}; expected string`)
    }
  }

  /**
   * @access protected
   * @throws {TypeError} if extends is not valid
   */
  validateExtends () {
    const extend = this.data.extends
    if (extend && typeof extend !== 'string') {
      throw new TypeError(`extends was ${typeof extend}; expected string`)
    }
  }

  /**
   * @access public
   * @throws {TypeError|RangeError} if typeParser is not valid
   */
  validate () {
    if (this.data === null || typeof this.data !== 'object') {
      throw new TypeError(`typeParser was ${typeof this.data}; expected object`)
    }
    this.validateDataType()
    this.validateParseType()
    this.validateTokenList()
    this.validatePropertyConstraint()
    this.validateElementConstraint()
    this.validateExtends()
  }

  // ==========================================================================
  // Simplification helpers
  // ==========================================================================

  /**
   * @access protected
   * @return {Array<module:@citation-js/core.plugins.input~predicate>}
   */
  parseTokenList () {
    let tokenList = this.data.tokenList

    if (!tokenList) {
      return []
    } else if (tokenList instanceof RegExp) {
      tokenList = { token: tokenList }
    }

    const { token, split = /\s+/, trim = true, every = true } = tokenList

    const trimInput = (input) => trim ? input.trim() : input
    const testTokens = every ? 'every' : 'some'

    const predicate = (input) =>
      trimInput(input).split(split)[testTokens](part => token.test(part))

    return [predicate]
  }

  /**
   * @access protected
   * @return {Array<module:@citation-js/core.plugins.input~predicate>}
   */
  parsePropertyConstraint () {
    const constraints = [].concat(this.data.propertyConstraint || [])

    return constraints.map(({ props, match, value }) => {
      props = [].concat(props)

      switch (match) {
        case 'any': // fall-through
        case 'some': return input => props.some(prop => prop in input && (!value || value(input[prop])))
        case 'none': return input => !props.some(prop => prop in input && (!value || value(input[prop])))
        case 'every': // fall-through
        default: return input => props.every(prop => prop in input && (!value || value(input[prop])))
      }
    })
  }

  /**
   * @access protected
   * @return {Array<module:@citation-js/core.plugins.input~predicate>}
   */
  parseElementConstraint () {
    const constraint = this.data.elementConstraint
    return !constraint ? [] : [input => input.every(entry => type(entry) === constraint)]
  }

  /**
   * @access protected
   * @return {Array<module:@citation-js/core.plugins.input~predicate>}
   */
  parsePredicate () {
    if (this.data.predicate instanceof RegExp) {
      return [this.data.predicate.test.bind(this.data.predicate)]
    } else if (this.data.predicate) {
      return [this.data.predicate]
    } else {
      return []
    }
  }

  /**
   * @access protected
   * @return {module:@citation-js/core.plugins.input~predicate}
   */
  getCombinedPredicate () {
    const predicates = [
      ...this.parsePredicate(),
      ...this.parseTokenList(),
      ...this.parsePropertyConstraint(),
      ...this.parseElementConstraint()
    ]

    if (predicates.length === 0) {
      return () => true
    } else if (predicates.length === 1) {
      return predicates[0]
    } else {
      return input => predicates.every(predicate => predicate(input))
    }
  }

  /**
   * @access protected
   * @return {module:@citation-js/core.plugins.input~dataType}
   */
  getDataType () {
    if (this.data.dataType) {
      return this.data.dataType
    } else if (this.data.predicate instanceof RegExp) {
      return 'String'
    } else if (this.data.tokenList) {
      return 'String'
    } else if (this.data.elementConstraint) {
      return 'Array'
    } else {
      return 'Primitive'
    }
  }

  // ==========================================================================
  // Data simplification
  // ==========================================================================

  /**
   * @type {module:@citation-js/core.plugins.input~dataType}
   */
  get dataType () {
    return this.getDataType()
  }

  /**
   * @type {module:@citation-js/core.plugins.input~predicate}
   */
  get predicate () {
    return this.getCombinedPredicate()
  }

  /**
   * @type {module:@citation-js/core.plugins.input~format}
   */
  get extends () {
    return this.data.extends
  }
}

/**
 * @memberof module:@citation-js/core.plugins.input.util
 * @param {module:@citation-js/core.plugins.input~dataParser|module:@citation-js/core.plugins.input~asyncDataParser} parser
 * @param {Object} options
 * @param {Boolean} [options.async=false]
 */
class DataParser {
  constructor (parser, { async } = {}) {
    this.parser = parser
    this.async = async
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  /**
   * @throws {TypeError} if dataParser is not valid
   */
  validate () {
    const parser = this.parser
    if (typeof parser !== 'function') {
      throw new TypeError(`parser was ${typeof parser}; expected function`)
    }
  }
}

/**
 * @memberof module:@citation-js/core.plugins.input.util
 * @param {module:@citation-js/core.plugins.input~format} format
 * @param {module:@citation-js/core.plugins.input~parsers} parsers
 */
class FormatParser {
  constructor (format, parsers = {}) {
    this.format = format

    if (parsers.parseType) {
      this.typeParser = new TypeParser(parsers.parseType)
    }
    if (parsers.parse) {
      this.dataParser = new DataParser(parsers.parse, { async: false })
    }
    if (parsers.parseAsync) {
      this.asyncDataParser = new DataParser(parsers.parseAsync, { async: true })
    }
  }

  // ==========================================================================
  // Validation
  // ==========================================================================

  /**
   * @access protected
   * @throws {TypeError} if format is not valid
   */
  validateFormat () {
    const format = this.format
    if (!typeMatcher.test(format)) {
      throw new TypeError(`format name was "${format}"; didn't match expected pattern`)
    }
  }

  /**
   * @throws {TypeError} if formatParser is not valid
   */
  validate () {
    this.validateFormat()
    if (this.typeParser) {
      this.typeParser.validate()
    }
    if (this.dataParser) {
      this.dataParser.validate()
    }
    if (this.asyncDataParser) {
      this.asyncDataParser.validate()
    }
  }
}

export { TypeParser, DataParser, FormatParser }