Merge branch 'issue-465' into 'main'

Resolve issue #465

Closes #465

See merge request nightlycommit/twing!611
This commit is contained in:
Eric MORAND 2024-07-27 07:43:06 +00:00
commit 7e02a05e4d
246 changed files with 6515 additions and 853 deletions

View File

@ -18,12 +18,12 @@ export {createTemplateLoadingError} from "./lib/error/loader";
export type {
TwingFilesystemLoader, TwingFilesystemLoaderFilesystem, TwingFilesystemLoaderFilesystemStats
} from "./lib/loader/filesystem";
export type {TwingArrayLoader} from "./lib/loader/array";
export type {TwingArrayLoader, TwingSynchronousArrayLoader} from "./lib/loader/array";
export type {TwingChainLoader} from "./lib/loader/chain";
export type {TwingLoader} from "./lib/loader";
export type {TwingLoader, TwingSynchronousLoader} from "./lib/loader";
export {createFilesystemLoader} from "./lib/loader/filesystem";
export {createArrayLoader} from "./lib/loader/array";
export {createFilesystemLoader, createSynchronousFilesystemLoader} from "./lib/loader/filesystem";
export {createArrayLoader, createSynchronousArrayLoader} from "./lib/loader/array";
export {createChainLoader} from "./lib/loader/chain";
// markup
@ -233,7 +233,7 @@ export {createEmbedNode} from "./lib/node/include/embed";
export {createIncludeNode} from "./lib/node/include/include";
// node executors
export {executeNode, type TwingNodeExecutor} from "./lib/node-executor";
export {executeNode, executeNodeSynchronously, type TwingNodeExecutor, type TwingSynchronousNodeExecutor} from "./lib/node-executor";
// tag handlers
export type {TwingTagHandler, TwingTokenParser} from "./lib/tag-handler";
@ -299,7 +299,7 @@ export interface TwingTemplate {
render: import("./lib/template").TwingTemplate["render"];
}
export {createEnvironment} from "./lib/environment";
export {createEnvironment, createSynchronousEnvironment} from "./lib/environment";
export {createExtensionSet} from "./lib/extension-set";
export {createFilter} from "./lib/filter";
export {createFunction} from "./lib/function";

View File

@ -27,3 +27,31 @@ export interface TwingCache {
*/
getTimestamp: (key: string) => Promise<number>;
}
export interface TwingSynchronousCache {
/**
* Writes a template AST to the cache.
*
* @param key The cache key
* @param content The template AST
*/
write: (key: string, content: TwingTemplateNode) => void;
/**
* Loads a template AST from the cache.
*
* @param key The cache key
*
* @returns The template AST
*/
load: (key: string) => TwingTemplateNode | null;
/**
* Returns the modification timestamp of a key.
*
* @param {string} key The cache key
*
* @returns The modification timestamp
*/
getTimestamp: (key: string) => number;
}

View File

@ -1,6 +1,7 @@
import type {TwingExecutionContext} from "./execution-context";
import {TwingExecutionContext, TwingSynchronousExecutionContext} from "./execution-context";
export type TwingCallable<A extends Array<any> = any, R = any> = (executionContext: TwingExecutionContext, ...args: A) => Promise<R>;
export type TwingSynchronousCallable<A extends Array<any> = any, R = any> = (executionContext: TwingSynchronousExecutionContext, ...args: A) => R;
export type TwingCallableArgument = {
name: string;
@ -29,6 +30,10 @@ export interface TwingCallableWrapper {
nativeArguments: Array<string>;
}
export interface TwingSynchronousCallableWrapper extends Omit<TwingCallableWrapper, "callable"> {
readonly callable: TwingSynchronousCallable;
}
export const createCallableWrapper = (
name: string,
callable: TwingCallable,
@ -37,7 +42,48 @@ export const createCallableWrapper = (
): TwingCallableWrapper => {
let nativeArguments: Array<string> = [];
const callableWrapper: TwingCallableWrapper = {
const callableWrapper = {
get callable() {
return callable;
},
get name() {
return name;
},
get acceptedArguments() {
return acceptedArguments;
},
get alternative() {
return options.alternative;
},
get deprecatedVersion() {
return options.deprecated;
},
get isDeprecated() {
return options.deprecated ? true : false;
},
get isVariadic() {
return options.is_variadic || false;
},
get nativeArguments() {
return nativeArguments;
},
set nativeArguments(values) {
nativeArguments = values;
}
};
return callableWrapper;
};
export const createSynchronousCallableWrapper = (
name: string,
callable: TwingSynchronousCallable,
acceptedArguments: Array<TwingCallableArgument>,
options: TwingCallableWrapperOptions
): TwingSynchronousCallableWrapper => {
let nativeArguments: Array<string> = [];
const callableWrapper = {
get callable() {
return callable;
},

View File

@ -1,13 +1,13 @@
export interface TwingContext<K, V> {
export interface TwingContext<K extends string, V> {
readonly size: number;
[Symbol.iterator](): IterableIterator<[K, V]>;
[Symbol.iterator](): IterableIterator<[string, V]>;
clone(): TwingContext<K, V>;
delete(key: K): boolean;
entries(): IterableIterator<[K, V]>;
entries(): IterableIterator<[string, V]>;
get(key: K): V | undefined;
@ -61,3 +61,13 @@ export const createContext = <K extends string, V>(
return context;
};
export const getEntries = <V>(context: Record<string, V>): IterableIterator<[string, V]> => {
return Object.entries(context)[Symbol.iterator]();
};
export const getValues = <V>(context: Record<string, V>): Array<V> => {
return Object.values(context);
};
export type TwingContext2 = Map<string, any>;

View File

@ -1,11 +1,11 @@
import {TwingTagHandler} from "./tag-handler";
import {TwingNodeVisitor} from "./node-visitor";
import {createExtensionSet} from "./extension-set";
import {TwingFilter} from "./filter";
import {TwingFilter, TwingSynchronousFilter} from "./filter";
import {createParser, TwingParser, TwingParserOptions} from "./parser";
import {TwingLoader} from "./loader";
import {TwingTest} from "./test";
import {TwingFunction} from "./function";
import {TwingLoader, TwingSynchronousLoader} from "./loader";
import {TwingSynchronousTest, TwingTest} from "./test";
import {TwingFunction, TwingSynchronousFunction} from "./function";
import {TwingOperator} from "./operator";
import {TwingEscapingStrategy, TwingEscapingStrategyHandler} from "./escaping-strategy";
import {createHtmlEscapingStrategyHandler} from "./escaping-stragegy/html";
@ -15,19 +15,19 @@ import {createUrlEscapingStrategyHandler} from "./escaping-stragegy/url";
import {createHtmlAttributeEscapingStrategyHandler} from "./escaping-stragegy/html-attribute";
import {TwingSource} from "./source";
import {createTokenStream, TwingTokenStream} from "./token-stream";
import {TwingExtension} from "./extension";
import {TwingExtension, TwingSynchronousExtension} from "./extension";
import {TwingTemplateNode} from "./node/template";
import {RawSourceMap} from "source-map";
import {createSourceMapRuntime} from "./source-map-runtime";
import {createSandboxSecurityPolicy, TwingSandboxSecurityPolicy} from "./sandbox/security-policy";
import {TwingTemplate} from "./template";
import {TwingSynchronousTemplate, TwingTemplate} from "./template";
import {Settings as DateTimeSettings} from "luxon";
import {createLexer, type TwingLexer} from "./lexer";
import {TwingCache} from "./cache";
import {createCoreExtension} from "./extension/core";
import {TwingCache, TwingSynchronousCache} from "./cache";
import {createCoreExtension, createSynchronousCoreExtension} from "./extension/core";
import {createAutoEscapeNode, createTemplateLoadingError, type TwingContext} from "../lib";
import {createTemplateLoader} from "./template-loader";
import {createContext} from "./context";
import {createSynchronousTemplateLoader, createTemplateLoader} from "./template-loader";
import {createContext, TwingContext2} from "./context";
import {iterableToMap} from "./helpers/iterator-to-map";
export type TwingNumberFormat = {
@ -48,7 +48,7 @@ export type TwingEnvironmentOptions = {
* The persistent cache instance.
*/
cache?: TwingCache;
/**
* The default charset. Defaults to "UTF-8".
*/
@ -62,6 +62,13 @@ export type TwingEnvironmentOptions = {
timezone?: string;
};
export type TwingSynchronousEnvironmentOptions = Omit<TwingEnvironmentOptions, "cache"> & {
/**
* The persistent cache instance.
*/
cache?: TwingSynchronousCache;
};
export interface TwingEnvironment {
readonly cache: TwingCache | null;
readonly charset: string;
@ -72,7 +79,7 @@ export interface TwingEnvironment {
readonly filters: Map<string, TwingFilter>;
readonly functions: Map<string, TwingFunction>;
readonly globals: TwingContext<string, any>;
readonly loader: TwingLoader;
readonly loader: TwingLoader | TwingSynchronousLoader;
readonly sandboxPolicy: TwingSandboxSecurityPolicy;
readonly tests: Map<string, TwingTest>;
readonly timezone: string;
@ -149,6 +156,93 @@ export interface TwingEnvironment {
tokenize(source: TwingSource): TwingTokenStream;
}
export interface TwingSynchronousEnvironment {
readonly cache: TwingSynchronousCache | null;
readonly charset: string;
readonly dateFormat: string;
readonly dateIntervalFormat: string;
readonly escapingStrategyHandlers: Record<TwingEscapingStrategy, TwingEscapingStrategyHandler>;
readonly numberFormat: TwingNumberFormat;
readonly filters: Map<string, TwingSynchronousFilter>;
readonly functions: Map<string, TwingSynchronousFunction>;
readonly globals: TwingContext2;
readonly loader: TwingSynchronousLoader;
readonly sandboxPolicy: TwingSandboxSecurityPolicy;
readonly tests: Map<string, TwingSynchronousTest>;
readonly timezone: string;
/**
* Convenient method...
*
* @param extension
*/
addExtension(extension: TwingSynchronousExtension): void;
addFilter(filter: TwingSynchronousFilter): void;
addFunction(aFunction: TwingSynchronousFunction): void;
addNodeVisitor(visitor: TwingNodeVisitor): void;
addOperator(operator: TwingOperator): void;
addTagHandler(parser: TwingTagHandler): void;
addTest(test: TwingSynchronousTest): void;
/**
* Loads a template by its name.
*
* @param name The name of the template to load
* @param from The name of the template that requested the load
*
* @throws {Error} When the template cannot be found
* @throws {TwingParsingError} When an error occurred during the parsing of the source
*
* @return
*/
loadTemplate(name: string, from?: string | null): TwingSynchronousTemplate;
/**
* Converts a token list to a template.
*
* @param {TwingTokenStream} stream
* @param {TwingParserOptions} options
* *
* @throws {TwingParsingError} When the token stream is syntactically or semantically wrong
*/
parse(stream: TwingTokenStream, options?: TwingParserOptions): TwingTemplateNode;
/**
* Convenient method that renders a template from its name.
*/
render(name: string, context: Record<string, any>, options?: {
sandboxed?: boolean;
strict?: boolean;
}): string;
/**
* Convenient method that renders a template from its name and returns both the render result and its belonging source map.
*/
renderWithSourceMap(name: string, context: Record<string, any>, options?: {
sandboxed?: boolean;
strict?: boolean;
}): {
data: string;
sourceMap: RawSourceMap;
};
registerEscapingStrategy(handler: TwingEscapingStrategyHandler, name: string): void;
/**
* Tokenizes a source code.
*
* @param {TwingSource} source The source to tokenize
* @return {TwingTokenStream}
*/
tokenize(source: TwingSource): TwingTokenStream;
}
/**
* Creates an instance of {@link TwingEnvironment} backed by the passed loader.
*
@ -156,7 +250,7 @@ export interface TwingEnvironment {
* @param options
*/
export const createEnvironment = (
loader: TwingLoader,
loader: TwingLoader | TwingSynchronousLoader,
options?: TwingEnvironmentOptions
): TwingEnvironment => {
const cssEscapingStrategy = createCssEscapingStrategyHandler();
@ -172,7 +266,7 @@ export const createEnvironment = (
js: jsEscapingStrategy,
url: urlEscapingStrategy
};
const extensionSet = createExtensionSet();
const extensionSet = createExtensionSet<TwingExtension>();
extensionSet.addExtension(createCoreExtension());
@ -299,7 +393,7 @@ export const createEnvironment = (
},
renderWithSourceMap: (name, context, options) => {
const sourceMapRuntime = createSourceMapRuntime();
return environment.loadTemplate(name)
.then((template) => {
return template.render(environment, context, {
@ -335,3 +429,181 @@ export const createEnvironment = (
return environment;
};
export const createSynchronousEnvironment = (
loader: TwingSynchronousLoader,
options?: TwingSynchronousEnvironmentOptions
): TwingSynchronousEnvironment => {
const cssEscapingStrategy = createCssEscapingStrategyHandler();
const htmlEscapingStrategy = createHtmlEscapingStrategyHandler();
const htmlAttributeEscapingStrategy = createHtmlAttributeEscapingStrategyHandler();
const jsEscapingStrategy = createJsEscapingStrategyHandler();
const urlEscapingStrategy = createUrlEscapingStrategyHandler();
const escapingStrategyHandlers: Record<TwingEscapingStrategy, TwingEscapingStrategyHandler> = {
css: cssEscapingStrategy,
html: htmlEscapingStrategy,
html_attr: htmlAttributeEscapingStrategy,
js: jsEscapingStrategy,
url: urlEscapingStrategy
};
const extensionSet = createExtensionSet<TwingSynchronousExtension>();
extensionSet.addExtension(createSynchronousCoreExtension());
const cache: TwingSynchronousCache | null = options?.cache || null;
const charset = options?.charset || 'UTF-8';
const dateFormat = options?.dateFormat || 'F j, Y H:i';
const dateIntervalFormat = options?.dateIntervalFormat || '%d days';
const numberFormat: TwingNumberFormat = options?.numberFormat || {
decimalPoint: '.',
numberOfDecimals: 0,
thousandSeparator: ','
};
const sandboxPolicy = options?.sandboxPolicy || createSandboxSecurityPolicy();
const globals = new Map(Object.entries(options?.globals || {}));
let lexer: TwingLexer;
let parser: TwingParser;
const environment: TwingSynchronousEnvironment = {
get cache() {
return cache;
},
get charset() {
return charset;
},
get dateFormat() {
return dateFormat;
},
get dateIntervalFormat() {
return dateIntervalFormat;
},
get escapingStrategyHandlers() {
return escapingStrategyHandlers;
},
get filters() {
return extensionSet.filters;
},
get functions() {
return extensionSet.functions;
},
get globals() {
return globals;
},
get loader() {
return loader;
},
get numberFormat() {
return numberFormat;
},
get sandboxPolicy() {
return sandboxPolicy;
},
get tests() {
return extensionSet.tests;
},
get timezone() {
return options?.timezone || DateTimeSettings.defaultZoneName
},
addExtension: extensionSet.addExtension,
addFilter: extensionSet.addFilter,
addFunction: extensionSet.addFunction,
addNodeVisitor: extensionSet.addNodeVisitor,
addOperator: extensionSet.addOperator,
addTagHandler: extensionSet.addTagHandler,
addTest: extensionSet.addTest,
loadTemplate: (name, from = null) => {
const templateLoader = createSynchronousTemplateLoader(environment);
const template = templateLoader(name, from);
if (template === null) {
throw createTemplateLoadingError([name]);
}
return template;
},
registerEscapingStrategy: (handler, name) => {
escapingStrategyHandlers[name] = handler;
},
parse: (stream, parserOptions) => {
if (!parser) {
const visitors = extensionSet.nodeVisitors;
if (options?.autoEscapingStrategy) {
const strategy = options.autoEscapingStrategy;
visitors.unshift({
enterNode: (node) => {
return node;
},
leaveNode: (node) => {
if (node.type === "template") {
node.children.body = createAutoEscapeNode(strategy, node.children.body, node.line, node.column);
}
return node;
}
})
}
parser = createParser(
extensionSet.unaryOperators,
extensionSet.binaryOperators,
extensionSet.tagHandlers,
extensionSet.nodeVisitors,
extensionSet.filters,
extensionSet.functions,
extensionSet.tests,
parserOptions || options?.parserOptions || {
strict: true,
level: 3
}
);
}
return parser.parse(stream);
},
render: (name, data, options) => {
const template = environment.loadTemplate(name);
const context: TwingContext2 = new Map(Object.entries(data));
return template.render(environment, context, options);
},
renderWithSourceMap: (name, data, options) => {
const sourceMapRuntime = createSourceMapRuntime();
const context: TwingContext2 = new Map(Object.entries(data));
const template = environment.loadTemplate(name);
const output = template.render(environment, context, {
...options,
sourceMapRuntime
});
const {sourceMap} = sourceMapRuntime;
return {
data: output,
sourceMap
};
},
tokenize: (source: TwingSource): TwingTokenStream => {
const level = options?.parserOptions?.level || 3;
if (!lexer) {
lexer = createLexer(
level,
extensionSet.binaryOperators,
extensionSet.unaryOperators
);
}
const stream = lexer.tokenizeSource(source);
return createTokenStream(stream.toAst(), stream.source);
}
};
return environment;
};

View File

@ -1,10 +1,13 @@
import type {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from "./template";
import type {TwingContext} from "./context";
import type {TwingContext, TwingContext2} from "./context";
import type {TwingOutputBuffer} from "./output-buffer";
import type {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingEnvironment} from "./environment";
import type {TwingEnvironment, TwingSynchronousEnvironment} from "./environment";
import type {TwingNodeExecutor} from "./node-executor";
import type {TwingTemplateLoader} from "./template-loader";
import {TwingSynchronousNodeExecutor} from "./node-executor";
import {TwingSynchronousTemplate, TwingSynchronousTemplateAliases, TwingSynchronousTemplateBlockMap} from "./template";
import {TwingSynchronousTemplateLoader} from "./template-loader";
export type TwingExecutionContext = {
aliases: TwingTemplateAliases;
@ -19,3 +22,17 @@ export type TwingExecutionContext = {
template: TwingTemplate;
templateLoader: TwingTemplateLoader;
};
export type TwingSynchronousExecutionContext = {
aliases: TwingSynchronousTemplateAliases;
blocks: TwingSynchronousTemplateBlockMap;
context: TwingContext2;
environment: TwingSynchronousEnvironment;
nodeExecutor: TwingSynchronousNodeExecutor;
outputBuffer: TwingOutputBuffer;
sandboxed: boolean;
sourceMapRuntime?: TwingSourceMapRuntime;
strict: boolean;
template: TwingSynchronousTemplate;
templateLoader: TwingSynchronousTemplateLoader;
};

View File

@ -1,25 +1,22 @@
import {TwingTagHandler} from "./tag-handler";
import {TwingFilter} from "./filter";
import {TwingFunction} from "./function";
import {TwingNodeVisitor} from "./node-visitor";
import {TwingTest} from "./test";
import {TwingOperator} from "./operator";
import type {TwingExtension} from "./extension";
import type {TwingExtension, TwingSynchronousExtension} from "./extension";
export interface TwingExtensionSet {
export interface TwingExtensionSet<Extension extends TwingExtension | TwingSynchronousExtension> {
readonly binaryOperators: Array<TwingOperator>;
readonly filters: Map<string, TwingFilter>;
readonly functions: Map<string, TwingFunction>;
readonly filters: Map<string, Extension["filters"][number]>;
readonly functions: Map<string, Extension["functions"][number]>;
readonly nodeVisitors: Array<TwingNodeVisitor>;
readonly tagHandlers: Array<TwingTagHandler>;
readonly tests: Map<string, TwingTest>;
readonly tests: Map<string, Extension["tests"][number]>;
readonly unaryOperators: Array<TwingOperator>;
addExtension(extension: TwingExtension): void;
addExtension(extension: Extension): void;
addFilter(filter: TwingFilter): void;
addFilter(filter: Extension["filters"][number]): void;
addFunction(twingFunction: TwingFunction): void;
addFunction(twingFunction: Extension["functions"][number]): void;
addNodeVisitor(visitor: TwingNodeVisitor): void;
@ -27,19 +24,19 @@ export interface TwingExtensionSet {
addTagHandler(tagHandler: TwingTagHandler): void;
addTest(test: TwingTest): void;
addTest(test: Extension["tests"][number]): void;
}
export const createExtensionSet = (): TwingExtensionSet => {
export const createExtensionSet = <Extension extends TwingExtension | TwingSynchronousExtension> (): TwingExtensionSet<Extension> => {
const binaryOperators: Array<TwingOperator> = [];
const filters: Map<string, TwingFilter> = new Map();
const functions: Map<string, TwingFunction> = new Map();
const filters: Map<string, Extension["filters"][number]> = new Map();
const functions: Map<string, Extension["functions"][number]> = new Map();
const nodeVisitors: Array<TwingNodeVisitor> = [];
const tagHandlers: Array<TwingTagHandler> = [];
const tests: Map<string, TwingTest> = new Map();
const tests: Map<string, Extension["tests"][number]> = new Map();
const unaryOperators: Array<TwingOperator> = [];
const extensionSet: TwingExtensionSet = {
const extensionSet: TwingExtensionSet<Extension> = {
get binaryOperators() {
return binaryOperators;
},

View File

@ -1,8 +1,8 @@
import {TwingTagHandler} from "./tag-handler";
import {TwingNodeVisitor} from "./node-visitor";
import {TwingFilter} from "./filter";
import {TwingFunction} from "./function";
import {TwingTest} from "./test";
import {TwingFilter, TwingSynchronousFilter} from "./filter";
import {TwingFunction, TwingSynchronousFunction} from "./function";
import {TwingSynchronousTest, TwingTest} from "./test";
import {TwingOperator} from "./operator";
export interface TwingExtension {
@ -48,3 +48,47 @@ export interface TwingExtension {
*/
readonly tests: Array<TwingTest>;
}
export interface TwingSynchronousExtension {
/**
* Returns a list of filters to add to the existing list.
*
* @return Array<TwingSynchronousFilter>
*/
readonly filters: Array<TwingSynchronousFilter>;
/**
* Returns a list of functions to add to the existing list.
*
* @return Array<TwingSynchronousFunction>
*/
readonly functions: Array<TwingSynchronousFunction>;
/**
* Returns the node visitor instances to add to the existing list.
*
* @return Array<TwingNodeVisitor>
*/
readonly nodeVisitors: Array<TwingNodeVisitor>;
/**
* Returns a list of operators to add to the existing list.
*
* @return TwingOperator[]
*/
readonly operators: Array<TwingOperator>;
/**
* Returns the token parser instances to add to the existing list.
*
* @return Array<TwingTagHandler>
*/
readonly tagHandlers: Array<TwingTagHandler>;
/**
* Returns a list of tests to add to the existing list.
*
* @returns Array<TwingTest>
*/
readonly tests: Array<TwingSynchronousTest>;
}

View File

@ -1,4 +1,4 @@
import type {TwingExtension} from "../extension";
import type {TwingExtension, TwingSynchronousExtension} from "../extension";
import {createAndNode} from "../node/expression/binary/and";
import {createIsInNode} from "../node/expression/binary/is-in";
import {createIsGreaterThanNode} from "../node/expression/binary/is-greater-than";
@ -6,7 +6,7 @@ import {createIsLessThanNode} from "../node/expression/binary/is-less-than";
import {createNotNode} from "../node/expression/unary/not";
import {createNegativeNode} from "../node/expression/unary/negative";
import {createPositiveNode} from "../node/expression/unary/positive";
import {createFunction} from "../function";
import {createFunction, createSynchronousFunction} from "../function";
import {createConcatenateNode} from "../node/expression/binary/concatenate";
import {createMultiplyNode} from "../node/expression/binary/multiply";
import {createDivideNode} from "../node/expression/binary/divide";
@ -27,77 +27,178 @@ import {createIsNotInNode} from "../node/expression/binary/is-not-in";
import {createNullishCoalescingNode} from "../node/expression/nullish-coalescing";
import {TwingBaseExpressionNode} from "../node/expression";
import {createPowerNode} from "../node/expression/binary/power";
import {createTest} from "../test";
import {createSynchronousTest, createTest} from "../test";
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 {createOperator} from "../operator";
import {isEven} from "./core/tests/is-even";
import {isOdd} from "./core/tests/is-odd";
import {isSameAs} from "./core/tests/is-same-as";
import {isNull} from "./core/tests/is-null";
import {isDivisibleBy} from "./core/tests/is-divisible-by";
import {min} from "./core/functions/min";
import {max} from "./core/functions/max";
import {date} from "./core/filters/date";
import {dateModify} from "./core/filters/date-modify";
import {format} from "./core/filters/format";
import {replace} from "./core/filters/replace";
import {numberFormat} from "./core/filters/number_format";
import {abs} from "./core/filters/abs";
import {url_encode} from "./core/filters/url_encode";
import {jsonEncode} from "./core/filters/json-encode";
import {convertEncoding} from "./core/filters/convert-encoding";
import {title} from "./core/filters/title";
import {capitalize} from "./core/filters/capitalize";
import {upper} from "./core/filters/upper";
import {lower} from "./core/filters/lower";
import {striptags} from "./core/filters/striptags";
import {trim} from "./core/filters/trim";
import {nl2br} from "./core/filters/nl2br";
import {raw} from "./core/filters/raw";
import {join} from "./core/filters/join";
import {split} from "./core/filters/split";
import {sort} from "./core/filters/sort";
import {merge as mergeFilter} from "./core/filters/merge";
import {batch} from "./core/filters/batch";
import {reverse as reverseFilter} from "./core/filters/reverse";
import {length} from "./core/filters/length";
import {slice as sliceFilter} from "./core/filters/slice";
import {first as firstFilter} from "./core/filters/first";
import {last} from "./core/filters/last";
import {defaultFilter} from "./core/filters/default";
import {escape} from "./core/filters/escape";
import {round} from "./core/filters/round";
import {include} from "./core/functions/include";
import {keys} from "./core/filters/keys";
import {spaceless} from "./core/filters/spaceless";
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 {range} from "./core/functions/range";
import {constant} from "./core/functions/constant";
import {cycle} from "./core/functions/cycle";
import {random} from "./core/functions/random";
import {source} from "./core/functions/source";
import {templateFromString} from "./core/functions/template-from-string";
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 {isDefined} from "./core/tests/is-defined";
import {isConstant} from "./core/tests/is-constant";
import {createFilter, createSynchronousFilter} from "../filter";
import {createOperator, TwingOperator} from "../operator";
import {isEven, isEvenSynchronously} from "./core/tests/is-even";
import {isOdd, isOddSynchronously} from "./core/tests/is-odd";
import {isSameAs, isSameAsSynchronously} from "./core/tests/is-same-as";
import {isNull, isNullSynchronously} from "./core/tests/is-null";
import {isDivisibleBy, isDivisibleBySynchronously} from "./core/tests/is-divisible-by";
import {min, minSynchronously} from "./core/functions/min";
import {max, maxSynchronously} from "./core/functions/max";
import {date, dateFilterSynchronously} from "./core/filters/date";
import {dateModify, dateModifySynchronously} from "./core/filters/date-modify";
import {format, formatSynchronously} from "./core/filters/format";
import {replace, replaceSynchronously} from "./core/filters/replace";
import {numberFormat, numberFormatSynchronously} from "./core/filters/number_format";
import {abs, absSynchronously} from "./core/filters/abs";
import {url_encode, urlEncodeSynchronously} from "./core/filters/url_encode";
import {jsonEncode, jsonEncodeSynchronously} from "./core/filters/json-encode";
import {convertEncoding, convertEncodingSynchronously} from "./core/filters/convert-encoding";
import {title, titleSynchronously} from "./core/filters/title";
import {capitalize, capitalizeSynchronously} from "./core/filters/capitalize";
import {upper, upperSynchronously} from "./core/filters/upper";
import {lower, lowerSynchronously} from "./core/filters/lower";
import {striptags, striptagsSynchronously} from "./core/filters/striptags";
import {trim, trimSynchronously} from "./core/filters/trim";
import {nl2br, nl2brSynchronously} from "./core/filters/nl2br";
import {raw, rawSynchronously} from "./core/filters/raw";
import {join, joinSynchronously} from "./core/filters/join";
import {split, splitSynchronously} from "./core/filters/split";
import {sort, sortSynchronously} from "./core/filters/sort";
import {merge as mergeFilter, mergeSynchronously} from "./core/filters/merge";
import {batch, batchSynchronously} from "./core/filters/batch";
import {reverse as reverseFilter, reverseSynchronously} from "./core/filters/reverse";
import {length, lengthSynchronously} from "./core/filters/length";
import {slice as sliceFilter, sliceSynchronously} from "./core/filters/slice";
import {first as firstFilter, firstSynchronously} from "./core/filters/first";
import {last, lastSynchronously} from "./core/filters/last";
import {defaultFilter, defaultFilterSynchronously} from "./core/filters/default";
import {escape, escapeSynchronously} from "./core/filters/escape";
import {round, roundSynchronously} from "./core/filters/round";
import {include, includeSynchronously} from "./core/functions/include";
import {keys, keysSynchronously} from "./core/filters/keys";
import {spaceless, spacelessSynchronously} from "./core/filters/spaceless";
import {column, columnSynchronously} from "./core/filters/column";
import {filter, filterSynchronously} from "./core/filters/filter";
import {map, mapSynchronously} from "./core/filters/map";
import {reduce, reduceSynchronously} from "./core/filters/reduce";
import {range, rangeSynchronously} from "./core/functions/range";
import {constant, constantSynchronously} from "./core/functions/constant";
import {cycle, cycleSynchronously} from "./core/functions/cycle";
import {random, randomSynchronously} from "./core/functions/random";
import {source, sourceSynchronously} from "./core/functions/source";
import {templateFromString, templateFromStringSynchronously} from "./core/functions/template-from-string";
import {dump, dumpSynchronously} from "./core/functions/dump";
import {isEmpty, isEmptySynchronously} from "./core/tests/is-empty";
import {isIterable, isIterableSynchronously} from "./core/tests/is-iterable";
import {date as dateFunction, dateSynchronously} from "./core/functions/date";
import {isDefined, isDefinedSynchronously} from "./core/tests/is-defined";
import {isConstant, isConstantSynchronously} 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";
const getOperators = (): Array<TwingOperator> => {
return [
createOperator('not', "UNARY", 50, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createNotNode(operands[0], line, column);
}),
createOperator('-', "UNARY", 500, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createNegativeNode(operands[0], line, column);
}),
createOperator('+', "UNARY", 500, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createPositiveNode(operands[0], line, column);
}),
createOperator('or', "BINARY", 10, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createOrNode(operands, line, column);
}),
createOperator('and', "BINARY", 15, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createAndNode(operands, line, column);
}),
createOperator('b-or', "BINARY", 16, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createBitwiseOrNode(operands, line, column);
}),
createOperator('b-xor', "BINARY", 17, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createBitwiseXorNode(operands, line, column);
}),
createOperator('b-and', "BINARY", 18, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createBitwiseAndNode(operands, line, column);
}),
createOperator('==', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsEqualNode(operands, line, column);
}),
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);
}),
createOperator('<=', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsLessThanOrEqualToNode(operands, line, column);
}),
createOperator('>', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsGreaterThanNode(operands, line, column);
}),
createOperator('>=', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsGreaterThanOrEqualToNode(operands, line, column);
}),
createOperator('not in', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsNotInNode(operands, line, column);
}),
createOperator('in', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createIsInNode(operands, line, column);
}),
createOperator('matches', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createMatchesNode(operands, line, column);
}),
createOperator('starts with', "BINARY", 20, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createStartsWithNode(operands, line, column);
}),
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);
}),
createOperator('+', "BINARY", 30, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createAddNode(operands, line, column);
}),
createOperator('-', "BINARY", 30, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createSubtractNode(operands, line, column);
}),
createOperator('~', "BINARY", 40, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createConcatenateNode(operands, line, column);
}),
createOperator('*', "BINARY", 60, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createMultiplyNode(operands, line, column);
}),
createOperator('/', "BINARY", 60, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createDivideNode(operands, line, column);
}),
createOperator('//', "BINARY", 60, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createDivideAndFloorNode(operands, line, column);
}),
createOperator('%', "BINARY", 60, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createModuloNode(operands, line, column);
}),
createOperator('**', "BINARY", 200, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createPowerNode(operands, line, column);
}, "RIGHT"),
createOperator('??', "BINARY", 300, (operands: [TwingBaseExpressionNode, TwingBaseExpressionNode], line: number, column: number) => {
return createNullishCoalescingNode(operands, line, column);
}, "RIGHT")
];
};
export const createCoreExtension = (): TwingExtension => {
return {
get filters() {
const escapeFilters = ['escape', 'e'].map((name) => {
return createFilter(name, escape, [
return createFilter(name, (escape), [
{
name: 'strategy',
defaultValue: null
@ -131,7 +232,7 @@ export const createCoreExtension = (): TwingExtension => {
name: 'name'
}
]),
createFilter('convert_encoding', convertEncoding, [
createFilter('convert_encoding', (convertEncoding), [
{
name: 'to'
},
@ -531,3 +632,345 @@ export const createCoreExtension = (): TwingExtension => {
}
};
};
export const createSynchronousCoreExtension = (): TwingSynchronousExtension => {
return {
get filters() {
const escapeFilters = ['escape', 'e'].map((name) => {
return createSynchronousFilter(name, escapeSynchronously, [
{
name: 'strategy',
defaultValue: null
},
{
name: 'charset',
defaultValue: null
}
]);
});
return [
...escapeFilters,
createSynchronousFilter('abs', absSynchronously, []),
createSynchronousFilter('batch', batchSynchronously, [
{
name: 'size'
},
{
name: 'fill',
defaultValue: null
},
{
name: 'preserve_keys',
defaultValue: true
}
]),
createSynchronousFilter('capitalize', capitalizeSynchronously, []),
createSynchronousFilter('column', columnSynchronously, [
{
name: 'name'
}
]),
createSynchronousFilter('convert_encoding', convertEncodingSynchronously, [
{
name: 'to'
},
{
name: 'from'
}
]),
createSynchronousFilter('date', dateFilterSynchronously, [
{
name: 'format',
defaultValue: null
},
{
name: 'timezone',
defaultValue: null
}
]),
createSynchronousFilter('date_modify', dateModifySynchronously, [
{
name: 'modifier'
}
]),
createSynchronousFilter('default', defaultFilterSynchronously, [
{
name: 'default',
defaultValue: null
}
]),
createSynchronousFilter('filter', filterSynchronously, [
{
name: 'array'
},
{
name: 'arrow',
defaultValue: null
}
]),
createSynchronousFilter('first', firstSynchronously, []),
createSynchronousFilter('format', formatSynchronously, [], {
is_variadic: true
}),
createSynchronousFilter('join', joinSynchronously, [
{
name: 'glue',
defaultValue: ''
},
{
name: 'and',
defaultValue: null
}
]),
createSynchronousFilter('json_encode', jsonEncodeSynchronously, [
{
name: 'options',
defaultValue: null
}
]),
createSynchronousFilter('keys', keysSynchronously, []),
createSynchronousFilter('last', lastSynchronously, []),
createSynchronousFilter('length', lengthSynchronously, []),
createSynchronousFilter('lower', lowerSynchronously, []),
createSynchronousFilter('map', mapSynchronously, [
{
name: 'arrow'
}
]),
createSynchronousFilter('merge', mergeSynchronously, [
{
name: 'source'
}
]),
createSynchronousFilter('nl2br', nl2brSynchronously, []),
createSynchronousFilter('number_format', numberFormatSynchronously, [
{
name: 'decimal',
defaultValue: null
},
{
name: 'decimal_point',
defaultValue: null
},
{
name: 'thousand_sep',
defaultValue: null
}
]),
createSynchronousFilter('raw', rawSynchronously, []),
createSynchronousFilter('reduce', reduceSynchronously, [
{
name: 'arrow'
},
{
name: 'initial',
defaultValue: null
}
]),
createSynchronousFilter('replace', replaceSynchronously, [
{
name: 'from'
}
]),
createSynchronousFilter('reverse', reverseSynchronously, [
{
name: 'preserve_keys',
defaultValue: false
}
]),
createSynchronousFilter('round', roundSynchronously, [
{
name: 'precision',
defaultValue: 0
},
{
name: 'method',
defaultValue: 'common'
}
]),
createSynchronousFilter('slice', sliceSynchronously, [
{
name: 'start'
},
{
name: 'length',
defaultValue: null
},
{
name: 'preserve_keys',
defaultValue: false
}
]),
createSynchronousFilter('sort', sortSynchronously, [{
name: 'arrow',
defaultValue: null
}]),
createSynchronousFilter('spaceless', spacelessSynchronously, []),
createSynchronousFilter('split', splitSynchronously, [
{
name: 'delimiter'
},
{
name: 'limit',
defaultValue: null
}
]),
createSynchronousFilter('striptags', striptagsSynchronously, [
{
name: 'allowable_tags',
defaultValue: ''
}
]),
createSynchronousFilter('title', titleSynchronously, []),
createSynchronousFilter('trim', trimSynchronously, [
{
name: 'character_mask',
defaultValue: null
},
{
name: 'side',
defaultValue: 'both'
}
]),
createSynchronousFilter('upper', upperSynchronously, []),
createSynchronousFilter('url_encode', urlEncodeSynchronously, []),
];
},
get functions() {
return [
createSynchronousFunction('constant', constantSynchronously, [
{name: 'name'},
{name: 'object', defaultValue: null}
]),
createSynchronousFunction('cycle', cycleSynchronously, [
{
name: 'values'
},
{
name: 'position'
}
]),
createSynchronousFunction('date', dateSynchronously, [
{
name: 'date',
defaultValue: null
},
{
name: 'timezone',
defaultValue: null
}
]),
createSynchronousFunction('dump', dumpSynchronously, [], {
is_variadic: true
}),
createSynchronousFunction('include', includeSynchronously, [
{
name: 'template'
},
{
name: 'variables',
defaultValue: {}
},
{
name: 'with_context',
defaultValue: true
},
{
name: 'ignore_missing',
defaultValue: false
},
{
name: 'sandboxed',
defaultValue: false
}
]),
createSynchronousFunction('max', maxSynchronously, [], {
is_variadic: true
}),
createSynchronousFunction('min', minSynchronously, [], {
is_variadic: true
}),
createSynchronousFunction('random', randomSynchronously, [
{
name: 'values',
defaultValue: null
},
{
name: 'max',
defaultValue: null
}
]),
createSynchronousFunction('range', rangeSynchronously, [
{
name: 'low'
},
{
name: 'high'
},
{
name: 'step',
defaultValue: 1
}
]),
createSynchronousFunction('source', sourceSynchronously, [
{
name: 'name'
},
{
name: 'ignore_missing',
defaultValue: false
}
]),
createSynchronousFunction('template_from_string', templateFromStringSynchronously, [
{
name: 'template'
},
{
name: 'name',
defaultValue: null
}
])
];
},
get nodeVisitors() {
return [];
},
get operators() {
return getOperators();
},
get tagHandlers() {
return [];
},
get tests() {
return [
createSynchronousTest('constant', isConstantSynchronously, [
{
name: 'constant'
},
{
name: 'object',
defaultValue: null
}
]),
createSynchronousTest('divisible by', isDivisibleBySynchronously, [
{
name: 'divisor'
}
]),
createSynchronousTest('defined', isDefinedSynchronously, []),
createSynchronousTest('empty', isEmptySynchronously, []),
createSynchronousTest('even', isEvenSynchronously, []),
createSynchronousTest('iterable', isIterableSynchronously, []),
createSynchronousTest('none', isNullSynchronously, []),
createSynchronousTest('null', isNullSynchronously, []),
createSynchronousTest('odd', isOddSynchronously, []),
createSynchronousTest('same as', isSameAsSynchronously, [
{
name: 'comparand'
}
]),
];
}
};
};

View File

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

View File

@ -1,6 +1,6 @@
import {chunk} from "../../../helpers/chunk";
import {chunk, chunkSynchronously} from "../../../helpers/chunk";
import {fillMap} from "../../../helpers/fill-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Batches item.
@ -35,3 +35,25 @@ export const batch: TwingCallable<[
return chunks;
});
};
export const batchSynchronously: TwingSynchronousCallable<[
items: Array<any>,
size: number,
fill: any,
preserveKeys: boolean
], Array<Map<any, any>>> = (_executionContext, items, size, fill, preserveKeys) => {
if ((items === null) || (items === undefined)) {
return [];
}
const chunks = chunkSynchronously(items, size, preserveKeys);
if (fill !== null && chunks.length) {
const last = chunks.length - 1;
const lastChunk: Map<any, any> = chunks[last];
fillMap(lastChunk, size, fill);
}
return chunks;
};

View File

@ -1,5 +1,6 @@
import type {TwingMarkup} from "../../../markup";
import type {TwingCallable} from "../../../callable-wrapper";
import {TwingSynchronousCallable} from "../../../callable-wrapper";
const words: (value: string) => string = require('capitalize');
@ -19,3 +20,13 @@ export const capitalize: TwingCallable<[
return Promise.resolve(words(string.toString()));
};
export const capitalizeSynchronously: TwingSynchronousCallable<[
string: string | TwingMarkup
], string> = (_executionContext, string) => {
if ((string === null) || (string === undefined) || string === '') {
return string;
}
return words(string.toString());
};

View File

@ -1,7 +1,7 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Return the values from a single column in the input array.
@ -34,3 +34,27 @@ export const column: TwingCallable = (_executionContext, thing: any, columnKey:
return Promise.resolve(result);
};
export const columnSynchronously: TwingSynchronousCallable = (_executionContext, thing: any, columnKey: any): Array<any> => {
let map: Map<any, any>;
if (!isTraversable(thing) || isPlainObject(thing)) {
throw new Error(`The column filter only works with arrays or "Traversable", got "${typeof thing}" as first argument.`);
} else {
map = iteratorToMap(thing);
}
const result: Array<any> = [];
for (const value of map.values()) {
const valueAsMap: Map<any, any> = iteratorToMap(value);
for (const [key, value] of valueAsMap) {
if (key === columnKey) {
result.push(value);
}
}
}
return result;
};

View File

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

View File

@ -1,6 +1,6 @@
import {DateTime} from "luxon";
import {createDate} from "../functions/date";
import {TwingCallable} from "../../../callable-wrapper";
import {createDateTime, createDateTimeSynchronously} from "../functions/date";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Returns a new date object modified.
@ -23,21 +23,46 @@ export const dateModify: TwingCallable = (
const {environment} = executionContext;
const {timezone: defaultTimezone} = environment;
return createDate(defaultTimezone, date, null)
return createDateTime(defaultTimezone, date, null)
.then((dateTime) => {
let regExp = new RegExp(/(\+|-)([0-9])(.*)/);
let parts = regExp.exec(modifier)!;
let regExp = new RegExp(/(\+|-)([0-9])(.*)/);
let parts = regExp.exec(modifier)!;
let operator: string = parts[1];
let operand: number = Number.parseInt(parts[2]);
let unit: string = parts[3].trim();
let operator: string = parts[1];
let operand: number = Number.parseInt(parts[2]);
let unit: string = parts[3].trim();
let duration: any = {};
let duration: any = {};
duration[unit] = operator === '-' ? -operand : operand;
duration[unit] = operator === '-' ? -operand : operand;
dateTime = dateTime.plus(duration);
dateTime = dateTime.plus(duration);
return dateTime;
return dateTime;
});
};
export const dateModifySynchronously: TwingSynchronousCallable = (
executionContext,
date: Date | DateTime | string,
modifier: string
): DateTime => {
const {environment} = executionContext;
const {timezone: defaultTimezone} = environment;
let dateTime = createDateTimeSynchronously(defaultTimezone, date, null);
let regExp = new RegExp(/(\+|-)([0-9])(.*)/);
let parts = regExp.exec(modifier)!;
let operator: string = parts[1];
let operand: number = Number.parseInt(parts[2]);
let unit: string = parts[3].trim();
let duration: any = {};
duration[unit] = operator === '-' ? -operand : operand;
dateTime = dateTime.plus(duration);
return dateTime;
};

View File

@ -1,8 +1,8 @@
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 {TwingCallable} from "../../../callable-wrapper";
import {date as createDate, dateSynchronously as createDateSynchronously} from "../functions/date";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Converts a date to the given format.
@ -44,3 +44,29 @@ export const date: TwingCallable = (
return Promise.resolve(formatDateTime(date, format));
});
};
export const dateFilterSynchronously: TwingSynchronousCallable = (
executionContext,
date: DateTime | Duration | string,
format: string | null,
timezone: string | null | false
): string => {
const {environment} = executionContext;
const {dateFormat, dateIntervalFormat} = environment;
const durationOrDateTime = createDateSynchronously(executionContext, date, timezone);
if (durationOrDateTime instanceof Duration) {
if (format === null) {
format = dateIntervalFormat;
}
return formatDuration(durationOrDateTime, format);
}
if (format === null) {
format = dateFormat;
}
return formatDateTime(durationOrDateTime, format);
};

View File

@ -1,5 +1,6 @@
import {isEmpty} from "../tests/is-empty";
import {isEmpty, isEmptySynchronously} from "../tests/is-empty";
import type {TwingCallable} from "../../../callable-wrapper";
import {TwingSynchronousCallable} from "../../../callable-wrapper";
export const defaultFilter: TwingCallable<[
value: any,
@ -15,3 +16,14 @@ export const defaultFilter: TwingCallable<[
}
});
};
export const defaultFilterSynchronously: TwingSynchronousCallable<[
value: any,
defaultValue: any | null
]> = (executionContext, value, defaultValue) => {
if (isEmptySynchronously(executionContext, value)) {
return defaultValue;
}
return value;
};

View File

@ -1,6 +1,7 @@
import {createMarkup, TwingMarkup} from "../../../markup";
import type {TwingCallable} from "../../../callable-wrapper";
import {escapeValue} from "../../../helpers/escape-value";
import {escapeValue, escapeValueSynchronously} from "../../../helpers/escape-value";
import {TwingSynchronousCallable} from "../../../callable-wrapper";
export const escape: TwingCallable<[
value: string | TwingMarkup | null,
@ -26,3 +27,27 @@ export const escape: TwingCallable<[
return value;
});
};
export const escapeSynchronously: TwingSynchronousCallable<[
value: string | TwingMarkup | null,
strategy: string | null
], string | boolean | TwingMarkup | null> = (
executionContext,
value,
strategy
) => {
if (strategy === null) {
strategy = "html";
}
const {template, environment} = executionContext;
// todo: probably we need to use traceable method
const escapedValue = escapeValueSynchronously(template, environment, value, strategy, environment.charset);
if (typeof escapedValue === "string") {
return createMarkup(escapedValue, environment.charset);
}
return escapedValue;
};

View File

@ -1,5 +1,5 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
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();
@ -14,3 +14,17 @@ export const filter: TwingCallable = async (_executionContext, map: any, callbac
return Promise.resolve(result);
};
export const filterSynchronously: TwingSynchronousCallable = (_executionContext, map: any, callback: (...args: Array<any>) => boolean): Map<any, any> => {
const result: Map<any, any> = new Map();
map = iteratorToMap(map);
for (const [key, value] of map) {
if (callback(value)) {
result.set(key, value);
}
}
return result;
};

View File

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

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const sprintf = require('locutus/php/strings/sprintf');
@ -7,3 +7,9 @@ export const format: TwingCallable = (_executionContext, ...args: any[]): Promis
return arg.toString();
})));
};
export const formatSynchronously: TwingSynchronousCallable = (_executionContext, ...args: any[]): string => {
return sprintf(...args.map((arg) => {
return arg.toString();
}));
};

View File

@ -1,6 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Joins the values to a string.
@ -64,3 +64,42 @@ export const join: TwingCallable<[
return Promise.resolve(_do());
};
export const joinSynchronously: TwingSynchronousCallable<[
value: any,
glue: string,
and: string | null
], string> = (_executionContext, value, glue, and) => {
if ((value == null) || (value === undefined)) {
return '';
}
if (isTraversable(value)) {
value = iteratorToArray(value);
// this is ugly, but we have to ensure that each element of the array is rendered as PHP would render it
const safeValue = value.map((item: any) => {
if (typeof item === 'boolean') {
return (item === true) ? '1' : ''
}
if (Array.isArray(item)) {
return 'Array';
}
return item;
});
if (and === null || and === glue) {
return safeValue.join(glue);
}
if (safeValue.length === 1) {
return safeValue[0];
}
return safeValue.slice(0, -1).join(glue) + and + safeValue[safeValue.length - 1];
}
return '';
};

View File

@ -3,7 +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";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
function isPureArray(map: Map<any, any>): boolean {
let result: boolean = true;
@ -59,3 +59,41 @@ export const jsonEncode: TwingCallable = (_executionContext, value: any): Promis
return Promise.resolve(JSON.stringify(_sanitize(value)));
}
export const jsonEncodeSynchronously: TwingSynchronousCallable = (_executionContext, value: any): string => {
const _sanitize = (value: any): any => {
if (isTraversable(value) || isPlainObject(value)) {
value = iteratorToMap(value);
}
if (value instanceof Map) {
let sanitizedValue: any;
if (isPureArray(value)) {
value = iteratorToArray(value);
sanitizedValue = [];
for (const key in value) {
sanitizedValue.push(_sanitize(value[key]));
}
}
else {
value = iteratorToHash(value);
sanitizedValue = {};
for (let key in value) {
sanitizedValue[key] = _sanitize(value[key]);
}
}
value = sanitizedValue;
}
return value;
};
return JSON.stringify(_sanitize(value));
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Returns the length of a thing.
@ -24,3 +24,21 @@ export const length: TwingCallable = (_executionContext,thing: any): Promise<num
return Promise.resolve(length);
};
export const lengthSynchronously: TwingSynchronousCallable = (_executionContext,thing: any): number => {
let length: number;
if ((thing === null) || (thing === undefined)) {
length = 0;
} else if (thing.length !== undefined) {
length = thing.length;
} else if (thing.size !== undefined) {
length = thing.size;
} else if (thing.toString && (typeof thing.toString === 'function')) {
length = thing.toString().length;
} else {
length = 1;
}
return length;
};

View File

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

View File

@ -1,5 +1,5 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
export const map: TwingCallable<[
map: any,
@ -15,3 +15,18 @@ export const map: TwingCallable<[
return Promise.resolve(result);
};
export const mapSynchronously: TwingSynchronousCallable<[
map: any,
callback: (...args: Array<any>) => any
], Map<any, any>> = (_executionContext, map, callback) => {
const result: Map<any, any> = new Map();
map = iteratorToMap(map);
for (const [key, value] of map) {
result.set(key, callback(value, key));
}
return result;
};

View File

@ -1,7 +1,7 @@
import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Merges an array with another one.
@ -34,3 +34,19 @@ export const merge: TwingCallable = (_executionContext, iterable1: any, source:
return Promise.resolve(mergeIterables(iteratorToMap(iterable1), iteratorToMap(source)));
};
export const mergeSynchronously: TwingSynchronousCallable = (_executionContext, iterable1: any, source: any): Map<any, any> => {
const isIterable1NullOrUndefined = (iterable1 === null) || (iterable1 === undefined);
if (isIterable1NullOrUndefined || (!isTraversable(iterable1) && (typeof iterable1 !== 'object'))) {
throw new Error(`The merge filter only works on arrays or "Traversable", got "${!isIterable1NullOrUndefined ? typeof iterable1 : iterable1}".`);
}
const isSourceNullOrUndefined = (source === null) || (source === undefined);
if (isSourceNullOrUndefined || (!isTraversable(source) && (typeof source !== 'object'))) {
throw new Error(`The merge filter only accepts arrays or "Traversable" as source, got "${!isSourceNullOrUndefined ? typeof source : source}".`);
}
return mergeIterables(iteratorToMap(iterable1), iteratorToMap(source));
};

View File

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

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpNumberFormat = require('locutus/php/strings/number_format');
@ -40,3 +40,28 @@ export const numberFormat: TwingCallable = (
return Promise.resolve(phpNumberFormat(number, numberOfDecimals, decimalPoint, thousandSeparator));
};
export const numberFormatSynchronously: TwingSynchronousCallable = (
executionContext,
number: any,
numberOfDecimals: number | null,
decimalPoint: string | null,
thousandSeparator: string | null
): string => {
const {environment} = executionContext;
const {numberFormat} = environment;
if (numberOfDecimals === null) {
numberOfDecimals = numberFormat.numberOfDecimals;
}
if (decimalPoint === null) {
decimalPoint = numberFormat.decimalPoint;
}
if (thousandSeparator === null) {
thousandSeparator = numberFormat.thousandSeparator;
}
return phpNumberFormat(number, numberOfDecimals, decimalPoint, thousandSeparator);
};

View File

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

View File

@ -1,5 +1,5 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
export const reduce: TwingCallable = (_executionContext, map: any, callback: (accumulator: any, currentValue: any) => any, initial: any): Promise<string> => {
map = iteratorToMap(map);
@ -10,3 +10,13 @@ export const reduce: TwingCallable = (_executionContext, map: any, callback: (ac
return (async () => callback(await previousValue, currentValue))();
}, initial));
};
export const reduceSynchronously: TwingSynchronousCallable = (_executionContext, map: any, callback: (accumulator: any, currentValue: any) => any, initial: any): Promise<string> => {
map = iteratorToMap(map);
const values: any[] = [...map.values()];
return values.reduce((previousValue: any, currentValue: any): any => {
return (() => callback(previousValue, currentValue))();
}, initial);
};

View File

@ -1,6 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToHash} from "../../../helpers/iterator-to-hash";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpStrtr = require('locutus/php/strings/strtr');
@ -14,18 +14,19 @@ const phpStrtr = require('locutus/php/strings/strtr');
*/
export const replace: TwingCallable = (_executionContext,value: string | null, from: any): Promise<string> => {
const _do = (): string => {
if (isTraversable(from)) {
from = iteratorToHash(from);
} else if (typeof from !== 'object') {
throw new Error(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
}
if (isTraversable(from)) {
from = iteratorToHash(from);
}
else if (typeof from !== 'object') {
throw new Error(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
}
if (value === null) {
value = '';
}
if (value === null) {
value = '';
}
return phpStrtr(value, from);
};
return phpStrtr(value, from);
};
try {
return Promise.resolve(_do());
@ -33,3 +34,18 @@ export const replace: TwingCallable = (_executionContext,value: string | null, f
return Promise.reject(error);
}
};
export const replaceSynchronously: TwingSynchronousCallable = (_executionContext, value: string | null, from: any): string => {
if (isTraversable(from)) {
from = iteratorToHash(from);
}
else if (typeof from !== 'object') {
throw new Error(`The "replace" filter expects an hash or "Iterable" as replace values, got "${typeof from}".`);
}
if (value === null) {
value = '';
}
return phpStrtr(value, from);
};

View File

@ -1,6 +1,6 @@
import {reverse as reverseHelper} from "../../../helpers/reverse";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const esrever = require('esrever');
@ -19,3 +19,11 @@ export const reverse: TwingCallable = (_executionContext, item: any, preserveKey
return Promise.resolve(reverseHelper(iteratorToMap(item as Map<any, any>), preserveKeys));
}
};
export const reverseSynchronously: TwingSynchronousCallable = (_executionContext, item: any, preserveKeys: boolean): string | Map<any, any> => {
if (typeof item === 'string') {
return esrever.reverse(item);
} else {
return reverseHelper(iteratorToMap(item as Map<any, any>), preserveKeys);
}
};

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpRound = require('locutus/php/math/round');
const phpCeil = require('locutus/php/math/ceil');
@ -15,23 +15,24 @@ const phpFloor = require('locutus/php/math/floor');
*/
export const round: TwingCallable = (_executionContext, value: any, precision: number, method: string): Promise<number> => {
const _do = (): number => {
if (method === 'common') {
return phpRound(value, precision);
}
if (method === 'common') {
return phpRound(value, precision);
}
if (method !== 'ceil' && method !== 'floor') {
throw new Error('The round filter only supports the "common", "ceil", and "floor" methods.');
}
if (method !== 'ceil' && method !== 'floor') {
throw new Error('The round filter only supports the "common", "ceil", and "floor" methods.');
}
const intermediateValue = value * Math.pow(10, precision);
const intermediateDivider = Math.pow(10, precision);
const intermediateValue = value * Math.pow(10, precision);
const intermediateDivider = Math.pow(10, precision);
if (method === 'ceil') {
return phpCeil(intermediateValue) / intermediateDivider;
} else {
return phpFloor(intermediateValue) / intermediateDivider;
}
};
if (method === 'ceil') {
return phpCeil(intermediateValue) / intermediateDivider;
}
else {
return phpFloor(intermediateValue) / intermediateDivider;
}
};
try {
const result = _do();
@ -41,3 +42,23 @@ export const round: TwingCallable = (_executionContext, value: any, precision: n
return Promise.reject(error);
}
};
export const roundSynchronously: TwingSynchronousCallable = (_executionContext, value: any, precision: number, method: string): number => {
if (method === 'common') {
return phpRound(value, precision);
}
if (method !== 'ceil' && method !== 'floor') {
throw new Error('The round filter only supports the "common", "ceil", and "floor" methods.');
}
const intermediateValue = value * Math.pow(10, precision);
const intermediateDivider = Math.pow(10, precision);
if (method === 'ceil') {
return phpCeil(intermediateValue) / intermediateDivider;
}
else {
return phpFloor(intermediateValue) / intermediateDivider;
}
};

View File

@ -1,7 +1,7 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {sliceMap} from "../../../helpers/slice-map";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Slices a variable.
@ -38,3 +38,28 @@ export const slice: TwingCallable<[
return Promise.resolve(item.substr(start, length));
};
export const sliceSynchronously: TwingSynchronousCallable<[
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);
if (length === null) {
length = iterableItem.size - start;
}
return sliceMap(iterableItem, start, length, preserveKeys);
}
item = '' + (item ? item : '');
if (length === null) {
length = item.length - start;
}
return item.substr(start, length);
};

View File

@ -1,7 +1,7 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {asort} from "../../../helpers/asort";
import {TwingCallable} from "../../../callable-wrapper";
import {asort, asortSynchronously} from "../../../helpers/asort";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Sorts an iterable.
@ -26,3 +26,18 @@ export const sort: TwingCallable<[
return map;
};
export const sortSynchronously: TwingSynchronousCallable<[
iterable: any,
arrow: ((a: any, b: any) => -1 | 0 | 1) | null
], Map<any, any>> = (_executionContext, iterable, arrow)=> {
if (!isTraversable(iterable)) {
throw new Error(`The sort filter only works with iterables, got "${typeof iterable}".`);
}
const map = iteratorToMap(iterable);
asortSynchronously(map, arrow || undefined);
return map;
};

View File

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

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const explode = require('locutus/php/strings/explode');
@ -27,28 +27,52 @@ const explode = require('locutus/php/strings/explode');
*/
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);
}
if (delimiter) {
return !limit ? explode(delimiter, value) : explode(delimiter, value, limit);
}
if (!limit || limit <= 1) {
return value.match(/.{1,1}/ug)!;
}
if (!limit || limit <= 1) {
return value.match(/.{1,1}/ug)!;
}
let length = value.length;
let length = value.length;
if (length < limit) {
return [value];
}
if (length < limit) {
return [value];
}
let r = [];
let r = [];
for (let i = 0; i < length; i += limit) {
r.push(value.substr(i, limit));
}
for (let i = 0; i < length; i += limit) {
r.push(value.substr(i, limit));
}
return r;
};
return r;
};
return Promise.resolve(_do());
};
export const splitSynchronously: TwingSynchronousCallable = (_executionContext, value: string, delimiter: string, limit: number | null): Array<string> => {
if (delimiter) {
return !limit ? explode(delimiter, value) : explode(delimiter, value, limit);
}
if (!limit || limit <= 1) {
return value.match(/.{1,1}/ug)!;
}
let length = value.length;
if (length < limit) {
return [value];
}
let r = [];
for (let i = 0; i < length; i += limit) {
r.push(value.substr(i, limit));
}
return r;
};

View File

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

View File

@ -1,5 +1,5 @@
import type {TwingMarkup} from "../../../markup";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpUcwords = require('locutus/php/strings/ucwords');
@ -18,3 +18,11 @@ export const title: TwingCallable<[
return Promise.resolve(result);
};
export const titleSynchronously: TwingSynchronousCallable<[
string: string | TwingMarkup
], string> = (_executionContext, string) => {
const result: string = phpUcwords(string.toString().toLowerCase());
return result;
};

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpTrim = require('locutus/php/strings/trim');
const phpLeftTrim = require('locutus/php/strings/ltrim');
@ -13,21 +13,21 @@ const phpRightTrim = require('locutus/php/strings/rtrim');
*/
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";
}
if (characterMask === null) {
characterMask = " \t\n\r\0\x0B";
}
switch (side) {
case 'both':
return phpTrim(string, characterMask);
case 'left':
return phpLeftTrim(string, characterMask);
case 'right':
return phpRightTrim(string, characterMask);
default:
throw new Error('Trimming side must be "left", "right" or "both".');
}
};
switch (side) {
case 'both':
return phpTrim(string, characterMask);
case 'left':
return phpLeftTrim(string, characterMask);
case 'right':
return phpRightTrim(string, characterMask);
default:
throw new Error('Trimming side must be "left", "right" or "both".');
}
};
try {
return Promise.resolve(_do());
@ -35,3 +35,21 @@ export const trim: TwingCallable = (_executionContext, string: string, character
return Promise.reject(error);
}
};
export const trimSynchronously: TwingSynchronousCallable = (_executionContext, string: string, characterMask: string | null, side: string): string => {
if (characterMask === null) {
characterMask = " \t\n\r\0\x0B";
}
switch (side) {
case 'both':
return phpTrim(string, characterMask);
case 'left':
return phpLeftTrim(string, characterMask);
case 'right':
return phpRightTrim(string, characterMask);
default:
throw new Error('Trimming side must be "left", "right" or "both".');
}
};

View File

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

View File

@ -1,6 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToHash} from "../../../helpers/iterator-to-hash";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const phpHttpBuildQuery = require('locutus/php/url/http_build_query');
@ -24,3 +24,17 @@ export const url_encode: TwingCallable = (_executionContext, url: string | {}):
return Promise.resolve(encodeURIComponent(url));
}
export const urlEncodeSynchronously: TwingSynchronousCallable = (_executionContext, url: string | {}): string => {
if (typeof url !== 'string') {
if (isTraversable(url)) {
url = iteratorToHash(url);
}
const builtUrl: string = phpHttpBuildQuery(url, '', '&');
return builtUrl.replace(/\+/g, '%20');
}
return encodeURIComponent(url);
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import {DateTime, Duration} from "luxon";
import {modifyDate} from "../../../helpers/modify-date";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Converts an input to a DateTime instance.
@ -17,74 +17,74 @@ import {TwingCallable} from "../../../callable-wrapper";
*
* @returns {Promise<DateTime | Duration>}
*/
export const createDate = (
export const createDateTime = (
defaultTimezone: string,
input: Date | DateTime | number | string | null,
timezone: string | null | false
): Promise<DateTime> => {
const _do = (): DateTime => {
let result: DateTime;
let result: DateTime;
if (input === null) {
if (input === null) {
result = DateTime.local();
}
else if (typeof input === 'number') {
result = DateTime.fromMillis(input * 1000);
}
else if (typeof input === 'string') {
if (input === 'now') {
result = DateTime.local();
}
else if (typeof input === 'number') {
result = DateTime.fromMillis(input * 1000);
}
else if (typeof input === 'string') {
if (input === 'now') {
result = DateTime.local();
}
else {
result = DateTime.fromISO(input, {
else {
result = DateTime.fromISO(input, {
setZone: true
});
if (!result.isValid) {
result = DateTime.fromRFC2822(input, {
setZone: true
});
if (!result.isValid) {
result = DateTime.fromRFC2822(input, {
setZone: true
});
}
if (!result.isValid) {
result = DateTime.fromSQL(input, {
setZone: true
});
}
if (!result.isValid && /^-{0,1}\d+$/.test(input)) {
result = DateTime.fromMillis(Number.parseInt(input) * 1000, {
setZone: true
});
}
if (!result.isValid) {
result = modifyDate(input);
}
}
}
else if (input instanceof DateTime) {
result = input;
}
else {
result = DateTime.fromJSDate(input);
}
if (!result || !result.isValid) {
throw new Error(`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);
if (!result.isValid) {
result = DateTime.fromSQL(input, {
setZone: true
});
}
if (!result.isValid && /^-{0,1}\d+$/.test(input)) {
result = DateTime.fromMillis(Number.parseInt(input) * 1000, {
setZone: true
});
}
if (!result.isValid) {
result = modifyDate(input);
}
}
}
else if (input instanceof DateTime) {
result = input;
}
else {
result = DateTime.fromJSDate(input);
}
if (!result || !result.isValid) {
throw new Error(`Failed to parse date "${input}".`);
}
// now let's apply timezone
// determine the timezone
if (timezone !== false) {
if (timezone === null) {
timezone = defaultTimezone;
}
return result;
result = result.setZone(timezone);
}
return result;
};
try {
@ -103,5 +103,86 @@ export const date: TwingCallable = (
return Promise.resolve(date);
}
return createDate(executionContext.environment.timezone, date, timezone);
return createDateTime(executionContext.environment.timezone, date, timezone);
}
export const createDateTimeSynchronously = (
defaultTimezone: string,
input: Date | DateTime | number | string | null,
timezone: string | null | false
): DateTime => {
let result: DateTime;
if (input === null) {
result = DateTime.local();
}
else if (typeof input === 'number') {
result = DateTime.fromMillis(input * 1000);
}
else if (typeof input === 'string') {
if (input === 'now') {
result = DateTime.local();
}
else {
result = DateTime.fromISO(input, {
setZone: true
});
if (!result.isValid) {
result = DateTime.fromRFC2822(input, {
setZone: true
});
}
if (!result.isValid) {
result = DateTime.fromSQL(input, {
setZone: true
});
}
if (!result.isValid && /^-{0,1}\d+$/.test(input)) {
result = DateTime.fromMillis(Number.parseInt(input) * 1000, {
setZone: true
});
}
if (!result.isValid) {
result = modifyDate(input);
}
}
}
else if (input instanceof DateTime) {
result = input;
}
else {
result = DateTime.fromJSDate(input);
}
if (!result || !result.isValid) {
throw new Error(`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;
}
export const dateSynchronously: TwingSynchronousCallable = (
executionContext,
date: Date | DateTime | Duration | number | string | null,
timezone: string | null | false
): DateTime | Duration => {
if (date instanceof Duration) {
return date;
}
return createDateTimeSynchronously(executionContext.environment.timezone, date, timezone);
}

View File

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

View File

@ -1,11 +1,11 @@
import {iteratorToMap} from "../../../helpers/iterator-to-map";
import {mergeIterables} from "../../../helpers/merge-iterables";
import {isTraversable} from "../../../helpers/is-traversable";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {createContext} from "../../../context";
import {createContext, TwingContext2} from "../../../context";
import {createMarkup, TwingMarkup} from "../../../markup";
import type {TwingTemplate} from "../../../template";
import type {TwingCallable} from "../../../callable-wrapper";
import type {TwingSynchronousTemplate, TwingTemplate} from "../../../template";
import type {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
import {iterableToMap, iteratorToMap} from "../../../helpers/iterator-to-map";
import {mergeIterables} from "../../../helpers/merge-iterables";
/**
* Renders a template.
@ -78,7 +78,7 @@ export const include: TwingCallable<[
if (template) {
return template.execute(
environment,
createContext(variables),
createContext(iterableToMap(variables)),
new Map(),
outputBuffer,
{
@ -100,3 +100,85 @@ export const include: TwingCallable<[
return createMarkup(result, environment.charset);
});
}
export const includeSynchronously: TwingSynchronousCallable<[
templates: string | TwingSynchronousTemplate | null | Array<string | TwingSynchronousTemplate | null>,
variables: TwingContext2,
withContext: boolean,
ignoreMissing: boolean,
sandboxed: boolean
]> = (
executionContext,
templates,
variables,
withContext,
ignoreMissing,
sandboxed
): TwingMarkup => {
const {
template,
environment,
templateLoader,
context,
nodeExecutor,
outputBuffer,
sourceMapRuntime,
strict
} = executionContext;
if (!isPlainObject(variables) && !isTraversable(variables)) {
const isVariablesNullOrUndefined = variables === null || variables === undefined;
throw new Error(`Variables passed to the "include" function or tag must be iterable, got "${!isVariablesNullOrUndefined ? typeof variables : variables}".`);
}
variables = iteratorToMap(variables);
if (withContext) {
variables = new Map([
...context.entries(),
...variables.entries()
]);
}
if (!Array.isArray(templates)) {
templates = [templates];
}
const resolveTemplate = (templates: Array<string | TwingSynchronousTemplate | null>): TwingSynchronousTemplate | null => {
try {
return template.loadTemplate(executionContext, templates);
} catch (error) {
if (!ignoreMissing) {
throw error;
}
else {
return null;
}
}
};
const resolvedTemplate = resolveTemplate(templates);
outputBuffer.start();
if (resolvedTemplate) {
resolvedTemplate.execute(
environment,
variables,
new Map(),
outputBuffer,
{
nodeExecutor,
sandboxed,
sourceMapRuntime: sourceMapRuntime || undefined,
strict,
templateLoader
}
);
}
const result = outputBuffer.getAndClean();
return createMarkup(result, environment.charset);
}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import {iconv} from "../../../helpers/iconv";
import {isTraversable} from "../../../helpers/is-traversable";
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const runes = require('runes');
const mt_rand = require('locutus/php/math/mt_rand');
@ -86,3 +86,68 @@ export const random: TwingCallable = (executionContext, values: any | null, max:
return Promise.resolve(_do());
}
export const randomSynchronously: TwingSynchronousCallable = (executionContext, values: any | null, max: number | null): any => {
const {environment} = executionContext;
const {charset} = environment;
if (values === null) {
return max === null ? mt_rand() : mt_rand(0, max);
}
if (typeof values === 'number') {
let min: number;
if (max === null) {
if (values < 0) {
max = 0;
min = values;
}
else {
max = values;
min = 0;
}
}
else {
min = values;
}
return mt_rand(min, max);
}
if (typeof values === 'string') {
values = Buffer.from(values);
}
if (Buffer.isBuffer(values)) {
if (values.toString() === '') {
return '';
}
if (charset !== 'UTF-8') {
values = iconv(charset, 'UTF-8', values);
}
// unicode split
values = runes(values.toString());
if (charset !== 'UTF-8') {
values = values.map((value: string) => {
return iconv('UTF-8', charset, Buffer.from(value)).toString();
});
}
}
else if (isTraversable(values)) {
values = iteratorToArray(values);
}
if (!Array.isArray(values)) {
return values;
}
if (values.length < 1) {
throw new Error('The random function cannot pick from an empty array.');
}
return values[array_rand(values, 1)];
}

View File

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

View File

@ -1,5 +1,6 @@
import {createTemplateLoadingError} from "../../../error/loader";
import type {TwingCallable} from "../../../callable-wrapper";
import type {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
import {TwingSynchronousTemplate} from "../../../template";
/**
* Returns a template content without rendering it.
@ -8,7 +9,7 @@ import type {TwingCallable} from "../../../callable-wrapper";
* @param name The template name
* @param ignoreMissing Whether to ignore missing templates or not
*
* @return {Promise<string>} The template source
* @return The template source
*/
export const source: TwingCallable<[
name: string,
@ -28,3 +29,25 @@ export const source: TwingCallable<[
return template?.source.code || null;
});
};
export const sourceSynchronously: TwingSynchronousCallable<[
name: string,
ignoreMissing: boolean
], string | null> = (executionContext, name, ignoreMissing) => {
const {template} = executionContext;
let loadedTemplate: TwingSynchronousTemplate | null;
try {
loadedTemplate = template.loadTemplate(executionContext, name)
}
catch (error) {
loadedTemplate = null;
}
if (!ignoreMissing && (loadedTemplate === null)) {
throw createTemplateLoadingError([name]);
}
return loadedTemplate?.source.code || null;
};

View File

@ -1,22 +1,10 @@
import {createTemplate, TwingTemplate} from "../../../template";
import {TwingCallable} from "../../../callable-wrapper";
import {createSynchronousTemplate, createTemplate, TwingSynchronousTemplate, TwingTemplate} from "../../../template";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
import * as createHash from "create-hash";
import {createSource} from "../../../source";
import {TwingExecutionContext, TwingSynchronousExecutionContext} from "../../../execution-context";
/**
* Loads a template from a string.
*
* <pre>
* {{ include(template_from_string("Hello {{ name }}")) }}
* </pre>
*
* @param executionContext
* @param code
* @param name An optional name for the template to be used in error messages
*
* @returns {Promise<TwingTemplate>}
*/
export const templateFromString: TwingCallable = (executionContext, code: string, name: string | null): Promise<TwingTemplate> => {
const getAST = (executionContext: TwingExecutionContext | TwingSynchronousExecutionContext, code: string, name: string | null) => {
const {environment} = executionContext;
const hash: string = createHash("sha256").update(code).digest("hex").toString();
@ -28,8 +16,28 @@ export const templateFromString: TwingCallable = (executionContext, code: string
name = `__string_template__${hash}`;
}
const ast = environment.parse(environment.tokenize(createSource(name, code)));
const template = createTemplate(ast);
return environment.parse(environment.tokenize(createSource(name, code)));
};
return Promise.resolve(template);
/**
* Loads a template from a string.
*
* <pre>
* {{ include(template_from_string("Hello {{ name }}")) }}
* </pre>
*
* @param executionContext
* @param code
* @param name An optional name for the template to be used in error messages
*/
export const templateFromString: TwingCallable = (executionContext, code: string, name: string | null): Promise<TwingTemplate> => {
const ast = getAST(executionContext, code, name);
return Promise.resolve(createTemplate(ast));
}
export const templateFromStringSynchronously: TwingSynchronousCallable = (executionContext, code: string, name: string | null): TwingSynchronousTemplate => {
const ast = getAST(executionContext, code, name);
return createSynchronousTemplate(ast);
}

View File

@ -1,5 +1,6 @@
import {getConstant} from "../../../helpers/get-constant";
import type {TwingCallable} from "../../../callable-wrapper";
import {TwingSynchronousCallable} from "../../../callable-wrapper";
export const isConstant: TwingCallable<[
comparand: any,
@ -13,3 +14,16 @@ export const isConstant: TwingCallable<[
) => {
return Promise.resolve(comparand === getConstant(executionContext.context, constant, object));
};
export const isConstantSynchronously: TwingSynchronousCallable<[
comparand: any,
constant: any,
object: any | null
], boolean> = (
executionContext,
comparand,
constant,
object
) => {
return comparand === getConstant(executionContext.context, constant, object);
};

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
export const isDefined: TwingCallable<[
value: any
@ -8,3 +8,12 @@ export const isDefined: TwingCallable<[
) => {
return Promise.resolve(!!value);
};
export const isDefinedSynchronously: TwingSynchronousCallable<[
value: any
], boolean> = (
_executionContext,
value
) => {
return !!value;
};

View File

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

View File

@ -1,5 +1,5 @@
import {iteratorToArray} from "../../../helpers/iterator-to-array";
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
const isPlainObject = require('is-plain-object');
@ -46,3 +46,32 @@ export const isEmpty: TwingCallable<[value: any], boolean> = (executionContext,
return Promise.resolve(value === false);
};
export const isEmptySynchronously: TwingSynchronousCallable<[value: any], boolean> = (executionContext, value) => {
if (value === null || value === undefined) {
return true;
}
if (typeof value === 'string') {
return value.length < 1;
}
if (typeof value[Symbol.iterator] === 'function') {
return value[Symbol.iterator]().next().done === true;
}
if (isPlainObject(value)) {
if (value.hasOwnProperty('toString') && typeof value.toString === 'function') {
return isEmptySynchronously(executionContext, value.toString());
}
else {
return iteratorToArray(value).length < 1;
}
}
if (typeof value === 'object' && value.toString && typeof value.toString === 'function') {
return isEmptySynchronously(executionContext, value.toString());
}
return value === false;
};

View File

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

View File

@ -1,4 +1,4 @@
import {TwingCallable} from "../../../callable-wrapper";
import {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
/**
* Checks if a variable is traversable.
@ -52,3 +52,38 @@ export const isIterable: TwingCallable<[value: any], boolean> = (_executionConte
return Promise.resolve(_do());
};
export const isIterableSynchronously: TwingSynchronousCallable<[value: any], boolean> = (_executionContext, value) => {
/*
Prevent `(null)[Symbol.iterator]`/`(undefined)[Symbol.iterator]` error,
and return `false` instead.
Note that `value` should only be `undefined` if it's been explicitly
set to that (e.g., in the JavaScript that provided the calling template
with the context). Values that are simply "not defined" will either have
been coerced to `null` or thrown a "does not exist" runtime error before
this function is called (depending on whether `strict_variables` is enabled).
This *does* mean that an explicitly `undefined` value will return `false`
instead of throwing an error if `strict_variables` is enabled, which is
probably unexpected behavior, but short of some major refactoring to allow
an environmental check here, the alternative is to have `undefined`
throw an error even when `strict_variables` is disabled, and that unexpected
behavior seems worse.
*/
if (value === null || value === undefined) {
return false;
}
// for Twig, a string is not traversable
if (typeof value === 'string') {
return false;
}
if (typeof value[Symbol.iterator] === 'function') {
return true;
}
// in PHP objects are not iterable so we have to ensure that the test reflects that
return false;
};

View File

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

View File

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

View File

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

View File

@ -1,7 +1,11 @@
import {
TwingCallableWrapperOptions,
TwingCallableArgument,
TwingCallable, TwingCallableWrapper, createCallableWrapper
TwingCallable,
TwingCallableWrapper,
createCallableWrapper,
TwingSynchronousCallableWrapper,
createSynchronousCallableWrapper, TwingSynchronousCallable
} from "./callable-wrapper";
export type TwingFilterOptions = TwingCallableWrapperOptions;
@ -10,9 +14,13 @@ export interface TwingFilter extends TwingCallableWrapper {
}
export const createFilter = <Callable extends TwingCallable>(
export interface TwingSynchronousFilter extends TwingSynchronousCallableWrapper {
}
export const createFilter = (
name: string,
callable: Callable,
callable: TwingCallable,
acceptedArguments: TwingCallableArgument[],
options: TwingFilterOptions = {}
): TwingFilter => {
@ -24,3 +32,18 @@ export const createFilter = <Callable extends TwingCallable>(
return filter;
};
export const createSynchronousFilter = (
name: string,
callable: TwingSynchronousCallable,
acceptedArguments: TwingCallableArgument[],
options: TwingFilterOptions = {}
): TwingSynchronousFilter => {
const callableWrapper = createSynchronousCallableWrapper(name, callable, acceptedArguments, options);
const filter: TwingSynchronousFilter = {
...callableWrapper
};
return filter;
};

View File

@ -1,16 +1,24 @@
import {
TwingCallableWrapperOptions,
TwingCallableArgument,
TwingCallable, TwingCallableWrapper, createCallableWrapper
TwingCallable,
TwingCallableWrapper,
createCallableWrapper,
TwingSynchronousCallableWrapper,
TwingSynchronousCallable, createSynchronousCallableWrapper
} from "./callable-wrapper";
export interface TwingFunction extends TwingCallableWrapper {
}
export const createFunction = <Callable extends TwingCallable>(
export interface TwingSynchronousFunction extends TwingSynchronousCallableWrapper {
}
export const createFunction = (
name: string,
callable: Callable,
callable: TwingCallable,
acceptedArguments: TwingCallableArgument[],
options: TwingCallableWrapperOptions = {}
): TwingFunction => {
@ -18,3 +26,14 @@ export const createFunction = <Callable extends TwingCallable>(
return callableWrapper;
};
export const createSynchronousFunction = (
name: string,
callable: TwingSynchronousCallable,
acceptedArguments: TwingCallableArgument[],
options: TwingCallableWrapperOptions = {}
): TwingSynchronousFunction => {
const callableWrapper = createSynchronousCallableWrapper(name, callable, acceptedArguments, options);
return callableWrapper;
};

View File

@ -11,7 +11,7 @@ export const asort = async (map: Map<any, any>, compareFunction?: (a: any, b: an
const sortedMap = new Map();
const keys: Array<any> = ([] as Array<any>).fill(null, 0, map.size);
const values = [...map.values()];
let sortedValues: Array<any>;
if (compareFunction) {
@ -20,7 +20,38 @@ export const asort = async (map: Map<any, any>, compareFunction?: (a: any, b: an
else {
sortedValues = values.sort();
}
for (const [key, value] of map) {
const index = sortedValues.indexOf(value);
keys[index] = key;
}
for (const key of keys) {
sortedMap.set(key, map.get(key));
}
map.clear();
for (const [key, value] of sortedMap) {
map.set(key, value);
}
}
export const asortSynchronously = (map: Map<any, any>, compareFunction?: (a: any, b: any) => -1 | 0 | 1) => {
const sortedMap = new Map();
const keys: Array<any> = ([] as Array<any>).fill(null, 0, map.size);
const values = [...map.values()];
let sortedValues: Array<any>;
if (compareFunction) {
sortedValues = values.sort(compareFunction);
}
else {
sortedValues = values.sort();
}
for (const [key, value] of map) {
const index = sortedValues.indexOf(value);

View File

@ -1,4 +1,4 @@
import {iterate} from "./iterate";
import {iterate, iterateSynchronously} from "./iterate";
/**
* Split an hash into chunks.
@ -34,3 +34,36 @@ export async function chunk(hash: any, size: number, preserveKeys: boolean): Pro
return result;
}
/**
* Split an hash into chunks, synchronously.
*
* @param {*} hash
* @param {number} size
* @param {boolean} preserveKeys
*/
export function chunkSynchronously(hash: any, size: number, preserveKeys: boolean): Array<Map<any, any>> {
let result: Array<Map<any, any>> = [];
let count = 0;
let currentMap: Map<any, any> | null;
iterateSynchronously(hash, (key: any, value: any) => {
if (!currentMap) {
currentMap = new Map();
result.push(currentMap);
}
currentMap.set(preserveKeys ? key : count, value);
count++;
if (count >= size) {
count = 0;
currentMap = null;
}
});
return result;
}

View File

@ -1,7 +1,7 @@
import {isAMarkup, TwingMarkup} from "../markup";
import {TwingEnvironment} from "../environment";
import {TwingEnvironment, TwingSynchronousEnvironment} from "../environment";
import {TwingEscapingStrategy} from "../escaping-strategy";
import {TwingTemplate} from "../template";
import {TwingSynchronousTemplate, TwingTemplate} from "../template";
export const escapeValue = (
template: TwingTemplate,
@ -35,3 +35,36 @@ export const escapeValue = (
return Promise.resolve(result);
}
export const escapeValueSynchronously = (
template: TwingTemplate | TwingSynchronousTemplate,
environment: TwingEnvironment | TwingSynchronousEnvironment,
value: string | boolean | TwingMarkup | null | undefined,
strategy: TwingEscapingStrategy | string,
charset: string | null
): string | boolean | TwingMarkup => {
if (typeof value === "boolean") {
return value;
}
if (isAMarkup(value)) {
return value;
}
let result: string;
if ((value === null) || (value === undefined)) {
result = '';
}
else {
const strategyHandler = environment.escapingStrategyHandlers[strategy];
if (strategyHandler === undefined) {
throw new Error(`Invalid escaping strategy "${strategy}" (valid ones: ${Object.keys(environment.escapingStrategyHandlers).sort().join(', ')}).`);
}
result = strategyHandler(value.toString(), charset || environment.charset, template.name);
}
return result;
}

View File

@ -4,7 +4,7 @@ import {isPlainObject} from "./is-plain-object";
import {get} from "./get";
import type {TwingAttributeAccessorCallType} from "../node/expression/attribute-accessor";
import {isBoolean, isFloat} from "./php";
import type {TwingEnvironment} from "../environment";
import type {TwingEnvironment, TwingSynchronousEnvironment} from "../environment";
const isObject = require('isobject');
@ -37,7 +37,7 @@ export const getAttribute = (
strict: boolean
): Promise<any> => {
const {sandboxPolicy} = environment;
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !strict : shouldIgnoreStrictCheck;
const _do = (): any => {
@ -75,10 +75,10 @@ export const getAttribute = (
}
}
if ((type === "array")
|| (isAMapLike(object))
if ((type === "array")
|| (isAMapLike(object))
|| (Array.isArray(object))
|| (object === null)
|| (object === null)
|| (typeof object !== 'object')
) {
if (shouldTestExistence) {
@ -88,7 +88,7 @@ export const getAttribute = (
if (shouldIgnoreStrictCheck) {
return;
}
if (object === null) {
// object is null
if (type === "array") {
@ -265,3 +265,236 @@ export const getAttribute = (
return Promise.reject(e);
}
};
export const getAttributeSynchronously = (
environment: TwingSynchronousEnvironment,
object: any,
attribute: any,
methodArguments: Map<any, any>,
type: TwingAttributeAccessorCallType,
shouldTestExistence: boolean,
shouldIgnoreStrictCheck: boolean | null,
sandboxed: boolean,
strict: boolean
): any => {
const {sandboxPolicy} = environment;
shouldIgnoreStrictCheck = (shouldIgnoreStrictCheck === null) ? !strict : shouldIgnoreStrictCheck;
let message: string;
// ANY_CALL or ARRAY_CALL
if (type !== "method") {
let arrayItem;
if (isBoolean(attribute)) {
arrayItem = attribute ? 1 : 0;
}
else if (isFloat(attribute)) {
arrayItem = parseInt(attribute);
}
else {
arrayItem = attribute;
}
if (object) {
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;
}
if (type !== "array" && sandboxed) {
sandboxPolicy.checkPropertyAllowed(object, attribute);
}
return get(object, arrayItem);
}
}
if ((type === "array")
|| (isAMapLike(object))
|| (Array.isArray(object))
|| (object === null)
|| (typeof object !== 'object')
) {
if (shouldTestExistence) {
return false;
}
if (shouldIgnoreStrictCheck) {
return;
}
if (object === null) {
// object is null
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}").`;
}
throw new Error(message);
}
}
// ANY_CALL or METHOD_CALL
if ((object === null) || (!isObject(object)) || (isAMapLike(object))) {
if (shouldTestExistence) {
return false;
}
if (shouldIgnoreStrictCheck) {
return;
}
if (object === null) {
message = `Impossible to invoke a method ("${attribute}") on a null variable.`;
}
else if (isAMapLike(object) || Array.isArray(object)) {
message = `Impossible to invoke a method ("${attribute}") on an array.`;
}
else {
message = `Impossible to invoke a method ("${attribute}") on a ${typeof object} variable ("${object}").`;
}
throw new Error(message);
}
// object property
if (type !== "method") {
if (Reflect.has(object, attribute) && (typeof object[attribute] !== 'function')) {
if (shouldTestExistence) {
return true;
}
if (sandboxed) {
sandboxPolicy.checkPropertyAllowed(object, attribute);
}
return get(object, attribute);
}
}
// object method
// precedence: getXxx() > isXxx() > hasXxx()
let methods: Array<string> = [];
for (let property of examineObject(object)) {
let candidate = object[property];
if (typeof candidate === 'function') {
methods.push(property);
}
}
methods.sort();
let lcMethods: Array<string> = methods.map((method) => {
return method.toLowerCase();
});
let candidates = new Map();
for (let i = 0; i < methods.length; i++) {
let method: string = methods[i];
let lcName: string = lcMethods[i];
candidates.set(method, method);
candidates.set(lcName, method);
let name: string = '';
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) {
name = method.substr(2);
lcName = lcName.substr(2);
}
else if (lcName[0] === 'h' && lcName.indexOf('has') === 0) {
name = method.substr(3);
lcName = lcName.substr(3);
if (lcMethods.includes('is' + lcName)) {
continue;
}
}
else {
continue;
}
// skip get() and is() methods (in which case, name is empty)
if (name.length > 0) {
if (!candidates.has(name)) {
candidates.set(name, method);
}
if (!candidates.has(lcName)) {
candidates.set(lcName, method);
}
}
}
let itemAsString: string = attribute as string;
let method: string;
let lcItem: string;
if (candidates.has(attribute)) {
method = candidates.get(attribute);
}
else if (candidates.has(lcItem = itemAsString.toLowerCase())) {
method = candidates.get(lcItem);
}
else {
if (shouldTestExistence) {
return false;
}
if (shouldIgnoreStrictCheck) {
return;
}
throw new Error(`Neither the property "${attribute}" nor one of the methods ${attribute}()" or "get${attribute}()"/"is${attribute}()"/"has${attribute}()" exist and have public access in class "${object.constructor.name}".`);
}
if (shouldTestExistence) {
return true;
}
if (sandboxed) {
sandboxPolicy.checkMethodAllowed(object, method);
}
return get(object, method).apply(object, [...methodArguments.values()]);
};

View File

@ -8,9 +8,9 @@
*
* @returns {any}
*/
import {TwingContext} from "../context";
import {TwingContext, TwingContext2} from "../context";
export function getConstant(context: TwingContext<any, any>, name: string, object: any | null): any {
export function getConstant(context: TwingContext<any, any> | TwingContext2, name: string, object: any | null): any {
if (object) {
return object[name];
} else {

View File

@ -1,4 +1,4 @@
import type {TwingContext} from "../context";
import type {TwingContext, TwingContext2} from "../context";
export const getContextValue = (
charset: string,
@ -46,3 +46,59 @@ export const getContextValue = (
return Promise.resolve(result);
};
export const getContextValueSynchronously = (
charset: string,
templateName: string,
isStrictVariables: boolean,
context: TwingContext2,
globals: TwingContext2,
name: string,
isAlwaysDefined: boolean,
shouldIgnoreStrictCheck: boolean,
shouldTestExistence: boolean
): any => {
const specialNames = new Map<string, any>([
['_self', templateName],
['_context', context],
['_charset', charset]
]);
const isSpecial = () => {
return specialNames.has(name);
};
let result: any;
if (shouldTestExistence) {
if (isSpecial()) {
result = true;
} else {
result = context.has(name) || globals.has(name);
}
} else if (isSpecial()) {
result = specialNames.get(name);
} else if (isAlwaysDefined) {
result = context.get(name);
if (result === undefined) {
result = globals.get(name);
}
} else {
if (shouldIgnoreStrictCheck || !isStrictVariables) {
result = context.has(name) ? context.get(name) : (globals.has(name) ? globals.get(name) : null);
} else {
result = context.get(name);
if (result === undefined) {
result = globals.get(name);
}
if (result === undefined) {
throw new Error(`Variable "${name}" does not exist.`);
}
}
}
return result;
};

View File

@ -1,4 +1,4 @@
import type {TwingFilter} from "../filter";
import type {TwingFilter, TwingSynchronousFilter} from "../filter";
/**
* Get a filter by name.
@ -7,10 +7,10 @@ import type {TwingFilter} from "../filter";
*
* @return {TwingFilter|false} A TwingFilter instance or false if the filter does not exist
*/
export const getFilter = (
filters: Map<string, TwingFilter>,
export const getFilter = <Filter extends TwingFilter | TwingSynchronousFilter>(
filters: Map<string, Filter>,
name: string
): TwingFilter | null => {
): Filter | null => {
const result = filters.get(name);
if (result) {

View File

@ -1,4 +1,5 @@
import type {TwingFunction} from "../function";
import {TwingSynchronousFunction} from "../function";
/**
* Get a function by name.
@ -6,10 +7,10 @@ import type {TwingFunction} from "../function";
* @param {string} name function name
* @returns {TwingFunction} A TwingFunction instance or null if the function does not exist
*/
export const getFunction = (
functions: Map<string, TwingFunction>,
export const getFunction = <Function extends TwingFunction | TwingSynchronousFunction>(
functions: Map<string, Function>,
name: string
): TwingFunction | null => {
): Function | null => {
const result = functions.get(name);
if (result) {

View File

@ -1,4 +1,5 @@
import type {TwingTest} from "../test";
import {TwingSynchronousTest} from "../test";
/**
* Gets a test by name.
@ -6,10 +7,10 @@ import type {TwingTest} from "../test";
* @param {string} name The test name
* @returns {TwingTest} A MyTest instance or null if the test does not exist
*/
export const getTest = (
tests: Map<string, TwingTest>,
export const getTest = <Test extends TwingTest | TwingSynchronousTest>(
tests: Map<string, Test>,
name: string
): TwingTest | null => {
): Test | null => {
const result = tests.get(name);
if (result) {

View File

@ -31,13 +31,6 @@ export function isIn(value: number | string | object | TwingMarkup, compare: str
break;
}
}
} else if (typeof compare === 'object') {
for (const key in compare) {
if (compareHelper((compare as any)[key], value)) {
result = true;
break;
}
}
}
return result;

View File

@ -1,5 +1,5 @@
const _isPlainObject = require('is-plain-object');
export function isPlainObject(thing: any) {
export function isPlainObject(thing: any): thing is Record<string, any> {
return _isPlainObject(thing);
}

View File

@ -1,3 +1,5 @@
import {isPlainObject} from "./is-plain-object";
/**
* Check that an object is traversable in the sense of PHP,
* i.e. implements PHP Traversable interface
@ -6,6 +8,10 @@
* @returns {boolean}
*/
export function isTraversable(value: any) {
if (isPlainObject(value)) {
return true;
}
if ((value !== null) && (value !== undefined)) {
if (typeof value === 'string') {
return false;

View File

@ -1,4 +1,5 @@
export type IterateCallback = (key: any, value: any) => Promise<void>;
export type SynchronousIterateCallback = (key: any, value: any) => void;
/**
* Executes the provided function once for each element of an iterable.
@ -35,3 +36,34 @@ export const iterate = async (iterable: any, callback: IterateCallback): Promise
}
}
}
export const iterateSynchronously = (iterable: any, callback: SynchronousIterateCallback): void => {
// todo: maybe useless when we pass records instead of TwingContext
if (iterable.entries) {
for (const [key, value] of iterable.entries()) {
callback(key, value);
}
}
else if (typeof iterable[Symbol.iterator] === 'function') {
let i: number = 0;
for (let value of iterable) {
callback(i++, value);
}
}
// todo: check why this is not covered anymore
// else if (typeof iterable['next'] === 'function') {
// let i: number = 0;
// let next: any;
//
// while ((next = iterable.next()) && !next.done) {
// callback(i++, next.value)
// }
// }
else {
for (const key in iterable) {
callback(key, iterable[key]);
}
}
}

View File

@ -1,5 +1,5 @@
export function iteratorToHash(value: any) {
let result: any;
export function iteratorToHash(value: any): Record<any, any> {
let result: Record<any, any>;
if (value.entries) {
result = {};

View File

@ -1,7 +1,7 @@
import {TwingContext} from "../context";
import {iteratorToMap} from "./iterator-to-map";
export type MapLike<K, V> = Map<K, V> | TwingContext<K, V>;
export type MapLike<K extends string, V> = Map<K, V> | TwingContext<K, V>;
export function isAMapLike(candidate: any): candidate is MapLike<any, any> {
return candidate !== null &&
@ -30,6 +30,23 @@ export const every = async (
return true;
};
export const everySynchronously = (
iterable: MapLike<any, any> | Array<any>,
comparator: (value: any, key: any) => boolean
): boolean => {
if (Array.isArray(iterable)) {
iterable = iteratorToMap(iterable);
}
for (const [key, value] of iterable) {
if (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>
@ -45,4 +62,21 @@ export const some = async (
}
return false;
};
};
export const someSynchronously = (
iterable: MapLike<any, any> | Array<any>,
comparator: (value: any, key: any) => boolean
): boolean => {
if (Array.isArray(iterable)) {
iterable = iteratorToMap(iterable);
}
for (const [key, value] of iterable) {
if (comparator(value, key) === true) {
return true;
}
}
return false;
};

View File

@ -17,3 +17,20 @@ export function getTraceableMethod<M extends (...args: Array<any>) => Promise<an
});
}) as typeof method;
}
export function getSynchronousTraceableMethod<M extends (...args: Array<any>) => any>(method: M, location: {
line: number;
column: number;
}, templateSource: TwingSource): M {
return ((...args: Array<any>) => {
try {
return method(...args);
} catch (error) {
if (!isATwingError(error as Error)) {
throw createRuntimeError((error as Error).message, location, templateSource, (error as Error));
}
throw error;
}
}) as typeof method;
}

View File

@ -44,3 +44,46 @@ export interface TwingLoader {
*/
exists: (name: string, from: string | null) => Promise<boolean>;
}
export interface TwingSynchronousLoader {
/**
* Returns the source for a given template logical name.
*
* @param {string} name The template logical name
* @param {TwingSource} from The source that initiated the template loading
*/
getSource: (name: string, from: string | null) => TwingSource | null;
/**
* Resolve a template FQN from its name and the name of the template that initiated the loading.
*
* @param {string} name The name of the template to load
* @param {TwingSource} from The source that initiated the template loading
*
* @returns The cache key
*/
resolve: (name: string, from: string | null) => string | null;
/**
* Returns true if the template is still fresh.
*
* @param {string} name The template name
* @param {number} time Timestamp of the last modification time of the cached template
* @param {TwingSource} from The source that initiated the template loading
*
* @returns true if the template is fresh, false otherwise; null if it does not exist
*
* @throws TwingErrorLoader When name is not found
*/
isFresh: (name: string, time: number, from: string | null) => boolean | null;
/**
* Check if we have the source code of a template, given its name.
*
* @param {string} name The name of the template to check if we can load
* @param {TwingSource} from The source that initiated the template loading
*
* @returns If the template source code is handled by this loader or not
*/
exists: (name: string, from: string | null) => boolean;
}

View File

@ -1,10 +1,14 @@
import type {TwingLoader} from "../loader";
import type {TwingLoader, TwingSynchronousLoader} from "../loader";
import {createSource} from "../source";
export interface TwingArrayLoader extends TwingLoader {
setTemplate(name: string, template: string): void;
}
export interface TwingSynchronousArrayLoader extends TwingSynchronousLoader {
setTemplate(name: string, template: string): void;
}
export const createArrayLoader = (
templates: Record<string, string>
): TwingArrayLoader => {
@ -42,3 +46,36 @@ export const createArrayLoader = (
return loader;
};
export const createSynchronousArrayLoader = (
templates: Record<string, string>
): TwingSynchronousArrayLoader => {
const loader: TwingSynchronousArrayLoader = {
setTemplate: (name, template) => {
templates[name] = template;
},
getSource: (name, from) => {
if (loader.exists(name, from)) {
return createSource(name, templates[name]);
}
return null;
},
exists(name) {
return templates[name] !== undefined;
},
resolve: (name, from) => {
if (loader.exists(name, from)) {
return name;
}
return null;
},
isFresh: () => {
return true;
}
};
return loader;
};

View File

@ -1,4 +1,4 @@
import type {TwingLoader} from "../loader";
import type {TwingLoader, TwingSynchronousLoader} from "../loader";
import type {TwingSource} from "../source";
import {join, isAbsolute, dirname, normalize} from "path";
import {createSource} from "../source";
@ -13,9 +13,9 @@ export interface TwingFilesystemLoaderFilesystemStats {
export interface TwingFilesystemLoaderFilesystem {
stat(
path: string,
path: string,
callback: (
error: Error | null,
error: Error | null,
stats: TwingFilesystemLoaderFilesystemStats | null
) => void
): void;
@ -23,6 +23,12 @@ export interface TwingFilesystemLoaderFilesystem {
readFile(path: string, callback: (error: Error | null, data: Buffer | null) => void): void;
}
export interface TwingSynchronousFilesystemLoaderFilesystem {
statSync(path: string): TwingFilesystemLoaderFilesystemStats | null;
readFileSync(path: string): Buffer | null;
}
export interface TwingFilesystemLoader extends TwingLoader {
/**
* Adds a path where templates are stored.
@ -41,17 +47,36 @@ export interface TwingFilesystemLoader extends TwingLoader {
prependPath(path: string, namespace?: string | null): void;
}
export interface TwingSynchronousFilesystemLoader extends TwingSynchronousLoader {
/**
* Adds a path where templates are stored.
*
* @param path A path where to look for templates
* @param namespace A path namespace
*/
addPath(path: string, namespace?: string | null): void;
/**
* Prepends a path where templates are stored.
*
* @param path A path where to look for templates
* @param namespace A path namespace
*/
prependPath(path: string, namespace?: string | null): void;
}
export const createFilesystemLoader = (
filesystem: TwingFilesystemLoaderFilesystem
): TwingFilesystemLoader => {
const namespacedPaths: Map<string | null, Array<string>> = new Map();
const stat = (path: string): Promise<TwingFilesystemLoaderFilesystemStats | null> => {
return new Promise((resolve) => {
filesystem.stat(path, (error, stats) => {
if (error) {
resolve(null);
} else {
}
else {
resolve(stats);
}
});
@ -73,13 +98,14 @@ export const createFilesystemLoader = (
// * if not found yet, resolve from "from"
const resolve = (name: string, from: string | null): Promise<string | null> => {
name = normalize(from ? resolvePathFromSource(name, from) : name);
const findTemplateInPath = async (path: string): Promise<string | null> => {
const stats = await stat(path);
if (stats && stats.isFile()) {
return Promise.resolve(path);
} else {
}
else {
return Promise.resolve(null);
}
};
@ -89,10 +115,11 @@ export const createFilesystemLoader = (
.then((templatePath) => {
if (templatePath) {
return templatePath;
} else {
}
else {
// then, search for the template from its namespaced name
const [namespace, shortname] = parseName(name);
const paths = namespacedPaths.get(namespace) || ['.'];
const findTemplateInPathAtIndex = async (index: number): Promise<string | null> => {
@ -102,11 +129,13 @@ export const createFilesystemLoader = (
if (templatePath) {
return Promise.resolve(templatePath);
} else {
}
else {
// let's continue searching
return findTemplateInPathAtIndex(index + 1);
}
} else {
}
else {
return Promise.resolve(null);
}
};
@ -151,7 +180,8 @@ export const createFilesystemLoader = (
if (!namespacePaths) {
namespacedPaths.set(namespace!, [path]);
} else {
}
else {
namespacePaths.unshift(path);
}
}
@ -170,12 +200,14 @@ export const createFilesystemLoader = (
.then((path) => {
if (path === null) {
return null;
} else {
}
else {
return new Promise<TwingSource | null>((resolve, reject) => {
filesystem.readFile(path, (error, data) => {
if (error) {
reject(error);
} else {
}
else {
resolve(createSource(path, data!.toString()));
}
});
@ -188,7 +220,8 @@ export const createFilesystemLoader = (
.then((path) => {
if (path === null) {
return true;
} else {
}
else {
return stat(path)
.then((stats) => {
return stats!.mtime.getTime() <= time
@ -199,3 +232,154 @@ export const createFilesystemLoader = (
prependPath
};
};
export const createSynchronousFilesystemLoader = (
filesystem: TwingSynchronousFilesystemLoaderFilesystem
): TwingSynchronousFilesystemLoader => {
const namespacedPaths: Map<string | null, Array<string>> = new Map();
const stat = (path: string): TwingFilesystemLoaderFilesystemStats | null => {
try {
return filesystem.statSync(path);
} catch (error) {
return null;
}
};
const resolvePathFromSource = (name: string, from: string): string => {
if (name && !isAbsolute(name) && name.startsWith('.')) {
name = join(dirname(from), name);
}
return name;
};
// todo: rework
// * if no slash, resolve from "from"
// * if contains a slash, extract namespace and check if registered:
// * if so, resolve from namespace
// * if not found yet, resolve from "from"
const resolve = (name: string, from: string | null): string | null => {
name = normalize(from ? resolvePathFromSource(name, from) : name);
const findTemplateInPath = (path: string): string | null => {
const stats = stat(path);
if (stats && stats.isFile()) {
return path;
}
else {
return null;
}
};
// first search for the template from its fully qualified name
const templatePath = findTemplateInPath(name);
if (templatePath) {
return templatePath;
}
else {
// then, search for the template from its namespaced name
const [namespace, shortname] = parseName(name);
const paths = namespacedPaths.get(namespace) || ['.'];
const findTemplateInPathAtIndex = (index: number): string | null => {
if (index < paths.length) {
const path = paths[index];
const templatePath = findTemplateInPath(join(path, shortname));
if (templatePath) {
return templatePath;
}
else {
// let's continue searching
return findTemplateInPathAtIndex(index + 1);
}
}
else {
return null;
}
};
return findTemplateInPathAtIndex(0);
}
};
const parseName = (name: string): [string | null, string] => {
// only non-relative names can be namespace references
if (name[0] !== '.') {
const position = name.indexOf('/');
if (position >= 0) {
const namespace = name.substring(0, position);
const shortname = name.substring(position + 1);
return [namespace, shortname];
}
}
return [null, name];
};
const addPath: TwingFilesystemLoader["addPath"] = (path, namespace = null) => {
let namespacePaths = namespacedPaths.get(namespace);
if (!namespacePaths) {
namespacePaths = [];
namespacedPaths.set(namespace, namespacePaths);
}
namespacePaths.push(rtrim(path, '\/\\\\'));
}
const prependPath: TwingFilesystemLoader["prependPath"] = (path, namespace = null) => {
path = rtrim(path, '\/\\\\');
const namespacePaths = namespacedPaths.get(namespace);
if (!namespacePaths) {
namespacedPaths.set(namespace!, [path]);
}
else {
namespacePaths.unshift(path);
}
}
return {
addPath,
exists: (name, from) => {
const path = resolve(name, from);
return path !== null;
},
resolve,
getSource: (name, from) => {
const path = resolve(name, from);
if (path === null) {
return null;
}
else {
const data = filesystem.readFileSync(path);
return createSource(path, data!.toString());
}
},
isFresh: (name, time, from) => {
const path = resolve(name, from);
if (path === null) {
return true;
}
else {
const stats = stat(path);
return stats!.mtime.getTime() <= time
}
},
prependPath
};
};

View File

@ -1,51 +1,63 @@
import type {TwingBaseNode} from "./node";
import type {TwingExecutionContext} from "./execution-context";
import {executeBinaryNode} from "./node-executor/expression/binary";
import {executeTemplateNode} from "./node-executor/template";
import {executePrintNode} from "./node-executor/print";
import {executeTextNode} from "./node-executor/text";
import {executeCallNode} from "./node-executor/expression/call";
import {executeMethodCall} from "./node-executor/expression/method-call";
import {executeAssignmentNode} from "./node-executor/expression/assignment";
import {executeImportNode} from "./node-executor/import";
import {executeParentFunction} from "./node-executor/expression/parent-function";
import {executeBlockFunction} from "./node-executor/expression/block-function";
import {executeBlockReferenceNode} from "./node-executor/block-reference";
import {executeUnaryNode} from "./node-executor/expression/unary";
import {executeArrayNode} from "./node-executor/expression/array";
import {executeHashNode} from "./node-executor/expression/hash";
import {executeAttributeAccessorNode} from "./node-executor/expression/attribute-accessor";
import {executeNameNode} from "./node-executor/expression/name";
import {executeSetNode} from "./node-executor/set";
import {executeIfNode} from "./node-executor/if";
import {executeForNode} from "./node-executor/for";
import {executeForLoopNode} from "./node-executor/for-loop";
import {executeCheckToStringNode} from "./node-executor/check-to-string";
import {executeConditionalNode} from "./node-executor/expression/conditional";
import {executeEmbedNode} from "./node-executor/include/embed";
import {executeIncludeNode} from "./node-executor/include/include";
import {executeWithNode} from "./node-executor/with";
import {executeSpacelessNode} from "./node-executor/spaceless";
import {executeApplyNode} from "./node-executor/apply";
import {executeEscapeNode} from "./node-executor/expression/escape";
import {executeArrowFunctionNode} from "./node-executor/expression/arrow-function";
import {executeSandboxNode} from "./node-executor/sandbox";
import {executeDoNode} from "./node-executor/do";
import {executeDeprecatedNode} from "./node-executor/deprecated";
import {executeSpreadNode} from "./node-executor/expression/spread";
import {executeCheckSecurityNode} from "./node-executor/check-security";
import {executeFlushNode} from "./node-executor/flush";
import {executeBinaryNode, executeBinaryNodeSynchronously} from "./node-executor/expression/binary";
import {executeTemplateNode, executeTemplateNodeSynchronously} from "./node-executor/template";
import {executePrintNode, executePrintNodeSynchronously} from "./node-executor/print";
import {executeTextNode, executeTextNodeSynchronously} from "./node-executor/text";
import {executeCallNode, executeCallNodeSynchronously} from "./node-executor/expression/call";
import {executeMethodCall, executeMethodCallSynchronously} from "./node-executor/expression/method-call";
import {executeAssignmentNode, executeAssignmentNodeSynchronously} from "./node-executor/expression/assignment";
import {executeImportNode, executeImportNodeSynchronously} from "./node-executor/import";
import {executeParentFunction, executeParentFunctionSynchronously} from "./node-executor/expression/parent-function";
import {executeBlockFunction, executeSynchronousBlockFunction} from "./node-executor/expression/block-function";
import {executeBlockReferenceNode, executeBlockReferenceNodeSynchronously} from "./node-executor/block-reference";
import {executeUnaryNode, executeUnaryNodeSynchronously} from "./node-executor/expression/unary";
import {executeArrayNode, executeArrayNodeSynchronously} from "./node-executor/expression/array";
import {executeHashNode, executeHashNodeSynchronously} from "./node-executor/expression/hash";
import {
executeAttributeAccessorNode,
executeAttributeAccessorNodeSynchronously
} from "./node-executor/expression/attribute-accessor";
import {executeNameNode, executeNameNodeSynchronously} from "./node-executor/expression/name";
import {executeSetNode, executeSetNodeSynchronously} from "./node-executor/set";
import {executeIfNode, executeIfNodeSynchronously} from "./node-executor/if";
import {executeForNode, executeForNodeSynchronously} from "./node-executor/for";
import {executeForLoopNode, executeForLoopNodeSynchronously} from "./node-executor/for-loop";
import {executeCheckToStringNode, executeCheckToStringNodeSynchronously} from "./node-executor/check-to-string";
import {executeConditionalNode, executeConditionalNodeSynchronously} from "./node-executor/expression/conditional";
import {executeEmbedNode, executeEmbedNodeSynchronously} from "./node-executor/include/embed";
import {executeIncludeNode, executeIncludeNodeSynchronously} from "./node-executor/include/include";
import {executeWithNode, executeWithNodeSynchronously} from "./node-executor/with";
import {executeSpacelessNode, executeSpacelessNodeSynchronously} from "./node-executor/spaceless";
import {executeApplyNode, executeApplyNodeSynchronously} from "./node-executor/apply";
import {executeEscapeNode, executeEscapeNodeSynchronously} from "./node-executor/expression/escape";
import {
executeArrowFunctionNode,
executeArrowFunctionNodeSynchronously
} from "./node-executor/expression/arrow-function";
import {executeSandboxNode, executeSandboxNodeSynchronously} from "./node-executor/sandbox";
import {executeDoNode, executeDoNodeSynchronously} from "./node-executor/do";
import {executeDeprecatedNode, executeDeprecatedNodeSynchronously} from "./node-executor/deprecated";
import {executeSpreadNode, executeSpreadNodeSynchronously} from "./node-executor/expression/spread";
import {executeCheckSecurityNode, executeCheckSecurityNodeSynchronously} from "./node-executor/check-security";
import {executeFlushNode, executeFlushNodeSynchronously} from "./node-executor/flush";
import {createRuntimeError} from "./error/runtime";
import {executeConstantNode} from "./node-executor/constant";
import {executeLineNode} from "./node-executor/line";
import {executeCommentNode} from "./node-executor/comment";
import {executeBaseNode} from "./node-executor/base";
import {executeConstantNode, executeConstantNodeSynchronously} from "./node-executor/constant";
import {executeLineNode, executeLineNodeSynchronously} from "./node-executor/line";
import {executeCommentNode, executeCommentNodeSynchronously} from "./node-executor/comment";
import {executeBaseNode, executeBaseNodeSynchronously} from "./node-executor/base";
import {TwingSynchronousExecutionContext} from "./execution-context";
export type TwingNodeExecutor<Node extends TwingBaseNode = TwingBaseNode> = (
node: Node,
executionContext: TwingExecutionContext
) => Promise<any>;
export type TwingSynchronousNodeExecutor<Node extends TwingBaseNode = TwingBaseNode> = (
node: Node,
executionContext: TwingSynchronousExecutionContext
) => any;
const binaryNodeTypes = ["add", "and", "bitwise_and", "bitwise_or", "bitwise_xor", "concatenate", "divide", "divide_and_floor", "ends_with", "has_every", "has_some", "is_equal_to", "is_greater_than", "is_greater_than_or_equal_to", "is_in", "is_less_than", "is_less_than_or_equal_to", "is_not_equal_to", "is_not_in", "matches", "modulo", "multiply", "or", "power", "range", "spaceship", "starts_with", "subtract"];
const isABinaryNode = (node: TwingBaseNode): boolean => {
@ -202,3 +214,136 @@ export const executeNode: TwingNodeExecutor = (node, executionContext) => {
return executor(node, executionContext);
};
export const executeNodeSynchronously: TwingSynchronousNodeExecutor = (node, executionContext) => {
let executor: TwingSynchronousNodeExecutor<any>;
if (isABinaryNode(node)) {
executor = executeBinaryNodeSynchronously;
}
else if (isACallNode(node)) {
executor = executeCallNodeSynchronously;
}
else if (isAUnaryNode(node)) {
executor = executeUnaryNodeSynchronously;
}
else if (node.type === null) {
executor = executeBaseNodeSynchronously;
}
else if (node.type === "apply") {
executor = executeApplyNodeSynchronously;
}
else if (node.type === "array") {
executor = executeArrayNodeSynchronously;
}
else if (node.type === "arrow_function") {
executor = executeArrowFunctionNodeSynchronously;
}
else if (node.type === "assignment") {
executor = executeAssignmentNodeSynchronously;
}
else if (node.type === "attribute_accessor") {
executor = executeAttributeAccessorNodeSynchronously;
}
else if (node.type === "block_function") {
executor = executeSynchronousBlockFunction;
}
else if (node.type === "block_reference") {
executor = executeBlockReferenceNodeSynchronously;
}
else if (node.type === "check_security") {
executor = executeCheckSecurityNodeSynchronously;
}
else if (node.type === "check_to_string") {
executor = executeCheckToStringNodeSynchronously;
}
else if (node.type === "comment") {
executor = executeCommentNodeSynchronously;
}
else if (node.type === "conditional") {
executor = executeConditionalNodeSynchronously;
}
else if (node.type === "constant") {
executor = executeConstantNodeSynchronously;
}
else if (node.type === "deprecated") {
executor = executeDeprecatedNodeSynchronously;
}
else if (node.type === "do") {
executor = executeDoNodeSynchronously;
}
else if (node.type === "embed") {
executor = executeEmbedNodeSynchronously;
}
else if (node.type === "escape") {
executor = executeEscapeNodeSynchronously;
}
else if (node.type === "flush") {
executor = executeFlushNodeSynchronously;
}
else if (node.type === "for") {
executor = executeForNodeSynchronously;
}
else if (node.type === "for_loop") {
executor = executeForLoopNodeSynchronously;
}
else if (node.type === "hash") {
executor = executeHashNodeSynchronously;
}
else if (node.type === "if") {
executor = executeIfNodeSynchronously;
}
else if (node.type === "import") {
executor = executeImportNodeSynchronously;
}
else if (node.type === "include") {
executor = executeIncludeNodeSynchronously;
}
else if (node.type === "line") {
executor = executeLineNodeSynchronously;
}
else if (node.type === "method_call") {
executor = executeMethodCallSynchronously;
}
else if (node.type === "name") {
executor = executeNameNodeSynchronously;
}
else if (node.type === "nullish_coalescing") {
executor = executeConditionalNodeSynchronously;
}
else if (node.type === "parent_function") {
executor = executeParentFunctionSynchronously;
}
else if (node.type === "print") {
executor = executePrintNodeSynchronously;
}
else if (node.type === "sandbox") {
executor = executeSandboxNodeSynchronously;
}
else if (node.type === "set") {
executor = executeSetNodeSynchronously;
}
else if (node.type === "spaceless") {
executor = executeSpacelessNodeSynchronously;
}
else if (node.type === "spread") {
executor = executeSpreadNodeSynchronously;
}
else if (node.type === "template") {
executor = executeTemplateNodeSynchronously;
}
else if (node.type === "text") {
executor = executeTextNodeSynchronously;
}
else if (node.type === "verbatim") {
executor = executeTextNodeSynchronously;
}
else if (node.type === "with") {
executor = executeWithNodeSynchronously;
}
else {
throw createRuntimeError(`Unrecognized node of type "${node.type}"`, node, executionContext.template.source);
}
return executor(node, executionContext);
};

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import type {TwingNodeExecutor} from "../node-executor";
import type {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor";
import type {TwingBlockReferenceNode} from "../node/block-reference";
import {getTraceableMethod} from "../helpers/traceable-method";
import {getSynchronousTraceableMethod, getTraceableMethod} from "../helpers/traceable-method";
export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNode> = (node, executionContext) => {
const {
@ -20,3 +20,24 @@ export const executeBlockReferenceNode: TwingNodeExecutor<TwingBlockReferenceNod
true,
);
};
export const executeBlockReferenceNodeSynchronously: TwingSynchronousNodeExecutor<TwingBlockReferenceNode> = (node, executionContext) => {
const {
template,
context
} = executionContext;
const {name} = node.attributes;
const displayBlock = getSynchronousTraceableMethod(template.displayBlock, node, template.source);
return displayBlock(
{
...executionContext,
// todo: was context: context.clone()
// context: context.clone()
context: new Map(context.entries())
},
name,
true,
);
};

View File

@ -1,4 +1,4 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor";
import {TwingCheckSecurityNode} from "../node/check-security";
import type {TwingNode} from "../node";
import {createRuntimeError} from "../error/runtime";
@ -33,3 +33,32 @@ export const executeCheckSecurityNode: TwingNodeExecutor<TwingCheckSecurityNode>
return Promise.resolve();
};
export const executeCheckSecurityNodeSynchronously: TwingSynchronousNodeExecutor<TwingCheckSecurityNode> = (node, executionContext) => {
const {template, environment, sandboxed} = executionContext;
const {usedTags, usedFunctions, usedFilters} = node.attributes;
if (sandboxed) {
const issue = environment.sandboxPolicy.checkSecurity(
[...usedTags.keys()],
[...usedFilters.keys()],
[...usedFunctions.keys()]
);
if (issue !== null) {
const {type, token} = issue;
let node: TwingNode;
if (type === "tag") {
node = usedTags.get(token)!;
} else if (type === "filter") {
node = usedFilters.get(token)!
} else {
node = usedFunctions.get(token)!;
}
throw createRuntimeError(issue.message, node, template.source);
}
}
};

View File

@ -1,12 +1,12 @@
import {TwingNodeExecutor} from "../node-executor";
import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor";
import {TwingCheckToStringNode} from "../node/check-to-string";
import {getTraceableMethod} from "../helpers/traceable-method";
import {getSynchronousTraceableMethod, getTraceableMethod} from "../helpers/traceable-method";
export const executeCheckToStringNode: TwingNodeExecutor<TwingCheckToStringNode> = (node, executionContext) => {
const {template, environment, nodeExecutor: execute, sandboxed} = executionContext;
const {value: valueNode} = node.children;
const {sandboxPolicy} = environment;
return execute(valueNode, executionContext)
.then((value) => {
if (sandboxed) {
@ -28,3 +28,25 @@ export const executeCheckToStringNode: TwingNodeExecutor<TwingCheckToStringNode>
return value;
});
};
export const executeCheckToStringNodeSynchronously: TwingSynchronousNodeExecutor<TwingCheckToStringNode> = (node, executionContext) => {
const {template, environment, nodeExecutor: execute, sandboxed} = executionContext;
const {value: valueNode} = node.children;
const {sandboxPolicy} = environment;
const value = execute(valueNode, executionContext);
if (sandboxed) {
const assertToStringAllowed = getSynchronousTraceableMethod((value: any) => {
if ((value !== null) && (typeof value === 'object')) {
sandboxPolicy.checkMethodAllowed(value, 'toString');
}
return value;
}, valueNode, template.source)
return assertToStringAllowed(value);
}
return value;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import type {TwingNodeExecutor} from "../../node-executor";
import type {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../../node-executor";
import {type TwingBaseArrayNode} from "../../node/expression/array";
import type {TwingNode} from "../../node";
import {getKeyValuePairs} from "../../helpers/get-key-value-pairs";
import {getValues} from "../../context";
export const executeArrayNode: TwingNodeExecutor<TwingBaseArrayNode<any>> = async (baseNode, executionContext) => {
const {nodeExecutor: execute} = executionContext;
@ -20,3 +21,23 @@ export const executeArrayNode: TwingNodeExecutor<TwingBaseArrayNode<any>> = asyn
return array;
};
export const executeArrayNodeSynchronously: TwingSynchronousNodeExecutor<TwingBaseArrayNode<any>> = (baseNode, executionContext) => {
const {nodeExecutor: execute} = executionContext;
const keyValuePairs = getKeyValuePairs(baseNode);
const array: Array<any> = [];
for (const {value: valueNode} of keyValuePairs) {
const value = execute(valueNode, executionContext);
if ((valueNode as TwingNode).type === "spread") {
const values = getValues(value);
array.push(...values);
} else {
array.push(value);
}
}
return array;
};

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import {TwingNodeExecutor} from "../../node-executor";
import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../../node-executor";
import {TwingAttributeAccessorNode} from "../../node/expression/attribute-accessor";
import {getTraceableMethod} from "../../helpers/traceable-method";
import {getAttribute} from "../../helpers/get-attribute";
import {getSynchronousTraceableMethod, getTraceableMethod} from "../../helpers/traceable-method";
import {getAttribute, getAttributeSynchronously} from "../../helpers/get-attribute";
export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAccessorNode> = (node, executionContext) => {
const {template, sandboxed, environment, nodeExecutor: execute, strict} = executionContext;
@ -28,3 +28,28 @@ export const executeAttributeAccessorNode: TwingNodeExecutor<TwingAttributeAcces
)
})
};
export const executeAttributeAccessorNodeSynchronously: TwingSynchronousNodeExecutor<TwingAttributeAccessorNode> = (node, executionContext) => {
const {template, sandboxed, environment, nodeExecutor: execute, strict} = executionContext;
const {target: targetNode, attribute: attributeNode, arguments: argumentsNode} = node.children;
const {type, shouldIgnoreStrictCheck, shouldTestExistence} = node.attributes;
const target = execute(targetNode, executionContext);
const attribute = execute(attributeNode, executionContext);
const methodArguments = execute(argumentsNode, executionContext);
const traceableGetAttribute = getSynchronousTraceableMethod(getAttributeSynchronously, node, template.source);
return traceableGetAttribute(
environment,
target,
attribute,
methodArguments,
type,
shouldTestExistence,
shouldIgnoreStrictCheck || null,
sandboxed,
strict
);
};

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