/**
* @namespace kdl
* @memberof module:kdljs.parser
*/
import { Lexer, MismatchedTokenException, createTokenInstance } from 'chevrotain'
import { BaseParser } from './base.js'
import * as Tokens from './tokens.js'
const tokens = {
defaultMode: 'main',
modes: {
main: [
Tokens.WhiteSpace,
Tokens.BOM,
Tokens.NewLine,
Tokens.BlockComment,
Tokens.LineComment,
Tokens.OpenMultiLineComment,
Tokens.Boolean,
Tokens.Null,
Tokens.FloatKeyword,
Tokens.MultiLineRawString,
Tokens.RawString,
Tokens.Integer,
Tokens.Float,
Tokens.SemiColon,
Tokens.Equals,
Tokens.LeftBrace,
Tokens.RightBrace,
Tokens.LeftParenthesis,
Tokens.RightParenthesis,
Tokens.EscLine,
Tokens.MultiLineOpenQuote,
Tokens.OpenQuote,
Tokens.Identifier
],
multilineComment: [
Tokens.OpenMultiLineComment,
Tokens.CloseMultiLineComment,
Tokens.MultiLineCommentContent
],
string: [
Tokens.Unicode,
Tokens.Escape,
Tokens.UnicodeEscape,
Tokens.WhiteSpaceEscape,
Tokens.CloseQuote
],
multilineString: [
Tokens.MultiLineCloseQuote,
Tokens.MultiLineSingleQuote,
Tokens.NewLine,
Tokens.WhiteSpace,
Tokens.Unicode,
Tokens.Escape,
Tokens.UnicodeEscape,
Tokens.WhiteSpaceEscape
]
}
}
const nodeEndTokens = new Set([
Tokens.RightBrace,
Tokens.LineComment,
Tokens.NewLine,
Tokens.SemiColon,
Tokens.EOF
])
/**
* @class
* @extends module:kdljs.parser.base.BaseParser
* @memberof module:kdljs.parser.kdl
*/
class KdlParser extends BaseParser {
constructor () {
super(tokens)
/**
* Consume a KDL document
* @method #document
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {module:kdljs~Document}
*/
this.RULE('document', () => {
this.OPTION(() => this.CONSUME(Tokens.BOM))
const nodes = this.SUBRULE(this.nodes)
this.CONSUME(Tokens.EOF)
return nodes
})
/**
* Consume a sequence of KDL nodes
* @method #nodes
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {module:kdljs~Document}
*/
this.RULE('nodes', () => {
const nodes = []
this.MANY(() => this.OR([
{
ALT: () => {
this.CONSUME(Tokens.BlockComment)
this.OPTION1(() => this.SUBRULE(this.lineSpace))
this.SUBRULE(this.node)
}
},
{ ALT: () => this.SUBRULE1(this.lineSpace) },
{ ALT: () => nodes.push(this.SUBRULE1(this.node)) }
]))
return nodes
})
/**
* Consume a KDL node
* @method #node
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {module:kdljs~Node}
*/
this.RULE('node', () => {
const node = {
name: undefined,
properties: {},
values: [],
children: [],
tags: {
name: undefined,
properties: {},
values: []
}
}
this.OPTION(() => {
node.tags.name = this.SUBRULE(this.tag)
this.OPTION1(() => this.SUBRULE(this.nodeSpace))
})
node.name = this.SUBRULE(this.string)
let entriesEnded = false
let childrenEnded = false
this.MANY({
GATE: () => !nodeEndTokens.has(this.LA(1).tokenType),
DEF: () => {
this.SUBRULE1(this.nodeSpace)
this.OR([
{
GATE: () => !entriesEnded && this.BACKTRACK(this.property).call(this),
ALT: () => {
const parts = this.SUBRULE(this.property)
node.properties[parts[0]] = parts[1]
node.tags.properties[parts[0]] = parts[2]
}
},
{
GATE: () => !entriesEnded && this.BACKTRACK(this.argument).call(this),
ALT: () => {
const parts = this.SUBRULE(this.argument)
node.values.push(parts[0])
node.tags.values.push(parts[1])
}
},
{
GATE: () => !childrenEnded,
ALT: () => {
node.children = this.SUBRULE(this.nodeChildren)
entriesEnded = true
childrenEnded = true
}
},
{
ALT: () => {
this.CONSUME(Tokens.BlockComment)
this.OPTION2(() => this.SUBRULE(this.lineSpace))
this.OR1([
{
GATE: () => !entriesEnded && this.BACKTRACK(this.property).call(this),
ALT: () => this.SUBRULE1(this.property)
},
{
GATE: () => !entriesEnded && this.BACKTRACK(this.argument).call(this),
ALT: () => this.SUBRULE1(this.argument)
},
{
ALT: () => {
this.SUBRULE1(this.nodeChildren)
entriesEnded = true
}
}
])
}
},
{
GATE: () => nodeEndTokens.has(this.LA(1).tokenType),
ALT: () => {}
}
])
}
})
if (this.LA(1).tokenType !== Tokens.RightBrace) {
this.SUBRULE(this.nodeTerminator)
}
return node
})
/**
* Consume a property
* @method #property
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {Array<module:kdljs~Value>} key-value-type tuple
*/
this.RULE('property', () => {
const key = this.SUBRULE(this.string)
this.OPTION(() => this.SUBRULE(this.nodeSpace))
this.CONSUME(Tokens.Equals)
this.OPTION1(() => this.SUBRULE1(this.nodeSpace))
const parts = this.SUBRULE(this.argument)
return [key, parts[0], parts[1]]
})
/**
* Consume an argument
* @method #argument
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {Array<module:kdljs~Value>} value-type tuple
*/
this.RULE('argument', () => {
const tag = this.OPTION(() => {
const tag = this.SUBRULE(this.tag)
this.OPTION1(() => this.SUBRULE(this.nodeSpace))
return tag
})
const value = this.SUBRULE(this.value)
return [value, tag]
})
/**
* Consume node children
* @method #nodeChildren
* @memberof module:kdljs.parser.kdl.KdlParser
* @return {module:kdljs~Document}
*/
this.RULE('nodeChildren', () => {
this.CONSUME(Tokens.LeftBrace)
const nodes = this.SUBRULE(this.nodes)
this.CONSUME(Tokens.RightBrace)
return nodes
})
/**
* Consume line space
* @method #lineSpace
* @memberof module:kdljs.parser.kdl.KdlParser
*/
this.RULE('lineSpace', () => {
this.AT_LEAST_ONE(() => this.OR([
{ ALT: () => this.SUBRULE(this.nodeSpace) },
{ ALT: () => this.CONSUME(Tokens.NewLine) },
{ ALT: () => this.SUBRULE(this.lineComment) }
]))
})
/**
* Consume a node terminator
* @method #nodeTerminator
* @memberof module:kdljs.parser.kdl.KdlParser
*/
this.RULE('nodeTerminator', () => {
this.OR([
{ ALT: () => this.SUBRULE(this.lineComment) },
{ ALT: () => this.CONSUME(Tokens.NewLine) },
{ ALT: () => this.CONSUME(Tokens.SemiColon) },
{ ALT: () => this.CONSUME(Tokens.EOF) }
])
})
this.performSelfAnalysis()
}
}
/**
* @access private
* @memberof module:kdljs.parser.kdl
* @param {Object} error
* @param {string} error.message
* @param {number} error.offset
* @param {number} error.length
* @param {number} error.line
* @param {number} error.column
* @param {Object[]} tokens
* @param {string} text
* @return {Object}
*/
function transformLexerError (error, tokens, text) {
const endOffset = error.offset + error.length
const image = text.slice(error.offset, endOffset)
const lines = image.split(/\r?\n/g)
const prevToken = tokens.find(token => token.endOffset + 1 === error.offset)
return new MismatchedTokenException(
error.message,
createTokenInstance(
Tokens.Unknown,
image,
error.offset,
endOffset,
error.line,
error.line + lines.length - 1,
error.column,
error.column + lines[lines.length - 1].length
),
prevToken
)
}
const lexer = new Lexer(tokens)
/**
* @constant {module:kdljs.parser.kdl.KdlParser}
* @memberof module:kdljs.parser.kdl
*/
const parser = new KdlParser()
/**
* @typedef ParseResult
* @memberof module:kdljs.parser.kdl
* @type {Object}
* @property {Array} errors - Parsing errors
* @property {module:kdljs~Document} output - KDL Document
*/
/**
* @function parse
* @memberof module:kdljs.parser.kdl
* @param {string} text - Input KDL file (or fragment)
* @return {module:kdljs.parser.kdl.ParseResult} Output
*/
export function parse (text) {
const { tokens, errors } = lexer.tokenize(text)
if (errors.length) {
return {
output: undefined,
errors: errors.map(error => transformLexerError(error, tokens, text))
}
}
parser.input = tokens
const output = parser.document()
return {
output,
errors: parser.errors
}
}
export {
lexer,
parser,
KdlParser
}