mirror of
https://gitlab.com/nightlycommit/twing.git
synced 2025-01-18 08:46:50 +02:00
Merge branch 'issue-562' into '6.x'
* Level 3: Remove support for the spaceless tag See merge request nightlycommit/twing!588
This commit is contained in:
commit
ba4383e3d3
@ -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": {
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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'
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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)));
|
||||
};
|
||||
|
@ -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)!;
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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();
|
||||
})));
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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 = {};
|
||||
|
@ -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()]);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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)) {
|
||||
|
@ -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());
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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'))) {
|
||||
|
@ -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)));
|
||||
};
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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() : ''));
|
||||
}
|
||||
};
|
||||
|
@ -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()];
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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()));
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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());
|
||||
};
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
};
|
||||
|
@ -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)));
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
|
||||
export type TwingFilterOptions = TwingCallableWrapperOptions;
|
||||
|
||||
export interface TwingFilter extends TwingCallableWrapper<TwingCallable> {
|
||||
export interface TwingFilter extends TwingCallableWrapper {
|
||||
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
TwingCallable, TwingCallableWrapper, createCallableWrapper
|
||||
} from "./callable-wrapper";
|
||||
|
||||
export interface TwingFunction extends TwingCallableWrapper<TwingCallable> {
|
||||
export interface TwingFunction extends TwingCallableWrapper {
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
77
src/lib/helpers/sort.ts
Normal 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);
|
||||
};
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
) => {
|
||||
|
@ -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);
|
||||
|
28
src/lib/node/expression/binary/has-every.ts
Normal file
28
src/lib/node/expression/binary/has-every.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
28
src/lib/node/expression/binary/has-some.ts
Normal file
28
src/lib/node/expression/binary/has-some.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
17
src/lib/node/expression/binary/spaceship.ts
Normal file
17
src/lib/node/expression/binary/spaceship.ts
Normal 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);
|
||||
}
|
||||
});
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
30
src/lib/node/expression/spread.ts
Normal file
30
src/lib/node/expression/spread.ts
Normal 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;
|
||||
};
|
@ -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>;
|
||||
}
|
||||
) => {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user