@coldsmirk/abacus-core 0.3.1 → 0.4.1
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 +1 -1
- package/dist/index.cjs +0 -345
- package/dist/index.d.cts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -345
- package/package.json +2 -2
- package/LICENSE +0 -201
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
//#region src/engine/errors.ts
|
|
2
|
-
/**
|
|
3
|
-
* Error raised when the ZEN engine fails to load or an expression cannot be
|
|
4
|
-
* evaluated. The original failure is preserved on {@link cause} and the
|
|
5
|
-
* offending expression (if any) on {@link expression}.
|
|
6
|
-
*/
|
|
7
2
|
var ExpressionError = class extends Error {
|
|
8
3
|
expression;
|
|
9
4
|
constructor(message, expression, cause) {
|
|
@@ -12,11 +7,6 @@ var ExpressionError = class extends Error {
|
|
|
12
7
|
this.expression = expression;
|
|
13
8
|
}
|
|
14
9
|
};
|
|
15
|
-
/**
|
|
16
|
-
* Raised by the synchronous evaluation helpers when the engine has not finished
|
|
17
|
-
* initializing yet. Await {@link loadEngine} (or render under
|
|
18
|
-
* `<ExpressionEngineProvider>`) before evaluating synchronously.
|
|
19
|
-
*/
|
|
20
10
|
var ExpressionNotReadyError = class extends ExpressionError {
|
|
21
11
|
constructor(message = "Expression engine is not initialized. Await loadEngine() or render under <ExpressionEngineProvider>.") {
|
|
22
12
|
super(message);
|
|
@@ -25,11 +15,6 @@ var ExpressionNotReadyError = class extends ExpressionError {
|
|
|
25
15
|
};
|
|
26
16
|
//#endregion
|
|
27
17
|
//#region src/internal/predicates.ts
|
|
28
|
-
/**
|
|
29
|
-
* Minimal internal type predicates. Inlined so the engine stays dependency-free
|
|
30
|
-
* (no shared-utility package), keeping `@coldsmirk/abacus-core` framework- and
|
|
31
|
-
* ecosystem-agnostic.
|
|
32
|
-
*/
|
|
33
18
|
function isUndefined(value) {
|
|
34
19
|
return value === void 0;
|
|
35
20
|
}
|
|
@@ -64,32 +49,10 @@ function loadFailureMessage() {
|
|
|
64
49
|
if (typeof window === "undefined") return `${base}. In a non-browser or non-DOM host (Node, SSR, Web Worker) the wasm cannot be auto-resolved; call configureEngine({ wasmInput }) with the wasm bytes or URL before loading.`;
|
|
65
50
|
return base;
|
|
66
51
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Configure how the wasm binary is located, before the engine loads. Must be
|
|
69
|
-
* called before the first {@link loadEngine} (or any evaluation), so that the
|
|
70
|
-
* configured input is the one actually used — calling it after the engine has
|
|
71
|
-
* started loading throws, rather than silently taking no effect.
|
|
72
|
-
*/
|
|
73
52
|
function configureEngine(options) {
|
|
74
53
|
if (enginePromise || engineSync) throw new ExpressionError("configureEngine() must be called before the engine loads.");
|
|
75
54
|
configuredInput = options.wasmInput;
|
|
76
55
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Load and initialize the ZEN expression engine exactly once. Concurrent and
|
|
79
|
-
* subsequent calls share the same in-flight promise / resolved instance. Set a
|
|
80
|
-
* custom wasm source up front with {@link configureEngine}.
|
|
81
|
-
*
|
|
82
|
-
* The GoRules wasm dependency is reached via a dynamic `import()` on purpose: it
|
|
83
|
-
* keeps this module usable from the package's CommonJS build (the dep is
|
|
84
|
-
* ESM-only, so a static `require` would throw) and defers the multi-megabyte
|
|
85
|
-
* wasm download until the engine is actually needed. Do NOT convert this to a
|
|
86
|
-
* static import.
|
|
87
|
-
*
|
|
88
|
-
* A failed load is not cached: the rejected promise is dropped (the error is
|
|
89
|
-
* latched on {@link getEngineError} for inspection), so calling `loadEngine()`
|
|
90
|
-
* again retries from scratch. This is the clear-and-reload retry primitive after
|
|
91
|
-
* a transient failure — an error boundary should call it before resetting.
|
|
92
|
-
*/
|
|
93
56
|
function loadEngine() {
|
|
94
57
|
if (enginePromise) return enginePromise;
|
|
95
58
|
engineError = null;
|
|
@@ -153,35 +116,16 @@ function loadEngine() {
|
|
|
153
116
|
});
|
|
154
117
|
return enginePromise;
|
|
155
118
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Whether the engine has finished initializing and is ready for sync use.
|
|
158
|
-
*/
|
|
159
119
|
function isEngineReady() {
|
|
160
120
|
return engineSync?.isReady() ?? false;
|
|
161
121
|
}
|
|
162
|
-
/**
|
|
163
|
-
* The error from the last failed {@link loadEngine} attempt, or `null`. Used by
|
|
164
|
-
* the React provider to surface a wasm-load failure to an error boundary rather
|
|
165
|
-
* than suspending forever, and by imperative pollers to tell "failed" apart from
|
|
166
|
-
* "still loading". Cleared when a new load starts or {@link resetEngine}.
|
|
167
|
-
*/
|
|
168
122
|
function getEngineError() {
|
|
169
123
|
return engineError;
|
|
170
124
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Return the already-initialized engine synchronously, or throw
|
|
173
|
-
* {@link ExpressionNotReadyError} if {@link loadEngine} has not resolved yet.
|
|
174
|
-
*/
|
|
175
125
|
function getEngineSync() {
|
|
176
126
|
if (!engineSync) throw new ExpressionNotReadyError();
|
|
177
127
|
return engineSync;
|
|
178
128
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Reset the engine singleton — drops the loaded engine, the cached type context,
|
|
181
|
-
* the latched error, and the configured wasm input. Mainly for tests, but also the
|
|
182
|
-
* way to re-run {@link configureEngine} after a load (configure throws once the
|
|
183
|
-
* engine has started loading).
|
|
184
|
-
*/
|
|
185
129
|
function resetEngine() {
|
|
186
130
|
typeContextCache?.handle.free();
|
|
187
131
|
typeContextCache = null;
|
|
@@ -192,16 +136,6 @@ function resetEngine() {
|
|
|
192
136
|
}
|
|
193
137
|
//#endregion
|
|
194
138
|
//#region src/internal/env.ts
|
|
195
|
-
/**
|
|
196
|
-
* Best-effort development-mode flag for diagnostic-only code paths.
|
|
197
|
-
*
|
|
198
|
-
* Detected from `process.env.NODE_ENV` (defined by Node, test runners, webpack,
|
|
199
|
-
* and Vite's SSR/`define`). The `typeof` guard is load-bearing: in a plain
|
|
200
|
-
* browser bundle `process` is not declared at all, and any bare reference would
|
|
201
|
-
* throw a ReferenceError while this module is being imported. Hosts without a
|
|
202
|
-
* `process` default to "production" — the safe choice, since `isDev` only gates
|
|
203
|
-
* extra developer-facing warnings.
|
|
204
|
-
*/
|
|
205
139
|
const isDev = detectDev();
|
|
206
140
|
function detectDev() {
|
|
207
141
|
if (typeof process === "undefined") return false;
|
|
@@ -209,14 +143,6 @@ function detectDev() {
|
|
|
209
143
|
}
|
|
210
144
|
//#endregion
|
|
211
145
|
//#region src/condition/subject.ts
|
|
212
|
-
/**
|
|
213
|
-
* Shared subject-path guard for the condition compilers. A subject / left-hand
|
|
214
|
-
* path is emitted **verbatim** into ZEN source, so both {@link compileCondition}
|
|
215
|
-
* (which writes that source) and {@link liftConditionTree} (which reads it back)
|
|
216
|
-
* gate paths through this one predicate — a single definition of "what is a safe
|
|
217
|
-
* field path" that cannot drift between the writer and the reader, which matters
|
|
218
|
-
* because the guard is also the compiler's injection defense.
|
|
219
|
-
*/
|
|
220
146
|
const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
|
|
221
147
|
const ZEN_RESERVED_WORDS = new Set([
|
|
222
148
|
"and",
|
|
@@ -227,26 +153,11 @@ const ZEN_RESERVED_WORDS = new Set([
|
|
|
227
153
|
"false",
|
|
228
154
|
"null"
|
|
229
155
|
]);
|
|
230
|
-
/**
|
|
231
|
-
* Whether `subject` is a plain identifier path safe to emit verbatim into ZEN
|
|
232
|
-
* source: dotted/indexed segments only, none of which is a ZEN reserved word.
|
|
233
|
-
*/
|
|
234
156
|
function isIdentifierPath(subject) {
|
|
235
157
|
return SUBJECT_PATTERN.test(subject) && (subject.match(/[A-Z_$][\w$]*/gi) ?? []).every((segment) => !ZEN_RESERVED_WORDS.has(segment));
|
|
236
158
|
}
|
|
237
159
|
//#endregion
|
|
238
160
|
//#region src/condition/compile.ts
|
|
239
|
-
/**
|
|
240
|
-
* Serialize a JavaScript value into a ZEN literal. Nullish becomes `null`;
|
|
241
|
-
* numbers / booleans / bigints are emitted verbatim; strings are quoted via
|
|
242
|
-
* {@link encodeZenString}; arrays become `[a, b, ...]`.
|
|
243
|
-
*
|
|
244
|
-
* Throws {@link ExpressionError} for a value with no faithful ZEN
|
|
245
|
-
* representation — an object, symbol, or function, or a string containing both
|
|
246
|
-
* quote styles. Callers that need a sentinel instead of a throw go through
|
|
247
|
-
* {@link compileCondition}, which degrades such a value to a non-compiling
|
|
248
|
-
* (null) condition.
|
|
249
|
-
*/
|
|
250
161
|
function toZenLiteral(value) {
|
|
251
162
|
if (isNullish(value)) return "null";
|
|
252
163
|
if (typeof value === "number") {
|
|
@@ -258,13 +169,6 @@ function toZenLiteral(value) {
|
|
|
258
169
|
if (isArray(value)) return `[${value.map((item) => toZenLiteral(item)).join(", ")}]`;
|
|
259
170
|
throw new ExpressionError(`Value of type "${typeof value}" has no ZEN literal representation`);
|
|
260
171
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Encode a string as a ZEN literal. ZEN string literals are **raw** between
|
|
263
|
-
* matching quotes and honor no backslash escapes (`'a\nb'` is the four
|
|
264
|
-
* characters `a \ n b`), so the encoder must not escape — it picks a delimiter
|
|
265
|
-
* the value does not contain. A value containing both quote styles cannot be
|
|
266
|
-
* represented as a raw ZEN literal and throws.
|
|
267
|
-
*/
|
|
268
172
|
function encodeZenString(value) {
|
|
269
173
|
const hasSingle = value.includes("'");
|
|
270
174
|
const hasDouble = value.includes("\"");
|
|
@@ -274,15 +178,6 @@ function encodeZenString(value) {
|
|
|
274
178
|
function toArrayLiteral(value) {
|
|
275
179
|
return isArray(value) ? toZenLiteral(value) : `[${toZenLiteral(value)}]`;
|
|
276
180
|
}
|
|
277
|
-
/**
|
|
278
|
-
* Emit the ZEN emptiness test matching the backend field evaluator's
|
|
279
|
-
* `isEmptyValue`: null, blank (whitespace-only) text, and empty arrays are
|
|
280
|
-
* empty; numbers and booleans never are. ZEN's `or` short-circuits, so the
|
|
281
|
-
* type-guarded branches never evaluate against a null subject. The backend
|
|
282
|
-
* additionally treats an empty map as empty for totality, but form values —
|
|
283
|
-
* the only subjects this compiler targets — are never objects, and ZEN's
|
|
284
|
-
* `len()` does not accept one.
|
|
285
|
-
*/
|
|
286
181
|
function zenIsEmpty(subject) {
|
|
287
182
|
return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0))`;
|
|
288
183
|
}
|
|
@@ -305,12 +200,6 @@ function compileFieldCondition(subject, operator, value) {
|
|
|
305
200
|
default: throw new ExpressionError(`Unsupported operator: ${String(operator)}`);
|
|
306
201
|
}
|
|
307
202
|
}
|
|
308
|
-
/**
|
|
309
|
-
* Compile a single condition into a ZEN boolean expression. Field conditions map
|
|
310
|
-
* their operator to ZEN; expression conditions are wrapped in parentheses to
|
|
311
|
-
* preserve their grouping. Returns `null` when the condition is empty, its
|
|
312
|
-
* subject is not an identifier path, or its value has no ZEN representation.
|
|
313
|
-
*/
|
|
314
203
|
function compileCondition(condition) {
|
|
315
204
|
if (condition.kind === "expression") {
|
|
316
205
|
const expression = condition.expression.trim();
|
|
@@ -324,34 +213,14 @@ function compileCondition(condition) {
|
|
|
324
213
|
return null;
|
|
325
214
|
}
|
|
326
215
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Compile a condition group (its conditions joined with AND). Returns `null`
|
|
329
|
-
* when the group has no compilable conditions.
|
|
330
|
-
*/
|
|
331
216
|
function compileGroup(group) {
|
|
332
217
|
const parts = group.conditions.map((condition) => compileCondition(condition)).filter((part) => part !== null);
|
|
333
218
|
return parts.length === 0 ? null : parts.join(" and ");
|
|
334
219
|
}
|
|
335
|
-
/**
|
|
336
|
-
* Compile a branch's condition groups into a single ZEN expression (groups
|
|
337
|
-
* joined with OR). Returns `null` when the branch has no compilable groups
|
|
338
|
-
* (e.g. a default branch).
|
|
339
|
-
*/
|
|
340
220
|
function compileBranch(branch) {
|
|
341
221
|
const groups = (branch.conditionGroups ?? []).map((group) => compileGroup(group)).filter((group) => group !== null);
|
|
342
222
|
return groups.length === 0 ? null : groups.map((group) => `(${group})`).join(" or ");
|
|
343
223
|
}
|
|
344
|
-
/**
|
|
345
|
-
* Pick the matching branch for the given context using a pre-loaded engine.
|
|
346
|
-
* Non-default branches are tested in ascending `priority` order; the first whose
|
|
347
|
-
* compiled expression evaluates to `true` wins. Falls back to the default
|
|
348
|
-
* branch, or a `null` id when neither matches.
|
|
349
|
-
*
|
|
350
|
-
* A branch whose expression throws for the given context (e.g. ZEN's `>`
|
|
351
|
-
* throws when the subject is missing or non-numeric) is treated as not matching
|
|
352
|
-
* rather than propagating — so a missing field degrades to the default branch
|
|
353
|
-
* instead of crashing the caller.
|
|
354
|
-
*/
|
|
355
224
|
function selectBranchWith(branches, context, engine) {
|
|
356
225
|
const ordered = branches.toSorted((a, b) => a.priority - b.priority);
|
|
357
226
|
for (const branch of ordered) {
|
|
@@ -388,30 +257,11 @@ function reportEvaluationFailure(engine, expression, error) {
|
|
|
388
257
|
}
|
|
389
258
|
if (!isNullish(diagnostic)) console.warn(`[expression] compiled branch expression failed to parse: ${expression}`, diagnostic, error);
|
|
390
259
|
}
|
|
391
|
-
/**
|
|
392
|
-
* Pick the matching branch for the given context, loading the ZEN engine on
|
|
393
|
-
* first use. See {@link selectBranchWith} for the selection semantics.
|
|
394
|
-
*/
|
|
395
260
|
async function selectBranch(branches, context) {
|
|
396
261
|
return selectBranchWith(branches, context, await loadEngine());
|
|
397
262
|
}
|
|
398
263
|
//#endregion
|
|
399
264
|
//#region src/condition/types.ts
|
|
400
|
-
/**
|
|
401
|
-
* Structural shapes for the visual condition model compiled to ZEN. They mirror
|
|
402
|
-
* the condition types used in the form and flow editors so a host can map those
|
|
403
|
-
* definitions to {@link compileCondition} / {@link selectBranch} without this
|
|
404
|
-
* package depending on either editor. The editors keep a flat working model for
|
|
405
|
-
* form ergonomics (a row can hold both a half-typed field triple and an
|
|
406
|
-
* expression while the author toggles between them); this discriminated input is
|
|
407
|
-
* the narrowed, compiler-facing shape where each kind carries only what it uses.
|
|
408
|
-
*/
|
|
409
|
-
/**
|
|
410
|
-
* The closed operator vocabulary understood by {@link compileCondition}, as a
|
|
411
|
-
* runtime constant so validators can build allow-lists from it instead of
|
|
412
|
-
* re-declaring the set. The {@link ConditionOperator} type derives from this
|
|
413
|
-
* array — one definition site for both the type and the runtime list.
|
|
414
|
-
*/
|
|
415
265
|
const CONDITION_OPERATORS = [
|
|
416
266
|
"eq",
|
|
417
267
|
"ne",
|
|
@@ -444,32 +294,15 @@ const CONDITION_OPERATOR_ARITIES = {
|
|
|
444
294
|
is_empty: "none",
|
|
445
295
|
is_not_empty: "none"
|
|
446
296
|
};
|
|
447
|
-
/**
|
|
448
|
-
* The arity of `operator`'s right-hand operand (see
|
|
449
|
-
* {@link ConditionOperatorArity}). One definition site next to
|
|
450
|
-
* {@link CONDITION_OPERATORS}, so the tree compiler's arity enforcement and a
|
|
451
|
-
* condition editor's operand controls classify operators identically instead of
|
|
452
|
-
* each keeping a drift-prone copy.
|
|
453
|
-
*/
|
|
454
297
|
function conditionOperatorArity(operator) {
|
|
455
298
|
return CONDITION_OPERATOR_ARITIES[operator];
|
|
456
299
|
}
|
|
457
300
|
//#endregion
|
|
458
301
|
//#region src/condition/compile-tree.ts
|
|
459
|
-
/**
|
|
460
|
-
* Compile a condition tree to a canonical ZEN expression, or `""` when no rule in
|
|
461
|
-
* the tree is compilable (see the module note for the drop semantics).
|
|
462
|
-
*/
|
|
463
302
|
function compileConditionTree(tree) {
|
|
464
303
|
const normalized = normalizeNode(tree);
|
|
465
304
|
return normalized === null ? "" : emitNode(normalized, true);
|
|
466
305
|
}
|
|
467
|
-
/**
|
|
468
|
-
* Prune a node to its compilable core: drop rules {@link compileRule} rejects, drop
|
|
469
|
-
* emptied groups, and collapse a single-surviving-item group to that item (so the
|
|
470
|
-
* emitted shape carries no redundant parentheses). Returns `null` when nothing in
|
|
471
|
-
* the node survives.
|
|
472
|
-
*/
|
|
473
306
|
function normalizeNode(node) {
|
|
474
307
|
if (node.kind === "rule") return compileRule(node) === null ? null : node;
|
|
475
308
|
const items = node.items.map((item) => normalizeNode(item)).filter((item) => item !== null);
|
|
@@ -481,27 +314,11 @@ function normalizeNode(node) {
|
|
|
481
314
|
items
|
|
482
315
|
};
|
|
483
316
|
}
|
|
484
|
-
/**
|
|
485
|
-
* Render a normalized node. A rule emits its lowered ZEN; a group joins its items
|
|
486
|
-
* with ` and ` / ` or ` and, unless it is the top-level group, wraps them in
|
|
487
|
-
* parentheses so the tree structure survives ZEN's operator precedence on lift.
|
|
488
|
-
*/
|
|
489
317
|
function emitNode(node, topLevel) {
|
|
490
318
|
if (node.kind === "rule") return compileRule(node);
|
|
491
319
|
const joined = node.items.map((item) => emitNode(item, false)).join(node.op === "and" ? " and " : " or ");
|
|
492
320
|
return topLevel ? joined : `(${joined})`;
|
|
493
321
|
}
|
|
494
|
-
/**
|
|
495
|
-
* Lower a leaf rule to ZEN via {@link compileCondition}, or `null` for a rule the
|
|
496
|
-
* compiler cannot represent. The rule's `right` must match its operator's arity —
|
|
497
|
-
* a single scalar for the comparison / string operators, an array of scalars for
|
|
498
|
-
* `in` / `not_in`, absent for the emptiness operators (the contract
|
|
499
|
-
* {@link ConditionTreeValue} documents); an off-arity rule is non-compilable and
|
|
500
|
-
* drops. Enforcing arity here, not just relying on `compileCondition`, is what
|
|
501
|
-
* keeps every emitted expression liftable: the flat compiler's lax value handling
|
|
502
|
-
* would happily serialize e.g. a missing `right` as a `null` literal, which is not
|
|
503
|
-
* part of the canonical grammar.
|
|
504
|
-
*/
|
|
505
322
|
function compileRule(rule) {
|
|
506
323
|
if (!matchesOperatorArity(rule)) return null;
|
|
507
324
|
return compileCondition({
|
|
@@ -562,13 +379,6 @@ const CALL_OPERATORS = {
|
|
|
562
379
|
endsWith: "ends_with"
|
|
563
380
|
};
|
|
564
381
|
const MAX_GROUP_DEPTH = 64;
|
|
565
|
-
/**
|
|
566
|
-
* Lift a ZEN expression to a condition tree, or `null` when it is not in the
|
|
567
|
-
* canonical form {@link compileConditionTree} produces (the consumer then keeps the
|
|
568
|
-
* raw expression). Groups nested deeper than 64 parenthesized levels are refused as
|
|
569
|
-
* non-canonical rather than risking parser-stack overflow on adversarial input. The
|
|
570
|
-
* returned root is always a group.
|
|
571
|
-
*/
|
|
572
382
|
function liftConditionTree(expression) {
|
|
573
383
|
const tokens = tokenize(expression);
|
|
574
384
|
if (tokens === null || tokens.length === 0) return null;
|
|
@@ -800,12 +610,6 @@ function liftConditionTree(expression) {
|
|
|
800
610
|
if (parsed === null || pos !== tokens.length) return null;
|
|
801
611
|
return asGroup(parsed);
|
|
802
612
|
}
|
|
803
|
-
/**
|
|
804
|
-
* Read a dotted/indexed identifier path starting at `from`, validated through
|
|
805
|
-
* {@link isIdentifierPath}. Pure over the token array (no cursor) so it can probe a
|
|
806
|
-
* candidate path — the emptiness oracle reads the blob's subject without committing
|
|
807
|
-
* the cursor. Returns the path and the index just past it, or `null`.
|
|
808
|
-
*/
|
|
809
613
|
function readPath(tokens, from) {
|
|
810
614
|
const head = tokens[from];
|
|
811
615
|
if (head === void 0 || head.kind !== "ident") return null;
|
|
@@ -843,13 +647,6 @@ function asGroup(node) {
|
|
|
843
647
|
function isPunct(token, value) {
|
|
844
648
|
return token !== void 0 && token.kind === "punct" && token.value === value;
|
|
845
649
|
}
|
|
846
|
-
/**
|
|
847
|
-
* Split a ZEN expression into tokens, or `null` on an unexpected character or an
|
|
848
|
-
* unterminated string. Whitespace is dropped, so the parser (and the emptiness
|
|
849
|
-
* oracle's token match) is insensitive to spacing. String literals are read raw
|
|
850
|
-
* between matching quotes — ZEN honors no backslash escapes, mirroring the encoder
|
|
851
|
-
* in {@link compileCondition}.
|
|
852
|
-
*/
|
|
853
650
|
function tokenize(input) {
|
|
854
651
|
const tokens = [];
|
|
855
652
|
let index = 0;
|
|
@@ -912,56 +709,20 @@ function tokenize(input) {
|
|
|
912
709
|
}
|
|
913
710
|
//#endregion
|
|
914
711
|
//#region src/condition/tree-types.ts
|
|
915
|
-
/**
|
|
916
|
-
* The visual condition **tree** model: an arbitrarily nested and/or tree of typed
|
|
917
|
-
* comparison rules that {@link compileConditionTree} serializes to a single ZEN
|
|
918
|
-
* boolean expression and {@link liftConditionTree} reconstructs from one. It is a
|
|
919
|
-
* distinct, self-contained shape from the compiler's flat {@link ConditionInput} —
|
|
920
|
-
* the tree is what a structured builder UI edits, whereas `ConditionInput` is the
|
|
921
|
-
* narrowed per-condition shape the compiler consumes. The two meet only at the
|
|
922
|
-
* leaf: a {@link ConditionTreeRule} lowers to a field {@link ConditionInput} so the
|
|
923
|
-
* operator vocabulary, literal encoding, and injection guard have one owner.
|
|
924
|
-
*/
|
|
925
|
-
/**
|
|
926
|
-
* The tree's operator vocabulary as a runtime list — the compiler's full
|
|
927
|
-
* {@link CONDITION_OPERATORS} set under the tree name. An alias, not a second
|
|
928
|
-
* hand-maintained list, so the tree vocabulary can never drift from the compiler's;
|
|
929
|
-
* it gives tree code and the builder UI one tree-named import for the operators they
|
|
930
|
-
* support.
|
|
931
|
-
*/
|
|
932
712
|
const CONDITION_TREE_OPERATORS = CONDITION_OPERATORS;
|
|
933
713
|
//#endregion
|
|
934
714
|
//#region src/engine/evaluate.ts
|
|
935
|
-
/**
|
|
936
|
-
* Evaluate a standard ZEN expression, loading the engine on first use.
|
|
937
|
-
*
|
|
938
|
-
* `T` is an **unchecked** assertion — the value is returned as `T` with no
|
|
939
|
-
* runtime validation, and ZEN's result type depends on the expression and
|
|
940
|
-
* context. Prefer the `unknown` default and narrow at the call site.
|
|
941
|
-
*/
|
|
942
715
|
async function evaluate(expression, context) {
|
|
943
716
|
await loadEngine();
|
|
944
717
|
return evaluateSync(expression, context);
|
|
945
718
|
}
|
|
946
|
-
/**
|
|
947
|
-
* Evaluate a ZEN unary (test) expression, loading the engine on first use.
|
|
948
|
-
*/
|
|
949
719
|
async function evaluateUnary(expression, context) {
|
|
950
720
|
await loadEngine();
|
|
951
721
|
return evaluateUnarySync(expression, context);
|
|
952
722
|
}
|
|
953
|
-
/**
|
|
954
|
-
* Evaluate a standard ZEN expression synchronously. Throws
|
|
955
|
-
* {@link ExpressionNotReadyError} when the engine has not loaded yet — use this
|
|
956
|
-
* only behind a readiness gate (e.g. under `<ExpressionEngineProvider>`).
|
|
957
|
-
*/
|
|
958
723
|
function evaluateSync(expression, context) {
|
|
959
724
|
return getEngineSync().evaluate(expression, context);
|
|
960
725
|
}
|
|
961
|
-
/**
|
|
962
|
-
* Evaluate a ZEN unary (test) expression synchronously. Throws
|
|
963
|
-
* {@link ExpressionNotReadyError} when the engine has not loaded yet.
|
|
964
|
-
*/
|
|
965
726
|
function evaluateUnarySync(expression, context) {
|
|
966
727
|
return getEngineSync().evaluateUnary(expression, context);
|
|
967
728
|
}
|
|
@@ -1070,9 +831,6 @@ const ZH_SOURCE_LABELS = {
|
|
|
1070
831
|
function sourceLabelFrom(table, fallback, type) {
|
|
1071
832
|
return (type === void 0 ? void 0 : table[type]) ?? fallback;
|
|
1072
833
|
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Built-in English message catalog (the default).
|
|
1075
|
-
*/
|
|
1076
834
|
const enMessages = {
|
|
1077
835
|
sourceLabel: (type) => sourceLabelFrom(EN_SOURCE_LABELS, "Error", type),
|
|
1078
836
|
completionInfo: (info) => info,
|
|
@@ -1080,9 +838,6 @@ const enMessages = {
|
|
|
1080
838
|
expectedBoolean: (actualType) => `Expected a boolean test expression, received \`${actualType}\`.`,
|
|
1081
839
|
expectedType: (expectedType, actualType) => `Expected \`${expectedType}\`, received \`${actualType}\`.`
|
|
1082
840
|
};
|
|
1083
|
-
/**
|
|
1084
|
-
* Built-in Simplified Chinese message catalog.
|
|
1085
|
-
*/
|
|
1086
841
|
const zhCNMessages = {
|
|
1087
842
|
sourceLabel: (type) => sourceLabelFrom(ZH_SOURCE_LABELS, "错误", type),
|
|
1088
843
|
completionInfo: (info) => info === "" ? "" : COMPLETION_INFO_ZH[info] ?? info,
|
|
@@ -1092,20 +847,9 @@ const zhCNMessages = {
|
|
|
1092
847
|
};
|
|
1093
848
|
const localeRegistry = new Map([["en-US", enMessages], ["zh-CN", zhCNMessages]]);
|
|
1094
849
|
let activeMessages = enMessages;
|
|
1095
|
-
/**
|
|
1096
|
-
* Register (or replace) the message catalog for a locale key, making it selectable
|
|
1097
|
-
* via {@link configureExpressionMessages}. This is how a host adds a language the
|
|
1098
|
-
* library does not ship — the built-in locales are registered the same way, so
|
|
1099
|
-
* there is no privileged path.
|
|
1100
|
-
*/
|
|
1101
850
|
function registerExpressionLocale(locale, messages) {
|
|
1102
851
|
localeRegistry.set(locale, messages);
|
|
1103
852
|
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Configure the active message catalog. Pass a `locale`, a partial `messages`
|
|
1106
|
-
* override, or both (overrides win). Idempotent and module-global, so a host
|
|
1107
|
-
* configures it once (e.g. through `ExpressionConfigProvider`).
|
|
1108
|
-
*/
|
|
1109
853
|
function configureExpressionMessages({ locale, messages }) {
|
|
1110
854
|
const base = locale === void 0 ? activeMessages : localeRegistry.get(locale) ?? activeMessages;
|
|
1111
855
|
activeMessages = messages === void 0 ? base : {
|
|
@@ -1113,9 +857,6 @@ function configureExpressionMessages({ locale, messages }) {
|
|
|
1113
857
|
...messages
|
|
1114
858
|
};
|
|
1115
859
|
}
|
|
1116
|
-
/**
|
|
1117
|
-
* The active {@link ExpressionMessages} catalog (English by default).
|
|
1118
|
-
*/
|
|
1119
860
|
function getExpressionMessages() {
|
|
1120
861
|
return activeMessages;
|
|
1121
862
|
}
|
|
@@ -1125,11 +866,6 @@ function parseOffset(text) {
|
|
|
1125
866
|
const trimmed = text?.trim();
|
|
1126
867
|
return trimmed ? Number(trimmed) : NaN;
|
|
1127
868
|
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Parse a trailing `... at (from, to)` / `... at pos` position out of a ZEN error
|
|
1130
|
-
* message. Returns a `[from, to]` range — a single offset collapses to
|
|
1131
|
-
* `[pos, pos]` — or `null` when no position is present.
|
|
1132
|
-
*/
|
|
1133
869
|
function extractPosition(message) {
|
|
1134
870
|
const segments = message.split(" at ");
|
|
1135
871
|
const last = segments.length <= 1 ? void 0 : segments.at(-1);
|
|
@@ -1140,10 +876,6 @@ function extractPosition(message) {
|
|
|
1140
876
|
const to = parseOffset(right);
|
|
1141
877
|
return [from, Number.isNaN(to) ? from : to];
|
|
1142
878
|
}
|
|
1143
|
-
/**
|
|
1144
|
-
* Normalize the wasm validate payload (`null` or `{ type, source }`) into a
|
|
1145
|
-
* positioned {@link ExpressionDiagnostic}, or `null` when the expression is valid.
|
|
1146
|
-
*/
|
|
1147
879
|
function normalizeDiagnostic(raw, source) {
|
|
1148
880
|
if (raw === null || raw === void 0) return null;
|
|
1149
881
|
const errorType = isRecord(raw) && typeof raw.type === "string" ? raw.type : void 0;
|
|
@@ -1156,10 +888,6 @@ function normalizeDiagnostic(raw, source) {
|
|
|
1156
888
|
source: getExpressionMessages().sourceLabel(errorType)
|
|
1157
889
|
};
|
|
1158
890
|
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Normalize the wasm `getCompletions` payload into a typed list, dropping
|
|
1161
|
-
* malformed entries and stripping the backtick markers ZEN wraps type names in.
|
|
1162
|
-
*/
|
|
1163
891
|
function normalizeCompletions(raw) {
|
|
1164
892
|
if (!Array.isArray(raw)) return [];
|
|
1165
893
|
const messages = getExpressionMessages();
|
|
@@ -1175,75 +903,38 @@ function normalizeCompletions(raw) {
|
|
|
1175
903
|
}];
|
|
1176
904
|
});
|
|
1177
905
|
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Validate an expression, loading the engine on first use. Resolves to `null`
|
|
1180
|
-
* when the expression is valid, or a positioned diagnostic otherwise.
|
|
1181
|
-
*/
|
|
1182
906
|
async function getDiagnostics(expression, mode) {
|
|
1183
907
|
await loadEngine();
|
|
1184
908
|
return getDiagnosticsSync(expression, mode);
|
|
1185
909
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Synchronous {@link getDiagnostics}. Throws `ExpressionNotReadyError` if the
|
|
1188
|
-
* engine has not loaded yet — use only behind a readiness gate.
|
|
1189
|
-
*/
|
|
1190
910
|
function getDiagnosticsSync(expression, mode) {
|
|
1191
911
|
const engine = getEngineSync();
|
|
1192
912
|
return normalizeDiagnostic(mode === "unary" ? engine.validateUnary(expression) : engine.validate(expression), expression);
|
|
1193
913
|
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Return the ZEN built-in completion list, loading the engine on first use.
|
|
1196
|
-
*/
|
|
1197
914
|
async function getCompletionItems() {
|
|
1198
915
|
await loadEngine();
|
|
1199
916
|
return getCompletionItemsSync();
|
|
1200
917
|
}
|
|
1201
|
-
/**
|
|
1202
|
-
* Synchronous {@link getCompletionItems}. Throws `ExpressionNotReadyError` if the
|
|
1203
|
-
* engine has not loaded yet.
|
|
1204
|
-
*/
|
|
1205
918
|
function getCompletionItemsSync() {
|
|
1206
919
|
return normalizeCompletions(getEngineSync().getCompletions());
|
|
1207
920
|
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Type-check an expression against a `variables` context, loading the engine on
|
|
1210
|
-
* first use. See {@link ExpressionAnalysis}.
|
|
1211
|
-
*/
|
|
1212
921
|
async function analyzeTypes(variables, source, mode) {
|
|
1213
922
|
await loadEngine();
|
|
1214
923
|
return analyzeTypesSync(variables, source, mode);
|
|
1215
924
|
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Synchronous {@link analyzeTypes}. Throws `ExpressionNotReadyError` if the
|
|
1218
|
-
* engine has not loaded yet.
|
|
1219
|
-
*/
|
|
1220
925
|
function analyzeTypesSync(variables, source, mode) {
|
|
1221
926
|
return getEngineSync().analyze(variables, source, mode === "unary");
|
|
1222
927
|
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Whether `actual` satisfies (is assignable to) `expected`, loading the engine on
|
|
1225
|
-
* first use. Used for expected-return-type validation.
|
|
1226
|
-
*/
|
|
1227
928
|
async function satisfiesType(actual, expected) {
|
|
1228
929
|
await loadEngine();
|
|
1229
930
|
return satisfiesTypeSync(actual, expected);
|
|
1230
931
|
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Synchronous {@link satisfiesType}. Throws `ExpressionNotReadyError` if the
|
|
1233
|
-
* engine has not loaded yet.
|
|
1234
|
-
*/
|
|
1235
932
|
function satisfiesTypeSync(actual, expected) {
|
|
1236
933
|
return getEngineSync().satisfies(actual, expected);
|
|
1237
934
|
}
|
|
1238
935
|
//#endregion
|
|
1239
936
|
//#region src/engine/template.ts
|
|
1240
937
|
const HOLE_PATTERN = /\{\{(?<expression>[^{}]*)\}\}/g;
|
|
1241
|
-
/**
|
|
1242
|
-
* Extract every `{{ expression }}` hole from a template document, in source
|
|
1243
|
-
* order. Literal text outside holes is ignored. Pure string scan — no engine
|
|
1244
|
-
* required — matching the `@gorules/lezer-zen-template` hole grammar, so it is
|
|
1245
|
-
* consistent with the editor's highlighting and completion gating.
|
|
1246
|
-
*/
|
|
1247
938
|
function parseTemplateHoles(source) {
|
|
1248
939
|
const holes = [];
|
|
1249
940
|
HOLE_PATTERN.lastIndex = 0;
|
|
@@ -1258,31 +949,12 @@ function parseTemplateHoles(source) {
|
|
|
1258
949
|
}
|
|
1259
950
|
return holes;
|
|
1260
951
|
}
|
|
1261
|
-
/**
|
|
1262
|
-
* The template hole whose inner expression range contains `pos` (boundaries
|
|
1263
|
-
* included, so a caret sitting right after `{{` or right before `}}` counts as
|
|
1264
|
-
* inside), or `null` when `pos` is in literal text. Drives the editor's
|
|
1265
|
-
* hole-scoped completion and hover: intelligence fires inside a hole, nothing in
|
|
1266
|
-
* the surrounding literal text.
|
|
1267
|
-
*/
|
|
1268
952
|
function templateHoleAt(source, pos) {
|
|
1269
953
|
return parseTemplateHoles(source).find((hole) => pos >= hole.from && pos <= hole.to) ?? null;
|
|
1270
954
|
}
|
|
1271
955
|
function hasExpression(hole) {
|
|
1272
956
|
return hole.expression.trim().length > 0;
|
|
1273
957
|
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Type-check every hole of a template against a `variables` context and merge the
|
|
1276
|
-
* results into one {@link ExpressionAnalysis} whose spans are offset onto the
|
|
1277
|
-
* template document. Each hole is a standard ZEN value expression; literal text
|
|
1278
|
-
* contributes nothing. `rootKind` is the context itself (shared by every hole),
|
|
1279
|
-
* so top-level completion works even in an empty hole.
|
|
1280
|
-
*
|
|
1281
|
-
* Best-effort: a hole that fails to analyze (e.g. mid-edit syntax) is skipped
|
|
1282
|
-
* rather than discarding the spans of its siblings. Throws
|
|
1283
|
-
* {@link ExpressionNotReadyError} if the engine has not loaded — use only behind
|
|
1284
|
-
* a readiness gate.
|
|
1285
|
-
*/
|
|
1286
958
|
function analyzeTemplateSync(variables, source) {
|
|
1287
959
|
if (!isEngineReady()) throw new ExpressionNotReadyError();
|
|
1288
960
|
const spans = [];
|
|
@@ -1300,22 +972,10 @@ function analyzeTemplateSync(variables, source) {
|
|
|
1300
972
|
spans
|
|
1301
973
|
};
|
|
1302
974
|
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Async {@link analyzeTemplateSync}, loading the engine on first use.
|
|
1305
|
-
*/
|
|
1306
975
|
async function analyzeTemplate(variables, source) {
|
|
1307
976
|
await loadEngine();
|
|
1308
977
|
return analyzeTemplateSync(variables, source);
|
|
1309
978
|
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Validate every hole of a template and return their syntax diagnostics, each
|
|
1312
|
-
* offset onto the template document (empty holes and literal text produce none).
|
|
1313
|
-
* The single-expression {@link getDiagnosticsSync} yields at most one diagnostic
|
|
1314
|
-
* per hole, so a template with several broken holes surfaces each one.
|
|
1315
|
-
*
|
|
1316
|
-
* Throws {@link ExpressionNotReadyError} if the engine has not loaded — use only
|
|
1317
|
-
* behind a readiness gate.
|
|
1318
|
-
*/
|
|
1319
979
|
function getTemplateDiagnosticsSync(source) {
|
|
1320
980
|
if (!isEngineReady()) throw new ExpressionNotReadyError();
|
|
1321
981
|
const diagnostics = [];
|
|
@@ -1330,14 +990,9 @@ function getTemplateDiagnosticsSync(source) {
|
|
|
1330
990
|
}
|
|
1331
991
|
return diagnostics;
|
|
1332
992
|
}
|
|
1333
|
-
/**
|
|
1334
|
-
* Async {@link getTemplateDiagnosticsSync}, loading the engine on first use.
|
|
1335
|
-
*/
|
|
1336
993
|
async function getTemplateDiagnostics(source) {
|
|
1337
994
|
await loadEngine();
|
|
1338
995
|
return getTemplateDiagnosticsSync(source);
|
|
1339
996
|
}
|
|
1340
997
|
//#endregion
|
|
1341
998
|
export { CONDITION_OPERATORS, CONDITION_TREE_OPERATORS, ExpressionError, ExpressionNotReadyError, analyzeTemplate, analyzeTemplateSync, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileConditionTree, compileGroup, conditionOperatorArity, configureEngine, configureExpressionMessages, enMessages, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, getTemplateDiagnostics, getTemplateDiagnosticsSync, isEngineReady, liftConditionTree, loadEngine, parseTemplateHoles, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, selectBranch, selectBranchWith, templateHoleAt, toZenLiteral, zhCNMessages };
|
|
1342
|
-
|
|
1343
|
-
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coldsmirk/abacus-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Framework-agnostic ZEN expression engine: compile, evaluate, and type-analyze expressions over the GoRules ZEN WASM engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zen",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "git+https://github.com/coldsmirk/abacus.git",
|
|
18
18
|
"directory": "packages/core"
|
|
19
19
|
},
|
|
20
|
-
"license": "
|
|
20
|
+
"license": "UNLICENSED",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Venus"
|
|
23
23
|
},
|