import iterateTopLevel from "./js.ts"; import tokenize, { type Token } from "./tokenizer.ts"; import { createError, SourceError } from "./errors.ts"; export interface TemplateResult { content: string; [key: string]: unknown; } export interface TemplateContext { source: string; code: string; path?: string; defaults?: Record; } export interface Template extends TemplateContext { (data?: Record): Promise; } export type TokenPreprocessor = ( env: Environment, tokens: Token[], path?: string, ) => Token[] | void; export type Tag = ( env: Environment, token: Token, output: string, tokens: Token[], ) => string | undefined; export type FilterThis = { data: Record; env: Environment; }; // deno-lint-ignore no-explicit-any export type Filter = (this: FilterThis, ...args: any[]) => any; export type Plugin = (env: Environment) => void; export interface TemplateSource { source: string; data?: Record; } export type PrecompiledTemplate = (env: Environment) => Template; export interface Loader { load(file: string): Promise; resolve(from: string, file: string): string; } export interface Options { loader: Loader; dataVarname: string; autoescape: boolean; autoDataVarname: boolean; strict: boolean; } export class Environment { cache: Map> = new Map(); options: Options; tags: Tag[] = []; tokenPreprocessors: TokenPreprocessor[] = []; filters: Record = {}; #tempVariablesCreated = 0; utils: Record = { callMethod, createError, }; constructor(options: Options) { this.options = options; this.utils.safeString = (str: string) => this.options.autoescape ? new SafeString(str) : str; } use(plugin: Plugin) { plugin(this); } async run( file: string, data?: Record, from?: string, position?: number, ): Promise { const template = await this.load(file, from, position); return await template(data); } async runString( source: string, data?: Record, file?: string, ): Promise { if (file) { const cached = this.cache.get(file); if (cached) { return (await cached)(data); } const template = this.compile(source, file); this.cache.set(file, template); return await template(data); } const template = this.compile(source, file); return await template(data); } compile( source: string, path?: string, defaults?: Record, ): Template { if (typeof source !== "string") { throw new TypeError( `The source code of "${path}" must be a string. Got ${typeof source}`, ); } const tokens = this.tokenize(source, path); const lastToken = tokens.at(-1)!; if (lastToken[0] != "string") { throw new SourceError("Unclosed tag", lastToken[2], path, source); } let code = ""; try { code = this.compileTokens(tokens).join("\n"); } catch (error) { if (error instanceof SourceError) { error.file ??= path; error.source ??= source; } throw error; } const { dataVarname, autoDataVarname, strict } = this.options; if (strict && autoDataVarname) { const innerCode = JSON.stringify(` const __exports = { content: "" }; ${code} return __exports; `); code = ` return new (async function(){}).constructor( "__env", "__template", "${dataVarname}", \`{\${Object.keys(${dataVarname}).join(",")}}\`, ${innerCode} )(__env, __template, ${dataVarname}, ${dataVarname}); `; } else if (autoDataVarname) { const generator = iterateTopLevel(code); const [, , variables] = generator.next().value; while (!generator.next().done); variables.delete(dataVarname); if (variables.size > 0) { code = ` var {${[...variables].join(",")}} = ${dataVarname}; {\n${code}\n} `; } } try { const constructor = new Function( "__env", `return async function __template(${dataVarname}) { let __pos=0; try { ${dataVarname} = Object.assign({}, __template.defaults, ${dataVarname}); const __exports = { content: "" }; ${code} return __exports; } catch (error) { throw __env.utils.createError(error, __template, __pos); } }`, ); const template = constructor(this); template.path = path; template.code = constructor.toString(); template.source = source; template.defaults = defaults || {}; return template; } catch (error) { if (error instanceof SyntaxError) { throw createError(error, { source, code, path }); } if (error instanceof SourceError) { error.file ??= path; error.source ??= source; } throw error; } } tokenize(source: string, path?: string): Token[] { let tokens = tokenize(source); for (const tokenPreprocessor of this.tokenPreprocessors) { const result = tokenPreprocessor(this, tokens, path); if (result !== undefined) { tokens = result; } } return tokens; } async load( file: string, from?: string, position?: number, ): Promise