UNPKG

twing

Version:

First-class Twig engine for Node.js

1,150 lines 50.5 kB
"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