@coldsmirk/abacus-core 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -208,11 +208,14 @@ function detectDev() {
208
208
  return process.env ? process.env.NODE_ENV !== "production" : false;
209
209
  }
210
210
  //#endregion
211
- //#region src/condition/compile.ts
211
+ //#region src/condition/subject.ts
212
212
  /**
213
- * A subject must be a plain identifier path (`amount`, `user.age`, `items[0]`).
214
- * It is emitted verbatim into ZEN source, so anything else is rejected to keep
215
- * the condition compiler from being an expression-injection sink.
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.
216
219
  */
217
220
  const SUBJECT_PATTERN = /^[A-Z_$][\w$]*(?:\.[A-Z_$][\w$]*|\[\d+\])*$/i;
218
221
  const ZEN_RESERVED_WORDS = new Set([
@@ -224,9 +227,15 @@ const ZEN_RESERVED_WORDS = new Set([
224
227
  "false",
225
228
  "null"
226
229
  ]);
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
+ */
227
234
  function isIdentifierPath(subject) {
228
235
  return SUBJECT_PATTERN.test(subject) && (subject.match(/[A-Z_$][\w$]*/gi) ?? []).every((segment) => !ZEN_RESERVED_WORDS.has(segment));
229
236
  }
237
+ //#endregion
238
+ //#region src/condition/compile.ts
230
239
  /**
231
240
  * Serialize a JavaScript value into a ZEN literal. Nullish becomes `null`;
232
241
  * numbers / booleans / bigints are emitted verbatim; strings are quoted via
@@ -419,6 +428,508 @@ const CONDITION_OPERATORS = [
419
428
  "is_empty",
420
429
  "is_not_empty"
421
430
  ];
431
+ const CONDITION_OPERATOR_ARITIES = {
432
+ eq: "scalar",
433
+ ne: "scalar",
434
+ gt: "scalar",
435
+ gte: "scalar",
436
+ lt: "scalar",
437
+ lte: "scalar",
438
+ contains: "scalar",
439
+ not_contains: "scalar",
440
+ starts_with: "scalar",
441
+ ends_with: "scalar",
442
+ in: "array",
443
+ not_in: "array",
444
+ is_empty: "none",
445
+ is_not_empty: "none"
446
+ };
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
+ function conditionOperatorArity(operator) {
455
+ return CONDITION_OPERATOR_ARITIES[operator];
456
+ }
457
+ //#endregion
458
+ //#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
+ function compileConditionTree(tree) {
464
+ const normalized = normalizeNode(tree);
465
+ return normalized === null ? "" : emitNode(normalized, true);
466
+ }
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
+ function normalizeNode(node) {
474
+ if (node.kind === "rule") return compileRule(node) === null ? null : node;
475
+ const items = node.items.map((item) => normalizeNode(item)).filter((item) => item !== null);
476
+ if (items.length === 0) return null;
477
+ if (items.length === 1) return items[0];
478
+ return {
479
+ kind: "group",
480
+ op: node.op,
481
+ items
482
+ };
483
+ }
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
+ function emitNode(node, topLevel) {
490
+ if (node.kind === "rule") return compileRule(node);
491
+ const joined = node.items.map((item) => emitNode(item, false)).join(node.op === "and" ? " and " : " or ");
492
+ return topLevel ? joined : `(${joined})`;
493
+ }
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
+ function compileRule(rule) {
506
+ if (!matchesOperatorArity(rule)) return null;
507
+ return compileCondition({
508
+ kind: "field",
509
+ subject: rule.left,
510
+ operator: rule.operator,
511
+ value: rule.right
512
+ });
513
+ }
514
+ function matchesOperatorArity(rule) {
515
+ switch (conditionOperatorArity(rule.operator)) {
516
+ case "scalar": return isConditionScalar(rule.right);
517
+ case "array": return isScalarArray(rule.right);
518
+ case "none": return rule.right === void 0;
519
+ }
520
+ }
521
+ function isConditionScalar(value) {
522
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
523
+ }
524
+ function isScalarArray(value) {
525
+ return isArray(value) && value.every((item) => isConditionScalar(item));
526
+ }
527
+ //#endregion
528
+ //#region src/condition/lift-tree.ts
529
+ const TWO_CHAR_PUNCTUATION = new Set([
530
+ "==",
531
+ "!=",
532
+ "<=",
533
+ ">="
534
+ ]);
535
+ const ONE_CHAR_PUNCTUATION = new Set([
536
+ "(",
537
+ ")",
538
+ "[",
539
+ "]",
540
+ ",",
541
+ ".",
542
+ "<",
543
+ ">",
544
+ "-"
545
+ ]);
546
+ const IDENT_START = /[a-z_$]/i;
547
+ const IDENT_PART = /[\w$]/;
548
+ const DIGIT = /\d/;
549
+ const NUMBER_PATTERN = /^\d+(?:\.\d+)?(?:e[+-]?\d+)?/i;
550
+ const INTEGER_PATTERN = /^\d+$/;
551
+ const COMPARISON_OPERATORS = {
552
+ "==": "eq",
553
+ "!=": "ne",
554
+ ">": "gt",
555
+ ">=": "gte",
556
+ "<": "lt",
557
+ "<=": "lte"
558
+ };
559
+ const CALL_OPERATORS = {
560
+ contains: "contains",
561
+ startsWith: "starts_with",
562
+ endsWith: "ends_with"
563
+ };
564
+ 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
+ function liftConditionTree(expression) {
573
+ const tokens = tokenize(expression);
574
+ if (tokens === null || tokens.length === 0) return null;
575
+ let pos = 0;
576
+ function peek(offset = 0) {
577
+ return tokens[pos + offset];
578
+ }
579
+ function consumePunct(value) {
580
+ const token = peek();
581
+ if (token !== void 0 && token.kind === "punct" && token.value === value) {
582
+ pos += 1;
583
+ return true;
584
+ }
585
+ return false;
586
+ }
587
+ function consumeIdent(value) {
588
+ const token = peek();
589
+ if (token !== void 0 && token.kind === "ident" && token.value === value) {
590
+ pos += 1;
591
+ return true;
592
+ }
593
+ return false;
594
+ }
595
+ function tokensMatchAt(from, expected) {
596
+ for (const [index, element] of expected.entries()) {
597
+ const actual = tokens[from + index];
598
+ const want = element;
599
+ if (actual === void 0 || actual.kind !== want.kind || actual.value !== want.value) return false;
600
+ }
601
+ return true;
602
+ }
603
+ function parsePath() {
604
+ const read = readPath(tokens, pos);
605
+ if (read === null) return null;
606
+ pos = read.next;
607
+ return read.path;
608
+ }
609
+ function parseLiteral() {
610
+ const token = peek();
611
+ if (token === void 0) return null;
612
+ if (token.kind === "string") {
613
+ pos += 1;
614
+ return token.value;
615
+ }
616
+ if (token.kind === "number") {
617
+ pos += 1;
618
+ return Number(token.value);
619
+ }
620
+ if (token.kind === "ident") {
621
+ if (token.value === "true") {
622
+ pos += 1;
623
+ return true;
624
+ }
625
+ if (token.value === "false") {
626
+ pos += 1;
627
+ return false;
628
+ }
629
+ return null;
630
+ }
631
+ if (token.kind === "punct" && token.value === "-") {
632
+ const digits = peek(1);
633
+ if (digits === void 0 || digits.kind !== "number") return null;
634
+ pos += 2;
635
+ return -Number(digits.value);
636
+ }
637
+ return null;
638
+ }
639
+ function parseArray() {
640
+ if (!consumePunct("[")) return null;
641
+ if (isPunct(peek(), "]")) {
642
+ pos += 1;
643
+ return [];
644
+ }
645
+ const first = parseLiteral();
646
+ if (first === null) return null;
647
+ const items = [first];
648
+ while (isPunct(peek(), ",")) {
649
+ pos += 1;
650
+ const next = parseLiteral();
651
+ if (next === null) return null;
652
+ items.push(next);
653
+ }
654
+ return consumePunct("]") ? items : null;
655
+ }
656
+ function parseCall(operator) {
657
+ if (!consumePunct("(")) return null;
658
+ const left = parsePath();
659
+ if (left === null || !consumePunct(",")) return null;
660
+ const right = parseLiteral();
661
+ if (right === null || !consumePunct(")")) return null;
662
+ return {
663
+ kind: "rule",
664
+ left,
665
+ operator,
666
+ right
667
+ };
668
+ }
669
+ function parseNotIn() {
670
+ const left = parsePath();
671
+ if (left === null || !consumeIdent("in")) return null;
672
+ const right = parseArray();
673
+ if (right === null || !consumePunct(")")) return null;
674
+ return {
675
+ kind: "rule",
676
+ left,
677
+ operator: "not_in",
678
+ right
679
+ };
680
+ }
681
+ function tryEmptiness() {
682
+ const first = peek();
683
+ if (first === void 0) return null;
684
+ let operator;
685
+ let pathIndex;
686
+ if (first.kind === "ident" && first.value === "not" && isPunct(peek(1), "(")) {
687
+ operator = "is_not_empty";
688
+ pathIndex = pos + 2;
689
+ } else if (first.kind === "punct" && first.value === "(") {
690
+ operator = "is_empty";
691
+ pathIndex = pos + 1;
692
+ } else return null;
693
+ const read = readPath(tokens, pathIndex);
694
+ if (read === null) return null;
695
+ const canonical = compileCondition({
696
+ kind: "field",
697
+ subject: read.path,
698
+ operator,
699
+ value: void 0
700
+ });
701
+ if (canonical === null) return null;
702
+ const expected = tokenize(canonical);
703
+ if (expected === null || !tokensMatchAt(pos, expected)) return null;
704
+ pos += expected.length;
705
+ return {
706
+ kind: "rule",
707
+ left: read.path,
708
+ operator
709
+ };
710
+ }
711
+ function parseRule() {
712
+ const token = peek();
713
+ if (token === void 0 || token.kind !== "ident") return null;
714
+ if (token.value === "not") {
715
+ const next = peek(1);
716
+ if (next === void 0) return null;
717
+ if (next.kind === "ident" && next.value === "contains" && isPunct(peek(2), "(")) {
718
+ pos += 2;
719
+ return parseCall("not_contains");
720
+ }
721
+ if (next.kind === "punct" && next.value === "(") {
722
+ pos += 2;
723
+ return parseNotIn();
724
+ }
725
+ return null;
726
+ }
727
+ if (isPunct(peek(1), "(")) {
728
+ const operator = CALL_OPERATORS[token.value];
729
+ if (operator === void 0) return null;
730
+ pos += 1;
731
+ return parseCall(operator);
732
+ }
733
+ const left = parsePath();
734
+ if (left === null) return null;
735
+ const operatorToken = peek();
736
+ if (operatorToken === void 0) return null;
737
+ if (operatorToken.kind === "ident" && operatorToken.value === "in") {
738
+ pos += 1;
739
+ const right = parseArray();
740
+ return right === null ? null : {
741
+ kind: "rule",
742
+ left,
743
+ operator: "in",
744
+ right
745
+ };
746
+ }
747
+ if (operatorToken.kind === "punct") {
748
+ const operator = COMPARISON_OPERATORS[operatorToken.value];
749
+ if (operator === void 0) return null;
750
+ pos += 1;
751
+ const right = parseLiteral();
752
+ return right === null ? null : {
753
+ kind: "rule",
754
+ left,
755
+ operator,
756
+ right
757
+ };
758
+ }
759
+ return null;
760
+ }
761
+ function parseItem(depth) {
762
+ const emptiness = tryEmptiness();
763
+ if (emptiness !== null) return emptiness;
764
+ if (isPunct(peek(), "(")) {
765
+ if (depth >= MAX_GROUP_DEPTH) return null;
766
+ pos += 1;
767
+ const inner = parseGroup(depth + 1);
768
+ if (inner === null || !consumePunct(")")) return null;
769
+ return asGroup(inner);
770
+ }
771
+ return parseRule();
772
+ }
773
+ function peekJoin() {
774
+ const token = peek();
775
+ if (token !== void 0 && token.kind === "ident" && (token.value === "and" || token.value === "or")) return token.value;
776
+ return null;
777
+ }
778
+ function parseGroup(depth) {
779
+ const first = parseItem(depth);
780
+ if (first === null) return null;
781
+ const items = [first];
782
+ let op = null;
783
+ let join = peekJoin();
784
+ while (join !== null) {
785
+ if (op === null) op = join;
786
+ else if (op !== join) return null;
787
+ pos += 1;
788
+ const next = parseItem(depth);
789
+ if (next === null) return null;
790
+ items.push(next);
791
+ join = peekJoin();
792
+ }
793
+ return items.length === 1 ? items[0] : {
794
+ kind: "group",
795
+ op: op ?? "and",
796
+ items
797
+ };
798
+ }
799
+ const parsed = parseGroup(0);
800
+ if (parsed === null || pos !== tokens.length) return null;
801
+ return asGroup(parsed);
802
+ }
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
+ function readPath(tokens, from) {
810
+ const head = tokens[from];
811
+ if (head === void 0 || head.kind !== "ident") return null;
812
+ let path = head.value;
813
+ let index = from + 1;
814
+ let segment = tokens[index];
815
+ while (segment !== void 0 && segment.kind === "punct" && (segment.value === "." || segment.value === "[")) {
816
+ if (segment.value === ".") {
817
+ const name = tokens[index + 1];
818
+ if (name === void 0 || name.kind !== "ident") return null;
819
+ path += `.${name.value}`;
820
+ index += 2;
821
+ } else {
822
+ const inner = tokens[index + 1];
823
+ if (inner === void 0 || inner.kind !== "number" || !INTEGER_PATTERN.test(inner.value)) return null;
824
+ const close = tokens[index + 2];
825
+ if (close === void 0 || close.kind !== "punct" || close.value !== "]") return null;
826
+ path += `[${inner.value}]`;
827
+ index += 3;
828
+ }
829
+ segment = tokens[index];
830
+ }
831
+ return isIdentifierPath(path) ? {
832
+ path,
833
+ next: index
834
+ } : null;
835
+ }
836
+ function asGroup(node) {
837
+ return node.kind === "group" ? node : {
838
+ kind: "group",
839
+ op: "and",
840
+ items: [node]
841
+ };
842
+ }
843
+ function isPunct(token, value) {
844
+ return token !== void 0 && token.kind === "punct" && token.value === value;
845
+ }
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
+ function tokenize(input) {
854
+ const tokens = [];
855
+ let index = 0;
856
+ while (index < input.length) {
857
+ const char = input[index];
858
+ if (char === " " || char === " " || char === "\n" || char === "\r") {
859
+ index += 1;
860
+ continue;
861
+ }
862
+ if (char === "'" || char === "\"") {
863
+ const end = input.indexOf(char, index + 1);
864
+ if (end === -1) return null;
865
+ tokens.push({
866
+ kind: "string",
867
+ value: input.slice(index + 1, end)
868
+ });
869
+ index = end + 1;
870
+ continue;
871
+ }
872
+ const pair = input.slice(index, index + 2);
873
+ if (TWO_CHAR_PUNCTUATION.has(pair)) {
874
+ tokens.push({
875
+ kind: "punct",
876
+ value: pair
877
+ });
878
+ index += 2;
879
+ continue;
880
+ }
881
+ if (ONE_CHAR_PUNCTUATION.has(char)) {
882
+ tokens.push({
883
+ kind: "punct",
884
+ value: char
885
+ });
886
+ index += 1;
887
+ continue;
888
+ }
889
+ if (DIGIT.test(char)) {
890
+ const match = NUMBER_PATTERN.exec(input.slice(index));
891
+ if (match === null) return null;
892
+ tokens.push({
893
+ kind: "number",
894
+ value: match[0]
895
+ });
896
+ index += match[0].length;
897
+ continue;
898
+ }
899
+ if (IDENT_START.test(char)) {
900
+ let end = index + 1;
901
+ while (end < input.length && IDENT_PART.test(input[end])) end += 1;
902
+ tokens.push({
903
+ kind: "ident",
904
+ value: input.slice(index, end)
905
+ });
906
+ index = end;
907
+ continue;
908
+ }
909
+ return null;
910
+ }
911
+ return tokens;
912
+ }
913
+ //#endregion
914
+ //#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
+ const CONDITION_TREE_OPERATORS = CONDITION_OPERATORS;
422
933
  //#endregion
423
934
  //#region src/engine/evaluate.ts
424
935
  /**
@@ -725,6 +1236,108 @@ function satisfiesTypeSync(actual, expected) {
725
1236
  return getEngineSync().satisfies(actual, expected);
726
1237
  }
727
1238
  //#endregion
728
- export { CONDITION_OPERATORS, ExpressionError, ExpressionNotReadyError, analyzeTypes, analyzeTypesSync, compileBranch, compileCondition, compileGroup, configureEngine, configureExpressionMessages, enMessages, evaluate, evaluateSync, evaluateUnary, evaluateUnarySync, getCompletionItems, getCompletionItemsSync, getDiagnostics, getDiagnosticsSync, getEngineError, getEngineSync, getExpressionMessages, isEngineReady, loadEngine, registerExpressionLocale, resetEngine, satisfiesType, satisfiesTypeSync, selectBranch, selectBranchWith, toZenLiteral, zhCNMessages };
1239
+ //#region src/engine/template.ts
1240
+ 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
+ function parseTemplateHoles(source) {
1248
+ const holes = [];
1249
+ HOLE_PATTERN.lastIndex = 0;
1250
+ for (let match = HOLE_PATTERN.exec(source); match !== null; match = HOLE_PATTERN.exec(source)) {
1251
+ const inner = match.groups?.expression ?? "";
1252
+ const from = match.index + 2;
1253
+ holes.push({
1254
+ from,
1255
+ to: from + inner.length,
1256
+ expression: inner
1257
+ });
1258
+ }
1259
+ return holes;
1260
+ }
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
+ function templateHoleAt(source, pos) {
1269
+ return parseTemplateHoles(source).find((hole) => pos >= hole.from && pos <= hole.to) ?? null;
1270
+ }
1271
+ function hasExpression(hole) {
1272
+ return hole.expression.trim().length > 0;
1273
+ }
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
+ function analyzeTemplateSync(variables, source) {
1287
+ if (!isEngineReady()) throw new ExpressionNotReadyError();
1288
+ const spans = [];
1289
+ for (const hole of parseTemplateHoles(source)) {
1290
+ if (!hasExpression(hole)) continue;
1291
+ try {
1292
+ for (const span of analyzeTypesSync(variables, hole.expression, "standard").spans) spans.push({
1293
+ ...span,
1294
+ span: [span.span[0] + hole.from, span.span[1] + hole.from]
1295
+ });
1296
+ } catch {}
1297
+ }
1298
+ return {
1299
+ rootKind: variables,
1300
+ spans
1301
+ };
1302
+ }
1303
+ /**
1304
+ * Async {@link analyzeTemplateSync}, loading the engine on first use.
1305
+ */
1306
+ async function analyzeTemplate(variables, source) {
1307
+ await loadEngine();
1308
+ return analyzeTemplateSync(variables, source);
1309
+ }
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
+ function getTemplateDiagnosticsSync(source) {
1320
+ if (!isEngineReady()) throw new ExpressionNotReadyError();
1321
+ const diagnostics = [];
1322
+ for (const hole of parseTemplateHoles(source)) {
1323
+ if (!hasExpression(hole)) continue;
1324
+ const diagnostic = getDiagnosticsSync(hole.expression, "standard");
1325
+ if (diagnostic) diagnostics.push({
1326
+ ...diagnostic,
1327
+ from: diagnostic.from + hole.from,
1328
+ to: diagnostic.to + hole.from
1329
+ });
1330
+ }
1331
+ return diagnostics;
1332
+ }
1333
+ /**
1334
+ * Async {@link getTemplateDiagnosticsSync}, loading the engine on first use.
1335
+ */
1336
+ async function getTemplateDiagnostics(source) {
1337
+ await loadEngine();
1338
+ return getTemplateDiagnosticsSync(source);
1339
+ }
1340
+ //#endregion
1341
+ 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 };
729
1342
 
730
1343
  //# sourceMappingURL=index.js.map