Merge branch 'milestone/7.0.0'

This commit is contained in:
Eric MORAND 2024-07-25 08:32:04 +00:00
commit 599a510b32
1059 changed files with 4402 additions and 4416 deletions

8
.gitignore vendored
View File

@ -1,9 +1,3 @@
.nyc_output
dist
coverage
docs/.bundle
docs/_site
docs/vendor
node_modules
package-lock.json
tmp
package-lock.json

View File

@ -1,35 +0,0 @@
{
"all": true,
"lines": 100,
"branches": 100,
"functions": 100,
"watermarks": {
"lines": [
100,
100
],
"functions": [
100,
100
],
"branches": [
100,
100
],
"statements": [
100,
100
]
},
"extension": [
".ts"
],
"reporter": [
"text-summary",
"html"
],
"include": [
"**/src/**"
],
"exclude": []
}

9
.odzrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"reporters": [
"text",
"html"
],
"sources": [
"src/main/**/*.ts"
]
}

View File

@ -7,7 +7,7 @@ First-class TypeScript and JavaScript Twig compiler
This projects needs at least **node.js 16.0.0** to run.
It is also strongly recommended to have [ts-node](https://www.npmjs.com/package/ts-node) and [nyc](https://www.npmjs.com/package/nyc) installed globally to ease the writing of tests and the tracking of the code coverage.
It is also strongly recommended to have [ts-node](https://www.npmjs.com/package/ts-node) and [One Double Zero](https://www.npmjs.com/package/one-double-zero) installed globally to ease the writing of tests and the tracking of the code coverage.
## Usage
@ -39,22 +39,22 @@ npm run test:browser
### Writing and executing tests
Assuming one want to execute the test located in `test/tests/integration/comparison/to-array.ts`, one would run:
Assuming one want to execute the test located in `src/test/tests/integration/comparison/to-array.ts`, one would run:
```shell
ts-node test/tests/integration/comparison/to-array.ts
ts-node src/test/tests/integration/comparison/to-array.ts
```
It is even possible - and recommended - to track the coverage while writing tests:
```shell
nyc ts-node test/tests/integration/comparison/to-array.ts
odz ts-node src/test/tests/integration/comparison/to-array.ts
```
Of course, it is also perfectly possible to pipe the result of the test to your favorite tap formatter:
```shell
test/tests/integration/comparison$ ts-node . | tap-nyan
src/test/tests/integration/comparison$ ts-node . | tap-nyan
9 -_-_-_-_-_,------,
0 -_-_-_-_-_| /\_/\
0 -_-_-_-_-^|__( ^ .^)

View File

@ -34,9 +34,9 @@
"test": "node dist/test",
"test:browser": "browserify dist/test/index.js | tape-run --sandbox=false",
"build": "npm run build:cjs && npm run build:types && npm run bundle",
"build:cjs": "rimraf dist/cjs && tsc --project . --module commonjs --outDir dist/cjs",
"build:test": "rimraf dist && tsc --project test --outDir dist",
"build:types": "rimraf dist/types && tsc --project . --declaration true --emitDeclarationOnly true --outDir dist/types",
"build:cjs": "rimraf dist/cjs && tsc --project src/main --module commonjs --outDir dist/cjs",
"build:test": "rimraf dist && tsc --project src/test --outDir dist",
"build:types": "rimraf dist/types && tsc --project src/main --declaration true --emitDeclarationOnly true --outDir dist/types",
"bundle": "browserify dist/cjs/lib.js -g uglifyify -s Twing -o dist/lib.min.js"
},
"dependencies": {
@ -44,11 +44,11 @@
"create-hash": "^1.2.0",
"esrever": "^0.2.0",
"htmlspecialchars": "^1.0.5",
"iconv-lite": "^0.4.19",
"iconv-lite": "^0.6.3",
"is-plain-object": "^2.0.4",
"isobject": "^3.0.1",
"levenshtein": "^1.0.5",
"locutus": "^2.0.11",
"locutus": "^2.0.31",
"luxon": "^1.19.3",
"pad": "^2.0.3",
"regex-parser": "^2.2.8",

View File

@ -1,32 +0,0 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
export const templateLoadingError = 'TwingTemplateLoadingError';
/**
* Exception thrown when an error occurs during template loading.
*/
export interface TwingTemplateLoadingError extends TwingBaseError<typeof templateLoadingError> {
}
export const createTemplateLoadingError = (names: Array<string | null>, location?: TwingErrorLocation, source?: string, previous?: any): TwingTemplateLoadingError => {
let message: string;
if (names.length === 1) {
const name = names[0];
message = `Unable to find template "${name ? name : ''}".`;
} else {
message = `Unable to find one of the following templates: "${names.join('", "')}".`;
}
const error = createBaseError(templateLoadingError, message, location, source, previous);
Error.captureStackTrace(error, createTemplateLoadingError);
return error;
};
export const isATemplateLoadingError = (candidate: any): candidate is TwingTemplateLoadingError => {
return (candidate as TwingTemplateLoadingError).name === templateLoadingError;
};

View File

@ -1,28 +0,0 @@
import type {TwingEscapingStrategyHandler} from "../escaping-strategy";
const phpBin2hex = require("locutus/php/strings/bin2hex");
const phpLtrim = require('locutus/php/strings/ltrim');
const strlen = require('utf8-binary-cutter').getBinarySize;
export const createCssEscapingStrategyHandler = (): TwingEscapingStrategyHandler => {
return (value) => {
value = value.replace(/[^a-zA-Z0-9]/ug, (matches: string) => {
let char = matches;
// \xHH
if (strlen(char) === 1) {
let hex = phpLtrim(phpBin2hex(char).toUpperCase(), '0');
if (strlen(hex) === 0) {
hex = '0';
}
return '\\' + hex + ' ';
}
// \uHHHH
return '\\' + phpLtrim(phpBin2hex(char).toUpperCase(), '0') + ' ';
});
return value;
}
};

View File

@ -1,21 +0,0 @@
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Loads a template from a string.
*
* <pre>
* {{ include(template_from_string("Hello {{ name }}")) }}
* </pre>
*
* @param executionContext A TwingTemplate instance
* @param string A template as a string or object implementing toString()
* @param name An optional name for the template to be used in error messages
*
* @returns {Promise<TwingTemplate>}
*/
export const templateFromString: TwingCallable = (executionContext, string: string, name: string | null): Promise<TwingTemplate> => {
const {template} = executionContext;
return template.createTemplateFromString(string, name);
}

View File

@ -1,23 +0,0 @@
import {isATwingError} from "../error";
import {createRuntimeError} from "../error/runtime";
export function getTraceableMethod<M extends (...args: Array<any>) => Promise<any>>(method: M, line: number, column: number, templateName: string): M {
return ((...args: Array<any>) => {
return method(...args)
.catch((error) => {
if (isATwingError(error)) {
if (error.location === undefined) {
error.location = {line, column};
error.source = templateName;
}
} else {
throw createRuntimeError(`An exception has been thrown during the rendering of a template ("${error.message}").`, {
line,
column
}, templateName, error);
}
throw error;
});
}) as typeof method;
}

View File

@ -1,58 +0,0 @@
import {createBaseNode, TwingBaseNode, TwingBaseNodeAttributes} from "../node";
import {getKeyValuePairs, TwingArrayNode} from "./expression/array";
import {createFilterNode} from "./expression/call/filter";
import {createConstantNode} from "./expression/constant";
export type TwingApplyNodeAttributes = TwingBaseNodeAttributes & {};
export type TwingApplyNodeChildren = {
body: TwingBaseNode;
filters: TwingArrayNode;
};
export interface TwingApplyNode extends TwingBaseNode<"apply", TwingApplyNodeAttributes, TwingApplyNodeChildren> {
}
export const createApplyNode = (
filters: TwingArrayNode,
body: TwingBaseNode,
line: number,
column: number
): TwingApplyNode => {
const baseNode = 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

@ -1,42 +0,0 @@
import {createBaseNode, TwingBaseNode, TwingBaseNodeAttributes} from "../node";
import {getTraceableMethod} from "../helpers/traceable-method";
export type TwingBlockReferenceNodeAttributes = TwingBaseNodeAttributes & {
name: string;
};
export interface TwingBlockReferenceNode extends TwingBaseNode<"block_reference", TwingBlockReferenceNodeAttributes, {}> {
}
export const createBlockReferenceNode = (
name: string,
line: number,
column: number,
tag: string
): TwingBlockReferenceNode => {
const outputNode = 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

@ -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,67 +0,0 @@
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>;
};
export interface TwingCheckSecurityNode extends TwingBaseNode<"check_security", TwingCheckSecurityNodeAttributes> {
}
export const createCheckSecurityNode = (
usedFilters: Map<string, TwingNode>,
usedTags: Map<string, TwingNode>,
usedFunctions: Map<string, TwingNode>,
line: number,
column: number
): TwingCheckSecurityNode => {
const baseNode = 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,55 +0,0 @@
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.
*
* For instance, when there is a simple Print statement, like {{ article }},
* and if the sandbox is enabled, we need to check that the toString()
* method is allowed if 'article' is an object. The same goes for {{ article|upper }}
* or {{ random(article) }}.
*/
export interface TwingCheckToStringNode extends TwingBaseNode<"check_to_string", TwingBaseNodeAttributes, {
value: TwingBaseExpressionNode;
}> {
}
export const createCheckToStringNode = (
value: TwingBaseExpressionNode,
line: number,
column: number
): TwingCheckToStringNode => {
const baseNode = 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

@ -1,33 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import {TwingBaseExpressionNode} from "./expression";
export interface TwingDeprecatedNode extends TwingBaseNode<"deprecated", TwingBaseNodeAttributes, {
message: TwingBaseExpressionNode;
}> {
}
export const createDeprecatedNode = (
message: TwingBaseExpressionNode,
line: number,
column: number,
tag: string
): TwingDeprecatedNode => {
const baseNode = 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

@ -1,46 +0,0 @@
import type {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes} from "../expression";
import type {TwingBaseNode} from "../../node";
import type {TwingAssignmentNode} from "./assignment";
import {createBaseExpressionNode} from "../expression";
export interface TwingArrowFunctionNode extends TwingBaseExpressionNode<"arrow_function", TwingBaseExpressionNodeAttributes, {
body: TwingBaseExpressionNode;
names: TwingBaseNode<any, any, Record<string, TwingAssignmentNode>>;
}> {
}
export const createArrowFunctionNode = (
body: TwingBaseExpressionNode,
names: TwingBaseNode<any, any, Record<any, TwingAssignmentNode>>,
line: number,
column: number
): TwingArrowFunctionNode => {
const baseNode = 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

@ -1,15 +0,0 @@
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);
}
});

View File

@ -1,26 +0,0 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
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);
}
}
);

View File

@ -1,26 +0,0 @@
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);
}
}
);

View File

@ -1,26 +0,0 @@
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);
}
}
);

View File

@ -1,14 +0,0 @@
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);
}
});

View File

@ -1,11 +0,0 @@
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));
}
});

View File

@ -1,11 +0,0 @@
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)))
}
});

View File

@ -1,11 +0,0 @@
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)))
}
});

View File

@ -1,15 +0,0 @@
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)
);
}
});

View File

@ -1,14 +0,0 @@
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);
}
});

View File

@ -1,15 +0,0 @@
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);
}
});

View File

@ -1,22 +0,0 @@
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);
}
});

View File

@ -1,93 +0,0 @@
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;
};
export type TwingBlockFunctionNodeChildren = {
name: TwingBaseNode;
template?: TwingBaseNode;
};
export interface TwingBlockFunctionNode extends TwingBaseExpressionNode<"block_function", TwingBlockFunctionNodeAttributes, TwingBlockFunctionNodeChildren> {
}
export const createBlockFunctionNode = (
name: TwingBaseNode,
template: TwingBaseNode | null,
line: number,
column: number,
tag?: string
): TwingBlockFunctionNode => {
const children: TwingBlockFunctionNodeChildren = {
name
};
if (template) {
children.template = template;
}
const baseNode = 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 = (
blockFunctionNode: TwingBlockFunctionNode
): TwingBlockFunctionNode => {
return createBlockFunctionNode(
blockFunctionNode.children.name,
blockFunctionNode.children.template || null,
blockFunctionNode.line,
blockFunctionNode.column
);
};

View File

@ -1,224 +0,0 @@
import {
TwingBaseExpressionNode,
TwingBaseExpressionNodeAttributes,
createBaseExpressionNode
} from "../expression";
import {TwingBaseNode} from "../../node";
import {TwingConstantNode, createConstantNode} from "./constant";
import {TwingArrayNode, getKeyValuePairs} from "./array";
import {TwingCallableArgument, TwingCallableWrapper} from "../../callable-wrapper";
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
| TwingFunctionNode
| TwingTestNode
;
export type TwingBaseCallNodeAttributes = TwingBaseExpressionNodeAttributes & {
operatorName: string;
};
export type TwingBaseCallNodeChildren = {
arguments: TwingArrayNode;
operand?: TwingBaseNode;
};
export interface TwingBaseCallNode<Type extends "filter" | "function" | "test"> extends TwingBaseExpressionNode<Type, TwingBaseCallNodeAttributes, TwingBaseCallNodeChildren> {
}
export const createBaseCallNode = <Type extends "filter" | "function" | "test">(
type: Type,
operatorName: string,
operand: TwingBaseNode | null,
callArguments: TwingArrayNode,
line: number,
column: number
): TwingBaseCallNode<typeof type> => {
let children: TwingBaseCallNodeChildren = {
arguments: callArguments
};
if (operand !== null) {
children.operand = operand;
}
const baseNode: TwingBaseExpressionNode<typeof type, TwingBaseCallNodeAttributes, typeof children> = 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

@ -1,41 +0,0 @@
import {TwingBaseNodeAttributes, TwingBaseNode} from "../../node";
import {TwingBaseExpressionNode, createBaseExpressionNode} from "../expression";
import {getTraceableMethod} from "../../helpers/traceable-method";
export interface TwingEscapeNodeAttributes extends TwingBaseNodeAttributes {
strategy: string;
}
export interface TwingEscapeNode extends TwingBaseExpressionNode<"escape", TwingEscapeNodeAttributes, {
body: TwingBaseNode;
}> {
}
export const createEscapeNode = (
body: TwingBaseNode,
strategy: string
): TwingEscapeNode => {
const baseNode = 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;
};

View File

@ -1,46 +0,0 @@
import {TwingBaseArrayNode, createBaseArrayNode, getKeyValuePairs} from "./array";
import type {TwingBaseExpressionNode} from "../expression";
import type {TwingNode} from "../../node";
export interface TwingHashNode extends TwingBaseArrayNode<"hash"> {
}
export const createHashNode = (
elements: Array<{
key: TwingBaseExpressionNode;
value: TwingBaseExpressionNode;
}>,
line: number,
column: number
): TwingHashNode => {
const baseNode = createBaseArrayNode("hash", elements, line, column);
const hashNode: TwingHashNode = {
...baseNode,
execute: async (executionContext): Promise<Map<string, any>> => {
const keyValuePairs = getKeyValuePairs(hashNode);
const hash: Map<any, any> = new Map();
for (const {key: keyNode, value: valueNode} of keyValuePairs) {
const [key, value] = await Promise.all([
keyNode.execute(executionContext),
valueNode.execute(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;
}
};
return hashNode;
};

View File

@ -1,103 +0,0 @@
import {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes, createBaseExpressionNode} from "../expression";
import type {TwingArrayNode} from "./array";
import {getKeyValuePairs} from "./array";
import {TwingBaseNode} from "../../node";
import {TwingTemplateMacroHandler, TwingTemplate} from "../../template";
import {createRuntimeError} from "../../error/runtime";
export type TwingMethodCallNodeAttributes = TwingBaseExpressionNodeAttributes & {
methodName: string;
shouldTestExistence: boolean;
};
export interface TwingMethodCallNode extends TwingBaseExpressionNode<"method_call", TwingMethodCallNodeAttributes, {
operand: TwingBaseNode<any, {
name: string;
}>;
arguments: TwingArrayNode;
}> {
}
export const createMethodCallNode = (
operand: TwingBaseNode<any, {
name: string;
}>,
methodName: string,
methodArguments: TwingArrayNode,
line: number,
column: number
): TwingMethodCallNode => {
const baseNode = createBaseExpressionNode("method_call", {
methodName,
shouldTestExistence: false
}, {
operand,
arguments: methodArguments
}, line, column);
const methodCallNode: TwingMethodCallNode = {
...baseNode,
execute: async (executionContext) => {
const {template, context, outputBuffer, aliases, sandboxed, sourceMapRuntime} = executionContext;
const {methodName, shouldTestExistence} = baseNode.attributes;
const {operand, arguments: methodArguments} = baseNode.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 valueNode.execute(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)
.then((parent) => {
if (parent) {
return getHandler(parent);
} else {
return null;
}
});
}
};
return getHandler(macroTemplate)
.then((handler) => {
if (handler) {
return handler(outputBuffer, sandboxed, sourceMapRuntime, ...macroArguments);
} else {
throw createRuntimeError(`Macro "${methodName}" is not defined in template "${macroTemplate.name}".`, methodCallNode, template.name);
}
});
}
}
};
return methodCallNode;
};
export const cloneMethodCallNode = (
methodCallNode: TwingMethodCallNode
): TwingMethodCallNode => {
return createMethodCallNode(
methodCallNode.children.operand,
methodCallNode.attributes.methodName,
methodCallNode.children.arguments,
methodCallNode.line,
methodCallNode.column
);
};

View File

@ -1,68 +0,0 @@
import type {TwingBaseExpressionNodeAttributes} from "../expression";
import {createBaseNode, TwingBaseNode} from "../../node";
import {getContextValue} from "../../helpers/get-context-value";
import {getTraceableMethod} from "../../helpers/traceable-method";
import {mergeIterables} from "../../helpers/merge-iterables";
import {createContext} from "../../context";
export type TwingNameNodeAttributes = TwingBaseExpressionNodeAttributes & {
name: string;
isAlwaysDefined: boolean;
shouldIgnoreStrictCheck: boolean;
shouldTestExistence: boolean;
};
export interface TwingNameNode extends TwingBaseNode<"name", TwingNameNodeAttributes> {
}
export const createNameNode = (
name: string,
line: number,
column: number
): TwingNameNode => {
const attributes: TwingNameNode["attributes"] = {
name,
isAlwaysDefined: false,
shouldIgnoreStrictCheck: false,
shouldTestExistence: false
};
const baseNode = createBaseNode("name", attributes, {}, line, column);
const nameNode: TwingNameNode = {
...baseNode,
execute: async ({template, context, charset, isStrictVariables, globals}) => {
const {name, isAlwaysDefined, shouldIgnoreStrictCheck, shouldTestExistence} = nameNode.attributes;
const traceableGetContextValue = getTraceableMethod(
getContextValue,
nameNode.line,
nameNode.column,
template.name
);
return traceableGetContextValue(
charset,
template.name,
isStrictVariables,
createContext(mergeIterables(globals, context)),
name,
isAlwaysDefined,
shouldIgnoreStrictCheck,
shouldTestExistence
);
}
};
return nameNode;
};
export const cloneNameNode = (
nameNode: TwingNameNode
): TwingNameNode => {
return createNameNode(
nameNode.attributes.name,
nameNode.line,
nameNode.column
);
};

View File

@ -1,33 +0,0 @@
import {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes, createBaseExpressionNode} from "../expression";
import {getTraceableMethod} from "../../helpers/traceable-method";
export type ParentNodeAttributes = TwingBaseExpressionNodeAttributes & {
name: string;
};
export interface TwingParentFunctionNode extends TwingBaseExpressionNode<"parent_function", ParentNodeAttributes> {
}
export const createParentFunctionNode = (
name: string,
line: number,
column: number
): TwingParentFunctionNode => {
const baseNode = createBaseExpressionNode("parent_function", {
name,
//output: false
}, {}, line, column);
const parentFunctionNode: TwingParentFunctionNode = {
...baseNode,
execute: (executionContext) => {
const {template, context, outputBuffer, sandboxed, sourceMapRuntime} = executionContext;
const {name} = parentFunctionNode.attributes;
const renderParentBlock = getTraceableMethod(template.renderParentBlock, parentFunctionNode.line, parentFunctionNode.column, template.name);
return renderParentBlock(name, context, outputBuffer, sandboxed, sourceMapRuntime);
}
};
return parentFunctionNode;
};

View File

@ -1,49 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
export type TwingForLoopNodeAttributes = TwingBaseNodeAttributes & {
hasAnIf: boolean;
hasAnElse: boolean;
};
export interface TwingForLoopNode extends TwingBaseNode<"for_loop", TwingForLoopNodeAttributes> {
}
export const createForLoopNode = (
line: number,
column: number,
tag: string
): TwingForLoopNode => {
const baseNode = createBaseNode("for_loop", {
hasAnIf: false,
hasAnElse: false
}, {}, line, column, tag);
const forLoopNode: TwingForLoopNode = {
...baseNode,
execute: (executionContext) => {
const {hasAnElse, hasAnIf} = forLoopNode.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();
}
};
return forLoopNode;
};

View File

@ -1,148 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import type {TwingAssignmentNode} from "./expression/assignment";
import {createForLoopNode} from "./for-loop";
import {createIfNode} from "./if";
import type {TwingBaseExpressionNode} from "./expression";
import {TwingContext} from "../context";
import {ensureTraversable} from "../helpers/ensure-traversable";
import {count} from "../helpers/count";
import {iterate} from "../helpers/iterate";
export type TwingForNodeAttributes = TwingBaseNodeAttributes & {
hasAnIf: boolean;
};
export type TwingForNodeChildren = {
keyTarget: TwingAssignmentNode;
valueTarget: TwingAssignmentNode;
sequence: TwingBaseExpressionNode;
body: TwingBaseNode;
else?: TwingBaseNode;
};
export interface TwingForNode extends TwingBaseNode<"for", TwingForNodeAttributes, TwingForNodeChildren> {
}
export const createForNode = (
keyTarget: TwingAssignmentNode,
valueTarget: TwingAssignmentNode,
sequence: TwingBaseExpressionNode,
ifExpression: TwingBaseExpressionNode | null,
body: TwingBaseNode,
elseNode: TwingBaseNode | null,
line: number,
column: number,
tag: string
): TwingForNode => {
const loop = createForLoopNode(line, column, tag);
const bodyChildren: Record<number, TwingBaseNode> = {};
let i: number = 0;
bodyChildren[i++] = body;
bodyChildren[i++] = loop;
let actualBody: TwingBaseNode = createBaseNode(null, {}, bodyChildren, line, column);
if (ifExpression) {
const ifChildren: Record<number, TwingBaseNode> = {};
let i: number = 0;
ifChildren[i++] = ifExpression;
ifChildren[i++] = actualBody;
actualBody = createIfNode(createBaseNode(null, {}, ifChildren, line, column), null, line, column);
loop.attributes.hasAnIf = true;
}
const children: TwingForNodeChildren = {
keyTarget: keyTarget,
valueTarget: valueTarget,
sequence: sequence,
body: actualBody,
};
if (elseNode) {
children.else = elseNode;
loop.attributes.hasAnElse = true;
}
const baseNode = createBaseNode("for", {
hasAnIf: ifExpression !== null
}, children, line, column, tag);
const forNode: TwingForNode = {
...baseNode,
execute: async (executionContext) => {
const {context} = 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 sequenceNode.execute(executionContext);
let sequence = ensureTraversable(executedSequence);
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 targetKeyNode.execute(executionContext);
const targetValue = await targetValueNode.execute(executionContext);
await iterate(context.get('_seq'), async (key, value) => {
context.set(targetKey, key);
context.set(targetValue, value);
await body.execute(executionContext);
});
if (elseNode) {
if (context.get('_iterated') === false) {
await elseNode.execute(executionContext);
}
}
const parent = context.get('_parent');
context.delete('_seq');
context.delete('_iterated');
context.delete(keyTarget.attributes.name);
context.delete(valueTarget.attributes.name);
context.delete('_parent');
context.delete('loop');
for (const [key, value] of parent) {
if (!context.has(key)) {
context.set(key, value);
}
}
}
};
return forNode;
};

View File

@ -1,57 +0,0 @@
import {TwingBaseNode, createBaseNode, getChildrenCount, TwingBaseNodeAttributes} from "../node";
import {evaluate} from "../helpers/evaluate";
export type TwingIfNodeChildren = {
tests: TwingBaseNode;
else?: TwingBaseNode;
};
export interface TwingIfNode extends TwingBaseNode<'if', TwingBaseNodeAttributes, TwingIfNodeChildren> {
}
export const createIfNode = (
testNode: TwingBaseNode,
elseNode: TwingBaseNode | null,
line: number,
column: number,
tag: string | null = null
): TwingIfNode => {
const children: TwingIfNodeChildren = {
tests: testNode
};
if (elseNode) {
children.else = elseNode;
}
const baseNode = createBaseNode('if', {}, children, line, column, tag);
const ifNode: TwingIfNode = {
...baseNode,
execute: async (executionContext) => {
const count = getChildrenCount(testNode);
let index: number = 0;
while (index < count) {
const condition = testNode.children[index];
const conditionResult = await condition.execute(executionContext);
if (evaluate(conditionResult)) {
// the condition is satisfied, we execute the belonging body and return the result
const body = testNode.children[index + 1];
return body.execute(executionContext);
}
index += 2;
}
if (elseNode !== null) {
return elseNode.execute(executionContext);
}
}
};
return ifNode;
}

View File

@ -1,61 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import type {TwingBaseExpressionNode, TwingExpressionNode} from "./expression";
import type {TwingAssignmentNode} from "./expression/assignment";
import type {TwingTemplate} from "../template";
import {getTraceableMethod} from "../helpers/traceable-method";
export type TwingImportNodeAttributes = TwingBaseNodeAttributes & {
global: boolean;
};
export interface TwingImportNode extends TwingBaseNode<"import", TwingImportNodeAttributes, {
templateName: TwingBaseExpressionNode;
alias: TwingAssignmentNode;
}> {
}
export const createImportNode = (
templateName: TwingExpressionNode,
alias: TwingAssignmentNode,
global: boolean,
line: number,
column: number,
tag: string
): TwingImportNode => {
const baseNode = createBaseNode("import", {
global
}, {
templateName,
alias
}, line, column, tag);
const importNode: TwingImportNode = {
...baseNode,
execute: async (executionContext) => {
const {template, aliases} = executionContext;
const {alias: aliasNode, templateName: templateNameNode} = baseNode.children;
const {global} = baseNode.attributes;
let aliasValue: TwingTemplate;
if (templateNameNode.type === "name" && templateNameNode.attributes.name === '_self') {
aliasValue = template;
} else {
const templateName = await templateNameNode.execute(executionContext);
const loadTemplate = getTraceableMethod(template.loadTemplate, importNode.line, importNode.column, template.name);
aliasValue = await loadTemplate(templateName);
}
aliases.set(aliasNode.attributes.name, aliasValue);
if (global) {
template.aliases.set(aliasNode.attributes.name, aliasValue);
}
}
};
return importNode;
};

View File

@ -1,60 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import type {TwingBaseExpressionNode} from "./expression";
import type {TwingTemplate} from "../template";
import {getTraceableMethod} from "../helpers/traceable-method";
import {include} from "../extension/core/functions/include";
import type {TwingExecutionContext} from "../execution-context";
export type TwingBaseIncludeNodeAttributes = TwingBaseNodeAttributes & {
only: boolean;
ignoreMissing: boolean;
};
export type TwingBaseIncludeNodeChildren = {
variables: TwingBaseExpressionNode;
};
export interface TwingBaseIncludeNode<
Type extends string,
Attributes extends TwingBaseIncludeNodeAttributes = TwingBaseIncludeNodeAttributes,
Children extends TwingBaseIncludeNodeChildren = TwingBaseIncludeNodeChildren
> extends TwingBaseNode<Type, Attributes, Children> {
}
export const createBaseIncludeNode = <Type extends string, Attributes extends TwingBaseIncludeNodeAttributes, Children extends TwingBaseIncludeNodeChildren = TwingBaseIncludeNodeChildren>(
type: Type,
attributes: Attributes,
children: Children,
getTemplate: (executionContext: TwingExecutionContext) => Promise<TwingTemplate | null | Array<TwingTemplate | null>>,
line: number,
column: number,
tag: string
): TwingBaseIncludeNode<Type, Attributes, Children> => {
const baseNode = createBaseNode(type, attributes, children, line, column, tag);
const baseIncludeNode: TwingBaseIncludeNode<Type, Attributes, Children> = {
...baseNode,
execute: async (executionContext) => {
const {outputBuffer, sandboxed, template} = executionContext;
const {variables} = baseIncludeNode.children;
const {only, ignoreMissing} = baseIncludeNode.attributes;
const templatesToInclude = await getTemplate(executionContext);
const traceableInclude = getTraceableMethod(include, baseNode.line, baseNode.column, template.name);
const output = await traceableInclude(
executionContext,
templatesToInclude,
await variables.execute(executionContext),
!only,
ignoreMissing,
sandboxed
);
outputBuffer.echo(output);
}
};
return baseIncludeNode;
};

View File

@ -1,40 +0,0 @@
import {TwingBaseExpressionNode} from "./expression";
import {createBaseNode, TwingBaseNode} from "../node";
export interface TwingPrintNode extends TwingBaseNode<"print", {}, {
expression: TwingBaseExpressionNode;
}> {
}
export const createPrintNode = (
expression: TwingBaseExpressionNode,
line: number,
column: number
): TwingPrintNode => {
const outputNode: TwingPrintNode = createBaseNode("print", {}, {
expression: expression
}, line, column, null);
const printNode: TwingPrintNode = {
...outputNode,
execute: (executionContext) => {
const {template, outputBuffer, sourceMapRuntime} = executionContext;
sourceMapRuntime?.enterSourceMapBlock(printNode.line, printNode.column, printNode.type, template.source, outputBuffer);
return printNode.children.expression.execute(executionContext)
.then((result) => {
if (Array.isArray(result)) {
result = 'Array';
}
outputBuffer.echo(result);
sourceMapRuntime?.leaveSourceMapBlock(outputBuffer);
});
}
};
return printNode;
};

View File

@ -1,83 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode, TwingNode} from "../node";
import {createConstantNode} from "./expression/constant";
import {createWrapperNode} from "./wrapper";
export type TwingSetNodeAttributes = TwingBaseNodeAttributes & {
captures: boolean;
};
export interface TwingSetNode extends TwingBaseNode<"set", TwingSetNodeAttributes, {
names: TwingBaseNode;
values: TwingBaseNode;
}> {
}
export const createSetNode = (
captures: boolean,
names: TwingSetNode["children"]["names"],
values: TwingSetNode["children"]["values"],
line: number,
column: number,
tag: string
): TwingSetNode => {
const baseNode = createBaseNode("set", {
captures
}, {
names,
values
}, line, column, tag);
/*
* Optimizes the node when capture is used for a large block of text.
*
* {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig_Markup("foo");
*/
if (baseNode.attributes.captures) {
const values = baseNode.children.values as TwingNode;
if (values.type === "text") {
baseNode.children.values = createWrapperNode({
0: createConstantNode(values.attributes.data, values.line, values.column)
}, values.line, values.column);
baseNode.attributes.captures = false;
}
}
const setNode: TwingSetNode = {
...baseNode,
execute: async (executionContext) => {
const {context, outputBuffer} = executionContext;
const {names: namesNode, values: valuesNode} = setNode.children;
const {captures} = setNode.attributes;
const names: Array<string> = await namesNode.execute(executionContext);
if (captures) {
outputBuffer.start();
await valuesNode.execute(executionContext);
const value = outputBuffer.getAndClean();
for (const name of names) {
context.set(name, value);
}
}
else {
const values: Array<any> = await valuesNode.execute(executionContext);
let index = 0;
for (const name of names) {
const value = values[index];
context.set(name, value);
index++;
}
}
}
};
return setNode;
};

View File

@ -1,35 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
export interface TwingSpacelessNode extends TwingBaseNode<"spaceless", TwingBaseNodeAttributes, {
body: TwingBaseNode;
}> {
}
export const createSpacelessNode = (
body: TwingBaseNode,
line: number,
column: number,
tag: string
): TwingSpacelessNode => {
const baseNode = createBaseNode("spaceless", {}, {
body
}, line, column, tag);
const spacelessNode: TwingSpacelessNode = {
...baseNode,
execute: (executionContext) => {
const {outputBuffer} = executionContext;
outputBuffer.start();
return spacelessNode.children.body.execute(executionContext)
.then(() => {
const content = outputBuffer.getAndClean().replace(/>\s+</g, '><').trim();
outputBuffer.echo(content);
});
}
};
return spacelessNode;
};

View File

@ -1,80 +0,0 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode} from "../node";
import {createRuntimeError} from "../error/runtime";
import {createContext, TwingContext} from "../context";
import {mergeIterables} from "../helpers/merge-iterables";
import {iteratorToMap} from "../helpers/iterator-to-map";
import {TwingBaseExpressionNode} from "./expression";
export type TwingWithNodeAttributes = TwingBaseNodeAttributes & {
only: boolean;
};
export type TwingWithNodeChildren = {
body: TwingBaseNode;
variables?: TwingBaseExpressionNode;
};
export interface TwingWithNode extends TwingBaseNode<"with", TwingWithNodeAttributes, TwingWithNodeChildren> {
}
export const createWithNode = (
body: TwingBaseNode,
variables: TwingBaseExpressionNode | null,
only: boolean,
line: number,
column: number,
tag: string
): TwingWithNode => {
const children: TwingWithNodeChildren = {
body
};
if (variables) {
children.variables = variables;
}
const baseNode = createBaseNode("with", {
only
}, children, line, column, tag);
const withNode: TwingWithNode = {
...baseNode,
execute: async (executionContext) => {
const {template, context} = executionContext;
const {variables: variablesNode, body} = baseNode.children;
const {only} = baseNode.attributes;
let scopedContext: TwingContext<any, any>;
if (variablesNode) {
const variables = await variablesNode.execute(executionContext);
if (typeof variables !== "object") {
throw createRuntimeError(`Variables passed to the "with" tag must be a hash.`, withNode, 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 body.execute({
...executionContext,
context: scopedContext
});
}
};
return withNode;
};

View File

@ -1,17 +0,0 @@
import type {TwingBaseNode} from "../node";
import {createBaseNode, TwingBaseNodeAttributes} from "../node";
import {TwingBaseExpressionNode} from "./expression";
export type TwingWrapperNodeChildren<T extends TwingBaseExpressionNode> = Record<string, T>;
export interface TwingWrapperNode<T extends TwingBaseExpressionNode = TwingBaseExpressionNode> extends TwingBaseNode<"wrapper", TwingBaseNodeAttributes, TwingWrapperNodeChildren<T>> {
}
export const createWrapperNode = <T extends TwingBaseExpressionNode>(
children: TwingWrapperNodeChildren<T>,
line: number,
column: number
): TwingWrapperNode<T> => {
return createBaseNode("wrapper", {}, children, line, column);
};

View File

@ -1,30 +0,0 @@
import type {TwingSandboxSecurityNotAllowedFilterError} from "./security-not-allowed-filter-error";
import type {TwingSandboxSecurityNotAllowedFunctionError} from "./security-not-allowed-function-error";
import type {TwingSandboxSecurityNotAllowedMethodError} from "./security-not-allowed-method-error";
import type {TwingSandboxSecurityNotAllowedPropertyError} from "./security-not-allowed-property-error";
import type {TwingSandboxSecurityNotAllowedTagError} from "./security-not-allowed-tag-error";
import {createBaseError, TwingErrorLocation, TwingBaseError} from "../error/base";
export type TwingSandboxSecurityError =
| TwingSandboxSecurityNotAllowedFilterError
| TwingSandboxSecurityNotAllowedFunctionError
| TwingSandboxSecurityNotAllowedMethodError
| TwingSandboxSecurityNotAllowedPropertyError
| TwingSandboxSecurityNotAllowedTagError
;
export const sandboxSecurityErrorName = 'TwingSandboxSecurityError';
/**
* Exception thrown when a security error occurs at runtime.
*/
export interface BaseSandboxSecurityError extends TwingBaseError<typeof sandboxSecurityErrorName> {
}
export const createBaseSandboxSecurityError = (message: string, location?: TwingErrorLocation, source?: string) => {
const error = createBaseError(sandboxSecurityErrorName, message, location, source);
Error.captureStackTrace(error, createBaseSandboxSecurityError);
return error;
};

View File

@ -1,29 +0,0 @@
import {
BaseSandboxSecurityError,
createBaseSandboxSecurityError,
TwingSandboxSecurityError
} from "./security-error";
import {TwingErrorLocation} from "../error/base";
/**
* Exception thrown when a not allowed filter is used in a template.
*/
export interface TwingSandboxSecurityNotAllowedFilterError extends BaseSandboxSecurityError {
readonly filterName: string;
}
export const createSandboxSecurityNotAllowedFilterError = (message: string, filterName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedFilterError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedFilterError);
return Object.assign(error, {
get filterName() {
return filterName;
}
});
};
export const isASandboxSecurityNotAllowedFilterError = (candidate: TwingSandboxSecurityError): candidate is TwingSandboxSecurityNotAllowedFilterError => {
return (candidate as TwingSandboxSecurityNotAllowedFilterError).filterName !== undefined;
};

View File

@ -1,18 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedFunctionError extends BaseSandboxSecurityError {
readonly functionName: string;
}
export const createSandboxSecurityNotAllowedFunctionError = (message: string, functionName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedFunctionError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedFunctionError);
return Object.assign(error, {
get functionName() {
return functionName;
}
});
};

View File

@ -1,13 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedMethodError extends BaseSandboxSecurityError {
}
export const createSandboxSecurityNotAllowedMethodError = (message: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedMethodError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedMethodError);
return error;
};

View File

@ -1,16 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError} from "./security-error";
import {TwingErrorLocation} from "../error/base";
/**
* Exception thrown when a not allowed object property is used in a template.
*/
export interface TwingSandboxSecurityNotAllowedPropertyError extends BaseSandboxSecurityError {
}
export const createSandboxSecurityNotAllowedPropertyError = (message: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedPropertyError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedPropertyError);
return error;
};

View File

@ -1,23 +0,0 @@
import {BaseSandboxSecurityError, createBaseSandboxSecurityError, TwingSandboxSecurityError} from "./security-error";
import {TwingSandboxSecurityNotAllowedPropertyError} from "./security-not-allowed-property-error";
import {TwingErrorLocation} from "../error/base";
export interface TwingSandboxSecurityNotAllowedTagError extends BaseSandboxSecurityError {
readonly tagName: string;
}
export const createSandboxSecurityNotAllowedTagError = (message: string, tagName: string, location?: TwingErrorLocation, source?: string): TwingSandboxSecurityNotAllowedPropertyError => {
const error = createBaseSandboxSecurityError(message, location, source);
Error.captureStackTrace(error, createSandboxSecurityNotAllowedTagError);
return Object.assign(error, {
get tagName() {
return tagName;
}
});
};
export const isASandboxSecurityNotAllowedTagError = (candidate: TwingSandboxSecurityError): candidate is TwingSandboxSecurityNotAllowedTagError => {
return (candidate as TwingSandboxSecurityNotAllowedTagError).tagName !== undefined;
};

View File

@ -1,766 +0,0 @@
import {createContext, TwingContext} from "./context";
import {TwingEnvironment} from "./environment";
import {createOutputBuffer, TwingOutputBuffer} from "./output-buffer";
import {TwingSourceMapRuntime} from "./source-map-runtime";
import {TwingTemplateNode} from "./node/template";
import {mergeIterables} from "./helpers/merge-iterables";
import {createRuntimeError} from "./error/runtime";
import {getChildren, getChildrenCount, TwingBaseNode} from "./node";
import {TwingError} from "./error";
import {createMarkup, isAMarkup, TwingMarkup} from "./markup";
import {createTemplateLoadingError, isATemplateLoadingError} from "./error/loader";
import {cloneMap} from "./helpers/clone-map";
import {createBodyNode} from "./node/body";
import {getKeyValuePairs} from "./node/expression/array";
import {iteratorToMap} from "./helpers/iterator-to-map";
import {createSource, TwingSource} from "./source";
import {TwingFilter} from "./filter";
import {TwingFunction} from "./function";
import {TwingTest} from "./test";
import {getFilter} from "./helpers/get-filter";
import {getTest} from "./helpers/get-test";
import {getFunction} from "./helpers/get-function";
import * as createHash from "create-hash";
import {TwingEscapingStrategy} from "./escaping-strategy";
import {getTraceableMethod} from "./helpers/traceable-method";
import {TwingConstantNode} from "./node/expression/constant";
export type TwingTemplateBlockMap = Map<string, [TwingTemplate, string]>;
export type TwingTemplateBlockHandler = (
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
) => Promise<void>;
export type TwingTemplateMacroHandler = (
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
sourceMapRuntime: TwingSourceMapRuntime | undefined,
...macroArguments: Array<any>
) => Promise<TwingMarkup>;
export type TwingTemplateAliases = TwingContext<string, TwingTemplate>;
export interface TwingTemplate {
readonly aliases: TwingTemplateAliases;
readonly blockHandlers: Map<string, TwingTemplateBlockHandler>;
readonly canBeUsedAsATrait: boolean;
readonly source: TwingSource;
readonly macroHandlers: Map<string, TwingTemplateMacroHandler>;
readonly name: string;
/**
* @param candidate
* @param method
*
* @throws {@link TwingSandboxSecurityNotAllowedMethodError} When the method of the passed candidate is not allowed to be executed
*/
checkMethodAllowed(candidate: any | TwingMarkup, method: string): void;
/**
* @param candidate
* @param property
*
* @throws {@link TwingSandboxSecurityNotAllowedPropertyError} When the property of the passed candidate is not allowed to be accessed
*/
checkPropertyAllowed(candidate: any | TwingMarkup, property: string): void;
checkSecurity(tags: Array<string>, filters: Array<string>, functions: Array<string>): void;
createTemplateFromString(content: string, name: string | null): Promise<TwingTemplate>;
displayBlock(
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
useBlocks: boolean,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<void>;
escape(value: string | boolean | TwingMarkup | null | undefined, strategy: TwingEscapingStrategy | string, charset: string | null, autoEscape?: boolean): Promise<string | boolean | TwingMarkup>;
execute(
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
childBlocks: TwingTemplateBlockMap,
options?: {
sandboxed?: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
}
): Promise<void>;
getBlocks(): Promise<TwingTemplateBlockMap>;
getFilter(name: string): TwingFilter | null;
getFunction(name: string): TwingFunction | null;
getParent(
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<TwingTemplate | null>;
getTemplateSource(name: string): Promise<TwingSource | null>;
getTest(name: string): TwingTest | null;
getTraits(): Promise<TwingTemplateBlockMap>;
hasBlock(
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<boolean>;
hasMacro(name: string): Promise<boolean>;
/**
* @param index
*
* @throws {TwingTemplateLoadingError} When no embedded template exists for the passed index.
*/
loadEmbeddedTemplate(
index: number
): Promise<TwingTemplate>;
/**
* @param identifier
*
* @throws {TwingTemplateLoadingError} When no embedded template exists for the passed identifier.
*/
loadTemplate(
identifier: TwingTemplate | string | Array<TwingTemplate | null>
): Promise<TwingTemplate>;
render(
context: Record<string, any>,
options?: {
outputBuffer?: TwingOutputBuffer;
sandboxed?: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
}
): Promise<string>;
renderBlock(
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
blocks: TwingTemplateBlockMap,
useBlocks: boolean,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<string>;
renderParentBlock(
name: string,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
sandboxed: boolean,
sourceMapRuntime?: TwingSourceMapRuntime
): Promise<string>;
/**
* Tries to load templates consecutively from an array.
*
* Similar to loadTemplate() but it also accepts instances of TwingTemplate and an array of templates where each is tried to be loaded.
*
* @param names A template or an array of templates to try consecutively
*/
resolveTemplate(names: Array<string | TwingTemplate | null>): Promise<TwingTemplate>;
}
export const createTemplate = (
environment: TwingEnvironment,
ast: TwingTemplateNode
): TwingTemplate => {
const {charset, dateFormat, dateIntervalFormat, isStrictVariables, numberFormat, timezone} = environment
// blocks
const blockHandlers: Map<string, TwingTemplateBlockHandler> = new Map();
let blocks: TwingTemplateBlockMap | null = null;
const {blocks: blockNodes} = ast.children;
for (const [name, blockNode] of getChildren(blockNodes)) {
const blockHandler: TwingTemplateBlockHandler = (context, outputBuffer, blocks, sandboxed, sourceMapRuntime) => {
const aliases = template.aliases.clone();
return blockNode.children.body.execute({
aliases,
blocks,
charset,
context,
dateFormat,
dateIntervalFormat,
globals: environment.globals,
isStrictVariables,
numberFormat,
outputBuffer,
sandboxed,
sourceMapRuntime,
template,
timezone
});
};
blockHandlers.set(name, blockHandler);
}
// macros
const macroHandlers: Map<string, TwingTemplateMacroHandler> = new Map();
const {macros: macrosNode} = ast.children;
for (const [name, macroNode] of Object.entries(macrosNode.children)) {
const macroHandler: TwingTemplateMacroHandler = async (outputBuffer, sandboxed, sourceMapRuntime, ...args) => {
const {body, arguments: macroArguments} = macroNode.children;
const keyValuePairs = getKeyValuePairs(macroArguments);
const aliases = template.aliases.clone();
const localVariables: Map<string, any> = new Map();
for (const {key: keyNode, value: defaultValueNode} of keyValuePairs) {
const key = keyNode.attributes.value as string;
const defaultValue = await defaultValueNode.execute({
aliases,
blocks: new Map(),
charset,
context: createContext(),
dateFormat,
dateIntervalFormat,
globals: environment.globals,
isStrictVariables,
numberFormat,
outputBuffer,
sandboxed,
sourceMapRuntime,
template,
timezone
});
let value = args.shift();
if (value === undefined) {
value = defaultValue;
}
localVariables.set(key, value);
}
localVariables.set('varargs', args);
const context = createContext(localVariables);
const blocks = new Map();
outputBuffer.start();
return await body.execute({
aliases,
blocks,
charset,
context,
dateFormat,
dateIntervalFormat,
globals: environment.globals,
isStrictVariables,
numberFormat,
outputBuffer,
sandboxed,
sourceMapRuntime,
template,
timezone
})
.then(() => {
const content = outputBuffer.getContents();
return createMarkup(content, environment.charset);
})
.finally(() => {
outputBuffer.endAndClean();
});
};
macroHandlers.set(name, macroHandler);
}
// traits
let traits: TwingTemplateBlockMap | null = null;
// embedded templates
const embeddedTemplates: Map<number, TwingTemplateNode> = new Map();
for (const embeddedTemplate of ast.embeddedTemplates) {
embeddedTemplates.set(embeddedTemplate.attributes.index, embeddedTemplate);
}
// parent
let parent: TwingTemplate | null = null;
const displayParentBlock = (name: string, context: TwingContext<any, any>, outputBuffer: TwingOutputBuffer, blocks: TwingTemplateBlockMap, sandboxed: boolean, sourceMapRuntime?: TwingSourceMapRuntime): Promise<void> => {
return template.getTraits()
.then((traits) => {
const trait = traits.get(name);
if (trait) {
const [blockTemplate, blockName] = trait;
return blockTemplate.displayBlock(
blockName,
context,
outputBuffer,
blocks,
false,
sandboxed,
sourceMapRuntime
);
}
else {
return template.getParent(context, outputBuffer, sandboxed, sourceMapRuntime)
.then((parent) => {
if (parent !== null) {
return parent.displayBlock(name, context, outputBuffer, blocks, false, sandboxed, sourceMapRuntime);
}
else {
throw createRuntimeError(`The template has no parent and no traits defining the "${name}" block.`, undefined, template.name);
}
});
}
});
};
// A template can be used as a trait if:
// * it has no parent
// * it has no macros
// * it has no body
//
// Put another way, a template can be used as a trait if it
// only contains blocks and use statements.
const {parent: parentNode, macros, body} = ast.children;
const {line, column} = ast;
let canBeUsedAsATrait = (parentNode === undefined) && (getChildrenCount(macros) === 0);
if (canBeUsedAsATrait) {
let node: TwingBaseNode = body.children.content;
if (getChildrenCount(node) === 0) {
node = createBodyNode(node, line, column);
}
for (const [, child] of Object.entries(node.children)) {
if (getChildrenCount(child) === 0) {
continue;
}
canBeUsedAsATrait = false;
break;
}
}
const template: TwingTemplate = {
get aliases() {
return aliases;
},
get blockHandlers() {
return blockHandlers;
},
get canBeUsedAsATrait() {
return canBeUsedAsATrait;
},
get macroHandlers() {
return macroHandlers;
},
get name() {
return template.source.name;
},
get source() {
return ast.attributes.source;
},
checkMethodAllowed: environment.sandboxPolicy.checkMethodAllowed,
checkPropertyAllowed: environment.sandboxPolicy.checkPropertyAllowed,
checkSecurity: environment.sandboxPolicy.checkSecurity,
createTemplateFromString: (code, name) => {
const hash: string = createHash("sha256").update(code).digest("hex").toString();
if (name !== null) {
name = `${name} (string template ${hash})`;
}
else {
name = `__string_template__${hash}`;
}
const ast = environment.parse(environment.tokenize(createSource(name, code)));
const template = createTemplate(environment, ast);
return Promise.resolve(template);
},
displayBlock: (name, context, outputBuffer, blocks, useBlocks, sandboxed, sourceMapRuntime) => {
return template.getBlocks()
.then((ownBlocks) => {
let blockHandler: TwingTemplateBlockHandler | undefined;
let block: [TwingTemplate, string] | undefined;
if (useBlocks && (block = blocks.get(name)) !== undefined) {
const [blockTemplate, blockName] = block;
blockHandler = blockTemplate.blockHandlers.get(blockName);
}
else if ((block = ownBlocks.get(name)) !== undefined) {
const [blockTemplate, blockName] = block;
blockHandler = blockTemplate.blockHandlers.get(blockName);
}
if (blockHandler) {
return blockHandler(context, outputBuffer, blocks, sandboxed, sourceMapRuntime);
}
else {
return template.getParent(context, outputBuffer, sandboxed, sourceMapRuntime).then((parent) => {
if (parent) {
return parent.displayBlock(name, context, outputBuffer, mergeIterables(ownBlocks, blocks), false, sandboxed, sourceMapRuntime);
}
else {
const block = blocks.get(name);
if (block) {
const [blockTemplate] = block!;
throw createRuntimeError(`Block "${name}" should not call parent() in "${blockTemplate.name}" as the block does not exist in the parent template "${template.name}".`, undefined, blockTemplate.name);
}
else {
throw createRuntimeError(`Block "${name}" on template "${template.name}" does not exist.`, undefined, template.name);
}
}
});
}
});
},
escape: (value, strategy, charset) => {
if (typeof value === "boolean") {
return Promise.resolve(value);
}
if (isAMarkup(value)) {
return Promise.resolve(value);
}
let result: string;
if ((value === null) || (value === undefined)) {
result = '';
}
else {
const strategyHandler = environment.escapingStrategyHandlers[strategy];
if (strategyHandler === undefined) {
return Promise.reject(createRuntimeError(`Invalid escaping strategy "${strategy}" (valid ones: ${Object.keys(environment.escapingStrategyHandlers).sort().join(', ')}).`));
}
result = strategyHandler(value.toString(), charset || environment.charset, template.name);
}
return Promise.resolve(result);
},
execute: async (context, outputBuffer, childBlocks, options) => {
const aliases = template.aliases.clone();
const sandboxed = options?.sandboxed || false;
const sourceMapRuntime = options?.sourceMapRuntime;
return Promise.all([
template.getParent(context, outputBuffer, sandboxed, sourceMapRuntime),
template.getBlocks()
]).then(([parent, ownBlocks]) => {
const blocks = mergeIterables(ownBlocks, childBlocks);
return ast.execute({
aliases,
blocks,
charset,
context,
dateFormat,
dateIntervalFormat,
globals: environment.globals,
isStrictVariables,
numberFormat,
outputBuffer,
sandboxed,
sourceMapRuntime,
template,
timezone
}).then(() => {
if (parent) {
return parent.execute(context, outputBuffer, blocks, {
sandboxed,
sourceMapRuntime
});
}
});
}).catch((error: TwingError) => {
if (!error.source) {
error.source = template.name;
}
if (isATemplateLoadingError(error)) {
error = createRuntimeError(error.rootMessage, error.location, error.source, error);
}
throw error;
});
},
getBlocks: () => {
if (blocks) {
return Promise.resolve(blocks);
}
else {
return template.getTraits()
.then((traits) => {
blocks = mergeIterables(traits, new Map([...blockHandlers.keys()].map((key) => {
return [key, [template, key]];
})));
return blocks;
});
}
},
getFilter: (name) => {
return getFilter(environment.filters, name);
},
getFunction: (name) => {
return getFunction(environment.functions, name);
},
getParent: async (context, outputBuffer, sandboxed, sourceMapRuntime) => {
if (parent !== null) {
return Promise.resolve(parent);
}
const parentNode = ast.children.parent;
if (parentNode) {
return template.getBlocks()
.then(async (blocks) => {
const parentName = await parentNode.execute({
aliases: createContext(),
blocks,
charset,
context,
dateFormat,
dateIntervalFormat,
globals: environment.globals,
isStrictVariables,
numberFormat,
outputBuffer,
sandboxed,
sourceMapRuntime,
template,
timezone
});
const loadTemplate = getTraceableMethod(
template.loadTemplate,
parentNode.line,
parentNode.column,
template.name
);
const loadedParent = await loadTemplate(parentName);
if (parentNode.type === "constant") {
parent = loadedParent;
}
return loadedParent;
});
}
else {
return Promise.resolve(null);
}
},
getTemplateSource: (name) => {
return environment.loader.getSource(name, template.name);
},
getTest: (name) => {
return getTest(environment.tests, name);
},
getTraits: async () => {
if (traits === null) {
traits = new Map();
const {traits: traitsNode} = ast.children;
for (const [, traitNode] of getChildren(traitsNode)) {
const {template: templateNameNode, targets} = traitNode.children;
const templateName = templateNameNode.attributes.value as string;
const loadTemplate = getTraceableMethod(
template.loadTemplate,
templateNameNode.line,
templateNameNode.column,
template.name
);
const traitExecutionContext = await loadTemplate(templateName);
if (!traitExecutionContext.canBeUsedAsATrait) {
throw createRuntimeError(`Template ${templateName} cannot be used as a trait.`, templateNameNode, template.name);
}
const traitBlocks = cloneMap(await traitExecutionContext.getBlocks());
for (const [key, target] of getChildren(targets)) {
const traitBlock = traitBlocks.get(key as string);
if (!traitBlock) {
throw createRuntimeError(`Block "${key}" is not defined in trait "${templateName}".`, templateNameNode, template.name);
}
const targetValue = (target as TwingConstantNode<string>).attributes.value;
traitBlocks.set(targetValue, traitBlock);
traitBlocks.delete((key as string));
}
traits = mergeIterables(traits, traitBlocks);
}
}
return Promise.resolve(traits);
},
hasBlock: (name, context, outputBuffer, blocks, sandboxed, sourceMapRuntime): Promise<boolean> => {
if (blocks.has(name)) {
return Promise.resolve(true);
}
else {
return template.getBlocks()
.then((blocks) => {
if (blocks.has(name)) {
return Promise.resolve(true);
}
else {
return template.getParent(context, outputBuffer, sandboxed, sourceMapRuntime)
.then((parent) => {
if (parent) {
return parent.hasBlock(name, context, outputBuffer, blocks, sandboxed, sourceMapRuntime);
}
else {
return false;
}
});
}
});
}
},
hasMacro: (name) => {
// @see https://github.com/twigphp/Twig/issues/3174 as to why we don't check macro existence in parents
return Promise.resolve(template.macroHandlers.has(name));
},
loadEmbeddedTemplate: (index) => {
const ast = embeddedTemplates.get(index);
if (ast === undefined) {
return Promise.reject(createTemplateLoadingError([`embedded#${index}`]));
}
return Promise.resolve(createTemplate(environment, ast));
},
loadTemplate: (identifier) => {
let promise: Promise<TwingTemplate>;
if (typeof identifier === "string") {
promise = environment.loadTemplate(identifier, template.name);
}
else if (Array.isArray(identifier)) {
promise = template.resolveTemplate(identifier);
}
else {
promise = Promise.resolve(identifier);
}
return promise;
},
render: (context, options) => {
const actualOutputBuffer: TwingOutputBuffer = options?.outputBuffer || createOutputBuffer();
actualOutputBuffer.start();
return template.execute(
createContext(iteratorToMap(context)),
actualOutputBuffer,
new Map(),
{
sandboxed: options?.sandboxed,
sourceMapRuntime: options?.sourceMapRuntime
}
).then(() => {
return actualOutputBuffer.getAndFlush();
});
},
renderBlock: (name, context, outputBuffer, blocks, useBlocks, sandboxed, sourceMapRuntime) => {
outputBuffer.start();
return template.displayBlock(name, context, outputBuffer, blocks, useBlocks, sandboxed, sourceMapRuntime).then(() => {
return outputBuffer.getAndClean();
});
},
renderParentBlock: (name, context, outputBuffer, sandboxed, sourceMapRuntime) => {
outputBuffer.start();
return template.getBlocks()
.then((blocks) => {
return displayParentBlock(name, context, outputBuffer, blocks, sandboxed, sourceMapRuntime).then(() => {
return outputBuffer.getAndClean();
})
});
},
resolveTemplate: (names) => {
const loadTemplateAtIndex = (index: number): Promise<TwingTemplate> => {
if (index < names.length) {
const name = names[index];
if (name === null) {
return loadTemplateAtIndex(index + 1);
}
else if (typeof name !== "string") {
return Promise.resolve(name);
}
else {
return template.loadTemplate(name)
.catch(() => {
return loadTemplateAtIndex(index + 1);
});
}
}
else {
return Promise.reject(createTemplateLoadingError((names as Array<string | null>).map((name) => {
if (name === null) {
return '';
}
return name;
}), undefined, template.name));
}
};
return loadTemplateAtIndex(0);
}
};
const aliases: TwingTemplateAliases = createContext();
aliases.set(`_self`, template);
return template;
};

View File

@ -8,12 +8,11 @@ export type {TwingError} from "./lib/error";
export type {TwingBaseError, TwingErrorLocation} from "./lib/error/base";
export type {TwingParsingError} from "./lib/error/parsing";
export type {TwingRuntimeError} from "./lib/error/runtime";
export type {TwingTemplateLoadingError} from "./lib/error/loader";
export {isATwingError} from "./lib/error";
export {createParsingError} from "./lib/error/parsing";
export {createRuntimeError, isARuntimeError} from "./lib/error/runtime";
export {createTemplateLoadingError, isATemplateLoadingError} from "./lib/error/loader";
export {createRuntimeError} from "./lib/error/runtime";
export {createTemplateLoadingError} from "./lib/error/loader";
// loader
export type {
@ -47,7 +46,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 +73,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 +100,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 +232,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";
@ -267,7 +265,7 @@ export {createWithTagHandler} from "./lib/tag-handler/with";
export type {
TwingCallable, TwingCallableArgument, TwingCallableWrapperOptions, TwingCallableWrapper
} from "./lib/callable-wrapper";
export type {TwingContext} from "./lib/context";
export {type TwingContext, createContext} from "./lib/context";
export type {TwingEnvironment, TwingEnvironmentOptions, TwingNumberFormat} from "./lib/environment";
export type {
TwingEscapingStrategy, TwingEscapingStrategyHandler, TwingEscapingStrategyResolver
@ -282,19 +280,12 @@ export type {TwingNodeVisitor} from "./lib/node-visitor";
export type {
TwingOperator, TwingOperatorAssociativity, TwingOperatorType, TwingOperatorExpressionFactory
} from "./lib/operator";
export type {TwingOutputBuffer} from "./lib/output-buffer";
export {type TwingOutputBuffer, createOutputBuffer} from "./lib/output-buffer";
export type {TwingParser, TwingParserOptions} from "./lib/parser";
export type {TwingSandboxSecurityError} from "./lib/sandbox/security-error";
export type {TwingSandboxSecurityPolicy} from "./lib/sandbox/security-policy";
export type {TwingSandboxSecurityNotAllowedFilterError} from "./lib/sandbox/security-not-allowed-filter-error";
export type {TwingSandboxSecurityNotAllowedFunctionError} from "./lib/sandbox/security-not-allowed-function-error";
export type {TwingSandboxSecurityNotAllowedMethodError} from "./lib/sandbox/security-not-allowed-method-error";
export type {TwingSandboxSecurityNotAllowedPropertyError} from "./lib/sandbox/security-not-allowed-property-error";
export type {TwingSandboxSecurityNotAllowedTagError} from "./lib/sandbox/security-not-allowed-tag-error";
export type {TwingSource} from "./lib/source";
export type {TwingSourceMapRuntime} from "./lib/source-map-runtime";
export type {
TwingTemplate,
TwingTemplateAliases,
TwingTemplateBlockMap,
TwingTemplateBlockHandler,
@ -303,15 +294,21 @@ export type {
export type {TwingTest} from "./lib/test";
export type {TwingTokenStream} from "./lib/token-stream";
export interface TwingTemplate {
execute: import("./lib/template").TwingTemplate["execute"];
render: import("./lib/template").TwingTemplate["render"];
}
export {createEnvironment} from "./lib/environment";
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";
export {createSourceMapRuntime} from "./lib/source-map-runtime";
export {createTemplate} from "./lib/template";
export {type TwingTemplateLoader, createTemplateLoader} from "./lib/template-loader";
export {createTest} from "./lib/test";

View File

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

View File

@ -1,21 +1,15 @@
import {TwingParsingError, parsingErrorName} from "./error/parsing";
import {TwingRuntimeError, runtimeErrorName} from "./error/runtime";
import {TwingTemplateLoadingError, templateLoadingError} from "./error/loader";
import {TwingSandboxSecurityError, sandboxSecurityErrorName} from "./sandbox/security-error";
export type TwingError =
| TwingTemplateLoadingError
| TwingRuntimeError
| TwingParsingError
| TwingSandboxSecurityError
;
export const isATwingError = (candidate: Error): candidate is TwingError => {
return [
templateLoadingError,
parsingErrorName,
runtimeErrorName,
sandboxSecurityErrorName
runtimeErrorName
].includes((candidate as TwingError).name);
};

View File

@ -1,3 +1,5 @@
import type {TwingSource} from "../source";
export type TwingErrorLocation = {
line: number;
column: number;
@ -7,14 +9,14 @@ export interface TwingBaseError<Name extends string> extends Error {
readonly name: Name;
readonly previous: any | undefined;
readonly rootMessage: string;
location: TwingErrorLocation | undefined;
source: string | undefined;
location: TwingErrorLocation;
source: TwingSource;
appendMessage(message: string): void;
}
export const createBaseError = <Name extends string>(
name: Name, message: string, location?: TwingErrorLocation, source?: string, previous?: any
name: Name, message: string, location: TwingErrorLocation, source: TwingSource, previous?: any
): TwingBaseError<Name> => {
const baseError = Error(message);
@ -22,20 +24,10 @@ export const createBaseError = <Name extends string>(
const error = Object.create(baseError, {
location: {
get: () => location,
set: (value: TwingErrorLocation) => {
location = value;
updateRepresentation();
}
get: () => location
},
source: {
get: () => source,
set: (value: string) => {
source = value;
updateRepresentation();
}
get: () => source
},
previous: {
value: previous
@ -69,15 +61,11 @@ export const createBaseError = <Name extends string>(
questionMark = true;
}
if (source) {
representation += ` in "${source}"`;
}
representation += ` in "${source.name}"`;
if (location !== undefined) {
const {line, column} = location;
representation += ` at line ${line}, column ${column}`;
}
const {line, column} = location;
representation += ` at line ${line}, column ${column}`;
if (dot) {
representation += '.';

View File

@ -0,0 +1,17 @@
export const createTemplateLoadingError = (names: Array<string | null>): Error => {
let message: string;
if (names.length === 1) {
const name = names[0];
message = `Unable to find template "${name ? name : ''}".`;
} else {
message = `Unable to find one of the following templates: "${names.join('", "')}".`;
}
const error = Error(message);
Error.captureStackTrace(error, createTemplateLoadingError);
return error;
};

View File

@ -1,4 +1,5 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
import type {TwingSource} from "../source";
const Levenshtein = require('levenshtein');
@ -15,7 +16,7 @@ export interface TwingParsingError extends TwingBaseError<typeof parsingErrorNam
}
export const createParsingError = (
message: string, location?: TwingErrorLocation, source?: string, previous?: Error
message: string, location: TwingErrorLocation, source: TwingSource, previous?: Error
): TwingParsingError => {
const baseError = createBaseError(parsingErrorName, message, location, source, previous);

View File

@ -1,4 +1,5 @@
import {createBaseError, TwingErrorLocation, TwingBaseError} from "./base";
import type {TwingSource} from "../source";
export const runtimeErrorName = 'TwingRuntimeError';
@ -6,11 +7,7 @@ export interface TwingRuntimeError extends TwingBaseError<typeof runtimeErrorNam
}
export const isARuntimeError = (candidate: Error): candidate is TwingRuntimeError => {
return (candidate as TwingRuntimeError).name === runtimeErrorName;
};
export const createRuntimeError = (message: string, location?: TwingErrorLocation, source?: string, previous?: Error): TwingRuntimeError => {
export const createRuntimeError = (message: string, location: TwingErrorLocation, source: TwingSource, previous?: Error): TwingRuntimeError => {
const error = createBaseError(runtimeErrorName, message, location, source, previous);
Error.captureStackTrace(error, createRuntimeError);

View File

@ -0,0 +1,14 @@
import type {TwingEscapingStrategyHandler} from "../escaping-strategy";
const phpSprintf = require('locutus/php/strings/sprintf');
export const createCssEscapingStrategyHandler = (): TwingEscapingStrategyHandler => {
return (value) => {
value = value.replace(/[^a-zA-Z0-9]/ug, (character: string) => {
const codePoint = character.codePointAt(0)!;
return phpSprintf('\\u%04X', codePoint);
});
return value;
}
};

View File

@ -1,8 +1,6 @@
import type {TwingEscapingStrategyHandler} from "../escaping-strategy";
const phpBin2hex = require("locutus/php/strings/bin2hex");
const phpSprintf = require('locutus/php/strings/sprintf');
const strlen = require('utf8-binary-cutter').getBinarySize;
export const createJsEscapingStrategyHandler = (): TwingEscapingStrategyHandler => {
return (value) => {
@ -30,16 +28,22 @@ export const createJsEscapingStrategyHandler = (): TwingEscapingStrategyHandler
return shortMap.get(char);
}
// \uHHHH
char = phpBin2hex(char).toUpperCase();
let codePoint = char.codePointAt(0)!;
if (strlen(char) <= 4) {
return phpSprintf('\\u%04s', char);
if (codePoint <= 0x10000) {
return phpSprintf('\\u%04X', codePoint);
}
// Split characters outside the BMP into surrogate pairs
// https://tools.ietf.org/html/rfc2781.html#section-2.1
codePoint = codePoint - 0x10000;
const high = 0xD800 | (codePoint >> 10);
const low = 0xDC00 | (codePoint & 0x3FF);
return phpSprintf('\\u%04s\\u%04s', char.substr(0, 4), char.substr(4, 4));
return phpSprintf('\\u%04X\\u%04X', high, low);
});
return value;
}
};

View File

@ -2,21 +2,20 @@ import type {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from ".
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 {TwingEnvironment} from "./environment";
import type {TwingNodeExecutor} from "./node-executor";
import type {TwingTemplateLoader} from "./template-loader";
export type TwingExecutionContext = {
aliases: TwingTemplateAliases;
blocks: TwingTemplateBlockMap;
charset: string,
context: TwingContext<any, any>;
dateFormat: string;
dateIntervalFormat: string;
globals: TwingContext<string, any>;
isStrictVariables: boolean;
numberFormat: TwingNumberFormat;
environment: TwingEnvironment;
nodeExecutor: TwingNodeExecutor;
outputBuffer: TwingOutputBuffer;
sandboxed: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
strict: boolean;
template: TwingTemplate;
timezone: string;
templateLoader: TwingTemplateLoader;
};

View File

@ -1,6 +1,5 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {createRuntimeError} from "../../../error/runtime";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {TwingCallable} from "../../../callable-wrapper";
@ -16,7 +15,7 @@ export const column: TwingCallable = (_executionContext, thing: any, columnKey:
let map: Map<any, any>;
if (!isTraversable(thing) || isPlainObject(thing)) {
return Promise.reject(createRuntimeError(`The column filter only works with arrays or "Traversable", got "${typeof thing}" as first argument.`));
return Promise.reject(new Error(`The column filter only works with arrays or "Traversable", got "${typeof thing}" as first argument.`));
} else {
map = iteratorToMap(thing);
}

View File

@ -20,7 +20,8 @@ export const dateModify: TwingCallable = (
date: Date | DateTime | string,
modifier: string
): Promise<DateTime> => {
const {timezone: defaultTimezone} = executionContext;
const {environment} = executionContext;
const {timezone: defaultTimezone} = environment;
return createDate(defaultTimezone, date, null)
.then((dateTime) => {

View File

@ -24,7 +24,8 @@ export const date: TwingCallable = (
format: string | null,
timezone: string | null | false
): Promise<string> => {
const {dateFormat, dateIntervalFormat} = executionContext;
const {environment} = executionContext;
const {dateFormat, dateIntervalFormat} = environment;
return createDate(executionContext, date, timezone)
.then((date) => {

View File

@ -1,5 +1,6 @@
import {createMarkup, TwingMarkup} from "../../../markup";
import type {TwingCallable} from "../../../callable-wrapper";
import {escapeValue} from "../../../helpers/escape-value";
export const escape: TwingCallable<[
value: string | TwingMarkup | null,
@ -13,12 +14,13 @@ export const escape: TwingCallable<[
strategy = "html";
}
const {template, charset} = executionContext;
const {template, environment} = executionContext;
return template.escape(value, strategy, charset)
// todo: probably we need to use traceable method
return escapeValue(template, environment, value, strategy, environment.charset)
.then((value) => {
if (typeof value === "string") {
return createMarkup(value, charset);
return createMarkup(value, environment.charset);
}
return value;

View File

@ -1,6 +1,5 @@
import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
@ -24,13 +23,13 @@ export const merge: TwingCallable = (_executionContext, iterable1: any, source:
const isIterable1NullOrUndefined = (iterable1 === null) || (iterable1 === undefined);
if (isIterable1NullOrUndefined || (!isTraversable(iterable1) && (typeof iterable1 !== 'object'))) {
return Promise.reject(createRuntimeError(`The merge filter only works on arrays or "Traversable", got "${!isIterable1NullOrUndefined ? typeof iterable1 : iterable1}".`));
return Promise.reject(new Error(`The merge filter only works on arrays or "Traversable", got "${!isIterable1NullOrUndefined ? typeof iterable1 : iterable1}".`));
}
const isSourceNullOrUndefined = (source === null) || (source === undefined);
if (isSourceNullOrUndefined || (!isTraversable(source) && (typeof source !== 'object'))) {
return Promise.reject(createRuntimeError(`The merge filter only accepts arrays or "Traversable" as source, got "${!isSourceNullOrUndefined ? typeof source : source}".`));
return Promise.reject(new Error(`The merge filter only accepts arrays or "Traversable" as source, got "${!isSourceNullOrUndefined ? typeof source : source}".`));
}
return Promise.resolve(mergeIterables(iteratorToMap(iterable1), iteratorToMap(source)));

View File

@ -23,7 +23,8 @@ export const numberFormat: TwingCallable = (
decimalPoint: string | null,
thousandSeparator: string | null
): Promise<string> => {
const {numberFormat} = executionContext;
const {environment} = executionContext;
const {numberFormat} = environment;
if (numberOfDecimals === null) {
numberOfDecimals = numberFormat.numberOfDecimals;

View File

@ -1,6 +1,5 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToHash} from "../../../helpers/iterator-to-hash";
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpStrtr = require('locutus/php/strings/strtr');
@ -18,7 +17,7 @@ export const replace: TwingCallable = (_executionContext,value: string | null, f
if (isTraversable(from)) {
from = iteratorToHash(from);
} else if (typeof from !== 'object') {
throw createRuntimeError(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
throw new Error(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
}
if (value === null) {

View File

@ -1,4 +1,3 @@
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpRound = require('locutus/php/math/round');
@ -21,7 +20,7 @@ export const round: TwingCallable = (_executionContext, value: any, precision: n
}
if (method !== 'ceil' && method !== 'floor') {
throw createRuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
throw new Error('The round filter only supports the "common", "ceil", and "floor" methods.');
}
const intermediateValue = value * Math.pow(10, precision);

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