Source: parser.js

/** @module kdljs/parser */

const {
  createToken,
  Lexer,
  EmbeddedActionsParser,
  EOF
} = require('chevrotain')

// Whitespace and comments
const WhiteSpace = createToken({
  name: 'WhiteSpace',
  // eslint-disable-next-line no-control-regex
  pattern: /[\x09\x20\xA0\u1680\u2000-\u200A\u202F\u205F\u3000]+/
})
const NewLine = createToken({
  name: 'NewLine',
  // eslint-disable-next-line no-control-regex
  pattern: /\x0D\x0A|[\x0A\x0C\x85\u2028\u2029]/
})
const BlockComment = createToken({ name: 'BlockComment', pattern: /\/-/ })
const LineComment = createToken({
  name: 'LineComment',
  // eslint-disable-next-line no-control-regex
  pattern: /\/\/[^]*?(\x0D\x0A|[\x0A\x0C\x85\u2028\u2029])/,
  line_breaks: true
})
const MultiLineComment = createToken({
  name: 'MultiLineComment',
  pattern: /\/\*[^]*?\*\//,
  line_breaks: true
})

// Values
const Boolean = createToken({ name: 'Boolean', pattern: /true|false/ })
const Null = createToken({ name: 'Null', pattern: /null/ })
const RawString = createToken({
  name: 'RawString',
  pattern: /r(#*)"[^]*?"\1/,
  line_breaks: true
})
const Float = createToken({
  name: 'Float',
  pattern: /[+-]?[0-9][0-9_]*(\.[0-9]+)?([eE][+-]?[0-9][0-9_]*)?/
})
const Integer = createToken({
  name: 'Integer',
  pattern: /0x[0-9a-fA-F][0-9a-fA-F_]*|0o[0-7][0-7_]*|0b[01][01_]*/
})

// Other
const Identifier = createToken({
  name: 'Identifier',
  pattern: /[\x21-\x2F\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF][\x21-\x3A\x3F-\x5A\x5E-\x7A\x7C\x7E-\uFFFF]*/
})
const SemiColon = createToken({ name: 'SemiColon', pattern: /;/ })
const Equals = createToken({ name: 'Equals', pattern: /=/ })
const LeftBrace = createToken({ name: 'LeftBrace', pattern: /\{/ })
const RightBrace = createToken({ name: 'RightBrace', pattern: /\}/ })
const EscLine = createToken({ name: 'EscLine', pattern: /\\/ })

// String
const OpenQuote = createToken({ name: 'OpenQuote', pattern: /"/, push_mode: 'string' })
const Unicode = createToken({
  name: 'Unicode',
  pattern: /[^\\"]+/,
  line_breaks: true
})
const Escape = createToken({ name: 'Escape', pattern: /\\[nrt\\"bf]/ })
const UnicodeEscape = createToken({ name: 'UnicodeEscape', pattern: /\\u\{[0-9a-fA-F]{0,6}\}/ })
const CloseQuote = createToken({ name: 'CloseQuote', pattern: /"/, pop_mode: true })

const tokens = {
  defaultMode: 'main',
  modes: {
    main: [
      WhiteSpace,
      NewLine,
      BlockComment,
      LineComment,
      MultiLineComment,
      Boolean,
      Null,
      RawString,
      Integer,
      Float,
      SemiColon,
      Equals,
      LeftBrace,
      RightBrace,
      EscLine,
      OpenQuote,
      Identifier
    ],
    string: [
      Unicode,
      Escape,
      UnicodeEscape,
      CloseQuote
    ]
  }
}

const escapes = {
  '\\n': '\n',
  '\\r': '\r',
  '\\t': '\t',
  '\\\\': '\\',
  '\\"': '"',
  '\\b': '\x08',
  '\\f': '\x0C'
}

const radix = {
  b: 2,
  o: 8,
  x: 16
}

/**
 * @class
 */
class KdlParser extends EmbeddedActionsParser {
  constructor () {
    super(tokens)

    this.RULE('nodes', () => {
      const nodes = []

      this.MANY(() => this.OR([
        {
          ALT: () => {
            this.CONSUME(BlockComment)
            this.OPTION1(() => this.CONSUME1(WhiteSpace))
            this.SUBRULE(this.node)
          }
        },
        { ALT: () => this.CONSUME(LineComment) },
        { ALT: () => this.CONSUME(MultiLineComment) },
        { ALT: () => this.CONSUME(WhiteSpace) },
        { ALT: () => this.CONSUME(NewLine) },
        { ALT: () => nodes.push(this.SUBRULE1(this.node)) }
      ]))

      return nodes
    })

    this.RULE('node', () => {
      const name = this.OR([
        { ALT: () => this.CONSUME(Identifier).image },
        { ALT: () => this.SUBRULE(this.string) }
      ])

      const properties = {}
      const values = []
      this.MANY(() => {
        this.SUBRULE(this.nodeSpace)
        this.OR1([
          {
            GATE: this.BACKTRACK(this.property),
            ALT: () => {
              const pair = this.SUBRULE(this.property)
              properties[pair[0]] = pair[1]
            }
          },
          {
            GATE: this.BACKTRACK(this.value),
            ALT: () => values.push(this.SUBRULE(this.value))
          }
        ])
      })

      this.SUBRULE1(this.nodeSpace)
      const children = this.OR2([
        {
          ALT: () => {
            const children = this.SUBRULE(this.nodeChildren)
            this.OPTION(() => this.SUBRULE(this.nodeTerminator))
            return children
          }
        },
        {
          ALT: () => {
            this.SUBRULE1(this.nodeTerminator)
            return []
          }
        }
      ])

      return { name, properties, values, children }
    })

    this.RULE('property', () => {
      const key = this.OR([
        { ALT: () => this.CONSUME(Identifier).image },
        { ALT: () => this.SUBRULE(this.string) }
      ])
      this.CONSUME(Equals)
      const value = this.SUBRULE(this.value)
      return [key, value]
    })

    this.RULE('nodeChildren', () => {
      this.CONSUME(LeftBrace)
      const nodes = this.SUBRULE(this.nodes)
      this.CONSUME(RightBrace)
      return nodes
    })

    this.RULE('nodeSpace', () => {
      this.MANY(() => this.OR([
        { ALT: () => this.CONSUME(WhiteSpace) },
        {
          ALT: () => {
            this.CONSUME(EscLine)
            this.OPTION(() => this.CONSUME1(WhiteSpace))
            this.OR1([
              { ALT: () => this.CONSUME(LineComment) },
              { ALT: () => this.CONSUME(NewLine) }
            ])
          }
        },
        {
          ALT: () => {
            this.CONSUME(BlockComment)
            this.OPTION1(() => this.CONSUME2(WhiteSpace))
            this.OR2([
              {
                GATE: this.BACKTRACK(this.property),
                ALT: () => this.SUBRULE(this.property)
              },
              {
                GATE: this.BACKTRACK(this.value),
                ALT: () => this.SUBRULE(this.value)
              },
              { ALT: () => this.SUBRULE(this.nodeChildren) }
            ])
          }
        }
      ]))
    })

    this.RULE('nodeTerminator', () => {
      this.OR([
        { ALT: () => this.CONSUME(NewLine) },
        { ALT: () => this.CONSUME(SemiColon) },
        { ALT: () => this.CONSUME(EOF) }
      ])
    })

    this.RULE('value', () => this.OR([
      { ALT: () => this.SUBRULE(this.string) },
      { ALT: () => this.CONSUME(Boolean).image === 'true' },
      {
        ALT: () => {
          this.CONSUME(Null)
          return null
        }
      },
      {
        ALT: () => {
          const number = this.CONSUME(Float).image.replace(/_/g, '')
          return parseFloat(number, 10)
        }
      },
      {
        ALT: () => {
          const number = this.CONSUME(Integer).image.slice(1).replace(/_/g, '')
          return parseInt(number.slice(1), radix[number[0]])
        }
      },
      {
        ALT: () => {
          const string = this.CONSUME(RawString).image
          const start = string.indexOf('"')
          return string.slice(start + 1, -start)
        }
      }
    ]))

    this.RULE('string', () => {
      let string = ''

      this.CONSUME(OpenQuote)
      this.MANY(() => {
        string += this.OR([
          { ALT: () => this.CONSUME(Unicode).image },
          { ALT: () => escapes[this.CONSUME(Escape).image] },
          {
            ALT: () => {
              const escape = this.CONSUME(UnicodeEscape).image.slice(3, -1)
              return String.fromCharCode(parseInt(escape, 16))
            }
          }
        ])
      })
      this.CONSUME(CloseQuote)

      return string
    })

    this.performSelfAnalysis()
  }

  /**
   * @method
   * @param {string} input - Input KDL file (or fragment)
   * @param {Object} error
   * @param {string} [message] - Override the error message
   * @param {Object} [options] - Further configuration
   * @param {number} [options.context=3] - How many lines before the problematic line to include
   */
  formatError (input, error, message = error.message, { context = 3 } = {}) {
    let output = message + '\n'

    const { startLine, endLine, startColumn, endColumn } = error.token
    const lines = input.split('\n')
    output += lines.slice(startLine - Math.min(startLine, context), endLine).join('\n')
    output += '\n'

    output += ' '.repeat(startColumn - 1) + '^'.repeat(endColumn - startColumn + 1)
    output += ` at ${startLine}:${startColumn}\n`

    output += error.context.ruleStack
      .map(rule => '  ' + rule)
      .join('\n')

    return output
  }
}

const lexer = new Lexer(tokens)
/**
 * @constant {module:kdljs/parser~KdlParser}
 */
const parser = new KdlParser()

/**
 * @typedef parseResult
 * @type {Object}
 * @property {Array} errors - Parsing errors
 * @property {module:kdljs~Document} output - KDL Document
 */

/**
 * @function parse
 * @param {string} text - Input KDL file (or fragment)
 * @return {module:kdljs/parser~parseResult} Output
 */
module.exports.parse = function parse (text) {
  parser.input = lexer.tokenize(text).tokens
  const output = parser.nodes()

  return {
    output,
    errors: parser.errors
  }
}

module.exports.lexer = lexer
module.exports.parser = parser
module.exports.KdlParser = KdlParser