twing
Version:
First-class Twig engine for Node.js
1,150 lines • 50.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createParser = void 0;
const parsing_1 = require("./error/parsing");
const node_1 = require("./node");
const text_1 = require("./node/text");
const print_1 = require("./node/print");
const template_1 = require("./node/template");
const node_traverser_1 = require("./node-traverser");
const comment_1 = require("./node/comment");
const is_made_of_whitespace_only_1 = require("./helpers/is-made-of-whitespace-only");
const constant_1 = require("./node/expression/constant");
const concatenate_1 = require("./node/expression/binary/concatenate");
const assignment_1 = require("./node/expression/assignment");
const arrow_function_1 = require("./node/expression/arrow-function");
const name_1 = require("./node/expression/name");
const parent_function_1 = require("./node/expression/parent-function");
const block_function_1 = require("./node/expression/block-function");
const attribute_accessor_1 = require("./node/expression/attribute-accessor");
const array_1 = require("./node/expression/array");
const method_call_1 = require("./node/expression/method-call");
const hash_1 = require("./node/expression/hash");
const not_1 = require("./node/expression/unary/not");
const conditional_1 = require("./node/expression/conditional");
const twig_lexer_1 = require("twig-lexer");
const lexer_1 = require("./lexer");
const function_1 = require("./node/expression/call/function");
const filter_1 = require("./node/expression/call/filter");
const record_1 = require("./helpers/record");
const get_function_1 = require("./helpers/get-function");
const get_filter_1 = require("./helpers/get-filter");
const get_test_1 = require("./helpers/get-test");
const core_1 = require("./node-visitor/core");
const sandbox_1 = require("./node-visitor/sandbox");
const test_1 = require("./node/expression/call/test");
const escaper_1 = require("./node-visitor/escaper");
const apply_1 = require("./tag-handler/apply");
const auto_escape_1 = require("./tag-handler/auto-escape");
const block_1 = require("./tag-handler/block");
const deprecated_1 = require("./tag-handler/deprecated");
const do_1 = require("./tag-handler/do");
const embed_1 = require("./tag-handler/embed");
const extends_1 = require("./tag-handler/extends");
const filter_2 = require("./tag-handler/filter");
const flush_1 = require("./tag-handler/flush");
const for_1 = require("./tag-handler/for");
const from_1 = require("./tag-handler/from");
const if_1 = require("./tag-handler/if");
const import_1 = require("./tag-handler/import");
const include_1 = require("./tag-handler/include");
const line_1 = require("./tag-handler/line");
const macro_1 = require("./tag-handler/macro");
const sandbox_2 = require("./tag-handler/sandbox");
const set_1 = require("./tag-handler/set");
const spaceless_1 = require("./tag-handler/spaceless");
const use_1 = require("./tag-handler/use");
const verbatim_1 = require("./tag-handler/verbatim");
const with_1 = require("./tag-handler/with");
const spread_1 = require("./node/expression/spread");
const get_key_value_pairs_1 = require("./helpers/get-key-value-pairs");
const nameRegExp = new RegExp(twig_lexer_1.namePattern);
const getNames = (map) => {
return [...map.values()].map(({ name }) => name);
};
const createParser = (unaryOperators, binaryOperators, additionalTagHandlers, visitors, filters, functions, tests, options) => {
const strict = (options === null || options === void 0 ? void 0 : options.strict) !== undefined ? options.strict : true;
const level = (options === null || options === void 0 ? void 0 : options.level) || 3;
// operators
const binaryOperatorsRegister = new Map(binaryOperators
.filter((operator) => operator.specificationLevel <= level)
.map((operator) => [operator.name, operator]));
const unaryOperatorsRegister = new Map(unaryOperators
.map((operator) => [operator.name, operator]));
// tag handlers
const tagHandlers = [
(0, apply_1.createApplyTagHandler)(),
(0, auto_escape_1.createAutoEscapeTagHandler)(),
(0, block_1.createBlockTagHandler)(),
(0, deprecated_1.createDeprecatedTagHandler)(),
(0, do_1.createDoTagHandler)(),
(0, embed_1.createEmbedTagHandler)(),
(0, extends_1.createExtendsTagHandler)(),
(0, flush_1.createFlushTagHandler)(),
(0, for_1.createForTagHandler)(),
(0, from_1.createFromTagHandler)(),
(0, if_1.createIfTagHandler)(),
(0, import_1.createImportTagHandler)(),
(0, include_1.createIncludeTagHandler)(),
(0, line_1.createLineTagHandler)(),
(0, macro_1.createMacroTagHandler)(),
(0, sandbox_2.createSandboxTagHandler)(),
(0, set_1.createSetTagHandler)(),
(0, use_1.createUseTagHandler)(),
(0, verbatim_1.createVerbatimTagHandler)(),
(0, with_1.createWithTagHandler)()
];
if (level === 2) {
tagHandlers.push(...[
(0, filter_2.createFilterTagHandler)(),
(0, spaceless_1.createSpacelessTagHandler)(),
]);
}
tagHandlers.push(...additionalTagHandlers);
const tokenParsers = new Map();
let varNameSalt = 0;
let parent = null;
let blocks = {};
let blockStack = [];
let macros = {};
let importedSymbols = [{
method: new Map(),
template: []
}];
let traits = {};
let embeddedTemplates = [];
let embeddedTemplateIndex = 1;
const filterNames = getNames(filters);
const functionNames = getNames(functions);
const testNames = getNames(tests);
const tags = tagHandlers.map(({ tag }) => tag);
const stack = [];
const addImportedSymbol = (type, alias, name, node) => {
const localScope = importedSymbols[0];
if (type === "method") {
localScope[type].set(alias, {
name: name,
node: node
});
}
else {
localScope[type].push(alias);
}
};
const addTrait = (trait) => {
(0, record_1.pushToRecord)(traits, trait);
};
// checks that the node only contains "constant" elements
const checkConstantExpression = (stackEntry, node) => {
if (!(node.type === "constant"
|| node.type === "array"
|| node.type === "hash"
|| node.type === "negative"
|| node.type === "positive")) {
return node;
}
for (const [, child] of (0, node_1.getChildren)(node)) {
if (checkConstantExpression(stackEntry, child) !== null) {
return child;
}
}
return null;
};
const embedTemplate = (template) => {
template.attributes.index = embeddedTemplateIndex++;
embeddedTemplates.push(template);
};
const filterChildBodyNode = (stream, node, nested = false) => {
// non-empty text nodes are not allowed as direct child of a
const testedNode = node;
if (testedNode.type === "text" && !(0, is_made_of_whitespace_only_1.isMadeOfWhitespaceOnly)(testedNode.attributes.data)) {
const { data } = testedNode.attributes;
if (data.indexOf(String.fromCharCode(0xEF, 0xBB, 0xBF)) > -1) {
const trailingData = data.substring(3);
if (trailingData === '' || (0, is_made_of_whitespace_only_1.isMadeOfWhitespaceOnly)(trailingData)) {
// bypass empty nodes starting with a BOM
return null;
}
}
throw (0, parsing_1.createParsingError)(`A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?`, node, stream.source);
}
const { type } = node;
// bypass nodes that "capture" the output
if (type === "set") {
return node;
}
// to be removed completely in Twig 3.0
if (!nested && (type === "spaceless")) {
console.warn(`Using the spaceless tag at the root level of a child template in "${stream.source.name}" at line ${node.line} is deprecated since Twig 2.5.0 and will become a syntax error in Twig 3.0.`);
}
// "block" tags that are not capturing (see above) are only used for defining
// the content of the block. In such a case, nesting it does not work as
// expected as the definition is not part of the default template code flow.
if (nested && (type === "block_reference")) {
if (level >= 3) {
throw (0, parsing_1.createParsingError)(`A block definition cannot be nested under non-capturing nodes.`, node, stream.source);
}
else {
console.warn(`Nesting a block definition under a non-capturing node in "${stream.source.name}" at line ${node.line} is deprecated since Twig 2.5.0 and will become a syntax error in Twig 3.0.`);
return null;
}
}
if (type === "block_reference" || type === "print" || type === "text") {
return null;
}
// here, nested means "being at the root level of a child template"
// we need to discard the wrapping node for the "body" node
nested = nested || (type !== null);
for (const [key, child] of (0, node_1.getChildren)(node)) {
if (child !== null && (filterChildBodyNode(stream, child, nested) === null)) {
delete node.children[key];
}
}
return node;
};
const getBlock = (name) => {
return blocks[name] || null;
};
const getBlockStack = () => {
return blockStack;
};
const getFilterExpressionFactory = (stream, name, line, column) => {
const filter = (0, get_filter_1.getFilter)(filters, name);
if (filter) {
if (filter.isDeprecated) {
let message = `Filter "${filter.name}" is deprecated`;
if (filter.deprecatedVersion !== true) {
message += ` since version ${filter.deprecatedVersion}`;
}
if (filter.alternative) {
message += `. Use "${filter.alternative}" instead`;
}
let src = stream.source;
message += ` in "${src.name}" at line ${line}.`;
console.warn(message);
}
}
else if (strict) {
const error = (0, parsing_1.createParsingError)(`Unknown filter "${name}".`, { line, column }, stream.source);
error.addSuggestions(name, filterNames);
throw error;
}
return filter_1.createFilterNode;
};
const getFunctionExpressionFactory = (stream, name, line, column) => {
const twingFunction = (0, get_function_1.getFunction)(functions, name);
if (twingFunction) {
if (twingFunction.isDeprecated) {
let message = `Function "${twingFunction.name}" is deprecated`;
if (twingFunction.deprecatedVersion !== true) {
message += ` since version ${twingFunction.deprecatedVersion}`;
}
if (twingFunction.alternative) {
message += `. Use "${twingFunction.alternative}" instead`;
}
const source = stream.source;
message += ` in "${source.name}" at line ${line}.`;
console.warn(message);
}
}
else if (strict) {
const error = (0, parsing_1.createParsingError)(`Unknown function "${name}".`, { line, column }, stream.source);
error.addSuggestions(name, functionNames);
throw error;
}
return function_1.createFunctionNode;
};
const getFunctionNode = (stream, name, line, column) => {
switch (name) {
case 'parent':
parseArguments(stream);
if (!getBlockStack().length) {
throw (0, parsing_1.createParsingError)('Calling "parent" outside a block is forbidden.', {
line,
column
}, stream.source);
}
if (!parent && !hasTraits()) {
throw (0, parsing_1.createParsingError)('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', {
line,
column
}, stream.source);
}
return (0, parent_function_1.createParentFunctionNode)(peekBlockStack(), line, column);
case 'block':
const blockArgs = parseArguments(stream);
const keyValuePairs = (0, get_key_value_pairs_1.getKeyValuePairs)(blockArgs);
if (keyValuePairs.length < 1) {
throw (0, parsing_1.createParsingError)('The "block" function takes one argument (the block name).', {
line,
column
}, stream.source);
}
return (0, block_function_1.createBlockFunctionNode)(keyValuePairs[0].value, keyValuePairs.length > 1 ? keyValuePairs[1].value : null, line, column);
case 'attribute':
const attributeArgs = parseArguments(stream);
const attributeKeyValuePairs = (0, get_key_value_pairs_1.getKeyValuePairs)(attributeArgs);
if (attributeKeyValuePairs.length < 2) {
throw (0, parsing_1.createParsingError)('The "attribute" function takes at least two arguments (the variable and the attributes).', {
line,
column
}, stream.source);
}
return (0, attribute_accessor_1.createAttributeAccessorNode)(attributeKeyValuePairs[0].value, attributeKeyValuePairs[1].value, attributeKeyValuePairs.length > 2 ? attributeKeyValuePairs[2].value : (0, array_1.createArrayNode)([], line, column), "any", line, column);
default:
const alias = getImportedMethod(name);
if (alias) {
const argumentsNode = parseArguments(stream);
const node = (0, method_call_1.createMethodCallNode)(alias.node, alias.name, argumentsNode, line, column);
return node;
}
const aliasArguments = parseArguments(stream, true);
const aliasFactory = getFunctionExpressionFactory(stream, name, line, column);
return aliasFactory(name, aliasArguments, line, column);
}
};
const getImportedMethod = (alias) => {
let result;
const testImportedSymbol = (importedSymbol) => {
const importedSymbolType = importedSymbol["method"];
if (importedSymbolType && importedSymbolType.has(alias)) {
return importedSymbolType.get(alias);
}
return null;
};
result = testImportedSymbol(importedSymbols[0]) || null;
// if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
let length = importedSymbols.length;
if (!result && (length > 1)) {
result = testImportedSymbol(importedSymbols[length - 1]) || null;
}
return result;
};
const getImportedTemplate = (alias) => {
let result;
const testImportedSymbol = (importedSymbol) => {
const importedSymbolType = importedSymbol["template"];
if (importedSymbolType && importedSymbolType.includes(alias)) {
return alias;
}
return null;
};
result = testImportedSymbol(importedSymbols[0]) || null;
// if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
let length = importedSymbols.length;
if (!result && (length > 1)) {
result = testImportedSymbol(importedSymbols[length - 1]) || null;
}
return result;
};
const getPrimary = (stream) => {
let token = stream.current;
let operator;
if ((operator = isUnary(token)) !== null) {
stream.next();
const expression = parseExpression(stream, operator.precedence);
const expressionFactory = operator.expressionFactory;
return parsePostfixExpression(stream, expressionFactory([expression, (0, node_1.createNode)()], token.line, token.column), token);
}
else if (token.test("PUNCTUATION", '(')) {
stream.next();
const expression = parseExpression(stream);
stream.expect("PUNCTUATION", ')', 'An opened parenthesis is not properly closed');
return parsePostfixExpression(stream, expression, token);
}
return parsePrimaryExpression(stream);
};
const getTestName = (stream) => {
const { line, column } = stream.current;
let name = stream.expect("NAME").value;
let test = (0, get_test_1.getTest)(tests, name);
if (!test) {
if (stream.test("NAME")) {
// try 2-words tests
name = name + ' ' + stream.current.value;
test = (0, get_test_1.getTest)(tests, name);
if (test) {
stream.next();
}
else {
// non-existing two-words test
if (!strict) {
stream.next();
test = {
name,
isDeprecated: false,
alternative: undefined,
deprecatedVersion: undefined
};
}
}
}
else {
// non-existing one-word test
if (!strict) {
test = {
name,
isDeprecated: false,
alternative: undefined,
deprecatedVersion: undefined
};
}
}
}
if (test) {
if (test.isDeprecated) {
let message = `Test "${test.name}" is deprecated`;
if (test.deprecatedVersion !== true) {
message += ` since version ${test.deprecatedVersion}`;
}
if (test.alternative) {
message += `. Use "${test.alternative}" instead`;
}
const source = stream.source;
message += ` in "${source.name}" at line ${line}.`;
console.warn(message);
}
return name;
}
const error = (0, parsing_1.createParsingError)(`Unknown test "${name}".`, { line, column }, stream.source);
error.addSuggestions(name, testNames);
throw error;
};
const getVarName = (prefix = '__internal_') => {
return `${prefix}${varNameSalt++}`;
};
const hasTraits = () => {
return Object.keys(traits).length > 0;
};
const isBinary = (token) => {
if (token.value === "is" || token.value === "is not") {
return {
expressionFactory: null,
name: token.value,
precedence: 100
};
}
return (token.test("OPERATOR") && binaryOperatorsRegister.get(token.value)) || null;
};
const isUnary = (token) => {
return (token.test("OPERATOR") && unaryOperatorsRegister.get(token.value)) || null;
};
const parse = (stream, tag = null, test = null) => {
stack.push({
stream,
parent,
blocks,
blockStack,
macros,
importedSymbols,
traits,
embeddedTemplates
});
parent = null;
blocks = {};
macros = {};
traits = {};
blockStack = [];
importedSymbols = [{
method: new Map(),
template: []
}];
embeddedTemplates = [];
let body = subparse(stream, tag, test);
if (parent !== null && (body = filterChildBodyNode(stream, body)) === null) {
body = (0, node_1.createNode)();
}
let node = (0, template_1.createTemplateNode)(body, parent, (0, node_1.createNode)(blocks), (0, node_1.createNode)(macros), (0, node_1.createNode)(traits), embeddedTemplates, stream.source, 1, 1);
// passed visitors
let traverse = (0, node_traverser_1.createNodeTraverser)(visitors);
node = traverse(node, stream.source);
// core visitors
traverse = (0, node_traverser_1.createNodeTraverser)([
(0, core_1.createCoreNodeVisitor)(),
(0, escaper_1.createEscaperNodeVisitor)(),
(0, sandbox_1.createSandboxNodeVisitor)()
]);
node = traverse(node, stream.source);
// restore previous stack so previous parse() call can resume working
const previousStackEntry = stack.pop();
parent = previousStackEntry.parent;
blocks = previousStackEntry.blocks;
macros = previousStackEntry.macros;
traits = previousStackEntry.traits;
blockStack = previousStackEntry.blockStack;
importedSymbols = previousStackEntry.importedSymbols;
embeddedTemplates = previousStackEntry.embeddedTemplates;
return node;
};
/**
* Parses arguments.
*
* @param stream
* @param namedArguments {boolean} Whether to allow named arguments or not
* @param definition {boolean} Whether we are parsing arguments for a macro definition
* @param allowArrow {boolean}
*
* @throws TwingErrorSyntax
*/
const parseArguments = (stream, namedArguments = false, definition = false, allowArrow) => {
const { line, column } = stream.current;
const elements = [];
let value;
let token;
stream.expect("PUNCTUATION", '(');
while (!stream.test("PUNCTUATION", ')')) {
if (elements.length > 0) {
stream.expect("PUNCTUATION", ',');
}
if (definition) {
token = stream.expect("NAME", null);
const { line, column } = stream.current;
value = (0, name_1.createNameNode)(token.value, line, column);
}
else {
value = parseExpression(stream, 0, allowArrow);
}
let key = undefined;
if (namedArguments && (token = stream.nextIf("OPERATOR", '='))) {
if (value.type !== "name") {
throw (0, parsing_1.createParsingError)(`A parameter name must be a string, "${value.type.toString()}" given.`, value, stream.source);
}
key = (0, constant_1.createConstantNode)(value.attributes.name, value.line, value.column);
if (definition) {
value = parsePrimaryExpression(stream);
const notConstantNode = checkConstantExpression(stream, value);
if (notConstantNode !== null) {
throw (0, parsing_1.createParsingError)(`A default value for an argument must be a constant (a boolean, a string, a number, or an array).`, notConstantNode, stream.source);
}
}
else {
value = parseExpression(stream, 0, allowArrow);
}
}
if (definition) {
if (key === undefined) {
key = (0, constant_1.createConstantNode)(value.attributes.name, line, column);
value = (0, constant_1.createConstantNode)(null, line, column);
}
}
elements.push({
key,
value
});
}
stream.expect("PUNCTUATION", ')');
const arrayNode = (0, array_1.createArrayNode)(elements, line, column);
return arrayNode;
};
const parseArrayExpression = (stream) => {
const { line, column } = stream.current;
stream.expect("PUNCTUATION", '[', 'An array element was expected');
const elements = [];
let first = true;
while (!stream.test("PUNCTUATION", ']')) {
if (!first) {
stream.expect("PUNCTUATION", ',', 'An array element must be followed by a comma');
// trailing ,?
if (stream.test("PUNCTUATION", ']')) {
break;
}
}
first = false;
if (stream.test("SPREAD_OPERATOR")) {
const { current } = stream;
stream.next();
const expression = parseExpression(stream);
const spreadNode = (0, spread_1.createSpreadNode)(expression, current.line, current.column);
elements.push(spreadNode);
}
else {
elements.push(parseExpression(stream));
}
}
stream.expect("PUNCTUATION", ']', 'An opened array is not properly closed');
return (0, array_1.createArrayNode)(elements.map((element) => {
return {
value: element
};
}), line, column);
};
const parseAssignmentExpression = (stream) => {
const targets = {};
const { line, column } = stream.current;
while (true) {
let token = stream.current;
if (stream.test("OPERATOR") && nameRegExp.exec(token.value)) {
// in this context, string operators are variable names
stream.next();
}
else {
stream.expect("NAME", null, 'Only variables can be assigned to');
}
let value = token.value;
if (['true', 'false', 'none', 'null'].indexOf(value.toLowerCase()) > -1) {
throw (0, parsing_1.createParsingError)(`You cannot assign a value to "${value}".`, token, stream.source);
}
(0, record_1.pushToRecord)(targets, (0, assignment_1.createAssignmentNode)(value, token.line, token.column));
if (!stream.nextIf("PUNCTUATION", ',')) {
break;
}
}
return (0, node_1.createNode)(targets, line, column);
};
const parseArrow = (stream) => {
let token;
let line;
let column;
let names;
// short array syntax (one argument, no parentheses)?
if (stream.look(1).test("ARROW")) {
line = stream.current.line;
column = stream.current.column;
token = stream.expect("NAME");
names = {
0: (0, assignment_1.createAssignmentNode)(token.value, token.line, token.column)
};
stream.expect("ARROW");
return (0, arrow_function_1.createArrowFunctionNode)(parseExpression(stream, 0), (0, node_1.createNode)(names), line, column);
}
// first, determine if we are parsing an arrow function by finding => (long form)
let i = 0;
if (!stream.look(i).test("PUNCTUATION", '(')) {
return null;
}
++i;
while (true) {
// variable name
++i;
if (!stream.look(i).test("PUNCTUATION", ',')) {
break;
}
++i;
}
stream.look(i).test("PUNCTUATION", ')');
++i;
if (!stream.look(i).test("ARROW")) {
return null;
}
// yes, let's parse it properly
token = stream.expect("PUNCTUATION", '(');
line = token.line;
column = token.column;
names = {};
i = 0;
while (true) {
token = stream.current;
if (!token.test("NAME")) {
throw (0, parsing_1.createParsingError)(`Unexpected token "${(0, lexer_1.typeToEnglish)(token.type)}" of value "${token.value}".`, token, stream.source);
}
names[i++] = (0, assignment_1.createAssignmentNode)(token.value, token.line, token.column);
stream.next();
if (!stream.nextIf("PUNCTUATION", ',')) {
break;
}
}
stream.expect("PUNCTUATION", ')');
stream.expect("ARROW");
return (0, arrow_function_1.createArrowFunctionNode)(parseExpression(stream, 0), (0, node_1.createNode)(names), line, column);
};
const parseConditionalExpression = (stream, expression) => {
let expr2;
let expr3;
while (stream.nextIf("PUNCTUATION", '?')) {
if (!stream.nextIf("PUNCTUATION", ':')) {
expr2 = parseExpression(stream);
if (stream.nextIf("PUNCTUATION", ':')) {
expr3 = parseExpression(stream);
}
else {
const { line, column } = stream.current;
expr3 = (0, constant_1.createConstantNode)('', line, column);
}
}
else {
expr2 = expression;
expr3 = parseExpression(stream);
}
const { line, column } = stream.current;
expression = (0, conditional_1.createConditionalNode)(expression, expr2, expr3, line, column);
}
return expression;
};
const parseExpression = (stream, precedence = 0, allowArrow = undefined) => {
if (allowArrow) {
const arrow = parseArrow(stream);
if (arrow) {
return arrow;
}
}
let expression = getPrimary(stream);
let token = stream.current;
let operator = null;
while (((operator = isBinary(token)) !== null && operator.precedence >= precedence)) {
stream.next();
if (operator.expressionFactory === null) {
expression = parseTestExpression(stream, expression);
if (operator.name === "is not") {
const { line, column } = stream.current;
expression = (0, not_1.createNotNode)(expression, line, column);
}
}
else {
const { expressionFactory } = operator;
const operand = parseExpression(stream, operator.associativity === "LEFT" ? operator.precedence + 1 : operator.precedence, true);
expression = expressionFactory([expression, operand], token.line, token.column);
}
token = stream.current;
}
if (precedence === 0) {
return parseConditionalExpression(stream, expression);
}
return expression;
};
const parseFilterExpression = (stream, node) => {
stream.next();
return parseFilterExpressionRaw(stream, node);
};
const parseFilterDefinitions = (stream) => {
const definitions = [];
while (true) {
const token = stream.expect("NAME");
const { value, line, column } = token;
getFilterExpressionFactory(stream, value, token.line, token.column);
let methodArguments;
if (!stream.test("PUNCTUATION", '(')) {
methodArguments = (0, array_1.createArrayNode)([], line, column);
}
else {
methodArguments = parseArguments(stream, true, false, true);
}
definitions.unshift({
name: value,
arguments: methodArguments
});
if (!stream.test("PUNCTUATION", '|')) {
break;
}
stream.next();
}
return definitions;
};
const parseFilterExpressionRaw = (stream, operand) => {
let filterNode = null;
while (true) {
const token = stream.expect("NAME");
const { value, line, column } = token;
let methodArguments;
if (!stream.test("PUNCTUATION", '(')) {
methodArguments = (0, array_1.createArrayNode)([], line, column);
}
else {
methodArguments = parseArguments(stream, true, false, true);
}
const factory = getFilterExpressionFactory(stream, value, line, column);
if (filterNode === null) {
filterNode = factory(operand, value, methodArguments, token.line, token.column);
}
else {
filterNode = factory(filterNode, value, methodArguments, token.line, token.column);
}
if (!stream.test("PUNCTUATION", '|')) {
break;
}
stream.next();
}
return filterNode;
};
const parseHashExpression = (stream) => {
stream.expect("PUNCTUATION", '{', 'A hash element was expected');
let first = true;
const elements = [];
while (!stream.test("PUNCTUATION", '}')) {
if (!first) {
stream.expect("PUNCTUATION", ',', 'A hash value must be followed by a comma');
// trailing ,?
if (stream.test("PUNCTUATION", '}')) {
break;
}
}
first = false;
if (stream.test("SPREAD_OPERATOR")) {
const { current } = stream;
stream.next();
const expression = parseExpression(stream);
const spreadNode = (0, spread_1.createSpreadNode)(expression, current.line, current.column);
elements.push({
key: (0, node_1.createNode)(),
value: spreadNode
});
continue;
}
// a hash key can be:
//
// * a number -- 12
// * a string -- 'a'
// * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2)
let token;
let key;
if (token = stream.nextIf("NAME")) {
key = (0, constant_1.createConstantNode)(token.value, token.line, token.column);
// {a} is a shortcut for {a:a}
if (stream.test("PUNCTUATION", [',', '}'])) {
elements.push({
key,
value: (0, name_1.createNameNode)(token.value, token.line, token.column)
});
continue;
}
}
else if ((token = stream.nextIf("STRING")) || (token = stream.nextIf("NUMBER"))) {
key = (0, constant_1.createConstantNode)(token.value, token.line, token.column);
}
else if (stream.test("PUNCTUATION", '(')) {
key = parseExpression(stream);
}
else {
const { type, line, value, column } = stream.current;
throw (0, parsing_1.createParsingError)(`A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "${(0, lexer_1.typeToEnglish)(type)}" of value "${value}".`, {
line,
column
}, stream.source);
}
stream.expect("PUNCTUATION", ':', 'A hash key must be followed by a colon (:)');
const value = parseExpression(stream);
elements.push({
key,
value
});
}
stream.expect("PUNCTUATION", '}', 'An opened hash is not properly closed');
return (0, hash_1.createHashNode)(elements, stream.current.line, stream.current.column);
};
const parseMultiTargetExpression = (stream) => {
const { line, column } = stream.current;
const targets = {};
while (true) {
(0, record_1.pushToRecord)(targets, parseExpression(stream));
if (!stream.nextIf("PUNCTUATION", ',')) {
break;
}
}
return (0, node_1.createNode)(targets, line, column);
};
const parsePostfixExpression = (stream, node, prefixToken) => {
while (true) {
let token = stream.current;
if (token.type === "PUNCTUATION") {
if (token.value === '.' || token.value === '[') {
node = parseSubscriptExpression(stream, node, prefixToken);
}
else if (token.value === '|') {
node = parseFilterExpression(stream, node);
}
else {
break;
}
}
else {
break;
}
}
return node;
};
const parsePrimaryExpression = (stream) => {
const token = stream.current;
let node;
switch (token.type) {
case "NAME":
stream.next();
switch (token.value) {
case 'true':
case 'TRUE':
node = (0, constant_1.createConstantNode)(true, token.line, token.column);
break;
case 'false':
case 'FALSE':
node = (0, constant_1.createConstantNode)(false, token.line, token.column);
break;
case 'none':
case 'NONE':
case 'null':
case 'NULL':
node = (0, constant_1.createConstantNode)(null, token.line, token.column);
break;
default:
if ('(' === stream.current.value) {
node = getFunctionNode(stream, token.value, token.line, token.column);
}
else {
node = (0, name_1.createNameNode)(token.value, token.line, token.column);
}
}
break;
case "NUMBER":
stream.next();
node = (0, constant_1.createConstantNode)(token.value, token.line, token.column);
break;
case "STRING":
case "INTERPOLATION_START":
node = parseStringExpression(stream);
break;
case "OPERATOR":
let match = nameRegExp.exec(token.value);
if (match !== null && match[0] === token.value) {
// in this context, string operators are variable names
stream.next();
node = (0, name_1.createNameNode)(token.value, token.line, token.column);
break;
}
else if (unaryOperatorsRegister.has(token.value)) {
const operator = unaryOperatorsRegister.get(token.value);
stream.next();
const expression = parsePrimaryExpression(stream);
const { expressionFactory } = operator;
node = expressionFactory([expression, (0, node_1.createNode)()], token.line, token.column);
break;
}
default:
if (token.test("PUNCTUATION", '[')) {
node = parseArrayExpression(stream);
}
else if (token.test("PUNCTUATION", '{')) {
node = parseHashExpression(stream);
}
else if (token.test("OPERATOR", '=') && (stream.look(-1).value === '==' || stream.look(-1).value === '!=')) {
throw (0, parsing_1.createParsingError)(`Unexpected operator of value "${token.value}". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.`, token, stream.source);
}
else {
throw (0, parsing_1.createParsingError)(`Unexpected token "${(0, lexer_1.typeToEnglish)(token.type)}" of value "${token.value}".`, token, stream.source);
}
}
return parsePostfixExpression(stream, node, token);
};
const parseStringExpression = (stream) => {
const nodes = [];
// a string cannot be followed by another string in a single expression
let nextCanBeString = true;
let token;
while (true) {
if (nextCanBeString && (token = stream.nextIf("STRING"))) {
nodes.push((0, constant_1.createConstantNode)(token.value, token.line, token.column));
nextCanBeString = false;
}
else if (stream.nextIf("INTERPOLATION_START")) {
nodes.push(parseExpression(stream));
stream.expect("INTERPOLATION_END");
nextCanBeString = true;
}
else {
break;
}
}
let expression = nodes.shift();
for (const node of nodes) {
expression = (0, concatenate_1.createConcatenateNode)([expression, node], node.line, node.column);
}
return expression;
};
const parseSubscriptExpression = (stream, node, prefixToken) => {
let token = stream.next();
let attribute;
let type = "any";
const { line, column } = token;
const { line: prefixTokenLine, column: prefixTokenColumn } = prefixToken;
const elements = [];
const createArrayNodeFromElements = () => {
return (0, array_1.createArrayNode)(elements.map((element) => {
return {
value: element
};
}), line, column);
};
if (token.value === '.') {
token = stream.next();
let match = nameRegExp.exec(token.value);
if ((token.type === "NAME") || (token.type === "NUMBER") || (token.type === "OPERATOR" && (match !== null))) {
attribute = (0, constant_1.createConstantNode)(token.value, line, column);
if (stream.test("PUNCTUATION", '(')) {
type = "method";
const argumentsNode = parseArguments(stream);
for (const { value } of (0, get_key_value_pairs_1.getKeyValuePairs)(argumentsNode)) {
elements.push(value);
}
}
}
else {
throw (0, parsing_1.createParsingError)('Expected name or number.', { line, column: column + 1 }, stream.source);
}
if ((node.type === "name") && (node.attributes.name === '_self' || getImportedTemplate(node.attributes.name))) {
const name = attribute.attributes.value;
const methodCallNode = (0, method_call_1.createMethodCallNode)(node, name, createArrayNodeFromElements(), line, column);
return methodCallNode;
}
}
else {
type = "array";
// slice?
let slice = false;
if (stream.test("PUNCTUATION", ':')) {
slice = true;
attribute = (0, constant_1.createConstantNode)(0, token.line, token.column);
}
else {
attribute = parseExpression(stream);
}
if (stream.nextIf("PUNCTUATION", ':')) {
slice = true;
}
if (slice) {
let length;
if (stream.test("PUNCTUATION", ']')) {
length = (0, constant_1.createConstantNode)(null, token.line, token.column);
}
else {
length = parseExpression(stream);
}
const factory = getFilterExpressionFactory(stream, 'slice', token.line, token.column);
const filterArguments = (0, array_1.createArrayNode)([
{
key: (0, constant_1.createConstantNode)(0, line, column),
value: attribute
},
{
key: (0, constant_1.createConstantNode)(1, line, column),
value: length
}
], 1, 1);
const filter = factory(node, 'slice', filterArguments, token.line, token.column);
stream.expect("PUNCTUATION", ']');
return filter;
}
stream.expect("PUNCTUATION", ']');
}
return (0, attribute_accessor_1.createAttributeAccessorNode)(node, attribute, createArrayNodeFromElements(), type, prefixTokenLine, prefixTokenColumn);
};
const parseTestExpression = (stream, node) => {
const { line, column } = stream.current;
const name = getTestName(stream);
let testArguments = (0, array_1.createArrayNode)([], line, column);
if (stream.test("PUNCTUATION", '(')) {
testArguments = parseArguments(stream, true);
}
if ((name === 'defined') && (node.type === "name")) {
const alias = getImportedMethod(node.attributes.name);
if (alias !== null) {
node = (0, method_call_1.createMethodCallNode)(alias.node, alias.name, (0, array_1.createArrayNode)([], node.line, node.column), node.line, node.column);
}
}
return (0, test_1.createTestNode)(node, name, testArguments, line, column);
};
const peekBlockStack = () => {
return blockStack[blockStack.length - 1];
};
const popBlockStack = () => {
blockStack.pop();
};
const popLocalScope = () => {
importedSymbols.shift();
};
const pushBlockStack = (name) => {
blockStack.push(name);
};
const pushLocalScope = () => {
importedSymbols.unshift({
method: new Map(),
template: []
});
};
const isMainScope = () => {
return importedSymbols.length === 1;
};
const setBlock = (name, node) => {
blocks[name] = node;
};
const setMacro = (name, node) => {
macros[name] = node;
};
const subparse = (stream, tag, test) => {
// token parsers
if (tokenParsers.size === 0) {
for (const handler of tagHandlers) {
tokenParsers.set(handler.tag, handler.initialize(parser, level));
}
}
let { line, column } = stream.current;
let children = {};
let i = 0;
let token;
while (!stream.isEOF()) {
switch (stream.current.type) {
case "TEXT":
token = stream.next();
children[i++] = (0, text_1.createTextNode)(token.value, token.line, token.column);
break;
case "VARIABLE_START":
token = stream.next();
const expression = parseExpression(stream);
stream.expect("VARIABLE_END");
children[i++] = (0, print_1.createPrintNode)(expression, token.line, token.column);
break;
case "TAG_START":
stream.next();
token = stream.current;
if (token.type !== "NAME") {
throw (0, parsing_1.createParsingError)('A block must start with a tag name.', token, stream.source);
}
if ((test !== null) && test(token)) {
if (Object.keys(children).length === 1) {
return children[0];
}
return (0, node_1.createNode)(children, line, column);
}
if (!tokenParsers.has(token.value)) {
let error;
if (test !== null) {
error = (0, parsing_1.createParsingError)(`Unexpected "${token.value}" tag`, token, stream.source);
error.appendMessage(` (expecting closing tag for the "${tag}" tag defined line ${line}).`);
}
else {
error = (0, parsing_1.createParsingError)(`Unknown "${token.value}" tag.`, token, stream.source);
error.addSuggestions(token.value, tags);
}
throw error;
}
stream.next();
const parseToken = tokenParsers.get(token.value);
const node = parseToken(token, stream);
if (node !== null) {
children[i++] = node;
}
break;
case "COMMENT_START":
token = stream.next();
if (stream.test("TEXT")) {
// non-empty comment
token = stream.expect("TEXT");
}
stream.expect("COMMENT_END");
children[i++] = (0, comment_1.createCommentNode)(token.value, token.line, token.column);
break;
}
}
if (Object.keys(children).length === 1) {
return children[0];
}
return (0, node_1.createNode)(children, line, column);
};
const parser = {
addImportedSymbol,
addTrait,
embedTemplate,
getBlock,
getVarName,
isMainScope,
parse,
parseArguments,
parseAssignmentExpression,
parseExpression,
p