Source: core/src/util/grammar.js

import { deepCopy } from './deepCopy.js'

/**
 * @typedef module:@citation-js/core.util.Grammar~ruleName
 * @type {String}
 */

/**
 * @callback module:@citation-js/core.util.Grammar~rule
 * @this module:@citation-js/core.util.Grammar
 */

/**
 * @memberof module:@citation-js/core.util
 *
 * @param {Object<module:@citation-js/core.util.Grammar~ruleName,module:@citation-js/core.util.Grammar~rule>} rules
 * @param {Object} state
 */
class Grammar {
  constructor (rules, state) {
    this.rules = rules
    this.defaultState = state
    this.mainRule = Object.keys(rules)[0]
    this.log = []
  }

  /**
   * @param iterator - lexer supporting formatError() and next()
   * @param [mainRule] - defaults to the first rule
   * @return result of the main rule
   */
  parse (iterator, mainRule) {
    this.lexer = iterator
    this.token = this.lexer.next()
    this.state = deepCopy(this.defaultState)
    this.log = []
    return this.consumeRule(mainRule || this.mainRule)
  }

  /**
   * @return {Boolean} true if there are no more tokens
   */
  matchEndOfFile () {
    return !this.token
  }

  /**
   * @param {String} type - a token type
   * @return {Boolean} true if the current token has the given type
   */
  matchToken (type) {
    return this.token && type === this.token.type
  }

  /**
   * @param {String} [type] - a token type
   * @param {Boolean} [optional=false] - false if it should throw an error if the type does not match
   * @return {Object} token information
   * @throws {SyntaxError} detailed syntax error if the current token is not the expected type or if there are no tokens left
   */
  consumeToken (type, optional) {
    const token = this.token

    if (!type || (token && token.type === type)) {
      this.token = this.lexer.next()
      return token
    } else if (optional) {
      return undefined
    } else {
      const got = token ? `"${token.type}"` : 'EOF'
      const error = new SyntaxError(this.lexer.formatError(token, `expected "${type}", got ${got}`))
      error.message += ` (${this.log.join('->')})`
      throw error
    }
  }

  /**
   * @param {String} rule - a rule name
   * @return whatever the rule function returns
   */
  consumeRule (rule) {
    this.log.push(rule)
    const result = this.rules[rule].call(this)
    this.log.pop()
    return result
  }
}

export { Grammar }