UNPKG

twing

Version:

First-class Twig engine for Node.js

702 lines (701 loc) 31.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSynchronousTemplate = exports.createTemplate = void 0; const context_1 = require("./context"); const output_buffer_1 = require("./output-buffer"); const merge_iterables_1 = require("./helpers/merge-iterables"); const runtime_1 = require("./error/runtime"); const node_1 = require("./node"); const markup_1 = require("./markup"); const loader_1 = require("./error/loader"); const clone_map_1 = require("./helpers/clone-map"); const traceable_method_1 = require("./helpers/traceable-method"); const node_executor_1 = require("./node-executor"); const get_key_value_pairs_1 = require("./helpers/get-key-value-pairs"); const template_loader_1 = require("./template-loader"); const iterator_to_map_1 = require("./helpers/iterator-to-map"); const createTemplate = (ast) => { // blocks const blockHandlers = new Map(); let blocks = null; const { blocks: blockNodes } = ast.children; for (const [name, blockNode] of (0, node_1.getChildren)(blockNodes)) { const blockHandler = (executionContent) => { const aliases = template.aliases.clone(); return executionContent.nodeExecutor(blockNode.children.body, Object.assign(Object.assign({}, executionContent), { aliases, template })); }; blockHandlers.set(name, blockHandler); } // macros const macroHandlers = new Map(); const { macros: macrosNode } = ast.children; for (const [name, macroNode] of Object.entries(macrosNode.children)) { const macroHandler = async (executionContent, ...args) => { const { environment, nodeExecutor, outputBuffer } = executionContent; const { body, arguments: macroArguments } = macroNode.children; const keyValuePairs = (0, get_key_value_pairs_1.getKeyValuePairs)(macroArguments); const aliases = template.aliases.clone(); const localVariables = new Map(); for (const { key: keyNode, value: defaultValueNode } of keyValuePairs) { const key = keyNode.attributes.value; const defaultValue = await nodeExecutor(defaultValueNode, Object.assign(Object.assign({}, executionContent), { aliases, blocks: new Map(), context: (0, context_1.createContext)() })); let value = args.shift(); if (value === undefined) { value = defaultValue; } localVariables.set(key, value); } localVariables.set('varargs', args); const context = (0, context_1.createContext)(localVariables); const blocks = new Map(); outputBuffer.start(); return await nodeExecutor(body, Object.assign(Object.assign({}, executionContent), { aliases, blocks, context, template })) .then(() => { const content = outputBuffer.getContents(); return (0, markup_1.createMarkup)(content, environment.charset); }) .finally(() => { outputBuffer.endAndClean(); }); }; macroHandlers.set(name, macroHandler); } // traits let traits = null; // embedded templates const embeddedTemplates = new Map(); for (const embeddedTemplate of ast.embeddedTemplates) { embeddedTemplates.set(embeddedTemplate.attributes.index, (0, exports.createTemplate)(embeddedTemplate)); } // parent let parent = null; // A template can be used as a trait if: // * it has no parent // * it has no macros // * it has no body // // Put another way, a template can be used as a trait if it // only contains blocks and use statements. const { parent: parentNode, macros, body } = ast.children; const { line, column } = ast; let canBeUsedAsATrait = (parentNode === undefined) && ((0, node_1.getChildrenCount)(macros) === 0); if (canBeUsedAsATrait) { let node = body; if ((0, node_1.getChildrenCount)(body) === 0) { node = (0, node_1.createNode)({ body }, line, column); } for (const [, child] of Object.entries(node.children)) { if ((0, node_1.getChildrenCount)(child) === 0) { continue; } canBeUsedAsATrait = false; break; } } /** * Tries to load templates consecutively from an array. * * Similar to loadTemplate() but it also accepts instances of TwingTemplate and an array of templates where each is tried to be loaded. * * @param executionContext * @param names A template or an array of templates to try consecutively */ const resolveTemplate = (executionContext, names) => { const loadTemplateAtIndex = (index) => { if (index < names.length) { const name = names[index]; if (name === null) { return loadTemplateAtIndex(index + 1); } else if (typeof name !== "string") { return Promise.resolve(name); } else { return template.loadTemplate(executionContext, name) .catch(() => { return loadTemplateAtIndex(index + 1); }); } } else { // todo: use traceable method? return Promise.reject((0, loader_1.createTemplateLoadingError)(names.map((name) => { if (name === null) { return ''; } return name; }))); } }; return loadTemplateAtIndex(0); }; const template = { get aliases() { return aliases; }, get ast() { return ast; }, get blockHandlers() { return blockHandlers; }, get canBeUsedAsATrait() { return canBeUsedAsATrait; }, get embeddedTemplates() { return embeddedTemplates; }, get macroHandlers() { return macroHandlers; }, get name() { return template.source.name; }, get source() { return ast.attributes.source; }, displayBlock: (executionContext, name, useBlocks) => { const { blocks } = executionContext; return template.getBlocks(executionContext) .then((ownBlocks) => { let blockHandler; let block; if (useBlocks && (block = blocks.get(name)) !== undefined) { const [blockTemplate, blockName] = block; blockHandler = blockTemplate.blockHandlers.get(blockName); } else if ((block = ownBlocks.get(name)) !== undefined) { const [blockTemplate, blockName] = block; blockHandler = blockTemplate.blockHandlers.get(blockName); } if (blockHandler) { return blockHandler(executionContext); } else { return template.getParent(executionContext).then((parent) => { if (parent) { return parent.displayBlock(executionContext, name, false); } else { const block = blocks.get(name); if (block) { const [blockTemplate] = block; throw new Error(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`); } else { throw new Error(`Block "${name}" on template "${template.name}" does not exist.`); } } }); } }); }, displayParentBlock: (executionContext, name) => { return template.getTraits(executionContext) .then((traits) => { const trait = traits.get(name); if (trait) { const [blockTemplate, blockName] = trait; return blockTemplate.displayBlock(executionContext, blockName, false); } else { return template.getParent(executionContext) .then((parent) => { if (parent !== null) { return parent.displayBlock(executionContext, name, false); } else { throw new Error(`The template has no parent and no traits defining the "${name}" block.`); } }); } }); }, execute: async (environment, context, blocks, outputBuffer, options) => { const aliases = template.aliases.clone(); const nodeExecutor = (options === null || options === void 0 ? void 0 : options.nodeExecutor) || node_executor_1.executeNode; const sandboxed = (options === null || options === void 0 ? void 0 : options.sandboxed) || false; const sourceMapRuntime = options === null || options === void 0 ? void 0 : options.sourceMapRuntime; const templateLoader = (options === null || options === void 0 ? void 0 : options.templateLoader) || (0, template_loader_1.createTemplateLoader)(environment); const executionContext = { aliases, blocks: new Map(), context, environment, nodeExecutor, outputBuffer, sandboxed, sourceMapRuntime, strict: (options === null || options === void 0 ? void 0 : options.strict) || false, template, templateLoader }; return Promise.all([ template.getParent(executionContext), template.getBlocks(executionContext) ]).then(([parent, ownBlocks]) => { blocks = (0, merge_iterables_1.mergeIterables)(ownBlocks, blocks); return nodeExecutor(ast, Object.assign(Object.assign({}, executionContext), { blocks })).then(() => { if (parent) { return parent.execute(environment, context, blocks, outputBuffer, options); } }); }); }, getBlocks: (executionContext) => { if (blocks) { return Promise.resolve(blocks); } else { return template.getTraits(executionContext) .then((traits) => { blocks = (0, merge_iterables_1.mergeIterables)(traits, new Map([...blockHandlers.keys()].map((key) => { return [key, [template, key]]; }))); return blocks; }); } }, getParent: async (executionContext) => { if (parent !== null) { return Promise.resolve(parent); } const parentNode = ast.children.parent; if (parentNode) { const { nodeExecutor } = executionContext; return template.getBlocks(executionContext) .then(async (blocks) => { const parentName = await nodeExecutor(parentNode, Object.assign(Object.assign({}, executionContext), { aliases: (0, context_1.createContext)(), blocks })); const loadTemplate = (0, traceable_method_1.getTraceableMethod)(template.loadTemplate, parentNode, template.source); const loadedParent = await loadTemplate(executionContext, parentName); if (parentNode.type === "constant") { parent = loadedParent; } return loadedParent; }); } else { return Promise.resolve(null); } }, getTraits: async (executionContext) => { if (traits === null) { traits = new Map(); const { traits: traitsNode } = ast.children; for (const [, traitNode] of (0, node_1.getChildren)(traitsNode)) { const { template: templateNameNode, targets } = traitNode.children; const templateName = templateNameNode.attributes.value; const loadTemplate = (0, traceable_method_1.getTraceableMethod)(template.loadTemplate, templateNameNode, template.source); const traitTemplate = await loadTemplate(executionContext, templateName); if (!traitTemplate.canBeUsedAsATrait) { throw (0, runtime_1.createRuntimeError)(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.source); } const traitBlocks = (0, clone_map_1.cloneMap)(await traitTemplate.getBlocks(executionContext)); for (const [key, target] of (0, node_1.getChildren)(targets)) { const traitBlock = traitBlocks.get(key); if (!traitBlock) { throw (0, runtime_1.createRuntimeError)(`Block "${key}" is not defined in trait "${templateName}".`, templateNameNode, template.source); } const targetValue = target.attributes.value; traitBlocks.set(targetValue, traitBlock); traitBlocks.delete(key); } traits = (0, merge_iterables_1.mergeIterables)(traits, traitBlocks); } } return Promise.resolve(traits); }, hasBlock: (executionContext, name, blocks) => { if (blocks.has(name)) { return Promise.resolve(true); } else { return template.getBlocks(executionContext) .then((blocks) => { if (blocks.has(name)) { return Promise.resolve(true); } else { return template.getParent(executionContext) .then((parent) => { if (parent) { return parent.hasBlock(executionContext, name, blocks); } else { return false; } }); } }); } }, hasMacro: (name) => { // @see https://github.com/twigphp/Twig/issues/3174 as to why we don't check macro existence in parents return Promise.resolve(template.macroHandlers.has(name)); }, loadTemplate: (executionContext, identifier) => { let promise; if (typeof identifier === "string") { promise = executionContext.templateLoader(identifier, template.name) .then((template) => { if (template === null) { throw (0, loader_1.createTemplateLoadingError)([identifier]); } return template; }); } else if (Array.isArray(identifier)) { promise = resolveTemplate(executionContext, identifier); } else { promise = Promise.resolve(identifier); } return promise; }, render: (environment, context, options) => { const outputBuffer = (options === null || options === void 0 ? void 0 : options.outputBuffer) || (0, output_buffer_1.createOutputBuffer)(); outputBuffer.start(); return template.execute(environment, (0, context_1.createContext)((0, iterator_to_map_1.iteratorToMap)(context)), new Map(), outputBuffer, options).then(() => { return outputBuffer.getAndFlush(); }); } }; const aliases = (0, context_1.createContext)(); aliases.set(`_self`, template); return template; }; exports.createTemplate = createTemplate; const createSynchronousTemplate = (ast) => { // blocks const blockHandlers = new Map(); let blocks = null; const { blocks: blockNodes } = ast.children; for (const [name, blockNode] of (0, node_1.getChildren)(blockNodes)) { const blockHandler = (executionContent) => { const aliases = Object.assign({}, template.aliases); return executionContent.nodeExecutor(blockNode.children.body, Object.assign(Object.assign({}, executionContent), { aliases, template })); }; blockHandlers.set(name, blockHandler); } // macros const macroHandlers = new Map(); const { macros: macrosNode } = ast.children; for (const [name, macroNode] of Object.entries(macrosNode.children)) { const macroHandler = (executionContent, ...args) => { const { environment, nodeExecutor, outputBuffer } = executionContent; const { body, arguments: macroArguments } = macroNode.children; const keyValuePairs = (0, get_key_value_pairs_1.getKeyValuePairs)(macroArguments); const aliases = Object.assign({}, template.aliases); const localVariables = new Map(); for (const { key: keyNode, value: defaultValueNode } of keyValuePairs) { const key = keyNode.attributes.value; const defaultValue = nodeExecutor(defaultValueNode, Object.assign(Object.assign({}, executionContent), { aliases, blocks: new Map(), context: new Map() })); let value = args.shift(); if (value === undefined) { value = defaultValue; } localVariables.set(key, value); } localVariables.set('varargs', args); const context = localVariables; const blocks = new Map(); outputBuffer.start(); try { nodeExecutor(body, Object.assign(Object.assign({}, executionContent), { aliases, blocks, context, template })); const content = outputBuffer.getContents(); return (0, markup_1.createMarkup)(content, environment.charset); } finally { outputBuffer.endAndClean(); } }; macroHandlers.set(name, macroHandler); } // traits let traits = null; // embedded templates const embeddedTemplates = new Map(); for (const embeddedTemplate of ast.embeddedTemplates) { embeddedTemplates.set(embeddedTemplate.attributes.index, (0, exports.createSynchronousTemplate)(embeddedTemplate)); } // parent let parent = null; // A template can be used as a trait if: // * it has no parent // * it has no macros // * it has no body // // Put another way, a template can be used as a trait if it // only contains blocks and use statements. const { parent: parentNode, macros, body } = ast.children; const { line, column } = ast; let canBeUsedAsATrait = (parentNode === undefined) && ((0, node_1.getChildrenCount)(macros) === 0); if (canBeUsedAsATrait) { let node = body; if ((0, node_1.getChildrenCount)(body) === 0) { node = (0, node_1.createNode)({ body }, line, column); } for (const [, child] of Object.entries(node.children)) { if ((0, node_1.getChildrenCount)(child) === 0) { continue; } canBeUsedAsATrait = false; break; } } /** * Tries to load templates consecutively from an array. * * Similar to loadTemplate() but it also accepts instances of TwingTemplate and an array of templates where each is tried to be loaded. * * @param executionContext * @param names A template or an array of templates to try consecutively */ const resolveTemplate = (executionContext, names) => { const loadTemplateAtIndex = (index) => { if (index < names.length) { const name = names[index]; if (name === null) { return loadTemplateAtIndex(index + 1); } else if (typeof name !== "string") { return name; } else { try { return template.loadTemplate(executionContext, name); } catch (error) { return loadTemplateAtIndex(index + 1); } } } else { throw (0, loader_1.createTemplateLoadingError)(names.map((name) => { if (name === null) { return ''; } return name; })); } }; return loadTemplateAtIndex(0); }; const template = { get aliases() { return aliases; }, get ast() { return ast; }, get blockHandlers() { return blockHandlers; }, get canBeUsedAsATrait() { return canBeUsedAsATrait; }, get embeddedTemplates() { return embeddedTemplates; }, get macroHandlers() { return macroHandlers; }, get name() { return template.source.name; }, get source() { return ast.attributes.source; }, displayBlock: (executionContext, name, useBlocks) => { const { blocks } = executionContext; const ownBlocks = template.getBlocks(executionContext); let blockHandler; let block; if (useBlocks && (block = blocks.get(name)) !== undefined) { const [blockTemplate, blockName] = block; blockHandler = blockTemplate.blockHandlers.get(blockName); } else if ((block = ownBlocks.get(name)) !== undefined) { const [blockTemplate, blockName] = block; blockHandler = blockTemplate.blockHandlers.get(blockName); } if (blockHandler) { return blockHandler(executionContext); } else { const parent = template.getParent(executionContext); if (parent) { return parent.displayBlock(executionContext, name, false); } else { const block = blocks.get(name); if (block) { const [blockTemplate] = block; throw new Error(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`); } else { throw new Error(`Block "${name}" on template "${template.name}" does not exist.`); } } } }, displayParentBlock: (executionContext, name) => { const traits = template.getTraits(executionContext); const trait = traits.get(name); if (trait) { const [blockTemplate, blockName] = trait; return blockTemplate.displayBlock(executionContext, blockName, false); } else { const parent = template.getParent(executionContext); if (parent !== null) { return parent.displayBlock(executionContext, name, false); } else { throw new Error(`The template has no parent and no traits defining the "${name}" block.`); } } }, execute: (environment, context, blocks, outputBuffer, options) => { const aliases = Object.assign({}, template.aliases); const nodeExecutor = (options === null || options === void 0 ? void 0 : options.nodeExecutor) || node_executor_1.executeNodeSynchronously; const sandboxed = (options === null || options === void 0 ? void 0 : options.sandboxed) || false; const sourceMapRuntime = options === null || options === void 0 ? void 0 : options.sourceMapRuntime; const templateLoader = (options === null || options === void 0 ? void 0 : options.templateLoader) || (0, template_loader_1.createSynchronousTemplateLoader)(environment); const executionContext = { aliases, blocks: new Map(), context, environment, nodeExecutor, outputBuffer, sandboxed, sourceMapRuntime, strict: (options === null || options === void 0 ? void 0 : options.strict) || false, template, templateLoader }; const parent = template.getParent(executionContext); const ownBlocks = template.getBlocks(executionContext); blocks = (0, merge_iterables_1.mergeIterables)(ownBlocks, blocks); nodeExecutor(ast, Object.assign(Object.assign({}, executionContext), { blocks })); if (parent) { return parent.execute(environment, context, blocks, outputBuffer, options); } }, getBlocks: (executionContext) => { if (blocks !== null) { return blocks; } const traits = template.getTraits(executionContext); blocks = (0, merge_iterables_1.mergeIterables)(traits, new Map([...blockHandlers.keys()].map((key) => { return [key, [template, key]]; }))); return blocks; }, getParent: (executionContext) => { if (parent !== null) { return parent; } const parentNode = ast.children.parent; if (parentNode) { const { nodeExecutor } = executionContext; const blocks = template.getBlocks(executionContext); const parentName = nodeExecutor(parentNode, Object.assign(Object.assign({}, executionContext), { aliases: {}, blocks })); const loadTemplate = (0, traceable_method_1.getSynchronousTraceableMethod)(template.loadTemplate, parentNode, template.source); const loadedParent = loadTemplate(executionContext, parentName); if (parentNode.type === "constant") { parent = loadedParent; } return loadedParent; } else { return null; } }, getTraits: (executionContext) => { if (traits === null) { traits = new Map(); const { traits: traitsNode } = ast.children; for (const [, traitNode] of (0, node_1.getChildren)(traitsNode)) { const { template: templateNameNode, targets } = traitNode.children; const templateName = templateNameNode.attributes.value; const loadTemplate = (0, traceable_method_1.getSynchronousTraceableMethod)(template.loadTemplate, templateNameNode, template.source); const traitTemplate = loadTemplate(executionContext, templateName); if (!traitTemplate.canBeUsedAsATrait) { throw (0, runtime_1.createRuntimeError)(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.source); } const traitBlocks = (0, clone_map_1.cloneMap)(traitTemplate.getBlocks(executionContext)); for (const [key, target] of (0, node_1.getChildren)(targets)) { const traitBlock = traitBlocks.get(key); if (!traitBlock) { throw (0, runtime_1.createRuntimeError)(`Block "${key}" is not defined in trait "${templateName}".`, templateNameNode, template.source); } const targetValue = target.attributes.value; traitBlocks.set(targetValue, traitBlock); traitBlocks.delete(key); } traits = (0, merge_iterables_1.mergeIterables)(traits, traitBlocks); } } return traits; }, hasBlock: (executionContext, name, blocks) => { if (blocks.has(name)) { return true; } else { const blocks = template.getBlocks(executionContext); if (blocks.has(name)) { return true; } else { const parent = template.getParent(executionContext); if (parent) { return parent.hasBlock(executionContext, name, blocks); } else { return false; } } } }, hasMacro: (name) => { // @see https://github.com/twigphp/Twig/issues/3174 as to why we don't check macro existence in parents return template.macroHandlers.has(name); }, loadTemplate: (executionContext, identifier) => { if (typeof identifier === "string") { const loadedTemplate = executionContext.templateLoader(identifier, template.name); if (loadedTemplate === null) { throw (0, loader_1.createTemplateLoadingError)([identifier]); } return loadedTemplate; } else if (Array.isArray(identifier)) { return resolveTemplate(executionContext, identifier); } else { return identifier; } }, render: (environment, context, options) => { const outputBuffer = (options === null || options === void 0 ? void 0 : options.outputBuffer) || (0, output_buffer_1.createOutputBuffer)(); outputBuffer.start(); template.execute(environment, context, new Map(), outputBuffer, options); return outputBuffer.getAndFlush(); } }; const aliases = {}; aliases[`_self`] = template; return template; }; exports.createSynchronousTemplate = createSynchronousTemplate;