@coldsmirk/abacus-core 0.3.1 → 0.4.0

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/index.cjs CHANGED
@@ -1,1385 +1,2 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/engine/errors.ts
3
- /**
4
- * Error raised when the ZEN engine fails to load or an expression cannot be
5
- * evaluated. The original failure is preserved on {@link cause} and the
6
- * offending expression (if any) on {@link expression}.
7
- */
8
- var ExpressionError = class extends Error {
9
- expression;
10
- constructor(message, expression, cause) {
11
- super(message, { cause });
12
- this.name = "ExpressionError";
13
- this.expression = expression;
14
- }
15
- };
16
- /**
17
- * Raised by the synchronous evaluation helpers when the engine has not finished
18
- * initializing yet. Await {@link loadEngine} (or render under
19
- * `<ExpressionEngineProvider>`) before evaluating synchronously.
20
- */
21
- var ExpressionNotReadyError = class extends ExpressionError {
22
- constructor(message = "Expression engine is not initialized. Await loadEngine() or render under <ExpressionEngineProvider>.") {
23
- super(message);
24
- this.name = "ExpressionNotReadyError";
25
- }
26
- };
27
- //#endregion
28
- //#region src/internal/predicates.ts
29
- /**
30
- * Minimal internal type predicates. Inlined so the engine stays dependency-free
31
- * (no shared-utility package), keeping `@coldsmirk/abacus-core` framework- and
32
- * ecosystem-agnostic.
33
- */
34
- function isUndefined(value) {
35
- return value === void 0;
36
- }
37
- function isString(value) {
38
- return typeof value === "string";
39
- }
40
- function isArray(value) {
41
- return Array.isArray(value);
42
- }
43
- function isNullish(value) {
44
- return value === null || value === void 0;
45
- }
46
- function isRecord(value) {
47
- return typeof value === "object" && value !== null && !Array.isArray(value);
48
- }
49
- //#endregion
50
- //#region src/engine/loader.ts
51
- let enginePromise = null;
52
- let engineSync = null;
53
- let engineError = null;
54
- let configuredInput;
55
- let typeContextCache = null;
56
- function evaluateSafely(expression, run) {
57
- try {
58
- return run();
59
- } catch (error) {
60
- throw new ExpressionError(`Failed to evaluate expression: ${expression}`, expression, error);
61
- }
62
- }
63
- function loadFailureMessage() {
64
- const base = "Failed to load the ZEN expression engine";
65
- 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.`;
66
- return base;
67
- }
68
- /**
69
- * Configure how the wasm binary is located, before the engine loads. Must be
70
- * called before the first {@link loadEngine} (or any evaluation), so that the
71
- * configured input is the one actually used — calling it after the engine has
72
- * started loading throws, rather than silently taking no effect.
73
- */
74
- function configureEngine(options) {
75
- if (enginePromise || engineSync) throw new ExpressionError("configureEngine() must be called before the engine loads.");
76
- configuredInput = options.wasmInput;
77
- }
78
- /**
79
- * Load and initialize the ZEN expression engine exactly once. Concurrent and
80
- * subsequent calls share the same in-flight promise / resolved instance. Set a
81
- * custom wasm source up front with {@link configureEngine}.
82
- *
83
- * The GoRules wasm dependency is reached via a dynamic `import()` on purpose: it
84
- * keeps this module usable from the package's CommonJS build (the dep is
85
- * ESM-only, so a static `require` would throw) and defers the multi-megabyte
86
- * wasm download until the engine is actually needed. Do NOT convert this to a
87
- * static import.
88
- *
89
- * A failed load is not cached: the rejected promise is dropped (the error is
90
- * latched on {@link getEngineError} for inspection), so calling `loadEngine()`
91
- * again retries from scratch. This is the clear-and-reload retry primitive after
92
- * a transient failure — an error boundary should call it before resetting.
93
- */
94
- function loadEngine() {
95
- if (enginePromise) return enginePromise;
96
- engineError = null;
97
- enginePromise = (async () => {
98
- const zen = await import("@gorules/zen-engine-wasm");
99
- await zen.default(isUndefined(configuredInput) ? void 0 : { module_or_path: configuredInput });
100
- const toVariableType = (type) => zen.VariableType.fromJson(type);
101
- const ensureTypeContext = (variables) => {
102
- if (typeContextCache && typeContextCache.variables === variables) return typeContextCache;
103
- const stale = typeContextCache;
104
- typeContextCache = null;
105
- stale?.handle.free();
106
- const handle = toVariableType(variables);
107
- try {
108
- typeContextCache = {
109
- variables,
110
- handle,
111
- rootKind: handle.toJson()
112
- };
113
- } catch (error) {
114
- handle.free();
115
- throw error;
116
- }
117
- return typeContextCache;
118
- };
119
- const engine = Object.freeze({
120
- evaluate: (expression, context = {}) => evaluateSafely(expression, () => zen.evaluateExpression(expression, context)),
121
- evaluateUnary: (expression, context = {}) => evaluateSafely(expression, () => zen.evaluateUnaryExpression(expression, context)),
122
- validate: (expression) => zen.validateExpression(expression),
123
- validateUnary: (expression) => zen.validateUnaryExpression(expression),
124
- getCompletions: () => zen.getCompletions(),
125
- analyze: (variables, source, unary) => {
126
- const context = ensureTypeContext(variables);
127
- const rawSpans = unary ? context.handle.typeCheckUnary(source) : context.handle.typeCheck(source);
128
- return {
129
- rootKind: context.rootKind,
130
- spans: Array.isArray(rawSpans) ? rawSpans : []
131
- };
132
- },
133
- satisfies: (actual, expected) => {
134
- const actualType = toVariableType(actual);
135
- try {
136
- const expectedType = toVariableType(expected);
137
- try {
138
- return actualType.satisfies(expectedType);
139
- } finally {
140
- expectedType.free();
141
- }
142
- } finally {
143
- actualType.free();
144
- }
145
- },
146
- isReady: () => zen.isReady()
147
- });
148
- engineSync = engine;
149
- return engine;
150
- })().catch((error) => {
151
- enginePromise = null;
152
- engineError = new ExpressionError(loadFailureMessage(), void 0, error);
153
- throw engineError;
154
- });
155
- return enginePromise;
156
- }
157
- /**
158
- * Whether the engine has finished initializing and is ready for sync use.
159
- */
160
- function isEngineReady() {
161
- return engineSync?.isReady() ?? false;
162
- }
163
- /**
164
- * The error from the last failed {@link loadEngine} attempt, or `null`. Used by
165
- * the React provider to surface a wasm-load failure to an error boundary rather
166
- * than suspending forever, and by imperative pollers to tell "failed" apart from
167
- * "still loading". Cleared when a new load starts or {@link resetEngine}.
168
- */
169
- function getEngineError() {
170
- return engineError;
171
- }
172
- /**
173
- * Return the already-initialized engine synchronously, or throw
174
- * {@link ExpressionNotReadyError} if {@link loadEngine} has not resolved yet.
175
- */
176
- function getEngineSync() {
177
- if (!engineSync) throw new ExpressionNotReadyError();
178
- return engineSync;
179
- }
180
- /**
181
- * Reset the engine singleton — drops the loaded engine, the cached type context,
182
- * the latched error, and the configured wasm input. Mainly for tests, but also the
183
- * way to re-run {@link configureEngine} after a load (configure throws once the
184
- * engine has started loading).
185
- */
186
- function resetEngine() {
187
- typeContextCache?.handle.free();
188
- typeContextCache = null;
189
- enginePromise = null;
190
- engineSync = null;
191
- engineError = null;
192
- configuredInput = void 0;
193
- }
194
- //#endregion
195
- //#region src/internal/env.ts
196
- /**
197
- * Best-effort development-mode flag for diagnostic-only code paths.
198
- *
199
- * Detected from `process.env.NODE_ENV` (defined by Node, test runners, webpack,
200
- * and Vite's SSR/`define`). The `typeof` guard is load-bearing: in a plain
201
- * browser bundle `process` is not declared at all, and any bare reference would
202
- * throw a ReferenceError while this module is being imported. Hosts without a
203
- * `process` default to "production" — the safe choice, since `isDev` only gates
204
- * extra developer-facing warnings.
205
- */
206
- const isDev = detectDev();
207
- function detectDev() {
208
- if (typeof process === "undefined") return false;
209
- return process.env ? process.env.NODE_ENV !== "production" : false;
210
- }
211
- //#endregion
212
- //#region src/condition/subject.ts
213
- /**
214
- * Shared subject-path guard for the condition compilers. A subject / left-hand
215
- * path is emitted **verbatim** into ZEN source, so both {@link compileCondition}
216
- * (which writes that source) and {@link liftConditionTree} (which reads it back)
217
- * gate paths through this one predicate — a single definition of "what is a safe
218
- * field path" that cannot drift between the writer and the reader, which matters
219
- * because the guard is also the compiler's injection defense.
220
- */
221
- const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
222
- const ZEN_RESERVED_WORDS = new Set([
223
- "and",
224
- "or",
225
- "not",
226
- "in",
227
- "true",
228
- "false",
229
- "null"
230
- ]);
231
- /**
232
- * Whether `subject` is a plain identifier path safe to emit verbatim into ZEN
233
- * source: dotted/indexed segments only, none of which is a ZEN reserved word.
234
- */
235
- function isIdentifierPath(subject) {
236
- return SUBJECT_PATTERN.test(subject) && (subject.match(/[A-Z_$][\w$]*/gi) ?? []).every((segment) => !ZEN_RESERVED_WORDS.has(segment));
237
- }
238
- //#endregion
239
- //#region src/condition/compile.ts
240
- /**
241
- * Serialize a JavaScript value into a ZEN literal. Nullish becomes `null`;
242
- * numbers / booleans / bigints are emitted verbatim; strings are quoted via
243
- * {@link encodeZenString}; arrays become `[a, b, ...]`.
244
- *
245
- * Throws {@link ExpressionError} for a value with no faithful ZEN
246
- * representation — an object, symbol, or function, or a string containing both
247
- * quote styles. Callers that need a sentinel instead of a throw go through
248
- * {@link compileCondition}, which degrades such a value to a non-compiling
249
- * (null) condition.
250
- */
251
- function toZenLiteral(value) {
252
- if (isNullish(value)) return "null";
253
- if (typeof value === "number") {
254
- if (!Number.isFinite(value)) throw new ExpressionError(`Number ${String(value)} has no ZEN literal representation`);
255
- return String(value);
256
- }
257
- if (typeof value === "boolean" || typeof value === "bigint") return String(value);
258
- if (isString(value)) return encodeZenString(value);
259
- if (isArray(value)) return `[${value.map((item) => toZenLiteral(item)).join(", ")}]`;
260
- throw new ExpressionError(`Value of type "${typeof value}" has no ZEN literal representation`);
261
- }
262
- /**
263
- * Encode a string as a ZEN literal. ZEN string literals are **raw** between
264
- * matching quotes and honor no backslash escapes (`'a\nb'` is the four
265
- * characters `a \ n b`), so the encoder must not escape — it picks a delimiter
266
- * the value does not contain. A value containing both quote styles cannot be
267
- * represented as a raw ZEN literal and throws.
268
- */
269
- function encodeZenString(value) {
270
- const hasSingle = value.includes("'");
271
- const hasDouble = value.includes("\"");
272
- if (hasSingle && hasDouble) throw new ExpressionError("String contains both single and double quotes and has no ZEN literal representation");
273
- return hasSingle ? `"${value}"` : `'${value}'`;
274
- }
275
- function toArrayLiteral(value) {
276
- return isArray(value) ? toZenLiteral(value) : `[${toZenLiteral(value)}]`;
277
- }
278
- /**
279
- * Emit the ZEN emptiness test matching the backend field evaluator's
280
- * `isEmptyValue`: null, blank (whitespace-only) text, and empty arrays are
281
- * empty; numbers and booleans never are. ZEN's `or` short-circuits, so the
282
- * type-guarded branches never evaluate against a null subject. The backend
283
- * additionally treats an empty map as empty for totality, but form values —
284
- * the only subjects this compiler targets — are never objects, and ZEN's
285
- * `len()` does not accept one.
286
- */
287
- function zenIsEmpty(subject) {
288
- return `(${subject} == null or (type(${subject}) == 'string' and len(trim(${subject})) == 0) or (type(${subject}) == 'array' and len(${subject}) == 0))`;
289
- }
290
- function compileFieldCondition(subject, operator, value) {
291
- switch (operator) {
292
- case "eq": return `${subject} == ${toZenLiteral(value)}`;
293
- case "ne": return `${subject} != ${toZenLiteral(value)}`;
294
- case "gt": return `${subject} > ${toZenLiteral(value)}`;
295
- case "gte": return `${subject} >= ${toZenLiteral(value)}`;
296
- case "lt": return `${subject} < ${toZenLiteral(value)}`;
297
- case "lte": return `${subject} <= ${toZenLiteral(value)}`;
298
- case "contains": return `contains(${subject}, ${toZenLiteral(value)})`;
299
- case "not_contains": return `not contains(${subject}, ${toZenLiteral(value)})`;
300
- case "starts_with": return `startsWith(${subject}, ${toZenLiteral(value)})`;
301
- case "ends_with": return `endsWith(${subject}, ${toZenLiteral(value)})`;
302
- case "in": return `${subject} in ${toArrayLiteral(value)}`;
303
- case "not_in": return `not (${subject} in ${toArrayLiteral(value)})`;
304
- case "is_empty": return zenIsEmpty(subject);
305
- case "is_not_empty": return `not ${zenIsEmpty(subject)}`;
306
- default: throw new ExpressionError(`Unsupported operator: ${String(operator)}`);
307
- }
308
- }
309
- /**
310
- * Compile a single condition into a ZEN boolean expression. Field conditions map
311
- * their operator to ZEN; expression conditions are wrapped in parentheses to
312
- * preserve their grouping. Returns `null` when the condition is empty, its
313
- * subject is not an identifier path, or its value has no ZEN representation.
314
- */
315
- function compileCondition(condition) {
316
- if (condition.kind === "expression") {
317
- const expression = condition.expression.trim();
318
- return expression === "" ? null : `(${expression})`;
319
- }
320
- const subject = condition.subject.trim();
321
- if (!isIdentifierPath(subject)) return null;
322
- try {
323
- return compileFieldCondition(subject, condition.operator, condition.value);
324
- } catch {
325
- return null;
326
- }
327
- }
328
- /**
329
- * Compile a condition group (its conditions joined with AND). Returns `null`
330
- * when the group has no compilable conditions.
331
- */
332
- function compileGroup(group) {
333
- const parts = group.conditions.map((condition) => compileCondition(condition)).filter((part) => part !== null);
334
- return parts.length === 0 ? null : parts.join(" and ");
335
- }
336
- /**
337
- * Compile a branch's condition groups into a single ZEN expression (groups
338
- * joined with OR). Returns `null` when the branch has no compilable groups
339
- * (e.g. a default branch).
340
- */
341
- function compileBranch(branch) {
342
- const groups = (branch.conditionGroups ?? []).map((group) => compileGroup(group)).filter((group) => group !== null);
343
- return groups.length === 0 ? null : groups.map((group) => `(${group})`).join(" or ");
344
- }
345
- /**
346
- * Pick the matching branch for the given context using a pre-loaded engine.
347
- * Non-default branches are tested in ascending `priority` order; the first whose
348
- * compiled expression evaluates to `true` wins. Falls back to the default
349
- * branch, or a `null` id when neither matches.
350
- *
351
- * A branch whose expression throws for the given context (e.g. ZEN's `>`
352
- * throws when the subject is missing or non-numeric) is treated as not matching
353
- * rather than propagating — so a missing field degrades to the default branch
354
- * instead of crashing the caller.
355
- */
356
- function selectBranchWith(branches, context, engine) {
357
- const ordered = branches.toSorted((a, b) => a.priority - b.priority);
358
- for (const branch of ordered) {
359
- if (branch.isDefault) continue;
360
- const expression = compileBranch(branch);
361
- if (expression === null) {
362
- if (isDev) console.warn(`[expression] branch "${branch.id}" has no compilable condition and can never match`);
363
- continue;
364
- }
365
- if (evaluatesTrue(engine, expression, context)) return {
366
- branchId: branch.id,
367
- matched: true
368
- };
369
- }
370
- return {
371
- branchId: ordered.find((branch) => branch.isDefault)?.id ?? null,
372
- matched: false
373
- };
374
- }
375
- function evaluatesTrue(engine, expression, context) {
376
- try {
377
- return engine.evaluate(expression, context) === true;
378
- } catch (error) {
379
- if (isDev) reportEvaluationFailure(engine, expression, error);
380
- return false;
381
- }
382
- }
383
- function reportEvaluationFailure(engine, expression, error) {
384
- let diagnostic;
385
- try {
386
- diagnostic = engine.validate(expression);
387
- } catch {
388
- return;
389
- }
390
- if (!isNullish(diagnostic)) console.warn(`[expression] compiled branch expression failed to parse: ${expression}`, diagnostic, error);
391
- }
392
- /**
393
- * Pick the matching branch for the given context, loading the ZEN engine on
394
- * first use. See {@link selectBranchWith} for the selection semantics.
395
- */
396
- async function selectBranch(branches, context) {
397
- return selectBranchWith(branches, context, await loadEngine());
398
- }
399
- //#endregion
400
- //#region src/condition/types.ts
401
- /**
402
- * Structural shapes for the visual condition model compiled to ZEN. They mirror
403
- * the condition types used in the form and flow editors so a host can map those
404
- * definitions to {@link compileCondition} / {@link selectBranch} without this
405
- * package depending on either editor. The editors keep a flat working model for
406
- * form ergonomics (a row can hold both a half-typed field triple and an
407
- * expression while the author toggles between them); this discriminated input is
408
- * the narrowed, compiler-facing shape where each kind carries only what it uses.
409
- */
410
- /**
411
- * The closed operator vocabulary understood by {@link compileCondition}, as a
412
- * runtime constant so validators can build allow-lists from it instead of
413
- * re-declaring the set. The {@link ConditionOperator} type derives from this
414
- * array — one definition site for both the type and the runtime list.
415
- */
416
- const CONDITION_OPERATORS = [
417
- "eq",
418
- "ne",
419
- "gt",
420
- "gte",
421
- "lt",
422
- "lte",
423
- "contains",
424
- "not_contains",
425
- "starts_with",
426
- "ends_with",
427
- "in",
428
- "not_in",
429
- "is_empty",
430
- "is_not_empty"
431
- ];
432
- const CONDITION_OPERATOR_ARITIES = {
433
- eq: "scalar",
434
- ne: "scalar",
435
- gt: "scalar",
436
- gte: "scalar",
437
- lt: "scalar",
438
- lte: "scalar",
439
- contains: "scalar",
440
- not_contains: "scalar",
441
- starts_with: "scalar",
442
- ends_with: "scalar",
443
- in: "array",
444
- not_in: "array",
445
- is_empty: "none",
446
- is_not_empty: "none"
447
- };
448
- /**
449
- * The arity of `operator`'s right-hand operand (see
450
- * {@link ConditionOperatorArity}). One definition site next to
451
- * {@link CONDITION_OPERATORS}, so the tree compiler's arity enforcement and a
452
- * condition editor's operand controls classify operators identically instead of
453
- * each keeping a drift-prone copy.
454
- */
455
- function conditionOperatorArity(operator) {
456
- return CONDITION_OPERATOR_ARITIES[operator];
457
- }
458
- //#endregion
459
- //#region src/condition/compile-tree.ts
460
- /**
461
- * Compile a condition tree to a canonical ZEN expression, or `""` when no rule in
462
- * the tree is compilable (see the module note for the drop semantics).
463
- */
464
- function compileConditionTree(tree) {
465
- const normalized = normalizeNode(tree);
466
- return normalized === null ? "" : emitNode(normalized, true);
467
- }
468
- /**
469
- * Prune a node to its compilable core: drop rules {@link compileRule} rejects, drop
470
- * emptied groups, and collapse a single-surviving-item group to that item (so the
471
- * emitted shape carries no redundant parentheses). Returns `null` when nothing in
472
- * the node survives.
473
- */
474
- function normalizeNode(node) {
475
- if (node.kind === "rule") return compileRule(node) === null ? null : node;
476
- const items = node.items.map((item) => normalizeNode(item)).filter((item) => item !== null);
477
- if (items.length === 0) return null;
478
- if (items.length === 1) return items[0];
479
- return {
480
- kind: "group",
481
- op: node.op,
482
- items
483
- };
484
- }
485
- /**
486
- * Render a normalized node. A rule emits its lowered ZEN; a group joins its items
487
- * with ` and ` / ` or ` and, unless it is the top-level group, wraps them in
488
- * parentheses so the tree structure survives ZEN's operator precedence on lift.
489
- */
490
- function emitNode(node, topLevel) {
491
- if (node.kind === "rule") return compileRule(node);
492
- const joined = node.items.map((item) => emitNode(item, false)).join(node.op === "and" ? " and " : " or ");
493
- return topLevel ? joined : `(${joined})`;
494
- }
495
- /**
496
- * Lower a leaf rule to ZEN via {@link compileCondition}, or `null` for a rule the
497
- * compiler cannot represent. The rule's `right` must match its operator's arity —
498
- * a single scalar for the comparison / string operators, an array of scalars for
499
- * `in` / `not_in`, absent for the emptiness operators (the contract
500
- * {@link ConditionTreeValue} documents); an off-arity rule is non-compilable and
501
- * drops. Enforcing arity here, not just relying on `compileCondition`, is what
502
- * keeps every emitted expression liftable: the flat compiler's lax value handling
503
- * would happily serialize e.g. a missing `right` as a `null` literal, which is not
504
- * part of the canonical grammar.
505
- */
506
- function compileRule(rule) {
507
- if (!matchesOperatorArity(rule)) return null;
508
- return compileCondition({
509
- kind: "field",
510
- subject: rule.left,
511
- operator: rule.operator,
512
- value: rule.right
513
- });
514
- }
515
- function matchesOperatorArity(rule) {
516
- switch (conditionOperatorArity(rule.operator)) {
517
- case "scalar": return isConditionScalar(rule.right);
518
- case "array": return isScalarArray(rule.right);
519
- case "none": return rule.right === void 0;
520
- }
521
- }
522
- function isConditionScalar(value) {
523
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
524
- }
525
- function isScalarArray(value) {
526
- return isArray(value) && value.every((item) => isConditionScalar(item));
527
- }
528
- //#endregion
529
- //#region src/condition/lift-tree.ts
530
- const TWO_CHAR_PUNCTUATION = new Set([
531
- "==",
532
- "!=",
533
- "<=",
534
- ">="
535
- ]);
536
- const ONE_CHAR_PUNCTUATION = new Set([
537
- "(",
538
- ")",
539
- "[",
540
- "]",
541
- ",",
542
- ".",
543
- "<",
544
- ">",
545
- "-"
546
- ]);
547
- const IDENT_START = /[a-z_$]/i;
548
- const IDENT_PART = /[\w$]/;
549
- const DIGIT = /\d/;
550
- const NUMBER_PATTERN = /^\d+(?:\.\d+)?(?:e[+-]?\d+)?/i;
551
- const INTEGER_PATTERN = /^\d+$/;
552
- const COMPARISON_OPERATORS = {
553
- "==": "eq",
554
- "!=": "ne",
555
- ">": "gt",
556
- ">=": "gte",
557
- "<": "lt",
558
- "<=": "lte"
559
- };
560
- const CALL_OPERATORS = {
561
- contains: "contains",
562
- startsWith: "starts_with",
563
- endsWith: "ends_with"
564
- };
565
- const MAX_GROUP_DEPTH = 64;
566
- /**
567
- * Lift a ZEN expression to a condition tree, or `null` when it is not in the
568
- * canonical form {@link compileConditionTree} produces (the consumer then keeps the
569
- * raw expression). Groups nested deeper than 64 parenthesized levels are refused as
570
- * non-canonical rather than risking parser-stack overflow on adversarial input. The
571
- * returned root is always a group.
572
- */
573
- function liftConditionTree(expression) {
574
- const tokens = tokenize(expression);
575
- if (tokens === null || tokens.length === 0) return null;
576
- let pos = 0;
577
- function peek(offset = 0) {
578
- return tokens[pos + offset];
579
- }
580
- function consumePunct(value) {
581
- const token = peek();
582
- if (token !== void 0 && token.kind === "punct" && token.value === value) {
583
- pos += 1;
584
- return true;
585
- }
586
- return false;
587
- }
588
- function consumeIdent(value) {
589
- const token = peek();
590
- if (token !== void 0 && token.kind === "ident" && token.value === value) {
591
- pos += 1;
592
- return true;
593
- }
594
- return false;
595
- }
596
- function tokensMatchAt(from, expected) {
597
- for (const [index, element] of expected.entries()) {
598
- const actual = tokens[from + index];
599
- const want = element;
600
- if (actual === void 0 || actual.kind !== want.kind || actual.value !== want.value) return false;
601
- }
602
- return true;
603
- }
604
- function parsePath() {
605
- const read = readPath(tokens, pos);
606
- if (read === null) return null;
607
- pos = read.next;
608
- return read.path;
609
- }
610
- function parseLiteral() {
611
- const token = peek();
612
- if (token === void 0) return null;
613
- if (token.kind === "string") {
614
- pos += 1;
615
- return token.value;
616
- }
617
- if (token.kind === "number") {
618
- pos += 1;
619
- return Number(token.value);
620
- }
621
- if (token.kind === "ident") {
622
- if (token.value === "true") {
623
- pos += 1;
624
- return true;
625
- }
626
- if (token.value === "false") {
627
- pos += 1;
628
- return false;
629
- }
630
- return null;
631
- }
632
- if (token.kind === "punct" && token.value === "-") {
633
- const digits = peek(1);
634
- if (digits === void 0 || digits.kind !== "number") return null;
635
- pos += 2;
636
- return -Number(digits.value);
637
- }
638
- return null;
639
- }
640
- function parseArray() {
641
- if (!consumePunct("[")) return null;
642
- if (isPunct(peek(), "]")) {
643
- pos += 1;
644
- return [];
645
- }
646
- const first = parseLiteral();
647
- if (first === null) return null;
648
- const items = [first];
649
- while (isPunct(peek(), ",")) {
650
- pos += 1;
651
- const next = parseLiteral();
652
- if (next === null) return null;
653
- items.push(next);
654
- }
655
- return consumePunct("]") ? items : null;
656
- }
657
- function parseCall(operator) {
658
- if (!consumePunct("(")) return null;
659
- const left = parsePath();
660
- if (left === null || !consumePunct(",")) return null;
661
- const right = parseLiteral();
662
- if (right === null || !consumePunct(")")) return null;
663
- return {
664
- kind: "rule",
665
- left,
666
- operator,
667
- right
668
- };
669
- }
670
- function parseNotIn() {
671
- const left = parsePath();
672
- if (left === null || !consumeIdent("in")) return null;
673
- const right = parseArray();
674
- if (right === null || !consumePunct(")")) return null;
675
- return {
676
- kind: "rule",
677
- left,
678
- operator: "not_in",
679
- right
680
- };
681
- }
682
- function tryEmptiness() {
683
- const first = peek();
684
- if (first === void 0) return null;
685
- let operator;
686
- let pathIndex;
687
- if (first.kind === "ident" && first.value === "not" && isPunct(peek(1), "(")) {
688
- operator = "is_not_empty";
689
- pathIndex = pos + 2;
690
- } else if (first.kind === "punct" && first.value === "(") {
691
- operator = "is_empty";
692
- pathIndex = pos + 1;
693
- } else return null;
694
- const read = readPath(tokens, pathIndex);
695
- if (read === null) return null;
696
- const canonical = compileCondition({
697
- kind: "field",
698
- subject: read.path,
699
- operator,
700
- value: void 0
701
- });
702
- if (canonical === null) return null;
703
- const expected = tokenize(canonical);
704
- if (expected === null || !tokensMatchAt(pos, expected)) return null;
705
- pos += expected.length;
706
- return {
707
- kind: "rule",
708
- left: read.path,
709
- operator
710
- };
711
- }
712
- function parseRule() {
713
- const token = peek();
714
- if (token === void 0 || token.kind !== "ident") return null;
715
- if (token.value === "not") {
716
- const next = peek(1);
717
- if (next === void 0) return null;
718
- if (next.kind === "ident" && next.value === "contains" && isPunct(peek(2), "(")) {
719
- pos += 2;
720
- return parseCall("not_contains");
721
- }
722
- if (next.kind === "punct" && next.value === "(") {
723
- pos += 2;
724
- return parseNotIn();
725
- }
726
- return null;
727
- }
728
- if (isPunct(peek(1), "(")) {
729
- const operator = CALL_OPERATORS[token.value];
730
- if (operator === void 0) return null;
731
- pos += 1;
732
- return parseCall(operator);
733
- }
734
- const left = parsePath();
735
- if (left === null) return null;
736
- const operatorToken = peek();
737
- if (operatorToken === void 0) return null;
738
- if (operatorToken.kind === "ident" && operatorToken.value === "in") {
739
- pos += 1;
740
- const right = parseArray();
741
- return right === null ? null : {
742
- kind: "rule",
743
- left,
744
- operator: "in",
745
- right
746
- };
747
- }
748
- if (operatorToken.kind === "punct") {
749
- const operator = COMPARISON_OPERATORS[operatorToken.value];
750
- if (operator === void 0) return null;
751
- pos += 1;
752
- const right = parseLiteral();
753
- return right === null ? null : {
754
- kind: "rule",
755
- left,
756
- operator,
757
- right
758
- };
759
- }
760
- return null;
761
- }
762
- function parseItem(depth) {
763
- const emptiness = tryEmptiness();
764
- if (emptiness !== null) return emptiness;
765
- if (isPunct(peek(), "(")) {
766
- if (depth >= MAX_GROUP_DEPTH) return null;
767
- pos += 1;
768
- const inner = parseGroup(depth + 1);
769
- if (inner === null || !consumePunct(")")) return null;
770
- return asGroup(inner);
771
- }
772
- return parseRule();
773
- }
774
- function peekJoin() {
775
- const token = peek();
776
- if (token !== void 0 && token.kind === "ident" && (token.value === "and" || token.value === "or")) return token.value;
777
- return null;
778
- }
779
- function parseGroup(depth) {
780
- const first = parseItem(depth);
781
- if (first === null) return null;
782
- const items = [first];
783
- let op = null;
784
- let join = peekJoin();
785
- while (join !== null) {
786
- if (op === null) op = join;
787
- else if (op !== join) return null;
788
- pos += 1;
789
- const next = parseItem(depth);
790
- if (next === null) return null;
791
- items.push(next);
792
- join = peekJoin();
793
- }
794
- return items.length === 1 ? items[0] : {
795
- kind: "group",
796
- op: op ?? "and",
797
- items
798
- };
799
- }
800
- const parsed = parseGroup(0);
801
- if (parsed === null || pos !== tokens.length) return null;
802
- return asGroup(parsed);
803
- }
804
- /**
805
- * Read a dotted/indexed identifier path starting at `from`, validated through
806
- * {@link isIdentifierPath}. Pure over the token array (no cursor) so it can probe a
807
- * candidate path — the emptiness oracle reads the blob's subject without committing
808
- * the cursor. Returns the path and the index just past it, or `null`.
809
- */
810
- function readPath(tokens, from) {
811
- const head = tokens[from];
812
- if (head === void 0 || head.kind !== "ident") return null;
813
- let path = head.value;
814
- let index = from + 1;
815
- let segment = tokens[index];
816
- while (segment !== void 0 && segment.kind === "punct" && (segment.value === "." || segment.value === "[")) {
817
- if (segment.value === ".") {
818
- const name = tokens[index + 1];
819
- if (name === void 0 || name.kind !== "ident") return null;
820
- path += `.${name.value}`;
821
- index += 2;
822
- } else {
823
- const inner = tokens[index + 1];
824
- if (inner === void 0 || inner.kind !== "number" || !INTEGER_PATTERN.test(inner.value)) return null;
825
- const close = tokens[index + 2];
826
- if (close === void 0 || close.kind !== "punct" || close.value !== "]") return null;
827
- path += `[${inner.value}]`;
828
- index += 3;
829
- }
830
- segment = tokens[index];
831
- }
832
- return isIdentifierPath(path) ? {
833
- path,
834
- next: index
835
- } : null;
836
- }
837
- function asGroup(node) {
838
- return node.kind === "group" ? node : {
839
- kind: "group",
840
- op: "and",
841
- items: [node]
842
- };
843
- }
844
- function isPunct(token, value) {
845
- return token !== void 0 && token.kind === "punct" && token.value === value;
846
- }
847
- /**
848
- * Split a ZEN expression into tokens, or `null` on an unexpected character or an
849
- * unterminated string. Whitespace is dropped, so the parser (and the emptiness
850
- * oracle's token match) is insensitive to spacing. String literals are read raw
851
- * between matching quotes — ZEN honors no backslash escapes, mirroring the encoder
852
- * in {@link compileCondition}.
853
- */
854
- function tokenize(input) {
855
- const tokens = [];
856
- let index = 0;
857
- while (index < input.length) {
858
- const char = input[index];
859
- if (char === " " || char === " " || char === "\n" || char === "\r") {
860
- index += 1;
861
- continue;
862
- }
863
- if (char === "'" || char === "\"") {
864
- const end = input.indexOf(char, index + 1);
865
- if (end === -1) return null;
866
- tokens.push({
867
- kind: "string",
868
- value: input.slice(index + 1, end)
869
- });
870
- index = end + 1;
871
- continue;
872
- }
873
- const pair = input.slice(index, index + 2);
874
- if (TWO_CHAR_PUNCTUATION.has(pair)) {
875
- tokens.push({
876
- kind: "punct",
877
- value: pair
878
- });
879
- index += 2;
880
- continue;
881
- }
882
- if (ONE_CHAR_PUNCTUATION.has(char)) {
883
- tokens.push({
884
- kind: "punct",
885
- value: char
886
- });
887
- index += 1;
888
- continue;
889
- }
890
- if (DIGIT.test(char)) {
891
- const match = NUMBER_PATTERN.exec(input.slice(index));
892
- if (match === null) return null;
893
- tokens.push({
894
- kind: "number",
895
- value: match[0]
896
- });
897
- index += match[0].length;
898
- continue;
899
- }
900
- if (IDENT_START.test(char)) {
901
- let end = index + 1;
902
- while (end < input.length && IDENT_PART.test(input[end])) end += 1;
903
- tokens.push({
904
- kind: "ident",
905
- value: input.slice(index, end)
906
- });
907
- index = end;
908
- continue;
909
- }
910
- return null;
911
- }
912
- return tokens;
913
- }
914
- //#endregion
915
- //#region src/condition/tree-types.ts
916
- /**
917
- * The visual condition **tree** model: an arbitrarily nested and/or tree of typed
918
- * comparison rules that {@link compileConditionTree} serializes to a single ZEN
919
- * boolean expression and {@link liftConditionTree} reconstructs from one. It is a
920
- * distinct, self-contained shape from the compiler's flat {@link ConditionInput} —
921
- * the tree is what a structured builder UI edits, whereas `ConditionInput` is the
922
- * narrowed per-condition shape the compiler consumes. The two meet only at the
923
- * leaf: a {@link ConditionTreeRule} lowers to a field {@link ConditionInput} so the
924
- * operator vocabulary, literal encoding, and injection guard have one owner.
925
- */
926
- /**
927
- * The tree's operator vocabulary as a runtime list — the compiler's full
928
- * {@link CONDITION_OPERATORS} set under the tree name. An alias, not a second
929
- * hand-maintained list, so the tree vocabulary can never drift from the compiler's;
930
- * it gives tree code and the builder UI one tree-named import for the operators they
931
- * support.
932
- */
933
- const CONDITION_TREE_OPERATORS = CONDITION_OPERATORS;
934
- //#endregion
935
- //#region src/engine/evaluate.ts
936
- /**
937
- * Evaluate a standard ZEN expression, loading the engine on first use.
938
- *
939
- * `T` is an **unchecked** assertion — the value is returned as `T` with no
940
- * runtime validation, and ZEN's result type depends on the expression and
941
- * context. Prefer the `unknown` default and narrow at the call site.
942
- */
943
- async function evaluate(expression, context) {
944
- await loadEngine();
945
- return evaluateSync(expression, context);
946
- }
947
- /**
948
- * Evaluate a ZEN unary (test) expression, loading the engine on first use.
949
- */
950
- async function evaluateUnary(expression, context) {
951
- await loadEngine();
952
- return evaluateUnarySync(expression, context);
953
- }
954
- /**
955
- * Evaluate a standard ZEN expression synchronously. Throws
956
- * {@link ExpressionNotReadyError} when the engine has not loaded yet — use this
957
- * only behind a readiness gate (e.g. under `<ExpressionEngineProvider>`).
958
- */
959
- function evaluateSync(expression, context) {
960
- return getEngineSync().evaluate(expression, context);
961
- }
962
- /**
963
- * Evaluate a ZEN unary (test) expression synchronously. Throws
964
- * {@link ExpressionNotReadyError} when the engine has not loaded yet.
965
- */
966
- function evaluateUnarySync(expression, context) {
967
- return getEngineSync().evaluateUnary(expression, context);
968
- }
969
- //#endregion
970
- //#region src/engine/messages.ts
971
- const COMPLETION_INFO_ZH = {
972
- "Returns the length of variable": "返回变量的长度",
973
- "Checks if variable contains a needle": "检查变量是否包含指定元素",
974
- "Flattens an array": "将数组扁平化",
975
- "Merges multiple objects into one.": "将多个对象合并为一个。",
976
- "Deeply merges multiple objects into one.": "将多个对象深度合并为一个。",
977
- "Converts all characters in a string to uppercase": "将字符串中所有字符转换为大写",
978
- "Converts all characters in a string to lowercase": "将字符串中所有字符转换为小写",
979
- "Returns the string with leading and trailing whitespace removed": "返回去除首尾空白后的字符串",
980
- "Returns true if the string starts with the specified prefix": "若字符串以指定前缀开头则返回 true",
981
- "Returns true if the string ends with the specified suffix": "若字符串以指定后缀结尾则返回 true",
982
- "Returns true if the string matches the specified pattern": "若字符串匹配指定模式则返回 true",
983
- "Extracts matching substrings according to a pattern": "按模式提取匹配的子串",
984
- "Performs a fuzzy search of the needle in the haystack, and returns the match score(s).": "在目标中对关键字进行模糊搜索,并返回匹配得分。",
985
- "Splits a string into an array of substrings using the specified delimiter.": "使用指定分隔符将字符串拆分为子串数组。",
986
- "Returns the absolute value of a number": "返回数字的绝对值",
987
- "Returns the sum of all elements in the input array.": "返回输入数组中所有元素之和。",
988
- "Calculates the average of all elements in the input array.": "计算输入数组中所有元素的平均值。",
989
- "Returns the smallest of the elements in the input array.": "返回输入数组中的最小元素。",
990
- "Returns the largest of the elements in the input array.": "返回输入数组中的最大元素。",
991
- "Generates a random number between 0 (inclusive) and max (inclusive).": "生成 0 到 max(均包含)之间的随机数。",
992
- "Calculates the median value of all elements in the input array.": "计算输入数组中所有元素的中位数。",
993
- "Finds the mode(s) of the input array, which are the most frequent element(s).": "求输入数组的众数,即出现最频繁的元素。",
994
- "Rounds a number down to the nearest integer.": "向下取整到最接近的整数。",
995
- "Rounds a number up to the nearest integer.": "向上取整到最接近的整数。",
996
- "Rounds a number to a specified number of decimal places.": "将数字四舍五入到指定的小数位数。",
997
- "Truncates a number to a specified number of decimal places.": "将数字截断到指定的小数位数。",
998
- "Checks if the given value is of a numeric type.": "检查给定值是否为数值类型。",
999
- "Converts the given value to a string.": "将给定值转换为字符串。",
1000
- "Converts the given value to a number.": "将给定值转换为数字。",
1001
- "Converts the given value to a boolean.": "将给定值转换为布尔值。",
1002
- "Returns a string representing the data type of the value.": "返回表示该值数据类型的字符串。",
1003
- "Returns an array of a given object's own enumerable property names.": "返回由给定对象自身可枚举属性名组成的数组。",
1004
- "Returns an array of a given object's own enumerable property values.": "返回由给定对象自身可枚举属性值组成的数组。",
1005
- "Returns a new date time instance.": "返回一个新的日期时间实例。",
1006
- "Converts a numeric timestamp to a unix timestamp.": "将数值时间戳转换为 Unix 时间戳。",
1007
- "Extracts the time from a numeric timestamp and returns it as a seconds from beginning of day.": "从数值时间戳中提取时间,以当天起始的秒数返回。",
1008
- "e.g. 1h30min": "例如 1h30min",
1009
- "Extracts the year from a given timestamp.": "从给定时间戳中提取年份。",
1010
- "Gets the day of the week from a given timestamp, where Sunday might be 0.": "获取给定时间戳的星期几(周日可能为 0)。",
1011
- "Extracts the day of the month from a given timestamp.": "从给定时间戳中提取当月的日期。",
1012
- "Gets the day of the year from a given timestamp.": "获取给定时间戳在一年中的第几天。",
1013
- "Calculates the week of the year from a given timestamp.": "计算给定时间戳在一年中的第几周。",
1014
- "Extracts the month from a given timestamp, typically with January as 1.": "从给定时间戳中提取月份(通常 1 月为 1)。",
1015
- "Converts the month from a given timestamp into its string representation (e.g., 'Jan').": "将给定时间戳的月份转换为字符串表示(例如 'Jan')。",
1016
- "Converts a timestamp to a human-readable date string.": "将时间戳转换为人类可读的日期字符串。",
1017
- "Converts the day of the week from a given timestamp into its string representation (e.g., 'Mon').": "将给定时间戳的星期几转换为字符串表示(例如 'Mon')。",
1018
- "Returns the timestamp representing the start of a specified unit (e.g., day, month, year) based on a given timestamp.": "返回给定时间戳在指定单位(如日、月、年)起始处的时间戳。",
1019
- "Returns the timestamp representing the end of a specified unit (e.g., day, month, year) based on a given timestamp.": "返回给定时间戳在指定单位(如日、月、年)结束处的时间戳。",
1020
- "Checks if all elements in the array satisfy the condition defined in the callback.": "检查数组中所有元素是否都满足回调中定义的条件。",
1021
- "Checks if no elements in the array satisfy the condition defined in the callback.": "检查数组中是否没有任何元素满足回调中定义的条件。",
1022
- "Checks if at least one element in the array satisfies the condition defined in the callback.": "检查数组中是否至少有一个元素满足回调中定义的条件。",
1023
- "Checks if exactly one element in the array satisfies the condition defined in the callback.": "检查数组中是否恰好有一个元素满足回调中定义的条件。",
1024
- "Creates a new array with all elements that satisfy the condition defined in the callback.": "创建一个仅包含满足回调中定义条件的元素的新数组。",
1025
- "Creates a new array populated with the results of calling the provided function on every element in the calling array.": "创建一个新数组,其元素为对原数组每个元素调用所提供函数的结果。",
1026
- "First maps each element using a mapping function, then flattens the result into a new array.": "先用映射函数处理每个元素,再将结果扁平化为新数组。",
1027
- "Counts the number of elements in the array that satisfy the condition defined in the callback.": "统计数组中满足回调中定义条件的元素个数。",
1028
- "Adds time to a date": "为日期增加时间",
1029
- "Subtracts time from a date": "从日期中减去时间",
1030
- "Sets a specific unit of time on a date": "设置日期的某个时间单位",
1031
- "Formats a date into a string representation": "将日期格式化为字符串",
1032
- "Returns the start of a specified time unit for a date": "返回日期在指定时间单位上的起始",
1033
- "Returns the end of a specified time unit for a date": "返回日期在指定时间单位上的结束",
1034
- "Calculates the difference between two dates": "计算两个日期之间的差值",
1035
- "Converts a date to a different timezone": "将日期转换到不同的时区",
1036
- "Checks if two dates are the same": "检查两个日期是否相同",
1037
- "Checks if a date is before another date": "检查日期是否早于另一个日期",
1038
- "Checks if a date is after another date": "检查日期是否晚于另一个日期",
1039
- "Checks if a date is the same as or before another date": "检查日期是否等于或早于另一个日期",
1040
- "Checks if a date is the same as or after another date": "检查日期是否等于或晚于另一个日期",
1041
- "Gets the seconds of a date": "获取日期的秒",
1042
- "Gets the minutes of a date": "获取日期的分钟",
1043
- "Gets the hours of a date": "获取日期的小时",
1044
- "Gets the day of the month for a date": "获取日期在当月的第几天",
1045
- "Gets the day of the year for a date": "获取日期在当年的第几天",
1046
- "Gets the week of the year for a date": "获取日期在当年的第几周",
1047
- "Gets the day of the week for a date": "获取日期的星期几",
1048
- "Gets the month for a date": "获取日期的月份",
1049
- "Gets the quarter for a date": "获取日期所在的季度",
1050
- "Gets the year for a date": "获取日期的年份",
1051
- "Gets the Unix timestamp for a date": "获取日期的 Unix 时间戳",
1052
- "Gets the timezone offset name for a date": "获取日期的时区偏移名称",
1053
- "Checks if a date is valid": "检查日期是否有效",
1054
- "Checks if a date is yesterday": "检查日期是否为昨天",
1055
- "Checks if a date is today": "检查日期是否为今天",
1056
- "Checks if a date is tomorrow": "检查日期是否为明天",
1057
- "Checks if the year of a date is a leap year": "检查日期所在年份是否为闰年"
1058
- };
1059
- const EN_SOURCE_LABELS = {
1060
- lexerError: "Lexer error",
1061
- parserError: "Parser error",
1062
- compilerError: "Compiler error",
1063
- vmError: "VM error"
1064
- };
1065
- const ZH_SOURCE_LABELS = {
1066
- lexerError: "词法错误",
1067
- parserError: "语法错误",
1068
- compilerError: "编译错误",
1069
- vmError: "运行时错误"
1070
- };
1071
- function sourceLabelFrom(table, fallback, type) {
1072
- return (type === void 0 ? void 0 : table[type]) ?? fallback;
1073
- }
1074
- /**
1075
- * Built-in English message catalog (the default).
1076
- */
1077
- const enMessages = {
1078
- sourceLabel: (type) => sourceLabelFrom(EN_SOURCE_LABELS, "Error", type),
1079
- completionInfo: (info) => info,
1080
- typeCheckSource: "Type check",
1081
- expectedBoolean: (actualType) => `Expected a boolean test expression, received \`${actualType}\`.`,
1082
- expectedType: (expectedType, actualType) => `Expected \`${expectedType}\`, received \`${actualType}\`.`
1083
- };
1084
- /**
1085
- * Built-in Simplified Chinese message catalog.
1086
- */
1087
- const zhCNMessages = {
1088
- sourceLabel: (type) => sourceLabelFrom(ZH_SOURCE_LABELS, "错误", type),
1089
- completionInfo: (info) => info === "" ? "" : COMPLETION_INFO_ZH[info] ?? info,
1090
- typeCheckSource: "类型检查",
1091
- expectedBoolean: (actualType) => `期望布尔测试表达式,实际类型为 \`${actualType}\`。`,
1092
- expectedType: (expectedType, actualType) => `期望 \`${expectedType}\`,实际为 \`${actualType}\`。`
1093
- };
1094
- const localeRegistry = new Map([["en-US", enMessages], ["zh-CN", zhCNMessages]]);
1095
- let activeMessages = enMessages;
1096
- /**
1097
- * Register (or replace) the message catalog for a locale key, making it selectable
1098
- * via {@link configureExpressionMessages}. This is how a host adds a language the
1099
- * library does not ship — the built-in locales are registered the same way, so
1100
- * there is no privileged path.
1101
- */
1102
- function registerExpressionLocale(locale, messages) {
1103
- localeRegistry.set(locale, messages);
1104
- }
1105
- /**
1106
- * Configure the active message catalog. Pass a `locale`, a partial `messages`
1107
- * override, or both (overrides win). Idempotent and module-global, so a host
1108
- * configures it once (e.g. through `ExpressionConfigProvider`).
1109
- */
1110
- function configureExpressionMessages({ locale, messages }) {
1111
- const base = locale === void 0 ? activeMessages : localeRegistry.get(locale) ?? activeMessages;
1112
- activeMessages = messages === void 0 ? base : {
1113
- ...base,
1114
- ...messages
1115
- };
1116
- }
1117
- /**
1118
- * The active {@link ExpressionMessages} catalog (English by default).
1119
- */
1120
- function getExpressionMessages() {
1121
- return activeMessages;
1122
- }
1123
- //#endregion
1124
- //#region src/engine/intellisense.ts
1125
- function parseOffset(text) {
1126
- const trimmed = text?.trim();
1127
- return trimmed ? Number(trimmed) : NaN;
1128
- }
1129
- /**
1130
- * Parse a trailing `... at (from, to)` / `... at pos` position out of a ZEN error
1131
- * message. Returns a `[from, to]` range — a single offset collapses to
1132
- * `[pos, pos]` — or `null` when no position is present.
1133
- */
1134
- function extractPosition(message) {
1135
- const segments = message.split(" at ");
1136
- const last = segments.length <= 1 ? void 0 : segments.at(-1);
1137
- if (last === void 0) return null;
1138
- const [left, right] = last.replace("(", "").replace(")", "").split(", ");
1139
- const from = parseOffset(left);
1140
- if (Number.isNaN(from)) return null;
1141
- const to = parseOffset(right);
1142
- return [from, Number.isNaN(to) ? from : to];
1143
- }
1144
- /**
1145
- * Normalize the wasm validate payload (`null` or `{ type, source }`) into a
1146
- * positioned {@link ExpressionDiagnostic}, or `null` when the expression is valid.
1147
- */
1148
- function normalizeDiagnostic(raw, source) {
1149
- if (raw === null || raw === void 0) return null;
1150
- const errorType = isRecord(raw) && typeof raw.type === "string" ? raw.type : void 0;
1151
- const message = isRecord(raw) && typeof raw.source === "string" ? raw.source : String(raw);
1152
- const [from, to] = extractPosition(message) ?? [0, source.length];
1153
- return {
1154
- from,
1155
- to,
1156
- message,
1157
- source: getExpressionMessages().sourceLabel(errorType)
1158
- };
1159
- }
1160
- /**
1161
- * Normalize the wasm `getCompletions` payload into a typed list, dropping
1162
- * malformed entries and stripping the backtick markers ZEN wraps type names in.
1163
- */
1164
- function normalizeCompletions(raw) {
1165
- if (!Array.isArray(raw)) return [];
1166
- const messages = getExpressionMessages();
1167
- return raw.flatMap((entry) => {
1168
- if (!isRecord(entry) || typeof entry.label !== "string" || entry.label === "") return [];
1169
- return [{
1170
- type: entry.type === "method" || entry.type === "variable" ? entry.type : "function",
1171
- label: entry.label,
1172
- detail: typeof entry.detail === "string" ? entry.detail.replaceAll("`", "") : "",
1173
- info: messages.completionInfo(typeof entry.info === "string" ? entry.info : ""),
1174
- boost: typeof entry.boost === "number" ? entry.boost : null,
1175
- methodFor: typeof entry.methodFor === "string" ? entry.methodFor : null
1176
- }];
1177
- });
1178
- }
1179
- /**
1180
- * Validate an expression, loading the engine on first use. Resolves to `null`
1181
- * when the expression is valid, or a positioned diagnostic otherwise.
1182
- */
1183
- async function getDiagnostics(expression, mode) {
1184
- await loadEngine();
1185
- return getDiagnosticsSync(expression, mode);
1186
- }
1187
- /**
1188
- * Synchronous {@link getDiagnostics}. Throws `ExpressionNotReadyError` if the
1189
- * engine has not loaded yet — use only behind a readiness gate.
1190
- */
1191
- function getDiagnosticsSync(expression, mode) {
1192
- const engine = getEngineSync();
1193
- return normalizeDiagnostic(mode === "unary" ? engine.validateUnary(expression) : engine.validate(expression), expression);
1194
- }
1195
- /**
1196
- * Return the ZEN built-in completion list, loading the engine on first use.
1197
- */
1198
- async function getCompletionItems() {
1199
- await loadEngine();
1200
- return getCompletionItemsSync();
1201
- }
1202
- /**
1203
- * Synchronous {@link getCompletionItems}. Throws `ExpressionNotReadyError` if the
1204
- * engine has not loaded yet.
1205
- */
1206
- function getCompletionItemsSync() {
1207
- return normalizeCompletions(getEngineSync().getCompletions());
1208
- }
1209
- /**
1210
- * Type-check an expression against a `variables` context, loading the engine on
1211
- * first use. See {@link ExpressionAnalysis}.
1212
- */
1213
- async function analyzeTypes(variables, source, mode) {
1214
- await loadEngine();
1215
- return analyzeTypesSync(variables, source, mode);
1216
- }
1217
- /**
1218
- * Synchronous {@link analyzeTypes}. Throws `ExpressionNotReadyError` if the
1219
- * engine has not loaded yet.
1220
- */
1221
- function analyzeTypesSync(variables, source, mode) {
1222
- return getEngineSync().analyze(variables, source, mode === "unary");
1223
- }
1224
- /**
1225
- * Whether `actual` satisfies (is assignable to) `expected`, loading the engine on
1226
- * first use. Used for expected-return-type validation.
1227
- */
1228
- async function satisfiesType(actual, expected) {
1229
- await loadEngine();
1230
- return satisfiesTypeSync(actual, expected);
1231
- }
1232
- /**
1233
- * Synchronous {@link satisfiesType}. Throws `ExpressionNotReadyError` if the
1234
- * engine has not loaded yet.
1235
- */
1236
- function satisfiesTypeSync(actual, expected) {
1237
- return getEngineSync().satisfies(actual, expected);
1238
- }
1239
- //#endregion
1240
- //#region src/engine/template.ts
1241
- const HOLE_PATTERN = /\{\{(?<expression>[^{}]*)\}\}/g;
1242
- /**
1243
- * Extract every `{{ expression }}` hole from a template document, in source
1244
- * order. Literal text outside holes is ignored. Pure string scan — no engine
1245
- * required — matching the `@gorules/lezer-zen-template` hole grammar, so it is
1246
- * consistent with the editor's highlighting and completion gating.
1247
- */
1248
- function parseTemplateHoles(source) {
1249
- const holes = [];
1250
- HOLE_PATTERN.lastIndex = 0;
1251
- for (let match = HOLE_PATTERN.exec(source); match !== null; match = HOLE_PATTERN.exec(source)) {
1252
- const inner = match.groups?.expression ?? "";
1253
- const from = match.index + 2;
1254
- holes.push({
1255
- from,
1256
- to: from + inner.length,
1257
- expression: inner
1258
- });
1259
- }
1260
- return holes;
1261
- }
1262
- /**
1263
- * The template hole whose inner expression range contains `pos` (boundaries
1264
- * included, so a caret sitting right after `{{` or right before `}}` counts as
1265
- * inside), or `null` when `pos` is in literal text. Drives the editor's
1266
- * hole-scoped completion and hover: intelligence fires inside a hole, nothing in
1267
- * the surrounding literal text.
1268
- */
1269
- function templateHoleAt(source, pos) {
1270
- return parseTemplateHoles(source).find((hole) => pos >= hole.from && pos <= hole.to) ?? null;
1271
- }
1272
- function hasExpression(hole) {
1273
- return hole.expression.trim().length > 0;
1274
- }
1275
- /**
1276
- * Type-check every hole of a template against a `variables` context and merge the
1277
- * results into one {@link ExpressionAnalysis} whose spans are offset onto the
1278
- * template document. Each hole is a standard ZEN value expression; literal text
1279
- * contributes nothing. `rootKind` is the context itself (shared by every hole),
1280
- * so top-level completion works even in an empty hole.
1281
- *
1282
- * Best-effort: a hole that fails to analyze (e.g. mid-edit syntax) is skipped
1283
- * rather than discarding the spans of its siblings. Throws
1284
- * {@link ExpressionNotReadyError} if the engine has not loaded — use only behind
1285
- * a readiness gate.
1286
- */
1287
- function analyzeTemplateSync(variables, source) {
1288
- if (!isEngineReady()) throw new ExpressionNotReadyError();
1289
- const spans = [];
1290
- for (const hole of parseTemplateHoles(source)) {
1291
- if (!hasExpression(hole)) continue;
1292
- try {
1293
- for (const span of analyzeTypesSync(variables, hole.expression, "standard").spans) spans.push({
1294
- ...span,
1295
- span: [span.span[0] + hole.from, span.span[1] + hole.from]
1296
- });
1297
- } catch {}
1298
- }
1299
- return {
1300
- rootKind: variables,
1301
- spans
1302
- };
1303
- }
1304
- /**
1305
- * Async {@link analyzeTemplateSync}, loading the engine on first use.
1306
- */
1307
- async function analyzeTemplate(variables, source) {
1308
- await loadEngine();
1309
- return analyzeTemplateSync(variables, source);
1310
- }
1311
- /**
1312
- * Validate every hole of a template and return their syntax diagnostics, each
1313
- * offset onto the template document (empty holes and literal text produce none).
1314
- * The single-expression {@link getDiagnosticsSync} yields at most one diagnostic
1315
- * per hole, so a template with several broken holes surfaces each one.
1316
- *
1317
- * Throws {@link ExpressionNotReadyError} if the engine has not loaded — use only
1318
- * behind a readiness gate.
1319
- */
1320
- function getTemplateDiagnosticsSync(source) {
1321
- if (!isEngineReady()) throw new ExpressionNotReadyError();
1322
- const diagnostics = [];
1323
- for (const hole of parseTemplateHoles(source)) {
1324
- if (!hasExpression(hole)) continue;
1325
- const diagnostic = getDiagnosticsSync(hole.expression, "standard");
1326
- if (diagnostic) diagnostics.push({
1327
- ...diagnostic,
1328
- from: diagnostic.from + hole.from,
1329
- to: diagnostic.to + hole.from
1330
- });
1331
- }
1332
- return diagnostics;
1333
- }
1334
- /**
1335
- * Async {@link getTemplateDiagnosticsSync}, loading the engine on first use.
1336
- */
1337
- async function getTemplateDiagnostics(source) {
1338
- await loadEngine();
1339
- return getTemplateDiagnosticsSync(source);
1340
- }
1341
- //#endregion
1342
- exports.CONDITION_OPERATORS = CONDITION_OPERATORS;
1343
- exports.CONDITION_TREE_OPERATORS = CONDITION_TREE_OPERATORS;
1344
- exports.ExpressionError = ExpressionError;
1345
- exports.ExpressionNotReadyError = ExpressionNotReadyError;
1346
- exports.analyzeTemplate = analyzeTemplate;
1347
- exports.analyzeTemplateSync = analyzeTemplateSync;
1348
- exports.analyzeTypes = analyzeTypes;
1349
- exports.analyzeTypesSync = analyzeTypesSync;
1350
- exports.compileBranch = compileBranch;
1351
- exports.compileCondition = compileCondition;
1352
- exports.compileConditionTree = compileConditionTree;
1353
- exports.compileGroup = compileGroup;
1354
- exports.conditionOperatorArity = conditionOperatorArity;
1355
- exports.configureEngine = configureEngine;
1356
- exports.configureExpressionMessages = configureExpressionMessages;
1357
- exports.enMessages = enMessages;
1358
- exports.evaluate = evaluate;
1359
- exports.evaluateSync = evaluateSync;
1360
- exports.evaluateUnary = evaluateUnary;
1361
- exports.evaluateUnarySync = evaluateUnarySync;
1362
- exports.getCompletionItems = getCompletionItems;
1363
- exports.getCompletionItemsSync = getCompletionItemsSync;
1364
- exports.getDiagnostics = getDiagnostics;
1365
- exports.getDiagnosticsSync = getDiagnosticsSync;
1366
- exports.getEngineError = getEngineError;
1367
- exports.getEngineSync = getEngineSync;
1368
- exports.getExpressionMessages = getExpressionMessages;
1369
- exports.getTemplateDiagnostics = getTemplateDiagnostics;
1370
- exports.getTemplateDiagnosticsSync = getTemplateDiagnosticsSync;
1371
- exports.isEngineReady = isEngineReady;
1372
- exports.liftConditionTree = liftConditionTree;
1373
- exports.loadEngine = loadEngine;
1374
- exports.parseTemplateHoles = parseTemplateHoles;
1375
- exports.registerExpressionLocale = registerExpressionLocale;
1376
- exports.resetEngine = resetEngine;
1377
- exports.satisfiesType = satisfiesType;
1378
- exports.satisfiesTypeSync = satisfiesTypeSync;
1379
- exports.selectBranch = selectBranch;
1380
- exports.selectBranchWith = selectBranchWith;
1381
- exports.templateHoleAt = templateHoleAt;
1382
- exports.toZenLiteral = toZenLiteral;
1383
- exports.zhCNMessages = zhCNMessages;
1384
-
1385
- //# sourceMappingURL=index.cjs.map
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class extends Error{expression;constructor(e,t,n){super(e,{cause:n}),this.name=`ExpressionError`,this.expression=t}},t=class extends e{constructor(e=`Expression engine is not initialized. Await loadEngine() or render under <ExpressionEngineProvider>.`){super(e),this.name=`ExpressionNotReadyError`}};function n(e){return e===void 0}function r(e){return typeof e==`string`}function i(e){return Array.isArray(e)}function a(e){return e==null}function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}let s=null,c=null,l=null,u,d=null;function f(t,n){try{return n()}catch(n){throw new e(`Failed to evaluate expression: ${t}`,t,n)}}function p(){let e=`Failed to load the ZEN expression engine`;return typeof window>`u`?`${e}. 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.`:e}function m(t){if(s||c)throw new e(`configureEngine() must be called before the engine loads.`);u=t.wasmInput}function h(){return s||(l=null,s=(async()=>{let e=await import(`@gorules/zen-engine-wasm`);await e.default(n(u)?void 0:{module_or_path:u});let t=t=>e.VariableType.fromJson(t),r=e=>{if(d&&d.variables===e)return d;let n=d;d=null,n?.handle.free();let r=t(e);try{d={variables:e,handle:r,rootKind:r.toJson()}}catch(e){throw r.free(),e}return d},i=Object.freeze({evaluate:(t,n={})=>f(t,()=>e.evaluateExpression(t,n)),evaluateUnary:(t,n={})=>f(t,()=>e.evaluateUnaryExpression(t,n)),validate:t=>e.validateExpression(t),validateUnary:t=>e.validateUnaryExpression(t),getCompletions:()=>e.getCompletions(),analyze:(e,t,n)=>{let i=r(e),a=n?i.handle.typeCheckUnary(t):i.handle.typeCheck(t);return{rootKind:i.rootKind,spans:Array.isArray(a)?a:[]}},satisfies:(e,n)=>{let r=t(e);try{let e=t(n);try{return r.satisfies(e)}finally{e.free()}}finally{r.free()}},isReady:()=>e.isReady()});return c=i,i})().catch(t=>{throw s=null,l=new e(p(),void 0,t),l}),s)}function g(){return c?.isReady()??!1}function _(){return l}function v(){if(!c)throw new t;return c}function ee(){d?.handle.free(),d=null,s=null,c=null,l=null,u=void 0}const y=te();function te(){return typeof process>`u`?!1:process.env?process.env.NODE_ENV!==`production`:!1}const ne=/^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i,re=new Set([`and`,`or`,`not`,`in`,`true`,`false`,`null`]);function b(e){return ne.test(e)&&(e.match(/[A-Z_$][\w$]*/gi)??[]).every(e=>!re.has(e))}function x(t){if(a(t))return`null`;if(typeof t==`number`){if(!Number.isFinite(t))throw new e(`Number ${String(t)} has no ZEN literal representation`);return String(t)}if(typeof t==`boolean`||typeof t==`bigint`)return String(t);if(r(t))return ie(t);if(i(t))return`[${t.map(e=>x(e)).join(`, `)}]`;throw new e(`Value of type "${typeof t}" has no ZEN literal representation`)}function ie(t){let n=t.includes(`'`),r=t.includes(`"`);if(n&&r)throw new e(`String contains both single and double quotes and has no ZEN literal representation`);return n?`"${t}"`:`'${t}'`}function S(e){return i(e)?x(e):`[${x(e)}]`}function C(e){return`(${e} == null or (type(${e}) == 'string' and len(trim(${e})) == 0) or (type(${e}) == 'array' and len(${e}) == 0))`}function ae(t,n,r){switch(n){case`eq`:return`${t} == ${x(r)}`;case`ne`:return`${t} != ${x(r)}`;case`gt`:return`${t} > ${x(r)}`;case`gte`:return`${t} >= ${x(r)}`;case`lt`:return`${t} < ${x(r)}`;case`lte`:return`${t} <= ${x(r)}`;case`contains`:return`contains(${t}, ${x(r)})`;case`not_contains`:return`not contains(${t}, ${x(r)})`;case`starts_with`:return`startsWith(${t}, ${x(r)})`;case`ends_with`:return`endsWith(${t}, ${x(r)})`;case`in`:return`${t} in ${S(r)}`;case`not_in`:return`not (${t} in ${S(r)})`;case`is_empty`:return C(t);case`is_not_empty`:return`not ${C(t)}`;default:throw new e(`Unsupported operator: ${String(n)}`)}}function w(e){if(e.kind===`expression`){let t=e.expression.trim();return t===``?null:`(${t})`}let t=e.subject.trim();if(!b(t))return null;try{return ae(t,e.operator,e.value)}catch{return null}}function T(e){let t=e.conditions.map(e=>w(e)).filter(e=>e!==null);return t.length===0?null:t.join(` and `)}function E(e){let t=(e.conditionGroups??[]).map(e=>T(e)).filter(e=>e!==null);return t.length===0?null:t.map(e=>`(${e})`).join(` or `)}function D(e,t,n){let r=e.toSorted((e,t)=>e.priority-t.priority);for(let e of r){if(e.isDefault)continue;let r=E(e);if(r===null){y&&console.warn(`[expression] branch "${e.id}" has no compilable condition and can never match`);continue}if(oe(n,r,t))return{branchId:e.id,matched:!0}}return{branchId:r.find(e=>e.isDefault)?.id??null,matched:!1}}function oe(e,t,n){try{return e.evaluate(t,n)===!0}catch(n){return y&&se(e,t,n),!1}}function se(e,t,n){let r;try{r=e.validate(t)}catch{return}a(r)||console.warn(`[expression] compiled branch expression failed to parse: ${t}`,r,n)}async function ce(e,t){return D(e,t,await h())}const O=[`eq`,`ne`,`gt`,`gte`,`lt`,`lte`,`contains`,`not_contains`,`starts_with`,`ends_with`,`in`,`not_in`,`is_empty`,`is_not_empty`],le={eq:`scalar`,ne:`scalar`,gt:`scalar`,gte:`scalar`,lt:`scalar`,lte:`scalar`,contains:`scalar`,not_contains:`scalar`,starts_with:`scalar`,ends_with:`scalar`,in:`array`,not_in:`array`,is_empty:`none`,is_not_empty:`none`};function k(e){return le[e]}function ue(e){let t=A(e);return t===null?``:j(t,!0)}function A(e){if(e.kind===`rule`)return M(e)===null?null:e;let t=e.items.map(e=>A(e)).filter(e=>e!==null);return t.length===0?null:t.length===1?t[0]:{kind:`group`,op:e.op,items:t}}function j(e,t){if(e.kind===`rule`)return M(e);let n=e.items.map(e=>j(e,!1)).join(e.op===`and`?` and `:` or `);return t?n:`(${n})`}function M(e){return de(e)?w({kind:`field`,subject:e.left,operator:e.operator,value:e.right}):null}function de(e){switch(k(e.operator)){case`scalar`:return N(e.right);case`array`:return fe(e.right);case`none`:return e.right===void 0}}function N(e){return typeof e==`string`||typeof e==`number`||typeof e==`boolean`}function fe(e){return i(e)&&e.every(e=>N(e))}const pe=new Set([`==`,`!=`,`<=`,`>=`]),me=new Set([`(`,`)`,`[`,`]`,`,`,`.`,`<`,`>`,`-`]),he=/[a-z_$]/i,ge=/[\w$]/,_e=/\d/,ve=/^\d+(?:\.\d+)?(?:e[+-]?\d+)?/i,ye=/^\d+$/,be={"==":`eq`,"!=":`ne`,">":`gt`,">=":`gte`,"<":`lt`,"<=":`lte`},xe={contains:`contains`,startsWith:`starts_with`,endsWith:`ends_with`};function Se(e){let t=L(e);if(t===null||t.length===0)return null;let n=0;function r(e=0){return t[n+e]}function i(e){let t=r();return t!==void 0&&t.kind===`punct`&&t.value===e?(n+=1,!0):!1}function a(e){let t=r();return t!==void 0&&t.kind===`ident`&&t.value===e?(n+=1,!0):!1}function o(e,n){for(let[r,i]of n.entries()){let n=t[e+r],a=i;if(n===void 0||n.kind!==a.kind||n.value!==a.value)return!1}return!0}function s(){let e=P(t,n);return e===null?null:(n=e.next,e.path)}function c(){let e=r();if(e===void 0)return null;if(e.kind===`string`)return n+=1,e.value;if(e.kind===`number`)return n+=1,Number(e.value);if(e.kind===`ident`)return e.value===`true`?(n+=1,!0):e.value===`false`?(n+=1,!1):null;if(e.kind===`punct`&&e.value===`-`){let e=r(1);return e===void 0||e.kind!==`number`?null:(n+=2,-Number(e.value))}return null}function l(){if(!i(`[`))return null;if(I(r(),`]`))return n+=1,[];let e=c();if(e===null)return null;let t=[e];for(;I(r(),`,`);){n+=1;let e=c();if(e===null)return null;t.push(e)}return i(`]`)?t:null}function u(e){if(!i(`(`))return null;let t=s();if(t===null||!i(`,`))return null;let n=c();return n===null||!i(`)`)?null:{kind:`rule`,left:t,operator:e,right:n}}function d(){let e=s();if(e===null||!a(`in`))return null;let t=l();return t===null||!i(`)`)?null:{kind:`rule`,left:e,operator:`not_in`,right:t}}function f(){let e=r();if(e===void 0)return null;let i,a;if(e.kind===`ident`&&e.value===`not`&&I(r(1),`(`))i=`is_not_empty`,a=n+2;else if(e.kind===`punct`&&e.value===`(`)i=`is_empty`,a=n+1;else return null;let s=P(t,a);if(s===null)return null;let c=w({kind:`field`,subject:s.path,operator:i,value:void 0});if(c===null)return null;let l=L(c);return l===null||!o(n,l)?null:(n+=l.length,{kind:`rule`,left:s.path,operator:i})}function p(){let e=r();if(e===void 0||e.kind!==`ident`)return null;if(e.value===`not`){let e=r(1);return e===void 0?null:e.kind===`ident`&&e.value===`contains`&&I(r(2),`(`)?(n+=2,u(`not_contains`)):e.kind===`punct`&&e.value===`(`?(n+=2,d()):null}if(I(r(1),`(`)){let t=xe[e.value];return t===void 0?null:(n+=1,u(t))}let t=s();if(t===null)return null;let i=r();if(i===void 0)return null;if(i.kind===`ident`&&i.value===`in`){n+=1;let e=l();return e===null?null:{kind:`rule`,left:t,operator:`in`,right:e}}if(i.kind===`punct`){let e=be[i.value];if(e===void 0)return null;n+=1;let r=c();return r===null?null:{kind:`rule`,left:t,operator:e,right:r}}return null}function m(e){let t=f();if(t!==null)return t;if(I(r(),`(`)){if(e>=64)return null;n+=1;let t=g(e+1);return t===null||!i(`)`)?null:F(t)}return p()}function h(){let e=r();return e!==void 0&&e.kind===`ident`&&(e.value===`and`||e.value===`or`)?e.value:null}function g(e){let t=m(e);if(t===null)return null;let r=[t],i=null,a=h();for(;a!==null;){if(i===null)i=a;else if(i!==a)return null;n+=1;let t=m(e);if(t===null)return null;r.push(t),a=h()}return r.length===1?r[0]:{kind:`group`,op:i??`and`,items:r}}let _=g(0);return _===null||n!==t.length?null:F(_)}function P(e,t){let n=e[t];if(n===void 0||n.kind!==`ident`)return null;let r=n.value,i=t+1,a=e[i];for(;a!==void 0&&a.kind===`punct`&&(a.value===`.`||a.value===`[`);){if(a.value===`.`){let t=e[i+1];if(t===void 0||t.kind!==`ident`)return null;r+=`.${t.value}`,i+=2}else{let t=e[i+1];if(t===void 0||t.kind!==`number`||!ye.test(t.value))return null;let n=e[i+2];if(n===void 0||n.kind!==`punct`||n.value!==`]`)return null;r+=`[${t.value}]`,i+=3}a=e[i]}return b(r)?{path:r,next:i}:null}function F(e){return e.kind===`group`?e:{kind:`group`,op:`and`,items:[e]}}function I(e,t){return e!==void 0&&e.kind===`punct`&&e.value===t}function L(e){let t=[],n=0;for(;n<e.length;){let r=e[n];if(r===` `||r===` `||r===`
2
+ `||r===`\r`){n+=1;continue}if(r===`'`||r===`"`){let i=e.indexOf(r,n+1);if(i===-1)return null;t.push({kind:`string`,value:e.slice(n+1,i)}),n=i+1;continue}let i=e.slice(n,n+2);if(pe.has(i)){t.push({kind:`punct`,value:i}),n+=2;continue}if(me.has(r)){t.push({kind:`punct`,value:r}),n+=1;continue}if(_e.test(r)){let r=ve.exec(e.slice(n));if(r===null)return null;t.push({kind:`number`,value:r[0]}),n+=r[0].length;continue}if(he.test(r)){let r=n+1;for(;r<e.length&&ge.test(e[r]);)r+=1;t.push({kind:`ident`,value:e.slice(n,r)}),n=r;continue}return null}return t}const Ce=O;async function we(e,t){return await h(),R(e,t)}async function Te(e,t){return await h(),z(e,t)}function R(e,t){return v().evaluate(e,t)}function z(e,t){return v().evaluateUnary(e,t)}const Ee={"Returns the length of variable":`返回变量的长度`,"Checks if variable contains a needle":`检查变量是否包含指定元素`,"Flattens an array":`将数组扁平化`,"Merges multiple objects into one.":`将多个对象合并为一个。`,"Deeply merges multiple objects into one.":`将多个对象深度合并为一个。`,"Converts all characters in a string to uppercase":`将字符串中所有字符转换为大写`,"Converts all characters in a string to lowercase":`将字符串中所有字符转换为小写`,"Returns the string with leading and trailing whitespace removed":`返回去除首尾空白后的字符串`,"Returns true if the string starts with the specified prefix":`若字符串以指定前缀开头则返回 true`,"Returns true if the string ends with the specified suffix":`若字符串以指定后缀结尾则返回 true`,"Returns true if the string matches the specified pattern":`若字符串匹配指定模式则返回 true`,"Extracts matching substrings according to a pattern":`按模式提取匹配的子串`,"Performs a fuzzy search of the needle in the haystack, and returns the match score(s).":`在目标中对关键字进行模糊搜索,并返回匹配得分。`,"Splits a string into an array of substrings using the specified delimiter.":`使用指定分隔符将字符串拆分为子串数组。`,"Returns the absolute value of a number":`返回数字的绝对值`,"Returns the sum of all elements in the input array.":`返回输入数组中所有元素之和。`,"Calculates the average of all elements in the input array.":`计算输入数组中所有元素的平均值。`,"Returns the smallest of the elements in the input array.":`返回输入数组中的最小元素。`,"Returns the largest of the elements in the input array.":`返回输入数组中的最大元素。`,"Generates a random number between 0 (inclusive) and max (inclusive).":`生成 0 到 max(均包含)之间的随机数。`,"Calculates the median value of all elements in the input array.":`计算输入数组中所有元素的中位数。`,"Finds the mode(s) of the input array, which are the most frequent element(s).":`求输入数组的众数,即出现最频繁的元素。`,"Rounds a number down to the nearest integer.":`向下取整到最接近的整数。`,"Rounds a number up to the nearest integer.":`向上取整到最接近的整数。`,"Rounds a number to a specified number of decimal places.":`将数字四舍五入到指定的小数位数。`,"Truncates a number to a specified number of decimal places.":`将数字截断到指定的小数位数。`,"Checks if the given value is of a numeric type.":`检查给定值是否为数值类型。`,"Converts the given value to a string.":`将给定值转换为字符串。`,"Converts the given value to a number.":`将给定值转换为数字。`,"Converts the given value to a boolean.":`将给定值转换为布尔值。`,"Returns a string representing the data type of the value.":`返回表示该值数据类型的字符串。`,"Returns an array of a given object's own enumerable property names.":`返回由给定对象自身可枚举属性名组成的数组。`,"Returns an array of a given object's own enumerable property values.":`返回由给定对象自身可枚举属性值组成的数组。`,"Returns a new date time instance.":`返回一个新的日期时间实例。`,"Converts a numeric timestamp to a unix timestamp.":`将数值时间戳转换为 Unix 时间戳。`,"Extracts the time from a numeric timestamp and returns it as a seconds from beginning of day.":`从数值时间戳中提取时间,以当天起始的秒数返回。`,"e.g. 1h30min":`例如 1h30min`,"Extracts the year from a given timestamp.":`从给定时间戳中提取年份。`,"Gets the day of the week from a given timestamp, where Sunday might be 0.":`获取给定时间戳的星期几(周日可能为 0)。`,"Extracts the day of the month from a given timestamp.":`从给定时间戳中提取当月的日期。`,"Gets the day of the year from a given timestamp.":`获取给定时间戳在一年中的第几天。`,"Calculates the week of the year from a given timestamp.":`计算给定时间戳在一年中的第几周。`,"Extracts the month from a given timestamp, typically with January as 1.":`从给定时间戳中提取月份(通常 1 月为 1)。`,"Converts the month from a given timestamp into its string representation (e.g., 'Jan').":`将给定时间戳的月份转换为字符串表示(例如 'Jan')。`,"Converts a timestamp to a human-readable date string.":`将时间戳转换为人类可读的日期字符串。`,"Converts the day of the week from a given timestamp into its string representation (e.g., 'Mon').":`将给定时间戳的星期几转换为字符串表示(例如 'Mon')。`,"Returns the timestamp representing the start of a specified unit (e.g., day, month, year) based on a given timestamp.":`返回给定时间戳在指定单位(如日、月、年)起始处的时间戳。`,"Returns the timestamp representing the end of a specified unit (e.g., day, month, year) based on a given timestamp.":`返回给定时间戳在指定单位(如日、月、年)结束处的时间戳。`,"Checks if all elements in the array satisfy the condition defined in the callback.":`检查数组中所有元素是否都满足回调中定义的条件。`,"Checks if no elements in the array satisfy the condition defined in the callback.":`检查数组中是否没有任何元素满足回调中定义的条件。`,"Checks if at least one element in the array satisfies the condition defined in the callback.":`检查数组中是否至少有一个元素满足回调中定义的条件。`,"Checks if exactly one element in the array satisfies the condition defined in the callback.":`检查数组中是否恰好有一个元素满足回调中定义的条件。`,"Creates a new array with all elements that satisfy the condition defined in the callback.":`创建一个仅包含满足回调中定义条件的元素的新数组。`,"Creates a new array populated with the results of calling the provided function on every element in the calling array.":`创建一个新数组,其元素为对原数组每个元素调用所提供函数的结果。`,"First maps each element using a mapping function, then flattens the result into a new array.":`先用映射函数处理每个元素,再将结果扁平化为新数组。`,"Counts the number of elements in the array that satisfy the condition defined in the callback.":`统计数组中满足回调中定义条件的元素个数。`,"Adds time to a date":`为日期增加时间`,"Subtracts time from a date":`从日期中减去时间`,"Sets a specific unit of time on a date":`设置日期的某个时间单位`,"Formats a date into a string representation":`将日期格式化为字符串`,"Returns the start of a specified time unit for a date":`返回日期在指定时间单位上的起始`,"Returns the end of a specified time unit for a date":`返回日期在指定时间单位上的结束`,"Calculates the difference between two dates":`计算两个日期之间的差值`,"Converts a date to a different timezone":`将日期转换到不同的时区`,"Checks if two dates are the same":`检查两个日期是否相同`,"Checks if a date is before another date":`检查日期是否早于另一个日期`,"Checks if a date is after another date":`检查日期是否晚于另一个日期`,"Checks if a date is the same as or before another date":`检查日期是否等于或早于另一个日期`,"Checks if a date is the same as or after another date":`检查日期是否等于或晚于另一个日期`,"Gets the seconds of a date":`获取日期的秒`,"Gets the minutes of a date":`获取日期的分钟`,"Gets the hours of a date":`获取日期的小时`,"Gets the day of the month for a date":`获取日期在当月的第几天`,"Gets the day of the year for a date":`获取日期在当年的第几天`,"Gets the week of the year for a date":`获取日期在当年的第几周`,"Gets the day of the week for a date":`获取日期的星期几`,"Gets the month for a date":`获取日期的月份`,"Gets the quarter for a date":`获取日期所在的季度`,"Gets the year for a date":`获取日期的年份`,"Gets the Unix timestamp for a date":`获取日期的 Unix 时间戳`,"Gets the timezone offset name for a date":`获取日期的时区偏移名称`,"Checks if a date is valid":`检查日期是否有效`,"Checks if a date is yesterday":`检查日期是否为昨天`,"Checks if a date is today":`检查日期是否为今天`,"Checks if a date is tomorrow":`检查日期是否为明天`,"Checks if the year of a date is a leap year":`检查日期所在年份是否为闰年`},De={lexerError:`Lexer error`,parserError:`Parser error`,compilerError:`Compiler error`,vmError:`VM error`},Oe={lexerError:`词法错误`,parserError:`语法错误`,compilerError:`编译错误`,vmError:`运行时错误`};function B(e,t,n){return(n===void 0?void 0:e[n])??t}const V={sourceLabel:e=>B(De,`Error`,e),completionInfo:e=>e,typeCheckSource:`Type check`,expectedBoolean:e=>`Expected a boolean test expression, received \`${e}\`.`,expectedType:(e,t)=>`Expected \`${e}\`, received \`${t}\`.`},H={sourceLabel:e=>B(Oe,`错误`,e),completionInfo:e=>e===``?``:Ee[e]??e,typeCheckSource:`类型检查`,expectedBoolean:e=>`期望布尔测试表达式,实际类型为 \`${e}\`。`,expectedType:(e,t)=>`期望 \`${e}\`,实际为 \`${t}\`。`},U=new Map([[`en-US`,V],[`zh-CN`,H]]);let W=V;function ke(e,t){U.set(e,t)}function Ae({locale:e,messages:t}){let n=e===void 0?W:U.get(e)??W;W=t===void 0?n:{...n,...t}}function G(){return W}function K(e){let t=e?.trim();return t?Number(t):NaN}function je(e){let t=e.split(` at `),n=t.length<=1?void 0:t.at(-1);if(n===void 0)return null;let[r,i]=n.replace(`(`,``).replace(`)`,``).split(`, `),a=K(r);if(Number.isNaN(a))return null;let o=K(i);return[a,Number.isNaN(o)?a:o]}function Me(e,t){if(e==null)return null;let n=o(e)&&typeof e.type==`string`?e.type:void 0,r=o(e)&&typeof e.source==`string`?e.source:String(e),[i,a]=je(r)??[0,t.length];return{from:i,to:a,message:r,source:G().sourceLabel(n)}}function Ne(e){if(!Array.isArray(e))return[];let t=G();return e.flatMap(e=>!o(e)||typeof e.label!=`string`||e.label===``?[]:[{type:e.type===`method`||e.type===`variable`?e.type:`function`,label:e.label,detail:typeof e.detail==`string`?e.detail.replaceAll("`",``):``,info:t.completionInfo(typeof e.info==`string`?e.info:``),boost:typeof e.boost==`number`?e.boost:null,methodFor:typeof e.methodFor==`string`?e.methodFor:null}])}async function Pe(e,t){return await h(),q(e,t)}function q(e,t){let n=v();return Me(t===`unary`?n.validateUnary(e):n.validate(e),e)}async function Fe(){return await h(),J()}function J(){return Ne(v().getCompletions())}async function Ie(e,t,n){return await h(),Y(e,t,n)}function Y(e,t,n){return v().analyze(e,t,n===`unary`)}async function Le(e,t){return await h(),X(e,t)}function X(e,t){return v().satisfies(e,t)}const Z=/\{\{(?<expression>[^{}]*)\}\}/g;function Q(e){let t=[];Z.lastIndex=0;for(let n=Z.exec(e);n!==null;n=Z.exec(e)){let e=n.groups?.expression??``,r=n.index+2;t.push({from:r,to:r+e.length,expression:e})}return t}function Re(e,t){return Q(e).find(e=>t>=e.from&&t<=e.to)??null}function ze(e){return e.expression.trim().length>0}function Be(e,n){if(!g())throw new t;let r=[];for(let t of Q(n))if(ze(t))try{for(let n of Y(e,t.expression,`standard`).spans)r.push({...n,span:[n.span[0]+t.from,n.span[1]+t.from]})}catch{}return{rootKind:e,spans:r}}async function Ve(e,t){return await h(),Be(e,t)}function $(e){if(!g())throw new t;let n=[];for(let t of Q(e)){if(!ze(t))continue;let e=q(t.expression,`standard`);e&&n.push({...e,from:e.from+t.from,to:e.to+t.from})}return n}async function He(e){return await h(),$(e)}exports.CONDITION_OPERATORS=O,exports.CONDITION_TREE_OPERATORS=Ce,exports.ExpressionError=e,exports.ExpressionNotReadyError=t,exports.analyzeTemplate=Ve,exports.analyzeTemplateSync=Be,exports.analyzeTypes=Ie,exports.analyzeTypesSync=Y,exports.compileBranch=E,exports.compileCondition=w,exports.compileConditionTree=ue,exports.compileGroup=T,exports.conditionOperatorArity=k,exports.configureEngine=m,exports.configureExpressionMessages=Ae,exports.enMessages=V,exports.evaluate=we,exports.evaluateSync=R,exports.evaluateUnary=Te,exports.evaluateUnarySync=z,exports.getCompletionItems=Fe,exports.getCompletionItemsSync=J,exports.getDiagnostics=Pe,exports.getDiagnosticsSync=q,exports.getEngineError=_,exports.getEngineSync=v,exports.getExpressionMessages=G,exports.getTemplateDiagnostics=He,exports.getTemplateDiagnosticsSync=$,exports.isEngineReady=g,exports.liftConditionTree=Se,exports.loadEngine=h,exports.parseTemplateHoles=Q,exports.registerExpressionLocale=ke,exports.resetEngine=ee,exports.satisfiesType=Le,exports.satisfiesTypeSync=X,exports.selectBranch=ce,exports.selectBranchWith=D,exports.templateHoleAt=Re,exports.toZenLiteral=x,exports.zhCNMessages=H;