Resolve issue #562

This commit is contained in:
Eric MORAND 2023-12-03 14:02:21 +00:00
parent 83cc625cf0
commit a73f62ffc6
172 changed files with 2073 additions and 761 deletions

View File

@ -54,7 +54,7 @@
"runes": "^0.4.3",
"snake-case": "^2.1.0",
"source-map": "^0.6.1",
"twig-lexer": "^0.8.0",
"twig-lexer": "^0.9.0",
"utf8-binary-cutter": "^0.9.2"
},
"devDependencies": {

View File

@ -76,7 +76,7 @@ export {createMacroNode} from "./lib/node/macro";
export {createTemplateNode, templateNodeType} from "./lib/node/template";
export {createSandboxNode, sandboxNodeType} from "./lib/node/sandbox";
export {createSetNode} from "./lib/node/set";
export {createTraitNode} from "./lib/node/trait";
export {createTraitNode, traitNodeType} from "./lib/node/trait";
export {createWithNode} from "./lib/node/with";
// node/expression
@ -105,7 +105,7 @@ export {createBaseBinaryNode} from "./lib/node/expression/binary";
export {createBlockFunctionNode, blockFunctionNodeType} from "./lib/node/expression/block-function";
export {createBaseCallNode} from "./lib/node/expression/call";
export {createBaseConditionalNode, createConditionalNode, conditionalNodeType} from "./lib/node/expression/conditional";
export {createConstantNode} from "./lib/node/expression/constant";
export {createConstantNode, constantNodeType} from "./lib/node/expression/constant";
export {createEscapeNode} from "./lib/node/expression/escape";
export {createHashNode, hashNodeType} from "./lib/node/expression/hash";
export {createMethodCallNode, methodCallNodeType} from "./lib/node/expression/method-call";
@ -149,7 +149,9 @@ export {createBitwiseXorNode, bitwiseXorNodeType} from "./lib/node/expression/bi
export {createConcatenateNode, concatenateNodeTYpe} from "./lib/node/expression/binary/concatenate";
export {createDivideAndFloorNode, divideAndFloorNodeType} from "./lib/node/expression/binary/divide-and-floor";
export {createDivideNode, divideNodeType} from "./lib/node/expression/binary/divide";
export {createEndsWithNode} from "./lib/node/expression/binary/ends-with";
export {createEndsWithNode, endsWithNodeType} from "./lib/node/expression/binary/ends-with";
export {createHasEveryNode, hasEveryNodeType} from "./lib/node/expression/binary/has-every";
export {createHasSomeNode, hasSomeNodeType} from "./lib/node/expression/binary/has-some";
export {createIsEqualNode} from "./lib/node/expression/binary/is-equal-to";
export {createIsGreaterThanNode} from "./lib/node/expression/binary/is-greater-than";
export {createIsGreaterThanOrEqualToNode} from "./lib/node/expression/binary/is-greater-than-or-equal-to";

View File

@ -1,6 +1,6 @@
import type {TwingRuntimeError} from "./error/runtime";
import type {TwingExecutionContext} from "./execution-context";
export type TwingCallable<R = any> = (...args: any[]) => Promise<R>;
export type TwingCallable<A extends Array<any> = any, R = any> = (executionContext: TwingExecutionContext, ...args: A) => Promise<R>;
export type TwingCallableArgument = {
name: string;
@ -8,19 +8,15 @@ export type TwingCallableArgument = {
};
export type TwingCallableWrapperOptions = {
needs_template?: boolean;
needs_context?: boolean;
needs_output_buffer?: boolean;
needs_source_map_runtime?: boolean;
is_variadic?: boolean;
deprecated?: boolean | string;
alternative?: string;
}
export interface TwingCallableWrapper<Callable extends TwingCallable> {
export interface TwingCallableWrapper {
readonly acceptedArguments: Array<TwingCallableArgument>;
readonly alternative: string | undefined;
readonly callable: Callable;
readonly callable: TwingCallable;
readonly deprecatedVersion: string | boolean | undefined;
readonly isDeprecated: boolean;
readonly isVariadic: boolean;
@ -31,23 +27,17 @@ export interface TwingCallableWrapper<Callable extends TwingCallable> {
* would generate native arguments ["bar","oof"] when the operator name is "foo-bar-oof"
*/
nativeArguments: Array<string>;
readonly needsContext: boolean;
readonly needsOutputBuffer: boolean;
readonly needsSourceMapRuntime: boolean;
readonly needsTemplate: boolean;
getTraceableCallable(line: number, column: number, source: string): Callable;
}
export const createCallableWrapper = <Callable extends TwingCallable>(
export const createCallableWrapper = (
name: string,
callable: Callable,
callable: TwingCallable,
acceptedArguments: Array<TwingCallableArgument>,
options: TwingCallableWrapperOptions
): TwingCallableWrapper<Callable> => {
): TwingCallableWrapper => {
let nativeArguments: Array<string> = [];
const callableWrapper: TwingCallableWrapper<Callable> = {
const callableWrapper: TwingCallableWrapper = {
get callable() {
return callable;
},
@ -74,34 +64,6 @@ export const createCallableWrapper = <Callable extends TwingCallable>(
},
set nativeArguments(values) {
nativeArguments = values;
},
get needsContext() {
return options.needs_context || false;
},
get needsOutputBuffer() {
return options.needs_output_buffer || false;
},
get needsSourceMapRuntime() {
return options.needs_source_map_runtime || false;
},
get needsTemplate() {
return options.needs_template || false;
},
getTraceableCallable: (line, column, source) => {
return ((...args) => {
return callable(...args)
.catch((error: TwingRuntimeError) => {
if (error.location === undefined) {
error.location = {line, column};
}
if (error.source === undefined) {
error.source = source;
}
throw error;
});
}) as typeof callable;
}
};

View File

@ -1,5 +1,3 @@
import {iteratorToMap} from "./helpers/iterator-to-map";
export interface TwingContext<K, V> {
readonly size: number;
@ -16,6 +14,8 @@ export interface TwingContext<K, V> {
has(key: K): boolean;
set(key: K, value: V): TwingContext<K, V>;
values(): IterableIterator<V>;
}
export const createContext = <K extends string, V>(
@ -44,13 +44,7 @@ export const createContext = <K extends string, V>(
return container.entries();
},
get: (key) => {
let value: any = container.get(key);
if (Array.isArray(value)) {
value = iteratorToMap(value);
}
return value;
return container.get(key);
},
has: (key) => {
return container.has(key);
@ -59,6 +53,9 @@ export const createContext = <K extends string, V>(
container.set(key, value);
return context;
},
values: () => {
return container.values();
}
};

View File

@ -153,7 +153,7 @@ export interface TwingEnvironment {
}>;
registerEscapingStrategy(handler: EscapingStrategyHandler, name: string): void;
/**
* Tokenizes a source code.
*
@ -169,10 +169,10 @@ export interface TwingEnvironment {
* @param loader
* @param options
*/
export function createEnvironment(
export const createEnvironment = (
loader: TwingLoader,
options?: TwingEnvironmentOptions
): TwingEnvironment {
): TwingEnvironment => {
const cssEscapingStrategy = createCssEscapingStrategyHandler();
const htmlEscapingStrategy = createHtmlEscapingStrategyHandler();
const htmlAttributeEscapingStrategy = createHtmlAttributeEscapingStrategyHandler();
@ -261,7 +261,8 @@ export function createEnvironment(
if (loadedTemplate) {
return Promise.resolve(loadedTemplate);
} else {
}
else {
const timestamp = cache ? await cache.getTimestamp(templateFqn) : 0;
const getAstFromCache = async (): Promise<TwingTemplateNode | null> => {
@ -280,10 +281,12 @@ export function createEnvironment(
if (isFresh) {
content = await cache.load(name);
} else {
}
else {
content = null;
}
} else {
}
else {
content = await cache.load(name);
}
@ -361,7 +364,8 @@ export function createEnvironment(
extensionSet.functions,
extensionSet.tests,
parserOptions || options?.parserOptions || {
strict: true
strict: true,
level: 3
}
);
}
@ -406,8 +410,14 @@ export function createEnvironment(
});
},
tokenize: (source: TwingSource): TwingTokenStream => {
const level = options?.parserOptions?.level || 3;
if (!lexer) {
lexer = createLexer(extensionSet.binaryOperators, extensionSet.unaryOperators);
lexer = createLexer(
level,
extensionSet.binaryOperators,
extensionSet.unaryOperators
);
}
const stream = lexer.tokenizeSource(source);

View File

@ -1,14 +1,21 @@
import {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from "./template";
import {TwingContext} from "./context";
import {TwingOutputBuffer} from "./output-buffer";
import {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from "./template";
import type {TwingContext} from "./context";
import type {TwingOutputBuffer} from "./output-buffer";
import type {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingNumberFormat} from "./environment";
export type TwingNodeExecutionContext = {
export type TwingExecutionContext = {
aliases: TwingTemplateAliases;
blocks: TwingTemplateBlockMap;
charset: string,
context: TwingContext<any, any>;
sandboxed: boolean;
dateFormat: string;
dateIntervalFormat: string;
isStrictVariables: boolean;
numberFormat: TwingNumberFormat;
outputBuffer: TwingOutputBuffer;
sandboxed: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
template: TwingTemplate;
timezone: string;
};

View File

@ -7,13 +7,13 @@ import {TwingOperator} from "./operator";
import type {TwingExtension} from "./extension";
export interface TwingExtensionSet {
readonly binaryOperators: Map<string, TwingOperator>;
readonly binaryOperators: Array<TwingOperator>;
readonly filters: Map<string, TwingFilter>;
readonly functions: Map<string, TwingFunction>;
readonly nodeVisitors: Array<TwingNodeVisitor>;
readonly tagHandlers: Array<TwingTagHandler>;
readonly tests: Map<string, TwingTest>;
readonly unaryOperators: Map<string, TwingOperator>;
readonly unaryOperators: Array<TwingOperator>;
addExtension(extension: TwingExtension): void;
@ -31,13 +31,13 @@ export interface TwingExtensionSet {
}
export const createExtensionSet = (): TwingExtensionSet => {
const binaryOperators: Map<string, TwingOperator> = new Map();
const binaryOperators: Array<TwingOperator> = [];
const filters: Map<string, TwingFilter> = new Map();
const functions: Map<string, TwingFunction> = new Map();
const nodeVisitors: Array<TwingNodeVisitor> = [];
const tagHandlers: Array<TwingTagHandler> = [];
const tests: Map<string, TwingTest> = new Map();
const unaryOperators: Map<string, TwingOperator> = new Map();
const unaryOperators: Array<TwingOperator> = [];
const extensionSet: TwingExtensionSet = {
get binaryOperators() {
@ -102,7 +102,7 @@ export const createExtensionSet = (): TwingExtensionSet => {
nodeVisitors.push(nodeVisitor);
},
addOperator: (operator) => {
let bucket: Map<string, TwingOperator>;
let bucket: Array<TwingOperator>;
if (operator.type === "UNARY") {
bucket = unaryOperators;
@ -110,7 +110,7 @@ export const createExtensionSet = (): TwingExtensionSet => {
bucket = binaryOperators;
}
bucket.set(operator.name, operator);
bucket.push(operator);
},
addTagHandler: (tagHandler) => {
tagHandlers.push(tagHandler);

View File

@ -32,7 +32,6 @@ import {createMatchesNode} from "../node/expression/binary/matches";
import {createStartsWithNode} from "../node/expression/binary/starts-with";
import {createEndsWithNode} from "../node/expression/binary/ends-with";
import {createFilter} from "../filter";
import {createApplyTagHandler} from "../tag-handler/apply";
import {createOperator} from "../operator";
import {isEven} from "./core/tests/is-even";
import {isOdd} from "./core/tests/is-odd";
@ -78,7 +77,6 @@ import {column} from "./core/filters/column";
import {filter} from "./core/filters/filter";
import {map} from "./core/filters/map";
import {reduce} from "./core/filters/reduce";
import {createAutoEscapeTagHandler} from "../tag-handler/auto-escape";
import {range} from "./core/functions/range";
import {constant} from "./core/functions/constant";
import {cycle} from "./core/functions/cycle";
@ -89,28 +87,11 @@ import {dump} from "./core/functions/dump";
import {isEmpty} from "./core/tests/is-empty";
import {isIterable} from "./core/tests/is-iterable";
import {date as dateFunction} from "./core/functions/date";
import {createSetTagHandler} from "../tag-handler/set";
import {createIfTagHandler} from "../tag-handler/if";
import {createForTagHandler} from "../tag-handler/for";
import {createVerbatimTagHandler} from "../tag-handler/verbatim";
import {createEmbedTagHandler} from "../tag-handler/embed";
import {createExtendsTagHandler} from "../tag-handler/extends";
import {createBlockTagHandler} from "../tag-handler/block";
import {createSpacelessTagHandler} from "../tag-handler/spaceless";
import {createIncludeTagHandler} from "../tag-handler/include";
import {createDeprecatedTagHandler} from "../tag-handler/deprecated";
import {createUseTagHandler} from "../tag-handler/use";
import {createImportTagHandler} from "../tag-handler/import";
import {createMacroTagHandler} from "../tag-handler/macro";
import {createFilterTagHandler} from "../tag-handler/filter";
import {createWithTagHandler} from "../tag-handler/with";
import {createFromTagHandler} from "../tag-handler/from";
import {createLineTagHandler} from "../tag-handler/line";
import {createSandboxTagHandler} from "../tag-handler/sandbox";
import {createDoTagHandler} from "../tag-handler/do";
import {createFlushTagHandler} from "../tag-handler/flush";
import {isDefined} from "./core/tests/is-defined";
import {isConstant} from "./core/tests/is-constant";
import {createSpaceshipNode} from "../node/expression/binary/spaceship";
import {createHasEveryNode} from "../node/expression/binary/has-every";
import {createHasSomeNode} from "../node/expression/binary/has-some";
export const createCoreExtension = (): TwingExtension => {
return {
@ -125,9 +106,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'charset',
defaultValue: null
}
], {
needs_template: true
});
]);
});
return [
@ -169,16 +148,12 @@ export const createCoreExtension = (): TwingExtension => {
name: 'timezone',
defaultValue: null
}
], {
needs_template: true
}),
]),
createFilter('date_modify', dateModify, [
{
name: 'modifier'
}
], {
needs_template: true
}),
]),
createFilter('default', defaultFilter, [
{
name: 'default',
@ -242,9 +217,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'thousand_sep',
defaultValue: null
}
], {
needs_template: true
}),
]),
createFilter('raw', raw, []),
createFilter('reduce', reduce, [
{
@ -289,7 +262,10 @@ export const createCoreExtension = (): TwingExtension => {
defaultValue: false
}
]),
createFilter('sort', sort, []),
createFilter('sort', sort, [{
name: 'arrow',
defaultValue: null
}]),
createFilter('spaceless', spaceless, []),
createFilter('split', split, [
{
@ -326,9 +302,7 @@ export const createCoreExtension = (): TwingExtension => {
createFunction('constant', constant, [
{name: 'name'},
{name: 'object', defaultValue: null}
], {
needs_context: true
}),
]),
createFunction('cycle', cycle, [
{
name: 'values'
@ -346,11 +320,8 @@ export const createCoreExtension = (): TwingExtension => {
name: 'timezone',
defaultValue: null
}
], {
needs_template: true
}),
]),
createFunction('dump', dump, [], {
needs_context: true,
is_variadic: true
}),
createFunction('include', include, [
@ -373,12 +344,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'sandboxed',
defaultValue: false
}
], {
needs_template: true,
needs_context: true,
needs_output_buffer: true,
needs_source_map_runtime: true
}),
]),
createFunction('max', max, [], {
is_variadic: true
}),
@ -394,9 +360,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'max',
defaultValue: null
}
], {
needs_template: true
}),
]),
createFunction('range', range, [
{
name: 'low'
@ -417,9 +381,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'ignore_missing',
defaultValue: false
}
], {
needs_template: true
}),
]),
createFunction('template_from_string', templateFromString, [
{
name: 'template'
@ -428,9 +390,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'name',
defaultValue: null
}
], {
needs_template: true
})
])
];
},
get nodeVisitors() {
@ -468,6 +428,9 @@ export const createCoreExtension = (): TwingExtension => {
createOperator('!=', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsNotEqualToNode(operands, line, column);
}),
createOperator('<=>', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createSpaceshipNode(operands, line, column);
}),
createOperator('<', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsLessThanNode(operands, line, column);
}),
@ -495,6 +458,12 @@ export const createCoreExtension = (): TwingExtension => {
createOperator('ends with', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createEndsWithNode(operands, line, column);
}),
createOperator('has some', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createHasSomeNode(operands, line, column);
}, "LEFT", 3),
createOperator('has every', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createHasEveryNode(operands, line, column);
}, "LEFT", 3),
createOperator('..', "BINARY", 25, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createRangeNode(operands, line, column);
}),
@ -528,30 +497,7 @@ export const createCoreExtension = (): TwingExtension => {
];
},
get tagHandlers() {
return [
createApplyTagHandler(),
createAutoEscapeTagHandler(),
createBlockTagHandler(),
createDeprecatedTagHandler(),
createDoTagHandler(),
createEmbedTagHandler(),
createExtendsTagHandler(),
createFilterTagHandler(),
createFlushTagHandler(),
createForTagHandler(),
createFromTagHandler(),
createIfTagHandler(),
createImportTagHandler(),
createIncludeTagHandler(),
createLineTagHandler(),
createMacroTagHandler(),
createSandboxTagHandler(),
createSetTagHandler(),
createSpacelessTagHandler(),
createUseTagHandler(),
createVerbatimTagHandler(),
createWithTagHandler()
];
return [];
},
get tests() {
return [
@ -563,9 +509,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'object',
defaultValue: null
}
], {
needs_context: true
}),
]),
createTest('divisible by', isDivisibleBy, [
{
name: 'divisor'

View File

@ -1,9 +1,12 @@
import {TwingCallable} from "../../../callable-wrapper";
/**
* Return the absolute value of a number.
*
* @param {number} x
* @param _executionContext
* @param x
* @returns {Promise<number>}
*/
export const abs = (x: number): Promise<number> => {
export const abs: TwingCallable = (_executionContext, x: number): Promise<number> => {
return Promise.resolve(Math.abs(x));
};

View File

@ -1,9 +1,11 @@
import {chunk} from "../../../helpers/chunk";
import {fillMap} from "../../../helpers/fill-map";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Batches item.
*
* @param _executionContext
* @param {any[]} items An array of items
* @param {number} size The size of the batch
* @param {any} fill A value used to fill missing items
@ -11,19 +13,25 @@ import {fillMap} from "../../../helpers/fill-map";
*
* @returns Promise<Map<any, any>[]>
*/
export const batch = (items: Array<any>, size: number, fill: any, preserveKeys: boolean): Promise<Array<Map<any, any>>> => {
export const batch: TwingCallable<[
items: Array<any>,
size: number,
fill: any,
preserveKeys: boolean
], Array<Map<any, any>>> = (_executionContext, items, size, fill, preserveKeys) => {
if ((items === null) || (items === undefined)) {
return Promise.resolve([]);
}
return chunk(items, size, preserveKeys)
.then((chunks) => {
if (fill !== null && chunks.length) {
const last = chunks.length - 1;
const lastChunk: Map<any, any> = chunks[last];
return chunk(items, size, preserveKeys).then((chunks) => {
if (fill !== null && chunks.length) {
const last = chunks.length - 1;
const lastChunk: Map<any, any> = chunks[last];
fillMap(lastChunk, size, fill);
}
return chunks;
});
fillMap(lastChunk, size, fill);
}
return chunks;
});
};

View File

@ -1,6 +1,7 @@
import type {TwingMarkup} from "../../../markup";
import type {TwingCallable} from "../../../callable-wrapper";
const words = require('capitalize');
const words: (value: string) => string = require('capitalize');
/**
* Returns a capitalized string.
@ -9,7 +10,9 @@ const words = require('capitalize');
*
* @returns {Promise<string>} The capitalized string
*/
export const capitalize = (string: string | TwingMarkup): Promise<string> => {
export const capitalize: TwingCallable<[
string: string | TwingMarkup
], string> = (_executionContext, string) => {
if ((string === null) || (string === undefined) || string === '') {
return Promise.resolve(string);
}

View File

@ -2,6 +2,7 @@ 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";
/**
* Return the values from a single column in the input array.
@ -11,7 +12,7 @@ import {isPlainObject} from "../../../helpers/is-plain-object";
*
* @return {Promise<Array<any>>} The array of values
*/
export const column = (thing: any, columnKey: any): Promise<Array<any>> => {
export const column: TwingCallable = (_executionContext, thing: any, columnKey: any): Promise<Array<any>> => {
let map: Map<any, any>;
if (!isTraversable(thing) || isPlainObject(thing)) {

View File

@ -1,5 +1,10 @@
import {iconv} from "../../../helpers/iconv";
import {TwingCallable} from "../../../callable-wrapper";
export const convertEncoding = (value: string | Buffer, to: string, from: string): Promise<Buffer> => {
export const convertEncoding: TwingCallable<[
value: string | Buffer,
to: string,
from: string
], Buffer> = (_executionContext, value, to, from) => {
return Promise.resolve(iconv(from, to, Buffer.from(value)));
};

View File

@ -1,6 +1,6 @@
import {DateTime} from "luxon";
import {createDate} from "../functions/date";
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Returns a new date object modified.
@ -15,12 +15,14 @@ import {TwingTemplate} from "../../../template";
*
* @returns {Promise<DateTime>} A new date object
*/
export const dateModify = (
template: TwingTemplate,
export const dateModify: TwingCallable = (
executionContext,
date: Date | DateTime | string,
modifier: string
): Promise<DateTime> => {
return createDate(template, date, null)
const {timezone: defaultTimezone} = executionContext;
return createDate(defaultTimezone, date, null)
.then((dateTime) => {
let regExp = new RegExp(/(\+|-)([0-9])(.*)/);
let parts = regExp.exec(modifier)!;

View File

@ -2,7 +2,7 @@ import {DateTime, Duration} from "luxon";
import {formatDuration} from "../../../helpers/format-duration";
import {formatDateTime} from "../../../helpers/format-date-time";
import {date as createDate} from "../functions/date";
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Converts a date to the given format.
@ -11,31 +11,33 @@ import {TwingTemplate} from "../../../template";
* {{ post.published_at|date("m/d/Y") }}
* </pre>
*
* @param {TwingTemplate} template
* @param {DateTime|Duration|string} date A date
* @param {string|null} format The target format, null to use the default
* @param {string|null|boolean} timezone The target timezone, null to use the default, false to leave unchanged
* @param executionContext
* @param date A date
* @param format The target format, null to use the default
* @param timezone The target timezone, null to use the default, false to leave unchanged
*
* @return {Promise<string>} The formatted date
*/
export const date = (
template: TwingTemplate,
export const date: TwingCallable = (
executionContext,
date: DateTime | Duration | string,
format: string | null,
timezone: string | null | false
): Promise<string> => {
return createDate(template, date, timezone)
const {dateFormat, dateIntervalFormat} = executionContext;
return createDate(executionContext, date, timezone)
.then((date) => {
if (date instanceof Duration) {
if (format === null) {
format = template.dateIntervalFormat;
format = dateIntervalFormat;
}
return Promise.resolve(formatDuration(date, format));
}
if (format === null) {
format = template.dateFormat;
format = dateFormat;
}
return Promise.resolve(formatDateTime(date, format));

View File

@ -1,11 +1,16 @@
import {isEmpty} from "../tests/is-empty";
import type {TwingCallable} from "../../../callable-wrapper";
export const defaultFilter = (value: any, defaultValue: any | null): Promise<any> => {
return isEmpty(value)
export const defaultFilter: TwingCallable<[
value: any,
defaultValue: any | null
]> = (executionContext, value, defaultValue) => {
return isEmpty(executionContext, value)
.then((isEmpty) => {
if (isEmpty) {
return Promise.resolve(defaultValue);
} else {
}
else {
return Promise.resolve(value);
}
});

View File

@ -1,22 +1,26 @@
import {createMarkup, TwingMarkup} from "../../../markup";
import {TwingTemplate} from "../../../template";
import type {TwingCallable} from "../../../callable-wrapper";
export const escape = (
template: TwingTemplate,
export const escape: TwingCallable<[
value: string | TwingMarkup | null,
strategy: string | null,
charset: string | null
): Promise<string | boolean | TwingMarkup | null> => {
strategy: string | null
], string | boolean | TwingMarkup | null> = (
executionContext,
value,
strategy
) => {
if (strategy === null) {
strategy = "html";
}
return template.escape(template, value, strategy, charset)
const {template, charset} = executionContext;
return template.escape(value, strategy, charset)
.then((value) => {
if (typeof value === "string") {
return createMarkup(value, template.charset);
return createMarkup(value, charset);
}
return value;
});
};

View File

@ -1,6 +1,7 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
export const filter = async (map: any, callback: (...args: Array<any>) => Promise<boolean>): Promise<Map<any, any>> => {
export const filter: TwingCallable = async (_executionContext, map: any, callback: (...args: Array<any>) => Promise<boolean>): Promise<Map<any, any>> => {
const result: Map<any, any> = new Map();
map = iteratorToMap(map);

View File

@ -1,15 +1,19 @@
import {getFirstValue} from "../../../helpers/get-first-value";
import {slice} from "./slice";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Returns the first element of the item.
*
* @param {any} item
* @param executionContext
* @param item
*
* @returns {Promise<any>} The first element of the item
*/
export const first = (item: any): Promise<any> => {
return slice(item, 0, 1, false)
export const first: TwingCallable<[
item: any
]> = (executionContext, item) => {
return slice(executionContext, item, 0, 1, false)
.then((elements) => {
return typeof elements === 'string' ? elements : getFirstValue(elements);
});

View File

@ -1,6 +1,8 @@
import {TwingCallable} from "../../../callable-wrapper";
const sprintf = require('locutus/php/strings/sprintf');
export const format = (...args: any[]): Promise<string> => {
export const format: TwingCallable = (_executionContext, ...args: any[]): Promise<string> => {
return Promise.resolve(sprintf(...args.map((arg) => {
return arg.toString();
})));

View File

@ -1,5 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Joins the values to a string.
@ -14,13 +15,18 @@ import {iteratorToArray} from "../../../helpers/iterator-to-array";
* {# returns 123 #}
* </pre>
*
* @param {any} value A value
* @param {string} glue The separator
* @param {string | null} and The separator for the last pair
* @param _executionContext
* @param value A value
* @param glue The separator
* @param and The separator for the last pair
*
* @returns {Promise<string>} The concatenated string
*/
export const join = (value: any, glue: string, and: string | null): Promise<string> => {
export const join: TwingCallable<[
value: any,
glue: string,
and: string | null
], string> = (_executionContext, value, glue, and) => {
const _do = (): string => {
if ((value == null) || (value === undefined)) {
return '';
@ -30,11 +36,14 @@ export const join = (value: any, glue: string, and: string | null): Promise<stri
value = iteratorToArray(value);
// this is ugly, but we have to ensure that each element of the array is rendered as PHP would render it
// this is mainly useful for booleans that are not rendered the same way in PHP and JavaScript
const safeValue = value.map((item: any) => {
if (typeof item === 'boolean') {
return (item === true) ? '1' : ''
}
if (Array.isArray(item)) {
return 'Array';
}
return item;
});

View File

@ -3,6 +3,7 @@ import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {isTraversable} from "../../../helpers/is-traversable";
import {TwingCallable} from "../../../callable-wrapper";
function isPureArray(map: Map<any, any>): boolean {
let result: boolean = true;
@ -21,8 +22,8 @@ function isPureArray(map: Map<any, any>): boolean {
return result;
}
export function jsonEncode(value: any): Promise<string> {
const _sanitize = (value: any): any=> {
export const jsonEncode: TwingCallable = (_executionContext, value: any): Promise<string> => {
const _sanitize = (value: any): any => {
if (isTraversable(value) || isPlainObject(value)) {
value = iteratorToMap(value);
}
@ -38,7 +39,8 @@ export function jsonEncode(value: any): Promise<string> {
for (const key in value) {
sanitizedValue.push(_sanitize(value[key]));
}
} else {
}
else {
value = iteratorToHash(value);
sanitizedValue = {};

View File

@ -1,20 +1,26 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Returns the keys of the passed array.
*
* @param {Array<any>} array An array
* @param _executionContext
* @param values An array
*
* @returns {Promise<Array<any>>} The keys
*/
export const keys = (array: Array<any>): Promise<Array<any>> => {
export const keys: TwingCallable<[
values: Array<any>
], Array<any>> = (
_executionContext,
values
) => {
let traversable;
if ((array === null) || (array === undefined)) {
if ((values === null) || (values === undefined)) {
traversable = new Map();
} else {
traversable = iteratorToMap(array);
traversable = iteratorToMap(values);
}
return Promise.resolve([...traversable.keys()]);

View File

@ -1,15 +1,19 @@
import {getFirstValue} from "../../../helpers/get-first-value";
import {slice} from "./slice";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Returns the last element of the item.
*
* @param executionContext
* @param item A variable
*
* @returns The last element of the item
*/
export const last = (item: any): Promise<any> => {
return slice(item, -1, 1, false)
export const last: TwingCallable<[
item: any
]> = (executionContext, item) => {
return slice(executionContext, item, -1, 1, false)
.then((elements) => {
return typeof elements === 'string' ? elements : getFirstValue(elements);
});

View File

@ -1,3 +1,5 @@
import {TwingCallable} from "../../../callable-wrapper";
/**
* Returns the length of a thing.
*
@ -5,7 +7,7 @@
*
* @returns {Promise<number>} The length of the thing
*/
export const length = (thing: any): Promise<number> => {
export const length: TwingCallable = (_executionContext,thing: any): Promise<number> => {
let length: number;
if ((thing === null) || (thing === undefined)) {

View File

@ -1,4 +1,5 @@
import type {TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Converts a string to lowercase.
@ -7,6 +8,6 @@ import type {TwingMarkup} from "../../../markup";
*
* @returns {Promise<string>} The lowercased string
*/
export const lower = (string: string | TwingMarkup): Promise<string> => {
export const lower: TwingCallable = (_executionContext,string: string | TwingMarkup): Promise<string> => {
return Promise.resolve(string.toString().toLowerCase());
};

View File

@ -1,6 +1,10 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
export const map = async (map: any, callback: (...args: Array<any>) => Promise<any>): Promise<Map<any, any>> => {
export const map: TwingCallable<[
map: any,
callback: (...args: Array<any>) => Promise<any>
], Map<any, any>> = async (_executionContext, map, callback) => {
const result: Map<any, any> = new Map();
map = iteratorToMap(map);

View File

@ -2,6 +2,7 @@ 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";
/**
* Merges an array with another one.
@ -19,7 +20,7 @@ import {iteratorToMap} from "../../../helpers/iterator-to-map";
*
* @return {Promise<Map<any, any>>} The merged map
*/
export const merge = (iterable1: any, source: any): Promise<Map<any, any>> => {
export const merge: TwingCallable = (_executionContext, iterable1: any, source: any): Promise<Map<any, any>> => {
const isIterable1NullOrUndefined = (iterable1 === null) || (iterable1 === undefined);
if (isIterable1NullOrUndefined || (!isTraversable(iterable1) && (typeof iterable1 !== 'object'))) {

View File

@ -1,8 +1,9 @@
import type {TwingMarkup} from "../../../markup";
import {createMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
const phpNl2br = require('locutus/php/strings/nl2br');
export const nl2br = (...args: Array<any>): Promise<TwingMarkup> => {
export const nl2br: TwingCallable = (_executionContext, ...args: Array<any>): Promise<TwingMarkup> => {
return Promise.resolve(createMarkup(phpNl2br(...args)));
};

View File

@ -1,4 +1,4 @@
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
const phpNumberFormat = require('locutus/php/strings/number_format');
@ -16,24 +16,26 @@ const phpNumberFormat = require('locutus/php/strings/number_format');
*
* @returns {Promise<string>} The formatted number
*/
export const numberFormat = (
template: TwingTemplate,
export const numberFormat: TwingCallable = (
executionContext,
number: any,
numberOfDecimals: number | null,
numberOfDecimals: number | null,
decimalPoint: string | null,
thousandSeparator: string | null
): Promise<string> => {
const {numberFormat} = executionContext;
if (numberOfDecimals === null) {
numberOfDecimals = template.numberFormat.numberOfDecimals;
numberOfDecimals = numberFormat.numberOfDecimals;
}
if (decimalPoint === null) {
decimalPoint = template.numberFormat.decimalPoint;
decimalPoint = numberFormat.decimalPoint;
}
if (thousandSeparator === null) {
thousandSeparator = template.numberFormat.thousandSeparator;
thousandSeparator = numberFormat.thousandSeparator;
}
return Promise.resolve(phpNumberFormat(number, numberOfDecimals, decimalPoint, thousandSeparator));
};

View File

@ -1,4 +1,5 @@
import {createMarkup, TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Marks a variable as being safe.
@ -7,6 +8,6 @@ import {createMarkup, TwingMarkup} from "../../../markup";
*
* @return {Promise<string>}
*/
export function raw(value: string | TwingMarkup | null): Promise<TwingMarkup> {
export const raw: TwingCallable = (_executionContext, value: string | TwingMarkup | null): Promise<TwingMarkup> => {
return Promise.resolve(createMarkup(value !== null ? value.toString() : ''));
}
};

View File

@ -1,6 +1,7 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
export const reduce = (map: any, callback: (accumulator: any, currentValue: any) => any, initial: any): Promise<string> => {
export const reduce: TwingCallable = (_executionContext, map: any, callback: (accumulator: any, currentValue: any) => any, initial: any): Promise<string> => {
map = iteratorToMap(map);
const values: any[] = [...map.values()];

View File

@ -1,6 +1,7 @@
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');
@ -12,7 +13,7 @@ const phpStrtr = require('locutus/php/strings/strtr');
*
* @returns {Promise<string>}
*/
export const replace = (value: string | null, from: any): Promise<string> => {
export const replace: TwingCallable = (_executionContext,value: string | null, from: any): Promise<string> => {
const _do = (): string => {
if (isTraversable(from)) {
from = iteratorToHash(from);

View File

@ -1,5 +1,6 @@
import {reverse as reverseHelper} from "../../../helpers/reverse";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
const esrever = require('esrever');
@ -11,7 +12,7 @@ const esrever = require('esrever');
*
* @returns {Promise<string | Map<any, any>>} The reversed input
*/
export const reverse = (item: any, preserveKeys: boolean): Promise<string | Map<any, any>> => {
export const reverse: TwingCallable = (_executionContext, item: any, preserveKeys: boolean): Promise<string | Map<any, any>> => {
if (typeof item === 'string') {
return Promise.resolve(esrever.reverse(item));
} else {

View File

@ -1,4 +1,5 @@
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpRound = require('locutus/php/math/round');
const phpCeil = require('locutus/php/math/ceil');
@ -13,7 +14,7 @@ const phpFloor = require('locutus/php/math/floor');
*
* @returns {Promise<number>} The rounded number
*/
export const round = (value: any, precision: number, method: string): Promise<number> => {
export const round: TwingCallable = (_executionContext, value: any, precision: number, method: string): Promise<number> => {
const _do = (): number => {
if (method === 'common') {
return phpRound(value, precision);

View File

@ -1,18 +1,25 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {sliceMap} from "../../../helpers/slice-map";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Slices a variable.
*
* @param _executionContext
* @param item A variable
* @param {number} start Start of the slice
* @param {number} length Size of the slice
* @param {boolean} preserveKeys Whether to preserve key or not (when the input is an object)
* @param start Start of the slice
* @param length Size of the slice
* @param preserveKeys Whether to preserve key or not (when the input is an object)
*
* @returns {Promise<string | Map<any, any>>} The sliced variable
*/
export const slice = (item: any, start: number, length: number | null, preserveKeys: boolean): Promise<string | Map<any, any>> => {
export const slice: TwingCallable<[
item: any,
start: number,
length: number | null,
preserveKeys: boolean
], string | Map<any, any>> = (_executionContext, item, start, length, preserveKeys) => {
if (isTraversable(item)) {
const iterableItem = iteratorToMap(item);

View File

@ -2,22 +2,28 @@ import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {asort} from "../../../helpers/asort";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Sorts an iterable.
*
* @param {any} iterable
* @param _executionContext
* @param iterable
* @param arrow
*
* @returns {Promise<Map<any, any>>}
*/
export const sort = (iterable: any): Promise<Map<any, any>> => {
export const sort: TwingCallable<[
iterable: any,
arrow: ((a: any, b: any) => Promise<-1 | 0 | 1>) | null
], Map<any, any>> = async (_executionContext, iterable, arrow)=> {
if (!isTraversable(iterable)) {
return Promise.reject(createRuntimeError(`The sort filter only works with iterables, got "${typeof iterable}".`));
}
const map = iteratorToMap(iterable);
asort(map);
return Promise.resolve(map);
await asort(map, arrow || undefined);
return map;
};

View File

@ -1,10 +1,11 @@
import {createMarkup, TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Removes whitespaces between HTML tags.
*
* @return {Promise<TwingMarkup>}
*/
export const spaceless = (content: string | TwingMarkup): Promise<TwingMarkup> => {
export const spaceless: TwingCallable = (_executionContext, content: string | TwingMarkup): Promise<TwingMarkup> => {
return Promise.resolve(createMarkup(content.toString().replace(/>\s+</g, '><').trim()));
};

View File

@ -1,3 +1,5 @@
import {TwingCallable} from "../../../callable-wrapper";
const explode = require('locutus/php/strings/explode');
/**
@ -23,7 +25,7 @@ const explode = require('locutus/php/strings/explode');
*
* @returns {Promise<Array<string>>} The split string as an array
*/
export const split = (value: string, delimiter: string, limit: number | null): Promise<Array<string>> => {
export const split: TwingCallable = (_executionContext, value: string, delimiter: string, limit: number | null): Promise<Array<string>> => {
let _do = (): Array<string> => {
if (delimiter) {
return !limit ? explode(delimiter, value) : explode(delimiter, value, limit);

View File

@ -1,5 +1,7 @@
import {TwingCallable} from "../../../callable-wrapper";
const phpStripTags = require('locutus/php/strings/strip_tags');
export const striptags = (input: string, allowedTags: string): Promise<string> => {
export const striptags: TwingCallable = (_executionContext, input: string, allowedTags: string): Promise<string> => {
return Promise.resolve(phpStripTags(input, allowedTags));
};

View File

@ -1,15 +1,19 @@
import type {TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
const phpUcwords = require('locutus/php/strings/ucwords');
/**
* Returns a title-cased string.
*
* @param {string | TwingMarkup} string A string
* @param _executionContext
* @param string A string
*
* @returns {Promise<string>} The title-cased string
* @returns The title-cased string
*/
export const title = (string: string | TwingMarkup): Promise<string> => {
export const title: TwingCallable<[
string: string | TwingMarkup
], string> = (_executionContext, string) => {
const result: string = phpUcwords(string.toString().toLowerCase());
return Promise.resolve(result);

View File

@ -1,4 +1,5 @@
import {createRuntimeError} from "../../../error/runtime";
import {TwingCallable} from "../../../callable-wrapper";
const phpTrim = require('locutus/php/strings/trim');
const phpLeftTrim = require('locutus/php/strings/ltrim');
@ -11,7 +12,7 @@ const phpRightTrim = require('locutus/php/strings/rtrim');
*
* @throws TwingErrorRuntime When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
*/
export const trim = (string: string, characterMask: string | null, side: string): Promise<string> => {
export const trim: TwingCallable = (_executionContext, string: string, characterMask: string | null, side: string): Promise<string> => {
const _do = (): string => {
if (characterMask === null) {
characterMask = " \t\n\r\0\x0B";
@ -31,8 +32,7 @@ export const trim = (string: string, characterMask: string | null, side: string)
try {
return Promise.resolve(_do());
}
catch (error: any) {
} catch (error: any) {
return Promise.reject(error);
}
};

View File

@ -1,4 +1,5 @@
import type {TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Converts a string to uppercase.
@ -7,6 +8,6 @@ import type {TwingMarkup} from "../../../markup";
*
* @returns {Promise<string>} The uppercased string
*/
export const upper = (string: string | TwingMarkup): Promise<string> => {
export const upper: TwingCallable = (_executionContext, string: string | TwingMarkup): Promise<string> => {
return Promise.resolve(string.toString().toUpperCase());
};

View File

@ -1,5 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToHash} from "../../../helpers/iterator-to-hash";
import {TwingCallable} from "../../../callable-wrapper";
const phpHttpBuildQuery = require('locutus/php/url/http_build_query');
@ -10,14 +11,14 @@ const phpHttpBuildQuery = require('locutus/php/url/http_build_query');
*
* @returns {Promise<string>} The URL encoded value
*/
export const url_encode = (url: string | {}): Promise<string> => {
export const url_encode: TwingCallable = (_executionContext, url: string | {}): Promise<string> => {
if (typeof url !== 'string') {
if (isTraversable(url)) {
url = iteratorToHash(url);
}
const builtUrl: string = phpHttpBuildQuery(url, '', '&');
return Promise.resolve(builtUrl.replace(/\+/g, '%20'));
}

View File

@ -1,10 +1,13 @@
import type {TwingContext} from "../../../context";
import {getConstant as constantHelper} from "../../../helpers/get-constant";
import type {TwingCallable} from "../../../callable-wrapper";
export const constant = (
context: TwingContext<any, any>,
name: string,
export const constant: TwingCallable<[
name: string,
object: any | null
]> = (
executionContext,
name,
object
): Promise<any> => {
return Promise.resolve(constantHelper(context, name, object));
return Promise.resolve(constantHelper(executionContext.context, name, object));
};

View File

@ -1,17 +1,34 @@
import {isAMapLike} from "../../../helpers/map-like";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Cycles over a value.
*
* @param {Map<any, any> | any} value
* @param {number} position The cycle position
* @param _executionContext
* @param value
* @param position The cycle position
*
* @returns {Promise<any>} The value at position
* @returns The value at position
*/
export function cycle(value: Map<any, any> | any, position: number): Promise<any> {
if (!isAMapLike(value)) {
export const cycle: TwingCallable<[
value: Map<any, any> | Array<any> | string | boolean | null,
position: number
]> = (_executionContext, value, position) => {
if (!isAMapLike(value) && !Array.isArray(value)) {
return Promise.resolve(value);
}
return Promise.resolve([...(value as Map<any, any>).values()][position % (value as Map<any, any>).size]);
let values: Array<any>;
let size: number;
if (Array.isArray(value)) {
values = value;
size = value.length;
}
else {
values = [...value.values()];
size = value.size;
}
return Promise.resolve(values[position % size]);
}

View File

@ -1,7 +1,7 @@
import {DateTime, Duration} from "luxon";
import {modifyDate} from "../../../helpers/modify-date";
import {createRuntimeError} from "../../../error/runtime";
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Converts an input to a DateTime instance.
@ -19,12 +19,10 @@ import {TwingTemplate} from "../../../template";
* @returns {Promise<DateTime | Duration>}
*/
export const createDate = (
template: TwingTemplate,
defaultTimezone: string,
input: Date | DateTime | number | string | null,
timezone: string | null | false
): Promise<DateTime> => {
const defaultTimezone = template.timezone;
const _do = (): DateTime => {
let result: DateTime;
@ -37,7 +35,8 @@ export const createDate = (
else if (typeof input === 'string') {
if (input === 'now') {
result = DateTime.local();
} else {
}
else {
result = DateTime.fromISO(input, {
setZone: true
});
@ -75,17 +74,17 @@ export const createDate = (
if (!result || !result.isValid) {
throw createRuntimeError(`Failed to parse date "${input}".`);
}
// now let's apply timezone
// determine the timezone
if (timezone !== false) {
if (timezone === null) {
timezone = defaultTimezone;
}
result = result.setZone(timezone);
}
return result;
};
@ -96,14 +95,14 @@ export const createDate = (
}
}
export const date = (
template: TwingTemplate,
export const date: TwingCallable = (
executionContext,
date: Date | DateTime | Duration | number | string | null,
timezone: string | null | false
): Promise<DateTime | Duration> => {
if (date instanceof Duration) {
return Promise.resolve(date);
}
return createDate(template, date, timezone);
return createDate(executionContext.timezone, date, timezone);
}

View File

@ -1,12 +1,15 @@
import {iterate} from "../../../helpers/iterate";
import {createMarkup, TwingMarkup} from "../../../markup";
import {varDump} from "../../../helpers/php";
import type {TwingCallable} from "../../../callable-wrapper";
export const dump = (context: any, ...vars: Array<any>): Promise<TwingMarkup> => {
export const dump: TwingCallable<[
...vars: Array<any>
], TwingMarkup> = (executionContext, ...vars) => {
if (vars.length < 1) {
const vars_ = new Map();
return iterate(context, (key, value) => {
return iterate(executionContext.context, (key, value) => {
vars_.set(key, value);
return Promise.resolve();

View File

@ -3,39 +3,38 @@ import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {createRuntimeError} from "../../../error/runtime";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {TwingOutputBuffer} from "../../../output-buffer";
import {createContext, TwingContext} from "../../../context";
import {isAMapLike} from "../../../helpers/map-like";
import {createContext} from "../../../context";
import {createMarkup, TwingMarkup} from "../../../markup";
import {TwingSourceMapRuntime} from "../../../source-map-runtime";
import {TwingTemplate} from "../../../template";
import type {TwingTemplate} from "../../../template";
import type {TwingCallable} from "../../../callable-wrapper";
/**
* Renders a template.
*
* @param {TwingTemplate} template
* @param {TwingContext<any, any>} context
* @param {TwingSource} from
* @param {TwingOutputBuffer} outputBuffer
* @param {string | Map<number, string | TwingTemplate>} templates The template to render or an array of templates to try consecutively
* @param {any} variables The variables to pass to the template
* @param {boolean} withContext
* @param {boolean} ignoreMissing Whether to ignore missing templates or not
* @param {boolean} sandboxed Whether to sandbox the template or not
* @param executionContext
* @param templates The template to render or an array of templates to try consecutively
* @param variables The variables to pass to the template
* @param withContext
* @param ignoreMissing Whether to ignore missing templates or not
* @param sandboxed
*
* @returns {Promise<TwingMarkup>} The rendered template
*/
export function include(
template: TwingTemplate,
context: TwingContext<any, any>,
outputBuffer: TwingOutputBuffer,
sourceMapRuntime: TwingSourceMapRuntime | null,
templates: string | Map<number, string | TwingTemplate | null> | TwingTemplate | null,
export const include: TwingCallable<[
templates: string | TwingTemplate | null | Array<string | TwingTemplate | null> ,
variables: Map<string, any>,
withContext: boolean,
ignoreMissing: boolean,
sandboxed: boolean
): Promise<TwingMarkup> {
]> = (
executionContext,
templates,
variables,
withContext,
ignoreMissing,
sandboxed
): Promise<TwingMarkup> => {
const {template, charset, context, outputBuffer, sourceMapRuntime} = executionContext;
const from = template.name;
if (!isPlainObject(variables) && !isTraversable(variables)) {
@ -50,27 +49,26 @@ export function include(
variables = mergeIterables(context, variables);
}
if (!isAMapLike(templates)) {
templates = new Map([[0, templates]]);
if (!Array.isArray(templates)) {
templates =[templates];
}
const resolveTemplate = (templates: Map<number, string | TwingTemplate | null>): Promise<TwingTemplate | null> => {
return template.resolveTemplate([...templates.values()])
const resolveTemplate = (templates: Array<string | TwingTemplate | null>): Promise<TwingTemplate | null> => {
return template.resolveTemplate(templates)
.catch((error) => {
if (!ignoreMissing) {
throw error;
} else {
}
else {
return null;
}
});
};
const {charset} = template;
return resolveTemplate(templates)
.then((template) => {
outputBuffer.start();
if (template) {
return template.render(
createContext(variables),
@ -80,13 +78,14 @@ export function include(
sourceMapRuntime: sourceMapRuntime || undefined
}
);
} else {
}
else {
return Promise.resolve('');
}
})
.then(() => {
const result = outputBuffer.getAndClean();
return createMarkup(result, charset);
});
}

View File

@ -1,10 +1,13 @@
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {max as phpMax} from "locutus/php/math";
import type {TwingCallable} from "../../../callable-wrapper";
export function max(...values: Array<any>): Promise<any> {
export const max: TwingCallable<[
...values: Array<any>
]> = (_executionContext, ...values) => {
if (values.length === 1) {
values = values[0];
}
return Promise.resolve(phpMax(iteratorToArray(values)));
}
};

View File

@ -1,10 +1,13 @@
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {min as phpMin} from "locutus/php/math";
import type {TwingCallable} from "../../../callable-wrapper";
export function min(...values: Array<any>): Promise<any> {
export const min: TwingCallable<[
...values: Array<any>
]> = (_executionContext, ...values) => {
if (values.length === 1) {
values = values[0];
}
return Promise.resolve(phpMin(iteratorToArray(values)));
}
};

View File

@ -2,7 +2,7 @@ import {iconv} from "../../../helpers/iconv";
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {createRuntimeError} from "../../../error/runtime";
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
const runes = require('runes');
const mt_rand = require('locutus/php/math/mt_rand');
@ -22,7 +22,9 @@ const array_rand = require('locutus/php/array/array_rand');
*
* @returns {Promise<any>} A random value from the given sequence
*/
export function random(template: TwingTemplate, values: any | null, max: number | null): any {
export const random: TwingCallable = (executionContext, values: any | null, max: number | null): any => {
const {charset} = executionContext;
let _do = (): any => {
if (values === null) {
return max === null ? mt_rand() : mt_rand(0, max);
@ -54,9 +56,7 @@ export function random(template: TwingTemplate, values: any | null, max: number
if (values.toString() === '') {
return '';
}
let charset = template.charset;
if (charset !== 'UTF-8') {
values = iconv(charset, 'UTF-8', values);
}

View File

@ -1,5 +1,12 @@
import {createRange} from "../../../helpers/create-range";
import {TwingCallable} from "../../../callable-wrapper";
export function range<V>(low: V, high: V, step: number): Promise<Map<number, V>> {
type Range<V = any> = TwingCallable<[
low: V,
high: V,
step: number
], Map<number, V>>;
export const range: Range = (_executionContext, low, high, step) => {
return Promise.resolve(createRange(low, high, step));
}

View File

@ -1,16 +1,21 @@
import {createTemplateLoadingError} from "../../../error/loader";
import {TwingTemplate} from "../../../template";
import type {TwingCallable} from "../../../callable-wrapper";
/**
* Returns a template content without rendering it.
*
* @param {TwingTemplate} template
* @param {string} name The template name
* @param {boolean} ignoreMissing Whether to ignore missing templates or not
* @param executionContext
* @param name The template name
* @param ignoreMissing Whether to ignore missing templates or not
*
* @return {Promise<string>} The template source
*/
export const source = (template: TwingTemplate, name: string, ignoreMissing: boolean): Promise<string | null> => {
export const source: TwingCallable<[
name: string,
ignoreMissing: boolean
], string | null> = (executionContext, name, ignoreMissing) => {
const {template} = executionContext;
return template.getTemplateSource(name)
.then((source) => {
if (!ignoreMissing && (source === null)) {

View File

@ -1,4 +1,5 @@
import {TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
/**
* Loads a template from a string.
@ -7,12 +8,14 @@ import {TwingTemplate} from "../../../template";
* {{ include(template_from_string("Hello {{ name }}")) }}
* </pre>
*
* @param {TwingTemplate} template A TwingTemplate instance
* @param {string} string A template as a string or object implementing toString()
* @param {string} name An optional name for the template to be used in error messages
* @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 function templateFromString(template: TwingTemplate, string: string, name: string | null): 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,11 +1,15 @@
import type {TwingContext} from "../../../context";
import {getConstant} from "../../../helpers/get-constant";
import type {TwingCallable} from "../../../callable-wrapper";
export function isConstant(
context: TwingContext<any, any>,
export const isConstant: TwingCallable<[
comparand: any,
constant: any,
object: any | null
): Promise<boolean> {
return Promise.resolve(comparand === getConstant(context, constant, object));
}
], boolean> = (
executionContext,
comparand,
constant,
object
) => {
return Promise.resolve(comparand === getConstant(executionContext.context, constant, object));
};

View File

@ -1,5 +1,10 @@
export function isDefined(
import {TwingCallable} from "../../../callable-wrapper";
export const isDefined: TwingCallable<[
value: any
): Promise<boolean> {
], boolean> = (
_executionContext,
value
) => {
return Promise.resolve(!!value);
}
};

View File

@ -1,3 +1,5 @@
export function isDivisibleBy(a: any, b: any): Promise<boolean> {
return Promise.resolve(a % b === 0);
}
import type {TwingCallable} from "../../../callable-wrapper";
export const isDivisibleBy: TwingCallable<[a: any, divisor: any], boolean> = (_executionContext, a, divisor) => {
return Promise.resolve(a % divisor === 0);
};

View File

@ -1,4 +1,5 @@
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {TwingCallable} from "../../../callable-wrapper";
const isPlainObject = require('is-plain-object');
@ -12,11 +13,12 @@ const isPlainObject = require('is-plain-object');
* {% endif %}
* </pre>
*
* @param executionContext
* @param value A variable
*
* @returns {boolean} true if the value is empty, false otherwise
*/
export function isEmpty(value: any): Promise<boolean> {
export const isEmpty: TwingCallable<[value: any], boolean> = (executionContext, value) => {
if (value === null || value === undefined) {
return Promise.resolve(true);
}
@ -31,7 +33,7 @@ export function isEmpty(value: any): Promise<boolean> {
if (isPlainObject(value)) {
if (value.hasOwnProperty('toString') && typeof value.toString === 'function') {
return isEmpty(value.toString());
return isEmpty(executionContext, value.toString());
}
else {
return Promise.resolve(iteratorToArray(value).length < 1);
@ -39,8 +41,8 @@ export function isEmpty(value: any): Promise<boolean> {
}
if (typeof value === 'object' && value.toString && typeof value.toString === 'function') {
return isEmpty(value.toString());
return isEmpty(executionContext, value.toString());
}
return Promise.resolve(value === false);
}
};

View File

@ -1,3 +1,5 @@
export function isEven(value: any): Promise<boolean> {
import {TwingCallable} from "../../../callable-wrapper";
export const isEven: TwingCallable<[value: any], boolean> = (_executionContext, value) => {
return Promise.resolve(value % 2 === 0);
}
};

View File

@ -1,3 +1,5 @@
import {TwingCallable} from "../../../callable-wrapper";
/**
* Checks if a variable is traversable.
*
@ -12,7 +14,7 @@
*
* @return {Promise<boolean>} true if the value is traversable
*/
export function isIterable(value: any): Promise<boolean> {
export const isIterable: TwingCallable<[value: any], boolean> = (_executionContext, value) => {
let _do = (): boolean => {
/*
Prevent `(null)[Symbol.iterator]`/`(undefined)[Symbol.iterator]` error,
@ -49,4 +51,4 @@ export function isIterable(value: any): Promise<boolean> {
};
return Promise.resolve(_do());
}
};

View File

@ -1,3 +1,5 @@
export function isNull(value: any): Promise<boolean> {
import {TwingCallable} from "../../../callable-wrapper";
export const isNull: TwingCallable<[value: any], boolean> = (_executionContext, value) => {
return Promise.resolve(value === null);
}
};

View File

@ -1,3 +1,5 @@
export function isOdd(value: any): Promise<boolean> {
import {TwingCallable} from "../../../callable-wrapper";
export const isOdd: TwingCallable<[value: any], boolean> = (_executionContext, value) => {
return Promise.resolve(value % 2 === 1);
}
};

View File

@ -1,3 +1,5 @@
export function isSameAs(a: any, b: any): Promise<boolean> {
return Promise.resolve(a === b);
}
import {TwingCallable} from "../../../callable-wrapper";
export const isSameAs: TwingCallable<[a: any, comparand: any], boolean> = (_executionContext, a, comparand) => {
return Promise.resolve(a === comparand);
};

View File

@ -6,7 +6,7 @@ import {
export type TwingFilterOptions = TwingCallableWrapperOptions;
export interface TwingFilter extends TwingCallableWrapper<TwingCallable> {
export interface TwingFilter extends TwingCallableWrapper {
}

View File

@ -4,7 +4,7 @@ import {
TwingCallable, TwingCallableWrapper, createCallableWrapper
} from "./callable-wrapper";
export interface TwingFunction extends TwingCallableWrapper<TwingCallable> {
export interface TwingFunction extends TwingCallableWrapper {
}

View File

@ -1,29 +1,39 @@
import {sortAsynchronously} from "./sort";
/**
* Sort a map and maintain index association.
*
* @param {Map<*, *>} map
* @param {Function} handler
* @returns {Map<* ,*>}
* @param map
* @param compareFunction
* @returns
*/
export function asort(map: Map<any, any>, handler: any = undefined) {
let sortedMap = new Map();
export const asort = async (map: Map<any, any>, compareFunction?: (a: any, b: any) => Promise<-1 | 0 | 1>) => {
const sortedMap = new Map();
const keys: Array<any> = ([] as Array<any>).fill(null, 0, map.size);
const values = [...map.values()];
let keys: Array<any> = ([] as Array<any>).fill(null, 0, map.size);
let sortedValues = [...map.values()].sort(handler);
let sortedValues: Array<any>;
for (let [key, value] of map) {
let index = sortedValues.indexOf(value);
if (compareFunction) {
sortedValues = await sortAsynchronously(values, compareFunction);
}
else {
sortedValues = values.sort();
}
for (const [key, value] of map) {
const index = sortedValues.indexOf(value);
keys[index] = key;
}
for (let key of keys) {
for (const key of keys) {
sortedMap.set(key, map.get(key));
}
map.clear();
for (let [key, value] of sortedMap) {
for (const [key, value] of sortedMap) {
map.set(key, value);
}
}

View File

@ -9,13 +9,23 @@ import {DateTime} from "luxon";
import {isAMapLike, MapLike} from "./map-like";
import {isAMarkup, TwingMarkup} from "../markup";
import {TwingContext} from "../context";
import {iteratorToMap} from "./iterator-to-map";
type Operand = Buffer | TwingMarkup | DateTime | MapLike<any, any> | string | boolean | number | null | object;
type Operand = Buffer | TwingMarkup | DateTime | MapLike<any, any> | string | boolean | number | null | object | Array<any>;
export function compare(
firstOperand: Operand,
secondOperand: Operand
): boolean {
// Array<any>
if (Array.isArray(firstOperand)) {
firstOperand = iteratorToMap(firstOperand);
}
if (Array.isArray(secondOperand)) {
secondOperand = iteratorToMap(secondOperand);
}
// null
if (firstOperand === null) {
return compareToNull(secondOperand);

View File

@ -1,7 +1,10 @@
import {isAMapLike} from "./map-like";
export const evaluate = (value: any): boolean => {
if (value === '0' || (isAMapLike(value) && value.size === 0)) {
if (value === '0'
|| (isAMapLike(value) && value.size === 0)
|| (Array.isArray(value) && value.length === 0)
) {
return false;
}
else if (Number.isNaN(value)) {

View File

@ -32,10 +32,11 @@ export const getAttribute = (
type: TwingGetAttributeCallType,
shouldTestExistence: boolean,
shouldIgnoreStrictCheck: boolean | null,
sandboxed: boolean
sandboxed: boolean,
isStrictVariables: boolean
): Promise<any> => {
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !template.isStrictVariables : shouldIgnoreStrictCheck;
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !isStrictVariables : shouldIgnoreStrictCheck;
const _do = (): any => {
let message: string;
@ -45,14 +46,20 @@ export const getAttribute = (
if (isBoolean(attribute)) {
arrayItem = attribute ? 1 : 0;
} else if (isFloat(attribute)) {
}
else if (isFloat(attribute)) {
arrayItem = parseInt(attribute);
} else {
}
else {
arrayItem = attribute;
}
if (object) {
if ((isAMapLike(object) && object.has(arrayItem)) || (isPlainObject(object) && Reflect.has(object, arrayItem))) {
if (
(isAMapLike(object) && object.has(arrayItem))
|| (Array.isArray(object) && (typeof arrayItem === "number") && (object.length > arrayItem))
|| (isPlainObject(object) && Reflect.has(object, arrayItem))
) {
if (shouldTestExistence) {
return true;
}
@ -65,7 +72,12 @@ export const getAttribute = (
}
}
if ((type === "array") || (isAMapLike(object)) || (object === null) || (typeof object !== 'object')) {
if ((type === "array")
|| (isAMapLike(object))
|| (Array.isArray(object))
|| (object === null)
|| (typeof object !== 'object')
) {
if (shouldTestExistence) {
return false;
}
@ -73,24 +85,37 @@ export const getAttribute = (
if (shouldIgnoreStrictCheck) {
return;
}
if (isAMapLike(object)) {
if ((object as Map<any, any>).size < 1) {
message = `Index "${arrayItem}" is out of bounds as the array is empty.`;
} else {
message = `Index "${arrayItem}" is out of bounds for array [${[...(object as Map<any, any>).values()]}].`;
}
} else if (type === "array") {
// object is another kind of object
if (object === null) {
message = `Impossible to access a key ("${attribute}") on a null variable.`;
} else {
message = `Impossible to access a key ("${attribute}") on a ${typeof object} variable ("${object.toString()}").`;
}
} else if (object === null) {
if (object === null) {
// object is null
message = `Impossible to access an attribute ("${attribute}") on a null variable.`;
} else {
if (type === "array") {
message = `Impossible to access a key ("${attribute}") on a null variable.`;
}
else {
message = `Impossible to access an attribute ("${attribute}") on a null variable.`;
}
}
else if (isAMapLike(object)) {
if (object.size < 1) {
message = `Index "${arrayItem}" is out of bounds as the array is empty.`;
}
else {
message = `Index "${arrayItem}" is out of bounds for array [${[...object.values()]}].`;
}
}
else if (Array.isArray(object)) {
if (object.length < 1) {
message = `Index "${arrayItem}" is out of bounds as the array is empty.`;
}
else {
message = `Index "${arrayItem}" is out of bounds for array [${[...object]}].`;
}
}
else if (type === "array") {
// object is another kind of object
message = `Impossible to access a key ("${attribute}") on a ${typeof object} variable ("${object.toString()}").`;
}
else {
// object is a primitive
message = `Impossible to access an attribute ("${attribute}") on a ${typeof object} variable ("${object}").`;
}
@ -111,9 +136,11 @@ export const getAttribute = (
if (object === null) {
message = `Impossible to invoke a method ("${attribute}") on a null variable.`;
} else if (isAMapLike(object)) {
}
else if (isAMapLike(object) || Array.isArray(object)) {
message = `Impossible to invoke a method ("${attribute}") on an array.`;
} else {
}
else {
message = `Impossible to invoke a method ("${attribute}") on a ${typeof object} variable ("${object}").`;
}
@ -167,17 +194,20 @@ export const getAttribute = (
if (lcName[0] === 'g' && lcName.indexOf('get') === 0) {
name = method.substr(3);
lcName = lcName.substr(3);
} else if (lcName[0] === 'i' && lcName.indexOf('is') === 0) {
}
else if (lcName[0] === 'i' && lcName.indexOf('is') === 0) {
name = method.substr(2);
lcName = lcName.substr(2);
} else if (lcName[0] === 'h' && lcName.indexOf('has') === 0) {
}
else if (lcName[0] === 'h' && lcName.indexOf('has') === 0) {
name = method.substr(3);
lcName = lcName.substr(3);
if (lcMethods.includes('is' + lcName)) {
continue;
}
} else {
}
else {
continue;
}
@ -199,9 +229,11 @@ export const getAttribute = (
if (candidates.has(attribute)) {
method = candidates.get(attribute);
} else if (candidates.has(lcItem = itemAsString.toLowerCase())) {
}
else if (candidates.has(lcItem = itemAsString.toLowerCase())) {
method = candidates.get(lcItem);
} else {
}
else {
if (shouldTestExistence) {
return false;
}

View File

@ -1,9 +1,10 @@
import type {TwingContext} from "../context";
import {createRuntimeError} from "../error/runtime";
import {TwingTemplate} from "../template";
export const getContextValue = (
template: TwingTemplate,
charset: string,
templateName: string,
isStrictVariables: boolean,
context: TwingContext<any, any>,
name: string,
isAlwaysDefined: boolean,
@ -11,9 +12,9 @@ export const getContextValue = (
shouldTestExistence: boolean
): Promise<any> => {
const specialNames = new Map<string, any>([
['_self', template.name],
['_self', templateName],
['_context', context],
['_charset', template.charset]
['_charset', charset]
]);
const isSpecial = () => {
@ -33,7 +34,7 @@ export const getContextValue = (
} else if (isAlwaysDefined) {
result = context.get(name);
} else {
if (shouldIgnoreStrictCheck || !template.isStrictVariables) {
if (shouldIgnoreStrictCheck || !isStrictVariables) {
result = context.has(name) ? context.get(name) : null;
} else {
result = context.get(name);

View File

@ -1,4 +1,5 @@
import {TwingContext} from "../context";
import {iteratorToMap} from "./iterator-to-map";
export type MapLike<K, V> = Map<K, V> | TwingContext<K, V>;
@ -11,3 +12,37 @@ export function isAMapLike(candidate: any): candidate is MapLike<any, any> {
(candidate as MapLike<any, any>).set !== undefined &&
(candidate as MapLike<any, any>).entries !== undefined;
}
export const every = async (
iterable: MapLike<any, any> | Array<any>,
comparator: (value: any, key: any) => Promise<boolean>
): Promise<boolean> => {
if (Array.isArray(iterable)) {
iterable = iteratorToMap(iterable);
}
for (const [key, value] of iterable) {
if (await comparator(value, key) === false) {
return false;
}
}
return true;
};
export const some = async (
iterable: MapLike<any, any> | Array<any>,
comparator: (value: any, key: any) => Promise<boolean>
): Promise<boolean> => {
if (Array.isArray(iterable)) {
iterable = iteratorToMap(iterable);
}
for (const [key, value] of iterable) {
if (await comparator(value, key) === true) {
return true;
}
}
return false;
};

77
src/lib/helpers/sort.ts Normal file
View File

@ -0,0 +1,77 @@
type Comparator = (a: any, b: any) => Promise<-1 | 0 | 1>;
export const sortAsynchronously = (array: Array<any>, comparator: Comparator) => {
/**
* return the median value among x, y, and z
*/
const getPivot = async (x: any, y: any, z: any, comparator: Comparator): Promise<any> => {
if (await comparator(x, y) < 0) {
if (await comparator(y, z) < 0) {
return y;
}
else if (await comparator(z, x) < 0) {
return x;
}
else {
return z;
}
}
else if (await comparator(y, z) > 0) {
return y;
}
else if (await comparator(z, x) > 0) {
return x;
}
else {
return z;
}
};
/**
* Asynchronous quick sort.
*
* @see https://gist.github.com/kimamula/fa34190db624239111bbe0deba72a6ab
*
* @param array The array to sort
* @param comparator The comparator function
* @param left The index where the range of elements to be sorted starts
* @param right The index where the range of elements to be sorted ends
*/
const quickSort = async (array: Array<any>, comparator: Comparator, left = 0, right = array.length - 1): Promise<Array<any>> => {
if (left < right) {
let i = left;
let j = right;
let tmp;
const pivot = await getPivot(array[i], array[i + Math.floor((j - i) / 2)], array[j], comparator);
while (true) {
while (await comparator(array[i], pivot) < 0) {
i++;
}
while (await comparator(pivot, array[j]) < 0) {
j--;
}
if (i >= j) {
break;
}
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
i++;
j--;
}
await quickSort(array, comparator, left, i - 1);
await quickSort(array, comparator, j + 1, right);
}
return array;
};
return quickSort(array, comparator);
};

View File

@ -42,6 +42,8 @@ export const typeToEnglish = (type: TokenType): string => {
return 'end of comment statement';
case "ARROW":
return 'arrow function';
case "SPREAD_OPERATOR":
return 'spread operator';
default:
throw new Error(`Token of type "${type}" does not exist.`)
}
@ -49,16 +51,17 @@ export const typeToEnglish = (type: TokenType): string => {
export class TwingLexer extends Lexer {
constructor(
binaryOperators: Map<string, TwingOperator>,
unaryOperators: Map<string, TwingOperator>
level: 2 | 3,
binaryOperators: Array<TwingOperator>,
unaryOperators: Array<TwingOperator>
) {
super();
super(level);
// custom operators
for (let operators of [binaryOperators, unaryOperators]) {
for (let [key] of operators) {
if (!this.operators.includes(key)) {
this.operators.push(key);
for (const operators of [binaryOperators, unaryOperators]) {
for (const {name} of operators) {
if (!this.operators.includes(name)) {
this.operators.push(name);
}
}
}
@ -78,8 +81,15 @@ export class TwingLexer extends Lexer {
}
export const createLexer = (
binaryOperators: Map<string, TwingOperator>,
unaryOperators: Map<string, TwingOperator>
level: 2 | 3,
binaryOperators: Array<TwingOperator>,
unaryOperators: Array<TwingOperator>
): TwingLexer => {
return new TwingLexer(binaryOperators, unaryOperators);
const keepCompatibleOperators = (operator: TwingOperator) => operator.specificationLevel <= level;
return new TwingLexer(
level,
binaryOperators.filter(keepCompatibleOperators),
unaryOperators.filter(keepCompatibleOperators)
);
};

View File

@ -1,9 +1,9 @@
import {TwingNodeVisitor} from "../node-visitor";
import {cloneGetAttributeNode, TwingAttributeAccessorNode} from "../node/expression/attribute-accessor";
import {cloneNameNode} from "../node/expression/name";
import {cloneNameNode, nameNodeType} from "../node/expression/name";
import {blockFunctionNodeType, cloneBlockReferenceExpressionNode} from "../node/expression/block-function";
import {createConstantNode} from "../node/expression/constant";
import {cloneMethodCallNode} from "../node/expression/method-call";
import {constantNodeType, createConstantNode} from "../node/expression/constant";
import {cloneMethodCallNode, methodCallNodeType} from "../node/expression/method-call";
import {TwingBaseExpressionNode} from "../node/expression";
import {createParsingError} from "../error/parsing";
import {createTestNode, testNodeType, TwingTestNode} from "../node/expression/call/test";
@ -11,10 +11,8 @@ import {createArrayNode, getKeyValuePairs} from "../node/expression/array";
import {createConditionalNode} from "../node/expression/conditional";
import {functionNodeType} from "../node/expression/call/function";
import {filterNodeType, TwingFilterNode} from "../node/expression/call/filter";
import {hashNodeType} from "../node/expression/hash";
/**
* todo: describe
*/
export const createCoreNodeVisitor = (): TwingNodeVisitor => {
const enteredNodes: Array<TwingBaseExpressionNode> = [];
@ -50,12 +48,13 @@ export const createCoreNodeVisitor = (): TwingNodeVisitor => {
const operand = node.children.operand!;
if (
!operand.is("name") &&
!operand.is(nameNodeType) &&
!operand.is("get_attribute") &&
!operand.is(blockFunctionNodeType) &&
!operand.is("constant") &&
!operand.is(constantNodeType) &&
!operand.is("array") &&
!operand.is("method_call") &&
!operand.is(hashNodeType) &&
!operand.is(methodCallNodeType) &&
!(operand.is(functionNodeType) && (operand.attributes.operatorName === 'constant'))
) {
throw createParsingError('The "defined" test only works with simple variables.', node);

View File

@ -29,9 +29,9 @@ import type {TwingIfNode} from "./node/if";
import type {TwingMethodCallNode} from "./node/expression/method-call";
import type {TwingEscapeNode} from "./node/expression/escape";
import type {TwingApplyNode} from "./node/apply";
import type {TwingNodeExecutionContext} from "./execution-context"; // todo: change
import type {TwingExecutionContext} from "./execution-context"; // todo: change
export type {TwingNodeExecutionContext} from "./execution-context"; // todo: change
export type {TwingExecutionContext} from "./execution-context"; // todo: change
export type TwingNode =
| TwingApplyNode
@ -88,7 +88,7 @@ export interface TwingBaseNode<
readonly tag: string | null;
readonly type: Type;
execute(executionContext: TwingNodeExecutionContext): Promise<any>;
execute(executionContext: TwingExecutionContext): Promise<any>;
is<Type extends string>(type: Type): this is TwingNode & {
type: Type;

View File

@ -16,6 +16,7 @@ import type {TwingMethodCallNode} from "./expression/method-call";
import type {TwingNullishCoalescingNode} from "./expression/nullish-coalescing";
import type {TwingParentFunctionNode} from "./expression/parent-function";
import type {ArgumentsNode} from "./expression/arguments";
import type {TwingSpreadNode} from "./expression/spread";
export type TwingExpressionNode =
| ArgumentsNode
@ -34,6 +35,7 @@ export type TwingExpressionNode =
| TwingNameNode
| TwingNullishCoalescingNode
| TwingParentFunctionNode
| TwingSpreadNode
| TwingUnaryNode
;

View File

@ -5,6 +5,7 @@ import {
} from "../expression";
import {TwingConstantNode, createConstantNode} from "./constant";
import {pushToRecord} from "../../helpers/record";
import {spreadNodeType} from "./spread";
const array_chunk = require('locutus/php/array/array_chunk');
@ -47,19 +48,7 @@ export const createBaseArrayNode = <Type extends string>(
const baseNode = createBaseExpressionNode(type, {}, children, line, column);
const node: TwingBaseArrayNode<Type> = {
...baseNode,
execute: (executionContext): Promise<Map<string, any>> => {
const keyValuePairs = getKeyValuePairs(node);
const promises = keyValuePairs.map(async ({key, value}) => {
return await Promise.all([
key.execute(executionContext),
value.execute(executionContext)
]);
});
return Promise.all(promises)
.then((entries) => new Map(entries));
}
...baseNode
};
return node;
@ -83,6 +72,23 @@ export const createArrayNode = (
}), line, column);
return {
...baseNode
...baseNode,
execute: async (executionContext) => {
const keyValuePairs = getKeyValuePairs(baseNode);
const array: Array<any> = [];
for (const {value: valueNode} of keyValuePairs) {
const value = await valueNode.execute(executionContext);
if (valueNode.is(spreadNodeType)) {
array.push(...value);
}
else {
array.push(value);
}
}
return array;
}
};
};

View File

@ -27,7 +27,7 @@ export const createArrowFunctionNode = (
const {context} = executionContext;
const {expr} = baseNode.children;
const assignmentNodes = Object.values(baseNode.children.names.children);
return Promise.resolve((...functionArgs: Array<any>): Promise<any> => {
let index = 0;
@ -38,7 +38,7 @@ export const createArrowFunctionNode = (
index++;
}
return expr.execute(executionContext);
});
}

View File

@ -45,7 +45,7 @@ export const createAttributeAccessorNode = (
const node: TwingAttributeAccessorNode = {
...baseNode,
execute: (executionContext) => {
const {template, sandboxed} = executionContext;
const {template, sandboxed, isStrictVariables} = executionContext;
const {target, attribute, arguments: methodArguments} = node.children;
const {type, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
@ -64,7 +64,8 @@ export const createAttributeAccessorNode = (
type,
shouldTestExistence,
shouldIgnoreStrictCheck || null,
sandboxed
sandboxed,
isStrictVariables
)
})
}

View File

@ -1,5 +1,5 @@
import type {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes} from "../expression";
import type {TwingNode, TwingNodeExecutionContext, TwingNodeType} from "../../node";
import type {TwingNode, TwingExecutionContext, TwingNodeType} from "../../node";
import type {TwingAddNode} from "./binary/add";
import type {TwingAndNode} from "./binary/and";
import type {TwingBitwiseAndNode} from "./binary/bitwise-and";
@ -26,6 +26,9 @@ import type {TwingRangeNode} from "./binary/range";
import type {TwingStartsWithNode} from "./binary/starts-with";
import type {TwingSubtractNode} from "./binary/subtract";
import {createBaseExpressionNode} from "../expression";
import type {TwingSpaceshipNode} from "./binary/spaceship";
import type {TwingHasEveryNode} from "./binary/has-every";
import type {TwingHasSomeNode} from "./binary/has-some";
export type TwingBinaryNode =
| TwingAddNode
@ -36,6 +39,8 @@ export type TwingBinaryNode =
| TwingConcatenateNode
| TwingDivideNode
| TwingEndsWithNode
| TwingHasEveryNode
| TwingHasSomeNode
| TwingIsEqualToNode
| TwingDivideAndFloorNode
| TwingIsGreaterThanNode
@ -51,6 +56,7 @@ export type TwingBinaryNode =
| TwingOrNode
| TwingPowerNode
| TwingRangeNode
| TwingSpaceshipNode
| TwingStartsWithNode
| TwingSubtractNode
;
@ -84,7 +90,7 @@ export const createBinaryNodeFactory = <InstanceType extends TwingBaseBinaryNode
execute: (
left: TwingBaseExpressionNode,
right: TwingBaseExpressionNode,
executionContext: TwingNodeExecutionContext
executionContext: TwingExecutionContext
) => Promise<any>
}
) => {

View File

@ -1,11 +1,13 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
export interface TwingEndsWithNode extends TwingBaseBinaryNode<"ends_with"> {
export const endsWithNodeType = "ends_with";
export interface TwingEndsWithNode extends TwingBaseBinaryNode<typeof endsWithNodeType> {
}
export const createEndsWithNode = createBinaryNodeFactory<TwingEndsWithNode>(
'ends_with',
endsWithNodeType,
{
execute: async (left, right, executionContext) => {
const leftValue = await left.execute(executionContext);

View File

@ -0,0 +1,28 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {every, isAMapLike} from "../../../helpers/map-like";
export const hasEveryNodeType = "has_every";
export interface TwingHasEveryNode extends TwingBaseBinaryNode<typeof hasEveryNodeType> {
}
export const createHasEveryNode = createBinaryNodeFactory<TwingHasEveryNode>(
hasEveryNodeType,
{
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

@ -0,0 +1,28 @@
import {TwingBaseBinaryNode, createBinaryNodeFactory} from "../binary";
import {isAMapLike, some} from "../../../helpers/map-like";
export const hasSomeNodeType = "has_some";
export interface TwingHasSomeNode extends TwingBaseBinaryNode<typeof hasSomeNodeType> {
}
export const createHasSomeNode = createBinaryNodeFactory<TwingHasSomeNode>(
hasSomeNodeType,
{
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

@ -0,0 +1,17 @@
import type {TwingBaseBinaryNode} from "../binary";
import {createBinaryNodeFactory} from "../binary";
import {compare} from "../../../helpers/compare";
export const spaceshipNodeType = "spaceship";
export interface TwingSpaceshipNode extends TwingBaseBinaryNode<typeof spaceshipNodeType> {
}
export const createSpaceshipNode = createBinaryNodeFactory<TwingSpaceshipNode>(spaceshipNodeType, {
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

@ -11,6 +11,7 @@ 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');
@ -82,7 +83,8 @@ export const createBaseCallNode = <Type extends CallType>(
if (typeof name === "string") {
named = true;
name = normalizeName(name);
} else if (named) {
}
else if (named) {
throw createRuntimeError(`Positional arguments cannot be used after named arguments for ${callType} "${callName}".`, baseNode);
}
@ -115,7 +117,8 @@ export const createBaseCallNode = <Type extends CallType>(
arguments_.push(parameter.value);
parameters.delete(name);
optionalArguments = [];
} else {
}
else {
const parameter = parameters.get(position);
if (parameter) {
@ -124,9 +127,11 @@ export const createBaseCallNode = <Type extends CallType>(
parameters.delete(position);
optionalArguments = [];
++position;
} else if (callableParameter.defaultValue !== undefined) {
}
else if (callableParameter.defaultValue !== undefined) {
arguments_.push(createConstantNode(callableParameter.defaultValue, line, column));
} else {
}
else {
throw createRuntimeError(`Value for argument "${name}" is required for ${callType} "${callName}".`, baseNode);
}
}
@ -164,10 +169,10 @@ export const createBaseCallNode = <Type extends CallType>(
const node: TwingBaseCallNode<typeof type> = {
...baseNode,
execute: async (executionContext) => {
const {template, context, outputBuffer, sourceMapRuntime} = executionContext
const {template} = executionContext
const {operatorName} = node.attributes;
let callableWrapper: TwingCallableWrapper<any> | null;
let callableWrapper: TwingCallableWrapper | null;
switch (type) {
case "filter":
@ -199,22 +204,6 @@ export const createBaseCallNode = <Type extends CallType>(
const actualArguments: Array<any> = [];
if (callableWrapper!.needsTemplate) {
actualArguments.push(template);
}
if (callableWrapper!.needsContext) {
actualArguments.push(context);
}
if (callableWrapper!.needsOutputBuffer) {
actualArguments.push(outputBuffer);
}
if (callableWrapper!.needsSourceMapRuntime) {
actualArguments.push(sourceMapRuntime);
}
actualArguments.push(...callableWrapper!.nativeArguments);
if (operand) {
@ -227,9 +216,9 @@ export const createBaseCallNode = <Type extends CallType>(
actualArguments.push(...providedArguments);
const traceableCallable = callableWrapper.getTraceableCallable(node.line, node.column, template.name);
const traceableCallable = getTraceableMethod(callableWrapper.callable, node.line, node.column, template.name);
return traceableCallable(...actualArguments);
return traceableCallable(executionContext, ...actualArguments);
}
};

View File

@ -1,13 +1,15 @@
import type {TwingBaseExpressionNode} from "../expression";
import {createBaseExpressionNode} from "../expression";
export const constantNodeType = "constant";
export type TwingConstantNodeValue = string | number | boolean | null;
type TwingConstantNodeAttributes<Value extends TwingConstantNodeValue> = {
value: Value;
};
export interface TwingConstantNode<Value extends TwingConstantNodeValue = TwingConstantNodeValue> extends TwingBaseExpressionNode<"constant", TwingConstantNodeAttributes<Value>> {
export interface TwingConstantNode<Value extends TwingConstantNodeValue = TwingConstantNodeValue> extends TwingBaseExpressionNode<typeof constantNodeType, TwingConstantNodeAttributes<Value>> {
}
export const createConstantNode = <Value extends string | number | boolean | null>(
@ -15,7 +17,7 @@ export const createConstantNode = <Value extends string | number | boolean | nul
line: number,
column: number
): TwingConstantNode<Value> => {
const parent = createBaseExpressionNode('constant', {
const parent = createBaseExpressionNode(constantNodeType, {
value
}, {}, line, column);
@ -25,6 +27,6 @@ export const createConstantNode = <Value extends string | number | boolean | nul
return Promise.resolve(node.attributes.value);
}
};
return node;
};

View File

@ -33,7 +33,7 @@ export const createEscapeNode = (
.then((value) => {
const escape = getTraceableMethod(template.escape, node.line, node.column, template.name);
return escape(template, value, strategy, null, true);
return escape(value, strategy, null, true);
});
}
};

View File

@ -1,5 +1,6 @@
import {TwingBaseArrayNode, createBaseArrayNode} from "./array";
import {TwingBaseArrayNode, createBaseArrayNode, getKeyValuePairs} from "./array";
import type {TwingBaseExpressionNode} from "../expression";
import {spreadNodeType} from "./spread";
export const hashNodeType = 'hash'
@ -16,10 +17,36 @@ export const createHashNode = (
): TwingHashNode => {
const baseNode = createBaseArrayNode(hashNodeType, elements, line, column);
return {
const hashNode: TwingHashNode = {
...baseNode,
is: (type) => {
return type === "hash" || type === "array";
}
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.is(spreadNodeType)) {
for (const [valueKey, valueValue] of value as Map<any, any>) {
hash.set(valueKey, valueValue);
}
}
else {
hash.set(key, value);
}
}
return hash;
},
// todo: remove once confirmed that it is not needed
// is: (type) => {
// return type === "hash" || type === "array";
// }
};
return hashNode;
};

View File

@ -31,7 +31,7 @@ export const createNameNode = (
const node: TwingNameNode = {
...baseNode,
execute: ({template, context}) => {
execute: async ({template, context, charset, isStrictVariables}) => {
const {name, isAlwaysDefined, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
const traceableGetContextValue = getTraceableMethod(
@ -40,9 +40,11 @@ export const createNameNode = (
node.column,
template.name
);
return traceableGetContextValue(
template,
charset,
template.name,
isStrictVariables,
context,
name,
isAlwaysDefined,

View File

@ -0,0 +1,30 @@
import {createBaseExpressionNode, TwingBaseExpressionNode} from "../expression";
export const spreadNodeType = "spread";
export interface TwingSpreadNode extends TwingBaseExpressionNode<typeof spreadNodeType, {}, {
iterable: TwingBaseExpressionNode;
}> {
}
export const createSpreadNode = (
iterable: TwingBaseExpressionNode,
line: number,
column: number
): TwingSpreadNode => {
const baseNode = createBaseExpressionNode(spreadNodeType, {}, {
iterable
}, line, column);
const spreadNode: TwingSpreadNode = {
...baseNode,
execute: (executionContext) => {
const {iterable} = spreadNode.children;
return iterable.execute(executionContext);
}
};
return spreadNode;
};

View File

@ -1,5 +1,5 @@
import {TwingBaseExpressionNode, TwingBaseExpressionNodeAttributes, createBaseExpressionNode} from "../expression";
import type {TwingNodeExecutionContext, TwingNodeType} from "../../node";
import type {TwingExecutionContext, TwingNodeType} from "../../node";
import type {TwingNegativeNode} from "./unary/neg";
import type {TwingNotNode} from "./unary/not";
import type {TwingPositiveNode} from "./unary/pos";
@ -20,7 +20,7 @@ export const createUnaryNodeFactory = <InstanceType extends TwingBaseUnaryNode<a
definition: {
execute: (
operand: TwingBaseExpressionNode,
executionContext: TwingNodeExecutionContext
executionContext: TwingExecutionContext
) => Promise<any>;
}
) => {

View File

@ -1,4 +1,4 @@
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode, TwingNodeExecutionContext} from "../node";
import {TwingBaseNode, TwingBaseNodeAttributes, createBaseNode, TwingExecutionContext} from "../node";
import type {TwingBaseExpressionNode} from "./expression";
import type {TwingTemplate} from "../template";
import {getTraceableMethod} from "../helpers/traceable-method";
@ -24,7 +24,7 @@ export const createBaseIncludeNode = <Type extends string, Attributes extends Ba
type: Type,
attributes: Attributes,
children: Children,
getTemplate: (executionContext: TwingNodeExecutionContext) => Promise<TwingTemplate | null>,
getTemplate: (executionContext: TwingExecutionContext) => Promise<TwingTemplate | null | Array<TwingTemplate | null>>,
line: number,
column: number,
tag: string
@ -34,20 +34,17 @@ export const createBaseIncludeNode = <Type extends string, Attributes extends Ba
const node: TwingBaseIncludeNode<Type, Attributes, Children> = {
...baseNode,
execute: async (executionContext) => {
const {context, outputBuffer, sandboxed, sourceMapRuntime, template} = executionContext;
const {outputBuffer, sandboxed, template} = executionContext;
const {variables} = node.children;
const {only, ignoreMissing} = node.attributes;
const templateToInclude = await getTemplate(executionContext);
const templatesToInclude = await getTemplate(executionContext);
const traceableInclude = getTraceableMethod(include, baseNode.line, baseNode.column, template.name);
const output = await traceableInclude(
template,
context,
outputBuffer,
sourceMapRuntime || null,
templateToInclude,
executionContext,
templatesToInclude,
await variables.execute(executionContext),
!only,
ignoreMissing,

View File

@ -26,6 +26,10 @@ export const createPrintNode = (
return printNode.children.expression.execute(executionContext)
.then((result) => {
if (Array.isArray(result)) {
result = 'Array';
}
outputBuffer.echo(result);
sourceMapRuntime?.leaveSourceMapBlock(outputBuffer);

View File

@ -65,14 +65,14 @@ export const createSetNode = (
}
} 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++;
}
}

View File

@ -22,6 +22,8 @@ export interface TwingOperator {
readonly associativity: OperatorAssociativity | null;
readonly specificationLevel: 2 | 3;
readonly expressionFactory: TwingOperatorExpressionFactory;
}
@ -30,7 +32,8 @@ export const createOperator = (
type: OperatorType,
precedence: number,
expressionFactory: TwingOperatorExpressionFactory,
associativity: OperatorAssociativity | null = null
associativity: OperatorAssociativity | null = null,
specificationLevel: 2 | 3 = 2,
): TwingOperator => {
associativity = type === "BINARY" ? (associativity || "LEFT") : null;
@ -47,6 +50,9 @@ export const createOperator = (
get precedence() {
return precedence;
},
get specificationLevel() {
return specificationLevel;
},
get type() {
return type;
}

View File

@ -50,6 +50,29 @@ import {createTestNode} from "./node/expression/call/test";
import {positiveNodeType} from "./node/expression/unary/pos";
import {negativeNodeType} from "./node/expression/unary/neg";
import {createEscaperNodeVisitor} from "./node-visitor/escaper";
import {createApplyTagHandler} from "./tag-handler/apply";
import {createAutoEscapeTagHandler} from "./tag-handler/auto-escape";
import {createBlockTagHandler} from "./tag-handler/block";
import {createDeprecatedTagHandler} from "./tag-handler/deprecated";
import {createDoTagHandler} from "./tag-handler/do";
import {createEmbedTagHandler} from "./tag-handler/embed";
import {createExtendsTagHandler} from "./tag-handler/extends";
import {createFilterTagHandler} from "./tag-handler/filter";
import {createFlushTagHandler} from "./tag-handler/flush";
import {createForTagHandler} from "./tag-handler/for";
import {createFromTagHandler} from "./tag-handler/from";
import {createIfTagHandler} from "./tag-handler/if";
import {createImportTagHandler} from "./tag-handler/import";
import {createIncludeTagHandler} from "./tag-handler/include";
import {createLineTagHandler} from "./tag-handler/line";
import {createMacroTagHandler} from "./tag-handler/macro";
import {createSandboxTagHandler} from "./tag-handler/sandbox";
import {createSetTagHandler} from "./tag-handler/set";
import {createSpacelessTagHandler} from "./tag-handler/spaceless";
import {createUseTagHandler} from "./tag-handler/use";
import {createVerbatimTagHandler} from "./tag-handler/verbatim";
import {createWithTagHandler} from "./tag-handler/with";
import {createSpreadNode} from "./node/expression/spread";
const nameRegExp = new RegExp(namePattern);
@ -68,7 +91,8 @@ type TwingParserImportedSymbol = {
};
export type TwingParserOptions = {
strict: boolean;
strict?: boolean;
level?: 2 | 3
};
type ParseTest = [tag: string, test: (token: Token) => boolean];
@ -136,21 +160,67 @@ export type StackEntry = {
};
const getNames = (
map: Map<string, TwingCallableWrapper<any>>
map: Map<string, TwingCallableWrapper>
): Array<string> => {
return [...map.values()].map(({name}) => name);
};
export const createParser = (
unaryOperators: Map<string, TwingOperator>,
binaryOperators: Map<string, TwingOperator>,
tagHandlers: Array<TwingTagHandler>,
unaryOperators: Array<TwingOperator>,
binaryOperators: Array<TwingOperator>,
additionalTagHandlers: Array<TwingTagHandler>,
visitors: Array<TwingNodeVisitor>,
filters: Map<string, TwingFilter>,
functions: Map<string, TwingFunction>,
tests: Map<string, TwingTest>,
options: TwingParserOptions
options?: TwingParserOptions
): TwingParser => {
const strict = options?.strict !== undefined ? options.strict : true;
const level = options?.level || 3;
// operators
const binaryOperatorsRegister: Map<string, TwingOperator> = new Map(binaryOperators
.filter((operator) => operator.specificationLevel <= level)
.map((operator) => [operator.name, operator])
);
const unaryOperatorsRegister: Map<string, TwingOperator> = new Map(unaryOperators
.map((operator) => [operator.name, operator])
);
// tag handlers
const tagHandlers: Array<TwingTagHandler> = [
createApplyTagHandler(),
createAutoEscapeTagHandler(),
createBlockTagHandler(),
createDeprecatedTagHandler(),
createDoTagHandler(),
createEmbedTagHandler(),
createExtendsTagHandler(),
createFlushTagHandler(),
createForTagHandler(),
createFromTagHandler(),
createIfTagHandler(),
createImportTagHandler(),
createIncludeTagHandler(),
createLineTagHandler(),
createMacroTagHandler(),
createSandboxTagHandler(),
createSetTagHandler(),
createUseTagHandler(),
createVerbatimTagHandler(),
createWithTagHandler()
];
if (level === 2) {
tagHandlers.push(...[
createFilterTagHandler(),
createSpacelessTagHandler(),
]);
}
tagHandlers.push(...additionalTagHandlers);
const tokenParsers: Map<string, TwingTokenParser> = new Map();
let varNameSalt = 0;
@ -182,7 +252,8 @@ export const createParser = (
name: name!,
node: node!
});
} else {
}
else {
localScope[type].push(alias);
}
};
@ -216,7 +287,7 @@ export const createParser = (
// non-empty text nodes are not allowed as direct child of a
if (node.is(textNodeType) && !isMadeOfWhitespaceOnly(node.attributes.data)) {
const {data} = node.attributes;
if (data.indexOf(String.fromCharCode(0xEF, 0xBB, 0xBF)) > -1) {
const trailingData = data.substring(3);
@ -248,9 +319,14 @@ export const createParser = (
// the content of the block. In such a case, nesting it does not work as
// expected as the definition is not part of the default template code flow.
if (nested && (node.type === "block_reference")) {
console.warn(`Nesting a block definition under a non-capturing node in "${stream.source.name}" at line ${node.line} is deprecated since Twig 2.5.0 and will become a syntax error in Twig 3.0.`);
if (level >= 3) {
throw createParsingError(`A block definition cannot be nested under non-capturing nodes.`, node, stream.source.name);
}
else {
console.warn(`Nesting a block definition under a non-capturing node in "${stream.source.name}" at line ${node.line} is deprecated since Twig 2.5.0 and will become a syntax error in Twig 3.0.`);
return null;
return null;
}
}
if (node.isAnOutputNode && (node.type !== "spaceless")) {
@ -279,7 +355,6 @@ export const createParser = (
};
const getFilterExpressionFactory = (stream: TwingTokenStream, name: string, line: number, column: number) => {
const {strict} = options;
const filter = getFilter(filters, name);
if (filter) {
@ -300,7 +375,8 @@ export const createParser = (
console.warn(message);
}
} else if (strict) {
}
else if (strict) {
const error = createParsingError(`Unknown filter "${name}".`, {line, column}, stream.source.name);
error.addSuggestions(name, filterNames);
@ -312,9 +388,8 @@ export const createParser = (
};
const getFunctionExpressionFactory = (stream: TwingTokenStream, name: string, line: number, column: number) => {
const {strict} = options;
const twingFunction = getFunction(functions, name);
if (twingFunction) {
if (twingFunction.isDeprecated) {
let message = `Function "${twingFunction.name}" is deprecated`;
@ -333,14 +408,15 @@ export const createParser = (
console.warn(message);
}
} else if (strict) {
}
else if (strict) {
const error = createParsingError(`Unknown function "${name}".`, {line, column}, stream.source.name);
error.addSuggestions(name, functionNames);
throw error;
}
return createFunctionNode;
};
@ -350,11 +426,17 @@ export const createParser = (
parseArguments(stream);
if (!getBlockStack().length) {
throw createParsingError('Calling "parent" outside a block is forbidden.', {line, column}, stream.source.name);
throw createParsingError('Calling "parent" outside a block is forbidden.', {
line,
column
}, stream.source.name);
}
if (!parent && !hasTraits()) {
throw createParsingError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', {line, column}, stream.source.name);
throw createParsingError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', {
line,
column
}, stream.source.name);
}
return createParentFunctionNode(peekBlockStack(), line, column);
@ -363,7 +445,10 @@ export const createParser = (
const keyValuePairs = getKeyValuePairs(blockArgs);
if (keyValuePairs.length < 1) {
throw createParsingError('The "block" function takes one argument (the block name).', {line, column}, stream.source.name);
throw createParsingError('The "block" function takes one argument (the block name).', {
line,
column
}, stream.source.name);
}
return createBlockFunctionNode(keyValuePairs[0].value, keyValuePairs.length > 1 ? keyValuePairs[1].value : null, line, column);
@ -372,7 +457,10 @@ export const createParser = (
const attributeKeyValuePairs = getKeyValuePairs(attributeArgs);
if (attributeKeyValuePairs.length < 2) {
throw createParsingError('The "attribute" function takes at least two arguments (the variable and the attributes).', {line, column}, stream.source.name);
throw createParsingError('The "attribute" function takes at least two arguments (the variable and the attributes).', {
line,
column
}, stream.source.name);
}
return createAttributeAccessorNode(
@ -461,11 +549,12 @@ export const createParser = (
const expressionFactory = operator.expressionFactory;
return parsePostfixExpression(stream, expressionFactory([expression, createBaseNode(null)], token.line, token.column), token);
} else if (token.test("PUNCTUATION", '(')) {
}
else if (token.test("PUNCTUATION", '(')) {
stream.next();
const expression = parseExpression(stream);
stream.expect("PUNCTUATION", ')', 'An opened parenthesis is not properly closed');
return parsePostfixExpression(stream, expression, token);
@ -476,7 +565,6 @@ export const createParser = (
const getTestName = (stream: TwingTokenStream): string => {
const {line, column} = stream.current;
const {strict} = options;
let name = stream.expect("NAME").value;
let test: Pick<TwingTest, "alternative" | "deprecatedVersion" | "isDeprecated" | "name"> | null = getTestByName(tests, name);
@ -490,7 +578,8 @@ export const createParser = (
if (test) {
stream.next();
} else {
}
else {
// non-existing two-words test
if (!strict) {
stream.next();
@ -503,7 +592,8 @@ export const createParser = (
};
}
}
} else {
}
else {
// non-existing one-word test
if (!strict) {
test = {
@ -554,11 +644,11 @@ export const createParser = (
};
const isBinary = (token: Token): TwingOperator | null => {
return (token.test("OPERATOR") && binaryOperators.get(token.value)) || null;
return (token.test("OPERATOR") && binaryOperatorsRegister.get(token.value)) || null;
};
const isUnary = (token: Token): TwingOperator | null => {
return (token.test("OPERATOR") && unaryOperators.get(token.value)) || null;
return (token.test("OPERATOR") && unaryOperatorsRegister.get(token.value)) || null;
};
const parse: TwingParser["parse"] = (stream, tag = null, test = null) => {
@ -642,6 +732,7 @@ export const createParser = (
/**
* Parses arguments.
*
* @param stream
* @param namedArguments {boolean} Whether to allow named arguments or not
* @param definition {boolean} Whether we are parsing arguments for a macro definition
* @param allowArrow {boolean}
@ -676,7 +767,8 @@ export const createParser = (
const {line, column} = stream.current;
value = createNameNode(token.value, line, column);
} else {
}
else {
value = parseExpression(stream, 0, allowArrow);
}
@ -697,7 +789,8 @@ export const createParser = (
if (notConstantNode !== null) {
throw createParsingError(`A default value for an argument must be a constant (a boolean, a string, a number, or an array).`, notConstantNode, stream.source.name);
}
} else {
}
else {
value = parseExpression(stream, 0, allowArrow);
}
}
@ -743,7 +836,19 @@ export const createParser = (
first = false;
elements.push(parseExpression(stream));
if (stream.test("SPREAD_OPERATOR")) {
const {current} = stream;
stream.next();
const expression = parseExpression(stream);
const spreadNode = createSpreadNode(expression, current.line, current.column);
elements.push(spreadNode);
}
else {
elements.push(parseExpression(stream));
}
}
stream.expect("PUNCTUATION", ']', 'An opened array is not properly closed');
@ -765,7 +870,8 @@ export const createParser = (
if (stream.test("OPERATOR") && nameRegExp.exec(token.value)) {
// in this context, string operators are variable names
stream.next();
} else {
}
else {
stream.expect("NAME", null, 'Only variables can be assigned to');
}
@ -873,12 +979,14 @@ export const createParser = (
if (stream.nextIf("PUNCTUATION", ':')) {
expr3 = parseExpression(stream);
} else {
}
else {
const {line, column} = stream.current;
expr3 = createConstantNode('', line, column);
}
} else {
}
else {
expr2 = expression;
expr3 = parseExpression(stream);
}
@ -909,16 +1017,18 @@ export const createParser = (
if (token.value === "is not") {
expression = parseNotTestExpression(stream, expression);
} else {
}
else {
expression = parseTestExpression(stream, expression);
}
} else {
}
else {
while (((operator = isBinary(token)) !== null) && operator.precedence >= precedence) {
stream.next();
const {expressionFactory} = operator;
const operand = parseExpression(stream, operator.associativity === "LEFT" ? operator.precedence + 1 : operator.precedence);
const operand = parseExpression(stream, operator.associativity === "LEFT" ? operator.precedence + 1 : operator.precedence, true);
expression = expressionFactory([expression, operand], token.line, token.column);
@ -946,11 +1056,14 @@ export const createParser = (
const token = stream.expect("NAME");
const {value, line, column} = token;
getFilterExpressionFactory(stream, value, token.line, token.column);
let methodArguments;
if (!stream.test("PUNCTUATION", '(')) {
methodArguments = createArrayNode([], line, column);
} else {
}
else {
methodArguments = parseArguments(stream, true, false, true);
}
@ -980,7 +1093,8 @@ export const createParser = (
if (!stream.test("PUNCTUATION", '(')) {
methodArguments = createArrayNode([], line, column);
} else {
}
else {
methodArguments = parseArguments(stream, true, false, true);
}
@ -988,7 +1102,8 @@ export const createParser = (
if (filterNode === null) {
filterNode = factory(operand, value, methodArguments, token.line, token.column);
} else {
}
else {
filterNode = factory(filterNode, value, methodArguments, token.line, token.column);
}
@ -1008,7 +1123,7 @@ export const createParser = (
let first = true;
const elements: Array<{
key: TwingBaseExpressionNode;
key: TwingBaseNode;
value: TwingBaseExpressionNode;
}> = [];
@ -1024,6 +1139,22 @@ export const createParser = (
first = false;
if (stream.test("SPREAD_OPERATOR")) {
const {current} = stream;
stream.next();
const expression = parseExpression(stream);
const spreadNode = createSpreadNode(expression, current.line, current.column);
elements.push({
key: createBaseNode(null),
value: spreadNode
});
continue;
}
// a hash key can be:
//
// * a number -- 12
@ -1035,12 +1166,17 @@ export const createParser = (
if ((token = stream.nextIf("STRING")) || (token = stream.nextIf("NAME")) || (token = stream.nextIf("NUMBER"))) {
key = createConstantNode(token.value, token.line, token.column);
} else if (stream.test("PUNCTUATION", '(')) {
}
else if (stream.test("PUNCTUATION", '(')) {
key = parseExpression(stream);
} else {
}
else {
const {type, line, value, column} = stream.current;
throw createParsingError(`A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "${typeToEnglish(type)}" of value "${value}".`, {line, column}, stream.source.name);
throw createParsingError(`A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "${typeToEnglish(type)}" of value "${value}".`, {
line,
column
}, stream.source.name);
}
stream.expect("PUNCTUATION", ':', 'A hash key must be followed by a colon (:)');
@ -1087,12 +1223,15 @@ export const createParser = (
if (token.type === "PUNCTUATION") {
if (token.value === '.' || token.value === '[') {
node = parseSubscriptExpression(stream, node, prefixToken);
} else if (token.value === '|') {
}
else if (token.value === '|') {
node = parseFilterExpression(stream, node);
} else {
}
else {
break;
}
} else {
}
else {
break;
}
}
@ -1130,7 +1269,8 @@ export const createParser = (
default:
if ('(' === stream.current.value) {
node = getFunctionNode(stream, token.value, token.line, token.column);
} else {
}
else {
node = createNameNode(token.value, token.line, token.column);
}
}
@ -1155,8 +1295,9 @@ export const createParser = (
node = createNameNode(token.value, token.line, token.column);
break;
} else if (unaryOperators.has(token.value)) {
const operator = unaryOperators.get(token.value)!;
}
else if (unaryOperatorsRegister.has(token.value)) {
const operator = unaryOperatorsRegister.get(token.value)!;
stream.next();
@ -1171,11 +1312,14 @@ export const createParser = (
default:
if (token.test("PUNCTUATION", '[')) {
node = parseArrayExpression(stream);
} else if (token.test("PUNCTUATION", '{')) {
}
else if (token.test("PUNCTUATION", '{')) {
node = parseHashExpression(stream);
} else if (token.test("OPERATOR", '=') && (stream.look(-1).value === '==' || stream.look(-1).value === '!=')) {
}
else if (token.test("OPERATOR", '=') && (stream.look(-1).value === '==' || stream.look(-1).value === '!=')) {
throw createParsingError(`Unexpected operator of value "${token.value}". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.`, token, stream.source.name);
} else {
}
else {
throw createParsingError(`Unexpected token "${typeToEnglish(token.type)}" of value "${token.value}".`, token, stream.source.name);
}
}
@ -1194,11 +1338,13 @@ export const createParser = (
if (nextCanBeString && (token = stream.nextIf("STRING"))) {
nodes.push(createConstantNode(token.value, token.line, token.column));
nextCanBeString = false;
} else if (stream.nextIf("INTERPOLATION_START")) {
}
else if (stream.nextIf("INTERPOLATION_START")) {
nodes.push(parseExpression(stream));
stream.expect("INTERPOLATION_END");
nextCanBeString = true;
} else {
}
else {
break;
}
}
@ -1236,7 +1382,7 @@ export const createParser = (
if ((token.type === "NAME") || (token.type === "NUMBER") || (token.type === "OPERATOR" && (match !== null))) {
attribute = createConstantNode(token.value, line, column);
if (stream.test("PUNCTUATION", '(')) {
type = "method";
@ -1246,7 +1392,8 @@ export const createParser = (
elements.push(value);
}
}
} else {
}
else {
throw createParsingError('Expected name or number.', {line, column: column + 1}, stream.source.name);
}
@ -1256,7 +1403,8 @@ export const createParser = (
return methodCallNode;
}
} else {
}
else {
type = "array";
// slice?
@ -1265,7 +1413,8 @@ export const createParser = (
if (stream.test("PUNCTUATION", ':')) {
slice = true;
attribute = createConstantNode(0, token.line, token.column);
} else {
}
else {
attribute = parseExpression(stream);
}
@ -1278,7 +1427,8 @@ export const createParser = (
if (stream.test("PUNCTUATION", ']')) {
length = createConstantNode(null, token.line, token.column);
} else {
}
else {
length = parseExpression(stream) as TwingConstantNode;
}
@ -1315,7 +1465,7 @@ export const createParser = (
if (stream.test("PUNCTUATION", '(')) {
testArguments = parseArguments(stream, true);
}
if ((name === 'defined') && (node.is("name"))) {
const alias = getImportedMethod(node.attributes.name);
@ -1361,12 +1511,12 @@ export const createParser = (
const setMacro: TwingParser["setMacro"] = (name, node) => {
macros[name] = node
};
const subparse: TwingParser["subparse"] = (stream, tag, test) => {
// token parsers
if (tokenParsers.size === 0) {
for (const handler of tagHandlers) {
tokenParsers.set(handler.tag, handler.initialize(parser));
tokenParsers.set(handler.tag, handler.initialize(parser, level));
}
}
@ -1411,7 +1561,7 @@ export const createParser = (
if (!tokenParsers.has(token.value)) {
let error;
if (test !== null) {
error = createParsingError(
`Unexpected "${token.value}" tag`,
@ -1420,7 +1570,8 @@ export const createParser = (
);
error.appendMessage(` (expecting closing tag for the "${tag}" tag defined line ${line}).`);
} else {
}
else {
error = createParsingError(
`Unknown "${token.value}" tag.`,
token,

View File

@ -10,7 +10,7 @@ export interface TwingTagHandler {
/**
* Initializes the tag handler with a parser and returns a token parser.
*/
initialize(parser: TwingParser): TwingTokenParser;
initialize(parser: TwingParser, level: 2 | 3): TwingTokenParser;
/**
* The tag handled by the tag handler.

View File

@ -62,7 +62,7 @@ export const createForTagHandler = (): TwingTagHandler => {
return {
tag,
initialize: (parser) => {
initialize: (parser, level) => {
return (token, stream) => {
const {line, column} = token;
const targets = parser.parseAssignmentExpression(stream);
@ -73,7 +73,7 @@ export const createForTagHandler = (): TwingTagHandler => {
let ifExpression = null;
if (stream.nextIf("NAME", 'if')) {
if ((level < 3) && stream.nextIf("NAME", 'if')) {
console.warn(`Using an "if" condition on "for" tag in "${stream.source.name}" at line ${line} is deprecated since Twig 2.10.0, use a "filter" filter or an "if" condition inside the "for" body instead (if your condition depends on a variable updated inside the loop).`);
ifExpression = parser.parseExpression(stream);

View File

@ -18,7 +18,7 @@ export const createSetTagHandler = (): TwingTagHandler => {
if (stream.nextIf("OPERATOR", '=')) {
values = parser.parseMultiTargetExpression(stream);
stream.expect("TAG_END");
if (getChildrenCount(names) !== getChildrenCount(values)) {

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