@doki-land/dejavu-engine 0.0.13 → 0.0.14
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/dist/runtime/filter.d.ts +13 -2
- package/dist/runtime/filter.js +27 -10
- package/dist/runtime/function.d.ts +16 -0
- package/dist/runtime/function.js +85 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/test.d.ts +18 -0
- package/dist/runtime/test.js +81 -0
- package/package.json +1 -1
package/dist/runtime/filter.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dejavu 1.0 合同过滤器集。
|
|
3
3
|
*
|
|
4
|
-
* 与 Rust `dejavu-ir` 的 `
|
|
4
|
+
* 与 Rust `dejavu-ir` 的 `registry` 逐项对齐,所有字符串强制转换都走
|
|
5
5
|
* `valueToString`(null → ""),不使用 JS 的 `String()`。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* 未识别的过滤器与参数个数违约都抛错,与 Rust 渲染器一致。
|
|
8
8
|
*/
|
|
9
|
+
export type FilterCapability = "pure" | "safe-output";
|
|
10
|
+
export interface FilterSpec {
|
|
11
|
+
name: string;
|
|
12
|
+
aliases: string[];
|
|
13
|
+
minArgs: number;
|
|
14
|
+
maxArgs: number;
|
|
15
|
+
capability: FilterCapability;
|
|
16
|
+
}
|
|
17
|
+
/** 与 Rust `dejavu_ir::registry::FILTERS` 同序同构的静态描述。 */
|
|
18
|
+
export declare const FILTERS: readonly FilterSpec[];
|
|
19
|
+
export declare function filterSpec(name: string): FilterSpec | undefined;
|
|
9
20
|
export declare function applyFilter(name: string, value: unknown, args: unknown[]): unknown;
|
package/dist/runtime/filter.js
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import { htmlEscape } from "./escape.js";
|
|
2
2
|
import { valueToString } from "./value.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
/** 与 Rust `dejavu_ir::registry::FILTERS` 同序同构的静态描述。 */
|
|
4
|
+
export const FILTERS = [
|
|
5
|
+
{ name: "uppercase", aliases: ["upper"], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
6
|
+
{ name: "lowercase", aliases: ["lower"], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
7
|
+
{ name: "trim", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
8
|
+
{ name: "default", aliases: [], minArgs: 1, maxArgs: 1, capability: "pure" },
|
|
9
|
+
{ name: "length", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
10
|
+
{ name: "join", aliases: [], minArgs: 0, maxArgs: 1, capability: "pure" },
|
|
11
|
+
{ name: "replace", aliases: [], minArgs: 2, maxArgs: 2, capability: "pure" },
|
|
12
|
+
{ name: "escape", aliases: ["e"], minArgs: 0, maxArgs: 0, capability: "safe-output" },
|
|
13
|
+
{ name: "safe", aliases: ["raw"], minArgs: 0, maxArgs: 0, capability: "safe-output" },
|
|
14
|
+
{ name: "date", aliases: [], minArgs: 0, maxArgs: 1, capability: "pure" },
|
|
15
|
+
{ name: "slug", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
16
|
+
];
|
|
17
|
+
export function filterSpec(name) {
|
|
18
|
+
return FILTERS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
19
|
+
}
|
|
11
20
|
export function applyFilter(name, value, args) {
|
|
21
|
+
const spec = filterSpec(name);
|
|
22
|
+
if (!spec) {
|
|
23
|
+
throw new Error(`dejavu.template.unknown-filter: ${name}`);
|
|
24
|
+
}
|
|
25
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
26
|
+
throw new Error(`dejavu.template.filter-arity: ${spec.name} expects ` +
|
|
27
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`);
|
|
28
|
+
}
|
|
12
29
|
switch (name) {
|
|
13
30
|
case "escape":
|
|
14
31
|
case "e":
|
|
15
32
|
return htmlEscape(valueToString(value));
|
|
16
33
|
case "raw":
|
|
17
34
|
case "safe":
|
|
18
|
-
case "s":
|
|
19
35
|
return value;
|
|
20
36
|
case "uppercase":
|
|
21
37
|
case "upper":
|
|
@@ -60,7 +76,8 @@ export function applyFilter(name, value, args) {
|
|
|
60
76
|
case "slug":
|
|
61
77
|
return slugify(valueToString(value));
|
|
62
78
|
default:
|
|
63
|
-
|
|
79
|
+
// Unreachable: `filterSpec` already rejected unknown names.
|
|
80
|
+
throw new Error(`dejavu.template.unknown-filter: ${name}`);
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dejavu `function` 命名空间:语言中立的静态表 + 纯函数实现。
|
|
3
|
+
*
|
|
4
|
+
* 与 Rust `dejavu_ir::registry::FUNCTIONS` 和 `builtins::call_function` 对齐。
|
|
5
|
+
* 仅允许 `name(args)` 形式的裸标识符调用;成员调用与未知名字一律报错。
|
|
6
|
+
*/
|
|
7
|
+
export interface FunctionSpec {
|
|
8
|
+
name: string;
|
|
9
|
+
aliases: string[];
|
|
10
|
+
minArgs: number;
|
|
11
|
+
maxArgs: number;
|
|
12
|
+
}
|
|
13
|
+
/** 与 Rust `dejavu_ir::registry::FUNCTIONS` 同序同构的静态描述。 */
|
|
14
|
+
export declare const FUNCTIONS: readonly FunctionSpec[];
|
|
15
|
+
export declare function functionSpec(name: string): FunctionSpec | undefined;
|
|
16
|
+
export declare function callFunction(name: string, args: unknown[]): unknown;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dejavu `function` 命名空间:语言中立的静态表 + 纯函数实现。
|
|
3
|
+
*
|
|
4
|
+
* 与 Rust `dejavu_ir::registry::FUNCTIONS` 和 `builtins::call_function` 对齐。
|
|
5
|
+
* 仅允许 `name(args)` 形式的裸标识符调用;成员调用与未知名字一律报错。
|
|
6
|
+
*/
|
|
7
|
+
/** 与 Rust `dejavu_ir::registry::FUNCTIONS` 同序同构的静态描述。 */
|
|
8
|
+
export const FUNCTIONS = [
|
|
9
|
+
{ name: "range", aliases: [], minArgs: 2, maxArgs: 3 },
|
|
10
|
+
{ name: "min", aliases: [], minArgs: 2, maxArgs: 2 },
|
|
11
|
+
{ name: "max", aliases: [], minArgs: 2, maxArgs: 2 },
|
|
12
|
+
];
|
|
13
|
+
/** `range` 允许产出的最大元素数,与 Rust `RANGE_LIMIT` 一致。 */
|
|
14
|
+
const RANGE_LIMIT = 10_000;
|
|
15
|
+
export function functionSpec(name) {
|
|
16
|
+
return FUNCTIONS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
17
|
+
}
|
|
18
|
+
export function callFunction(name, args) {
|
|
19
|
+
const spec = functionSpec(name);
|
|
20
|
+
if (!spec) {
|
|
21
|
+
throw new Error(`dejavu.template.unknown-function: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
24
|
+
throw new Error(`dejavu.template.function-arity: ${spec.name} expects ` +
|
|
25
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`);
|
|
26
|
+
}
|
|
27
|
+
switch (spec.name) {
|
|
28
|
+
case "range":
|
|
29
|
+
return range(args);
|
|
30
|
+
case "min":
|
|
31
|
+
return numericPair(args, true);
|
|
32
|
+
case "max":
|
|
33
|
+
return numericPair(args, false);
|
|
34
|
+
default:
|
|
35
|
+
// Unreachable: `functionSpec` already rejected unknown names.
|
|
36
|
+
throw new Error(`dejavu.template.unknown-function: ${name}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** `range(start, stop, step=1)`,非法输入按值模型规则返回 null。 */
|
|
40
|
+
function range(args) {
|
|
41
|
+
const start = integral(args[0]);
|
|
42
|
+
if (start === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
const stop = integral(args[1]);
|
|
45
|
+
if (stop === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
let step = 1;
|
|
48
|
+
if (args.length > 2) {
|
|
49
|
+
const parsed = integral(args[2]);
|
|
50
|
+
if (parsed === undefined || parsed === 0)
|
|
51
|
+
return null;
|
|
52
|
+
step = parsed;
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
let current = start;
|
|
56
|
+
while ((step > 0 && current < stop) ||
|
|
57
|
+
(step < 0 && current > stop)) {
|
|
58
|
+
if (out.length >= RANGE_LIMIT)
|
|
59
|
+
return null;
|
|
60
|
+
out.push(current);
|
|
61
|
+
current += step;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** `min(a, b)` / `max(a, b)`,仅接受有限数字。 */
|
|
66
|
+
function numericPair(args, min) {
|
|
67
|
+
const a = finiteNumber(args[0]);
|
|
68
|
+
const b = finiteNumber(args[1]);
|
|
69
|
+
if (a === undefined || b === undefined)
|
|
70
|
+
return null;
|
|
71
|
+
return min ? Math.min(a, b) : Math.max(a, b);
|
|
72
|
+
}
|
|
73
|
+
function finiteNumber(value) {
|
|
74
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
75
|
+
return undefined;
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/** 整数(与 Rust `integral` 对齐:非整数、非数字、越界都视为非法)。 */
|
|
79
|
+
function integral(value) {
|
|
80
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
81
|
+
return undefined;
|
|
82
|
+
if (value < -(2 ** 63) || value > 2 ** 63 - 1)
|
|
83
|
+
return undefined;
|
|
84
|
+
return value;
|
|
85
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -7,4 +7,6 @@
|
|
|
7
7
|
export { htmlEscape } from "./escape.js";
|
|
8
8
|
export { valueToString, isTruthy, valuesEqual } from "./value.js";
|
|
9
9
|
export { applyFilter } from "./filter.js";
|
|
10
|
+
export { FUNCTIONS, functionSpec, callFunction } from "./function.js";
|
|
11
|
+
export { TESTS, testSpec, applyTest } from "./test.js";
|
|
10
12
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -7,4 +7,6 @@
|
|
|
7
7
|
export { htmlEscape } from "./escape.js";
|
|
8
8
|
export { valueToString, isTruthy, valuesEqual } from "./value.js";
|
|
9
9
|
export { applyFilter } from "./filter.js";
|
|
10
|
+
export { FUNCTIONS, functionSpec, callFunction } from "./function.js";
|
|
11
|
+
export { TESTS, testSpec, applyTest } from "./test.js";
|
|
10
12
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dejavu `test` 命名空间:语言中立的谓词表 + 纯函数实现。
|
|
3
|
+
*
|
|
4
|
+
* 与 Rust `dejavu_ir::registry::TESTS` 和 `builtins::call_test` 对齐。
|
|
5
|
+
* 语法为 `expr is name` / `expr is not name` / `expr is name(args)`,
|
|
6
|
+
* 本文件只负责对已求值的左值执行谓词。
|
|
7
|
+
*/
|
|
8
|
+
export interface TestSpec {
|
|
9
|
+
name: string;
|
|
10
|
+
aliases: string[];
|
|
11
|
+
minArgs: number;
|
|
12
|
+
maxArgs: number;
|
|
13
|
+
}
|
|
14
|
+
/** 与 Rust `dejavu_ir::registry::TESTS` 同序同构的静态描述。 */
|
|
15
|
+
export declare const TESTS: readonly TestSpec[];
|
|
16
|
+
export declare function testSpec(name: string): TestSpec | undefined;
|
|
17
|
+
/** 对已求值的左值执行谓词;`negated` 为 `is not` 形式。 */
|
|
18
|
+
export declare function applyTest(name: string, value: unknown, args: unknown[], negated?: boolean): boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dejavu `test` 命名空间:语言中立的谓词表 + 纯函数实现。
|
|
3
|
+
*
|
|
4
|
+
* 与 Rust `dejavu_ir::registry::TESTS` 和 `builtins::call_test` 对齐。
|
|
5
|
+
* 语法为 `expr is name` / `expr is not name` / `expr is name(args)`,
|
|
6
|
+
* 本文件只负责对已求值的左值执行谓词。
|
|
7
|
+
*/
|
|
8
|
+
import { isTruthy } from "./value.js";
|
|
9
|
+
/** 与 Rust `dejavu_ir::registry::TESTS` 同序同构的静态描述。 */
|
|
10
|
+
export const TESTS = [
|
|
11
|
+
{ name: "null", aliases: ["none"], minArgs: 0, maxArgs: 0 },
|
|
12
|
+
{ name: "truthy", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
13
|
+
{ name: "empty", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
14
|
+
{ name: "array", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
15
|
+
{ name: "object", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
16
|
+
{ name: "string", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
17
|
+
{ name: "number", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
18
|
+
{ name: "boolean", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
19
|
+
{ name: "even", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
20
|
+
{ name: "odd", aliases: [], minArgs: 0, maxArgs: 0 },
|
|
21
|
+
];
|
|
22
|
+
export function testSpec(name) {
|
|
23
|
+
return TESTS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
24
|
+
}
|
|
25
|
+
/** 对已求值的左值执行谓词;`negated` 为 `is not` 形式。 */
|
|
26
|
+
export function applyTest(name, value, args, negated = false) {
|
|
27
|
+
const spec = testSpec(name);
|
|
28
|
+
if (!spec) {
|
|
29
|
+
throw new Error(`dejavu.template.unknown-test: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
32
|
+
throw new Error(`dejavu.template.test-arity: ${spec.name} expects ` +
|
|
33
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`);
|
|
34
|
+
}
|
|
35
|
+
let result;
|
|
36
|
+
switch (spec.name) {
|
|
37
|
+
case "null":
|
|
38
|
+
result = value === null || value === undefined;
|
|
39
|
+
break;
|
|
40
|
+
case "truthy":
|
|
41
|
+
result = isTruthy(value);
|
|
42
|
+
break;
|
|
43
|
+
case "empty":
|
|
44
|
+
result =
|
|
45
|
+
value === ""
|
|
46
|
+
|| (Array.isArray(value) && value.length === 0)
|
|
47
|
+
|| (isPlainObject(value) && Object.keys(value).length === 0);
|
|
48
|
+
break;
|
|
49
|
+
case "array":
|
|
50
|
+
result = Array.isArray(value);
|
|
51
|
+
break;
|
|
52
|
+
case "object":
|
|
53
|
+
result = isPlainObject(value);
|
|
54
|
+
break;
|
|
55
|
+
case "string":
|
|
56
|
+
result = typeof value === "string";
|
|
57
|
+
break;
|
|
58
|
+
case "number":
|
|
59
|
+
result = typeof value === "number";
|
|
60
|
+
break;
|
|
61
|
+
case "boolean":
|
|
62
|
+
result = typeof value === "boolean";
|
|
63
|
+
break;
|
|
64
|
+
case "even":
|
|
65
|
+
result = isInteger(value) && value % 2 === 0;
|
|
66
|
+
break;
|
|
67
|
+
case "odd":
|
|
68
|
+
result = isInteger(value) && value % 2 !== 0;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
// Unreachable: `testSpec` already rejected unknown names.
|
|
72
|
+
throw new Error(`dejavu.template.unknown-test: ${name}`);
|
|
73
|
+
}
|
|
74
|
+
return result !== negated;
|
|
75
|
+
}
|
|
76
|
+
function isPlainObject(value) {
|
|
77
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function isInteger(value) {
|
|
80
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
81
|
+
}
|