@cloudpss/template
Version:
Lightweight string and object templating utilities with interpolation and formula support.
122 lines (118 loc) • 4.49 kB
text/typescript
import { unwrapFromVmValue } from '@mirascript/mirascript';
import { buildError } from './builder.js';
import type { TemplateFunction, TemplateOptions } from './index.js';
import { parseTemplate } from './parser.js';
import {
hasOwn,
isError,
isArrayBuffer,
copyArrayBuffer,
stringify,
toString,
isArrayBufferView,
isArray,
} from './utils.js';
/** 模板序列号 */
let seq = 0;
const $init = [unwrapFromVmValue] as const;
const $unwrapFromVmValue = 0;
/** 创建模板 */
export class TemplateCompiler {
constructor(
readonly template: unknown,
readonly options: Required<TemplateOptions>,
) {}
private readonly $: unknown[] = [...$init];
/** 放入 $,返回索引 */
private use(value: unknown): number {
this.$.push(value);
return this.$.length - 1;
}
/** 构建数组 */
private buildArray(arr: unknown[]): string {
let result = '[';
for (let i = 0, len = arr.length; i < len; i++) {
result += this.buildValue(arr[i]);
result += ',\n';
}
result += ']';
return result;
}
/** 构建 ArrayBuffer */
private buildArrayBuffer(buffer: ArrayBuffer | SharedArrayBuffer): string {
const copy = copyArrayBuffer(buffer, 0);
return `$[${this.use(copy)}].slice(0)`;
}
/** 构建 ArrayBufferView */
private buildArrayBufferView(view: ArrayBufferView): string {
const copy = copyArrayBuffer(view.buffer, view.byteOffset, view.byteLength);
return `new ${view.constructor.name}($[${this.use(copy)}].slice(0))`;
}
/** 构建可能为模板的字符串 */
private buildTemplate(value: string): string {
const template = parseTemplate(value);
if (typeof template == 'string') {
return stringify(template);
} else {
return `$[${$unwrapFromVmValue}]($[${this.use(template.value)}](context))`;
}
}
/** 构建对象 */
private buildObject(obj: Record<string, unknown>): string {
let result = '{';
for (const key in obj) {
if (!hasOwn(obj, key)) continue;
result += '[';
if (this.options.objectKeyMode === 'ignore') {
result += stringify(key);
} else {
result += this.buildTemplate(key);
}
result += ']:';
const value = obj[key];
result += this.buildValue(value);
result += ',\n';
}
result += '}';
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.buildTemplate(value);
/* 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 buildError(value);
if (isArray(value)) return this.buildArray(value);
if (isArrayBuffer(value)) return this.buildArrayBuffer(value);
if (isArrayBufferView(value)) return this.buildArrayBufferView(value);
return this.buildObject(value as Record<string, unknown>);
}
/** 构建模板 */
build(): TemplateFunction {
const source = this.buildValue(this.template);
try {
// eslint-disable-next-line @typescript-eslint/no-implied-eval, @typescript-eslint/no-unsafe-call
const result = new Function(
'$',
[
`//# sourceURL=cloudpss-template[${seq++}]`, // sourceURL 用于调试
`return (context) => (${source});`,
].join('\n'),
)(this.$) as TemplateFunction;
return result;
/* c8 ignore next 3 */
} catch (e) {
throw new Error(`Failed to compile template: ${source}\n${(e as Error).message}`, { cause: e });
}
}
}