Resolve issue #608

This commit is contained in:
Eric MORAND 2024-03-15 17:46:13 +01:00
parent cdeb823b73
commit 17b70f7e95
59 changed files with 655 additions and 858 deletions

View File

@ -317,4 +317,5 @@ export {createSandboxSecurityPolicy} from "./lib/sandbox/security-policy";
export {createSource} from "./lib/source";
export {createSourceMapRuntime} from "./lib/source-map-runtime";
export {createTemplate} from "./lib/template";
export {type TwingTemplateLoader, createTemplateLoader} from "./lib/template-loader";
export {createTest} from "./lib/test";

View File

@ -20,15 +20,14 @@ import {TwingTemplateNode} from "./node/template";
import {RawSourceMap} from "source-map";
import {createSourceMapRuntime} from "./source-map-runtime";
import {createSandboxSecurityPolicy, TwingSandboxSecurityPolicy} from "./sandbox/security-policy";
import {createTemplate, TwingTemplate} from "./template";
import {TwingTemplate} from "./template";
import {Settings as DateTimeSettings} from "luxon";
import {EventEmitter} from "events";
import {createTemplateLoadingError} from "./error/loader";
import {TwingParsingError} from "./error/parsing";
import {createLexer, TwingLexer} from "./lexer";
import {TwingCache} from "./cache";
import {createCoreExtension} from "./extension/core";
import {createAutoEscapeNode} from "../lib";
import {createAutoEscapeNode, createTemplateLoadingError} from "../lib";
import {createTemplateLoader} from "./template-loader";
export type TwingNumberFormat = {
numberOfDecimals: number;
@ -44,16 +43,11 @@ export type TwingEnvironmentOptions = {
*/
autoEscapingStrategy?: string;
/**
* Controls whether the templates are recompiled whenever their content changes or not.
*
* When set to `true`, templates are recompiled whenever their content changes instead of fetching them from the persistent cache. Note that this won't invalidate the environment inner cache but only the cache passed using the `cache` option. Defaults to `false`.
*/
autoReload?: boolean;
/**
* The persistent cache instance.
*/
cache?: TwingCache;
/**
* The default charset. Defaults to "UTF-8".
*/
@ -62,21 +56,12 @@ export type TwingEnvironmentOptions = {
dateIntervalFormat?: string;
numberFormat?: TwingNumberFormat;
parserOptions?: TwingParserOptions;
sandboxed?: boolean;
sandboxPolicy?: TwingSandboxSecurityPolicy;
/**
* Controls whether accessing invalid variables (variables and or attributes/methods that do not exist) triggers a runtime error.
*
* When set to `true`, accessing invalid variables triggers a runtime error.
* When set to `false`, accessing invalid variables returns `null`.
*
* Defaults to `false`.
*/
strictVariables?: boolean;
timezone?: string;
};
export interface TwingEnvironment {
readonly cache: TwingCache | null;
readonly charset: string;
readonly dateFormat: string;
readonly dateIntervalFormat: string;
@ -84,7 +69,6 @@ export interface TwingEnvironment {
readonly numberFormat: TwingNumberFormat;
readonly filters: Map<string, TwingFilter>;
readonly functions: Map<string, TwingFunction>;
readonly isStrictVariables: boolean;
readonly loader: TwingLoader;
readonly sandboxPolicy: TwingSandboxSecurityPolicy;
readonly tests: Map<string, TwingTest>;
@ -122,13 +106,6 @@ export interface TwingEnvironment {
*/
loadTemplate(name: string, from?: string | null): Promise<TwingTemplate>;
/**
* Register the passed listener...
*
* When a template is encountered, Twing environment emits a `template` event with the name of the encountered template and the source of the template that initiated the loading.
*/
on(eventName: "load", listener: (name: string, from: string | null) => void): void;
/**
* Converts a token list to a template.
*
@ -142,12 +119,18 @@ export interface TwingEnvironment {
/**
* Convenient method that renders a template from its name.
*/
render(name: string, context: Record<string, any>): Promise<string>;
render(name: string, context: Record<string, any>, options?: {
sandboxed?: boolean;
strict?: boolean;
}): Promise<string>;
/**
* Convenient method that renders a template from its name and returns both the render result and its belonging source map.
*/
renderWithSourceMap(name: string, context: Record<string, any>): Promise<{
renderWithSourceMap(name: string, context: Record<string, any>, options?: {
sandboxed?: boolean;
strict?: boolean;
}): Promise<{
data: string;
sourceMap: RawSourceMap;
}>;
@ -190,7 +173,6 @@ export const createEnvironment = (
extensionSet.addExtension(createCoreExtension());
const shouldAutoReload = options?.autoReload || false;
const cache: TwingCache | null = options?.cache || null;
const charset = options?.charset || 'UTF-8';
const dateFormat = options?.dateFormat || 'F j, Y H:i';
@ -200,16 +182,15 @@ export const createEnvironment = (
numberOfDecimals: 0,
thousandSeparator: ','
};
const eventEmitter = new EventEmitter();
const sandboxPolicy = options?.sandboxPolicy || createSandboxSecurityPolicy();
let isSandboxed = options?.sandboxed ? true : false;
let lexer: TwingLexer;
let parser: TwingParser;
const loadedTemplates: Map<string, TwingTemplate> = new Map();
const environment: TwingEnvironment = {
get cache() {
return cache;
},
get charset() {
return charset;
},
@ -228,9 +209,6 @@ export const createEnvironment = (
get functions() {
return extensionSet.functions;
},
get isStrictVariables() {
return options?.strictVariables ? true : false;
},
get loader() {
return loader;
},
@ -254,80 +232,16 @@ export const createEnvironment = (
addTagHandler: extensionSet.addTagHandler,
addTest: extensionSet.addTest,
loadTemplate: async (name, from = null) => {
eventEmitter.emit('load', name, from);
const templateLoader = createTemplateLoader(environment);
let templateFqn = await loader.resolve(name, from) || name;
let loadedTemplate = loadedTemplates.get(templateFqn);
if (loadedTemplate) {
return Promise.resolve(loadedTemplate);
}
else {
const timestamp = cache ? await cache.getTimestamp(templateFqn) : 0;
const getAstFromCache = async (): Promise<TwingTemplateNode | null> => {
if (cache === null) {
return Promise.resolve(null);
return templateLoader(name, from)
.then((template) => {
if (template === null) {
throw createTemplateLoadingError([name]);
}
let content: TwingTemplateNode | null;
/**
* When auto-reload is disabled, we always challenge the cache
* When auto-reload is enabled, we challenge the cache only if the template is considered as fresh by the loader
*/
if (shouldAutoReload) {
const isFresh = await loader.isFresh(name, timestamp, from);
if (isFresh) {
content = await cache.load(name);
}
else {
content = null;
}
}
else {
content = await cache.load(name);
}
return content;
};
const getAstFromLoader = async (): Promise<TwingTemplateNode | null> => {
const source = await loader.getSource(name, from);
if (source === null) {
return null;
}
const ast = environment.parse(environment.tokenize(source));
if (cache !== null) {
await cache.write(name, ast);
}
return ast;
};
let ast = await getAstFromCache();
if (ast === null) {
ast = await getAstFromLoader();
}
if (ast === null) {
throw createTemplateLoadingError([name]);
}
const template = createTemplate(ast);
loadedTemplates.set(templateFqn, template);
return template;
}
},
on: (eventName, listener) => {
eventEmitter.on(eventName, listener);
return template;
});
},
registerEscapingStrategy: (handler, name) => {
escapingStrategyHandlers[name] = handler;
@ -380,21 +294,19 @@ export const createEnvironment = (
throw error;
}
},
render: (name, context) => {
render: (name, context, options) => {
return environment.loadTemplate(name)
.then((template) => {
return template.render(environment, context, {
sandboxed: isSandboxed
});
return template.render(environment, context, options);
});
},
renderWithSourceMap: (name, context) => {
renderWithSourceMap: (name, context, options) => {
const sourceMapRuntime = createSourceMapRuntime();
return environment.loadTemplate(name)
.then((template) => {
return template.render(environment, context, {
sandboxed: isSandboxed,
...options,
sourceMapRuntime
});
})

View File

@ -4,6 +4,7 @@ import type {TwingOutputBuffer} from "./output-buffer";
import type {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingEnvironment} from "./environment";
import type {TwingNodeExecutor} from "./node-executor";
import type {TwingTemplateLoader} from "./template-loader";
export type TwingExecutionContext = {
aliases: TwingTemplateAliases;
@ -14,5 +15,7 @@ export type TwingExecutionContext = {
outputBuffer: TwingOutputBuffer;
sandboxed: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
strict: boolean;
template: TwingTemplate;
templateLoader: TwingTemplateLoader;
};

View File

@ -21,7 +21,7 @@ import type {TwingCallable} from "../../../callable-wrapper";
* @returns {Promise<TwingMarkup>} The rendered template
*/
export const include: TwingCallable<[
templates: string | TwingTemplate | null | Array<string | TwingTemplate | null> ,
templates: string | TwingTemplate | null | Array<string | TwingTemplate | null>,
variables: Map<string, any>,
withContext: boolean,
ignoreMissing: boolean,
@ -34,7 +34,16 @@ export const include: TwingCallable<[
ignoreMissing,
sandboxed
): Promise<TwingMarkup> => {
const {template, environment, context, nodeExecutor, outputBuffer, sourceMapRuntime} = executionContext;
const {
template,
environment,
templateLoader,
context,
nodeExecutor,
outputBuffer,
sourceMapRuntime,
strict
} = executionContext;
const from = template.name;
if (!isPlainObject(variables) && !isTraversable(variables)) {
@ -50,11 +59,11 @@ export const include: TwingCallable<[
}
if (!Array.isArray(templates)) {
templates =[templates];
templates = [templates];
}
const resolveTemplate = (templates: Array<string | TwingTemplate | null>): Promise<TwingTemplate | null> => {
return template.resolveTemplate(environment, templates)
return template.resolveTemplate(executionContext, templates)
.catch((error) => {
if (!ignoreMissing) {
throw error;
@ -64,25 +73,27 @@ export const include: TwingCallable<[
}
});
};
return resolveTemplate(templates)
.then((template) => {
outputBuffer.start();
if (template) {
return template.render(
return template.execute(
environment,
createContext(variables),
outputBuffer,
{
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime: sourceMapRuntime || undefined
sourceMapRuntime: sourceMapRuntime || undefined,
strict,
templateLoader
}
);
}
else {
return Promise.resolve('');
return Promise.resolve();
}
})
.then(() => {

View File

@ -14,7 +14,7 @@ const array_rand = require('locutus/php/array/array_rand');
* - a random character from a string
* - a random integer between 0 and the integer parameter.
*
* @param {TwingTemplate} template
* @param executionContext
* @param {*} values The values to pick a random item from
* @param {number} max Maximum value used when values is an integer
*

View File

@ -14,9 +14,9 @@ export const source: TwingCallable<[
name: string,
ignoreMissing: boolean
], string | null> = (executionContext, name, ignoreMissing) => {
const {template, environment} = executionContext;
const {template} = executionContext;
return environment.loadTemplate(name, template.name)
return template.loadTemplate(executionContext, name)
.catch(() => {
return null;
})

View File

@ -20,6 +20,7 @@ const isObject = require('isobject');
* @param {boolean} shouldTestExistence Whether this is only a defined check
* @param {boolean} shouldIgnoreStrictCheck Whether to ignore the strict attribute check or not
* @param sandboxed
* @param strict
*
* @return {Promise<any>} The attribute value, or a boolean when isDefinedTest is true, or null when the attribute is not set and ignoreStrictCheck is true
*
@ -33,11 +34,12 @@ export const getAttribute = (
type: TwingAttributeAccessorCallType,
shouldTestExistence: boolean,
shouldIgnoreStrictCheck: boolean | null,
sandboxed: boolean
sandboxed: boolean,
strict: boolean
): Promise<any> => {
const {sandboxPolicy} = environment;
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !environment.isStrictVariables : shouldIgnoreStrictCheck;
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !strict : shouldIgnoreStrictCheck;
const _do = (): any => {
let message: string;

View File

@ -11,6 +11,8 @@ 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

@ -6,26 +6,18 @@ export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNod
const {
template,
context,
environment,
outputBuffer,
blocks,
nodeExecutor: execute,
sandboxed,
sourceMapRuntime
outputBuffer
} = executionContext;
const {name} = node.attributes;
const renderBlock = getTraceableMethod(template.renderBlock, node.line, node.column, template.name);
return renderBlock(
environment,
{
...executionContext,
context: context.clone()
},
name,
context.clone(),
outputBuffer,
blocks,
true,
sandboxed,
execute,
sourceMapRuntime
).then(outputBuffer.echo);
};

View File

@ -4,7 +4,7 @@ import {getTraceableMethod} from "../../helpers/traceable-method";
import {getAttribute} from "../../helpers/get-attribute";
export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAccessorNode> = (node, executionContext) => {
const {template, sandboxed, environment, nodeExecutor: execute} = executionContext;
const {template, sandboxed, environment, nodeExecutor: execute, strict} = executionContext;
const {target, attribute, arguments: methodArguments} = node.children;
const {type, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
@ -23,7 +23,8 @@ export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAcces
type,
shouldTestExistence,
shouldIgnoreStrictCheck || null,
sandboxed
sandboxed,
strict
)
})
};

View File

@ -4,7 +4,12 @@ import {TwingTemplate} from "../../template";
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = async (node, executionContext) => {
const {template, context, environment, nodeExecutor: execute, outputBuffer, blocks, sandboxed, sourceMapRuntime} = executionContext;
const {
template,
context,
nodeExecutor: execute,
blocks
} = executionContext;
const {template: templateNode, name: blockNameNode} = node.children;
const blockName = await execute(blockNameNode, executionContext);
@ -21,24 +26,33 @@ export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = a
template.name
);
resolveTemplate = loadTemplate(environment, templateName);
resolveTemplate = loadTemplate(executionContext, templateName);
} else {
resolveTemplate = Promise.resolve(template)
}
return resolveTemplate
.then<Promise<boolean | string>>((executionContextOfTheBlock) => {
.then<Promise<boolean | string>>((templateOfTheBlock) => {
if (node.attributes.shouldTestExistence) {
const hasBlock = getTraceableMethod(executionContextOfTheBlock.hasBlock, node.line, node.column, template.name);
const hasBlock = getTraceableMethod(templateOfTheBlock.hasBlock, node.line, node.column, template.name);
return hasBlock(environment, blockName, context.clone(), outputBuffer, blocks, sandboxed, execute);
return hasBlock({
...executionContext,
context: context.clone()
}, blockName, blocks);
} else {
const renderBlock = getTraceableMethod(executionContextOfTheBlock.renderBlock, node.line, node.column, template.name);
const renderBlock = getTraceableMethod(templateOfTheBlock.renderBlock, node.line, node.column, template.name);
if (templateNode) {
return renderBlock(environment, blockName, context.clone(), outputBuffer, new Map(), false, sandboxed, execute, sourceMapRuntime);
return renderBlock({
...executionContext,
context: context.clone()
}, blockName, false);
} else {
return renderBlock(environment, blockName, context.clone(), outputBuffer, blocks, true, sandboxed, execute, sourceMapRuntime);
return renderBlock({
...executionContext,
context: context.clone()
}, blockName, true);
}
}
});

View File

@ -5,7 +5,7 @@ import type {TwingMethodCallNode} from "../../node/expression/method-call";
import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
export const executeMethodCall: TwingNodeExecutor<TwingMethodCallNode> = async (node, executionContext) => {
const {template, context, environment, outputBuffer, aliases, nodeExecutor: execute, sandboxed, sourceMapRuntime} = executionContext;
const {template, aliases, nodeExecutor: execute} = executionContext;
const {methodName, shouldTestExistence} = node.attributes;
const {operand, arguments: methodArguments} = node.children;
@ -25,13 +25,15 @@ 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);
if (macroHandler) {
return Promise.resolve(macroHandler);
} else {
return template.getParent(environment, context, outputBuffer, sandboxed, execute)
return template.getParent(executionContext)
.then((parent) => {
if (parent) {
return getHandler(parent);
@ -45,7 +47,7 @@ export const executeMethodCall: TwingNodeExecutor<TwingMethodCallNode> = async (
return getHandler(macroTemplate)
.then((handler) => {
if (handler) {
return handler(environment, outputBuffer, sandboxed, sourceMapRuntime, execute, ...macroArguments);
return handler(executionContext, ...macroArguments);
} else {
throw createRuntimeError(`Macro "${methodName}" is not defined in template "${macroTemplate.name}".`, node, template.name);
}

View File

@ -6,7 +6,8 @@ import {getContextValue} from "../../helpers/get-context-value";
export const executeNameNode: TwingNodeExecutor<TwingNameNode> = (node, {
template,
context,
environment
environment,
strict
}) => {
const {name, isAlwaysDefined, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
@ -20,7 +21,7 @@ export const executeNameNode: TwingNodeExecutor<TwingNameNode> = (node, {
return traceableGetContextValue(
environment.charset,
template.name,
environment.isStrictVariables,
strict,
context,
name,
isAlwaysDefined,

View File

@ -3,9 +3,9 @@ import type {TwingParentFunctionNode} from "../../node/expression/parent-functio
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeParentFunction: TwingNodeExecutor<TwingParentFunctionNode> = (node, executionContext) => {
const {template, context, environment, nodeExecutor: execute, outputBuffer, sandboxed, sourceMapRuntime,} = executionContext;
const {template} = executionContext;
const {name} = node.attributes;
const renderParentBlock = getTraceableMethod(template.renderParentBlock, node.line, node.column, template.name);
return renderParentBlock(environment, name, context, outputBuffer, sandboxed, execute, sourceMapRuntime);
return renderParentBlock(executionContext, name);
};

View File

@ -5,7 +5,7 @@ import {getTraceableMethod} from "../helpers/traceable-method";
import type {TwingNameNode} from "../node/expression/name";
export const executeImportNode: TwingNodeExecutor<TwingImportNode> = async (node, executionContext) => {
const {template, environment, aliases, nodeExecutor: execute,} = executionContext;
const {template, aliases, nodeExecutor: execute,} = executionContext;
const {alias: aliasNode, templateName: templateNameNode} = node.children;
const {global} = node.attributes;
@ -19,12 +19,16 @@ export const executeImportNode: TwingNodeExecutor<TwingImportNode> = async (node
const loadTemplate = getTraceableMethod(template.loadTemplate, node.line, node.column, template.name);
aliasValue = await loadTemplate(environment, templateName);
aliasValue = await loadTemplate(executionContext, templateName);
}
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

@ -0,0 +1,79 @@
import {createTemplate, type TwingTemplate} from "./template";
import type {TwingTemplateNode} from "./node/template";
import type {TwingEnvironment} from "./environment";
/**
* Loads a template by its name.
*
* @param name The name of the template to load
* @param from The name of the template that requested the load
*/
export type TwingTemplateLoader = (name: string, from?: string | null) => Promise<TwingTemplate | null>;
export const createTemplateLoader = (environment: TwingEnvironment): TwingTemplateLoader => {
const registry: Map<string, TwingTemplate> = new Map();
return async (name, from = null) => {
const {loader} = environment;
let templateFqn = await loader.resolve(name, from) || name;
let loadedTemplate = registry.get(templateFqn);
if (loadedTemplate) {
return Promise.resolve(loadedTemplate);
} else {
const {cache} = environment;
const timestamp = cache ? await cache.getTimestamp(templateFqn) : 0;
const getAstFromCache = async (): Promise<TwingTemplateNode | null> => {
if (cache === null) {
return Promise.resolve(null);
}
let content: TwingTemplateNode | null;
const isFresh = await loader.isFresh(name, timestamp, from);
if (isFresh) {
content = await cache.load(name);
} else {
content = null;
}
return content;
};
const getAstFromLoader = async (): Promise<TwingTemplateNode | null> => {
const source = await loader.getSource(name, from);
if (source === null) {
return null;
}
const ast = environment.parse(environment.tokenize(source));
if (cache !== null) {
await cache.write(name, ast);
}
return ast;
};
let ast = await getAstFromCache();
if (ast === null) {
ast = await getAstFromLoader();
}
if (ast === null) {
return null;
}
const template = createTemplate(ast);
registry.set(templateFqn, template);
return template;
}
}
};

View File

@ -16,28 +16,21 @@ import {getTraceableMethod} from "./helpers/traceable-method";
import {TwingConstantNode} from "./node/expression/constant";
import {executeNode, type TwingNodeExecutor} from "./node-executor";
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 TwingTemplateBlockHandler = (
environment: TwingEnvironment,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
) => Promise<void>;
export type TwingTemplateBlockHandler = (executionContent: TwingExecutionContext) => Promise<void>;
export type TwingTemplateMacroHandler = (
environment: TwingEnvironment,
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
sourceMapRuntime: TwingSourceMapRuntime | undefined,
nodeExecutor: TwingNodeExecutor,
executionContent: TwingExecutionContext,
...macroArguments: Array<any>
) => Promise<TwingMarkup>;
export type TwingTemplateAliases = TwingContext<string, TwingTemplate>;
/**
* The shape of a template. A template
*/
export interface TwingTemplate {
readonly aliases: TwingTemplateAliases;
readonly ast: TwingTemplateNode;
@ -46,53 +39,52 @@ export interface TwingTemplate {
readonly source: TwingSource;
readonly macroHandlers: Map<string, TwingTemplateMacroHandler>;
readonly name: string;
displayBlock(
environment: TwingEnvironment,
executionContext: TwingExecutionContext,
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
useBlocks: boolean,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
useBlocks: boolean
): Promise<void>;
/**
* Execute the template against an environment,
*
* Theoretically speaking,
*
* @param environment
* @param context
* @param outputBuffer
* @param options
*/
execute(
environment: TwingEnvironment,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
childBlocks: TwingTemplateBlockMap,
nodeExecutor: TwingNodeExecutor,
options?: {
sandboxed?: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
blocks?: TwingTemplateBlockMap;
nodeExecutor?: TwingNodeExecutor;
sandboxed?: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
/**
* Controls whether accessing invalid variables (variables and or attributes/methods that do not exist) triggers an error.
*
* When set to `true`, accessing invalid variables triggers an error.
*/
strict?: boolean;
templateLoader?: TwingTemplateLoader
}
): Promise<void>;
getBlocks(environment: TwingEnvironment): Promise<TwingTemplateBlockMap>;
getParent(
environment: TwingEnvironment,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<TwingTemplate | null>;
getBlocks(executionContext: TwingExecutionContext): Promise<TwingTemplateBlockMap>;
getTraits(environment: TwingEnvironment): Promise<TwingTemplateBlockMap>;
getParent(executionContext: TwingExecutionContext): Promise<TwingTemplate | null>;
getTraits(executionContext: TwingExecutionContext): Promise<TwingTemplateBlockMap>;
hasBlock(
environment: TwingEnvironment,
executionContext: TwingExecutionContext,
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
blocks: TwingTemplateBlockMap
): Promise<boolean>;
hasMacro(name: string): Promise<boolean>;
@ -110,7 +102,7 @@ export interface TwingTemplate {
* @throws {TwingTemplateLoadingError} When no embedded template exists for the passed identifier.
*/
loadTemplate(
environment: TwingEnvironment,
executionContext: TwingExecutionContext,
identifier: TwingTemplate | string | Array<TwingTemplate | null>,
): Promise<TwingTemplate>;
@ -122,29 +114,24 @@ export interface TwingTemplate {
outputBuffer?: TwingOutputBuffer;
sandboxed?: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
/**
* Controls whether accessing invalid variables (variables and or attributes/methods that do not exist) triggers an error.
*
* When set to `true`, accessing invalid variables triggers an error.
*/
strict?: boolean;
}
): Promise<string>;
renderBlock(
environment: TwingEnvironment,
executionContext: TwingExecutionContext,
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
useBlocks: boolean,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
useBlocks: boolean
): Promise<string>;
renderParentBlock(
environment: TwingEnvironment,
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
executionContext: TwingExecutionContext,
name: string
): Promise<string>;
/**
@ -152,10 +139,13 @@ export interface TwingTemplate {
*
* Similar to loadTemplate() but it also accepts instances of TwingTemplate and an array of templates where each is tried to be loaded.
*
* @param environment
* @param executionContext
* @param names A template or an array of templates to try consecutively
*/
resolveTemplate(environment: TwingEnvironment, names: Array<string | TwingTemplate | null>): Promise<TwingTemplate>;
resolveTemplate(
executionContext: TwingExecutionContext,
names: Array<string | TwingTemplate | null>
): Promise<TwingTemplate>;
}
export const createTemplate = (
@ -169,18 +159,12 @@ export const createTemplate = (
const {blocks: blockNodes} = ast.children;
for (const [name, blockNode] of getChildren(blockNodes)) {
const blockHandler: TwingTemplateBlockHandler = (environment, context, outputBuffer, blocks, sandboxed, nodeExecutor, sourceMapRuntime) => {
const blockHandler: TwingTemplateBlockHandler = (executionContent) => {
const aliases = template.aliases.clone();
return nodeExecutor(blockNode.children.body, {
return executionContent.nodeExecutor(blockNode.children.body, {
...executionContent,
aliases,
blocks,
context,
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
template
});
};
@ -194,7 +178,8 @@ export const createTemplate = (
const {macros: macrosNode} = ast.children;
for (const [name, macroNode] of Object.entries(macrosNode.children)) {
const macroHandler: TwingTemplateMacroHandler = async (environment, outputBuffer, sandboxed, sourceMapRuntime, nodeExecutor, ...args) => {
const macroHandler: TwingTemplateMacroHandler = async (executionContent, ...args) => {
const {environment, nodeExecutor, outputBuffer} = executionContent;
const {body, arguments: macroArguments} = macroNode.children;
const keyValuePairs = getKeyValuePairs(macroArguments);
@ -205,15 +190,10 @@ export const createTemplate = (
for (const {key: keyNode, value: defaultValueNode} of keyValuePairs) {
const key = keyNode.attributes.value as string;
const defaultValue = await nodeExecutor(defaultValueNode, {
...executionContent,
aliases,
blocks: new Map(),
context: createContext(),
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
template
context: createContext()
});
let value = args.shift();
@ -233,14 +213,10 @@ export const createTemplate = (
outputBuffer.start();
return await nodeExecutor(body, {
...executionContent,
aliases,
blocks,
context,
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
template
})
.then(() => {
@ -269,42 +245,21 @@ export const createTemplate = (
// parent
let parent: TwingTemplate | null = null;
const displayParentBlock = (
environment: TwingEnvironment,
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
sandboxed: boolean,
nodeExecutor: TwingNodeExecutor,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<void> => {
return template.getTraits(environment)
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(
environment,
blockName,
context,
outputBuffer,
blocks,
false,
sandboxed,
nodeExecutor,
sourceMapRuntime
);
}
else {
return template.getParent(environment, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime)
return blockTemplate.displayBlock(executionContext, blockName, false);
} else {
return template.getParent(executionContext)
.then((parent) => {
if (parent !== null) {
return parent.displayBlock(environment, name, context, outputBuffer, blocks, false, sandboxed, nodeExecutor, sourceMapRuntime);
}
else {
return parent.displayBlock(executionContext, name, false);
} else {
throw createRuntimeError(`The template has no parent and no traits defining the "${name}" block.`, undefined, template.name);
}
});
@ -365,8 +320,10 @@ export const createTemplate = (
get source() {
return ast.attributes.source;
},
displayBlock: (environment, name, context, outputBuffer, blocks, useBlocks, sandboxed, nodeExecutor, sourceMapRuntime) => {
return template.getBlocks(environment)
displayBlock: (executionContext, name, useBlocks) => {
const {blocks} = executionContext;
return template.getBlocks(executionContext)
.then((ownBlocks) => {
let blockHandler: TwingTemplateBlockHandler | undefined;
let block: [TwingTemplate, string] | undefined;
@ -375,30 +332,26 @@ 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);
}
if (blockHandler) {
return blockHandler(environment, context, outputBuffer, blocks, sandboxed, nodeExecutor, sourceMapRuntime);
}
else {
return template.getParent(environment, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime).then((parent) => {
return blockHandler(executionContext);
} else {
return template.getParent(executionContext).then((parent) => {
if (parent) {
return parent.displayBlock(environment, name, context, outputBuffer, mergeIterables(ownBlocks, blocks), false, sandboxed, nodeExecutor, sourceMapRuntime);
}
else {
return parent.displayBlock(executionContext, name, false);
} 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);
}
}
@ -407,32 +360,42 @@ export const createTemplate = (
}
});
},
execute: async (environment, context, outputBuffer, childBlocks, nodeExecutor, options) => {
execute: async (environment, context, 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;
const templateLoader = options?.templateLoader || createTemplateLoader(environment);
const executionContext: TwingExecutionContext = {
aliases,
blocks: new Map(),
context,
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
strict: options?.strict || false,
template,
templateLoader
};
return Promise.all([
template.getParent(environment, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime),
template.getBlocks(environment)
template.getParent(executionContext),
template.getBlocks(executionContext)
]).then(([parent, ownBlocks]) => {
const blocks = mergeIterables(ownBlocks, childBlocks);
return nodeExecutor(ast, {
aliases,
blocks,
context,
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
template
...executionContext,
blocks
}).then(() => {
if (parent) {
return parent.execute(environment, context, outputBuffer, blocks, nodeExecutor, {
sandboxed,
sourceMapRuntime
return parent.execute(environment, context, outputBuffer, {
...options,
blocks
});
}
});
@ -448,12 +411,11 @@ export const createTemplate = (
throw error;
});
},
getBlocks: (environment) => {
getBlocks: (executionContext) => {
if (blocks) {
return Promise.resolve(blocks);
}
else {
return template.getTraits(environment)
} else {
return template.getTraits(executionContext)
.then((traits) => {
blocks = mergeIterables(traits, new Map([...blockHandlers.keys()].map((key) => {
return [key, [template, key]];
@ -463,7 +425,7 @@ export const createTemplate = (
});
}
},
getParent: async (environment, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime) => {
getParent: async (executionContext) => {
if (parent !== null) {
return Promise.resolve(parent);
}
@ -471,18 +433,14 @@ export const createTemplate = (
const parentNode = ast.children.parent;
if (parentNode) {
return template.getBlocks(environment)
const {nodeExecutor} = executionContext;
return template.getBlocks(executionContext)
.then(async (blocks) => {
const parentName = await nodeExecutor(parentNode, {
...executionContext,
aliases: createContext(),
blocks,
context,
environment,
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime,
template
blocks
});
const loadTemplate = getTraceableMethod(
@ -492,7 +450,7 @@ export const createTemplate = (
template.name
);
const loadedParent = await loadTemplate(environment, parentName);
const loadedParent = await loadTemplate(executionContext, parentName);
if (parentNode.type === "constant") {
parent = loadedParent;
@ -500,12 +458,11 @@ export const createTemplate = (
return loadedParent;
});
}
else {
} else {
return Promise.resolve(null);
}
},
getTraits: async (environment) => {
getTraits: async (executionContext) => {
if (traits === null) {
traits = new Map();
@ -522,13 +479,13 @@ export const createTemplate = (
template.name
);
const traitTemplate = await loadTemplate(environment, templateName);
const traitTemplate = await loadTemplate(executionContext, templateName);
if (!traitTemplate.canBeUsedAsATrait) {
throw createRuntimeError(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.name);
}
const traitBlocks = cloneMap(await traitTemplate.getBlocks(environment));
const traitBlocks = cloneMap(await traitTemplate.getBlocks(executionContext));
for (const [key, target] of getChildren(targets)) {
const traitBlock = traitBlocks.get(key);
@ -549,23 +506,20 @@ export const createTemplate = (
return Promise.resolve(traits);
},
hasBlock: (environment, name, context, outputBuffer, blocks, sandboxed, nodeExecutor, sourceMapRuntime): Promise<boolean> => {
hasBlock: (executionContext, name, blocks): Promise<boolean> => {
if (blocks.has(name)) {
return Promise.resolve(true);
}
else {
return template.getBlocks(environment)
} else {
return template.getBlocks(executionContext)
.then((blocks) => {
if (blocks.has(name)) {
return Promise.resolve(true);
}
else {
return template.getParent(environment, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime)
} else {
return template.getParent(executionContext)
.then((parent) => {
if (parent) {
return parent.hasBlock(environment, name, context, outputBuffer, blocks, sandboxed, nodeExecutor, sourceMapRuntime);
}
else {
return parent.hasBlock(executionContext, name, blocks);
} else {
return false;
}
});
@ -586,78 +540,80 @@ export const createTemplate = (
return Promise.resolve(createTemplate(ast));
},
loadTemplate: (environment, identifier) => {
loadTemplate: (executionContext, identifier) => {
let promise: Promise<TwingTemplate>;
if (typeof identifier === "string") {
promise = environment.loadTemplate(identifier, template.name);
}
else if (Array.isArray(identifier)) {
promise = template.resolveTemplate(environment, identifier);
}
else {
promise = executionContext.templateLoader(identifier, template.name)
.then((template) => {
if (template === null) {
throw createTemplateLoadingError([identifier]);
}
return template;
});
} else if (Array.isArray(identifier)) {
promise = template.resolveTemplate(executionContext, identifier);
} else {
promise = Promise.resolve(identifier);
}
return promise;
},
render: (environment, context, options) => {
const actualOutputBuffer: TwingOutputBuffer = options?.outputBuffer || createOutputBuffer();
const outputBuffer = options?.outputBuffer || createOutputBuffer();
actualOutputBuffer.start();
const nodeExecutor = options?.nodeExecutor || executeNode;
outputBuffer.start();
return template.execute(
environment,
createContext(iteratorToMap(context)),
actualOutputBuffer,
new Map(),
nodeExecutor,
{
sandboxed: options?.sandboxed,
sourceMapRuntime: options?.sourceMapRuntime
}
outputBuffer,
options
).then(() => {
return actualOutputBuffer.getAndFlush();
return outputBuffer.getAndFlush();
});
},
renderBlock: (environment, name, context, outputBuffer, blocks, useBlocks, sandboxed, nodeExecutor, sourceMapRuntime) => {
renderBlock: (executionContext, name, useBlocks) => {
const {outputBuffer} = executionContext;
outputBuffer.start();
return template.displayBlock(environment, name, context, outputBuffer, blocks, useBlocks, sandboxed, nodeExecutor, sourceMapRuntime).then(() => {
return template.displayBlock(executionContext, name, useBlocks).then(() => {
return outputBuffer.getAndClean();
});
},
renderParentBlock: (environment, name, context, outputBuffer, sandboxed, nodeExecutor, sourceMapRuntime) => {
renderParentBlock: (executionContext, name) => {
const {outputBuffer} = executionContext;
outputBuffer.start();
return template.getBlocks(environment)
return template.getBlocks(executionContext)
.then((blocks) => {
return displayParentBlock(environment, name, context, outputBuffer, blocks, sandboxed, nodeExecutor, sourceMapRuntime).then(() => {
return displayParentBlock({
...executionContext,
blocks
}, name).then(() => {
return outputBuffer.getAndClean();
})
});
},
resolveTemplate: (environment, names) => {
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") {
} else if (typeof name !== "string") {
return Promise.resolve(name);
}
else {
return template.loadTemplate(environment, name)
} else {
return template.loadTemplate(executionContext, name)
.catch(() => {
return loadTemplateAtIndex(index + 1);
});
}
}
else {
} else {
return Promise.reject(createTemplateLoadingError((names as Array<string | null>).map((name) => {
if (name === null) {
return '';

View File

@ -203,6 +203,10 @@ export default abstract class {
getExpectedDeprecationMessages(): string[] | null {
return null;
}
getStrict(): boolean {
return true;
}
}
/**
@ -262,12 +266,14 @@ export const runTest = async (
expectedErrorMessage,
expectedDeprecationMessages,
expectedSourceMapMappings,
sandboxed,
sandboxPolicy,
sandboxSecurityPolicyFilters,
sandboxSecurityPolicyTags,
sandboxSecurityPolicyFunctions,
sandboxSecurityPolicyMethods,
sandboxSecurityPolicyProperties,
strict,
trimmedExpectation
} = integrationTest;
@ -290,6 +296,10 @@ export const runTest = async (
if (environmentOptions.parserOptions.level === undefined) {
environmentOptions.parserOptions.level = 2;
}
if (strict === undefined) {
strict = true;
}
let environment = createEnvironment(loader, Object.assign({}, <TwingEnvironmentOptions>{
sandboxPolicy: sandboxPolicy || createSandboxSecurityPolicy({
@ -299,7 +309,6 @@ export const runTest = async (
allowedMethods: sandboxSecurityPolicyMethods,
allowedProperties: sandboxSecurityPolicyProperties
}),
strictVariables: true,
emitsSourceMap: expectedSourceMapMappings !== undefined
}, environmentOptions));
@ -323,7 +332,7 @@ export const runTest = async (
consoleData.push(data);
});
}
return (context || Promise.resolve({})).then(async (context: Record<string, any>) => {
if (!expectedErrorMessage) {
try {
@ -333,13 +342,19 @@ export const runTest = async (
let sourceMap: RawSourceMap | null = null;
if (expectedSourceMapMappings !== undefined) {
const result = await environment.renderWithSourceMap('index.twig', context);
const result = await environment.renderWithSourceMap('index.twig', context, {
sandboxed,
strict
});
actual = result.data;
sourceMap = result.sourceMap;
}
else {
actual = await environment.render('index.twig', context);
actual = await environment.render('index.twig', context, {
sandboxed,
strict
});
}
console.timeEnd(description);
@ -383,6 +398,8 @@ export const runTest = async (
same(mappings, expectedSourceMapMappings);
}
} catch (e) {
console.log(e);
console.timeEnd(description);
fail(`${description}: should not throw an error (${e})`);
@ -391,8 +408,11 @@ export const runTest = async (
else {
try {
console.time(description);
await environment.render('index.twig', context);
await environment.render('index.twig', context, {
sandboxed,
strict
});
fail(`${description}: should throw an error`);
} catch (error: any) {

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../TestBase";
import {createIntegrationTest} from "../test";
import {TwingEnvironmentOptions} from "../../../../src/lib/environment";
class Test extends TestBase {
getDescription() {
@ -34,10 +33,8 @@ class Test extends TestBase {
return 'TwingRuntimeError: Variable "foo2" does not exist in "index.twig" at line 11, column 10.';
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: true
};
getStrict(): boolean {
return true;
}
}

View File

@ -34,9 +34,9 @@ for (const [name, context, errorMessage] of testCases) {
context: Promise.resolve(context),
trimmedExpectation: strictVariables ? undefined : '',
expectedErrorMessage: strictVariables ? errorMessage : undefined,
strict: strictVariables,
environmentOptions: {
cache,
strictVariables
cache
}
});
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../TestBase";
import {createIntegrationTest} from "../test";
import {TwingEnvironmentOptions} from "../../../../src/lib/environment";
export class Test extends TestBase {
getDescription() {
@ -100,10 +99,8 @@ export class StrictVariablesSetToFalse extends Test {
return super.getDescription() + ' (strict_variables set to false)';
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
}

View File

@ -35,9 +35,9 @@ for (const [name, context, errorMessage] of testCases) {
context: Promise.resolve(context),
trimmedExpectation: sandboxed ? undefined : 'bar',
expectedErrorMessage: sandboxed ? errorMessage : undefined,
sandboxed,
environmentOptions: {
cache,
sandboxed,
sandboxPolicy: createSandboxSecurityPolicy()
}
});

View File

@ -34,9 +34,9 @@ for (const [name, context, errorMessage] of testCases) {
context: Promise.resolve(context),
trimmedExpectation: strictVariables ? undefined : '',
expectedErrorMessage: strictVariables ? errorMessage : undefined,
strict: strictVariables,
environmentOptions: {
cache,
strictVariables
cache
}
});
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../TestBase";
import {createIntegrationTest} from "../test";
import {TwingEnvironmentOptions} from "../../../../src/lib/environment";
export class Test extends TestBase {
getDescription() {
@ -70,10 +69,8 @@ x`;
};
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
}

View File

@ -35,9 +35,9 @@ for (const [name, context, errorMessage] of testCases) {
context: Promise.resolve(context),
trimmedExpectation: sandboxed ? undefined : 'bar',
expectedErrorMessage: sandboxed ? errorMessage : undefined,
sandboxed,
environmentOptions: {
cache,
sandboxed,
sandboxPolicy: createSandboxSecurityPolicy()
}
});

View File

@ -32,9 +32,9 @@ for (const [name, context, errorMessage] of testCases) {
context: Promise.resolve(context),
trimmedExpectation: strictVariables ? undefined : '',
expectedErrorMessage: strictVariables ? errorMessage : undefined,
strict: strictVariables,
environmentOptions: {
cache,
strictVariables
cache
}
});
}

View File

@ -86,10 +86,8 @@ not
}
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
}

View File

@ -17,10 +17,8 @@ class Test extends TestBase {
};
}
getEnvironmentOptions() {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
getExpected() {

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../TestBase";
import {createIntegrationTest} from "../test";
import {TwingEnvironmentOptions} from "../../../../src/lib/environment";
class Test extends TestBase {
getDescription() {
@ -25,11 +24,9 @@ I like Twing.`;
undef: undefined
};
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
}

View File

@ -153,10 +153,8 @@ ok
} as any;
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
class Test extends TestBase {
getDescription() {
@ -20,10 +19,8 @@ class Test extends TestBase {
`;
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../TestBase";
import {createIntegrationTest} from "../test";
import {TwingEnvironmentOptions} from "../../../../src/lib/environment";
class Test extends TestBase {
getDescription() {
@ -20,10 +19,8 @@ class Test extends TestBase {
`;
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
}

View File

@ -39,8 +39,8 @@ runTest({
{{ foo|e }}
{{ foo|e }}`
},
sandboxed: true,
environmentOptions: {
sandboxed: true,
sandboxPolicy: createSandboxSecurityPolicy({
allowedFunctions: ['include']
})

View File

@ -12,8 +12,6 @@ runTest({
}
}),
expectedErrorMessage: `TwingSandboxSecurityError: Tag "do" is not allowed in "index.twig" at line 1, column 4.`,
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxPolicy: createSandboxSecurityPolicy()
})

View File

@ -7,9 +7,7 @@ runTest({
{{ 5|upper }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxSecurityPolicyFilters: [
'upper'
],
@ -23,8 +21,6 @@ runTest({
{{ 5|upper }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Filter "upper" is not allowed in "index.twig" at line 2, column 6.'
});

View File

@ -7,9 +7,7 @@ runTest({
{{ dump(5) }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxSecurityPolicyFunctions: [
'dump'
],
@ -22,9 +20,7 @@ runTest({
"index.twig": `
{{ dump(5) }}
`
},
environmentOptions: {
sandboxed: true
},
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Function "dump" is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -7,9 +7,7 @@ runTest({
{{ foo.bar() }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxSecurityPolicyMethods: new Map([
[Object, ['bar']]
]),
@ -28,9 +26,7 @@ runTest({
{{ foo.bar() }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
context: Promise.resolve({
foo: {
bar: () => 5

View File

@ -5,10 +5,8 @@ runTest({
templates: {
"index.twig": `{{ foo }}`
},
environmentOptions: {
sandboxed: true,
strictVariables: false
},
sandboxed: true,
strict: false,
expectation: ''
});
@ -17,9 +15,7 @@ runTest({
templates: {
"index.twig": `{{ foo.bar() }}`
},
environmentOptions: {
sandboxed: true,
strictVariables: false
},
sandboxed: true,
strict: false,
expectation: ''
});
});

View File

@ -7,9 +7,7 @@ runTest({
{{ foo.bar }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxSecurityPolicyProperties: new Map([
[Object, ['bar']]
]),
@ -28,9 +26,7 @@ runTest({
{{ foo.bar }}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
context: Promise.resolve({
foo: {
bar: 5

View File

@ -7,9 +7,7 @@ runTest({
{% block foo %}5{% endblock %}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
sandboxSecurityPolicyTags: [
'block'
],
@ -23,8 +21,6 @@ runTest({
{% block foo %}5{% endblock %}
`
},
environmentOptions: {
sandboxed: true
},
sandboxed: true,
expectedErrorMessage: 'TwingSandboxSecurityError: Tag "block" is not allowed in "index.twig" at line 2, column 4.'
});

View File

@ -32,10 +32,8 @@ class Test extends TestBase {
};
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
getExpectedDeprecationMessages() {

View File

@ -16,10 +16,8 @@ class Test extends TestBase {
}
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
getContext() {

View File

@ -17,10 +17,8 @@ class Test extends TestBase {
}
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
getContext() {

View File

@ -16,10 +16,8 @@ class Test extends TestBase {
}
}
getEnvironmentOptions() {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
getExpected() {

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
export class EmptyString extends TestBase {
getDescription(): string {
@ -189,11 +188,9 @@ export class Undefined extends TestBase {
undefined: undefined
};
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
export class Test extends TestBase {
getDescription() {
@ -14,10 +13,8 @@ export class Test extends TestBase {
};
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
getExpectedErrorMessage() {

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
export class Test extends TestBase {
getDescription() {
@ -24,10 +23,8 @@ export class StrictVariablesSetToFalse extends Test {
return super.getDescription() + ' (strict_variables set to false)';
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
};
getStrict(): boolean {
return false;
}
getExpectedErrorMessage() {

View File

@ -9,8 +9,6 @@ runTest({
`,
'foo.twig': `{{ foo }}`
},
environmentOptions: {
strictVariables: false
},
strict: false,
expectation: ''
});
});

View File

@ -13,9 +13,7 @@ runTest({
},
sandboxSecurityPolicyFilters: ['upper'],
sandboxSecurityPolicyTags: ['sandbox', 'include'],
environmentOptions: {
sandboxed: true
},
sandboxed: true,
context: Promise.resolve({
foo: {
bar: 'foo.bar'

View File

@ -21,12 +21,14 @@ export type IntegrationTest = {
expectedSourceMapMappings?: Array<MappingItem>;
expectation?: string;
globals?: Record<string, any>;
sandboxed?: boolean;
sandboxPolicy?: TwingSandboxSecurityPolicy;
sandboxSecurityPolicyTags?: Array<string>;
sandboxSecurityPolicyFilters?: Array<string>;
sandboxSecurityPolicyFunctions?: Array<string>;
sandboxSecurityPolicyProperties?: Map<Function, Array<string>>;
sandboxSecurityPolicyMethods?: Map<Function, Array<string>>;
strict?: boolean;
trimmedExpectation?: string;
} & ({
templates: {
@ -50,7 +52,8 @@ export const createIntegrationTest = (
sandboxSecurityPolicyTags: testInstance.getSandboxSecurityPolicyTags(),
sandboxSecurityPolicyFilters: testInstance.getSandboxSecurityPolicyFilters(),
sandboxSecurityPolicyFunctions: testInstance.getSandboxSecurityPolicyFunctions(),
expectedDeprecationMessages: testInstance.getExpectedDeprecationMessages()
expectedDeprecationMessages: testInstance.getExpectedDeprecationMessages(),
strict: testInstance.getStrict()
};
};

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
class Foo {
public array: any[];
@ -138,11 +137,9 @@ export class StrictVariablesSetToFalse extends Test {
getDescription(): string {
return super.getDescription() + ' (strict_variables set to false)';
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
}
getStrict(): boolean {
return false;
}
}

View File

@ -1,6 +1,5 @@
import TestBase, {runTest} from "../../TestBase";
import {createIntegrationTest} from "../../test";
import {TwingEnvironmentOptions} from "../../../../../src/lib/environment";
export class Test extends TestBase {
getDescription() {
@ -33,10 +32,8 @@ export class StrictVariablesSetToFalse extends Test {
return super.getDescription() + ' (strict_variables set to false)';
}
getEnvironmentOptions(): TwingEnvironmentOptions {
return {
strictVariables: false
}
getStrict() {
return false;
}
}

View File

@ -162,10 +162,8 @@ export class NotDefinedTernaryNotStrictIterableTest extends TestBase {
};
}
getEnvironmentOptions() {
return {
strictVariables: false
};
getStrict() {
return false;
}
getExpected() {
@ -184,10 +182,8 @@ export class NotDefinedIfNotStrictIterableTest extends TestBase {
};
}
getEnvironmentOptions() {
return {
strictVariables: false
};
getStrict() {
return false;
}
getExpected() {
@ -212,10 +208,8 @@ export class UndefinedTernaryNotStrictIterableTest extends TestBase {
}
}
getEnvironmentOptions() {
return {
strictVariables: false
};
getStrict() {
return false;
}
getExpected() {
@ -240,10 +234,8 @@ export class UndefinedIfNotStrictIterableTest extends TestBase {
}
}
getEnvironmentOptions() {
return {
strictVariables: false
};
getStrict() {
return false;
}
getExpected() {

View File

@ -126,7 +126,8 @@ tape('library index', ({same, end}) => {
'createSourceMapRuntime',
'createTemplate',
'createTest',
'executeNode'
'executeNode',
'createTemplateLoader'
];
const propertyNames = Object.getOwnPropertyNames(index).filter((name) => name !== '__esModule');

View File

@ -2,27 +2,8 @@ import * as tape from "tape";
import {createEnvironment} from "../../../../../src/lib/environment";
import {createArrayLoader} from "../../../../../src/lib/loader/array";
import {Settings} from "luxon";
import {spy, stub} from "sinon";
import {createSource} from "../../../../../src/lib/source";
import {TwingCache} from "../../../../../src/lib/cache";
import {TwingTemplateNode} from "../../../../../src/lib/node/template";
const createMockCache = (): TwingCache => {
return {
write: () => {
return Promise.resolve();
},
load: () => {
return Promise.resolve(null);
},
getTimestamp: () => {
return Promise.resolve(0);
}
};
};
// todo: unit test every property because this is the public API
import "./load-template";
import "./loader";
tape('createEnvironment ', ({test}) => {
@ -32,7 +13,6 @@ tape('createEnvironment ', ({test}) => {
const environment = createEnvironment(createArrayLoader({}));
same(environment.isStrictVariables, false);
same(environment.charset, 'UTF-8');
same(environment.dateFormat, 'F j, Y H:i');
same(environment.numberFormat, {
@ -44,168 +24,6 @@ tape('createEnvironment ', ({test}) => {
end();
});
test('autoReload', ({test}) => {
test('when enabled', ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const cache = createMockCache();
const getEnvironment = () => createEnvironment(
loader,
{
autoReload: true,
cache
}
);
let count: number = -1;
const cachedTemplates: Map<string, TwingTemplateNode> = new Map();
stub(loader, "getSource").callsFake(() => {
return Promise.resolve(createSource('foo', `${count}`));
});
const isFreshStub = stub(loader, "isFresh").callsFake(() => {
count++;
const isFresh = count !== 1;
return Promise.resolve(isFresh);
});
const loadStub = stub(cache, "load").callsFake((key) => {
return Promise.resolve(cachedTemplates.get(key) || null);
});
const writeStub = stub(cache, "write").callsFake((key, content) => {
cachedTemplates.set(key, content);
return Promise.resolve();
});
const environment = getEnvironment();
return environment.loadTemplate('foo')
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then((content) => {
same(content, '1');
same(isFreshStub.callCount, 3);
same(loadStub.callCount, 2);
same(writeStub.callCount, 2);
})
.finally(end);
});
test('when disabled, always hit the cache', ({test}) => {
const testCases = [false, undefined];
for (const testCase of testCases) {
test(`${testCase === undefined ? 'default' : 'false'}`, ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const cache = createMockCache();
const getEnvironment = () => createEnvironment(
loader,
{
autoReload: testCase,
cache
}
);
let count: number = -1;
const cachedTemplates: Map<string, TwingTemplateNode> = new Map();
stub(loader, "getSource").callsFake(() => {
return Promise.resolve(createSource('foo', `${count}`));
});
const isFreshStub = stub(loader, "isFresh").callsFake(() => {
count++;
const isFresh = count !== 1;
return Promise.resolve(isFresh);
});
const loadStub = stub(cache, "load").callsFake((key) => {
return Promise.resolve(cachedTemplates.get(key) || null);
});
const writeStub = stub(cache, "write").callsFake((key, content) => {
cachedTemplates.set(key, content);
return Promise.resolve();
});
const environment = getEnvironment();
return environment.loadTemplate('foo')
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then((content) => {
same(content, '-1');
same(isFreshStub.callCount, 0);
same(loadStub.callCount, 3);
same(writeStub.callCount, 1);
})
.finally(end);
});
}
});
test('when no options is passed', ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const getEnvironment = () => createEnvironment(
loader
);
const isFreshStub = stub(loader, "isFresh").callsFake(() => {
return Promise.resolve(false);
});
const environment = getEnvironment();
return environment.loadTemplate('foo')
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then(() => {
return getEnvironment().loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then(() => {
same(isFreshStub.callCount, 0);
})
.finally(end);
});
});
});
test('render', ({test}) => {
@ -223,64 +41,4 @@ tape('createEnvironment ', ({test}) => {
.finally(end);
})
});
test('on', ({test}) => {
test('load', ({same, end}) => {
const environment = createEnvironment(
createArrayLoader({
foo: '{{ include("bar") }}',
bar: 'bar'
}),
{}
);
const loadedTemplates: Array<string> = [];
environment.on("load", (template) => {
loadedTemplates.push(template);
});
return environment.loadTemplate('foo')
.then(() => {
same(loadedTemplates, ['foo']);
})
.finally(end);
});
});
test('loadTemplate', ({test}) => {
test('always hits the internal cache', ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const cache = createMockCache();
const getEnvironment = () => createEnvironment(
loader,
{
cache
}
);
const getSourceContextSpy = spy(loader, "getSource");
const environment = getEnvironment();
return environment.loadTemplate('foo')
.then(() => {
return environment.loadTemplate('foo');
})
.then(() => {
return environment.loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then((content) => {
same(content, 'bar');
same(getSourceContextSpy.callCount, 1);
})
.finally(end);
});
});
});

View File

@ -1,69 +0,0 @@
import * as tape from "tape";
import {createEnvironment} from "../../../../../src/lib/environment";
import {createFilesystemLoader, TwingFilesystemLoaderFilesystem} from "../../../../../src/lib/loader/filesystem";
import {spy} from "sinon";
import {createArrayLoader} from "../../../../../src/lib/loader/array";
tape('createEnvironment::loadTemplate', ({test}) => {
test('cache the loaded template under it fully qualified name', ({same, end}) => {
const fileSystem: TwingFilesystemLoaderFilesystem = {
readFile(_path, callback) {
callback(null, Buffer.from(''));
},
stat(path, callback) {
callback(null, path === 'foo/bar' ? {
isFile() {
return true;
},
mtime: new Date(0)
} : null);
}
};
const loader = createFilesystemLoader(fileSystem);
loader.addPath('foo', '@Foo');
loader.addPath('foo', 'Bar');
const environment = createEnvironment(loader);
const getSourceSpy = spy(loader, "getSource");
return environment.loadTemplate('@Foo/bar')
.then(() => {
return Promise.all([
environment.loadTemplate('foo/bar'),
environment.loadTemplate('./foo/bar'),
environment.loadTemplate('../foo/bar', 'there/index.html'),
environment.loadTemplate('Bar/bar'),
]).then(() => {
same(getSourceSpy.callCount, 1);
});
})
.finally(end);
});
test('emits a "load" event', ({same, end}) => {
const environment = createEnvironment(createArrayLoader({
index: `{{ include("partial") }}`,
partial: ``
}));
const loadedTemplates: Array<[string, string | null]> = [];
environment.on("load", (name, from) => {
loadedTemplates.push([name, from]);
});
return environment.loadTemplate(('index'))
.then((template) => {
return template.render(environment, {});
})
.then(() => {
same(loadedTemplates, [
['index', null],
['partial', 'index']
]);
})
.finally(end);
});
});

View File

@ -8,3 +8,4 @@ import "./node-traverser";
import "./output-buffer";
import "./parser";
import "./template";
import "./template-loader";

View File

@ -0,0 +1,135 @@
import * as tape from "tape";
import {createEnvironment} from "../../../../../src/lib/environment";
import {createFilesystemLoader, TwingFilesystemLoaderFilesystem} from "../../../../../src/lib/loader/filesystem";
import {spy, stub} from "sinon";
import {createTemplateLoader} from "../../../../../src/lib/template-loader";
import {createArrayLoader} from "../../../../../src/lib/loader/array";
import type {TwingCache} from "../../../../../src/lib/cache";
const createMockCache = (): TwingCache => {
return {
write: () => {
return Promise.resolve();
},
load: () => {
return Promise.resolve(null);
},
getTimestamp: () => {
return Promise.resolve(0);
}
};
};
tape('createTemplateLoader::()', ({test}) => {
test('cache the loaded template under it fully qualified name', ({same, end}) => {
const fileSystem: TwingFilesystemLoaderFilesystem = {
readFile(_path, callback) {
callback(null, Buffer.from(''));
},
stat(path, callback) {
callback(null, path === 'foo/bar' ? {
isFile() {
return true;
},
mtime: new Date(0)
} : null);
}
};
const loader = createFilesystemLoader(fileSystem);
loader.addPath('foo', '@Foo');
loader.addPath('foo', 'Bar');
const environment = createEnvironment(loader);
const loadTemplate = createTemplateLoader(environment);
const getSourceSpy = spy(loader, "getSource");
return loadTemplate('@Foo/bar')
.then(() => {
return Promise.all([
loadTemplate('foo/bar'),
loadTemplate('./foo/bar'),
loadTemplate('../foo/bar', 'there/index.html'),
loadTemplate('Bar/bar'),
]).then(() => {
same(getSourceSpy.callCount, 1);
});
})
.finally(end);
});
test('hits the loader when the templates is considered as dirty', ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const cache = createMockCache();
stub(loader, "isFresh").resolves(false);
const loadSpy = spy(cache, "load");
const getSourceSpy = spy(loader, "getSource");
const environment = createEnvironment(
loader,
{
cache
}
);
const loadTemplate = createTemplateLoader(environment);
return loadTemplate('foo')
.then(() => {
return loadTemplate('foo');
})
.then(() => {
return loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then((content) => {
same(content, 'bar');
same(loadSpy.callCount, 0);
same(getSourceSpy.callCount, 1);
})
.finally(end);
});
test('hits the cache when the templates is considered as fresh', ({same, end}) => {
const loader = createArrayLoader({
foo: 'bar'
});
const cache = createMockCache();
const loadSpy = spy(cache, "load");
const getSourceSpy = spy(loader, "getSource");
const environment = createEnvironment(
loader,
{
cache
}
);
const loadTemplate = createTemplateLoader(environment);
return loadTemplate('foo')
.then(() => {
return loadTemplate('foo');
})
.then(() => {
stub(loader, "isFresh").resolves(true);
return loadTemplate('foo');
})
.then((template) => {
return template?.render(environment, {});
})
.then((content) => {
same(content, 'bar');
same(loadSpy.callCount, 1);
same(getSourceSpy.callCount, 1);
})
.finally(end);
});
});

View File

@ -12,6 +12,7 @@ import {createSourceMapRuntime} from "../../../../../src/lib/source-map-runtime"
import {executeNode, type TwingNodeExecutor} from "../../../../../src/lib/node-executor";
import {createTextNode} from "../../../../../src/lib/node/text";
import {createVerbatimNode} from "../../../../../src/lib/node/verbatim";
import {createTemplateLoader, type TwingTemplateLoader} from "../../../../../src/lib/template-loader";
tape('createTemplate => ::execute', ({test}) => {
test('executes the AST according to the passed options', ({test}) => {
@ -34,12 +35,18 @@ tape('createTemplate => ::execute', ({test}) => {
const template = createTemplate(ast);
return template.execute(environment, createContext(), createOutputBuffer(), new Map(), executeNodeSpy)
.then(() => {
same(executeNodeSpy.firstCall.args[1].sandboxed, false);
same(executeNodeSpy.firstCall.args[1].sourceMapRuntime, undefined);
})
.finally(end);
return template.execute(
environment,
createContext(),
createOutputBuffer(),
{
blocks: new Map(),
nodeExecutor: executeNodeSpy
}
).then(() => {
same(executeNodeSpy.firstCall.args[1].sandboxed, false);
same(executeNodeSpy.firstCall.args[1].sourceMapRuntime, undefined);
}).finally(end);
});
test('when some options are passed', ({same, end}) => {
@ -63,10 +70,17 @@ tape('createTemplate => ::execute', ({test}) => {
const sourceMapRuntime = createSourceMapRuntime();
return template.execute(environment, createContext(), createOutputBuffer(), new Map(), executeNodeSpy, {
sandboxed: true,
sourceMapRuntime
}).then(() => {
return template.execute(
environment,
createContext(),
createOutputBuffer(),
{
blocks: new Map(),
nodeExecutor: executeNodeSpy,
sandboxed: true,
sourceMapRuntime
}
).then(() => {
same(executeNodeSpy.firstCall.args[1].sandboxed, true);
same(executeNodeSpy.firstCall.args[1].sourceMapRuntime, sourceMapRuntime);
}).finally(end);
@ -96,7 +110,8 @@ tape('createTemplate => ::execute', ({test}) => {
executionContext.outputBuffer.echo('foo');
return Promise.resolve();
} else {
}
else {
return executeNode(node, executionContext);
}
};
@ -106,8 +121,46 @@ tape('createTemplate => ::execute', ({test}) => {
outputBuffer.start();
return template.execute(environment, createContext(), outputBuffer, new Map(), nodeExecutor).then(() => {
return template.execute(environment, createContext(), outputBuffer, {
nodeExecutor
}).then(() => {
same(outputBuffer.getContents(), 'foo5');
}).finally(end);
});
test('honors the passed template loader', ({same, end}) => {
const environment = createEnvironment(createArrayLoader({
bar: 'BAR',
foo: 'FOO'
}));
const ast = environment.parse(environment.tokenize(createSource('index', `{{ include("foo") }}{{ include("bar") }}`)));
const loadedTemplates: Array<string> = [];
const baseTemplateLoader = createTemplateLoader(environment);
const templateLoader: TwingTemplateLoader = (name, from) => {
loadedTemplates.push(`${from}::${name}`);
return baseTemplateLoader(name, from);
}
const template = createTemplate(ast);
const outputBuffer = createOutputBuffer();
outputBuffer.start();
return template.execute(
environment,
createContext(),
outputBuffer,
{
templateLoader
}
).then(() => {
same(loadedTemplates, [
'index::foo',
'index::bar'
]);
}).finally(end);
});
});