ts-fusion-parser
Version:
Parser for Neos Fusion Files
599 lines (598 loc) • 27 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObjectTreeParser = void 0;
const Comment_1 = require("../common/Comment");
const NodePosition_1 = require("../common/NodePosition");
const ParserError_1 = require("../common/ParserError");
const lexer_1 = require("../dsl/eel/lexer");
const parser_1 = require("../dsl/eel/parser");
const lexer_2 = require("./lexer");
const AssignedObjectPath_1 = require("./nodes/AssignedObjectPath");
const Block_1 = require("./nodes/Block");
const BoolValue_1 = require("./nodes/BoolValue");
const DslExpressionValue_1 = require("./nodes/DslExpressionValue");
const EelExpressionValue_1 = require("./nodes/EelExpressionValue");
const FloatValue_1 = require("./nodes/FloatValue");
const FusionFile_1 = require("./nodes/FusionFile");
const FusionObjectValue_1 = require("./nodes/FusionObjectValue");
const IncludeStatement_1 = require("./nodes/IncludeStatement");
const IncompletePathSegment_1 = require("./nodes/IncompletePathSegment");
const IntValue_1 = require("./nodes/IntValue");
const MetaPathSegment_1 = require("./nodes/MetaPathSegment");
const NullValue_1 = require("./nodes/NullValue");
const ObjectPath_1 = require("./nodes/ObjectPath");
const ObjectStatement_1 = require("./nodes/ObjectStatement");
const PathSegment_1 = require("./nodes/PathSegment");
const PropertyDocumentationDefinition_1 = require("./nodes/PropertyDocumentationDefinition");
const PrototypeDocumentationDefinition_1 = require("./nodes/PrototypeDocumentationDefinition");
const PrototypePathSegment_1 = require("./nodes/PrototypePathSegment");
const StatementList_1 = require("./nodes/StatementList");
const StringValue_1 = require("./nodes/StringValue");
const ValueAssignment_1 = require("./nodes/ValueAssignment");
const ValueCopy_1 = require("./nodes/ValueCopy");
const ValueUnset_1 = require("./nodes/ValueUnset");
const token_1 = require("./token");
const stripSlashes = (str) => str.replace(/\\./g, (match) => (new Function(`return "${match}"`))() || match);
const stripCSlashes = stripSlashes;
class ObjectTreeParser {
constructor(lexer, contextPathAndFilename, options) {
this.lexer = lexer;
this.contextPathAndFilename = contextPathAndFilename;
this.nodesByType = new Map();
this.ignoredErrors = [];
this.options = {
ignoreErrors: false,
allowIncompleteObjectStatements: false
};
this.lexer = lexer;
this.contextPathAndFilename = contextPathAndFilename;
if (options)
this.options = options;
}
static parse(sourceCode, contextPathAndFilename = undefined, options) {
const lexer = new lexer_2.Lexer(sourceCode);
const parser = new ObjectTreeParser(lexer, contextPathAndFilename, options);
return parser.parseFusionFile();
}
consume() {
return this.lexer.consumeLookahead();
}
/**
* Accepts a token of a given type.
* The Lexer will look up the regex for the token and try to match it on the current string.
* First match wins.
*
* @param {number} tokenType
* @return bool
*/
accept(tokenType, debug = false) {
const token = this.lexer.getCachedLookaheadOrTryToGenerateLookaheadForTokenAndGetLookahead(tokenType, debug);
if (token === null) {
return false;
}
return token.getType() === tokenType;
}
/**
* Expects a token of a given type.
* The Lexer will look up the regex for the token and try to match it on the current string.
* First match wins.
*
* @param {number} tokenType
* @return Token
* @throws ParserUnexpectedCharException
*/
expect(tokenType) {
const token = this.lexer.getCachedLookaheadOrTryToGenerateLookaheadForTokenAndGetLookahead(tokenType);
if (token === null || token.getType() !== tokenType) {
throw Error(`Expected token: '${token_1.Token.typeToString(tokenType)}'.`);
// // throw new ParserUnexpectedCharException("Expected token: 'tokenReadable'.", 1646988824);
}
return this.lexer.consumeLookahead();
}
/**
* Checks, if the token type matches the current, if so consume it and return true.
* @param {number} tokenType
* @return bool|null
*/
lazyExpect(tokenType) {
const token = this.lexer.getCachedLookaheadOrTryToGenerateLookaheadForTokenAndGetLookahead(tokenType);
if (token === null || token.getType() !== tokenType) {
return false;
}
this.lexer.consumeLookahead();
return true;
}
lazyBigGap() {
const comments = [];
while (true) {
switch (true) {
case this.accept(token_1.Token.SPACE):
case this.accept(token_1.Token.NEWLINE):
this.consume();
break;
case this.accept(token_1.Token.PROPERTY_DOCUMENTATION_DEFINITION):
return comments;
case this.accept(token_1.Token.PROTOTYPE_DOCUMENTATION_DEFINITION):
return comments;
case this.accept(token_1.Token.SLASH_COMMENT):
case this.accept(token_1.Token.HASH_COMMENT):
case this.accept(token_1.Token.MULTILINE_COMMENT):
comments.push(this.parseComment());
break;
default:
return comments;
}
}
}
lazySmallGap() {
while (true) {
switch (true) {
case this.accept(token_1.Token.SPACE):
this.consume();
break;
case this.accept(token_1.Token.PROPERTY_DOCUMENTATION_DEFINITION):
return;
case this.accept(token_1.Token.PROTOTYPE_DOCUMENTATION_DEFINITION):
return;
case this.accept(token_1.Token.SLASH_COMMENT):
case this.accept(token_1.Token.HASH_COMMENT):
return this.parseComment();
default:
return;
}
}
}
parsePropertyDocumentationDefinition(parent) {
const position = this.createPosition();
const rawContent = this.consume().getValue();
position.begin -= rawContent.length;
return new PropertyDocumentationDefinition_1.PropertyDocumentationDefinition(rawContent, this.endPosition(position), parent);
}
parsePrototypeDocumentationDefinition(parent) {
const position = this.createPosition();
const rawContent = this.consume().getValue();
position.begin -= rawContent.length;
return new PrototypeDocumentationDefinition_1.PrototypeDocumentationDefinition(rawContent, this.endPosition(position), parent);
}
parseComment() {
let type = undefined;
let prefix = undefined;
if (this.accept(token_1.Token.SLASH_COMMENT)) {
type = token_1.Token.SLASH_COMMENT;
prefix = "//";
}
else if (this.accept(token_1.Token.HASH_COMMENT)) {
type = token_1.Token.HASH_COMMENT;
prefix = "#";
}
else if (this.accept(token_1.Token.MULTILINE_COMMENT)) {
type = token_1.Token.MULTILINE_COMMENT;
prefix = "/*";
}
else
throw new Error('Expected Comment');
const position = this.createPosition();
const rawComment = this.consume().getValue();
position.begin -= rawComment.length;
const comment = new Comment_1.Comment(rawComment.replace(prefix, ""), type === token_1.Token.MULTILINE_COMMENT, prefix, this.endPosition(position));
this.addNodeToNodesByType(comment);
return comment;
}
parseFusionFile() {
// try {
const file = new FusionFile_1.FusionFile(this.parseStatementList(null, null, "file"), this.contextPathAndFilename);
file.nodesByType = this.flushNodesByType();
file.errors = this.ignoredErrors;
return file;
// } catch (e) {
// throw e;
// } catch (ParserUnexpectedCharException e) {
// throw this.prepareParserException(new ParserException())
// .setCode(e.getCode())
// .setMessageCreator (MessageLinePart nextLine) use (e) {
// return "Unexpected char {nextLine.charPrint()}. {e.getMessage()}";
// })
// .setPrevious(e)
// .build();
// } catch (Fusion\Exception e) {
// throw this.prepareParserException(new ParserException())
// .setCode(e.getCode())
// .setMessage('Exception while parsing: ' . e.getMessage())
// .setHideColumnInformation()
// .setPrevious(e)
// .build();
// }
}
/**
* StatementList
* = ( Statement )*
*
* @param ?int stopLookahead When this tokenType is encountered the loop will be stopped
*/
parseStatementList(parent, stopLookahead = null, debugName = "") {
const position = this.createPosition();
const statements = [];
const comments = [];
comments.push(...this.lazyBigGap());
while (this.accept(token_1.Token.EOF) === false && (stopLookahead === null || this.accept(stopLookahead) === false)) {
let statement;
try {
statement = this.parseStatement(null);
this.addNodeToNodesByType(statement);
statements.push(statement);
// TODO: save order of statements and comments => check again why this is needed => probably for printing/prettier
comments.push(...this.lazyBigGap());
}
catch (e) {
if (!this.options.ignoreErrors) {
throw e;
}
else {
this.ignoredErrors.push(e);
}
break;
}
}
// TODO: positioning of StatementList begins to late
const statementList = new StatementList_1.StatementList(statements, comments, this.endPosition(position), parent);
this.addNodeToNodesByType(statementList);
return statementList;
}
/**
* Statement
* = IncludeStatement / ObjectStatement
*/
parseStatement(parent) {
// watch out for the order, its regex matching and first one wins.
if (this.accept(token_1.Token.PROPERTY_DOCUMENTATION_DEFINITION)) {
return this.parsePropertyDocumentationDefinition(parent);
}
if (this.accept(token_1.Token.PROTOTYPE_DOCUMENTATION_DEFINITION)) {
return this.parsePrototypeDocumentationDefinition(parent);
}
if (this.accept(token_1.Token.INCLUDE))
return this.parseIncludeStatement(parent);
if (this.accept(token_1.Token.PROTOTYPE_START))
return this.parseObjectStatement(parent);
if (this.accept(token_1.Token.OBJECT_PATH_PART)) {
this.lexer.advanceCursor(-1 * this.lexer.consumeLookahead().getValue().length);
return this.parseObjectStatement(parent);
}
if (this.accept(token_1.Token.META_PATH_START)
|| this.accept(token_1.Token.STRING_SINGLE_QUOTED)
|| this.accept(token_1.Token.STRING_DOUBLE_QUOTED)) {
return this.parseObjectStatement(parent);
}
if (!this.options.ignoreErrors)
console.log("parseStatement");
if (!this.options.ignoreErrors)
this.lexer.debug();
throw new ParserError_1.ParserError("Error while parsing statement", this.lexer.getCursor());
}
/**
* IncludeStatement
* = INCLUDE ( STRING / CHAR / FILE_PATTERN ) EndOfStatement
*/
parseIncludeStatement(parent) {
const position = this.createPosition();
this.expect(token_1.Token.INCLUDE);
this.lazyExpect(token_1.Token.SPACE);
let filePattern;
switch (true) {
case this.accept(token_1.Token.STRING_DOUBLE_QUOTED):
case this.accept(token_1.Token.STRING_SINGLE_QUOTED):
const stringWrapped = this.consume().getValue();
filePattern = stringWrapped.substring(1, stringWrapped.length - 1);
break;
case this.accept(token_1.Token.FILE_PATTERN):
filePattern = this.consume().getValue();
break;
default:
throw new Error('Expected file pattern in quotes or [a-zA-Z0-9.*:/_-]');
// // throw new ParserUnexpectedCharException('Expected file pattern in quotes or [a-zA-Z0-9.*:/_-]', 1646988832);
}
this.parseEndOfStatement("parseIncludeStatement");
return new IncludeStatement_1.IncludeStatement(filePattern, this.endPosition(position), parent);
}
/**
* ObjectStatement
* = ObjectPath ( ValueAssignment / ValueUnset / ValueCopy )? ( Block / EndOfStatement )
*/
parseObjectStatement(parent) {
const position = this.createPosition();
const currentPath = this.parseObjectPath(null);
this.addNodeToNodesByType(currentPath);
this.lazyExpect(token_1.Token.SPACE);
const cursorAfterObjectPath = this.lexer.getCursor();
let operation = null;
switch (true) {
case this.accept(token_1.Token.ASSIGNMENT):
operation = this.parseValueAssignment(null);
break;
case this.accept(token_1.Token.UNSET):
operation = this.parseValueUnset(null);
break;
case this.accept(token_1.Token.COPY):
operation = this.parseValueCopy(null);
break;
}
if (operation !== null) {
this.addNodeToNodesByType(operation);
}
this.lazyExpect(token_1.Token.SPACE);
if (this.accept(token_1.Token.LBRACE)) {
const block = this.parseBlock(null);
return new ObjectStatement_1.ObjectStatement(currentPath, operation, block, cursorAfterObjectPath, this.endPosition(position), parent);
}
if (operation === null) {
if (this.options.ignoreErrors || this.options.allowIncompleteObjectStatements) {
const newPosition = {
begin: this.lexer.getCursor(),
end: this.lexer.getCursor()
};
// TODO: do not reuse "newPosition" => check why
operation = new ValueAssignment_1.ValueAssignment(new NullValue_1.NullValue(newPosition), newPosition, null);
}
else {
throw Error("operation is null");
}
}
if (!(operation instanceof ValueAssignment_1.ValueAssignment && operation.pathValue instanceof EelExpressionValue_1.EelExpressionValue)) {
this.parseEndOfStatement("parseObjectStatement");
}
return new ObjectStatement_1.ObjectStatement(currentPath, operation, undefined, cursorAfterObjectPath, this.endPosition(position), parent);
}
/**
* ObjectPath
* = PathSegment ( '.' PathSegment )*
*
*/
parseObjectPath(parent) {
const position = this.createPosition();
const segments = [];
do {
const segment = this.parsePathSegment();
this.addNodeToNodesByType(segment);
segments.push(segment);
} while (this.lazyExpect(token_1.Token.DOT));
position.end = segments[segments.length - 1].position.end;
return new ObjectPath_1.ObjectPath(position, parent, ...segments);
}
/**
* PathSegment
* = ( PROTOTYPE_START FUSION_OBJECT_NAME ')' / OBJECT_PATH_PART / '@' OBJECT_PATH_PART / STRING / CHAR )
*/
parsePathSegment() {
let position = this.createPosition();
switch (true) {
case this.accept(token_1.Token.PROTOTYPE_START):
this.consume();
position = this.createPosition();
let prototypeName;
try {
prototypeName = this.expect(token_1.Token.FUSION_OBJECT_NAME).getValue();
}
catch (error) {
throw error;
// // throw this.prepareParserException(new ParserException())
// // .setCode(1646991578)
// // .setMessageCreator([MessageCreator.class, 'forPathSegmentPrototypeName'])
// // .build();
}
position = this.endPosition(position);
this.expect(token_1.Token.RPAREN);
return new PrototypePathSegment_1.PrototypePathSegment(prototypeName, position);
case this.accept(token_1.Token.OBJECT_PATH_PART):
const pathKey = this.consume().getValue();
position = this.createPosition();
position.begin -= pathKey.length;
return new PathSegment_1.PathSegment(pathKey, this.endPosition(position));
case this.accept(token_1.Token.META_PATH_START):
const nodePosition = this.createPosition();
this.consume();
const metaPathSegmentKey = this.expect(token_1.Token.OBJECT_PATH_PART).getValue();
return new MetaPathSegment_1.MetaPathSegment(metaPathSegmentKey, this.endPosition(nodePosition));
case this.accept(token_1.Token.STRING_DOUBLE_QUOTED):
case this.accept(token_1.Token.STRING_SINGLE_QUOTED):
const stringWrapped = this.consume().getValue();
const quotedPathKey = stringWrapped.substring(1, stringWrapped.length - 1);
position.begin -= quotedPathKey.length;
return new PathSegment_1.PathSegment(quotedPathKey, this.endPosition(position));
case this.accept(token_1.Token.SPACE):
case this.accept(token_1.Token.NEWLINE):
if (this.options.ignoreErrors)
return new IncompletePathSegment_1.IncompletePathSegment("", this.endPosition(this.createPosition()));
}
throw Error("Could not parse segment");
// // throw this.prepareParserException(new ParserException())
// // .setCode(1635708755)
// // .setMessageCreator([MessageCreator.class, 'forParsePathSegment'])
// // .build();
}
parseValueAssignment(parent) {
const position = this.createPosition();
this.expect(token_1.Token.ASSIGNMENT);
this.lazyExpect(token_1.Token.SPACE);
const value = this.parsePathValue();
this.addNodeToNodesByType(value);
return new ValueAssignment_1.ValueAssignment(value, this.endPosition(position), parent);
}
/**
* PathValue
* = ( CHAR / STRING / DSL_EXPRESSION / FusionObject / EelExpression )
*/
parsePathValue() {
var _a;
// watch out for the order, its regex matching and first one wins.
// sorted by likelyhood
const position = this.createPosition();
let stringContent;
switch (true) {
case this.accept(token_1.Token.STRING_SINGLE_QUOTED):
const charWrapped = this.consume().getValue();
stringContent = charWrapped.substring(1, charWrapped.length - 1);
return new StringValue_1.StringValue(stripSlashes(stringContent), this.endPosition(position));
case this.accept(token_1.Token.STRING_DOUBLE_QUOTED):
const stringWrapped = this.consume().getValue();
stringContent = stringWrapped.substring(1, stringWrapped.length - 1);
return new StringValue_1.StringValue(stripCSlashes(stringContent), this.endPosition(position));
case this.accept(token_1.Token.DSL_EXPRESSION_START):
return this.parseDslExpression();
case this.accept(token_1.Token.FLOAT):
return new FloatValue_1.FloatValue(parseFloat(this.consume().getValue()), this.endPosition(position));
case this.accept(token_1.Token.INTEGER):
return new IntValue_1.IntValue(parseInt(this.consume().getValue()), this.endPosition(position));
case this.accept(token_1.Token.TRUE_VALUE):
this.consume();
return new BoolValue_1.BoolValue(true, this.endPosition(position));
case this.accept(token_1.Token.FALSE_VALUE):
this.consume();
return new BoolValue_1.BoolValue(false, this.endPosition(position));
case this.accept(token_1.Token.NULL_VALUE):
this.consume();
return new NullValue_1.NullValue(this.endPosition(position));
case this.accept(token_1.Token.FUSION_OBJECT_NAME):
const nodePosition = this.createPosition();
const value = this.consume().getValue();
nodePosition.begin -= value.length;
return new FusionObjectValue_1.FusionObjectValue(value, this.endPosition(nodePosition));
case this.accept(token_1.Token.EEL_EXPRESSION_START):
this.consume();
const eelPosition = this.createPosition();
const eelLexer = new lexer_1.Lexer(this.lexer.getRemainingCode());
const eelParser = new parser_1.Parser(eelLexer, this.lexer.getCursor(), this.options.eelParserOptions);
const eelNode = eelParser.parse();
for (const [type, nodes] of eelParser.nodesByType.entries()) {
const list = (_a = this.nodesByType.get(type)) !== null && _a !== void 0 ? _a : [];
for (const node of nodes) {
list.push(node);
}
this.nodesByType.set(type, list);
}
this.lexer.advanceCursor(eelLexer.getCursor() + 1);
const code = this.lexer.getCode().slice(eelNode.position.begin, eelNode.position.end);
return new EelExpressionValue_1.EelExpressionValue(code, this.endPosition(eelPosition, -1), eelNode);
}
if (!this.options.ignoreErrors)
console.log("parsePathValue");
if (!this.options.ignoreErrors)
this.lexer.debug();
throw new ParserError_1.ParserError("Could not parse value", this.lexer.getCursor());
// // throw this.prepareParserException(new ParserException())
// // .setCode(1646988841)
// // .setMessageCreator([MessageCreator.class, 'forParsePathValue'])
// // .build();
}
parseDslExpression() {
var _a;
const dslIdentifier = this.expect(token_1.Token.DSL_EXPRESSION_START).getValue();
const position = this.createPosition();
position.begin -= dslIdentifier.length;
let dslCode = '';
try {
dslCode = this.expect(token_1.Token.DSL_EXPRESSION_CONTENT).getValue();
}
catch (error) {
if (this.options.ignoreErrors)
this.ignoredErrors.push(error);
else
throw error;
}
dslCode = dslCode.substring(1, dslCode.length - 1);
const dslExpression = new DslExpressionValue_1.DslExpressionValue(dslIdentifier, dslCode, this.endPosition(position), this.options.afxParserOptions);
const nodesByType = dslExpression.parse();
for (const [type, nodes] of nodesByType.entries()) {
const list = (_a = this.nodesByType.get(type)) !== null && _a !== void 0 ? _a : [];
for (const node of nodes) {
list.push(node);
}
this.nodesByType.set(type, list);
}
return dslExpression;
}
parseValueUnset(parent) {
const position = this.createPosition();
this.expect(token_1.Token.UNSET);
return new ValueUnset_1.ValueUnset(this.endPosition(position), parent);
}
parseValueCopy(parent) {
const position = this.createPosition();
this.expect(token_1.Token.COPY);
this.lazyExpect(token_1.Token.SPACE);
const sourcePath = this.parseAssignedObjectPath(null);
return new ValueCopy_1.ValueCopy(sourcePath, this.endPosition(position), parent);
}
parseAssignedObjectPath(parent) {
const position = this.createPosition();
const isRelative = this.lazyExpect(token_1.Token.DOT);
return new AssignedObjectPath_1.AssignedObjectPath(this.endPosition(position), parent, this.parseObjectPath(null), Boolean(isRelative));
}
/**
* Block:
* = '{' StatementList? '}'
*/
parseBlock(parent, debugName = "") {
this.expect(token_1.Token.LBRACE);
const blockPosition = this.createPosition();
blockPosition.begin -= 1;
this.parseEndOfStatement("parseBlock");
const statementList = this.parseStatementList(null, token_1.Token.RBRACE, debugName);
try {
this.expect(token_1.Token.RBRACE);
}
catch (error) {
if (this.options.ignoreErrors) {
this.ignoredErrors.push(error);
}
else {
throw error;
}
}
const block = new Block_1.Block(parent, statementList, this.endPosition(blockPosition));
this.addNodeToNodesByType(block);
return block;
}
/**
* EndOfStatement
* = ( EOF / NEWLINE )
*/
parseEndOfStatement(debugFrom = '') {
this.lazySmallGap();
if (this.accept(token_1.Token.EOF)) {
return;
}
if (this.accept(token_1.Token.NEWLINE)) {
this.consume();
return;
}
if (!this.options.ignoreErrors)
console.log("parseEndOfStatement");
if (!this.options.ignoreErrors)
this.lexer.debug();
throw Error("parsed EOF from: " + debugFrom);
// // throw this.prepareParserException(new ParserException())
// // .setCode(1635878683)
// // .setMessageCreator([MessageCreator.class, 'forParseEndOfStatement'])
// // .build();
}
createPosition() {
return new NodePosition_1.NodePosition(this.lexer.getCursor());
}
endPosition(position, offset = 0) {
position.end = this.lexer.getCursor() + offset;
return position;
}
addNodeToNodesByType(node) {
var _a;
const type = node.constructor;
const list = (_a = this.nodesByType.get(type)) !== null && _a !== void 0 ? _a : [];
list.push(node);
this.nodesByType.set(type, list);
}
flushNodesByType() {
const map = new Map(this.nodesByType);
this.nodesByType.clear();
return map;
}
}
exports.ObjectTreeParser = ObjectTreeParser;