@cloudpss/template
Version:
String and object template engine for Node.js and the browser.
152 lines (145 loc) • 6.7 kB
text/typescript
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 });
}
}
}