@cloudpss/template 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.js +128 -0
- package/dist/parser.js.map +1 -0
- package/dist/template/compiler.d.ts +27 -0
- package/dist/template/compiler.js +157 -0
- package/dist/template/compiler.js.map +1 -0
- package/dist/template/index.d.ts +30 -0
- package/dist/template/index.js +25 -0
- package/dist/template/index.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +22 -0
- package/src/index.ts +4 -0
- package/src/parser.ts +143 -0
- package/src/template/compiler.ts +138 -0
- package/src/template/index.ts +59 -0
- package/tests/parser.js +184 -0
- package/tests/template.js +261 -0
- package/tests/tsconfig.json +8 -0
- package/tsconfig.json +7 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 字符串模板 */
|
|
2
|
+
export type Template = string | {
|
|
3
|
+
type: 'formula';
|
|
4
|
+
value: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'interpolation';
|
|
7
|
+
templates: string[];
|
|
8
|
+
values: string[];
|
|
9
|
+
};
|
|
10
|
+
/** 字符串模板类型 */
|
|
11
|
+
export type TemplateType = (Template & object)['type'];
|
|
12
|
+
/**
|
|
13
|
+
* 解析字符串模板
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseTemplate(template: string): Template;
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
var ParserStatus;
|
|
2
|
+
(function (ParserStatus) {
|
|
3
|
+
ParserStatus[ParserStatus["Text"] = 0] = "Text";
|
|
4
|
+
ParserStatus[ParserStatus["ExpressionSimple"] = 1] = "ExpressionSimple";
|
|
5
|
+
ParserStatus[ParserStatus["ExpressionComplex"] = 2] = "ExpressionComplex";
|
|
6
|
+
})(ParserStatus || (ParserStatus = {}));
|
|
7
|
+
Object.freeze(ParserStatus);
|
|
8
|
+
/**
|
|
9
|
+
* 解析字符串插值模板
|
|
10
|
+
* @example parseInterpolation("$hello $name! I am $age years old. I'll be ${age+1} next year. Give me $$100.")
|
|
11
|
+
* // {
|
|
12
|
+
* // type: 'interpolation',
|
|
13
|
+
* // templates: ['hello ', '! I am ', ' years old. I\'ll be ', ' next year. Give me $$100.'],
|
|
14
|
+
* // values: ['name', 'age', 'age+1']
|
|
15
|
+
* // }
|
|
16
|
+
*/
|
|
17
|
+
function parseInterpolation(template) {
|
|
18
|
+
const templates = [];
|
|
19
|
+
const values = [];
|
|
20
|
+
let currentTemplate = '';
|
|
21
|
+
let currentValue = '';
|
|
22
|
+
let expressionComplexDepth = 0;
|
|
23
|
+
let status = ParserStatus.Text;
|
|
24
|
+
for (let i = 1; i < template.length; i++) {
|
|
25
|
+
const char = template[i];
|
|
26
|
+
if (status === ParserStatus.Text) {
|
|
27
|
+
if (char !== '$') {
|
|
28
|
+
currentTemplate += char;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (i === template.length - 1) {
|
|
32
|
+
// End of input
|
|
33
|
+
currentTemplate += char;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const nextChar = template[i + 1];
|
|
37
|
+
if (nextChar === '$') {
|
|
38
|
+
// Escaped dollar sign
|
|
39
|
+
currentTemplate += char;
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (nextChar === '{') {
|
|
44
|
+
// Start of expression
|
|
45
|
+
templates.push(currentTemplate);
|
|
46
|
+
currentTemplate = '';
|
|
47
|
+
status = ParserStatus.ExpressionComplex;
|
|
48
|
+
expressionComplexDepth = 1;
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/[a-zA-Z_]/.exec(nextChar)) {
|
|
53
|
+
// Start of expression
|
|
54
|
+
templates.push(currentTemplate);
|
|
55
|
+
currentTemplate = '';
|
|
56
|
+
status = ParserStatus.ExpressionSimple;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Not an expression
|
|
60
|
+
currentTemplate += char;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (status === ParserStatus.ExpressionSimple) {
|
|
64
|
+
if (/[a-zA-Z0-9_]/.exec(char)) {
|
|
65
|
+
currentValue += char;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// End of expression
|
|
69
|
+
values.push(currentValue);
|
|
70
|
+
currentValue = '';
|
|
71
|
+
status = ParserStatus.Text;
|
|
72
|
+
i--;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (status === ParserStatus.ExpressionComplex) {
|
|
76
|
+
if (char === '{') {
|
|
77
|
+
expressionComplexDepth++;
|
|
78
|
+
}
|
|
79
|
+
else if (char === '}') {
|
|
80
|
+
expressionComplexDepth--;
|
|
81
|
+
if (expressionComplexDepth === 0) {
|
|
82
|
+
// End of expression
|
|
83
|
+
values.push(currentValue.trim());
|
|
84
|
+
currentValue = '';
|
|
85
|
+
status = ParserStatus.Text;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
currentValue += char;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (status === ParserStatus.Text) {
|
|
94
|
+
templates.push(currentTemplate);
|
|
95
|
+
}
|
|
96
|
+
else if (status === ParserStatus.ExpressionSimple) {
|
|
97
|
+
values.push(currentValue);
|
|
98
|
+
templates.push('');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new Error('Unexpected end of input');
|
|
102
|
+
}
|
|
103
|
+
if (values.length === 0)
|
|
104
|
+
return templates[0];
|
|
105
|
+
return {
|
|
106
|
+
type: 'interpolation',
|
|
107
|
+
templates,
|
|
108
|
+
values,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 解析字符串模板
|
|
113
|
+
*/
|
|
114
|
+
export function parseTemplate(template) {
|
|
115
|
+
if (!template)
|
|
116
|
+
return '';
|
|
117
|
+
if (template.startsWith('=')) {
|
|
118
|
+
return {
|
|
119
|
+
type: 'formula',
|
|
120
|
+
value: template.slice(1).trim(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (template.startsWith('$')) {
|
|
124
|
+
return parseInterpolation(template);
|
|
125
|
+
}
|
|
126
|
+
return template;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TemplateFunction, TemplateOptions } from './index.js';
|
|
2
|
+
/** 创建模板 */
|
|
3
|
+
export declare class TemplateCompiler {
|
|
4
|
+
readonly template: unknown;
|
|
5
|
+
readonly options: Required<TemplateOptions>;
|
|
6
|
+
constructor(template: unknown, options: Required<TemplateOptions>);
|
|
7
|
+
private readonly params;
|
|
8
|
+
private readonly copyable;
|
|
9
|
+
/** 构建求值 */
|
|
10
|
+
private buildEval;
|
|
11
|
+
/** 构建字符串 */
|
|
12
|
+
private buildString;
|
|
13
|
+
/** 构建 Error */
|
|
14
|
+
private buildError;
|
|
15
|
+
/** 构建数组 */
|
|
16
|
+
private buildArray;
|
|
17
|
+
/** 构建 ArrayBuffer */
|
|
18
|
+
private buildArrayBuffer;
|
|
19
|
+
/** 构建 ArrayBufferView */
|
|
20
|
+
private buildArrayBufferView;
|
|
21
|
+
/** 构建对象 */
|
|
22
|
+
private buildObject;
|
|
23
|
+
/** 构建值 */
|
|
24
|
+
private buildValue;
|
|
25
|
+
/** 构建模板 */
|
|
26
|
+
build(): TemplateFunction;
|
|
27
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { parseTemplate } from '../parser.js';
|
|
2
|
+
/** 是否为 ArrayBuffer */
|
|
3
|
+
function isArrayBuffer(value) {
|
|
4
|
+
if (value instanceof ArrayBuffer)
|
|
5
|
+
return true;
|
|
6
|
+
if (typeof SharedArrayBuffer == 'function' && value instanceof SharedArrayBuffer)
|
|
7
|
+
return true;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
/** 是否为 Error */
|
|
11
|
+
function isError(value) {
|
|
12
|
+
return value instanceof Error || (typeof DOMException == 'function' && value instanceof DOMException);
|
|
13
|
+
}
|
|
14
|
+
const KNOWN_ERRORS = [EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError];
|
|
15
|
+
/** 创建模板 */
|
|
16
|
+
export class TemplateCompiler {
|
|
17
|
+
template;
|
|
18
|
+
options;
|
|
19
|
+
constructor(template, options) {
|
|
20
|
+
this.template = template;
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
params = new Map();
|
|
24
|
+
copyable = [];
|
|
25
|
+
/** 构建求值 */
|
|
26
|
+
buildEval(expression, type) {
|
|
27
|
+
const { evaluator } = this.options;
|
|
28
|
+
if (!this.params.has('evaluator')) {
|
|
29
|
+
this.params.set('evaluator', evaluator.inject);
|
|
30
|
+
}
|
|
31
|
+
return evaluator.compile(expression, type);
|
|
32
|
+
}
|
|
33
|
+
/** 构建字符串 */
|
|
34
|
+
buildString(str) {
|
|
35
|
+
const parsed = parseTemplate(str);
|
|
36
|
+
if (typeof parsed === 'string')
|
|
37
|
+
return JSON.stringify(parsed);
|
|
38
|
+
if (parsed.type === 'formula')
|
|
39
|
+
return this.buildEval(parsed.value, parsed.type);
|
|
40
|
+
let result = '';
|
|
41
|
+
for (let i = 0; i < parsed.templates.length; i++) {
|
|
42
|
+
if (parsed.templates[i]) {
|
|
43
|
+
result += (result ? '+' : '') + JSON.stringify(parsed.templates[i]);
|
|
44
|
+
}
|
|
45
|
+
if (i < parsed.values.length) {
|
|
46
|
+
if (!result)
|
|
47
|
+
result = '""';
|
|
48
|
+
result += '+' + this.buildEval(parsed.values[i], parsed.type);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return '(' + result + ')';
|
|
52
|
+
}
|
|
53
|
+
/** 构建 Error */
|
|
54
|
+
buildError(err) {
|
|
55
|
+
if (typeof DOMException == 'function' && err instanceof DOMException) {
|
|
56
|
+
return `new DOMException(${this.buildString(err.message)}, ${this.buildString(err.name)})`;
|
|
57
|
+
}
|
|
58
|
+
const constructor = KNOWN_ERRORS.find((type) => err instanceof type)?.name ?? 'Error';
|
|
59
|
+
if (err.name === constructor) {
|
|
60
|
+
return `new ${constructor}(${this.buildString(err.message)})`;
|
|
61
|
+
}
|
|
62
|
+
return `Object.assign(new ${constructor}(${this.buildString(err.message)}), {name: ${this.buildString(err.name)}})`;
|
|
63
|
+
}
|
|
64
|
+
/** 构建数组 */
|
|
65
|
+
buildArray(arr) {
|
|
66
|
+
return `[${arr.map(this.buildValue.bind(this)).join(',')}]`;
|
|
67
|
+
}
|
|
68
|
+
/** 构建 ArrayBuffer */
|
|
69
|
+
buildArrayBuffer(buffer) {
|
|
70
|
+
this.copyable.push(buffer.slice(0));
|
|
71
|
+
return `copyable[${this.copyable.length - 1}].slice(0)`;
|
|
72
|
+
}
|
|
73
|
+
/** 构建 ArrayBufferView */
|
|
74
|
+
buildArrayBufferView(view) {
|
|
75
|
+
this.copyable.push(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
76
|
+
return `new ${view.constructor.name}(copyable[${this.copyable.length - 1}].slice(0))`;
|
|
77
|
+
}
|
|
78
|
+
/** 构建对象 */
|
|
79
|
+
buildObject(obj) {
|
|
80
|
+
let result = '';
|
|
81
|
+
for (const key in obj) {
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key))
|
|
83
|
+
continue;
|
|
84
|
+
const value = obj[key];
|
|
85
|
+
if (result)
|
|
86
|
+
result += ',';
|
|
87
|
+
if (this.options.objectKeyMode === 'ignore') {
|
|
88
|
+
result += JSON.stringify(key);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
result += '[' + this.buildString(key) + ']';
|
|
92
|
+
}
|
|
93
|
+
result += ':';
|
|
94
|
+
result += this.buildValue(value);
|
|
95
|
+
}
|
|
96
|
+
return '{' + result + '}';
|
|
97
|
+
}
|
|
98
|
+
/** 构建值 */
|
|
99
|
+
buildValue(value) {
|
|
100
|
+
if (value === null)
|
|
101
|
+
return 'null';
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return 'undefined';
|
|
104
|
+
if (value === true)
|
|
105
|
+
return 'true';
|
|
106
|
+
if (value === false)
|
|
107
|
+
return 'false';
|
|
108
|
+
if (typeof value == 'function')
|
|
109
|
+
return 'undefined';
|
|
110
|
+
if (typeof value == 'symbol')
|
|
111
|
+
return 'undefined';
|
|
112
|
+
if (typeof value == 'bigint')
|
|
113
|
+
return `${value}n`;
|
|
114
|
+
if (typeof value == 'number')
|
|
115
|
+
return String(value);
|
|
116
|
+
if (typeof value == 'string')
|
|
117
|
+
return this.buildString(value);
|
|
118
|
+
/* c8 ignore next */
|
|
119
|
+
if (typeof value != 'object')
|
|
120
|
+
throw new Error(`Unsupported value: ${Object.prototype.toString.call(value)}`);
|
|
121
|
+
if (value instanceof Date)
|
|
122
|
+
return `new Date(${value.getTime()})`;
|
|
123
|
+
if (value instanceof RegExp)
|
|
124
|
+
return value.toString();
|
|
125
|
+
if (isError(value))
|
|
126
|
+
return this.buildError(value);
|
|
127
|
+
if (Array.isArray(value))
|
|
128
|
+
return this.buildArray(value);
|
|
129
|
+
if (isArrayBuffer(value))
|
|
130
|
+
return this.buildArrayBuffer(value);
|
|
131
|
+
if (ArrayBuffer.isView(value))
|
|
132
|
+
return this.buildArrayBufferView(value);
|
|
133
|
+
return this.buildObject(value);
|
|
134
|
+
}
|
|
135
|
+
/** 构建模板 */
|
|
136
|
+
build() {
|
|
137
|
+
const source = this.buildValue(this.template);
|
|
138
|
+
if (this.copyable.length) {
|
|
139
|
+
this.params.set('copyable', this.copyable);
|
|
140
|
+
}
|
|
141
|
+
const params = [...this.params];
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
144
|
+
const result = new Function(...params.map(([key]) => key), 'context', `
|
|
145
|
+
if (context == null) context = {};
|
|
146
|
+
return (${source});
|
|
147
|
+
`).bind(undefined, ...params.map(([, value]) => value));
|
|
148
|
+
result.source = source;
|
|
149
|
+
return result;
|
|
150
|
+
/* c8 ignore next 3 */
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
throw new Error(`Failed to compile template: ${source}\n${e.message}`, { cause: e });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=compiler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler.js","sourceRoot":"","sources":["../../src/template/compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TemplateType } from '../parser.js';
|
|
2
|
+
/** 模板编译求值 */
|
|
3
|
+
export type TemplateEvaluator = {
|
|
4
|
+
/** 注入为 `evaluator` 变量,可在生成的求值代码中使用 */
|
|
5
|
+
inject: unknown;
|
|
6
|
+
/** 生成求值代码,可用变量:`evaluator` `context` */
|
|
7
|
+
compile: (expression: string, type: TemplateType) => string;
|
|
8
|
+
};
|
|
9
|
+
/** 模板选项 */
|
|
10
|
+
export interface TemplateOptions {
|
|
11
|
+
/**
|
|
12
|
+
* 模板求值器
|
|
13
|
+
* @default context?.[expression] ?? (type === 'formula' ? undefined : '')
|
|
14
|
+
*/
|
|
15
|
+
evaluator?: TemplateEvaluator;
|
|
16
|
+
/**
|
|
17
|
+
* 对 object key 的处理方式
|
|
18
|
+
* - `template` 使用模板进行插值
|
|
19
|
+
* - `ignore` 原样输出
|
|
20
|
+
* @default 'template'
|
|
21
|
+
*/
|
|
22
|
+
objectKeyMode?: 'template' | 'ignore';
|
|
23
|
+
}
|
|
24
|
+
export declare const defaultEvaluator: TemplateEvaluator;
|
|
25
|
+
/** 已编译的模板函数 */
|
|
26
|
+
export type TemplateFunction<T = unknown, C = Record<string, unknown>> = ((context?: C) => T) & {
|
|
27
|
+
source: string;
|
|
28
|
+
};
|
|
29
|
+
/** 创建模板 */
|
|
30
|
+
export declare function template<T = unknown, C = Record<string, unknown>>(template: T, options?: TemplateOptions): TemplateFunction<T, C>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TemplateCompiler } from './compiler.js';
|
|
2
|
+
export const defaultEvaluator = {
|
|
3
|
+
inject: undefined,
|
|
4
|
+
compile: (expression, type) => {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case 'formula':
|
|
7
|
+
return `context[${JSON.stringify(expression)}]`;
|
|
8
|
+
case 'interpolation':
|
|
9
|
+
return `(context[${JSON.stringify(expression)}] ?? '')`;
|
|
10
|
+
/* c8 ignore next 2 */
|
|
11
|
+
default:
|
|
12
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
/** 创建模板 */
|
|
17
|
+
export function template(template, options = {}) {
|
|
18
|
+
const opt = {
|
|
19
|
+
objectKeyMode: 'template',
|
|
20
|
+
evaluator: defaultEvaluator,
|
|
21
|
+
...options,
|
|
22
|
+
};
|
|
23
|
+
return new TemplateCompiler(template, opt).build();
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/template/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AA0BjD,MAAM,CAAC,MAAM,gBAAgB,GAAsB;IAC/C,MAAM,EAAE,SAAS;IACjB,OAAO,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE;QAC1B,QAAQ,IAAI,EAAE,CAAC;YACX,KAAK,SAAS;gBACV,OAAO,WAAW,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC;YACpD,KAAK,eAAe;gBAChB,OAAO,YAAY,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC;YAC5D,sBAAsB;YACtB;gBACI,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAA8B,EAAE,CAAC,CAAC;QAC/E,CAAC;IACL,CAAC;CACJ,CAAC;AAOF,WAAW;AACX,MAAM,UAAU,QAAQ,CACpB,QAAW,EACX,UAA2B,EAAE;IAE7B,MAAM,GAAG,GAAG;QACR,aAAa,EAAE,UAAU;QACzB,SAAS,EAAE,gBAAgB;QAC3B,GAAG,OAAO;KACb,CAAC;IACF,OAAO,IAAI,gBAAgB,CAAC,QAAQ,EAAE,GAAgC,CAAC,CAAC,KAAK,EAA4B,CAAC;AAC9G,CAAC"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudpss/template",
|
|
3
|
+
"version": "0.5.23",
|
|
4
|
+
"author": "CloudPSS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"template",
|
|
8
|
+
"string-template",
|
|
9
|
+
"template-string"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"module": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"exports": "./dist/index.js",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "pnpm clean && tsc --watch",
|
|
18
|
+
"build": "pnpm clean && tsc",
|
|
19
|
+
"clean": "rimraf dist",
|
|
20
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --experimental-vm-modules\" jest"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
package/src/parser.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** 字符串模板 */
|
|
2
|
+
export type Template =
|
|
3
|
+
| string
|
|
4
|
+
| {
|
|
5
|
+
type: 'formula';
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
type: 'interpolation';
|
|
10
|
+
templates: string[];
|
|
11
|
+
values: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** 字符串模板类型 */
|
|
15
|
+
export type TemplateType = (Template & object)['type'];
|
|
16
|
+
|
|
17
|
+
enum ParserStatus {
|
|
18
|
+
Text,
|
|
19
|
+
ExpressionSimple,
|
|
20
|
+
ExpressionComplex,
|
|
21
|
+
}
|
|
22
|
+
Object.freeze(ParserStatus);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析字符串插值模板
|
|
26
|
+
* @example parseInterpolation("$hello $name! I am $age years old. I'll be ${age+1} next year. Give me $$100.")
|
|
27
|
+
* // {
|
|
28
|
+
* // type: 'interpolation',
|
|
29
|
+
* // templates: ['hello ', '! I am ', ' years old. I\'ll be ', ' next year. Give me $$100.'],
|
|
30
|
+
* // values: ['name', 'age', 'age+1']
|
|
31
|
+
* // }
|
|
32
|
+
*/
|
|
33
|
+
function parseInterpolation(template: string): Template {
|
|
34
|
+
const templates: string[] = [];
|
|
35
|
+
const values: string[] = [];
|
|
36
|
+
let currentTemplate = '';
|
|
37
|
+
let currentValue = '';
|
|
38
|
+
let expressionComplexDepth = 0;
|
|
39
|
+
let status = ParserStatus.Text;
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i < template.length; i++) {
|
|
42
|
+
const char = template[i];
|
|
43
|
+
|
|
44
|
+
if (status === ParserStatus.Text) {
|
|
45
|
+
if (char !== '$') {
|
|
46
|
+
currentTemplate += char;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (i === template.length - 1) {
|
|
50
|
+
// End of input
|
|
51
|
+
currentTemplate += char;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const nextChar = template[i + 1];
|
|
55
|
+
if (nextChar === '$') {
|
|
56
|
+
// Escaped dollar sign
|
|
57
|
+
currentTemplate += char;
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (nextChar === '{') {
|
|
62
|
+
// Start of expression
|
|
63
|
+
templates.push(currentTemplate);
|
|
64
|
+
currentTemplate = '';
|
|
65
|
+
status = ParserStatus.ExpressionComplex;
|
|
66
|
+
expressionComplexDepth = 1;
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (/[a-zA-Z_]/.exec(nextChar)) {
|
|
71
|
+
// Start of expression
|
|
72
|
+
templates.push(currentTemplate);
|
|
73
|
+
currentTemplate = '';
|
|
74
|
+
status = ParserStatus.ExpressionSimple;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Not an expression
|
|
78
|
+
currentTemplate += char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (status === ParserStatus.ExpressionSimple) {
|
|
82
|
+
if (/[a-zA-Z0-9_]/.exec(char)) {
|
|
83
|
+
currentValue += char;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// End of expression
|
|
87
|
+
values.push(currentValue);
|
|
88
|
+
currentValue = '';
|
|
89
|
+
status = ParserStatus.Text;
|
|
90
|
+
i--;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (status === ParserStatus.ExpressionComplex) {
|
|
94
|
+
if (char === '{') {
|
|
95
|
+
expressionComplexDepth++;
|
|
96
|
+
} else if (char === '}') {
|
|
97
|
+
expressionComplexDepth--;
|
|
98
|
+
if (expressionComplexDepth === 0) {
|
|
99
|
+
// End of expression
|
|
100
|
+
values.push(currentValue.trim());
|
|
101
|
+
currentValue = '';
|
|
102
|
+
status = ParserStatus.Text;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
currentValue += char;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (status === ParserStatus.Text) {
|
|
111
|
+
templates.push(currentTemplate);
|
|
112
|
+
} else if (status === ParserStatus.ExpressionSimple) {
|
|
113
|
+
values.push(currentValue);
|
|
114
|
+
templates.push('');
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error('Unexpected end of input');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (values.length === 0) return templates[0];
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: 'interpolation',
|
|
123
|
+
templates,
|
|
124
|
+
values,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 解析字符串模板
|
|
130
|
+
*/
|
|
131
|
+
export function parseTemplate(template: string): Template {
|
|
132
|
+
if (!template) return '';
|
|
133
|
+
if (template.startsWith('=')) {
|
|
134
|
+
return {
|
|
135
|
+
type: 'formula',
|
|
136
|
+
value: template.slice(1).trim(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (template.startsWith('$')) {
|
|
140
|
+
return parseInterpolation(template);
|
|
141
|
+
}
|
|
142
|
+
return template;
|
|
143
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { parseTemplate, type TemplateType } from '../parser.js';
|
|
2
|
+
import type { TemplateFunction, TemplateOptions } from './index.js';
|
|
3
|
+
|
|
4
|
+
/** 是否为 ArrayBuffer */
|
|
5
|
+
function isArrayBuffer(value: object): value is ArrayBuffer | SharedArrayBuffer {
|
|
6
|
+
if (value instanceof ArrayBuffer) return true;
|
|
7
|
+
if (typeof SharedArrayBuffer == 'function' && value instanceof SharedArrayBuffer) return true;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 是否为 Error */
|
|
12
|
+
function isError(value: unknown): value is Error {
|
|
13
|
+
return value instanceof Error || (typeof DOMException == 'function' && value instanceof DOMException);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const KNOWN_ERRORS = [EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError] as const;
|
|
17
|
+
|
|
18
|
+
/** 创建模板 */
|
|
19
|
+
export class TemplateCompiler {
|
|
20
|
+
constructor(
|
|
21
|
+
readonly template: unknown,
|
|
22
|
+
readonly options: Required<TemplateOptions>,
|
|
23
|
+
) {}
|
|
24
|
+
private readonly params = new Map<string, unknown>();
|
|
25
|
+
private readonly copyable: unknown[] = [];
|
|
26
|
+
/** 构建求值 */
|
|
27
|
+
private buildEval(expression: string, type: TemplateType): string {
|
|
28
|
+
const { evaluator } = this.options;
|
|
29
|
+
if (!this.params.has('evaluator')) {
|
|
30
|
+
this.params.set('evaluator', evaluator.inject);
|
|
31
|
+
}
|
|
32
|
+
return evaluator.compile(expression, type);
|
|
33
|
+
}
|
|
34
|
+
/** 构建字符串 */
|
|
35
|
+
private buildString(str: string): string {
|
|
36
|
+
const parsed = parseTemplate(str);
|
|
37
|
+
if (typeof parsed === 'string') return JSON.stringify(parsed);
|
|
38
|
+
if (parsed.type === 'formula') return this.buildEval(parsed.value, parsed.type);
|
|
39
|
+
let result = '';
|
|
40
|
+
for (let i = 0; i < parsed.templates.length; i++) {
|
|
41
|
+
if (parsed.templates[i]) {
|
|
42
|
+
result += (result ? '+' : '') + JSON.stringify(parsed.templates[i]);
|
|
43
|
+
}
|
|
44
|
+
if (i < parsed.values.length) {
|
|
45
|
+
if (!result) result = '""';
|
|
46
|
+
result += '+' + this.buildEval(parsed.values[i], parsed.type);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return '(' + result + ')';
|
|
50
|
+
}
|
|
51
|
+
/** 构建 Error */
|
|
52
|
+
private buildError(err: Error): string {
|
|
53
|
+
if (typeof DOMException == 'function' && err instanceof DOMException) {
|
|
54
|
+
return `new DOMException(${this.buildString(err.message)}, ${this.buildString(err.name)})`;
|
|
55
|
+
}
|
|
56
|
+
const constructor = KNOWN_ERRORS.find((type) => err instanceof type)?.name ?? 'Error';
|
|
57
|
+
if (err.name === constructor) {
|
|
58
|
+
return `new ${constructor}(${this.buildString(err.message)})`;
|
|
59
|
+
}
|
|
60
|
+
return `Object.assign(new ${constructor}(${this.buildString(err.message)}), {name: ${this.buildString(err.name)}})`;
|
|
61
|
+
}
|
|
62
|
+
/** 构建数组 */
|
|
63
|
+
private buildArray(arr: unknown[]): string {
|
|
64
|
+
return `[${arr.map(this.buildValue.bind(this)).join(',')}]`;
|
|
65
|
+
}
|
|
66
|
+
/** 构建 ArrayBuffer */
|
|
67
|
+
private buildArrayBuffer(buffer: ArrayBuffer | SharedArrayBuffer): string {
|
|
68
|
+
this.copyable.push(buffer.slice(0));
|
|
69
|
+
return `copyable[${this.copyable.length - 1}].slice(0)`;
|
|
70
|
+
}
|
|
71
|
+
/** 构建 ArrayBufferView */
|
|
72
|
+
private buildArrayBufferView(view: ArrayBufferView): string {
|
|
73
|
+
this.copyable.push(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
|
|
74
|
+
return `new ${view.constructor.name}(copyable[${this.copyable.length - 1}].slice(0))`;
|
|
75
|
+
}
|
|
76
|
+
/** 构建对象 */
|
|
77
|
+
private buildObject(obj: Record<string, unknown>): string {
|
|
78
|
+
let result = '';
|
|
79
|
+
for (const key in obj) {
|
|
80
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
81
|
+
const value = obj[key];
|
|
82
|
+
if (result) result += ',';
|
|
83
|
+
if (this.options.objectKeyMode === 'ignore') {
|
|
84
|
+
result += JSON.stringify(key);
|
|
85
|
+
} else {
|
|
86
|
+
result += '[' + this.buildString(key) + ']';
|
|
87
|
+
}
|
|
88
|
+
result += ':';
|
|
89
|
+
result += this.buildValue(value);
|
|
90
|
+
}
|
|
91
|
+
return '{' + result + '}';
|
|
92
|
+
}
|
|
93
|
+
/** 构建值 */
|
|
94
|
+
private buildValue(value: unknown): string {
|
|
95
|
+
if (value === null) return 'null';
|
|
96
|
+
if (value === undefined) return 'undefined';
|
|
97
|
+
if (value === true) return 'true';
|
|
98
|
+
if (value === false) return 'false';
|
|
99
|
+
if (typeof value == 'function') return 'undefined';
|
|
100
|
+
if (typeof value == 'symbol') return 'undefined';
|
|
101
|
+
if (typeof value == 'bigint') return `${value}n`;
|
|
102
|
+
if (typeof value == 'number') return String(value);
|
|
103
|
+
if (typeof value == 'string') return this.buildString(value);
|
|
104
|
+
/* c8 ignore next */
|
|
105
|
+
if (typeof value != 'object') throw new Error(`Unsupported value: ${Object.prototype.toString.call(value)}`);
|
|
106
|
+
if (value instanceof Date) return `new Date(${value.getTime()})`;
|
|
107
|
+
if (value instanceof RegExp) return value.toString();
|
|
108
|
+
if (isError(value)) return this.buildError(value);
|
|
109
|
+
if (Array.isArray(value)) return this.buildArray(value);
|
|
110
|
+
if (isArrayBuffer(value)) return this.buildArrayBuffer(value);
|
|
111
|
+
if (ArrayBuffer.isView(value)) return this.buildArrayBufferView(value);
|
|
112
|
+
return this.buildObject(value as Record<string, unknown>);
|
|
113
|
+
}
|
|
114
|
+
/** 构建模板 */
|
|
115
|
+
build(): TemplateFunction {
|
|
116
|
+
const source = this.buildValue(this.template);
|
|
117
|
+
if (this.copyable.length) {
|
|
118
|
+
this.params.set('copyable', this.copyable);
|
|
119
|
+
}
|
|
120
|
+
const params = [...this.params];
|
|
121
|
+
try {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
123
|
+
const result = new Function(
|
|
124
|
+
...params.map(([key]) => key),
|
|
125
|
+
'context',
|
|
126
|
+
`
|
|
127
|
+
if (context == null) context = {};
|
|
128
|
+
return (${source});
|
|
129
|
+
`,
|
|
130
|
+
).bind(undefined, ...params.map(([, value]) => value)) as TemplateFunction;
|
|
131
|
+
result.source = source;
|
|
132
|
+
return result;
|
|
133
|
+
/* c8 ignore next 3 */
|
|
134
|
+
} catch (e) {
|
|
135
|
+
throw new Error(`Failed to compile template: ${source}\n${(e as Error).message}`, { cause: e });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { TemplateType } from '../parser.js';
|
|
2
|
+
import { TemplateCompiler } from './compiler.js';
|
|
3
|
+
|
|
4
|
+
/** 模板编译求值 */
|
|
5
|
+
export type TemplateEvaluator = {
|
|
6
|
+
/** 注入为 `evaluator` 变量,可在生成的求值代码中使用 */
|
|
7
|
+
inject: unknown;
|
|
8
|
+
/** 生成求值代码,可用变量:`evaluator` `context` */
|
|
9
|
+
compile: (expression: string, type: TemplateType) => string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** 模板选项 */
|
|
13
|
+
export interface TemplateOptions {
|
|
14
|
+
/**
|
|
15
|
+
* 模板求值器
|
|
16
|
+
* @default context?.[expression] ?? (type === 'formula' ? undefined : '')
|
|
17
|
+
*/
|
|
18
|
+
evaluator?: TemplateEvaluator;
|
|
19
|
+
/**
|
|
20
|
+
* 对 object key 的处理方式
|
|
21
|
+
* - `template` 使用模板进行插值
|
|
22
|
+
* - `ignore` 原样输出
|
|
23
|
+
* @default 'template'
|
|
24
|
+
*/
|
|
25
|
+
objectKeyMode?: 'template' | 'ignore';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const defaultEvaluator: TemplateEvaluator = {
|
|
29
|
+
inject: undefined,
|
|
30
|
+
compile: (expression, type) => {
|
|
31
|
+
switch (type) {
|
|
32
|
+
case 'formula':
|
|
33
|
+
return `context[${JSON.stringify(expression)}]`;
|
|
34
|
+
case 'interpolation':
|
|
35
|
+
return `(context[${JSON.stringify(expression)}] ?? '')`;
|
|
36
|
+
/* c8 ignore next 2 */
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unsupported type: ${type satisfies never as string}`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** 已编译的模板函数 */
|
|
44
|
+
export type TemplateFunction<T = unknown, C = Record<string, unknown>> = ((context?: C) => T) & {
|
|
45
|
+
source: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** 创建模板 */
|
|
49
|
+
export function template<T = unknown, C = Record<string, unknown>>(
|
|
50
|
+
template: T,
|
|
51
|
+
options: TemplateOptions = {},
|
|
52
|
+
): TemplateFunction<T, C> {
|
|
53
|
+
const opt = {
|
|
54
|
+
objectKeyMode: 'template',
|
|
55
|
+
evaluator: defaultEvaluator,
|
|
56
|
+
...options,
|
|
57
|
+
};
|
|
58
|
+
return new TemplateCompiler(template, opt as Required<TemplateOptions>).build() as TemplateFunction<T, C>;
|
|
59
|
+
}
|
package/tests/parser.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { parseTemplate } from '../dist/parser.js';
|
|
2
|
+
|
|
3
|
+
describe('parseTemplate', () => {
|
|
4
|
+
describe('should parse plain', () => {
|
|
5
|
+
it('empty string', () => {
|
|
6
|
+
// @ts-expect-error test
|
|
7
|
+
expect(parseTemplate(null)).toEqual('');
|
|
8
|
+
// @ts-expect-error test
|
|
9
|
+
expect(parseTemplate(undefined)).toEqual('');
|
|
10
|
+
expect(parseTemplate('')).toEqual('');
|
|
11
|
+
});
|
|
12
|
+
it('string', () => {
|
|
13
|
+
expect(parseTemplate('Hello, world!')).toEqual('Hello, world!');
|
|
14
|
+
expect(parseTemplate('\u0000')).toEqual('\u0000');
|
|
15
|
+
expect(parseTemplate(' ')).toEqual(' ');
|
|
16
|
+
// bad surrogate pair
|
|
17
|
+
expect(parseTemplate('\uD800')).toEqual('\uD800');
|
|
18
|
+
expect(parseTemplate('\uDC00')).toEqual('\uDC00');
|
|
19
|
+
});
|
|
20
|
+
it('template like (but not)', () => {
|
|
21
|
+
expect(parseTemplate(' = 1+1')).toEqual(' = 1+1');
|
|
22
|
+
expect(parseTemplate(' =1+1')).toEqual(' =1+1');
|
|
23
|
+
expect(parseTemplate(' =1+1 ')).toEqual(' =1+1 ');
|
|
24
|
+
expect(parseTemplate(' =1+1 ')).toEqual(' =1+1 ');
|
|
25
|
+
expect(parseTemplate(' $aa')).toEqual(' $aa');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should parse formulas', () => {
|
|
30
|
+
expect(parseTemplate('= 1+1')).toEqual({ type: 'formula', value: '1+1' });
|
|
31
|
+
expect(parseTemplate('= 1+1 ')).toEqual({ type: 'formula', value: '1+1' });
|
|
32
|
+
expect(parseTemplate('=arg')).toEqual({ type: 'formula', value: 'arg' });
|
|
33
|
+
expect(parseTemplate('=arg1+arg2')).toEqual({ type: 'formula', value: 'arg1+arg2' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('should parse interpolation', () => {
|
|
37
|
+
it('optimize for no interpolation', () => {
|
|
38
|
+
expect(parseTemplate('$')).toBe('');
|
|
39
|
+
expect(parseTemplate('$not')).toBe('not');
|
|
40
|
+
expect(parseTemplate('$not interpolation')).toBe('not interpolation');
|
|
41
|
+
expect(parseTemplate('$$')).toBe('$');
|
|
42
|
+
expect(parseTemplate('$$$')).toBe('$');
|
|
43
|
+
expect(parseTemplate('$$$$')).toBe('$$');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should parse simple interpolation', () => {
|
|
47
|
+
expect(parseTemplate('$hello $name!')).toEqual({
|
|
48
|
+
type: 'interpolation',
|
|
49
|
+
templates: ['hello ', '!'],
|
|
50
|
+
values: ['name'],
|
|
51
|
+
});
|
|
52
|
+
expect(parseTemplate('$$name')).toEqual({
|
|
53
|
+
type: 'interpolation',
|
|
54
|
+
templates: ['', ''],
|
|
55
|
+
values: ['name'],
|
|
56
|
+
});
|
|
57
|
+
expect(parseTemplate('$$name_is_long')).toEqual({
|
|
58
|
+
type: 'interpolation',
|
|
59
|
+
templates: ['', ''],
|
|
60
|
+
values: ['name_is_long'],
|
|
61
|
+
});
|
|
62
|
+
expect(parseTemplate('$$_001')).toEqual({
|
|
63
|
+
type: 'interpolation',
|
|
64
|
+
templates: ['', ''],
|
|
65
|
+
values: ['_001'],
|
|
66
|
+
});
|
|
67
|
+
expect(parseTemplate('$$001$a')).toEqual({
|
|
68
|
+
type: 'interpolation',
|
|
69
|
+
templates: ['$001', ''],
|
|
70
|
+
values: ['a'],
|
|
71
|
+
});
|
|
72
|
+
expect(parseTemplate('$$name$a')).toEqual({
|
|
73
|
+
type: 'interpolation',
|
|
74
|
+
templates: ['', '', ''],
|
|
75
|
+
values: ['name', 'a'],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should parse complex interpolation', () => {
|
|
80
|
+
expect(parseTemplate('$hello ${name}!')).toEqual({
|
|
81
|
+
type: 'interpolation',
|
|
82
|
+
templates: ['hello ', '!'],
|
|
83
|
+
values: ['name'],
|
|
84
|
+
});
|
|
85
|
+
expect(parseTemplate('$${name}')).toEqual({
|
|
86
|
+
type: 'interpolation',
|
|
87
|
+
templates: ['', ''],
|
|
88
|
+
values: ['name'],
|
|
89
|
+
});
|
|
90
|
+
expect(parseTemplate('$${name_is_long}')).toEqual({
|
|
91
|
+
type: 'interpolation',
|
|
92
|
+
templates: ['', ''],
|
|
93
|
+
values: ['name_is_long'],
|
|
94
|
+
});
|
|
95
|
+
expect(parseTemplate('$${_001}')).toEqual({
|
|
96
|
+
type: 'interpolation',
|
|
97
|
+
templates: ['', ''],
|
|
98
|
+
values: ['_001'],
|
|
99
|
+
});
|
|
100
|
+
expect(parseTemplate('$1${ }2')).toEqual({
|
|
101
|
+
type: 'interpolation',
|
|
102
|
+
templates: ['1', '2'],
|
|
103
|
+
values: [''],
|
|
104
|
+
});
|
|
105
|
+
expect(parseTemplate('$${ name }${ a}')).toEqual({
|
|
106
|
+
type: 'interpolation',
|
|
107
|
+
templates: ['', '', ''],
|
|
108
|
+
values: ['name', 'a'],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should parse mixed interpolation', () => {
|
|
113
|
+
expect(parseTemplate('$$name${a}')).toEqual({
|
|
114
|
+
type: 'interpolation',
|
|
115
|
+
templates: ['', '', ''],
|
|
116
|
+
values: ['name', 'a'],
|
|
117
|
+
});
|
|
118
|
+
expect(parseTemplate('$${name }$a')).toEqual({
|
|
119
|
+
type: 'interpolation',
|
|
120
|
+
templates: ['', '', ''],
|
|
121
|
+
values: ['name', 'a'],
|
|
122
|
+
});
|
|
123
|
+
expect(
|
|
124
|
+
parseTemplate("$hello $name! I am $age years old. I'll be ${age+1} next year. Give me $$100."),
|
|
125
|
+
).toEqual({
|
|
126
|
+
type: 'interpolation',
|
|
127
|
+
templates: ['hello ', '! I am ', " years old. I'll be ", ' next year. Give me $100.'],
|
|
128
|
+
values: ['name', 'age', 'age+1'],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should not break on normal braces', () => {
|
|
133
|
+
expect(parseTemplate('$$aa{$b}')).toEqual({
|
|
134
|
+
type: 'interpolation',
|
|
135
|
+
templates: ['', '{', '}'],
|
|
136
|
+
values: ['aa', 'b'],
|
|
137
|
+
});
|
|
138
|
+
expect(parseTemplate('$$aa{$}')).toEqual({
|
|
139
|
+
type: 'interpolation',
|
|
140
|
+
templates: ['', '{$}'],
|
|
141
|
+
values: ['aa'],
|
|
142
|
+
});
|
|
143
|
+
expect(parseTemplate('$$aa{}')).toEqual({
|
|
144
|
+
type: 'interpolation',
|
|
145
|
+
templates: ['', '{}'],
|
|
146
|
+
values: ['aa'],
|
|
147
|
+
});
|
|
148
|
+
expect(parseTemplate('$$$$aa{}')).toEqual({
|
|
149
|
+
type: 'interpolation',
|
|
150
|
+
templates: ['$', '{}'],
|
|
151
|
+
values: ['aa'],
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw on invalid interpolation', () => {
|
|
156
|
+
expect(() => parseTemplate('$${')).toThrow('Unexpected end of input');
|
|
157
|
+
expect(() => parseTemplate('$${123')).toThrow('Unexpected end of input');
|
|
158
|
+
expect(() => parseTemplate('$${{123}')).toThrow('Unexpected end of input');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should pair braces', () => {
|
|
162
|
+
expect(parseTemplate('$${n{am}e}')).toEqual({
|
|
163
|
+
type: 'interpolation',
|
|
164
|
+
templates: ['', ''],
|
|
165
|
+
values: ['n{am}e'],
|
|
166
|
+
});
|
|
167
|
+
expect(parseTemplate('$${n{{a}m}e}')).toEqual({
|
|
168
|
+
type: 'interpolation',
|
|
169
|
+
templates: ['', ''],
|
|
170
|
+
values: ['n{{a}m}e'],
|
|
171
|
+
});
|
|
172
|
+
expect(parseTemplate('$aaa${n{{a}m}e}bbb')).toEqual({
|
|
173
|
+
type: 'interpolation',
|
|
174
|
+
templates: ['aaa', 'bbb'],
|
|
175
|
+
values: ['n{{a}m}e'],
|
|
176
|
+
});
|
|
177
|
+
expect(parseTemplate('$aaa${n{{a}m}e}${n{{a}m}e}bbb${n{{a}m}e}ccc')).toEqual({
|
|
178
|
+
type: 'interpolation',
|
|
179
|
+
templates: ['aaa', '', 'bbb', 'ccc'],
|
|
180
|
+
values: ['n{{a}m}e', 'n{{a}m}e', 'n{{a}m}e'],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomBytes, randomFillSync } from 'node:crypto';
|
|
2
|
+
import { template } from '@cloudpss/template';
|
|
3
|
+
|
|
4
|
+
describe('template', () => {
|
|
5
|
+
it('should handle primitives', () => {
|
|
6
|
+
expect(template('')()).toEqual('');
|
|
7
|
+
expect(template('Hello, world!')()).toEqual('Hello, world!');
|
|
8
|
+
expect(template(1)()).toEqual(1);
|
|
9
|
+
expect(template(1.1)()).toEqual(1.1);
|
|
10
|
+
expect(template(Number.NaN)()).toEqual(Number.NaN);
|
|
11
|
+
expect(template(Number.POSITIVE_INFINITY)()).toEqual(Number.POSITIVE_INFINITY);
|
|
12
|
+
expect(template(Number.NEGATIVE_INFINITY)()).toEqual(Number.NEGATIVE_INFINITY);
|
|
13
|
+
expect(template(Number.EPSILON)()).toEqual(Number.EPSILON);
|
|
14
|
+
expect(template(true)()).toEqual(true);
|
|
15
|
+
expect(template(false)()).toEqual(false);
|
|
16
|
+
expect(template(null)()).toEqual(null);
|
|
17
|
+
expect(template(undefined)()).toEqual(undefined);
|
|
18
|
+
expect(template(1n)()).toEqual(1n);
|
|
19
|
+
expect(
|
|
20
|
+
template({
|
|
21
|
+
string: 'Hello, world!',
|
|
22
|
+
number: 1,
|
|
23
|
+
boolean: true,
|
|
24
|
+
null: null,
|
|
25
|
+
undefined: undefined,
|
|
26
|
+
bigint: 1n,
|
|
27
|
+
})(),
|
|
28
|
+
).toEqual({
|
|
29
|
+
string: 'Hello, world!',
|
|
30
|
+
number: 1,
|
|
31
|
+
boolean: true,
|
|
32
|
+
null: null,
|
|
33
|
+
undefined: undefined,
|
|
34
|
+
bigint: 1n,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it('should ignore symbols', () => {
|
|
38
|
+
expect(template(Symbol(''))()).toEqual(undefined);
|
|
39
|
+
expect(
|
|
40
|
+
template({
|
|
41
|
+
key: Symbol(''),
|
|
42
|
+
[Symbol('')]: 1,
|
|
43
|
+
})(),
|
|
44
|
+
).toEqual({
|
|
45
|
+
key: undefined,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should ignore functions', () => {
|
|
49
|
+
expect(template(() => 1)()).toEqual(undefined);
|
|
50
|
+
expect(
|
|
51
|
+
template({
|
|
52
|
+
key: () => 1,
|
|
53
|
+
})(),
|
|
54
|
+
).toEqual({
|
|
55
|
+
key: undefined,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
it('should copy arrays', () => {
|
|
59
|
+
const arr = [1, 2, {}];
|
|
60
|
+
const result = template(arr)();
|
|
61
|
+
expect(result).toEqual(arr);
|
|
62
|
+
expect(result).not.toBe(arr);
|
|
63
|
+
expect(result[2]).not.toBe(arr[2]);
|
|
64
|
+
});
|
|
65
|
+
it('should copy objects', () => {
|
|
66
|
+
const obj = { a: 1, b: 2, c: {} };
|
|
67
|
+
const result = template(obj)();
|
|
68
|
+
expect(result).toEqual(obj);
|
|
69
|
+
expect(result).not.toBe(obj);
|
|
70
|
+
expect(result.c).not.toBe(obj.c);
|
|
71
|
+
});
|
|
72
|
+
it('should copy dates', () => {
|
|
73
|
+
const date = new Date();
|
|
74
|
+
const result = template(date)();
|
|
75
|
+
expect(result).toEqual(date);
|
|
76
|
+
expect(result).not.toBe(date);
|
|
77
|
+
});
|
|
78
|
+
it('should copy regexps', () => {
|
|
79
|
+
const regexp = /./gi;
|
|
80
|
+
const result = template(regexp)();
|
|
81
|
+
expect(result).toEqual(regexp);
|
|
82
|
+
expect(result).not.toBe(regexp);
|
|
83
|
+
});
|
|
84
|
+
describe('should copy errors', () => {
|
|
85
|
+
it('Error', () => {
|
|
86
|
+
const error = new Error('test error');
|
|
87
|
+
const result = template(error)();
|
|
88
|
+
expect(result).toBeInstanceOf(Error);
|
|
89
|
+
expect(result.name).toBe(error.name);
|
|
90
|
+
expect(result.message).toBe(error.message);
|
|
91
|
+
expect(result).not.toBe(error);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('TypeError', () => {
|
|
95
|
+
const error = new TypeError('test error');
|
|
96
|
+
const result = template(error)();
|
|
97
|
+
expect(result).toBeInstanceOf(TypeError);
|
|
98
|
+
expect(result.name).toBe(error.name);
|
|
99
|
+
expect(result.message).toBe(error.message);
|
|
100
|
+
expect(result).not.toBe(error);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('SyntaxError with custom name', () => {
|
|
104
|
+
const error = new SyntaxError('test error');
|
|
105
|
+
error.name = 'CustomError';
|
|
106
|
+
const result = template(error)();
|
|
107
|
+
expect(result).toBeInstanceOf(SyntaxError);
|
|
108
|
+
expect(result.name).toBe(error.name);
|
|
109
|
+
expect(result.message).toBe(error.message);
|
|
110
|
+
expect(result).not.toBe(error);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('DOMException', () => {
|
|
114
|
+
const error = new DOMException('test error', 'AbortError');
|
|
115
|
+
const result = template(error)();
|
|
116
|
+
expect(result).toBeInstanceOf(DOMException);
|
|
117
|
+
expect(result.name).toBe(error.name);
|
|
118
|
+
expect(result.message).toBe(error.message);
|
|
119
|
+
expect(result.code).toBe(error.code);
|
|
120
|
+
expect(result).not.toBe(error);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('CustomError', () => {
|
|
124
|
+
const error = new (class CustomError extends Error {})('test error');
|
|
125
|
+
const result = template(error)();
|
|
126
|
+
expect(result).toBeInstanceOf(Error);
|
|
127
|
+
expect(result.name).toBe(error.name);
|
|
128
|
+
expect(result.message).toBe(error.message);
|
|
129
|
+
expect(result).not.toBe(error);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('should copy ArrayBuffers', () => {
|
|
133
|
+
it('ArrayBuffer', () => {
|
|
134
|
+
const buffer = randomBytes(16).buffer;
|
|
135
|
+
const t = template(buffer);
|
|
136
|
+
const result1 = t();
|
|
137
|
+
const result2 = t();
|
|
138
|
+
expect(new Uint8Array(result1)).toEqual(new Uint8Array(buffer));
|
|
139
|
+
expect(new Uint8Array(result2)).toEqual(new Uint8Array(buffer));
|
|
140
|
+
expect(result1).not.toBe(buffer);
|
|
141
|
+
expect(result2).not.toBe(buffer);
|
|
142
|
+
expect(result1).not.toBe(result2);
|
|
143
|
+
});
|
|
144
|
+
it('SharedArrayBuffer', () => {
|
|
145
|
+
const buffer = new SharedArrayBuffer(16);
|
|
146
|
+
randomFillSync(new Uint8Array(buffer));
|
|
147
|
+
const result = template(buffer)();
|
|
148
|
+
expect(new Uint8Array(result)).toEqual(new Uint8Array(buffer));
|
|
149
|
+
expect(result).toBeInstanceOf(SharedArrayBuffer);
|
|
150
|
+
expect(result).not.toBe(buffer);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('should copy ArrayBufferViews', () => {
|
|
154
|
+
it('DataView', () => {
|
|
155
|
+
const buffer = randomBytes(64).buffer;
|
|
156
|
+
const view = new DataView(buffer, 32, 16);
|
|
157
|
+
const result = template(view)();
|
|
158
|
+
expect(result).toEqual(view);
|
|
159
|
+
expect(result).not.toBe(view);
|
|
160
|
+
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
|
|
161
|
+
expect(result.buffer).not.toBe(buffer);
|
|
162
|
+
});
|
|
163
|
+
it('Uint8Array', () => {
|
|
164
|
+
const buffer = randomBytes(64).buffer;
|
|
165
|
+
const view = new Uint8Array(buffer, 32, 16);
|
|
166
|
+
const result = template(view)();
|
|
167
|
+
expect(result).toEqual(view);
|
|
168
|
+
expect(result).not.toBe(view);
|
|
169
|
+
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
|
|
170
|
+
expect(result.buffer).not.toBe(buffer);
|
|
171
|
+
});
|
|
172
|
+
it('Float64Array', () => {
|
|
173
|
+
const buffer = randomBytes(64).buffer;
|
|
174
|
+
const view = new Float64Array(buffer, 32, 1);
|
|
175
|
+
const result = template(view)();
|
|
176
|
+
expect(result).toEqual(view);
|
|
177
|
+
expect(result).not.toBe(view);
|
|
178
|
+
expect(new Uint8Array(result.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
|
|
179
|
+
expect(result.buffer).not.toBe(buffer);
|
|
180
|
+
});
|
|
181
|
+
it('BigInt64Array', () => {
|
|
182
|
+
const buffer = randomBytes(64).buffer;
|
|
183
|
+
const view = new BigInt64Array(buffer, 32, 1);
|
|
184
|
+
const t = template(view);
|
|
185
|
+
const result1 = t();
|
|
186
|
+
const result2 = t();
|
|
187
|
+
expect(result1).toEqual(view);
|
|
188
|
+
expect(result1).not.toBe(view);
|
|
189
|
+
expect(result2).toEqual(view);
|
|
190
|
+
expect(result2).not.toBe(view);
|
|
191
|
+
expect(result2).not.toBe(result1);
|
|
192
|
+
expect(new Uint8Array(result1.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
|
|
193
|
+
expect(new Uint8Array(result2.buffer)).toEqual(new Uint8Array(buffer, view.byteOffset, view.byteLength));
|
|
194
|
+
expect(result1.buffer).not.toBe(buffer);
|
|
195
|
+
expect(result2.buffer).not.toBe(buffer);
|
|
196
|
+
expect(result1.buffer).not.toBe(result2.buffer);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
it('should build interpolated strings', () => {
|
|
200
|
+
const t = template({ hello: '$Hello, ${name}!', not: 'Hello $name', array: ['$$name', '$not'] });
|
|
201
|
+
const result = t({
|
|
202
|
+
name: 'world',
|
|
203
|
+
});
|
|
204
|
+
expect(result).toEqual({
|
|
205
|
+
hello: 'Hello, world!',
|
|
206
|
+
not: 'Hello $name',
|
|
207
|
+
array: ['world', 'not'],
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('should ignore properties on the prototype chain', () => {
|
|
211
|
+
const obj = Object.create({ prototype: 'chain' });
|
|
212
|
+
obj.own = 'property';
|
|
213
|
+
const result = template(obj)();
|
|
214
|
+
expect(result).toEqual({ own: 'property' });
|
|
215
|
+
});
|
|
216
|
+
it('should ignore non-enumerable properties', () => {
|
|
217
|
+
const obj = Object.create(null, {
|
|
218
|
+
enumerable: {
|
|
219
|
+
value: 'property',
|
|
220
|
+
enumerable: false,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const result = template(obj)();
|
|
224
|
+
expect(result).toEqual({});
|
|
225
|
+
});
|
|
226
|
+
describe('should work while objectKeyMode is', () => {
|
|
227
|
+
it('template', () => {
|
|
228
|
+
const obj = { $: 1, _: 2, '=x': 3, $$y: 4 };
|
|
229
|
+
const result = template(obj, { objectKeyMode: 'template' })({ x: 11n, y: 12 });
|
|
230
|
+
expect(result).toEqual({
|
|
231
|
+
'': 1,
|
|
232
|
+
_: 2,
|
|
233
|
+
11: 3,
|
|
234
|
+
12: 4,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
it('ignore', () => {
|
|
238
|
+
const obj = { $: 1, _: 2, '=x': 3, $$y: 4 };
|
|
239
|
+
const result = template(obj, { objectKeyMode: 'ignore' })({ x: 11n, y: 12 });
|
|
240
|
+
expect(result).toEqual({
|
|
241
|
+
$: 1,
|
|
242
|
+
_: 2,
|
|
243
|
+
'=x': 3,
|
|
244
|
+
$$y: 4,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('should work with default evaluator', () => {
|
|
250
|
+
it('with formula', () => {
|
|
251
|
+
const obj = { a: '=a', b: '=b' };
|
|
252
|
+
const result = template(obj)({ a: 1 });
|
|
253
|
+
expect(result).toEqual({ a: 1, b: undefined });
|
|
254
|
+
});
|
|
255
|
+
it('with interpolation', () => {
|
|
256
|
+
const obj = { a: '$$a', b: '$$b' };
|
|
257
|
+
const result = template(obj)({ a: 1 });
|
|
258
|
+
expect(result).toEqual({ a: '1', b: '' });
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|