Resolve issue #611

This commit is contained in:
Eric MORAND 2024-03-17 14:44:37 +00:00
parent 84d734d233
commit be55f5cbfb
16 changed files with 232 additions and 213 deletions

View File

@ -63,7 +63,7 @@ export const include: TwingCallable<[
}
const resolveTemplate = (templates: Array<string | TwingTemplate | null>): Promise<TwingTemplate | null> => {
return template.resolveTemplate(executionContext, templates)
return template.loadTemplate(executionContext, templates)
.catch((error) => {
if (!ignoreMissing) {
throw error;
@ -82,6 +82,7 @@ export const include: TwingCallable<[
return template.execute(
environment,
createContext(variables),
new Map(),
outputBuffer,
{
nodeExecutor,

View File

@ -11,8 +11,6 @@ export function getTraceableMethod<M extends (...args: Array<any>) => Promise<an
error.source = templateName;
}
} else {
console.log(error);
throw createRuntimeError(`An exception has been thrown during the rendering of a template ("${error.message}").`, {
line,
column

View File

@ -5,19 +5,18 @@ import {getTraceableMethod} from "../helpers/traceable-method";
export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNode> = (node, executionContext) => {
const {
template,
context,
outputBuffer
context
} = executionContext;
const {name} = node.attributes;
const renderBlock = getTraceableMethod(template.renderBlock, node.line, node.column, template.name);
const displayBlock = getTraceableMethod(template.displayBlock, node.line, node.column, template.name);
return renderBlock(
return displayBlock(
{
...executionContext,
context: context.clone()
},
name,
true,
).then(outputBuffer.echo);
);
};

View File

@ -8,7 +8,8 @@ export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = a
template,
context,
nodeExecutor: execute,
blocks
blocks,
outputBuffer
} = executionContext;
const {template: templateNode, name: blockNameNode} = node.children;
@ -41,19 +42,18 @@ export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = a
context: context.clone()
}, blockName, blocks);
} else {
const renderBlock = getTraceableMethod(templateOfTheBlock.renderBlock, node.line, node.column, template.name);
const displayBlock = getTraceableMethod(templateOfTheBlock.displayBlock, node.line, node.column, template.name);
if (templateNode) {
return renderBlock({
...executionContext,
context: context.clone()
}, blockName, false);
} else {
return renderBlock({
...executionContext,
context: context.clone()
}, blockName, true);
}
let useBlocks = templateNode === undefined;
outputBuffer.start();
return displayBlock({
...executionContext,
context: context.clone()
}, blockName, useBlocks).then<string>(() => {
return outputBuffer.getAndClean();
});
}
});
};

View File

@ -24,8 +24,6 @@ export const executeMethodCall: TwingNodeExecutor<TwingMethodCallNode> = async (
// by nature, the alias exists - the parser only creates a method call node when the name _is_ an alias.
const macroTemplate = aliases.get(operand.attributes.name)!;
console.log('executeMethodCall', template.name, aliases.has('macros'));
const getHandler = (template: TwingTemplate): Promise<TwingTemplateMacroHandler | null> => {
const macroHandler = template.macroHandlers.get(methodName);

View File

@ -3,9 +3,11 @@ import type {TwingParentFunctionNode} from "../../node/expression/parent-functio
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeParentFunction: TwingNodeExecutor<TwingParentFunctionNode> = (node, executionContext) => {
const {template} = executionContext;
const {template, outputBuffer} = executionContext;
const {name} = node.attributes;
const renderParentBlock = getTraceableMethod(template.renderParentBlock, node.line, node.column, template.name);
return renderParentBlock(executionContext, name);
const displayParentBlock = getTraceableMethod(template.displayParentBlock, node.line, node.column, template.name);
outputBuffer.start();
return displayParentBlock(executionContext, name).then(() => outputBuffer.getAndClean());
};

View File

@ -25,10 +25,6 @@ export const executeImportNode: TwingNodeExecutor<TwingImportNode> = async (node
aliases.set(aliasNode.attributes.name, aliasValue);
if (global) {
console.log('executeImportNode', template.name, template.aliases.has('macros'));
template.aliases.set(aliasNode.attributes.name, aliasValue);
console.log('>>> executeImportNode', template.name, template.aliases.has('macros'));
}
};

View File

@ -14,7 +14,7 @@ export const executeBaseIncludeNode = async (
const {only, ignoreMissing} = node.attributes;
const templatesToInclude = await getTemplate(executionContext);
const traceableInclude = getTraceableMethod(include, node.line, node.column, template.name);
const output = await traceableInclude(

View File

@ -7,8 +7,15 @@ export const executeEmbedNode: TwingNodeExecutor<TwingEmbedNode> = (node, execut
return executeBaseIncludeNode(node, executionContext, ({template}) => {
const {index} = node.attributes;
const loadTemplate = getTraceableMethod(template.loadEmbeddedTemplate, node.line, node.column, template.name);
const loadEmbeddedTemplate = getTraceableMethod(() => {
const {embeddedTemplates} = template;
return loadTemplate(index);
// by design, it is guaranteed that an embed node is always executed with an index that corresponds to an existing embedded template
const embeddedTemplate = embeddedTemplates.get(index)!;
return Promise.resolve(embeddedTemplate);
}, node.line, node.column, template.name);
return loadEmbeddedTemplate();
})
};

View File

@ -19,7 +19,7 @@ import {getKeyValuePairs} from "./helpers/get-key-value-pairs";
import {createTemplateLoader, type TwingTemplateLoader} from "./template-loader";
import type {TwingExecutionContext} from "./execution-context";
export type TwingTemplateBlockMap = Map<string, [TwingTemplate, string]>;
export type TwingTemplateBlockMap = Map<string, [template: TwingTemplate, name: string]>;
export type TwingTemplateBlockHandler = (executionContent: TwingExecutionContext) => Promise<void>;
export type TwingTemplateMacroHandler = (
executionContent: TwingExecutionContext,
@ -36,6 +36,7 @@ export interface TwingTemplate {
readonly ast: TwingTemplateNode;
readonly blockHandlers: Map<string, TwingTemplateBlockHandler>;
readonly canBeUsedAsATrait: boolean;
readonly embeddedTemplates: Map<number, TwingTemplate>;
readonly source: TwingSource;
readonly macroHandlers: Map<string, TwingTemplateMacroHandler>;
readonly name: string;
@ -46,22 +47,32 @@ export interface TwingTemplate {
useBlocks: boolean
): Promise<void>;
displayParentBlock(
executionContext: TwingExecutionContext,
name: string
): Promise<void>;
/**
* Execute the template against an environment,
*
* Theoretically speaking,
*
* Semantically speaking, provide a closure to the template:
* * an environment providing the filters, functions, globals and tests;
* * a context providing the variables;
* * a block map providing the available blocks;
* * an output buffer providing the mean to output the result of an expression.
*
* Technically speaking, execute the template and stream the result to the output buffer.
*
* @param environment
* @param context
* @param blocks
* @param outputBuffer
* @param options
*/
execute(
environment: TwingEnvironment,
context: TwingContext<any, any>,
blocks: TwingTemplateBlockMap,
outputBuffer: TwingOutputBuffer,
options?: {
blocks?: TwingTemplateBlockMap;
nodeExecutor?: TwingNodeExecutor;
sandboxed?: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
@ -89,21 +100,12 @@ export interface TwingTemplate {
hasMacro(name: string): Promise<boolean>;
/**
* @param index
*
* @throws {TwingTemplateLoadingError} When no embedded template exists for the passed index.
*/
loadEmbeddedTemplate(
index: number
): Promise<TwingTemplate>;
/**
* @throws {TwingTemplateLoadingError} When no embedded template exists for the passed identifier.
*/
loadTemplate(
executionContext: TwingExecutionContext,
identifier: TwingTemplate | string | Array<TwingTemplate | null>,
identifier: TwingTemplate | string | Array<string | TwingTemplate | null>,
): Promise<TwingTemplate>;
render(
@ -122,30 +124,6 @@ export interface TwingTemplate {
strict?: boolean;
}
): Promise<string>;
renderBlock(
executionContext: TwingExecutionContext,
name: string,
useBlocks: boolean
): Promise<string>;
renderParentBlock(
executionContext: TwingExecutionContext,
name: string
): Promise<string>;
/**
* 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
*/
resolveTemplate(
executionContext: TwingExecutionContext,
names: Array<string | TwingTemplate | null>
): Promise<TwingTemplate>;
}
export const createTemplate = (
@ -236,38 +214,15 @@ export const createTemplate = (
let traits: TwingTemplateBlockMap | null = null;
// embedded templates
const embeddedTemplates: Map<number, TwingTemplateNode> = new Map();
const embeddedTemplates: Map<number, TwingTemplate> = new Map();
for (const embeddedTemplate of ast.embeddedTemplates) {
embeddedTemplates.set(embeddedTemplate.attributes.index, embeddedTemplate);
embeddedTemplates.set(embeddedTemplate.attributes.index, createTemplate(embeddedTemplate));
}
// parent
let parent: TwingTemplate | null = null;
const displayParentBlock = (executionContext: TwingExecutionContext, name: string): Promise<void> => {
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 createRuntimeError(`The template has no parent and no traits defining the "${name}" block.`, undefined, template.name);
}
});
}
});
};
// A template can be used as a trait if:
// * it has no parent
// * it has no macros
@ -298,6 +253,49 @@ export const createTemplate = (
}
}
/**
* 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: TwingExecutionContext,
names: Array<string | TwingTemplate | null>
) => {
const loadTemplateAtIndex = (index: number): Promise<TwingTemplate> => {
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 {
return Promise.reject(createTemplateLoadingError((names as Array<string | null>).map((name) => {
if (name === null) {
return '';
}
return name;
}), undefined, template.name));
}
};
return loadTemplateAtIndex(0);
};
const template: TwingTemplate = {
get aliases() {
return aliases;
@ -311,6 +309,9 @@ export const createTemplate = (
get canBeUsedAsATrait() {
return canBeUsedAsATrait;
},
get embeddedTemplates() {
return embeddedTemplates;
},
get macroHandlers() {
return macroHandlers;
},
@ -332,7 +333,8 @@ export const createTemplate = (
const [blockTemplate, blockName] = block;
blockHandler = blockTemplate.blockHandlers.get(blockName);
} else if ((block = ownBlocks.get(name)) !== undefined) {
}
else if ((block = ownBlocks.get(name)) !== undefined) {
const [blockTemplate, blockName] = block;
blockHandler = blockTemplate.blockHandlers.get(blockName);
@ -340,18 +342,21 @@ export const createTemplate = (
if (blockHandler) {
return blockHandler(executionContext);
} else {
}
else {
return template.getParent(executionContext).then((parent) => {
if (parent) {
return parent.displayBlock(executionContext, name, false);
} else {
}
else {
const block = blocks.get(name);
if (block) {
const [blockTemplate] = block!;
throw createRuntimeError(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`, undefined, blockTemplate.name);
} else {
}
else {
throw createRuntimeError(`Block "${name}" on template "${template.name}" does not exist.`, undefined, template.name);
}
}
@ -360,9 +365,32 @@ export const createTemplate = (
}
});
},
execute: async (environment, context, outputBuffer, options) => {
displayParentBlock: (executionContext: TwingExecutionContext, name: string): Promise<void> => {
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 createRuntimeError(`The template has no parent and no traits defining the "${name}" block.`, undefined, template.name);
}
});
}
});
},
execute: async (environment, context, blocks, outputBuffer, options) => {
const aliases = template.aliases.clone();
const childBlocks = options?.blocks || new Map();
const nodeExecutor = options?.nodeExecutor || executeNode;
const sandboxed = options?.sandboxed || false;
const sourceMapRuntime = options?.sourceMapRuntime;
@ -386,17 +414,14 @@ export const createTemplate = (
template.getParent(executionContext),
template.getBlocks(executionContext)
]).then(([parent, ownBlocks]) => {
const blocks = mergeIterables(ownBlocks, childBlocks);
blocks = mergeIterables(ownBlocks, blocks);
return nodeExecutor(ast, {
...executionContext,
blocks
}).then(() => {
if (parent) {
return parent.execute(environment, context, outputBuffer, {
...options,
blocks
});
return parent.execute(environment, context, blocks, outputBuffer, options);
}
});
}).catch((error: TwingError) => {
@ -414,7 +439,8 @@ export const createTemplate = (
getBlocks: (executionContext) => {
if (blocks) {
return Promise.resolve(blocks);
} else {
}
else {
return template.getTraits(executionContext)
.then((traits) => {
blocks = mergeIterables(traits, new Map([...blockHandlers.keys()].map((key) => {
@ -458,7 +484,8 @@ export const createTemplate = (
return loadedParent;
});
} else {
}
else {
return Promise.resolve(null);
}
},
@ -509,17 +536,20 @@ export const createTemplate = (
hasBlock: (executionContext, name, blocks): Promise<boolean> => {
if (blocks.has(name)) {
return Promise.resolve(true);
} else {
}
else {
return template.getBlocks(executionContext)
.then((blocks) => {
if (blocks.has(name)) {
return Promise.resolve(true);
} else {
}
else {
return template.getParent(executionContext)
.then((parent) => {
if (parent) {
return parent.hasBlock(executionContext, name, blocks);
} else {
}
else {
return false;
}
});
@ -531,15 +561,6 @@ export const createTemplate = (
// @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));
},
loadEmbeddedTemplate: (index) => {
const ast = embeddedTemplates.get(index);
if (ast === undefined) {
return Promise.reject(createTemplateLoadingError([`embedded#${index}`]));
}
return Promise.resolve(createTemplate(ast));
},
loadTemplate: (executionContext, identifier) => {
let promise: Promise<TwingTemplate>;
@ -549,12 +570,14 @@ export const createTemplate = (
if (template === null) {
throw createTemplateLoadingError([identifier]);
}
return template;
});
} else if (Array.isArray(identifier)) {
promise = template.resolveTemplate(executionContext, identifier);
} else {
}
else if (Array.isArray(identifier)) {
promise = resolveTemplate(executionContext, identifier);
}
else {
promise = Promise.resolve(identifier);
}
@ -568,63 +591,12 @@ export const createTemplate = (
return template.execute(
environment,
createContext(iteratorToMap(context)),
new Map(),
outputBuffer,
options
).then(() => {
return outputBuffer.getAndFlush();
});
},
renderBlock: (executionContext, name, useBlocks) => {
const {outputBuffer} = executionContext;
outputBuffer.start();
return template.displayBlock(executionContext, name, useBlocks).then(() => {
return outputBuffer.getAndClean();
});
},
renderParentBlock: (executionContext, name) => {
const {outputBuffer} = executionContext;
outputBuffer.start();
return template.getBlocks(executionContext)
.then((blocks) => {
return displayParentBlock({
...executionContext,
blocks
}, name).then(() => {
return outputBuffer.getAndClean();
})
});
},
resolveTemplate: (executionContext, names) => {
const loadTemplateAtIndex = (index: number): Promise<TwingTemplate> => {
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 {
return Promise.reject(createTemplateLoadingError((names as Array<string | null>).map((name) => {
if (name === null) {
return '';
}
return name;
}), undefined, template.name));
}
};
return loadTemplateAtIndex(0);
}
};

View File

@ -2,6 +2,7 @@ import "./basic";
import "./complex_dynamic_parent";
import "./dynamic_parent";
import "./error_line";
import "./missing";
import "./multiple";
import "./nested";
import "./with_extends";

View File

@ -0,0 +1,11 @@
import {runTest} from "../../TestBase";
runTest({
description: '"embed" tag with missing template',
templates: {
'index.twig': `{% embed "missing" %}{% endembed %}`
},
expectedErrorMessage: `TwingRuntimeError: Unable to find template "missing" in "index.twig" at line 1, column 4.`
});

View File

@ -0,0 +1,36 @@
import * as tape from "tape";
import {createTemplate} from "../../../../../src/lib/template";
import {createTemplateNode} from "../../../../../src/lib/node/template";
import {createBaseNode} from "../../../../../src/lib/node";
import {createSource} from "../../../../../src/lib/source";
tape('createTemplate => ::embeddedTemplates', ({test}) => {
test('throws an error on invalid index', ({same, end}) => {
const embeddedTemplateNode = createTemplateNode(createBaseNode(null),
null,
createBaseNode(null),
createBaseNode(null),
createBaseNode(null),
[],
createSource('', ''),
1, 1
);
const template = createTemplate(createTemplateNode(
createBaseNode(null),
null,
createBaseNode(null),
createBaseNode(null),
createBaseNode(null),
[
embeddedTemplateNode
],
createSource('', ''),
1, 1
));
same(template.embeddedTemplates.get(0)?.ast, embeddedTemplateNode);
end();
});
});

View File

@ -38,9 +38,9 @@ tape('createTemplate => ::execute', ({test}) => {
return template.execute(
environment,
createContext(),
new Map(),
createOutputBuffer(),
{
blocks: new Map(),
nodeExecutor: executeNodeSpy
}
).then(() => {
@ -73,9 +73,9 @@ tape('createTemplate => ::execute', ({test}) => {
return template.execute(
environment,
createContext(),
new Map(),
createOutputBuffer(),
{
blocks: new Map(),
nodeExecutor: executeNodeSpy,
sandboxed: true,
sourceMapRuntime
@ -121,7 +121,7 @@ tape('createTemplate => ::execute', ({test}) => {
outputBuffer.start();
return template.execute(environment, createContext(), outputBuffer, {
return template.execute(environment, createContext(), new Map(), outputBuffer, {
nodeExecutor
}).then(() => {
same(outputBuffer.getContents(), 'foo5');
@ -152,6 +152,7 @@ tape('createTemplate => ::execute', ({test}) => {
return template.execute(
environment,
createContext(),
new Map(),
outputBuffer,
{
templateLoader
@ -163,4 +164,30 @@ tape('createTemplate => ::execute', ({test}) => {
]);
}).finally(end);
});
test('with some blocks', async ({same, end}) => {
const environment = createEnvironment(createArrayLoader({
index: '{{ block("foo") }}, {{ block("aliased-bar") }}',
blocks: `{% block foo %}foo block content{% endblock %}{% block bar %}bar block content{% endblock %}`
}));
const template = await environment.loadTemplate('index');
const outputBuffer = createOutputBuffer();
outputBuffer.start();
const blockTemplate = await environment.loadTemplate('blocks');
return template.execute(
environment,
createContext(),
new Map([
['foo', [blockTemplate, 'foo']],
['aliased-bar', [blockTemplate, 'bar']]
]),
outputBuffer
).then(() => {
same(outputBuffer.getContents(), 'foo block content, bar block content');
}).finally(end);
});
});

View File

@ -1,4 +1,4 @@
import "./ast";
import "./embedded-templates";
import "./execute";
import "./load-embedded-template";
import "./render";

View File

@ -1,29 +0,0 @@
import * as tape from "tape";
import {createTemplate} from "../../../../../src/lib/template";
import {createTemplateNode} from "../../../../../src/lib/node/template";
import {createBaseNode} from "../../../../../src/lib/node";
import {createSource} from "../../../../../src/lib/source";
tape('createTemplate => ::loadEmbeddedTemplate', ({test}) => {
test('throws an error on invalid index', ({fail, same, end}) => {
const template = createTemplate(createTemplateNode(
createBaseNode(null, {}, {
content: createBaseNode(null)
}, 1, 1),
null,
createBaseNode(null),
createBaseNode(null),
createBaseNode(null),
[],
createSource('', ''),
1, 1
));
return template.loadEmbeddedTemplate(0)
.then(() => fail())
.catch((error) => {
same((error as Error).message, 'Unable to find template "embedded#0".');
})
.finally(end);
});
});