@doki-land/dejavu-engine 0.0.12 → 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.
@@ -1,23 +1,23 @@
1
1
  /**
2
2
  * Dejavu 1.0 支持的二元运算符。
3
- * 与 Rust `dejavu-ir::IrExprBinaryOp` 枚举一一对应。
3
+ * 与 Rust `dejavu-ir` 的 `eval_binary` 一一对应,**不借用 JS 隐式转换**:
4
4
  *
5
- * 行为:
6
- * - `+` 特殊:任何一侧为字符串时做字符串拼接,否则数值加法。
7
- * - `&&` / `||` 返回原值(短路),不是布尔。
8
- * - `in`:R 是 object / array 时检查 key 存在。
9
- * - 比较与相等用 JS 原生语义。
5
+ * - `+`:两侧都是 number 时相加,否则按 `valueToString` 做字符串拼接。
6
+ * - `-` `*` `/` `%`:只接受 number,非 number 或结果非有限数 → null。
7
+ * - `==` / `!=`:结构相等(见 `valuesEqual`),不做类型转换。
8
+ * - `<` `<=` `>` `>=`:两侧都是 number 才比较,否则 false。
9
+ * - `&&` / `||`:返回 **boolean**(Dejavu truthiness),不是原值。
10
+ * - `in`:数组按成员、字符串按子串、对象按 key 判断。
10
11
  */
11
12
  export declare function binary(op: string, l: unknown, r: unknown): unknown;
12
13
  /**
13
- * 一元运算符。
14
+ * 一元运算符 —— 与 Rust `eval_unary` 对齐。
14
15
  *
15
- * `!` 使用 **Dejavu truthiness**(不是 JS falsy)。
16
+ * `!` 使用 Dejavu truthiness;`-` 只接受 number;`+` 是恒等。
16
17
  */
17
18
  export declare function unary(op: string, a: unknown): unknown;
18
19
  /**
19
- * 成员访问 —— `obj?.[property] ?? null`。
20
- * 对不存在的键返回 null(不是 undefined)。
20
+ * 成员访问 —— 对不存在的键返回 null(不是 undefined)。
21
21
  */
22
22
  export declare function exprMember(obj: unknown, property: string): unknown;
23
23
  /**
@@ -1,81 +1,100 @@
1
- import { isTruthy } from "./value.js";
1
+ import { isTruthy, valueToString, valuesEqual } from "./value.js";
2
2
  /**
3
3
  * Dejavu 1.0 支持的二元运算符。
4
- * 与 Rust `dejavu-ir::IrExprBinaryOp` 枚举一一对应。
4
+ * 与 Rust `dejavu-ir` 的 `eval_binary` 一一对应,**不借用 JS 隐式转换**:
5
5
  *
6
- * 行为:
7
- * - `+` 特殊:任何一侧为字符串时做字符串拼接,否则数值加法。
8
- * - `&&` / `||` 返回原值(短路),不是布尔。
9
- * - `in`:R 是 object / array 时检查 key 存在。
10
- * - 比较与相等用 JS 原生语义。
6
+ * - `+`:两侧都是 number 时相加,否则按 `valueToString` 做字符串拼接。
7
+ * - `-` `*` `/` `%`:只接受 number,非 number 或结果非有限数 → null。
8
+ * - `==` / `!=`:结构相等(见 `valuesEqual`),不做类型转换。
9
+ * - `<` `<=` `>` `>=`:两侧都是 number 才比较,否则 false。
10
+ * - `&&` / `||`:返回 **boolean**(Dejavu truthiness),不是原值。
11
+ * - `in`:数组按成员、字符串按子串、对象按 key 判断。
11
12
  */
12
13
  export function binary(op, l, r) {
13
14
  switch (op) {
14
15
  case "+": {
15
- if (typeof l === "string" || typeof r === "string") {
16
- return String(l) + String(r);
16
+ if (typeof l === "number" && typeof r === "number") {
17
+ return finite(l + r);
17
18
  }
18
- return l + r;
19
+ return valueToString(l) + valueToString(r);
19
20
  }
20
21
  case "-":
21
- return l - r;
22
22
  case "*":
23
- return l * r;
24
23
  case "/":
25
- return l / r;
26
- case "%":
27
- return l % r;
24
+ case "%": {
25
+ if (typeof l !== "number" || typeof r !== "number")
26
+ return null;
27
+ const value = op === "-"
28
+ ? l - r
29
+ : op === "*"
30
+ ? l * r
31
+ : op === "/"
32
+ ? l / r
33
+ : l % r;
34
+ return finite(value);
35
+ }
28
36
  case "==":
29
- // Dejavu 1.0 只用 ==/!= —— JS 宽松相等
30
- // eslint-disable-next-line eqeqeq
31
- return l == r;
37
+ return valuesEqual(l, r);
32
38
  case "!=":
33
- // eslint-disable-next-line eqeqeq
34
- return l != r;
35
- case "===":
36
- return l === r;
37
- case "!==":
38
- return l !== r;
39
+ return !valuesEqual(l, r);
39
40
  case "<":
40
- return l < r;
41
41
  case "<=":
42
- return l <= r;
43
42
  case ">":
44
- return l > r;
45
- case ">=":
46
- return l >= r;
43
+ case ">=": {
44
+ if (typeof l !== "number" || typeof r !== "number")
45
+ return false;
46
+ if (!Number.isFinite(l) || !Number.isFinite(r))
47
+ return false;
48
+ switch (op) {
49
+ case "<":
50
+ return l < r;
51
+ case "<=":
52
+ return l <= r;
53
+ case ">":
54
+ return l > r;
55
+ default:
56
+ return l >= r;
57
+ }
58
+ }
47
59
  case "&&":
48
- return l && r;
60
+ return isTruthy(l) && isTruthy(r);
49
61
  case "||":
50
- return l || r;
62
+ return isTruthy(l) || isTruthy(r);
51
63
  case "in":
52
- if (r === null || r === undefined)
53
- return false;
54
- return String(l) in r;
64
+ if (Array.isArray(r))
65
+ return r.some((item) => valuesEqual(item, l));
66
+ if (typeof r === "string")
67
+ return r.includes(valueToString(l));
68
+ if (r !== null && typeof r === "object") {
69
+ return Object.prototype.hasOwnProperty.call(r, valueToString(l));
70
+ }
71
+ return false;
55
72
  default:
56
73
  return null;
57
74
  }
58
75
  }
76
+ function finite(value) {
77
+ return Number.isFinite(value) ? value : null;
78
+ }
59
79
  /**
60
- * 一元运算符。
80
+ * 一元运算符 —— 与 Rust `eval_unary` 对齐。
61
81
  *
62
- * `!` 使用 **Dejavu truthiness**(不是 JS falsy)。
82
+ * `!` 使用 Dejavu truthiness;`-` 只接受 number;`+` 是恒等。
63
83
  */
64
84
  export function unary(op, a) {
65
85
  switch (op) {
66
86
  case "!":
67
87
  return !isTruthy(a);
68
88
  case "-":
69
- return -(Number(a) || 0);
89
+ return typeof a === "number" ? finite(-a) : null;
70
90
  case "+":
71
- return +(Number(a) || 0);
91
+ return a;
72
92
  default:
73
93
  return null;
74
94
  }
75
95
  }
76
96
  /**
77
- * 成员访问 —— `obj?.[property] ?? null`。
78
- * 对不存在的键返回 null(不是 undefined)。
97
+ * 成员访问 —— 对不存在的键返回 null(不是 undefined)。
79
98
  */
80
99
  export function exprMember(obj, property) {
81
100
  if (obj === null || obj === undefined)
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * Dejavu 1.0 合同过滤器集。
3
3
  *
4
- * 与 Rust `dejavu-types` 过滤器系统(见 `projects/crates/dejavu-types/src/values/filter.rs`)
5
- * 以及 `dejavu-codegen` Node stub 对齐。
4
+ * 与 Rust `dejavu-ir` 的 `registry` 逐项对齐,所有字符串强制转换都走
5
+ * `valueToString`(null → ""),不使用 JS 的 `String()`。
6
6
  *
7
- * 未识别的过滤器 **宽松返回原值**(与 Rust 渲染器一致,不抛错)。
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;
@@ -1,71 +1,118 @@
1
1
  import { htmlEscape } from "./escape.js";
2
- import { isTruthy, valueToString } from "./value.js";
3
- /**
4
- * Dejavu 1.0 合同过滤器集。
5
- *
6
- * 与 Rust `dejavu-types` 过滤器系统(见 `projects/crates/dejavu-types/src/values/filter.rs`)
7
- * 以及 `dejavu-codegen` Node stub 对齐。
8
- *
9
- * 未识别的过滤器 **宽松返回原值**(与 Rust 渲染器一致,不抛错)。
10
- */
2
+ import { valueToString } from "./value.js";
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
- return valueToString(value);
35
+ return value;
20
36
  case "uppercase":
21
37
  case "upper":
22
- return String(value).toUpperCase();
38
+ return valueToString(value).toUpperCase();
23
39
  case "lowercase":
24
40
  case "lower":
25
- return String(value).toLowerCase();
41
+ return valueToString(value).toLowerCase();
26
42
  case "trim":
27
- return String(value).trim();
28
- case "default":
29
- return isTruthy(value) ? value : args[0] ?? "";
43
+ return valueToString(value).trim();
44
+ case "default": {
45
+ // 只有 null 与空串算缺失 —— 0 / false / [] 保留原值。
46
+ if (value === null || value === undefined || value === "") {
47
+ return args[0] ?? null;
48
+ }
49
+ return value;
50
+ }
30
51
  case "length": {
31
- if (value === null || value === undefined)
32
- return 0;
33
- if (Array.isArray(value))
34
- return value.length;
35
52
  if (typeof value === "string")
53
+ return [...value].length;
54
+ if (Array.isArray(value))
36
55
  return value.length;
37
- const maybe = value.length;
38
- return typeof maybe === "number" ? maybe : String(value).length;
56
+ if (value !== null && typeof value === "object") {
57
+ return Object.keys(value).length;
58
+ }
59
+ return 0;
39
60
  }
40
- case "join":
61
+ case "join": {
62
+ const sep = valueToString(args[0] ?? ",");
41
63
  return Array.isArray(value)
42
- ? value.join(String(args[0] ?? ","))
43
- : String(value);
44
- case "replace":
45
- return String(value).replace(args[0] ?? "", String(args[1] ?? ""));
64
+ ? value.map(valueToString).join(sep)
65
+ : valueToString(value);
66
+ }
67
+ case "replace": {
68
+ const from = valueToString(args[0] ?? "");
69
+ const to = valueToString(args[1] ?? "");
70
+ return valueToString(value).split(from).join(to);
71
+ }
46
72
  case "date": {
47
- let d = null;
48
- if (value instanceof Date) {
49
- d = value;
50
- }
51
- else if (typeof value === "number") {
52
- d = new Date(value);
53
- }
54
- else if (typeof value === "string") {
55
- const parsed = new Date(value);
56
- if (!Number.isNaN(parsed.getTime()))
57
- d = parsed;
58
- }
59
- if (!d)
60
- return "";
61
- // 第一期只支持 ISO 8601(args[0] 不使用 —— Rust 过滤器默认也是 ISO)。
62
- return d.toISOString();
73
+ const pattern = valueToString(args[0] ?? "%Y-%m-%d");
74
+ return formatDate(valueToString(value), pattern);
63
75
  }
64
76
  case "slug":
65
- return slugify(String(value));
77
+ return slugify(valueToString(value));
66
78
  default:
67
- return value;
79
+ // Unreachable: `filterSpec` already rejected unknown names.
80
+ throw new Error(`dejavu.template.unknown-filter: ${name}`);
81
+ }
82
+ }
83
+ /**
84
+ * 与 Rust `format_date` 对齐的最小 strftime:`%Y %m %d %H %M %S %%`。
85
+ *
86
+ * 输入:空串 → "";`now` → 当前时间;纯数字 → epoch 毫秒;其他 → ISO 解析。
87
+ */
88
+ function formatDate(input, pattern) {
89
+ if (input === "")
90
+ return "";
91
+ let ms;
92
+ if (input === "now") {
93
+ ms = Date.now();
94
+ }
95
+ else if (/^[0-9]+$/.test(input)) {
96
+ ms = Number(input);
97
+ }
98
+ else {
99
+ ms = Date.parse(input);
100
+ if (Number.isNaN(ms))
101
+ return "";
68
102
  }
103
+ if (Number.isNaN(ms))
104
+ return "";
105
+ const d = new Date(ms);
106
+ const pad = (n) => String(n).padStart(2, "0");
107
+ return pattern
108
+ .replaceAll("%%", "\u0000")
109
+ .replaceAll("%Y", String(d.getUTCFullYear()))
110
+ .replaceAll("%m", pad(d.getUTCMonth() + 1))
111
+ .replaceAll("%d", pad(d.getUTCDate()))
112
+ .replaceAll("%H", pad(d.getUTCHours()))
113
+ .replaceAll("%M", pad(d.getUTCMinutes()))
114
+ .replaceAll("%S", pad(d.getUTCSeconds()))
115
+ .replaceAll("\u0000", "%");
69
116
  }
70
117
  /**
71
118
  * 与 Rust `slug_filter` 对齐:
@@ -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
+ }
@@ -5,6 +5,8 @@
5
5
  * 与 Rust `dejavu-codegen` Node stub 以及 `dejavu-types` 的 filter 系统语义一致。
6
6
  */
7
7
  export { htmlEscape } from "./escape.js";
8
- export { valueToString, isTruthy } from "./value.js";
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";
@@ -5,6 +5,8 @@
5
5
  * 与 Rust `dejavu-codegen` Node stub 以及 `dejavu-types` 的 filter 系统语义一致。
6
6
  */
7
7
  export { htmlEscape } from "./escape.js";
8
- export { valueToString, isTruthy } from "./value.js";
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
+ }
@@ -11,14 +11,17 @@
11
11
  */
12
12
  export declare function isTruthy(v: unknown): boolean;
13
13
  /**
14
- * 将任意 JS 值渲染为字符串。与 Rust 渲染器的 debug-print 语义对齐:
14
+ * 将任意 JS 值渲染为字符串。与 Rust `value_to_string` 对齐:
15
15
  *
16
16
  * - null / undefined → ""
17
17
  * - boolean → "true" / "false"
18
18
  * - number → String()(NaN / Infinity 保持原样)
19
19
  * - Date → toISOString()
20
- * - Array → 递归 valueToString 后 "," join
21
- * - object → JSON.stringify(fallback 到 String())
20
+ * - Array / object → 紧凑 JSON(与 serde_json 输出一致)
22
21
  * - string / 其他 → String()
23
22
  */
24
23
  export declare function valueToString(v: unknown): string;
24
+ /**
25
+ * 结构相等 —— 与 Rust `serde_json::Value` 的 `==` 对齐,不做类型转换。
26
+ */
27
+ export declare function valuesEqual(a: unknown, b: unknown): boolean;
@@ -26,14 +26,13 @@ export function isTruthy(v) {
26
26
  return true;
27
27
  }
28
28
  /**
29
- * 将任意 JS 值渲染为字符串。与 Rust 渲染器的 debug-print 语义对齐:
29
+ * 将任意 JS 值渲染为字符串。与 Rust `value_to_string` 对齐:
30
30
  *
31
31
  * - null / undefined → ""
32
32
  * - boolean → "true" / "false"
33
33
  * - number → String()(NaN / Infinity 保持原样)
34
34
  * - Date → toISOString()
35
- * - Array → 递归 valueToString 后 "," join
36
- * - object → JSON.stringify(fallback 到 String())
35
+ * - Array / object → 紧凑 JSON(与 serde_json 输出一致)
37
36
  * - string / 其他 → String()
38
37
  */
39
38
  export function valueToString(v) {
@@ -46,9 +45,7 @@ export function valueToString(v) {
46
45
  }
47
46
  if (v instanceof Date)
48
47
  return v.toISOString();
49
- if (Array.isArray(v))
50
- return v.map(valueToString).join(",");
51
- if (typeof v === "object") {
48
+ if (Array.isArray(v) || typeof v === "object") {
52
49
  try {
53
50
  return JSON.stringify(v);
54
51
  }
@@ -58,3 +55,27 @@ export function valueToString(v) {
58
55
  }
59
56
  return String(v);
60
57
  }
58
+ /**
59
+ * 结构相等 —— 与 Rust `serde_json::Value` 的 `==` 对齐,不做类型转换。
60
+ */
61
+ export function valuesEqual(a, b) {
62
+ if (a === b)
63
+ return true;
64
+ if (a === null || b === null || a === undefined || b === undefined)
65
+ return false;
66
+ if (Array.isArray(a) && Array.isArray(b)) {
67
+ return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
68
+ }
69
+ if (Array.isArray(a) !== Array.isArray(b))
70
+ return false;
71
+ if (typeof a === "object" && typeof b === "object") {
72
+ const ka = Object.keys(a);
73
+ const kb = Object.keys(b);
74
+ if (ka.length !== kb.length)
75
+ return false;
76
+ if (ka.some((k) => !Object.prototype.hasOwnProperty.call(b, k)))
77
+ return false;
78
+ return ka.every((k) => valuesEqual(a[k], b[k]));
79
+ }
80
+ return false;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doki-land/dejavu-engine",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Dejavu pure TypeScript AOT runtime — generated template modules depend on this package",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",