mirror of
https://gitlab.com/nightlycommit/twing.git
synced 2025-01-18 08:46:50 +02:00
Merge branch 'issue-608' into 'milestone/7.0.0'
Resolve issue #608 - Reloading a changed Template from FileSystem without restarting app? See merge request nightlycommit/twing!602
This commit is contained in:
commit
84d734d233
@ -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";
|
||||
|
@ -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
|
||||
});
|
||||
})
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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(() => {
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
})
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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
|
||||
)
|
||||
})
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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'));
|
||||
}
|
||||
};
|
||||
|
79
src/lib/template-loader.ts
Normal file
79
src/lib/template-loader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
@ -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 '';
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
});
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -86,10 +86,8 @@ not
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,8 @@ class Test extends TestBase {
|
||||
};
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
};
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getExpected() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,10 +153,8 @@ ok
|
||||
} as any;
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,8 +39,8 @@ runTest({
|
||||
{{ foo|e }}
|
||||
{{ foo|e }}`
|
||||
},
|
||||
sandboxed: true,
|
||||
environmentOptions: {
|
||||
sandboxed: true,
|
||||
sandboxPolicy: createSandboxSecurityPolicy({
|
||||
allowedFunctions: ['include']
|
||||
})
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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.'
|
||||
});
|
||||
|
@ -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.'
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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: ''
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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.'
|
||||
});
|
||||
|
@ -32,10 +32,8 @@ class Test extends TestBase {
|
||||
};
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getExpectedDeprecationMessages() {
|
||||
|
@ -16,10 +16,8 @@ class Test extends TestBase {
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getContext() {
|
||||
|
@ -17,10 +17,8 @@ class Test extends TestBase {
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getContext() {
|
||||
|
@ -16,10 +16,8 @@ class Test extends TestBase {
|
||||
}
|
||||
}
|
||||
|
||||
getEnvironmentOptions() {
|
||||
return {
|
||||
strictVariables: false
|
||||
}
|
||||
getStrict(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getExpected() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -9,8 +9,6 @@ runTest({
|
||||
`,
|
||||
'foo.twig': `{{ foo }}`
|
||||
},
|
||||
environmentOptions: {
|
||||
strictVariables: false
|
||||
},
|
||||
strict: false,
|
||||
expectation: ''
|
||||
});
|
||||
});
|
||||
|
@ -13,9 +13,7 @@ runTest({
|
||||
},
|
||||
sandboxSecurityPolicyFilters: ['upper'],
|
||||
sandboxSecurityPolicyTags: ['sandbox', 'include'],
|
||||
environmentOptions: {
|
||||
sandboxed: true
|
||||
},
|
||||
sandboxed: true,
|
||||
context: Promise.resolve({
|
||||
foo: {
|
||||
bar: 'foo.bar'
|
||||
|
@ -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()
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -126,7 +126,8 @@ tape('library index', ({same, end}) => {
|
||||
'createSourceMapRuntime',
|
||||
'createTemplate',
|
||||
'createTest',
|
||||
'executeNode'
|
||||
'executeNode',
|
||||
'createTemplateLoader'
|
||||
];
|
||||
|
||||
const propertyNames = Object.getOwnPropertyNames(index).filter((name) => name !== '__esModule');
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -8,3 +8,4 @@ import "./node-traverser";
|
||||
import "./output-buffer";
|
||||
import "./parser";
|
||||
import "./template";
|
||||
import "./template-loader";
|
||||
|
135
test/tests/unit/lib/template-loader/index.ts
Normal file
135
test/tests/unit/lib/template-loader/index.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user