Resolve issue #609

This commit is contained in:
Eric MORAND 2024-03-08 17:46:33 +01:00
parent 77727ba1ad
commit e55bb6a14c
148 changed files with 1925 additions and 1572 deletions

View File

@ -47,7 +47,6 @@ export type {TwingApplyNode, TwingApplyNodeAttributes, TwingApplyNodeChildren} f
export type {TwingAutoEscapeNode, TwingAutoEscapeNodeAttributes} from "./lib/node/auto-escape";
export type {TwingBlockNode, TwingBlockNodeAttributes} from "./lib/node/block";
export type {TwingBlockReferenceNode, TwingBlockReferenceNodeAttributes} from "./lib/node/block-reference";
export type {TwingBodyNode} from "./lib/node/body";
export type {TwingCheckSecurityNode, TwingCheckSecurityNodeAttributes} from "./lib/node/check-security";
export type {TwingCheckToStringNode} from "./lib/node/check-to-string";
export type {TwingCommentNode, TwingCommentNodeAttributes} from "./lib/node/comment";
@ -75,13 +74,11 @@ export type {TwingTextNode, TwingBaseTextNode, TwingBaseTextNodeAttributes} from
export type {TwingTraitNode} from "./lib/node/trait";
export type {TwingVerbatimNode} from "./lib/node/verbatim";
export type {TwingWithNode, TwingWithNodeAttributes, TwingWithNodeChildren} from "./lib/node/with";
export type {TwingWrapperNode, TwingWrapperNodeChildren} from "./lib/node/wrapper";
export {createApplyNode} from "./lib/node/apply";
export {createAutoEscapeNode} from "./lib/node/auto-escape";
export {createBlockNode} from "./lib/node/block";
export {createBlockReferenceNode} from "./lib/node/block-reference";
export {createBodyNode} from "./lib/node/body";
export {createCheckSecurityNode} from "./lib/node/check-security";
export {createCheckToStringNode} from "./lib/node/check-to-string";
export {createCommentNode} from "./lib/node/comment";
@ -104,7 +101,6 @@ export {createTextNode} from "./lib/node/text";
export {createTraitNode} from "./lib/node/trait";
export {createVerbatimNode} from "./lib/node/verbatim";
export {createWithNode} from "./lib/node/with";
export {createWrapperNode} from "./lib/node/wrapper";
// node/expression
export type {TwingBaseArrayNode, TwingArrayNode} from "./lib/node/expression/array";
@ -237,6 +233,9 @@ export type {TwingIncludeNode, TwingIncludeNodeChildren} from "./lib/node/includ
export {createEmbedNode} from "./lib/node/include/embed";
export {createIncludeNode} from "./lib/node/include/include";
// node executors
export {executeNode, type TwingNodeExecutor} from "./lib/node-executor";
// tag handlers
export type {TwingTagHandler, TwingTokenParser} from "./lib/tag-handler";
@ -308,7 +307,7 @@ export {createExtensionSet} from "./lib/extension-set";
export {createFilter} from "./lib/filter";
export {createFunction} from "./lib/function";
export {createLexer} from "./lib/lexer";
export {createBaseNode, getChildren, getChildrenCount} from "./lib/node";
export {createBaseNode, createNode, getChildren, getChildrenCount} from "./lib/node";
export {createOperator} from "./lib/operator";
export {createSandboxSecurityPolicy} from "./lib/sandbox/security-policy";
export {createSource} from "./lib/source";

View File

@ -345,9 +345,7 @@ export const createEnvironment = (
},
leaveNode: (node) => {
if (node.type === "template") {
const autoEscapeNode = createAutoEscapeNode(strategy, node.children.body.children.content, node.line, node.column, 'foo');
node.children.body.children.content = autoEscapeNode;
node.children.body = createAutoEscapeNode(strategy, node.children.body, node.line, node.column);
}
return node;

View File

@ -3,6 +3,7 @@ import type {TwingContext} from "./context";
import type {TwingOutputBuffer} from "./output-buffer";
import type {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingNumberFormat} from "./environment";
import type {TwingNodeExecutor} from "./node-executor";
export type TwingExecutionContext = {
aliases: TwingTemplateAliases;
@ -12,6 +13,7 @@ export type TwingExecutionContext = {
dateFormat: string;
dateIntervalFormat: string;
isStrictVariables: boolean;
nodeExecutor: TwingNodeExecutor;
numberFormat: TwingNumberFormat;
outputBuffer: TwingOutputBuffer;
sandboxed: boolean;

View File

@ -34,7 +34,7 @@ export const include: TwingCallable<[
ignoreMissing,
sandboxed
): Promise<TwingMarkup> => {
const {template, charset, context, outputBuffer, sourceMapRuntime} = executionContext;
const {template, charset, context, nodeExecutor, outputBuffer, sourceMapRuntime} = executionContext;
const from = template.name;
if (!isPlainObject(variables) && !isTraversable(variables)) {
@ -73,6 +73,7 @@ export const include: TwingCallable<[
return template.render(
createContext(variables),
{
nodeExecutor,
outputBuffer,
sandboxed,
sourceMapRuntime: sourceMapRuntime || undefined

View File

@ -0,0 +1,18 @@
import type {TwingBaseArrayNode} from "../node/expression/array";
import {TwingConstantNode} from "../node/expression/constant";
import {TwingBaseExpressionNode} from "../node/expression";
const array_chunk = require('locutus/php/array/array_chunk');
export const getKeyValuePairs = (
node: TwingBaseArrayNode<any>
): Array<{
key: TwingConstantNode,
value: TwingBaseExpressionNode
}> => {
const chunks: Array<[key: TwingConstantNode, value: TwingBaseExpressionNode]> = array_chunk(Object.values(node.children), 2);
return chunks.map(([key, value]) => {
return {key, value};
});
};

204
src/lib/node-executor.ts Normal file
View File

@ -0,0 +1,204 @@
import type {TwingBaseNode} from "./node";
import type {TwingExecutionContext} from "./execution-context";
import {executeBinaryNode} from "./node-executor/expression/binary";
import {executeTemplateNode} from "./node-executor/template";
import {executePrintNode} from "./node-executor/print";
import {executeTextNode} from "./node-executor/text";
import {executeCallNode} from "./node-executor/expression/call";
import {executeMethodCall} from "./node-executor/expression/method-call";
import {executeAssignmentNode} from "./node-executor/expression/assignment";
import {executeImportNode} from "./node-executor/import";
import {executeParentFunction} from "./node-executor/expression/parent-function";
import {executeBlockFunction} from "./node-executor/expression/block-function";
import {executeBlockReferenceNode} from "./node-executor/block-reference";
import {executeUnaryNode} from "./node-executor/expression/unary";
import {executeArrayNode} from "./node-executor/expression/array";
import {executeHashNode} from "./node-executor/expression/hash";
import {executeAttributeAccessorNode} from "./node-executor/expression/attribute-accessor";
import {executeNameNode} from "./node-executor/expression/name";
import {executeSetNode} from "./node-executor/set";
import {executeIfNode} from "./node-executor/if";
import {executeForNode} from "./node-executor/for";
import {executeForLoopNode} from "./node-executor/for-loop";
import {executeCheckToStringNode} from "./node-executor/check-to-string";
import {executeConditionalNode} from "./node-executor/expression/conditional";
import {executeEmbedNode} from "./node-executor/include/embed";
import {executeIncludeNode} from "./node-executor/include/include";
import {executeWithNode} from "./node-executor/with";
import {executeSpacelessNode} from "./node-executor/spaceless";
import {executeApplyNode} from "./node-executor/apply";
import {executeEscapeNode} from "./node-executor/expression/escape";
import {executeArrowFunctionNode} from "./node-executor/expression/arrow-function";
import {executeSandboxNode} from "./node-executor/sandbox";
import {executeDoNode} from "./node-executor/do";
import {executeDeprecatedNode} from "./node-executor/deprecated";
import {executeSpreadNode} from "./node-executor/expression/spread";
import {executeCheckSecurityNode} from "./node-executor/check-security";
import {executeFlushNode} from "./node-executor/flush";
import {createRuntimeError} from "./error/runtime";
import {executeConstantNode} from "./node-executor/constant";
import {executeLineNode} from "./node-executor/line";
import {executeCommentNode} from "./node-executor/comment";
import {executeBaseNode} from "./node-executor/base";
export type TwingNodeExecutor<Node extends TwingBaseNode = TwingBaseNode> = (
node: Node,
executionContext: TwingExecutionContext
) => Promise<any>;
const binaryNodeTypes = ["add", "and", "bitwise_and", "bitwise_or", "bitwise_xor", "concatenate", "divide", "divide_and_floor", "ends_with", "has_every", "has_some", "is_equal_to", "is_greater_than", "is_greater_than_or_equal_to", "is_in", "is_less_than", "is_less_than_or_equal_to", "is_not_equal_to", "is_not_in", "matches", "modulo", "multiply", "or", "power", "range", "spaceship", "starts_with", "subtract"];
const isABinaryNode = (node: TwingBaseNode): boolean => {
return binaryNodeTypes.includes(node.type);
};
const unaryNodeTypes = ["negative", "not", "positive"];
const isAUnaryNode = (node: TwingBaseNode): boolean => {
return unaryNodeTypes.includes(node.type);
};
const callNodeTypes = ["filter", "function", "test"];
const isACallNode = (node: TwingBaseNode): boolean => {
return callNodeTypes.includes(node.type);
};
/**
* Execute the passed node against the passed execution context.
*
* @param node The node to execute
* @param executionContext The context the node is executed against
*/
export const executeNode: TwingNodeExecutor = (node, executionContext) => {
let executor: TwingNodeExecutor<any>;
if (isABinaryNode(node)) {
executor = executeBinaryNode;
}
else if (isACallNode(node)) {
executor = executeCallNode;
}
else if (isAUnaryNode(node)) {
executor = executeUnaryNode;
}
else if (node.type === null) {
executor = executeBaseNode;
}
else if (node.type === "apply") {
executor = executeApplyNode;
}
else if (node.type === "array") {
executor = executeArrayNode;
}
else if (node.type === "arrow_function") {
executor = executeArrowFunctionNode;
}
else if (node.type === "assignment") {
executor = executeAssignmentNode;
}
else if (node.type === "attribute_accessor") {
executor = executeAttributeAccessorNode;
}
else if (node.type === "block_function") {
executor = executeBlockFunction;
}
else if (node.type === "block_reference") {
executor = executeBlockReferenceNode;
}
else if (node.type === "check_security") {
executor = executeCheckSecurityNode;
}
else if (node.type === "check_to_string") {
executor = executeCheckToStringNode;
}
else if (node.type === "comment") {
executor = executeCommentNode;
}
else if (node.type === "conditional") {
executor = executeConditionalNode;
}
else if (node.type === "constant") {
executor = executeConstantNode;
}
else if (node.type === "deprecated") {
executor = executeDeprecatedNode;
}
else if (node.type === "do") {
executor = executeDoNode;
}
else if (node.type === "embed") {
executor = executeEmbedNode;
}
else if (node.type === "escape") {
executor = executeEscapeNode;
}
else if (node.type === "flush") {
executor = executeFlushNode;
}
else if (node.type === "for") {
executor = executeForNode;
}
else if (node.type === "for_loop") {
executor = executeForLoopNode;
}
else if (node.type === "hash") {
executor = executeHashNode;
}
else if (node.type === "if") {
executor = executeIfNode;
}
else if (node.type === "import") {
executor = executeImportNode;
}
else if (node.type === "include") {
executor = executeIncludeNode;
}
else if (node.type === "line") {
executor = executeLineNode;
}
else if (node.type === "method_call") {
executor = executeMethodCall;
}
else if (node.type === "name") {
executor = executeNameNode;
}
else if (node.type === "nullish_coalescing") {
executor = executeConditionalNode;
}
else if (node.type === "parent_function") {
executor = executeParentFunction;
}
else if (node.type === "print") {
executor = executePrintNode;
}
else if (node.type === "sandbox") {
executor = executeSandboxNode;
}
else if (node.type === "set") {
executor = executeSetNode;
}
else if (node.type === "spaceless") {
executor = executeSpacelessNode;
}
else if (node.type === "spread") {
executor = executeSpreadNode;
}
else if (node.type === "template") {
executor = executeTemplateNode;
}
else if (node.type === "text") {
executor = executeTextNode;
}
else if (node.type === "verbatim") {
executor = executeTextNode;
}
else if (node.type === "with") {
executor = executeWithNode;
}
else {
return Promise.reject(createRuntimeError(`Unrecognized node of type "${node.type}"`, node));
}
return executor(node, executionContext);
};

View File

@ -0,0 +1,31 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingApplyNode} from "../node/apply";
import {getKeyValuePairs} from "../helpers/get-key-value-pairs";
import {createFilterNode} from "../node/expression/call/filter";
import {createConstantNode} from "../node/expression/constant";
export const executeApplyNode: TwingNodeExecutor<TwingApplyNode> = (node, executionContext) => {
const {outputBuffer, nodeExecutor: execute} = executionContext;
const {body, filters} = node.children;
const {line, column} = node;
outputBuffer.start();
return execute(body, executionContext)
.then(async () => {
let content = outputBuffer.getAndClean();
const keyValuePairs = getKeyValuePairs(filters);
while (keyValuePairs.length > 0) {
const {key, value: filterArguments} = keyValuePairs.pop()!;
const filterName = key.attributes.value as string;
const filterNode = createFilterNode(createConstantNode(content, line, column), filterName, filterArguments, line, column);
content = await execute(filterNode, executionContext);
}
outputBuffer.echo(content);
});
};

View File

@ -0,0 +1,12 @@
import type {TwingNodeExecutor} from "../node-executor";
export const executeBaseNode: TwingNodeExecutor = async (node, executionContext) => {
const output: Array<any> = [];
const {nodeExecutor: execute} = executionContext;
for (const [, child] of Object.entries(node.children)) {
output.push(await execute(child, executionContext));
}
return output;
};

View File

@ -0,0 +1,29 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingBlockReferenceNode} from "../node/block-reference";
import {getTraceableMethod} from "../helpers/traceable-method";
export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNode> = (node, executionContext) => {
const {
template,
context,
outputBuffer,
blocks,
nodeExecutor: execute,
sandboxed,
sourceMapRuntime
} = executionContext;
const {name} = node.attributes;
const renderBlock = getTraceableMethod(template.renderBlock, node.line, node.column, template.name);
return renderBlock(
name,
context.clone(),
outputBuffer,
blocks,
true,
sandboxed,
execute,
sourceMapRuntime
).then(outputBuffer.echo);
};

View File

@ -0,0 +1,42 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingCheckSecurityNode} from "../node/check-security";
import {
isASandboxSecurityNotAllowedFilterError,
TwingSandboxSecurityNotAllowedFilterError
} from "../sandbox/security-not-allowed-filter-error";
import {TwingSandboxSecurityNotAllowedFunctionError} from "../sandbox/security-not-allowed-function-error";
import {
isASandboxSecurityNotAllowedTagError,
TwingSandboxSecurityNotAllowedTagError
} from "../sandbox/security-not-allowed-tag-error";
export const executeCheckSecurityNode: TwingNodeExecutor<TwingCheckSecurityNode> = (node, executionContext) => {
const {template, sandboxed} = executionContext;
const {usedTags, usedFunctions, usedFilters} = node.attributes;
try {
sandboxed && template.checkSecurity(
[...usedTags.keys()],
[...usedFilters.keys()],
[...usedFunctions.keys()]
);
} catch (error: any) {
const supplementError = (error: TwingSandboxSecurityNotAllowedFilterError | TwingSandboxSecurityNotAllowedFunctionError | TwingSandboxSecurityNotAllowedTagError) => {
error.source = template.name;
if (isASandboxSecurityNotAllowedTagError(error)) {
error.location = usedTags.get(error.tagName);
} else if (isASandboxSecurityNotAllowedFilterError(error)) {
error.location = usedFilters.get(error.filterName)
} else {
error.location = usedFunctions.get(error.functionName);
}
}
supplementError(error);
throw error;
}
return Promise.resolve();
};

View File

@ -0,0 +1,29 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingCheckToStringNode} from "../node/check-to-string";
import {getTraceableMethod} from "../helpers/traceable-method";
export const executeCheckToStringNode: TwingNodeExecutor<TwingCheckToStringNode> = (node, executionContext) => {
const {template, nodeExecutor: execute, sandboxed} = executionContext;
const {value: valueNode} = node.children;
return execute(valueNode, executionContext)
.then((value) => {
if (sandboxed) {
const assertToStringAllowed = getTraceableMethod((value: any) => {
if ((value !== null) && (typeof value === 'object')) {
try {
template.checkMethodAllowed(value, 'toString');
} catch (error) {
return Promise.reject(error);
}
}
return Promise.resolve(value);
}, valueNode.line, valueNode.column, template.name)
return assertToStringAllowed(value);
}
return value;
});
};

View File

@ -0,0 +1,6 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingCommentNode} from "../node/comment";
export const executeCommentNode: TwingNodeExecutor<TwingCommentNode> = () => {
return Promise.resolve();
};

View File

@ -0,0 +1,6 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingConstantNode} from "../node/expression/constant";
export const executeConstantNode: TwingNodeExecutor<TwingConstantNode> = (node) => {
return Promise.resolve(node.attributes.value);
};

View File

@ -0,0 +1,12 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingDeprecatedNode} from "../node/deprecated";
export const executeDeprecatedNode: TwingNodeExecutor<TwingDeprecatedNode> = (node, executionContext) => {
const {template, nodeExecutor: execute} = executionContext;
const {message} = node.children;
return execute(message, executionContext)
.then((message) => {
console.warn(`${message} ("${template.name}" at line ${node.line}, column ${node.column})`);
});
};

View File

@ -0,0 +1,6 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingDoNode} from "../node/do";
export const executeDoNode: TwingNodeExecutor<TwingDoNode> = (node, executionContext) => {
return executionContext.nodeExecutor(node.children.body, executionContext);
};

View File

@ -0,0 +1,22 @@
import type {TwingNodeExecutor} from "../../node-executor";
import {type TwingBaseArrayNode} from "../../node/expression/array";
import type {TwingNode} from "../../node";
import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
export const executeArrayNode: TwingNodeExecutor<TwingBaseArrayNode<any>> = async (baseNode, executionContext) => {
const {nodeExecutor: execute} = executionContext;
const keyValuePairs = getKeyValuePairs(baseNode);
const array: Array<any> = [];
for (const {value: valueNode} of keyValuePairs) {
const value = await execute(valueNode, executionContext);
if ((valueNode as TwingNode).type === "spread") {
array.push(...value);
} else {
array.push(value);
}
}
return array;
};

View File

@ -0,0 +1,22 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingArrowFunctionNode} from "../../node/expression/arrow-function";
export const executeArrowFunctionNode: TwingNodeExecutor<TwingArrowFunctionNode> = (node, executionContext) => {
const {context, nodeExecutor: execute} = executionContext;
const {body, names} = node.children;
const assignmentNodes = Object.values(names.children);
return Promise.resolve((...functionArgs: Array<any>): Promise<any> => {
let index = 0;
for (const assignmentNode of assignmentNodes) {
const {name} = assignmentNode.attributes;
context.set(name, functionArgs[index]);
index++;
}
return execute(body, executionContext);
});
};

View File

@ -0,0 +1,6 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingAssignmentNode} from "../../node/expression/assignment";
export const executeAssignmentNode: TwingNodeExecutor<TwingAssignmentNode> = (node) => {
return Promise.resolve(node.attributes.name);
};

View File

@ -0,0 +1,30 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingAttributeAccessorNode} from "../../node/expression/attribute-accessor";
import {getTraceableMethod} from "../../helpers/traceable-method";
import {getAttribute} from "../../helpers/get-attribute";
export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAccessorNode> = (node, executionContext) => {
const {template, sandboxed, isStrictVariables, nodeExecutor: execute} = executionContext;
const {target, attribute, arguments: methodArguments} = node.children;
const {type, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
return Promise.all([
execute(target, executionContext),
execute(attribute, executionContext),
execute(methodArguments, executionContext)
]).then(([target, attribute, methodArguments]) => {
const traceableGetAttribute = getTraceableMethod(getAttribute, node.line, node.column, template.name);
return traceableGetAttribute(
template,
target,
attribute,
methodArguments,
type,
shouldTestExistence,
shouldIgnoreStrictCheck || null,
sandboxed,
isStrictVariables
)
})
};

View File

@ -0,0 +1,165 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingBaseBinaryNode} from "../../node/expression/binary";
import {compare} from "../../helpers/compare";
import {concatenate} from "../../helpers/concatenate";
import {every, isAMapLike, some} from "../../helpers/map-like";
import {isIn} from "../../helpers/is-in";
import {parseRegularExpression} from "../../helpers/parse-regular-expression";
import {createRange} from "../../helpers/create-range";
import {createRuntimeError} from "../../error/runtime";
export const executeBinaryNode: TwingNodeExecutor<TwingBaseBinaryNode<any>> = async (node, executionContext) => {
const {left, right} = node.children;
const {nodeExecutor: execute} = executionContext;
switch (node.type) {
case "add": {
return await execute(left, executionContext) + await execute(right, executionContext);
}
case "and": {
return !!(await execute(left, executionContext) && await execute(right, executionContext));
}
case "bitwise_and": {
return await execute(left, executionContext) & await execute(right, executionContext);
}
case "bitwise_or": {
return await execute(left, executionContext) | await execute(right, executionContext);
}
case "bitwise_xor": {
return await execute(left, executionContext) ^ await execute(right, executionContext);
}
case "concatenate": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
return concatenate(leftValue, rightValue);
}
case "divide": {
return await execute(left, executionContext) / await execute(right, executionContext);
}
case "divide_and_floor": {
return Math.floor(await execute(left, executionContext) / await execute(right, executionContext));
}
case "ends_with": {
const leftValue = await execute(left, executionContext);
if (typeof leftValue !== "string") {
return false;
}
const rightValue = await execute(right, executionContext);
if (typeof rightValue !== "string") {
return false;
}
return rightValue.length < 1 || leftValue.endsWith(rightValue);
}
case "has_every": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
if (typeof rightValue !== "function") {
return Promise.resolve(true);
}
if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) {
return Promise.resolve(true);
}
return every(leftValue, rightValue);
}
case "has_some": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
if (typeof rightValue !== "function") {
return Promise.resolve(false);
}
if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) {
return Promise.resolve(false);
}
return some(leftValue, rightValue);
}
case "is_equal_to": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
return compare(leftValue, rightValue);
}
case "is_greater_than": {
return await execute(left, executionContext) > await execute(right, executionContext);
}
case "is_greater_than_or_equal_to": {
return await execute(left, executionContext) >= await execute(right, executionContext);
}
case "is_in": {
return isIn(await execute(left, executionContext), await execute(right, executionContext));
}
case "is_less_than": {
return await execute(left, executionContext) < await execute(right, executionContext);
}
case "is_less_than_or_equal_to": {
return await execute(left, executionContext) <= await execute(right, executionContext);
}
case "is_not_equal_to": {
return Promise.resolve(!compare(await execute(left, executionContext), await execute(right, executionContext)))
}
case "is_not_in": {
return Promise.resolve(!isIn(await execute(left, executionContext), await execute(right, executionContext)))
}
case "matches": {
return parseRegularExpression(
await execute(right, executionContext)
).test(
await execute(left, executionContext)
);
}
case "modulo": {
return await execute(left, executionContext) % await execute(right, executionContext);
}
case "multiply": {
return await execute(left, executionContext) * await execute(right, executionContext);
}
case "or": {
return !!(await execute(left, executionContext) || await execute(right, executionContext));
}
case "power": {
return Math.pow(await execute(left, executionContext), await execute(right, executionContext));
}
case "range": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
return createRange(leftValue, rightValue, 1);
}
case "spaceship": {
const leftValue = await execute(left, executionContext);
const rightValue = await execute(right, executionContext);
return compare(leftValue, rightValue) ? 0 : (leftValue < rightValue ? -1 : 1);
}
case "starts_with": {
const leftValue = await execute(left, executionContext);
if (typeof leftValue !== "string") {
return false;
}
const rightValue = await execute(right, executionContext);
if (typeof rightValue !== "string") {
return false;
}
return rightValue.length < 1 || leftValue.startsWith(rightValue);
}
case "subtract": {
return await execute(left, executionContext) - await execute(right, executionContext);
}
}
return Promise.reject(createRuntimeError(`Unrecognized binary node of type "${node.type}"`, node));
};

View File

@ -0,0 +1,45 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingBlockFunctionNode} from "../../node/expression/block-function";
import {TwingTemplate} from "../../template";
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeBlockFunction: TwingNodeExecutor<TwingBlockFunctionNode> = async (node, executionContext) => {
const {template, context, nodeExecutor: execute, outputBuffer, blocks, sandboxed, sourceMapRuntime} = executionContext;
const {template: templateNode, name: blockNameNode} = node.children;
const blockName = await execute(blockNameNode, executionContext);
let resolveTemplate: Promise<TwingTemplate>;
if (templateNode) {
const templateName = await execute(templateNode, executionContext);
const loadTemplate = getTraceableMethod(
template.loadTemplate,
templateNode.line,
templateNode.column,
template.name
);
resolveTemplate = loadTemplate(templateName);
} else {
resolveTemplate = Promise.resolve(template)
}
return resolveTemplate
.then<Promise<boolean | string>>((executionContextOfTheBlock) => {
if (node.attributes.shouldTestExistence) {
const hasBlock = getTraceableMethod(executionContextOfTheBlock.hasBlock, node.line, node.column, template.name);
return hasBlock(blockName, context.clone(), outputBuffer, blocks, sandboxed, execute);
} else {
const renderBlock = getTraceableMethod(executionContextOfTheBlock.renderBlock, node.line, node.column, template.name);
if (templateNode) {
return renderBlock(blockName, context.clone(), outputBuffer, new Map(), false, sandboxed, execute, sourceMapRuntime);
} else {
return renderBlock(blockName, context.clone(), outputBuffer, blocks, true, sandboxed, execute, sourceMapRuntime);
}
}
});
};

View File

@ -0,0 +1,180 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingBaseCallNode} from "../../node/expression/call";
import {TwingCallableArgument, TwingCallableWrapper} from "../../callable-wrapper";
import {createRuntimeError} from "../../error/runtime";
import {getTraceableMethod} from "../../helpers/traceable-method";
import {TwingArrayNode} from "../../node/expression/array";
import {TwingBaseNode} from "../../node";
import {createConstantNode, TwingConstantNode} from "../../node/expression/constant";
import {TwingBaseExpressionNode} from "../../node/expression";
import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
const array_merge = require('locutus/php/array/array_merge');
const snakeCase = require('snake-case');
const normalizeName = (name: string) => {
return snakeCase(name).toLowerCase();
};
const getArguments = (
node: TwingBaseCallNode<any>,
argumentsNode: TwingArrayNode,
acceptedArguments: Array<TwingCallableArgument>,
isVariadic: boolean
): Array<TwingBaseNode> => {
const callType = node.type;
const callName = node.attributes.operatorName;
const parameters: Map<string | number, {
key: TwingConstantNode;
value: TwingBaseExpressionNode;
}> = new Map();
let named = false;
const keyPairs = getKeyValuePairs(argumentsNode);
for (let {key, value} of keyPairs) {
let name = key.attributes.value as string | number;
if (typeof name === "string") {
named = true;
name = normalizeName(name);
}
else if (named) {
throw createRuntimeError(`Positional arguments cannot be used after named arguments for ${callType} "${callName}".`, node);
}
parameters.set(name, {
key,
value
});
}
const callableParameters = acceptedArguments;
const names: Array<string> = [];
let optionalArguments: Array<string | TwingConstantNode> = [];
let arguments_: Array<TwingBaseNode> = [];
let position = 0;
for (const callableParameter of callableParameters) {
const name = '' + normalizeName(callableParameter.name);
names.push(name);
const parameter = parameters.get(name);
if (parameter) {
if (parameters.has(position)) {
throw createRuntimeError(`Argument "${name}" is defined twice for ${callType} "${callName}".`, node);
}
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(parameter.value);
parameters.delete(name);
optionalArguments = [];
}
else {
const parameter = parameters.get(position);
if (parameter) {
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(parameter.value);
parameters.delete(position);
optionalArguments = [];
++position;
}
else if (callableParameter.defaultValue !== undefined) {
arguments_.push(createConstantNode(callableParameter.defaultValue, node.line, node.column));
}
else {
throw createRuntimeError(`Value for argument "${name}" is required for ${callType} "${callName}".`, node);
}
}
}
if (isVariadic) {
const resolvedKeys: Array<any> = [];
const arbitraryArguments: Array<TwingBaseExpressionNode> = [];
for (const [key, value] of parameters) {
arbitraryArguments.push(value.value);
resolvedKeys.push(key);
}
for (const key of resolvedKeys) {
parameters.delete(key);
}
if (arbitraryArguments.length) {
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(...arbitraryArguments);
}
}
if (parameters.size > 0) {
const unknownParameter = [...parameters.values()][0];
throw createRuntimeError(`Unknown argument${parameters.size > 1 ? 's' : ''} "${[...parameters.keys()].join('", "')}" for ${callType} "${callName}(${names.join(', ')})".`, unknownParameter.key);
}
return arguments_;
}
export const executeCallNode: TwingNodeExecutor<TwingBaseCallNode<any>> = async (node, executionContext) => {
const {type} = node;
const {template, nodeExecutor: execute} = executionContext
const {operatorName} = node.attributes;
let callableWrapper: TwingCallableWrapper | null;
switch (type) {
case "filter":
callableWrapper = template.getFilter(operatorName);
break;
case "function":
callableWrapper = template.getFunction(operatorName);
break;
// for some reason, using `case "test"` makes the compiler assume that callableWrapper is used
// before it is assigned a value; this is probably a bug of the compiler
default:
callableWrapper = template.getTest(operatorName);
break;
}
if (callableWrapper === null) {
throw createRuntimeError(`Unknown ${type} "${operatorName}".`, node);
}
const {operand, arguments: callArguments} = node.children;
const argumentNodes = getArguments(
node,
callArguments,
callableWrapper.acceptedArguments,
callableWrapper.isVariadic
);
const actualArguments: Array<any> = [];
actualArguments.push(...callableWrapper!.nativeArguments);
if (operand) {
actualArguments.push(await execute(operand, executionContext));
}
const providedArguments = await Promise.all([
...argumentNodes.map((node) => execute(node, executionContext))
]);
actualArguments.push(...providedArguments);
const traceableCallable = getTraceableMethod(callableWrapper.callable, node.line, node.column, template.name);
return traceableCallable(executionContext, ...actualArguments).then((value) => {
return value;
});
};

View File

@ -0,0 +1,9 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingBaseConditionalNode} from "../../node/expression/conditional";
export const executeConditionalNode: TwingNodeExecutor<TwingBaseConditionalNode<any>> = async (node, executionContext) => {
const {expr1, expr2, expr3} = node.children;
const {nodeExecutor: execute} = executionContext;
return (await execute(expr1, executionContext)) ? execute(expr2, executionContext) : execute(expr3, executionContext);
};

View File

@ -0,0 +1,16 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingEscapeNode} from "../../node/expression/escape";
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeEscapeNode: TwingNodeExecutor<TwingEscapeNode> = (node, executionContext) => {
const {template, nodeExecutor: execute} = executionContext;
const {strategy} = node.attributes;
const {body} = node.children;
return execute(body, executionContext)
.then((value) => {
const escape = getTraceableMethod(template.escape, node.line, node.column, template.name);
return escape(value, strategy, null, true);
});
};

View File

@ -0,0 +1,27 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingHashNode} from "../../node/expression/hash";
import type {TwingNode} from "../../node";
import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
export const executeHashNode: TwingNodeExecutor<TwingHashNode> = async (node, executionContext) => {
const {nodeExecutor: execute} = executionContext;
const keyValuePairs = getKeyValuePairs(node);
const hash: Map<any, any> = new Map();
for (const {key: keyNode, value: valueNode} of keyValuePairs) {
const [key, value] = await Promise.all([
execute(keyNode, executionContext),
execute(valueNode, executionContext)
]);
if ((valueNode as TwingNode).type === "spread") {
for (const [valueKey, valueValue] of value as Map<any, any>) {
hash.set(valueKey, valueValue);
}
} else {
hash.set(key, value);
}
}
return hash;
};

View File

@ -0,0 +1,54 @@
import type {TwingNodeExecutor} from "../../node-executor";
import {TwingTemplate, TwingTemplateMacroHandler} from "../../template";
import {createRuntimeError} from "../../error/runtime";
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, outputBuffer, aliases, nodeExecutor: execute, sandboxed, sourceMapRuntime} = executionContext;
const {methodName, shouldTestExistence} = node.attributes;
const {operand, arguments: methodArguments} = node.children;
if (shouldTestExistence) {
return (aliases.get(operand.attributes.name) as TwingTemplate).hasMacro(methodName);
} else {
const keyValuePairs = getKeyValuePairs(methodArguments);
const macroArguments: Array<any> = [];
for (const {value: valueNode} of keyValuePairs) {
const value = await execute(valueNode, executionContext);
macroArguments.push(value);
}
// 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)!;
const getHandler = (template: TwingTemplate): Promise<TwingTemplateMacroHandler | null> => {
const macroHandler = template.macroHandlers.get(methodName);
if (macroHandler) {
return Promise.resolve(macroHandler);
} else {
return template.getParent(context, outputBuffer, sandboxed, execute)
.then((parent) => {
if (parent) {
return getHandler(parent);
} else {
return null;
}
});
}
};
return getHandler(macroTemplate)
.then((handler) => {
if (handler) {
return handler(outputBuffer, sandboxed, sourceMapRuntime, execute, ...macroArguments);
} else {
throw createRuntimeError(`Macro "${methodName}" is not defined in template "${macroTemplate.name}".`, node, template.name);
}
});
}
};

View File

@ -0,0 +1,31 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingNameNode} from "../../node/expression/name";
import {getTraceableMethod} from "../../helpers/traceable-method";
import {getContextValue} from "../../helpers/get-context-value";
export const executeNameNode: TwingNodeExecutor<TwingNameNode> = (node, {
template,
context,
charset,
isStrictVariables
}) => {
const {name, isAlwaysDefined, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
const traceableGetContextValue = getTraceableMethod(
getContextValue,
node.line,
node.column,
template.name
);
return traceableGetContextValue(
charset,
template.name,
isStrictVariables,
context,
name,
isAlwaysDefined,
shouldIgnoreStrictCheck,
shouldTestExistence
);
};

View File

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

View File

@ -0,0 +1,9 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingSpreadNode} from "../../node/expression/spread";
export const executeSpreadNode: TwingNodeExecutor<TwingSpreadNode> = (node, executionContext) => {
const {iterable} = node.children;
const {nodeExecutor: execute} = executionContext;
return execute(iterable, executionContext);
};

View File

@ -0,0 +1,22 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingBaseUnaryNode} from "../../node/expression/unary";
import {createRuntimeError} from "../../error/runtime";
export const executeUnaryNode: TwingNodeExecutor<TwingBaseUnaryNode<any>> = (node, executionContext) => {
const {operand} = node.children;
const {nodeExecutor: execute} = executionContext;
switch (node.type) {
case "negative": {
return execute(operand, executionContext).then((value) => -(value));
}
case "not": {
return execute(operand, executionContext).then((value) => !(value));
}
case "positive": {
return execute(operand, executionContext).then((value) => +(value));
}
}
return Promise.reject(createRuntimeError(`Unrecognized unary node of type "${node.type}"`, node));
};

View File

@ -0,0 +1,8 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingFlushNode} from "../node/flush";
export const executeFlushNode: TwingNodeExecutor<TwingFlushNode> = (_node, {outputBuffer}) => {
outputBuffer.flush();
return Promise.resolve();
};

View File

@ -0,0 +1,26 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingForLoopNode} from "../node/for-loop";
export const executeForLoopNode: TwingNodeExecutor<TwingForLoopNode> = (node, executionContext) => {
const {hasAnElse, hasAnIf} = node.attributes;
const {context} = executionContext;
if (hasAnElse) {
context.set('_iterated', true);
}
const loop: Map<string, any> = context.get('loop');
loop.set('index0', loop.get('index0') + 1);
loop.set('index', loop.get('index') + 1);
loop.set('first', false);
if (!hasAnIf && loop.has('length')) {
loop.set('revindex0', loop.get('revindex0') - 1);
loop.set('revindex', loop.get('revindex') - 1);
loop.set('last', loop.get('revindex0') === 0);
}
return Promise.resolve();
};

View File

@ -0,0 +1,83 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingForNode} from "../node/for";
import {TwingContext} from "../context";
import {ensureTraversable} from "../helpers/ensure-traversable";
import {count} from "../helpers/count";
import {iterate} from "../helpers/iterate";
export const executeForNode: TwingNodeExecutor<TwingForNode> = async (forNode, executionContext) => {
const {context, nodeExecutor: execute} = executionContext;
const {
sequence: sequenceNode,
body,
else: elseNode,
valueTarget: targetValueNode,
keyTarget: targetKeyNode
} = forNode.children;
const {hasAnIf} = forNode.attributes;
context.set('_parent', context.clone());
const executedSequence: TwingContext<any, any> | any = await execute(sequenceNode, executionContext);
let sequence = ensureTraversable(executedSequence);
if (sequence === context) {
context.set('_seq', context.clone());
} else {
context.set('_seq', sequence);
}
if (elseNode) {
context.set('_iterated', false);
}
context.set('loop', new Map([
['parent', context.get('_parent')],
['index0', 0],
['index', 1],
['first', true],
]));
if (!hasAnIf) {
const length = count(context.get('_seq'));
const loop: Map<string, any> = context.get('loop');
loop.set('revindex0', length - 1);
loop.set('revindex', length);
loop.set('length', length);
loop.set('last', (length === 1));
}
const targetKey = await execute(targetKeyNode, executionContext);
const targetValue = await execute(targetValueNode, executionContext);
await iterate(context.get('_seq'), async (key, value) => {
context.set(targetKey, key);
context.set(targetValue, value);
await execute(body, executionContext);
});
if (elseNode) {
if (context.get('_iterated') === false) {
await execute(elseNode, executionContext);
}
}
const parent = context.get('_parent');
context.delete('_seq');
context.delete('_iterated');
context.delete(targetKeyNode.attributes.name);
context.delete(targetValueNode.attributes.name);
context.delete('_parent');
context.delete('loop');
for (const [key, value] of parent) {
if (!context.has(key)) {
context.set(key, value);
}
}
};

View File

@ -0,0 +1,30 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingIfNode} from "../node/if";
import {getChildrenCount} from "../node";
import {evaluate} from "../helpers/evaluate";
export const executeIfNode: TwingNodeExecutor<TwingIfNode> = async (node, executionContext) => {
const {tests: testsNode, else: elseNode} = node.children;
const count = getChildrenCount(testsNode);
const {nodeExecutor: execute} = executionContext;
let index: number = 0;
while (index < count) {
const condition = testsNode.children[index];
const conditionResult = await execute(condition, executionContext);
if (evaluate(conditionResult)) {
// the condition is satisfied, we execute the belonging body and return the result
const body = testsNode.children[index + 1];
return execute(body, executionContext);
}
index += 2;
}
if (elseNode !== undefined) {
return execute(elseNode, executionContext);
}
};

View File

@ -0,0 +1,30 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingImportNode} from "../node/import";
import type {TwingTemplate} from "../template";
import {getTraceableMethod} from "../helpers/traceable-method";
import type {TwingNameNode} from "../node/expression/name";
export const executeImportNode: TwingNodeExecutor<TwingImportNode> = async (node, executionContext) => {
const {template, aliases, nodeExecutor: execute,} = executionContext;
const {alias: aliasNode, templateName: templateNameNode} = node.children;
const {global} = node.attributes;
let aliasValue: TwingTemplate;
if (templateNameNode.type === "name" && (templateNameNode as TwingNameNode).attributes.name === '_self') {
aliasValue = template;
} else {
const templateName = await execute(templateNameNode, executionContext);
const loadTemplate = getTraceableMethod(template.loadTemplate, node.line, node.column, template.name);
aliasValue = await loadTemplate(templateName);
}
aliases.set(aliasNode.attributes.name, aliasValue);
if (global) {
template.aliases.set(aliasNode.attributes.name, aliasValue);
}
};

View File

@ -0,0 +1,30 @@
import {TwingBaseIncludeNode} from "../node/include";
import {getTraceableMethod} from "../helpers/traceable-method";
import {include} from "../extension/core/functions/include";
import {TwingExecutionContext} from "../execution-context";
import {TwingTemplate} from "../template";
export const executeBaseIncludeNode = async (
node: TwingBaseIncludeNode<any>,
executionContext: TwingExecutionContext,
getTemplate: (executionContext: TwingExecutionContext) => Promise<TwingTemplate | null | Array<TwingTemplate | null>>
) => {
const {nodeExecutor: execute, outputBuffer, sandboxed, template} = executionContext;
const {variables} = node.children;
const {only, ignoreMissing} = node.attributes;
const templatesToInclude = await getTemplate(executionContext);
const traceableInclude = getTraceableMethod(include, node.line, node.column, template.name);
const output = await traceableInclude(
executionContext,
templatesToInclude,
await execute(variables, executionContext),
!only,
ignoreMissing,
sandboxed
);
outputBuffer.echo(output);
};

View File

@ -0,0 +1,14 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingEmbedNode} from "../../node/include/embed";
import {executeBaseIncludeNode} from "../include";
import {getTraceableMethod} from "../../helpers/traceable-method";
export const executeEmbedNode: TwingNodeExecutor<TwingEmbedNode> = (node, executionContext) => {
return executeBaseIncludeNode(node, executionContext, ({template}) => {
const {index} = node.attributes;
const loadTemplate = getTraceableMethod(template.loadEmbeddedTemplate, node.line, node.column, template.name);
return loadTemplate(index);
})
};

View File

@ -0,0 +1,11 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingIncludeNode} from "../../node/include/include";
import {executeBaseIncludeNode} from "../include";
export const executeIncludeNode: TwingNodeExecutor<TwingIncludeNode> = (node, executionContext) => {
const {nodeExecutor: execute} = executionContext;
return executeBaseIncludeNode(node, executionContext, (executionContext) => {
return execute(node.children.expression, executionContext);
})
};

View File

@ -0,0 +1,6 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingLineNode} from "../node/line";
export const executeLineNode: TwingNodeExecutor<TwingLineNode> = () => {
return Promise.resolve();
};

View File

@ -0,0 +1,19 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingPrintNode} from "../node/print";
export const executePrintNode: TwingNodeExecutor<TwingPrintNode> = (node, executionContext) => {
const {template, nodeExecutor: execute, outputBuffer, sourceMapRuntime} = executionContext;
sourceMapRuntime?.enterSourceMapBlock(node.line, node.column, node.type, template.source, outputBuffer);
return execute(node.children.expression, executionContext)
.then((result) => {
if (Array.isArray(result)) {
result = 'Array';
}
outputBuffer.echo(result);
sourceMapRuntime?.leaveSourceMapBlock(outputBuffer);
});
};

View File

@ -0,0 +1,12 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingSandboxNode} from "../node/sandbox";
export const executeSandboxNode: TwingNodeExecutor<TwingSandboxNode> = (node, executionContext) => {
const {body} = node.children;
const {nodeExecutor: execute} = executionContext;
return execute(body, {
...executionContext,
sandboxed: true
});
};

View File

@ -0,0 +1,34 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingSetNode} from "../node/set";
export const executeSetNode: TwingNodeExecutor<TwingSetNode> = async (node, executionContext) => {
const {context, nodeExecutor: execute, outputBuffer} = executionContext;
const {names: namesNode, values: valuesNode} = node.children;
const {captures} = node.attributes;
const names: Array<string> = await execute(namesNode, executionContext);
if (captures) {
outputBuffer.start();
await execute(valuesNode, executionContext);
const value = outputBuffer.getAndClean();
for (const name of names) {
context.set(name, value);
}
} else {
const values: Array<any> = await execute(valuesNode, executionContext);
let index = 0;
for (const name of names) {
const value = values[index];
context.set(name, value);
index++;
}
}
};

View File

@ -0,0 +1,16 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingSpacelessNode} from "../node/spaceless";
export const executeSpacelessNode: TwingNodeExecutor<TwingSpacelessNode> = (node, executionContext) => {
const {outputBuffer} = executionContext;
const {nodeExecutor: execute} = executionContext;
outputBuffer.start();
return execute(node.children.body, executionContext)
.then(() => {
const content = outputBuffer.getAndClean().replace(/>\s+</g, '><').trim();
outputBuffer.echo(content);
});
};

View File

@ -0,0 +1,16 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingTemplateNode} from "../node/template";
export const executeTemplateNode: TwingNodeExecutor<TwingTemplateNode> = (node, executionContext) => {
const {template, nodeExecutor: execute, outputBuffer, sourceMapRuntime} = executionContext;
const {securityCheck, body} = node.children;
return execute(securityCheck, executionContext)
.then(() => {
sourceMapRuntime?.enterSourceMapBlock(node.line, node.column, node.type, template.source, outputBuffer);
return execute(body, executionContext).then(() => {
sourceMapRuntime?.leaveSourceMapBlock(outputBuffer);
});
});
};

View File

@ -0,0 +1,14 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingTextNode} from "../node/text";
export const executeTextNode: TwingNodeExecutor<TwingTextNode> = (textNode, executionContext) => {
const {template, outputBuffer, sourceMapRuntime} = executionContext;
sourceMapRuntime?.enterSourceMapBlock(textNode.line, textNode.column, textNode.type, template.source, outputBuffer);
outputBuffer.echo(textNode.attributes.data);
sourceMapRuntime?.leaveSourceMapBlock(outputBuffer);
return Promise.resolve();
};

View File

@ -0,0 +1,42 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingWithNode} from "../node/with";
import {createContext, TwingContext} from "../context";
import {createRuntimeError} from "../error/runtime";
import {mergeIterables} from "../helpers/merge-iterables";
import {iteratorToMap} from "../helpers/iterator-to-map";
export const executeWithNode: TwingNodeExecutor<TwingWithNode> = async (node, executionContext) => {
const {template, nodeExecutor: execute, context} = executionContext;
const {variables: variablesNode, body} = node.children;
const {only} = node.attributes;
let scopedContext: TwingContext<any, any>;
if (variablesNode) {
const variables = await execute(variablesNode, executionContext);
if (typeof variables !== "object") {
throw createRuntimeError(`Variables passed to the "with" tag must be a hash.`, node, template.name);
}
if (only) {
scopedContext = createContext();
} else {
scopedContext = context.clone();
}
scopedContext = createContext(mergeIterables(
scopedContext,
iteratorToMap(variables)
))
} else {
scopedContext = context.clone();
}
scopedContext.set('_parent', context.clone());
await execute(body, {
...executionContext,
context: scopedContext
});
};

View File

@ -10,10 +10,11 @@ import {cloneMethodCallNode} from "../node/expression/method-call";
import {TwingBaseExpressionNode} from "../node/expression";
import {createParsingError} from "../error/parsing";
import {createTestNode, TwingTestNode} from "../node/expression/call/test";
import {createArrayNode, getKeyValuePairs} from "../node/expression/array";
import {createArrayNode} from "../node/expression/array";
import {createConditionalNode} from "../node/expression/conditional";
import {TwingFilterNode} from "../node/expression/call/filter";
import type {TwingNode} from "../node";
import {getKeyValuePairs} from "../helpers/get-key-value-pairs";
export const createCoreNodeVisitor = (): TwingNodeVisitor => {
const enteredNodes: Array<TwingBaseExpressionNode> = [];

View File

@ -2,7 +2,6 @@ import {TwingBaseNode, getChildren, TwingNode} from "../node";
import {createCheckSecurityNode} from "../node/check-security";
import {createCheckToStringNode} from "../node/check-to-string";
import {createNodeVisitor, TwingNodeVisitor} from "../node-visitor";
import type {TwingWrapperNode} from "../node/wrapper";
export const createSandboxNodeVisitor = (): TwingNodeVisitor => {
let tags: Map<string, TwingBaseNode>;
@ -103,7 +102,7 @@ export const createSandboxNodeVisitor = (): TwingNodeVisitor => {
};
const wrapArrayNode = <T extends TwingNode>(node: T, name: keyof T["children"]) => {
const args: TwingWrapperNode = (node.children as any)[name]; // todo: check with TS team with we have to cast children as any
const args: TwingBaseNode = (node.children as any)[name]; // todo: check with TS team with we have to cast children as any
for (const [name] of getChildren(args)) {
wrapNode(args, name);

View File

@ -3,7 +3,6 @@ import type {TwingPrintNode} from "./node/print";
import type {TwingBlockReferenceNode} from "./node/block-reference";
import type {TwingTextNode} from "./node/text";
import type {TwingAutoEscapeNode} from "./node/auto-escape";
import type {TwingBodyNode} from "./node/body";
import type {TwingCheckSecurityNode} from "./node/check-security";
import type {TwingCheckToStringNode} from "./node/check-to-string";
import type {TwingCommentNode} from "./node/comment";
@ -29,15 +28,12 @@ import type {TwingIfNode} from "./node/if";
import type {TwingMethodCallNode} from "./node/expression/method-call";
import type {TwingEscapeNode} from "./node/expression/escape";
import type {TwingApplyNode} from "./node/apply";
import type {TwingExecutionContext} from "./execution-context";
import type {TwingWrapperNode} from "./node/wrapper";
export type TwingNode =
| TwingApplyNode
| TwingAutoEscapeNode
| TwingBlockNode
| TwingBlockReferenceNode
| TwingBodyNode
| TwingCheckSecurityNode
| TwingCheckToStringNode
| TwingCommentNode
@ -64,7 +60,6 @@ export type TwingNode =
| TwingTraitNode
| TwingVerbatimNode
| TwingWithNode
| TwingWrapperNode
;
export type TwingNodeType<T> = T extends TwingBaseNode<infer Type, any, any> ? Type : never;
@ -85,8 +80,6 @@ export interface TwingBaseNode<
readonly line: number;
readonly tag: string | null;
readonly type: Type;
execute(executionContext: TwingExecutionContext): Promise<any>;
}
export type KeysOf<T> = T extends T ? keyof T : never;
@ -121,15 +114,25 @@ export const createBaseNode = <
column,
line,
tag,
type,
execute: async (executionContext) => {
const output: Array<any> = [];
for (const [, child] of Object.entries(children)) {
output.push(await child.execute(executionContext));
}
return output;
}
type
};
};
/**
* Create a node acting as a container for the passed list of indexed nodes.
*
* @param children The children of the created node
* @param line The line of the created node
* @param column The column of the created node
* @param tag The tag of the created node
*/
export const createNode = <
Children extends TwingBaseNodeChildren
>(
children: Children = {} as Children,
line: number = 0,
column: number = 0,
tag: string | null = null
): TwingBaseNode<null, {}, Children> => {
return createBaseNode(null, {}, children, line, column, tag);
};

View File

@ -1,7 +1,5 @@
import {createBaseNode, TwingBaseNode, TwingBaseNodeAttributes} from "../node";
import {getKeyValuePairs, TwingArrayNode} from "./expression/array";
import {createFilterNode} from "./expression/call/filter";
import {createConstantNode} from "./expression/constant";
import {TwingArrayNode} from "./expression/array";
export type TwingApplyNodeAttributes = TwingBaseNodeAttributes & {};
@ -20,39 +18,8 @@ export const createApplyNode = (
line: number,
column: number
): TwingApplyNode => {
const baseNode = createBaseNode("apply", {}, {
return createBaseNode("apply", {}, {
body,
filters
}, line, column, 'apply');
const applyNode: TwingApplyNode = {
...baseNode,
execute: async (executionContext) => {
const {outputBuffer} = executionContext;
const {body, filters} = applyNode.children;
const {line, column} = applyNode;
outputBuffer.start();
return body.execute(executionContext)
.then(async () => {
let content = outputBuffer.getAndClean();
const keyValuePairs = getKeyValuePairs(filters);
while (keyValuePairs.length > 0) {
const {key, value: filterArguments} = keyValuePairs.pop()!;
const filterName = key.attributes.value as string;
const filterNode = createFilterNode(createConstantNode(content, line, column), filterName, filterArguments, line, column);
content = await filterNode.execute(executionContext);
}
outputBuffer.echo(content);
});
}
};
return applyNode;
};

View File

@ -15,15 +15,11 @@ export const createAutoEscapeNode = (
body: TwingBaseNode,
line: number,
column: number,
tag: string
tag?: string
): TwingAutoEscapeNode => {
const baseNode = createBaseNode("auto_escape", {
return createBaseNode("auto_escape", {
strategy
}, {
body
}, line, column, tag);
return {
...baseNode
};
};

View File

@ -1,5 +1,4 @@
import {createBaseNode, TwingBaseNode, TwingBaseNodeAttributes} from "../node";
import {getTraceableMethod} from "../helpers/traceable-method";
export type TwingBlockReferenceNodeAttributes = TwingBaseNodeAttributes & {
name: string;
@ -14,29 +13,7 @@ export const createBlockReferenceNode = (
column: number,
tag: string
): TwingBlockReferenceNode => {
const outputNode = createBaseNode("block_reference", {
return createBaseNode("block_reference", {
name
}, {}, line, column, tag);
const blockReferenceNode: TwingBlockReferenceNode = {
...outputNode,
execute: (executionContext) => {
const {template, context, outputBuffer, blocks, sandboxed, sourceMapRuntime} = executionContext;
const {name} = blockReferenceNode.attributes;
const renderBlock = getTraceableMethod(template.renderBlock, blockReferenceNode.line, blockReferenceNode.column, template.name);
return renderBlock(
name,
context.clone(),
outputBuffer,
blocks,
true,
sandboxed,
sourceMapRuntime
).then(outputBuffer.echo);
}
};
return blockReferenceNode;
};

View File

@ -16,11 +16,5 @@ export const createBlockNode = (
column: number,
tag: string | null = null
): TwingBlockNode => {
const baseNode = createBaseNode("block", {name}, {body}, line, column, tag);
const blockNode: TwingBlockNode = {
...baseNode
};
return blockNode;
return createBaseNode("block", {name}, {body}, line, column, tag);
};

View File

@ -1,15 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
export interface TwingBodyNode extends TwingBaseNode<"body", TwingBaseNodeAttributes, {
content: TwingBaseNode;
}> {
}
export const createBodyNode = (
content: TwingBaseNode,
line: number,
column: number,
tag?: string
): TwingBodyNode => createBaseNode("body", {}, {
content
}, line, column, tag);

View File

@ -1,18 +1,9 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode, TwingNode} from "../node";
import {
isASandboxSecurityNotAllowedFilterError,
TwingSandboxSecurityNotAllowedFilterError
} from "../sandbox/security-not-allowed-filter-error";
import {
isASandboxSecurityNotAllowedTagError,
TwingSandboxSecurityNotAllowedTagError
} from "../sandbox/security-not-allowed-tag-error";
import {TwingSandboxSecurityNotAllowedFunctionError} from "../sandbox/security-not-allowed-function-error";
export type TwingCheckSecurityNodeAttributes = TwingBaseNodeAttributes & {
usedFilters: Map<string, TwingNode | string>;
usedTags: Map<string, TwingNode | string>;
usedFunctions: Map<string, TwingNode | string>;
usedFilters: Map<string, TwingNode>;
usedTags: Map<string, TwingNode>;
usedFunctions: Map<string, TwingNode>;
};
export interface TwingCheckSecurityNode extends TwingBaseNode<"check_security", TwingCheckSecurityNodeAttributes> {
@ -25,43 +16,9 @@ export const createCheckSecurityNode = (
line: number,
column: number
): TwingCheckSecurityNode => {
const baseNode = createBaseNode("check_security", {
return createBaseNode("check_security", {
usedFilters,
usedTags,
usedFunctions
}, {}, line, column);
return {
...baseNode,
execute: (executionContext) => {
const {template, sandboxed} = executionContext;
const {usedTags, usedFunctions, usedFilters} = baseNode.attributes;
try {
sandboxed && template.checkSecurity(
[...usedTags.keys()],
[...usedFilters.keys()],
[...usedFunctions.keys()]
);
} catch (error: any) {
const supplementError = (error: TwingSandboxSecurityNotAllowedFilterError | TwingSandboxSecurityNotAllowedFunctionError | TwingSandboxSecurityNotAllowedTagError) => {
error.source = template.name;
if (isASandboxSecurityNotAllowedTagError(error)) {
error.location = usedTags.get(error.tagName);
} else if (isASandboxSecurityNotAllowedFilterError(error)) {
error.location = usedFilters.get(error.filterName)
} else {
error.location = usedFunctions.get(error.functionName);
}
}
supplementError(error);
throw error;
}
return Promise.resolve();
}
}
};

View File

@ -1,6 +1,5 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import {TwingBaseExpressionNode} from "./expression";
import {getTraceableMethod} from "../helpers/traceable-method";
/**
* Checks if casting an expression to toString() is allowed by the sandbox.
@ -20,36 +19,7 @@ export const createCheckToStringNode = (
line: number,
column: number
): TwingCheckToStringNode => {
const baseNode = createBaseNode("check_to_string", {}, {
return createBaseNode("check_to_string", {}, {
value
}, line, column);
return {
...baseNode,
execute: (executionContext) => {
const {template, sandboxed} = executionContext;
const {value: valueNode} = baseNode.children;
return valueNode.execute(executionContext)
.then((value) => {
if (sandboxed) {
const assertToStringAllowed = getTraceableMethod((value: any) => {
if ((value !== null) && (typeof value === 'object')) {
try {
template.checkMethodAllowed(value, 'toString');
} catch (error) {
return Promise.reject(error);
}
}
return Promise.resolve(value);
}, valueNode.line, valueNode.column, template.name)
return assertToStringAllowed(value);
}
return value;
});
}
}
};

View File

@ -12,11 +12,7 @@ export const createCommentNode = (
line: number,
column: number
): TwingCommentNode => {
const baseNode = createBaseNode("comment", {
return createBaseNode("comment", {
data
}, {}, line, column);
return {
...baseNode
}
};

View File

@ -12,22 +12,7 @@ export const createDeprecatedNode = (
column: number,
tag: string
): TwingDeprecatedNode => {
const baseNode = createBaseNode("deprecated", {}, {
return createBaseNode("deprecated", {}, {
message
}, line, column, tag);
const deprecatedNode: TwingDeprecatedNode = {
...baseNode,
execute: (executionContext) => {
const {template} = executionContext;
const {message} = deprecatedNode.children;
return message.execute(executionContext)
.then((message) => {
console.warn(`${message} ("${template.name}" at line ${deprecatedNode.line}, column ${deprecatedNode.column})`);
});
}
};
return deprecatedNode;
};

View File

@ -18,12 +18,7 @@ export const createDoNode = (
column: number,
tag: string | null
): TwingDoNode => {
const baseNode = createBaseNode("do", {}, {
return createBaseNode("do", {}, {
body
}, line, column, tag);
return {
...baseNode,
execute: baseNode.children.body.execute
};
};

View File

@ -3,11 +3,8 @@ import {
TwingBaseExpressionNodeAttributes,
createBaseExpressionNode
} from "../expression";
import {TwingConstantNode, createConstantNode} from "./constant";
import {createConstantNode} from "./constant";
import {pushToRecord} from "../../helpers/record";
import type {TwingNode} from "../../node";
const array_chunk = require('locutus/php/array/array_chunk');
export interface TwingBaseArrayNode<Type extends string> extends TwingBaseExpressionNode<Type, TwingBaseExpressionNodeAttributes, Record<number, TwingBaseExpressionNode>> {
}
@ -16,19 +13,6 @@ export interface TwingBaseArrayNode<Type extends string> extends TwingBaseExpres
export interface TwingArrayNode extends TwingBaseArrayNode<"array"> {
}
export const getKeyValuePairs = (
node: TwingBaseArrayNode<any>
): Array<{
key: TwingConstantNode,
value: TwingBaseExpressionNode
}> => {
const chunks: Array<[key: TwingConstantNode, value: TwingBaseExpressionNode]> = array_chunk(Object.values(node.children), 2);
return chunks.map(([key, value]) => {
return {key, value};
});
};
export const createBaseArrayNode = <Type extends string>(
type: Type,
elements: Array<{
@ -45,13 +29,7 @@ export const createBaseArrayNode = <Type extends string>(
pushToRecord(children, value);
}
const baseNode = createBaseExpressionNode(type, {}, children, line, column);
const node: TwingBaseArrayNode<Type> = {
...baseNode
};
return node;
return createBaseExpressionNode(type, {}, children, line, column);
};
export const createArrayNode = (
@ -72,23 +50,6 @@ export const createArrayNode = (
}), line, column);
return {
...baseNode,
execute: async (executionContext) => {
const keyValuePairs = getKeyValuePairs(baseNode);
const array: Array<any> = [];
for (const {value: valueNode} of keyValuePairs) {
const value = await valueNode.execute(executionContext);
if ((valueNode as TwingNode).type === "spread") {
array.push(...value);
}
else {
array.push(value);
}
}
return array;
}
...baseNode
};
};

View File

@ -16,31 +16,8 @@ export const createArrowFunctionNode = (
line: number,
column: number
): TwingArrowFunctionNode => {
const baseNode = createBaseExpressionNode("arrow_function", {}, {
return createBaseExpressionNode("arrow_function", {}, {
body,
names
}, line, column);
return {
...baseNode,
execute: (executionContext) => {
const {context} = executionContext;
const {body} = baseNode.children;
const assignmentNodes = Object.values(baseNode.children.names.children);
return Promise.resolve((...functionArgs: Array<any>): Promise<any> => {
let index = 0;
for (const assignmentNode of assignmentNodes) {
const {name} = assignmentNode.attributes;
context.set(name, functionArgs[index]);
index++;
}
return body.execute(executionContext);
});
}
};
};

View File

@ -14,16 +14,7 @@ export const createAssignmentNode = (
line: number,
column: number
): TwingAssignmentNode => {
const baseNode = createBaseNode("assignment", {
return createBaseNode("assignment", {
name
}, {}, line, column);
const assignmentNode: TwingAssignmentNode = {
...baseNode,
execute: () => {
return Promise.resolve(assignmentNode.attributes.name);
}
};
return assignmentNode;
};

View File

@ -3,8 +3,6 @@ import {
TwingBaseExpressionNodeAttributes,
createBaseExpressionNode, TwingExpressionNode
} from "../expression";
import {getAttribute} from "../../helpers/get-attribute";
import {getTraceableMethod} from "../../helpers/traceable-method";
export type TwingAttributeAccessorCallType = "any" | "array" | "method";
@ -32,7 +30,7 @@ export const createAttributeAccessorNode = (
line: number,
column: number
): TwingAttributeAccessorNode => {
const baseNode = createBaseExpressionNode("attribute_accessor", {
return createBaseExpressionNode("attribute_accessor", {
isOptimizable: true,
type,
shouldTestExistence: false
@ -41,37 +39,6 @@ export const createAttributeAccessorNode = (
attribute,
arguments: methodArguments
}, line, column);
const attributeAccessorNode: TwingAttributeAccessorNode = {
...baseNode,
execute: (executionContext) => {
const {template, sandboxed, isStrictVariables} = executionContext;
const {target, attribute, arguments: methodArguments} = attributeAccessorNode.children;
const {type, shouldIgnoreStrictCheck, shouldTestExistence} = attributeAccessorNode.attributes;
return Promise.all([
target.execute(executionContext),
attribute.execute(executionContext),
methodArguments.execute(executionContext)
]).then(([target, attribute, methodArguments]) => {
const traceableGetAttribute = getTraceableMethod(getAttribute, attributeAccessorNode.line, attributeAccessorNode.column, template.name);
return traceableGetAttribute(
template,
target,
attribute,
methodArguments,
type,
shouldTestExistence,
shouldIgnoreStrictCheck || null,
sandboxed,
isStrictVariables
)
})
}
};
return attributeAccessorNode;
};
export const cloneGetAttributeNode = (

View File

@ -29,7 +29,6 @@ import {createBaseExpressionNode} from "../expression";
import type {TwingSpaceshipNode} from "./binary/spaceship";
import type {TwingHasEveryNode} from "./binary/has-every";
import type {TwingHasSomeNode} from "./binary/has-some";
import type {TwingExecutionContext} from "../../execution-context";
export type TwingBinaryNode =
| TwingAddNode
@ -87,13 +86,6 @@ export const createBaseBinaryNode = <Type extends string>(
export const createBinaryNodeFactory = <InstanceType extends TwingBaseBinaryNode<any>>(
type: TwingNodeType<InstanceType>,
definition: {
execute: (
left: TwingBaseExpressionNode,
right: TwingBaseExpressionNode,
executionContext: TwingExecutionContext
) => Promise<any>
}
) => {
const factory = (
operands: [TwingBaseExpressionNode, TwingBaseExpressionNode],
@ -104,9 +96,6 @@ export const createBinaryNodeFactory = <InstanceType extends TwingBaseBinaryNode
return {
...baseNode,
execute: (executionContext) => {
return definition.execute(baseNode.children.left, baseNode.children.right, executionContext);
}
} as InstanceType;
};

View File

@ -4,8 +4,4 @@ import {createBinaryNodeFactory} from "../binary";
export interface TwingAddNode extends TwingBaseBinaryNode<"add"> {
}
export const createAddNode = createBinaryNodeFactory<TwingAddNode>("add", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) + await right.execute(executionContext);
}
});
export const createAddNode = createBinaryNodeFactory<TwingAddNode>("add");

View File

@ -4,8 +4,4 @@ import {createBinaryNodeFactory} from "../binary";
export interface TwingAndNode extends TwingBaseBinaryNode<"and"> {
}
export const createAndNode = createBinaryNodeFactory<TwingAndNode>("and", {
execute: async (left, right, executionContext) => {
return !!(await left.execute(executionContext) && await right.execute(executionContext));
}
});
export const createAndNode = createBinaryNodeFactory<TwingAndNode>("and");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingBitwiseAndNode extends TwingBaseBinaryNode<"bitwise_and"> {
}
export const createBitwiseAndNode = createBinaryNodeFactory<TwingBitwiseAndNode>("bitwise_and", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) & await right.execute(executionContext);
}
});
export const createBitwiseAndNode = createBinaryNodeFactory<TwingBitwiseAndNode>("bitwise_and",);

View File

@ -4,8 +4,4 @@ import {createBinaryNodeFactory} from "../binary";
export interface TwingBitwiseOrNode extends TwingBaseBinaryNode<"bitwise_or"> {
}
export const createBitwiseOrNode = createBinaryNodeFactory<TwingBitwiseOrNode>("bitwise_or", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) | await right.execute(executionContext);
}
});
export const createBitwiseOrNode = createBinaryNodeFactory<TwingBitwiseOrNode>("bitwise_or");

View File

@ -4,8 +4,4 @@ import {createBinaryNodeFactory} from "../binary";
export interface TwingBitwiseXorNode extends TwingBaseBinaryNode<"bitwise_xor"> {
}
export const createBitwiseXorNode = createBinaryNodeFactory<TwingBitwiseXorNode>("bitwise_xor", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) ^ await right.execute(executionContext);
}
});
export const createBitwiseXorNode = createBinaryNodeFactory<TwingBitwiseXorNode>("bitwise_xor");

View File

@ -1,15 +1,7 @@
import type {TwingBaseBinaryNode} from "../binary";
import {createBinaryNodeFactory} from "../binary";
import {concatenate} from "../../../helpers/concatenate";
export interface TwingConcatenateNode extends TwingBaseBinaryNode<"concatenate"> {
}
export const createConcatenateNode = createBinaryNodeFactory<TwingConcatenateNode>("concatenate", {
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
return concatenate(leftValue, rightValue);
}
});
export const createConcatenateNode = createBinaryNodeFactory<TwingConcatenateNode>("concatenate");

View File

@ -4,8 +4,4 @@ import {createBinaryNodeFactory} from "../binary";
export interface TwingDivideAndFloorNode extends TwingBaseBinaryNode<"divide_and_floor"> {
}
export const createDivideAndFloorNode = createBinaryNodeFactory<TwingDivideAndFloorNode>("divide_and_floor", {
execute: async (left, right, executionContext) => {
return Math.floor(await left.execute(executionContext) / await right.execute(executionContext));
}
});
export const createDivideAndFloorNode = createBinaryNodeFactory<TwingDivideAndFloorNode>("divide_and_floor");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingDivideNode extends TwingBaseBinaryNode<"divide"> {
}
export const createDivideNode = createBinaryNodeFactory<TwingDivideNode>("divide", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) / await right.execute(executionContext);
}
});
export const createDivideNode = createBinaryNodeFactory<TwingDivideNode>("divide");

View File

@ -4,23 +4,4 @@ export interface TwingEndsWithNode extends TwingBaseBinaryNode<"ends_with"> {
}
export const createEndsWithNode = createBinaryNodeFactory<TwingEndsWithNode>(
"ends_with",
{
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
if (typeof leftValue !== "string") {
return false;
}
const rightValue = await right.execute(executionContext);
if (typeof rightValue !== "string") {
return false;
}
return rightValue.length < 1 || leftValue.endsWith(rightValue);
}
}
);
export const createEndsWithNode = createBinaryNodeFactory<TwingEndsWithNode>("ends_with");

View File

@ -1,26 +1,7 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {every, isAMapLike} from "../../../helpers/map-like";
export interface TwingHasEveryNode extends TwingBaseBinaryNode<"has_every"> {
}
export const createHasEveryNode = createBinaryNodeFactory<TwingHasEveryNode>(
"has_every",
{
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
if (typeof rightValue !== "function") {
return Promise.resolve(true);
}
if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) {
return Promise.resolve(true);
}
return every(leftValue, rightValue);
}
}
);
export const createHasEveryNode = createBinaryNodeFactory<TwingHasEveryNode>("has_every");

View File

@ -1,26 +1,7 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {isAMapLike, some} from "../../../helpers/map-like";
export interface TwingHasSomeNode extends TwingBaseBinaryNode<"has_some"> {
}
export const createHasSomeNode = createBinaryNodeFactory<TwingHasSomeNode>(
"has_some",
{
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
if (typeof rightValue !== "function") {
return Promise.resolve(false);
}
if (!isAMapLike(leftValue) && !Array.isArray(leftValue)) {
return Promise.resolve(false);
}
return some(leftValue, rightValue);
}
}
);
export const createHasSomeNode = createBinaryNodeFactory<TwingHasSomeNode>("has_some");

View File

@ -1,14 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {compare} from "../../../helpers/compare";
export interface TwingIsEqualToNode extends TwingBaseBinaryNode<"is_equal_to"> {
}
export const createIsEqualNode = createBinaryNodeFactory<TwingIsEqualToNode>("is_equal_to", {
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
return compare(leftValue, rightValue);
}
});
export const createIsEqualNode = createBinaryNodeFactory<TwingIsEqualToNode>("is_equal_to");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingIsGreaterThanOrEqualToNode extends TwingBaseBinaryNode<"is_greater_than_or_equal_to"> {
}
export const createIsGreaterThanOrEqualToNode = createBinaryNodeFactory<TwingIsGreaterThanOrEqualToNode>("is_greater_than_or_equal_to", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) >= await right.execute(executionContext);
}
});
export const createIsGreaterThanOrEqualToNode = createBinaryNodeFactory<TwingIsGreaterThanOrEqualToNode>("is_greater_than_or_equal_to");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingIsGreaterThanNode extends TwingBaseBinaryNode<"is_greater_than"> {
}
export const createIsGreaterThanNode = createBinaryNodeFactory<TwingIsGreaterThanNode>("is_greater_than", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) > await right.execute(executionContext);
}
});
export const createIsGreaterThanNode = createBinaryNodeFactory<TwingIsGreaterThanNode>("is_greater_than");

View File

@ -1,11 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {isIn} from "../../../helpers/is-in";
export interface TwingIsInNode extends TwingBaseBinaryNode<"is_in"> {
}
export const createIsInNode = createBinaryNodeFactory<TwingIsInNode>("is_in", {
execute: async (left, right, executionContext) => {
return isIn(await left.execute(executionContext), await right.execute(executionContext));
}
});
export const createIsInNode = createBinaryNodeFactory<TwingIsInNode>("is_in");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingIsLessThanOrEqualToNode extends TwingBaseBinaryNode<"is_less_than_or_equal_to"> {
}
export const createIsLessThanOrEqualToNode = createBinaryNodeFactory<TwingIsLessThanOrEqualToNode>("is_less_than_or_equal_to", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) <= await right.execute(executionContext);
}
});
export const createIsLessThanOrEqualToNode = createBinaryNodeFactory<TwingIsLessThanOrEqualToNode>("is_less_than_or_equal_to");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingIsLessThanNode extends TwingBaseBinaryNode<"is_less_than"> {
}
export const createIsLessThanNode = createBinaryNodeFactory<TwingIsLessThanNode>("is_less_than", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) < await right.execute(executionContext);
}
});
export const createIsLessThanNode = createBinaryNodeFactory<TwingIsLessThanNode>("is_less_than");

View File

@ -1,11 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {compare} from "../../../helpers/compare";
export interface TwingIsNotEqualToNode extends TwingBaseBinaryNode<"is_not_equal_to"> {
}
export const createIsNotEqualToNode = createBinaryNodeFactory<TwingIsNotEqualToNode>("is_not_equal_to", {
execute: async (left, right, executionContext) => {
return Promise.resolve(!compare(await left.execute(executionContext), await right.execute(executionContext)))
}
});
export const createIsNotEqualToNode = createBinaryNodeFactory<TwingIsNotEqualToNode>("is_not_equal_to");

View File

@ -1,11 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {isIn} from "../../../helpers/is-in";
export interface TwingIsNotInNode extends TwingBaseBinaryNode<"is_not_in"> {
}
export const createIsNotInNode = createBinaryNodeFactory<TwingIsNotInNode>("is_not_in", {
execute: async (left, right, executionContext) => {
return Promise.resolve(!isIn(await left.execute(executionContext), await right.execute(executionContext)))
}
});
export const createIsNotInNode = createBinaryNodeFactory<TwingIsNotInNode>("is_not_in");

View File

@ -1,15 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {parseRegularExpression} from "../../../helpers/parse-regular-expression";
export interface TwingMatchesNode extends TwingBaseBinaryNode<"matches"> {
}
export const createMatchesNode = createBinaryNodeFactory<TwingMatchesNode>("matches", {
execute: async (left, right, executionContext) => {
return parseRegularExpression(
await right.execute(executionContext)
).test(
await left.execute(executionContext)
);
}
});
export const createMatchesNode = createBinaryNodeFactory<TwingMatchesNode>("matches");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingModuloNode extends TwingBaseBinaryNode<"modulo"> {
}
export const createModuloNode = createBinaryNodeFactory<TwingModuloNode>("modulo", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) % await right.execute(executionContext);
}
});
export const createModuloNode = createBinaryNodeFactory<TwingModuloNode>("modulo");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingMultiplyNode extends TwingBaseBinaryNode<"multiply"> {
}
export const createMultiplyNode = createBinaryNodeFactory<TwingMultiplyNode>("multiply", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) * await right.execute(executionContext);
}
});
export const createMultiplyNode = createBinaryNodeFactory<TwingMultiplyNode>("multiply");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingOrNode extends TwingBaseBinaryNode<"or"> {
}
export const createOrNode = createBinaryNodeFactory<TwingOrNode>("or", {
execute: async (left, right, executionContext) => {
return !!(await left.execute(executionContext) || await right.execute(executionContext));
}
});
export const createOrNode = createBinaryNodeFactory<TwingOrNode>("or");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingPowerNode extends TwingBaseBinaryNode<"power"> {
}
export const createPowerNode = createBinaryNodeFactory<TwingPowerNode>("power", {
execute: async (left, right, executionContext) => {
return Math.pow(await left.execute(executionContext), await right.execute(executionContext));
}
});
export const createPowerNode = createBinaryNodeFactory<TwingPowerNode>("power");

View File

@ -1,14 +1,6 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {createRange} from "../../../helpers/create-range";
export interface TwingRangeNode extends TwingBaseBinaryNode<"range"> {
}
export const createRangeNode = createBinaryNodeFactory<TwingRangeNode>("range", {
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
return createRange(leftValue, rightValue, 1);
}
});
export const createRangeNode = createBinaryNodeFactory<TwingRangeNode>("range");

View File

@ -1,15 +1,7 @@
import type {TwingBaseBinaryNode} from "../binary";
import {createBinaryNodeFactory} from "../binary";
import {compare} from "../../../helpers/compare";
export interface TwingSpaceshipNode extends TwingBaseBinaryNode<"spaceship"> {
}
export const createSpaceshipNode = createBinaryNodeFactory<TwingSpaceshipNode>("spaceship", {
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
const rightValue = await right.execute(executionContext);
return compare(leftValue, rightValue) ? 0 : (leftValue < rightValue ? -1 : 1);
}
});
export const createSpaceshipNode = createBinaryNodeFactory<TwingSpaceshipNode>("spaceship");

View File

@ -3,20 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingStartsWithNode extends TwingBaseBinaryNode<"starts_with"> {
}
export const createStartsWithNode = createBinaryNodeFactory<TwingStartsWithNode>("starts_with", {
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);
if (typeof leftValue !== "string") {
return false;
}
const rightValue = await right.execute(executionContext);
if (typeof rightValue !== "string") {
return false;
}
return rightValue.length < 1 || leftValue.startsWith(rightValue);
}
});
export const createStartsWithNode = createBinaryNodeFactory<TwingStartsWithNode>("starts_with");

View File

@ -3,8 +3,4 @@ import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingSubtractNode extends TwingBaseBinaryNode<"subtract"> {
}
export const createSubtractNode = createBinaryNodeFactory<TwingSubtractNode>("subtract", {
execute: async (left, right, executionContext) => {
return await left.execute(executionContext) - await right.execute(executionContext);
}
});
export const createSubtractNode = createBinaryNodeFactory<TwingSubtractNode>("subtract");

View File

@ -1,7 +1,5 @@
import {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes, createBaseExpressionNode} from "../expression";
import {TwingBaseNode} from "../../node";
import {TwingTemplate} from "../../template";
import {getTraceableMethod} from "../../helpers/traceable-method";
export type TwingBlockFunctionNodeAttributes = TwingBaseExpressionNodeAttributes & {
shouldTestExistence: boolean;
@ -30,55 +28,9 @@ export const createBlockFunctionNode = (
children.template = template;
}
const baseNode = createBaseExpressionNode("block_function", {
return createBaseExpressionNode("block_function", {
shouldTestExistence: false
}, children, line, column, tag);
const blockFunctionNode: TwingBlockFunctionNode = {
...baseNode,
execute: async (executionContext) => {
const {template, context, outputBuffer, blocks, sandboxed, sourceMapRuntime} = executionContext;
const {template: templateNode, name: blockNameNode} = blockFunctionNode.children;
const blockName = await blockNameNode.execute(executionContext);
let resolveTemplate: Promise<TwingTemplate>;
if (templateNode) {
const templateName = await templateNode.execute(executionContext);
const loadTemplate = getTraceableMethod(
template.loadTemplate,
templateNode.line,
templateNode.column,
template.name
);
resolveTemplate = loadTemplate(templateName);
} else {
resolveTemplate = Promise.resolve(template)
}
return resolveTemplate
.then<Promise<boolean | string>>((executionContextOfTheBlock) => {
if (blockFunctionNode.attributes.shouldTestExistence) {
const hasBlock = getTraceableMethod(executionContextOfTheBlock.hasBlock, blockFunctionNode.line, blockFunctionNode.column, template.name);
return hasBlock(blockName, context.clone(), outputBuffer, blocks, sandboxed);
} else {
const renderBlock = getTraceableMethod(executionContextOfTheBlock.renderBlock, blockFunctionNode.line, blockFunctionNode.column, template.name);
if (templateNode) {
return renderBlock(blockName, context.clone(), outputBuffer, new Map(), false, sandboxed, sourceMapRuntime);
} else {
return renderBlock(blockName, context.clone(), outputBuffer, blocks, true, sandboxed, sourceMapRuntime);
}
}
});
}
};
return blockFunctionNode;
};
export const cloneBlockReferenceExpressionNode = (

View File

@ -4,17 +4,10 @@ import {
createBaseExpressionNode
} from "../expression";
import {TwingBaseNode} from "../../node";
import {TwingConstantNode, createConstantNode} from "./constant";
import {TwingArrayNode, getKeyValuePairs} from "./array";
import {TwingCallableArgument, TwingCallableWrapper} from "../../callable-wrapper";
import {TwingArrayNode} from "./array";
import type {TwingFilterNode} from "./call/filter";
import type {TwingFunctionNode} from "./call/function";
import type {TwingTestNode} from "./call/test";
import {createRuntimeError} from "../../error/runtime";
import {getTraceableMethod} from "../../helpers/traceable-method";
const array_merge = require('locutus/php/array/array_merge');
const snakeCase = require('snake-case');
export type TwingCallNode =
| TwingFilterNode
@ -51,174 +44,7 @@ export const createBaseCallNode = <Type extends "filter" | "function" | "test">(
children.operand = operand;
}
const baseNode: TwingBaseExpressionNode<typeof type, TwingBaseCallNodeAttributes, typeof children> = createBaseExpressionNode(type, {
return createBaseExpressionNode(type, {
operatorName
}, children, line, column);
const normalizeName = (name: string) => {
return snakeCase(name).toLowerCase();
};
const getArguments = (
argumentsNode: TwingArrayNode,
acceptedArguments: Array<TwingCallableArgument>,
isVariadic: boolean
): Array<TwingBaseNode> => {
const callType = type;
const callName = baseNode.attributes.operatorName;
const parameters: Map<string | number, {
key: TwingConstantNode;
value: TwingBaseExpressionNode;
}> = new Map();
let named = false;
const keyPairs = getKeyValuePairs(argumentsNode);
for (let {key, value} of keyPairs) {
let name = key.attributes.value as string | number;
if (typeof name === "string") {
named = true;
name = normalizeName(name);
}
else if (named) {
throw createRuntimeError(`Positional arguments cannot be used after named arguments for ${callType} "${callName}".`, baseNode);
}
parameters.set(name, {
key,
value
});
}
const callableParameters = acceptedArguments;
const names: Array<string> = [];
let optionalArguments: Array<string | TwingConstantNode> = [];
let arguments_: Array<TwingBaseNode> = [];
let position = 0;
for (const callableParameter of callableParameters) {
const name = '' + normalizeName(callableParameter.name);
names.push(name);
const parameter = parameters.get(name);
if (parameter) {
if (parameters.has(position)) {
throw createRuntimeError(`Argument "${name}" is defined twice for ${callType} "${callName}".`, baseNode);
}
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(parameter.value);
parameters.delete(name);
optionalArguments = [];
}
else {
const parameter = parameters.get(position);
if (parameter) {
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(parameter.value);
parameters.delete(position);
optionalArguments = [];
++position;
}
else if (callableParameter.defaultValue !== undefined) {
arguments_.push(createConstantNode(callableParameter.defaultValue, line, column));
}
else {
throw createRuntimeError(`Value for argument "${name}" is required for ${callType} "${callName}".`, baseNode);
}
}
}
if (isVariadic) {
const resolvedKeys: Array<any> = [];
const arbitraryArguments: Array<TwingBaseExpressionNode> = [];
for (const [key, value] of parameters) {
arbitraryArguments.push(value.value);
resolvedKeys.push(key);
}
for (const key of resolvedKeys) {
parameters.delete(key);
}
if (arbitraryArguments.length) {
arguments_ = array_merge(arguments_, optionalArguments);
arguments_.push(...arbitraryArguments);
}
}
if (parameters.size > 0) {
const unknownParameter = [...parameters.values()][0];
throw createRuntimeError(`Unknown argument${parameters.size > 1 ? 's' : ''} "${[...parameters.keys()].join('", "')}" for ${callType} "${callName}(${names.join(', ')})".`, unknownParameter.key);
}
return arguments_;
}
const baseCallNode: TwingBaseCallNode<typeof type> = {
...baseNode,
execute: async (executionContext) => {
const {template} = executionContext
const {operatorName} = baseCallNode.attributes;
let callableWrapper: TwingCallableWrapper | null;
switch (type) {
case "filter":
callableWrapper = template.getFilter(operatorName);
break;
case "function":
callableWrapper = template.getFunction(operatorName);
break;
// for some reason, using `case "test"` makes the compiler assume that callableWrapper is used
// before it is assigned a value; this is probably a bug of the compiler
default:
callableWrapper = template.getTest(operatorName);
break;
}
if (callableWrapper === null) {
throw createRuntimeError(`Unknown ${type} "${operatorName}".`, baseNode);
}
const {operand, arguments: callArguments} = baseCallNode.children;
const argumentNodes = getArguments(
callArguments,
callableWrapper.acceptedArguments,
callableWrapper.isVariadic
);
const actualArguments: Array<any> = [];
actualArguments.push(...callableWrapper!.nativeArguments);
if (operand) {
actualArguments.push(await operand.execute(executionContext));
}
const providedArguments = await Promise.all([
...argumentNodes.map((node) => node.execute(executionContext))
]);
actualArguments.push(...providedArguments);
const traceableCallable = getTraceableMethod(callableWrapper.callable, baseCallNode.line, baseCallNode.column, template.name);
return traceableCallable(executionContext, ...actualArguments);
}
};
return baseCallNode;
};

View File

@ -12,7 +12,5 @@ export const createFilterNode = (
line: number,
column: number
): TwingFilterNode => {
const node = createBaseCallNode("filter", filterName, operand, filterArguments, line, column);
return node;
return createBaseCallNode("filter", filterName, operand, filterArguments, line, column);
};

View File

@ -10,7 +10,5 @@ export const createFunctionNode = (
line: number,
column: number
): TwingFunctionNode => {
const node = createBaseCallNode("function", functionName, null, functionArguments, line, column);
return node;
return createBaseCallNode("function", functionName, null, functionArguments, line, column);
};

View File

@ -12,7 +12,5 @@ export const createTestNode = (
line: number,
column: number
): TwingTestNode => {
const node = createBaseCallNode("test", testName, operand, testArguments, line, column);
return node;
return createBaseCallNode("test", testName, operand, testArguments, line, column);
};

View File

@ -23,18 +23,9 @@ export const createBaseConditionalNode = <Type extends string>(
line: number,
column: number
): TwingBaseConditionalNode<Type> => {
const baseNode = createBaseExpressionNode(type, {}, {
return createBaseExpressionNode(type, {}, {
expr1, expr2, expr3
}, line, column);
return {
...baseNode,
execute: async (executionContext) => {
const {expr1, expr2, expr3} = baseNode.children;
return (await expr1.execute(executionContext)) ? expr2.execute(executionContext) : expr3.execute(executionContext);
}
};
};
export const createConditionalNode = (

View File

@ -15,16 +15,7 @@ export const createConstantNode = <Value extends string | number | boolean | nul
line: number,
column: number
): TwingConstantNode<Value> => {
const parent = createBaseExpressionNode("constant", {
return createBaseExpressionNode("constant", {
value
}, {}, line, column);
const constantNode: TwingConstantNode<Value> = {
...parent,
execute: () => {
return Promise.resolve(constantNode.attributes.value);
}
};
return constantNode;
};

View File

@ -1,6 +1,5 @@
import {TwingBaseNodeAttributes, TwingBaseNode} from "../../node";
import {TwingBaseExpressionNode, createBaseExpressionNode} from "../expression";
import {getTraceableMethod} from "../../helpers/traceable-method";
export interface TwingEscapeNodeAttributes extends TwingBaseNodeAttributes {
strategy: string;
@ -15,27 +14,9 @@ export const createEscapeNode = (
body: TwingBaseNode,
strategy: string
): TwingEscapeNode => {
const baseNode = createBaseExpressionNode("escape", {
return createBaseExpressionNode("escape", {
strategy
}, {
body
}, body.line, body.column);
const escapeNode: TwingEscapeNode = {
...baseNode,
execute: (executionContext) => {
const {template} = executionContext;
const {strategy} = escapeNode.attributes;
const {body} = escapeNode.children;
return body.execute(executionContext)
.then((value) => {
const escape = getTraceableMethod(template.escape, escapeNode.line, escapeNode.column, template.name);
return escape(value, strategy, null, true);
});
}
};
return escapeNode;
};

Some files were not shown because too many files have changed in this diff Show More