UNPKG

@cloudpss/template

Version:

Lightweight string and object templating utilities with interpolation and formula support.

122 lines (118 loc) 4.49 kB
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 }); } } }