UNPKG

@cloudpss/template

Version:

String and object template engine for Node.js and the browser.

152 lines (145 loc) 6.7 kB
import { parseTemplate, type TemplateType } from '../parser.js'; import type { TemplateFunction, TemplateOptions } from './index.js'; /** 是否为 ArrayBuffer */ function isArrayBuffer(value: object): value is ArrayBuffer | SharedArrayBuffer { if (value instanceof ArrayBuffer) return true; if (typeof SharedArrayBuffer == 'function' && value instanceof SharedArrayBuffer) return true; return false; } /** 是否为 Error */ function isError(value: unknown): value is Error { return value instanceof Error || (typeof DOMException == 'function' && value instanceof DOMException); } const KNOWN_ERRORS = [EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError] as const; // eslint-disable-next-line @typescript-eslint/unbound-method const toString = Function.call.bind(Object.prototype.toString) as (value: unknown) => string; const { hasOwn, defineProperty } = Object; /** 模板序列号 */ let seq = 0; /** 创建模板 */ export class TemplateCompiler { constructor( readonly template: unknown, readonly options: Required<TemplateOptions>, ) {} private readonly params = new Map<string, unknown>(); private readonly copyable: unknown[] = []; /** 构建求值 */ private buildEval(expression: string, type: TemplateType): string { const { evaluator } = this.options; if (!this.params.has('evaluator')) { this.params.set('evaluator', evaluator.inject); } return evaluator.compile(expression, type); } /** 构建字符串 */ private buildString(str: string): [result: string, isExpression: boolean] { const parsed = parseTemplate(str); if (typeof parsed === 'string') return [JSON.stringify(parsed), false]; if (parsed.type === 'formula') return [this.buildEval(parsed.value, parsed.type), true]; let result = ''; for (let i = 0; i < parsed.templates.length; i++) { if (parsed.templates[i]) { result += (result ? '+' : '') + JSON.stringify(parsed.templates[i]!); } if (i < parsed.values.length) { if (!result) result = '""'; result += '+' + this.buildEval(parsed.values[i]!, parsed.type); } } return [result, true]; } /** 构建 Error */ private buildError(err: Error): string { if (typeof DOMException == 'function' && err instanceof DOMException) { return `new DOMException(${this.buildString(err.message)[0]}, ${this.buildString(err.name)[0]})`; } const constructor = KNOWN_ERRORS.find((type) => err instanceof type)?.name ?? 'Error'; if (err.name === constructor) { return `new ${constructor}(${this.buildString(err.message)[0]})`; } return `Object.assign(new ${constructor}(${this.buildString(err.message)[0]}), {name: ${this.buildString(err.name)[0]}})`; } /** 构建数组 */ private buildArray(arr: unknown[]): string { return `[${arr.map(this.buildValue.bind(this)).join(',\n')}]`; } /** 构建 ArrayBuffer */ private buildArrayBuffer(buffer: ArrayBuffer | SharedArrayBuffer): string { this.copyable.push(buffer.slice(0)); return `copyable[${this.copyable.length - 1}].slice(0)`; } /** 构建 ArrayBufferView */ private buildArrayBufferView(view: ArrayBufferView): string { this.copyable.push(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)); return `new ${view.constructor.name}(copyable[${this.copyable.length - 1}].slice(0))`; } /** 构建对象 */ private buildObject(obj: Record<string, unknown>): string { let result = ''; for (const key in obj) { if (!hasOwn(obj, key)) continue; const value = obj[key]; if (result) result += ',\n'; if (this.options.objectKeyMode === 'ignore') { result += JSON.stringify(key); } else { const [e, isExpression] = this.buildString(key); if (isExpression) { result += '[' + e + ']'; } else { result += e; } } result += ':'; result += this.buildValue(value); } return '{' + result + '}'; } /** 构建值 */ private buildValue(value: unknown): string { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (value === true) return 'true'; if (value === false) return 'false'; if (typeof value == 'function') return 'undefined'; if (typeof value == 'symbol') return 'undefined'; if (typeof value == 'bigint') return `${value}n`; if (typeof value == 'number') return String(value); if (typeof value == 'string') return this.buildString(value)[0]; /* c8 ignore next */ if (typeof value != 'object') throw new Error(`Unsupported value: ${toString(value)}`); if (value instanceof Date) return `new Date(${value.getTime()})`; if (value instanceof RegExp) return value.toString(); if (isError(value)) return this.buildError(value); if (Array.isArray(value)) return this.buildArray(value); if (isArrayBuffer(value)) return this.buildArrayBuffer(value); if (ArrayBuffer.isView(value)) return this.buildArrayBufferView(value); return this.buildObject(value as Record<string, unknown>); } /** 构建模板 */ build(): TemplateFunction { const source = this.buildValue(this.template); if (this.copyable.length) { this.params.set('copyable', this.copyable); } const params = [...this.params]; try { // eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call const result = new Function( ...params.map(([key]) => key), [ `//# sourceURL=cloudpss-template[${seq++}]`, // sourceURL 用于调试 this.options.evaluator.async ? `return async (context = {}) => (${source});` : `return (context = {}) => (${source});`, ].join('\n'), )(...params.map(([, value]) => value)) as TemplateFunction; defineProperty(result, 'source', { value: source, configurable: true }); return result; /* c8 ignore next 3 */ } catch (e) { throw new Error(`Failed to compile template: ${source}\n${(e as Error).message}`, { cause: e }); } } }