Resolve issue #618

This commit is contained in:
Eric MORAND 2024-07-27 08:34:33 +00:00
parent 7e02a05e4d
commit 2fd56ff4f8
14 changed files with 443 additions and 55 deletions

View File

@ -1,7 +1,7 @@
/* istanbul ignore file */
// cache
export type {TwingCache} from "./lib/cache";
export type {TwingCache, TwingSynchronousCache} from "./lib/cache";
// error
export type {TwingError} from "./lib/error";
@ -16,15 +16,15 @@ export {createTemplateLoadingError} from "./lib/error/loader";
// loader
export type {
TwingFilesystemLoader, TwingFilesystemLoaderFilesystem, TwingFilesystemLoaderFilesystemStats
TwingFilesystemLoader, TwingFilesystemLoaderFilesystem, TwingFilesystemLoaderFilesystemStats, TwingSynchronousFilesystemLoader, TwingSynchronousFilesystemLoaderFilesystem
} from "./lib/loader/filesystem";
export type {TwingArrayLoader, TwingSynchronousArrayLoader} from "./lib/loader/array";
export type {TwingChainLoader} from "./lib/loader/chain";
export type {TwingChainLoader, TwingSynchronousChainLoader} from "./lib/loader/chain";
export type {TwingLoader, TwingSynchronousLoader} from "./lib/loader";
export {createFilesystemLoader, createSynchronousFilesystemLoader} from "./lib/loader/filesystem";
export {createArrayLoader, createSynchronousArrayLoader} from "./lib/loader/array";
export {createChainLoader} from "./lib/loader/chain";
export {createChainLoader, createSynchronousChainLoader} from "./lib/loader/chain";
// markup
export type {TwingMarkup} from "./lib/markup";
@ -263,18 +263,18 @@ export {createWithTagHandler} from "./lib/tag-handler/with";
// core
export type {
TwingCallable, TwingCallableArgument, TwingCallableWrapperOptions, TwingCallableWrapper
TwingCallable, TwingCallableArgument, TwingCallableWrapperOptions, TwingCallableWrapper, TwingSynchronousCallable, TwingSynchronousCallableWrapper
} from "./lib/callable-wrapper";
export {type TwingContext, createContext} from "./lib/context";
export type {TwingEnvironment, TwingEnvironmentOptions, TwingNumberFormat} from "./lib/environment";
export type {TwingEnvironment, TwingEnvironmentOptions, TwingNumberFormat, TwingSynchronousEnvironment, TwingSynchronousEnvironmentOptions} from "./lib/environment";
export type {
TwingEscapingStrategy, TwingEscapingStrategyHandler, TwingEscapingStrategyResolver
} from "./lib/escaping-strategy";
export type {TwingExecutionContext} from "./lib/execution-context";
export type {TwingExtension} from "./lib/extension";
export type {TwingExecutionContext, TwingSynchronousExecutionContext} from "./lib/execution-context";
export type {TwingExtension, TwingSynchronousExtension} from "./lib/extension";
export type {TwingExtensionSet} from "./lib/extension-set";
export type {TwingFilter} from "./lib/filter";
export type {TwingFunction} from "./lib/function";
export type {TwingFilter, TwingSynchronousFilter} from "./lib/filter";
export type {TwingFunction, TwingSynchronousFunction} from "./lib/function";
export type {TwingLexer} from "./lib/lexer";
export type {TwingNodeVisitor} from "./lib/node-visitor";
export type {
@ -289,26 +289,25 @@ export type {
TwingTemplateAliases,
TwingTemplateBlockMap,
TwingTemplateBlockHandler,
TwingTemplateMacroHandler
TwingTemplateMacroHandler,
TwingSynchronousTemplateAliases,
TwingSynchronousTemplateBlockHandler,
TwingSynchronousTemplateBlockMap,
TwingSynchronousTemplateMacroHandler
} from "./lib/template";
export type {TwingTest} from "./lib/test";
export type {TwingTest, TwingSynchronousTest} from "./lib/test";
export type {TwingTokenStream} from "./lib/token-stream";
export interface TwingTemplate {
execute: import("./lib/template").TwingTemplate["execute"];
render: import("./lib/template").TwingTemplate["render"];
}
export {createEnvironment, createSynchronousEnvironment} from "./lib/environment";
export {createExtensionSet} from "./lib/extension-set";
export {createFilter} from "./lib/filter";
export {createFunction} from "./lib/function";
export {createFilter, createSynchronousFilter} from "./lib/filter";
export {createFunction, createSynchronousFunction} from "./lib/function";
export {createLexer} from "./lib/lexer";
export {createBaseNode, createNode, getChildren, getChildrenCount} from "./lib/node";
export {createOperator} from "./lib/operator";
export {createSandboxSecurityPolicy} from "./lib/sandbox/security-policy";
export {createSource} from "./lib/source";
export {createSourceMapRuntime} from "./lib/source-map-runtime";
export {createTemplate} from "./lib/template";
export {type TwingTemplateLoader, createTemplateLoader} from "./lib/template-loader";
export {createTest} from "./lib/test";
export {type TwingTemplate, createTemplate, type TwingSynchronousTemplate, createSynchronousTemplate} from "./lib/template";
export {type TwingTemplateLoader, type TwingSynchronousTemplateLoader, createTemplateLoader, createSynchronousTemplateLoader} from "./lib/template-loader";
export {createTest, createSynchronousTest} from "./lib/test";

View File

@ -69,5 +69,3 @@ export const getEntries = <V>(context: Record<string, V>): IterableIterator<[str
export const getValues = <V>(context: Record<string, V>): Array<V> => {
return Object.values(context);
};
export type TwingContext2 = Map<string, any>;

View File

@ -27,7 +27,7 @@ import {TwingCache, TwingSynchronousCache} from "./cache";
import {createCoreExtension, createSynchronousCoreExtension} from "./extension/core";
import {createAutoEscapeNode, createTemplateLoadingError, type TwingContext} from "../lib";
import {createSynchronousTemplateLoader, createTemplateLoader} from "./template-loader";
import {createContext, TwingContext2} from "./context";
import {createContext} from "./context";
import {iterableToMap} from "./helpers/iterator-to-map";
export type TwingNumberFormat = {
@ -165,7 +165,7 @@ export interface TwingSynchronousEnvironment {
readonly numberFormat: TwingNumberFormat;
readonly filters: Map<string, TwingSynchronousFilter>;
readonly functions: Map<string, TwingSynchronousFunction>;
readonly globals: TwingContext2;
readonly globals: Map<string, any>;
readonly loader: TwingSynchronousLoader;
readonly sandboxPolicy: TwingSandboxSecurityPolicy;
readonly tests: Map<string, TwingSynchronousTest>;
@ -567,14 +567,14 @@ export const createSynchronousEnvironment = (
},
render: (name, data, options) => {
const template = environment.loadTemplate(name);
const context: TwingContext2 = new Map(Object.entries(data));
const context: Map<string, any> = 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 context: Map<string, any> = new Map(Object.entries(data));
const template = environment.loadTemplate(name);
const output = template.render(environment, context, {
...options,

View File

@ -1,5 +1,5 @@
import type {TwingTemplate, TwingTemplateAliases, TwingTemplateBlockMap} from "./template";
import type {TwingContext, TwingContext2} from "./context";
import type {TwingContext} from "./context";
import type {TwingOutputBuffer} from "./output-buffer";
import type {TwingSourceMapRuntime} from "./source-map-runtime";
import type {TwingEnvironment, TwingSynchronousEnvironment} from "./environment";
@ -26,7 +26,7 @@ export type TwingExecutionContext = {
export type TwingSynchronousExecutionContext = {
aliases: TwingSynchronousTemplateAliases;
blocks: TwingSynchronousTemplateBlockMap;
context: TwingContext2;
context: Map<string, any>;
environment: TwingSynchronousEnvironment;
nodeExecutor: TwingSynchronousNodeExecutor;
outputBuffer: TwingOutputBuffer;

View File

@ -1,6 +1,6 @@
import {isTraversable} from "../../../helpers/is-traversable";
import {isPlainObject} from "../../../helpers/is-plain-object";
import {createContext, TwingContext2} from "../../../context";
import {createContext} from "../../../context";
import {createMarkup, TwingMarkup} from "../../../markup";
import type {TwingSynchronousTemplate, TwingTemplate} from "../../../template";
import type {TwingCallable, TwingSynchronousCallable} from "../../../callable-wrapper";
@ -103,7 +103,7 @@ export const include: TwingCallable<[
export const includeSynchronously: TwingSynchronousCallable<[
templates: string | TwingSynchronousTemplate | null | Array<string | TwingSynchronousTemplate | null>,
variables: TwingContext2,
variables: Map<string, any>,
withContext: boolean,
ignoreMissing: boolean,
sandboxed: boolean

View File

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

View File

@ -1,4 +1,4 @@
import type {TwingContext, TwingContext2} from "../context";
import type {TwingContext} from "../context";
export const getContextValue = (
charset: string,
@ -51,8 +51,8 @@ export const getContextValueSynchronously = (
charset: string,
templateName: string,
isStrictVariables: boolean,
context: TwingContext2,
globals: TwingContext2,
context: Map<string, any>,
globals: Map<string, any>,
name: string,
isAlwaysDefined: boolean,
shouldIgnoreStrictCheck: boolean,

View File

@ -1,4 +1,4 @@
import type {TwingLoader} from "../loader";
import type {TwingLoader, TwingSynchronousLoader} from "../loader";
import type {TwingSource} from "../source";
export interface TwingChainLoader extends TwingLoader {
@ -7,6 +7,12 @@ export interface TwingChainLoader extends TwingLoader {
addLoader(loader: TwingLoader): void;
}
export interface TwingSynchronousChainLoader extends TwingSynchronousLoader {
readonly loaders: Array<TwingSynchronousLoader>;
addLoader(loader: TwingSynchronousLoader): void;
}
export const createChainLoader = (
loaders: Array<TwingLoader>
): TwingChainLoader => {
@ -139,3 +145,125 @@ export const createChainLoader = (
return loader;
};
export const createSynchronousChainLoader = (
loaders: Array<TwingSynchronousLoader>
): TwingSynchronousChainLoader => {
let existsCache: Map<string, boolean> = new Map();
const addLoader: TwingSynchronousChainLoader["addLoader"] = (loader) => {
loaders.push(loader);
existsCache = new Map();
};
const loader: TwingSynchronousChainLoader = {
get loaders() {
return loaders
},
addLoader,
exists: (name, from) => {
const cachedResult = existsCache.get(name);
if (cachedResult) {
return cachedResult;
}
const existsAtIndex = (index: number): boolean => {
if (index < loaders.length) {
const loader = loaders[index];
const exists = loader.exists(name, from);
existsCache.set(name, exists);
if (!exists) {
return existsAtIndex(index + 1);
} else {
return true;
}
} else {
return false;
}
};
const exists = existsAtIndex(0);
existsCache.set(name, exists);
return exists;
},
resolve: (name, from) => {
const resolveAtIndex = (index: number): string | null => {
if (index < loaders.length) {
const loader = loaders[index];
const exists = loader.exists(name, from);
const key = exists ? loader.resolve(name, from) : resolveAtIndex(index + 1);
if (key === null) {
return resolveAtIndex(index + 1);
}
return key;
} else {
return null;
}
};
const key = resolveAtIndex(0);
if (key) {
return key;
} else {
return null;
}
},
getSource: (name, from) => {
const getSourceContextAtIndex = (index: number): TwingSource | null => {
if (index < loaders.length) {
let loader = loaders[index];
const source = loader.getSource(name, from);
if (source === null) {
return getSourceContextAtIndex(index + 1);
}
return source;
} else {
return null;
}
};
const source = getSourceContextAtIndex(0);
if (source) {
return source;
} else {
return null;
}
},
isFresh: (name, time, from) => {
const isFreshAtIndex = (index: number): boolean | null => {
if (index < loaders.length) {
const loader = loaders[index];
const isFresh = loader.isFresh(name, time, from);
if (isFresh === null) {
return isFreshAtIndex(index + 1);
}
return isFresh;
} else {
return null;
}
};
return isFreshAtIndex(0);
}
};
return loader;
};

View File

@ -1,6 +1,6 @@
import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor";
import {TwingForNode} from "../node/for";
import {TwingContext, TwingContext2} from "../context";
import {TwingContext} from "../context";
import {ensureTraversable} from "../helpers/ensure-traversable";
import {count} from "../helpers/count";
import {iterate, iterateSynchronously} from "../helpers/iterate";
@ -140,7 +140,7 @@ export const executeForNodeSynchronously: TwingSynchronousNodeExecutor<TwingForN
}
}
const parent: TwingContext2 = context.get('_parent');
const parent: Map<string, any> = context.get('_parent');
context.delete('_seq');
context.delete('_iterated');

View File

@ -1,6 +1,6 @@
import {TwingNodeExecutor, TwingSynchronousNodeExecutor} from "../node-executor";
import {TwingWithNode} from "../node/with";
import {createContext, TwingContext, TwingContext2} from "../context";
import {createContext, TwingContext} from "../context";
import {createRuntimeError} from "../error/runtime";
import {mergeIterables} from "../helpers/merge-iterables";
import {iteratorToMap} from "../helpers/iterator-to-map";
@ -47,7 +47,7 @@ export const executeWithNodeSynchronously: TwingSynchronousNodeExecutor<TwingWit
const {variables: variablesNode, body} = node.children;
const {only} = node.attributes;
let scopedContext: TwingContext2;
let scopedContext: Map<string, any>;
if (variablesNode) {
let variables = execute(variablesNode, executionContext);

View File

@ -5,7 +5,7 @@ export interface TwingSandboxSecurityPolicy {
* @param {any | TwingMarkup} candidate
* @param {string} method
*
* @throws {@link TwingSandboxSecurityNotAllowedMethodError} When the method is not allowed on the passed object
* @throws When the method is not allowed on the passed object
*/
checkMethodAllowed(candidate: any | TwingMarkup, method: string): void;

View File

@ -1,4 +1,4 @@
import {createContext, TwingContext, TwingContext2} from "./context";
import {createContext, TwingContext} from "./context";
import {TwingEnvironment, TwingSynchronousEnvironment} from "./environment";
import {createOutputBuffer, TwingOutputBuffer} from "./output-buffer";
import {TwingSourceMapRuntime} from "./source-map-runtime";
@ -184,7 +184,7 @@ export interface TwingSynchronousTemplate {
*/
execute(
environment: TwingSynchronousEnvironment,
context: TwingContext2,
context: Map<string, any>,
blocks: TwingSynchronousTemplateBlockMap,
outputBuffer: TwingOutputBuffer,
options?: {
@ -225,7 +225,7 @@ export interface TwingSynchronousTemplate {
render(
environment: TwingSynchronousEnvironment,
context: TwingContext2,
context: Map<string, any>,
options?: {
nodeExecutor?: TwingSynchronousNodeExecutor;
outputBuffer?: TwingOutputBuffer;
@ -749,7 +749,7 @@ export const createSynchronousTemplate = (
const aliases = {...template.aliases};
const localVariables: TwingContext2 = new Map();
const localVariables: Map<string, any> = new Map();
for (const {key: keyNode, value: defaultValueNode} of keyValuePairs) {
const key = keyNode.attributes.value as string;

View File

@ -9,7 +9,7 @@ tape('library index', ({same, end}) => {
'createParsingError',
'createFilesystemLoader', 'createSynchronousFilesystemLoader',
'createArrayLoader', 'createSynchronousArrayLoader',
'createChainLoader',
'createChainLoader', 'createSynchronousChainLoader',
'createMarkup', 'isAMarkup',
'createApplyNode',
'createAutoEscapeNode',
@ -117,17 +117,17 @@ tape('library index', ({same, end}) => {
'getChildrenCount',
'createEnvironment', 'createSynchronousEnvironment',
'createExtensionSet',
'createFilter',
'createFunction',
'createFilter', 'createSynchronousFilter',
'createFunction', 'createSynchronousFunction',
'createLexer',
'createOperator',
'createSandboxSecurityPolicy',
'createSource',
'createSourceMapRuntime',
'createTemplate',
'createTest',
'createTemplate', 'createSynchronousTemplate',
'createTest', 'createSynchronousTest',
'executeNode', 'executeNodeSynchronously',
'createTemplateLoader',
'createTemplateLoader', 'createSynchronousTemplateLoader',
'createContext',
'createOutputBuffer'
];

View File

@ -1,6 +1,6 @@
import * as tape from 'tape';
import {createChainLoader} from "../../../../../../main/lib/loader/chain";
import {createArrayLoader} from "../../../../../../main/lib/loader/array";
import {createChainLoader, createSynchronousChainLoader} from "../../../../../../main/lib/loader/chain";
import {createArrayLoader, createSynchronousArrayLoader} from "../../../../../../main/lib/loader/array";
import {spy, stub} from "sinon";
tape('createChainLoader', ({test}) => {
@ -267,3 +267,266 @@ tape('createChainLoader', ({test}) => {
});
});
});
tape('createSynchronousChainLoader', ({test}) => {
test('getSourceContext', ({test}) => {
let loader = createSynchronousChainLoader([
createSynchronousArrayLoader({'foo': 'bar'}),
createSynchronousArrayLoader({'errors/index.html': 'baz'})
]);
test('return the source context of the first loader that returns a source context', ({test}) => {
test('foo', ({same, end}) => {
const source = loader.getSource('foo', null);
same(source?.name, 'foo');
same(source?.code, 'bar');
end();
});
test('errors/index.html', ({same, end}) => {
const source = loader.getSource('errors/index.html', null);
same(source?.name, 'errors/index.html');
same(source?.code, 'baz');
end();
});
});
test('returns null when the template does not exist', ({same, end}) => {
const loader = createSynchronousChainLoader([]);
same(loader.getSource('foo', null), null);
end();
});
});
test('resolve', ({test}) => {
test('returns the template FQN when the template exists', ({same, end}) => {
let loader = createSynchronousChainLoader([
createSynchronousArrayLoader({'foo': 'bar'}),
createSynchronousArrayLoader({'foo': 'foobar', 'bar': 'foo'}),
]);
same(loader.resolve('foo', null), 'foo');
same(loader.resolve('bar', null), 'bar');
let resolveStub = stub(loader, 'resolve').returns(null);
loader = createSynchronousChainLoader([
loader
]);
same(loader.resolve('foo', null), null);
resolveStub.restore();
let loader2 = createSynchronousArrayLoader({'foo': 'bar'});
resolveStub = stub(loader2, 'resolve').returns(null);
loader = createSynchronousChainLoader([
loader2
]);
same(loader.resolve('foo', null), null);
resolveStub.restore();
end();
});
test('returns null when the template does not exist', ({same, end}) => {
const loader = createSynchronousChainLoader([]);
same(loader.resolve('foo', null), null);
end();
});
});
test('addLoader', ({same, end}) => {
const loader = createSynchronousChainLoader([]);
loader.addLoader(createSynchronousArrayLoader({'foo': 'bar'}));
const source = loader.getSource('foo', null);
same(source?.code, 'bar');
end();
});
test('getLoaders', (test) => {
let loaders = [
createArrayLoader({'foo': 'bar'})
];
let loader = createChainLoader(loaders);
test.same(loader.loaders, loaders);
test.end();
});
test('exists', ({test}) => {
let loader1 = createSynchronousArrayLoader({});
let loader1ExistsStub = stub(loader1, 'exists').returns(false);
let loader1GetSourceSpy = spy(loader1, 'getSource');
let loader2 = createSynchronousArrayLoader({});
let loader2ExistsStub = stub(loader2, 'exists').returns(true);
let loader2GetSourceSpy = spy(loader2, 'getSource');
let loader3 = createSynchronousArrayLoader({});
let loader3ExistsStub = stub(loader3, 'exists').returns(true);
let loader3GetSourceSpy = spy(loader3, 'getSource');
test('resolves to true as soon as a loader resolves to true', ({same, end}) => {
let loader = createSynchronousChainLoader([
loader1,
loader2,
loader3
]);
same(loader.exists('foo', null), true);
same(loader1ExistsStub.callCount, 1, 'loader 1 exists is called once');
same(loader2ExistsStub.callCount, 1, 'loader 2 exists is called once');
same(loader3ExistsStub.callCount, 0, 'loader 3 exists is not called');
same(loader1GetSourceSpy.callCount, 0, 'loader 1 getSourceContext is not called');
same(loader2GetSourceSpy.callCount, 0, 'loader 2 getSourceContext is not called');
same(loader3GetSourceSpy.callCount, 0, 'loader 3 getSourceContext is not called');
loader1ExistsStub.restore();
loader2ExistsStub.restore();
loader3ExistsStub.restore();
end();
});
test('resolves to false is all loaders resolve to false', ({same, end}) => {
let loader = createSynchronousChainLoader([
loader1,
loader2
]);
loader1ExistsStub = stub(loader1, 'exists').returns(false);
loader2ExistsStub = stub(loader2, 'exists').returns(false);
same(loader.exists('foo', null), false);
loader1ExistsStub.restore();
loader2ExistsStub.restore();
end();
});
test('hits cache on subsequent calls', async ({same, end}) => {
let loader = createSynchronousChainLoader([
createSynchronousArrayLoader({
foo: 'foo'
})
]);
const existsSpy = spy(loader.loaders[0], 'exists');
await loader.exists('foo', null);
await loader.exists('foo', null);
same(existsSpy.callCount, 1);
existsSpy.restore();
end();
});
});
test('isFresh', ({same, end}) => {
let loader = createSynchronousChainLoader([
createSynchronousArrayLoader({'foo': 'bar'}),
createSynchronousArrayLoader({'foo': 'foobar', 'bar': 'foo'}),
]);
same(loader.isFresh('foo', 0, null), true);
same(loader.isFresh('bar', 0, null), true);
let isFreshStub = stub(loader, 'isFresh').returns(null);
loader = createSynchronousChainLoader([
loader
]);
same(loader.isFresh('foo', 0, null), null);
isFreshStub.restore();
const loader2 = createSynchronousArrayLoader({'foo': 'bar'});
isFreshStub = stub(loader2, 'isFresh').returns(null);
loader = createSynchronousChainLoader([
loader2
]);
same(loader.isFresh('foo', 0, null), null);
isFreshStub.restore();
end();
});
test('resolve', ({test}) => {
test('returns whatever the first loader that handles the passed name returns', ({same, end}) => {
const loader = createSynchronousChainLoader([
createSynchronousArrayLoader({'foo': 'bar'}),
createSynchronousArrayLoader({'bar': 'foo'}),
]);
same(loader.resolve('bar', null), 'bar');
end();
});
test('when some loaders return null', ({same, end}) => {
const loader1 = createSynchronousArrayLoader({});
stub(loader1, 'exists').returns(true);
stub(loader1, 'resolve').returns(null);
const loader2 = createSynchronousArrayLoader({'bar': 'foo'});
const loader = createSynchronousChainLoader([
loader1,
loader2
]);
same(loader.resolve('bar', null), 'bar');
end();
});
test('when all loaders return null', ({same, end}) => {
const loader1 = createSynchronousArrayLoader({});
stub(loader1, 'exists').returns(true);
stub(loader1, 'resolve').returns(null);
const loader2 = createSynchronousArrayLoader({});
stub(loader2, 'exists').returns(true);
stub(loader2, 'resolve').returns(null);
const loader = createSynchronousChainLoader([
loader1,
loader2
]);
same(loader.resolve('foo', null), null);
end();
});
});
});