ts-fusion-parser
Version:
Parser for Neos Fusion Files
298 lines (297 loc) • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
const Comment_1 = require("../../common/Comment");
const ParserError_1 = require("../../common/ParserError");
const lexer_1 = require("../eel/lexer");
const parser_1 = require("../eel/parser");
const lexer_2 = require("./lexer");
const InlineEelNode_1 = require("./nodes/InlineEelNode");
const TagAttributeNode_1 = require("./nodes/TagAttributeNode");
const TagNameNode_1 = require("./nodes/TagNameNode");
const TagNode_1 = require("./nodes/TagNode");
const TagSpreadEelAttributeNode_1 = require("./nodes/TagSpreadEelAttributeNode");
const TextNode_1 = require("./nodes/TextNode");
const tokens_1 = require("./tokens");
class Parser {
constructor(lexer, positionOffset = 0, options) {
this.nodesByType = new Map();
this.options = {
allowUnclosedTags: false
};
this.lexer = lexer;
this.positionOffset = positionOffset;
if (options)
this.options = options;
}
applyOffset(position) {
if (position.begin !== -1)
position.begin += this.positionOffset;
if (position.end !== -1)
position.end += this.positionOffset;
return position;
}
parse() {
return [...this.parseTextsOrTags()];
}
*parseText(parent = undefined) {
const position = { begin: -1, end: -1 };
let currentText = '';
let currentTextPosition = { begin: this.lexer.getCursor(), end: -1 };
while (!this.lexer.isEOF() && (this.lexer.lookAhead(tokens_1.CharacterToken) || this.lexer.lookAhead(tokens_1.EscapedCharacterToken))) {
const charToken = this.lexer.consumeLookAhead();
if ((new tokens_1.AttributeEelBeginToken).regex.test(charToken.value)) {
if (currentText !== '') {
currentTextPosition.end = this.lexer.getCursor() - 1;
const textNode = new TextNode_1.TextNode(this.applyOffset(currentTextPosition), currentText, parent);
this.addNodeToNodesByType(textNode);
yield textNode;
currentTextPosition = { begin: this.lexer.getCursor(), end: -1 };
currentText = '';
}
const eelBegin = charToken;
const eelParser = this.buildEelParser();
const result = this.handover(eelParser);
const eelEnd = this.lexer.consume(tokens_1.AttributeEelEndToken);
const position = {
begin: eelBegin.position.begin,
end: eelEnd.position.end
};
const inlineEelNode = new InlineEelNode_1.InlineEelNode(this.applyOffset(position), result);
this.addNodeToNodesByType(inlineEelNode);
yield inlineEelNode;
}
else {
currentText += charToken.value;
}
if (position.begin === -1)
position.begin = charToken.position.begin;
position.end = charToken.position.end;
}
if (currentText !== '') {
currentTextPosition.end = this.lexer.getCursor() - 1;
const textNode = new TextNode_1.TextNode(this.applyOffset(currentTextPosition), currentText, parent);
this.addNodeToNodesByType(textNode);
yield textNode;
}
}
parseJavascript(parent = undefined) {
let text = "";
const position = { begin: -1, end: -1 };
while (!this.lexer.isEOF() && !this.lexer.lookAhead(tokens_1.ScriptEndToken) && this.lexer.lookAhead(tokens_1.AnyCharacterToken)) {
const charToken = this.lexer.consumeLookAhead();
text += charToken.value;
if (position.begin === -1)
position.begin = charToken.position.begin;
position.end = charToken.position.end;
}
const textNode = new TextNode_1.TextNode(this.applyOffset(position), text, parent);
this.addNodeToNodesByType(textNode);
return textNode;
}
*parseTextsOrTags(parent = undefined) {
while (!this.lexer.isEOF()) {
this.parseLazyWhitespace();
switch (true) {
case this.lexer.lookAhead(tokens_1.CharacterToken):
yield* this.parseText(parent);
break;
case this.lexer.lookAhead(tokens_1.TagBeginToken):
yield this.parseTag(parent);
break;
case this.lexer.lookAhead(tokens_1.CommentToken):
yield* this.parseComment();
break;
default:
return;
}
}
}
*parseComment() {
const commentValueRegex = /^<!--([.]*?)-->$/gm;
const token = this.lexer.consume(tokens_1.CommentToken);
const commentValueRegexResult = commentValueRegex.exec(token.value);
if (commentValueRegexResult) {
const comment = new Comment_1.Comment(commentValueRegexResult[1], token.value.includes("\n"), '<!--', this.applyOffset(token.position));
this.addNodeToNodesByType(comment);
yield comment;
}
}
parseTag(parent = undefined) {
const token = this.lexer.consume(tokens_1.TagBeginToken);
const nameNode = TagNameNode_1.TagNameNode.From(token);
this.lexer.tagStack.push(nameNode.toString());
const position = { ...token.position };
this.parseLazyWhitespace();
const attributes = [];
try {
while (!this.lexer.lookAhead(tokens_1.TagCloseToken) && !this.lexer.lookAhead(tokens_1.TagSelfCloseToken)) {
this.parseLazyWhitespace();
const isSpreadEelAttribute = this.lexer.lookAhead(tokens_1.AttributeEelBeginToken);
const attribute = isSpreadEelAttribute ? this.parseSpreadEelAttribute() : this.parseTagAttribute();
this.addNodeToNodesByType(attribute);
attributes.push(attribute);
this.parseLazyWhitespace();
}
}
catch (error) {
if (!this.options.allowUnclosedTags) {
if (error instanceof Error) {
throw new ParserError_1.ParserError(error.message, this.lexer.getCursor());
}
else
throw error;
}
const endToken = new tokens_1.TagSelfCloseToken();
endToken["value"] = ">";
endToken.position = {
begin: position.begin + 1,
end: position.end
};
position.end = endToken.position.end;
this.lexer.tagStack.pop();
const tagNode = new TagNode_1.TagNode(this.applyOffset(position), nameNode.toString(), nameNode, attributes, [], TagNameNode_1.TagNameNode.From(endToken), true, parent);
tagNode["artificiallyClosed"] = true;
this.addNodeToNodesByType(tagNode);
return tagNode;
}
const endToken = this.lexer.consumeLookAhead();
this.parseLazyWhitespace();
if (endToken instanceof tokens_1.TagSelfCloseToken) {
position.end = endToken.position.end;
this.lexer.tagStack.pop();
const tagNode = new TagNode_1.TagNode(this.applyOffset(position), nameNode.toString(), nameNode, attributes, [], TagNameNode_1.TagNameNode.From(endToken), true, parent);
this.addNodeToNodesByType(tagNode);
return tagNode;
}
const isScript = nameNode.toString() === "script";
const content = isScript ? [this.parseJavascript()] : [...this.parseTextsOrTags()];
this.parseLazyWhitespace();
if (this.lexer.isEOF()) {
const endToken = new tokens_1.TagSelfCloseToken();
endToken["value"] = ">";
endToken.position = {
begin: position.end + 1,
end: position.end + 2
};
position.end = endToken.position.end;
this.lexer.tagStack.pop();
const endNode = TagNameNode_1.TagNameNode.From(endToken);
const tagNode = new TagNode_1.TagNode(this.applyOffset(position), nameNode.toString(), nameNode, attributes, [], endNode, true, parent);
tagNode["artificiallyClosed"] = true;
this.addNodeToNodesByType(tagNode);
return tagNode;
}
const closingTagToken = this.lexer.consume(tokens_1.TagEndToken);
position.end = closingTagToken.position.end;
closingTagToken.position = this.applyOffset(closingTagToken.position);
this.lexer.tagStack.pop();
const tagNode = new TagNode_1.TagNode(this.applyOffset(position), nameNode.toString(), nameNode, attributes, content, TagNameNode_1.TagNameNode.From(closingTagToken), false, parent);
this.addNodeToNodesByType(tagNode);
return tagNode;
}
parseSpreadEelAttribute() {
const eelBegin = this.lexer.consumeLookAhead();
const eelParser = this.buildEelParser();
const result = this.handover(eelParser);
const eelEnd = this.lexer.consume(tokens_1.AttributeEelEndToken);
const position = {
begin: eelBegin.position.begin,
end: eelEnd.position.end
};
const eel = this.lexer.getSnippet(position.begin + 1, position.end - 1);
return new TagSpreadEelAttributeNode_1.TagSpreadEelAttributeNode(this.applyOffset(position), Array.isArray(result) ? result : [result], eel);
}
parseTagAttribute() {
this.parseLazyWhitespace();
// TODO: check if TagAttribute begins with `"`. If so, parse it as a string instead of using the token regex
const name = this.lexer.consume(tokens_1.AttributeNameToken);
const position = name.position;
let value;
if (this.lexer.lazyConsume(tokens_1.AttributeValueAssignToken)) {
switch (true) {
case this.lexer.lookAhead(tokens_1.WordToken):
case this.lexer.lookAhead(tokens_1.AttributeStringValueToken):
value = this.lexer.consumeLookAhead();
break;
case this.lexer.lookAhead(tokens_1.AttributeEelBeginToken): {
const eelBegin = this.lexer.consumeLookAhead();
const eelParser = this.buildEelParser();
const result = this.handover(eelParser);
// console.log("result", result)
const eelEnd = this.lexer.consume(tokens_1.AttributeEelEndToken);
value = {
value: result, position: {
begin: eelBegin.position.begin,
end: eelEnd.position.end
}
};
break;
}
}
}
if (value) {
position.end = value.position.end;
}
return new TagAttributeNode_1.TagAttributeNode(this.applyOffset(position), name.value, value ? value.value : null);
}
parseLazyWhitespace() {
this.lexer.lazyConsume(tokens_1.WhitespaceToken);
}
handover(parser, parent = undefined) {
const text = this.lexer.getRemainingText();
const result = parser.receiveHandover(text, this.lexer["cursor"] + this.positionOffset);
this.addNodesFromHandoverResult(result, parent);
this.lexer["cursor"] += result.cursor;
return Array.isArray(result.nodeOrNodes) ? result.nodeOrNodes : [result.nodeOrNodes];
}
addNodesFromHandoverResult(result, parent = undefined) {
var _a;
if (Array.isArray(result.nodeOrNodes)) {
for (const node of result.nodeOrNodes) {
node["parent"] = parent;
}
}
else {
result.nodeOrNodes["parent"] = parent;
}
for (const [type, nodes] of result.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);
}
}
receiveHandover(text) {
const currentLexer = this.lexer;
this.lexer = new lexer_2.Lexer(text);
const nodeOrNodes = this.parse();
const result = {
nodeOrNodes: nodeOrNodes,
cursor: this.lexer["cursor"],
nodesByType: this.nodesByType
};
this.lexer = currentLexer;
return result;
}
logRemaining(cap = undefined) {
console.log(">>::" + this.lexer.getRemainingText().substring(0, cap));
}
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;
}
buildEelParser() {
return new parser_1.Parser(new lexer_1.Lexer(""), undefined, this.options.eelParserOptions);
}
}
exports.Parser = Parser;