twing
Version:
First-class Twig engine for Node.js
702 lines (701 loc) • 31.4 kB
JavaScript
;
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;