@doki-land/dejavu-engine 0.0.13 → 0.0.15
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/errors.d.ts +15 -0
- package/dist/runtime/errors.js +22 -0
- package/dist/runtime/filter.d.ts +13 -2
- package/dist/runtime/filter.js +33 -10
- package/dist/runtime/function.d.ts +17 -0
- package/dist/runtime/function.js +91 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/test.d.ts +19 -0
- package/dist/runtime/test.js +87 -0
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured runtime error for generated AOT modules.
|
|
3
|
+
*
|
|
4
|
+
* Generated code never throws a bare `Error`: every failure carries the stable
|
|
5
|
+
* diagnostic `code`, the `render` stage, structured `args` and a human-readable
|
|
6
|
+
* message so hosts can classify errors without parsing `message`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class DejavuRuntimeError extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly stage = "render";
|
|
11
|
+
readonly args: Record<string, unknown>;
|
|
12
|
+
constructor(code: string, detail?: string, args?: Record<string, unknown>);
|
|
13
|
+
}
|
|
14
|
+
/** Build a structured render error. */
|
|
15
|
+
export declare function runtimeError(code: string, detail?: string, args?: Record<string, unknown>): DejavuRuntimeError;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured runtime error for generated AOT modules.
|
|
3
|
+
*
|
|
4
|
+
* Generated code never throws a bare `Error`: every failure carries the stable
|
|
5
|
+
* diagnostic `code`, the `render` stage, structured `args` and a human-readable
|
|
6
|
+
* message so hosts can classify errors without parsing `message`.
|
|
7
|
+
*/
|
|
8
|
+
export class DejavuRuntimeError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
stage = "render";
|
|
11
|
+
args;
|
|
12
|
+
constructor(code, detail, args) {
|
|
13
|
+
super(detail ? `${code}: ${detail}` : code);
|
|
14
|
+
this.name = "DejavuRuntimeError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.args = args ?? {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Build a structured render error. */
|
|
20
|
+
export function runtimeError(code, detail, args) {
|
|
21
|
+
return new DejavuRuntimeError(code, detail, args);
|
|
22
|
+
}
|
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,43 @@
|
|
|
1
1
|
import { htmlEscape } from "./escape.js";
|
|
2
|
+
import { runtimeError } from "./errors.js";
|
|
2
3
|
import { valueToString } from "./value.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/** 与 Rust `dejavu_ir::registry::FILTERS` 同序同构的静态描述。 */
|
|
5
|
+
export const FILTERS = [
|
|
6
|
+
{ name: "uppercase", aliases: ["upper"], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
7
|
+
{ name: "lowercase", aliases: ["lower"], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
8
|
+
{ name: "trim", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
9
|
+
{ name: "default", aliases: [], minArgs: 1, maxArgs: 1, capability: "pure" },
|
|
10
|
+
{ name: "length", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
11
|
+
{ name: "join", aliases: [], minArgs: 0, maxArgs: 1, capability: "pure" },
|
|
12
|
+
{ name: "replace", aliases: [], minArgs: 2, maxArgs: 2, capability: "pure" },
|
|
13
|
+
{ name: "escape", aliases: ["e"], minArgs: 0, maxArgs: 0, capability: "safe-output" },
|
|
14
|
+
{ name: "safe", aliases: ["raw"], minArgs: 0, maxArgs: 0, capability: "safe-output" },
|
|
15
|
+
{ name: "date", aliases: [], minArgs: 0, maxArgs: 1, capability: "pure" },
|
|
16
|
+
{ name: "slug", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
17
|
+
];
|
|
18
|
+
export function filterSpec(name) {
|
|
19
|
+
return FILTERS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
20
|
+
}
|
|
11
21
|
export function applyFilter(name, value, args) {
|
|
22
|
+
const spec = filterSpec(name);
|
|
23
|
+
if (!spec) {
|
|
24
|
+
throw runtimeError("dejavu.template.unknown-filter", name, { filter: name });
|
|
25
|
+
}
|
|
26
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
27
|
+
throw runtimeError("dejavu.template.filter-arity", `${spec.name} expects ` +
|
|
28
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`, {
|
|
29
|
+
filter: spec.name,
|
|
30
|
+
minArgs: spec.minArgs,
|
|
31
|
+
maxArgs: spec.maxArgs,
|
|
32
|
+
got: args.length,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
12
35
|
switch (name) {
|
|
13
36
|
case "escape":
|
|
14
37
|
case "e":
|
|
15
38
|
return htmlEscape(valueToString(value));
|
|
16
39
|
case "raw":
|
|
17
40
|
case "safe":
|
|
18
|
-
case "s":
|
|
19
41
|
return value;
|
|
20
42
|
case "uppercase":
|
|
21
43
|
case "upper":
|
|
@@ -60,7 +82,8 @@ export function applyFilter(name, value, args) {
|
|
|
60
82
|
case "slug":
|
|
61
83
|
return slugify(valueToString(value));
|
|
62
84
|
default:
|
|
63
|
-
|
|
85
|
+
// Unreachable: `filterSpec` already rejected unknown names.
|
|
86
|
+
throw runtimeError("dejavu.template.unknown-filter", name);
|
|
64
87
|
}
|
|
65
88
|
}
|
|
66
89
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
capability: string;
|
|
13
|
+
}
|
|
14
|
+
/** 与 Rust `dejavu_ir::registry::FUNCTIONS` 同序同构的静态描述。 */
|
|
15
|
+
export declare const FUNCTIONS: readonly FunctionSpec[];
|
|
16
|
+
export declare function functionSpec(name: string): FunctionSpec | undefined;
|
|
17
|
+
export declare function callFunction(name: string, args: unknown[]): unknown;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dejavu `function` 命名空间:语言中立的静态表 + 纯函数实现。
|
|
3
|
+
*
|
|
4
|
+
* 与 Rust `dejavu_ir::registry::FUNCTIONS` 和 `builtins::call_function` 对齐。
|
|
5
|
+
* 仅允许 `name(args)` 形式的裸标识符调用;成员调用与未知名字一律报错。
|
|
6
|
+
*/
|
|
7
|
+
import { runtimeError } from "./errors.js";
|
|
8
|
+
/** 与 Rust `dejavu_ir::registry::FUNCTIONS` 同序同构的静态描述。 */
|
|
9
|
+
export const FUNCTIONS = [
|
|
10
|
+
{ name: "range", aliases: [], minArgs: 2, maxArgs: 3, capability: "pure" },
|
|
11
|
+
{ name: "min", aliases: [], minArgs: 2, maxArgs: 2, capability: "pure" },
|
|
12
|
+
{ name: "max", aliases: [], minArgs: 2, maxArgs: 2, capability: "pure" },
|
|
13
|
+
];
|
|
14
|
+
/** `range` 允许产出的最大元素数,与 Rust `RANGE_LIMIT` 一致。 */
|
|
15
|
+
const RANGE_LIMIT = 10_000;
|
|
16
|
+
export function functionSpec(name) {
|
|
17
|
+
return FUNCTIONS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
18
|
+
}
|
|
19
|
+
export function callFunction(name, args) {
|
|
20
|
+
const spec = functionSpec(name);
|
|
21
|
+
if (!spec) {
|
|
22
|
+
throw runtimeError("dejavu.template.unknown-function", name, { function: name });
|
|
23
|
+
}
|
|
24
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
25
|
+
throw runtimeError("dejavu.template.function-arity", `${spec.name} expects ` +
|
|
26
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`, {
|
|
27
|
+
function: spec.name,
|
|
28
|
+
minArgs: spec.minArgs,
|
|
29
|
+
maxArgs: spec.maxArgs,
|
|
30
|
+
got: args.length,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
switch (spec.name) {
|
|
34
|
+
case "range":
|
|
35
|
+
return range(args);
|
|
36
|
+
case "min":
|
|
37
|
+
return numericPair(args, true);
|
|
38
|
+
case "max":
|
|
39
|
+
return numericPair(args, false);
|
|
40
|
+
default:
|
|
41
|
+
// Unreachable: `functionSpec` already rejected unknown names.
|
|
42
|
+
throw runtimeError("dejavu.template.unknown-function", name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** `range(start, stop, step=1)`,非法输入按值模型规则返回 null。 */
|
|
46
|
+
function range(args) {
|
|
47
|
+
const start = integral(args[0]);
|
|
48
|
+
if (start === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const stop = integral(args[1]);
|
|
51
|
+
if (stop === undefined)
|
|
52
|
+
return null;
|
|
53
|
+
let step = 1;
|
|
54
|
+
if (args.length > 2) {
|
|
55
|
+
const parsed = integral(args[2]);
|
|
56
|
+
if (parsed === undefined || parsed === 0)
|
|
57
|
+
return null;
|
|
58
|
+
step = parsed;
|
|
59
|
+
}
|
|
60
|
+
const out = [];
|
|
61
|
+
let current = start;
|
|
62
|
+
while ((step > 0 && current < stop) ||
|
|
63
|
+
(step < 0 && current > stop)) {
|
|
64
|
+
if (out.length >= RANGE_LIMIT)
|
|
65
|
+
return null;
|
|
66
|
+
out.push(current);
|
|
67
|
+
current += step;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** `min(a, b)` / `max(a, b)`,仅接受有限数字。 */
|
|
72
|
+
function numericPair(args, min) {
|
|
73
|
+
const a = finiteNumber(args[0]);
|
|
74
|
+
const b = finiteNumber(args[1]);
|
|
75
|
+
if (a === undefined || b === undefined)
|
|
76
|
+
return null;
|
|
77
|
+
return min ? Math.min(a, b) : Math.max(a, b);
|
|
78
|
+
}
|
|
79
|
+
function finiteNumber(value) {
|
|
80
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
81
|
+
return undefined;
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
/** 整数(与 Rust `integral` 对齐:非整数、非数字、越界都视为非法)。 */
|
|
85
|
+
function integral(value) {
|
|
86
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
87
|
+
return undefined;
|
|
88
|
+
if (value < -(2 ** 63) || value > 2 ** 63 - 1)
|
|
89
|
+
return undefined;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 与 Rust `dejavu-codegen` Node stub 以及 `dejavu-types` 的 filter 系统语义一致。
|
|
6
6
|
*/
|
|
7
7
|
export { htmlEscape } from "./escape.js";
|
|
8
|
+
export { DejavuRuntimeError, runtimeError } from "./errors.js";
|
|
8
9
|
export { valueToString, isTruthy, valuesEqual } from "./value.js";
|
|
9
10
|
export { applyFilter } from "./filter.js";
|
|
11
|
+
export { FUNCTIONS, functionSpec, callFunction } from "./function.js";
|
|
12
|
+
export { TESTS, testSpec, applyTest } from "./test.js";
|
|
10
13
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* 与 Rust `dejavu-codegen` Node stub 以及 `dejavu-types` 的 filter 系统语义一致。
|
|
6
6
|
*/
|
|
7
7
|
export { htmlEscape } from "./escape.js";
|
|
8
|
+
export { DejavuRuntimeError, runtimeError } from "./errors.js";
|
|
8
9
|
export { valueToString, isTruthy, valuesEqual } from "./value.js";
|
|
9
10
|
export { applyFilter } from "./filter.js";
|
|
11
|
+
export { FUNCTIONS, functionSpec, callFunction } from "./function.js";
|
|
12
|
+
export { TESTS, testSpec, applyTest } from "./test.js";
|
|
10
13
|
export { binary, unary, exprMember, exprIndex } from "./expr.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
capability: string;
|
|
14
|
+
}
|
|
15
|
+
/** 与 Rust `dejavu_ir::registry::TESTS` 同序同构的静态描述。 */
|
|
16
|
+
export declare const TESTS: readonly TestSpec[];
|
|
17
|
+
export declare function testSpec(name: string): TestSpec | undefined;
|
|
18
|
+
/** 对已求值的左值执行谓词;`negated` 为 `is not` 形式。 */
|
|
19
|
+
export declare function applyTest(name: string, value: unknown, args: unknown[], negated?: boolean): boolean;
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { runtimeError } from "./errors.js";
|
|
9
|
+
import { isTruthy } from "./value.js";
|
|
10
|
+
/** 与 Rust `dejavu_ir::registry::TESTS` 同序同构的静态描述。 */
|
|
11
|
+
export const TESTS = [
|
|
12
|
+
{ name: "null", aliases: ["none"], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
13
|
+
{ name: "truthy", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
14
|
+
{ name: "empty", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
15
|
+
{ name: "array", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
16
|
+
{ name: "object", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
17
|
+
{ name: "string", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
18
|
+
{ name: "number", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
19
|
+
{ name: "boolean", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
20
|
+
{ name: "even", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
21
|
+
{ name: "odd", aliases: [], minArgs: 0, maxArgs: 0, capability: "pure" },
|
|
22
|
+
];
|
|
23
|
+
export function testSpec(name) {
|
|
24
|
+
return TESTS.find((spec) => spec.name === name || spec.aliases.includes(name));
|
|
25
|
+
}
|
|
26
|
+
/** 对已求值的左值执行谓词;`negated` 为 `is not` 形式。 */
|
|
27
|
+
export function applyTest(name, value, args, negated = false) {
|
|
28
|
+
const spec = testSpec(name);
|
|
29
|
+
if (!spec) {
|
|
30
|
+
throw runtimeError("dejavu.template.unknown-test", name, { test: name });
|
|
31
|
+
}
|
|
32
|
+
if (args.length < spec.minArgs || args.length > spec.maxArgs) {
|
|
33
|
+
throw runtimeError("dejavu.template.test-arity", `${spec.name} expects ` +
|
|
34
|
+
`${spec.minArgs}..${spec.maxArgs} argument(s), got ${args.length}`, {
|
|
35
|
+
test: spec.name,
|
|
36
|
+
minArgs: spec.minArgs,
|
|
37
|
+
maxArgs: spec.maxArgs,
|
|
38
|
+
got: args.length,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
let result;
|
|
42
|
+
switch (spec.name) {
|
|
43
|
+
case "null":
|
|
44
|
+
result = value === null || value === undefined;
|
|
45
|
+
break;
|
|
46
|
+
case "truthy":
|
|
47
|
+
result = isTruthy(value);
|
|
48
|
+
break;
|
|
49
|
+
case "empty":
|
|
50
|
+
result =
|
|
51
|
+
value === ""
|
|
52
|
+
|| (Array.isArray(value) && value.length === 0)
|
|
53
|
+
|| (isPlainObject(value) && Object.keys(value).length === 0);
|
|
54
|
+
break;
|
|
55
|
+
case "array":
|
|
56
|
+
result = Array.isArray(value);
|
|
57
|
+
break;
|
|
58
|
+
case "object":
|
|
59
|
+
result = isPlainObject(value);
|
|
60
|
+
break;
|
|
61
|
+
case "string":
|
|
62
|
+
result = typeof value === "string";
|
|
63
|
+
break;
|
|
64
|
+
case "number":
|
|
65
|
+
result = typeof value === "number";
|
|
66
|
+
break;
|
|
67
|
+
case "boolean":
|
|
68
|
+
result = typeof value === "boolean";
|
|
69
|
+
break;
|
|
70
|
+
case "even":
|
|
71
|
+
result = isInteger(value) && value % 2 === 0;
|
|
72
|
+
break;
|
|
73
|
+
case "odd":
|
|
74
|
+
result = isInteger(value) && value % 2 !== 0;
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
// Unreachable: `testSpec` already rejected unknown names.
|
|
78
|
+
throw runtimeError("dejavu.template.unknown-test", name);
|
|
79
|
+
}
|
|
80
|
+
return result !== negated;
|
|
81
|
+
}
|
|
82
|
+
function isPlainObject(value) {
|
|
83
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
function isInteger(value) {
|
|
86
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
87
|
+
}
|