@doki-land/dejavu-engine 0.0.12 → 0.0.13
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/expr.d.ts +10 -10
- package/dist/runtime/expr.js +59 -40
- package/dist/runtime/filter.d.ts +2 -2
- package/dist/runtime/filter.js +67 -37
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/value.d.ts +6 -3
- package/dist/runtime/value.js +27 -6
- package/package.json +1 -1
package/dist/runtime/expr.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dejavu 1.0 支持的二元运算符。
|
|
3
|
-
* 与 Rust `dejavu-ir
|
|
3
|
+
* 与 Rust `dejavu-ir` 的 `eval_binary` 一一对应,**不借用 JS 隐式转换**:
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
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
|
-
* `!` 使用
|
|
16
|
+
* `!` 使用 Dejavu truthiness;`-` 只接受 number;`+` 是恒等。
|
|
16
17
|
*/
|
|
17
18
|
export declare function unary(op: string, a: unknown): unknown;
|
|
18
19
|
/**
|
|
19
|
-
* 成员访问 ——
|
|
20
|
-
* 对不存在的键返回 null(不是 undefined)。
|
|
20
|
+
* 成员访问 —— 对不存在的键返回 null(不是 undefined)。
|
|
21
21
|
*/
|
|
22
22
|
export declare function exprMember(obj: unknown, property: string): unknown;
|
|
23
23
|
/**
|
package/dist/runtime/expr.js
CHANGED
|
@@ -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
|
|
4
|
+
* 与 Rust `dejavu-ir` 的 `eval_binary` 一一对应,**不借用 JS 隐式转换**:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
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 === "
|
|
16
|
-
return
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
// eslint-disable-next-line eqeqeq
|
|
31
|
-
return l == r;
|
|
37
|
+
return valuesEqual(l, r);
|
|
32
38
|
case "!=":
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
53
|
-
return
|
|
54
|
-
|
|
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
|
-
* `!` 使用
|
|
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 -
|
|
89
|
+
return typeof a === "number" ? finite(-a) : null;
|
|
70
90
|
case "+":
|
|
71
|
-
return
|
|
91
|
+
return a;
|
|
72
92
|
default:
|
|
73
93
|
return null;
|
|
74
94
|
}
|
|
75
95
|
}
|
|
76
96
|
/**
|
|
77
|
-
* 成员访问 ——
|
|
78
|
-
* 对不存在的键返回 null(不是 undefined)。
|
|
97
|
+
* 成员访问 —— 对不存在的键返回 null(不是 undefined)。
|
|
79
98
|
*/
|
|
80
99
|
export function exprMember(obj, property) {
|
|
81
100
|
if (obj === null || obj === undefined)
|
package/dist/runtime/filter.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dejavu 1.0 合同过滤器集。
|
|
3
3
|
*
|
|
4
|
-
* 与 Rust `dejavu-
|
|
5
|
-
*
|
|
4
|
+
* 与 Rust `dejavu-ir` 的 `apply_filter` 逐项对齐,所有字符串强制转换都走
|
|
5
|
+
* `valueToString`(null → ""),不使用 JS 的 `String()`。
|
|
6
6
|
*
|
|
7
7
|
* 未识别的过滤器 **宽松返回原值**(与 Rust 渲染器一致,不抛错)。
|
|
8
8
|
*/
|
package/dist/runtime/filter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { htmlEscape } from "./escape.js";
|
|
2
|
-
import {
|
|
2
|
+
import { valueToString } from "./value.js";
|
|
3
3
|
/**
|
|
4
4
|
* Dejavu 1.0 合同过滤器集。
|
|
5
5
|
*
|
|
6
|
-
* 与 Rust `dejavu-
|
|
7
|
-
*
|
|
6
|
+
* 与 Rust `dejavu-ir` 的 `apply_filter` 逐项对齐,所有字符串强制转换都走
|
|
7
|
+
* `valueToString`(null → ""),不使用 JS 的 `String()`。
|
|
8
8
|
*
|
|
9
9
|
* 未识别的过滤器 **宽松返回原值**(与 Rust 渲染器一致,不抛错)。
|
|
10
10
|
*/
|
|
@@ -16,57 +16,87 @@ export function applyFilter(name, value, args) {
|
|
|
16
16
|
case "raw":
|
|
17
17
|
case "safe":
|
|
18
18
|
case "s":
|
|
19
|
-
return
|
|
19
|
+
return value;
|
|
20
20
|
case "uppercase":
|
|
21
21
|
case "upper":
|
|
22
|
-
return
|
|
22
|
+
return valueToString(value).toUpperCase();
|
|
23
23
|
case "lowercase":
|
|
24
24
|
case "lower":
|
|
25
|
-
return
|
|
25
|
+
return valueToString(value).toLowerCase();
|
|
26
26
|
case "trim":
|
|
27
|
-
return
|
|
28
|
-
case "default":
|
|
29
|
-
|
|
27
|
+
return valueToString(value).trim();
|
|
28
|
+
case "default": {
|
|
29
|
+
// 只有 null 与空串算缺失 —— 0 / false / [] 保留原值。
|
|
30
|
+
if (value === null || value === undefined || value === "") {
|
|
31
|
+
return args[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
30
35
|
case "length": {
|
|
31
|
-
if (value === null || value === undefined)
|
|
32
|
-
return 0;
|
|
33
|
-
if (Array.isArray(value))
|
|
34
|
-
return value.length;
|
|
35
36
|
if (typeof value === "string")
|
|
37
|
+
return [...value].length;
|
|
38
|
+
if (Array.isArray(value))
|
|
36
39
|
return value.length;
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
if (value !== null && typeof value === "object") {
|
|
41
|
+
return Object.keys(value).length;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
39
44
|
}
|
|
40
|
-
case "join":
|
|
45
|
+
case "join": {
|
|
46
|
+
const sep = valueToString(args[0] ?? ",");
|
|
41
47
|
return Array.isArray(value)
|
|
42
|
-
? value.join(
|
|
43
|
-
:
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
? value.map(valueToString).join(sep)
|
|
49
|
+
: valueToString(value);
|
|
50
|
+
}
|
|
51
|
+
case "replace": {
|
|
52
|
+
const from = valueToString(args[0] ?? "");
|
|
53
|
+
const to = valueToString(args[1] ?? "");
|
|
54
|
+
return valueToString(value).split(from).join(to);
|
|
55
|
+
}
|
|
46
56
|
case "date": {
|
|
47
|
-
|
|
48
|
-
|
|
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();
|
|
57
|
+
const pattern = valueToString(args[0] ?? "%Y-%m-%d");
|
|
58
|
+
return formatDate(valueToString(value), pattern);
|
|
63
59
|
}
|
|
64
60
|
case "slug":
|
|
65
|
-
return slugify(
|
|
61
|
+
return slugify(valueToString(value));
|
|
66
62
|
default:
|
|
67
63
|
return value;
|
|
68
64
|
}
|
|
69
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 与 Rust `format_date` 对齐的最小 strftime:`%Y %m %d %H %M %S %%`。
|
|
68
|
+
*
|
|
69
|
+
* 输入:空串 → "";`now` → 当前时间;纯数字 → epoch 毫秒;其他 → ISO 解析。
|
|
70
|
+
*/
|
|
71
|
+
function formatDate(input, pattern) {
|
|
72
|
+
if (input === "")
|
|
73
|
+
return "";
|
|
74
|
+
let ms;
|
|
75
|
+
if (input === "now") {
|
|
76
|
+
ms = Date.now();
|
|
77
|
+
}
|
|
78
|
+
else if (/^[0-9]+$/.test(input)) {
|
|
79
|
+
ms = Number(input);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
ms = Date.parse(input);
|
|
83
|
+
if (Number.isNaN(ms))
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
if (Number.isNaN(ms))
|
|
87
|
+
return "";
|
|
88
|
+
const d = new Date(ms);
|
|
89
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
90
|
+
return pattern
|
|
91
|
+
.replaceAll("%%", "\u0000")
|
|
92
|
+
.replaceAll("%Y", String(d.getUTCFullYear()))
|
|
93
|
+
.replaceAll("%m", pad(d.getUTCMonth() + 1))
|
|
94
|
+
.replaceAll("%d", pad(d.getUTCDate()))
|
|
95
|
+
.replaceAll("%H", pad(d.getUTCHours()))
|
|
96
|
+
.replaceAll("%M", pad(d.getUTCMinutes()))
|
|
97
|
+
.replaceAll("%S", pad(d.getUTCSeconds()))
|
|
98
|
+
.replaceAll("\u0000", "%");
|
|
99
|
+
}
|
|
70
100
|
/**
|
|
71
101
|
* 与 Rust `slug_filter` 对齐:
|
|
72
102
|
*
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
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
10
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
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
10
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
package/dist/runtime/value.d.ts
CHANGED
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function isTruthy(v: unknown): boolean;
|
|
13
13
|
/**
|
|
14
|
-
* 将任意 JS 值渲染为字符串。与 Rust
|
|
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 →
|
|
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;
|
package/dist/runtime/value.js
CHANGED
|
@@ -26,14 +26,13 @@ export function isTruthy(v) {
|
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* 将任意 JS 值渲染为字符串。与 Rust
|
|
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 →
|
|
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
|
+
}
|