@stll/template-conditions 0.3.0 → 0.5.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.js CHANGED
@@ -192,11 +192,12 @@ const COMPARE_SYMBOL_TO_OP = {
192
192
  };
193
193
  const escapeRegexLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
194
194
  const COMPARE_SYMBOL_PATTERN = Object.keys(COMPARE_SYMBOL_TO_OP).sort((left, right) => right.length - left.length).map(escapeRegexLiteral).join("|");
195
- const NON_STRING_TOKEN_RE = new RegExp(String.raw`(?<token>${COMPARE_SYMBOL_PATTERN}|!(?!=)|and\b|or\b|contains\b|[()]|-?\d[\p{N}_.]*|[\p{L}\p{N}_.]+(?:-[\p{L}\p{N}_.]+)*)`, "uy");
195
+ const NON_STRING_TOKEN_RE = new RegExp(String.raw`(?<token>${COMPARE_SYMBOL_PATTERN}|and\b|or\b|not\b|in\b|is\b|defined\b|[()]|-?\d[\p{N}_.]*|[\p{L}\p{N}_.]+(?:-[\p{L}\p{N}_.]+)*)`, "uy");
196
196
  const STARTS_WITH_DIGIT_RE = /^-?\d/u;
197
197
  const isCompareSymbol = (raw) => Object.hasOwn(COMPARE_SYMBOL_TO_OP, raw);
198
198
  /**
199
- * Scan a `"..."` literal starting at `expr[start]` (the opening quote),
199
+ * Scan a `"..."` or `'...'` literal starting at `expr[start]` (the opening
200
+ * quote),
200
201
  * honoring `\\`-escapes without interpreting them. A single linear pass:
201
202
  * each character is visited at most once, so this is safe on adversarial
202
203
  * input regardless of how many `"` or `\\` characters it contains.
@@ -206,9 +207,10 @@ const isCompareSymbol = (raw) => Object.hasOwn(COMPARE_SYMBOL_TO_OP, raw);
206
207
  * closing quote simply closes at end of input.
207
208
  */
208
209
  const scanString = (expr, start) => {
210
+ const quote = expr[start];
209
211
  let i = start + 1;
210
212
  while (i < expr.length) {
211
- if (expr[i] === "\"") return {
213
+ if (expr[i] === quote) return {
212
214
  content: expr.slice(start + 1, i),
213
215
  end: i + 1
214
216
  };
@@ -222,8 +224,11 @@ const scanString = (expr, start) => {
222
224
  const classifyNonString = (raw) => {
223
225
  if (raw === "and") return { type: "and" };
224
226
  if (raw === "or") return { type: "or" };
225
- if (raw === "!") return { type: "not" };
226
- if (raw === "contains" || isCompareSymbol(raw)) return {
227
+ if (raw === "not") return { type: "not" };
228
+ if (raw === "in") return { type: "in" };
229
+ if (raw === "is") return { type: "is" };
230
+ if (raw === "defined") return { type: "defined" };
231
+ if (isCompareSymbol(raw)) return {
227
232
  type: "op",
228
233
  raw
229
234
  };
@@ -238,11 +243,11 @@ const tokenize = (expr) => {
238
243
  const tokens = [];
239
244
  let pos = 0;
240
245
  while (pos < expr.length) {
241
- if (expr[pos] === "\"") {
246
+ if (expr[pos] === "\"" || expr[pos] === "'") {
242
247
  const { content, end } = scanString(expr, pos);
243
248
  tokens.push({
244
249
  type: "string",
245
- raw: content.replace(/\\"/gu, "\"")
250
+ raw: content.replace(/\\(?<quote>["'])/gu, "$<quote>")
246
251
  });
247
252
  pos = end;
248
253
  continue;
@@ -289,9 +294,9 @@ const operandFromToken = (token) => {
289
294
  path: token.raw
290
295
  };
291
296
  };
292
- /** `contains` carries a literal payload (`string`), not an operand: render the
293
- * right-hand token as a string. */
294
- const containsValue = (token) => {
297
+ /** `"x" in path` carries a literal payload (`string`), not an operand: render
298
+ * the left-hand token as a string. */
299
+ const membershipValue = (token) => {
295
300
  const operand = operandFromToken(token);
296
301
  return operand.type === "literal" ? String(operand.value) : token.raw;
297
302
  };
@@ -358,6 +363,39 @@ const parseCondition = (expression) => {
358
363
  }
359
364
  pos += 1;
360
365
  const left = operandFromToken(leftTok);
366
+ if (peek()?.type === "is") {
367
+ pos += 1;
368
+ const negated = peek()?.type === "not";
369
+ if (negated) pos += 1;
370
+ if (peek()?.type === "defined") pos += 1;
371
+ return {
372
+ type: "predicate",
373
+ operand: left,
374
+ op: negated ? "is_empty" : "is_not_empty"
375
+ };
376
+ }
377
+ if (peek()?.type === "in") {
378
+ pos += 1;
379
+ const rightTok = peek();
380
+ if (!rightTok || rightTok.type !== "value" && rightTok.type !== "string") return {
381
+ type: "predicate",
382
+ operand: left,
383
+ op: "is_truthy"
384
+ };
385
+ pos += 1;
386
+ const right = operandFromToken(rightTok);
387
+ if (right.type !== "path") return {
388
+ type: "predicate",
389
+ operand: right,
390
+ op: "is_truthy"
391
+ };
392
+ return {
393
+ type: "predicate",
394
+ operand: right,
395
+ op: "contains",
396
+ value: membershipValue(leftTok)
397
+ };
398
+ }
361
399
  const opTok = peek();
362
400
  if (opTok?.type !== "op") return {
363
401
  type: "predicate",
@@ -372,12 +410,6 @@ const parseCondition = (expression) => {
372
410
  op: "is_truthy"
373
411
  };
374
412
  pos += 1;
375
- if (opTok.raw === "contains") return {
376
- type: "predicate",
377
- operand: left,
378
- op: "contains",
379
- value: containsValue(rightTok)
380
- };
381
413
  return {
382
414
  type: "compare",
383
415
  left,
@@ -387,196 +419,46 @@ const parseCondition = (expression) => {
387
419
  };
388
420
  return parseOr();
389
421
  };
390
- const DIRECTIVE_KINDS = [
391
- "placeholder",
392
- "clause",
393
- "num",
394
- "ref",
395
- "index",
396
- "count",
397
- "if",
398
- "elseif",
399
- "else",
400
- "endif",
401
- "each",
402
- "endeach"
403
- ];
404
- /** Directives that occupy their own paragraph, in canonical grammar order. */
405
- const BLOCK_DIRECTIVE_KINDS = [
406
- "if",
407
- "elseif",
408
- "else",
409
- "endif",
410
- "each",
411
- "endeach"
412
- ];
413
- const isBlockDirectiveKind = (kind) => typeof kind === "string" && BLOCK_DIRECTIVE_KINDS.some((blockKind) => blockKind === kind);
414
- /** Any `{{...}}` marker; group 1 is the inner text (untrimmed — callers trim). */
415
- const markerPattern = () => /\{\{(?<inner>[^{}]*)\}\}/gu;
416
- /** Inline field / clause / numbering marker (inner allows the directive sigils). */
417
- const placeholderPattern = () => /\{\{\s*(?<name>[\p{L}\p{N}_.@:-]+)\s*\}\}/gu;
418
- /** `{{@clause:Name}}` / `{{@clause:Name:v3}}` — `name` and `modifier` groups. */
419
- const clauseSlotPattern = () => /\{\{\s*@clause:(?<name>[^:}\s]+)(?::(?<modifier>[^}\s]+))?\s*\}\}/gu;
420
- /** `{{@num:Key}}` — the `key` group. */
421
- const numPattern = () => /\{\{\s*@num:(?<key>[\p{L}\p{N}_.-]+)\s*\}\}/gu;
422
- /** `{{@ref:Key}}` — the `key` group. */
423
- const refPattern = () => /\{\{\s*@ref:(?<key>[\p{L}\p{N}_.-]+)\s*\}\}/gu;
424
- /** `{{@index}}` — the 1-based position within the innermost enclosing loop. */
425
- const indexPattern = () => /\{\{\s*@index\s*\}\}/gu;
426
- /** `{{@count}}` — the item count of the innermost enclosing loop. */
427
- const countPattern = () => /\{\{\s*@count\s*\}\}/gu;
428
- /** Cheap presence test for any numbering marker (no capture). */
429
- const hasNumberingPattern = () => /\{\{\s*@(?:num|ref):/u;
430
- /** Cheap presence test for any block directive (no capture). */
431
- const hasBlockDirectivePattern = () => /\{\{\s*[#/]/u;
432
- /** A whole line that is a single block directive — `tag` and `expr` groups. */
433
- const blockDirectiveLinePattern = () => /^\s*\{\{\s*(?<tag>#if|#elseif|#else|#each|\/if|\/each)\b(?<expr>[^{}]*)\}\}\s*$/u;
434
- const FIELD_PATH_RE = /^[\p{L}\p{N}_.-]+$/u;
435
- const UNSAFE_FIELD_PATH_SEGMENTS = /* @__PURE__ */ new Set([
436
- "__proto__",
437
- "constructor",
438
- "prototype"
439
- ]);
440
- /** Whether `value` is a valid field path per the marker grammar (dotted
441
- * segments of letters/digits/underscore/dash — no brackets or spaces). */
442
- const isFieldPath = (value) => FIELD_PATH_RE.test(value);
443
- /** Whether `value` is safe to use as a dotted object path. */
444
- const isSafeFieldPath = (value) => isFieldPath(value) && value.split(".").every((segment) => !UNSAFE_FIELD_PATH_SEGMENTS.has(segment));
445
- const CLAUSE_SLOT_NAME_RE = /^[\p{L}\p{N}_.-]+$/u;
446
- /** Whether `value` is a valid name for authoring a `{{@clause:NAME}}` slot:
447
- * the intersection of the clause-slot parse grammar and the fill pipeline's
448
- * placeholder charset, so every accepted name round-trips through fill. */
449
- const isClauseSlotName = (value) => CLAUSE_SLOT_NAME_RE.test(value);
450
- const CLAUSE_INNER_RE = /^@clause:(?<name>[^:}\s]+)(?::(?<version>[^}\s]+))?$/u;
451
- const NUM_INNER_RE = /^@num:(?<key>[\p{L}\p{N}_.-]+)$/u;
452
- const REF_INNER_RE = /^@ref:(?<key>[\p{L}\p{N}_.-]+)$/u;
453
- const INDEX_INNER_RE = /^@index$/u;
454
- const COUNT_INNER_RE = /^@count$/u;
455
- const BLOCK_INNER_RE = /^(?<token>#if|#elseif|#else|#each|\/if|\/each)\b(?<expr>.*)$/u;
456
- /**
457
- * Classify the inner text of a `{{...}}` marker. Returns `null` when the text is
458
- * not a recognized directive (so a recognizer can leave stray braces alone).
459
- */
460
- const classifyMarker = (innerRaw) => {
461
- const inner = innerRaw.trim();
462
- const block = BLOCK_INNER_RE.exec(inner);
463
- if (block) {
464
- const token = block.groups?.["token"] ?? "";
465
- const expr = (block.groups?.["expr"] ?? "").trim();
466
- if (token === "#if") return {
467
- kind: "if",
468
- expr
469
- };
470
- if (token === "#elseif") return {
471
- kind: "elseif",
472
- expr
473
- };
474
- if (token === "#each") return {
475
- kind: "each",
476
- expr
477
- };
478
- if (token === "#else") return { kind: "else" };
479
- if (token === "/if") return { kind: "endif" };
480
- if (token === "/each") return { kind: "endeach" };
481
- }
482
- const clause = CLAUSE_INNER_RE.exec(inner);
483
- if (clause) return {
484
- kind: "clause",
485
- name: clause.groups?.["name"] ?? "",
486
- version: clause.groups?.["version"]
487
- };
488
- const num = NUM_INNER_RE.exec(inner);
489
- if (num) return {
490
- kind: "num",
491
- key: num.groups?.["key"] ?? ""
492
- };
493
- const ref = REF_INNER_RE.exec(inner);
494
- if (ref) return {
495
- kind: "ref",
496
- key: ref.groups?.["key"] ?? ""
497
- };
498
- if (INDEX_INNER_RE.test(inner)) return { kind: "index" };
499
- if (COUNT_INNER_RE.test(inner)) return { kind: "count" };
500
- if (FIELD_PATH_RE.test(inner)) return {
501
- kind: "placeholder",
502
- expr: inner
503
- };
504
- return null;
505
- };
506
- /**
507
- * Find every recognized `{{...}}` marker in `text`, in document order, with
508
- * offsets and classified metadata. Unrecognized `{{...}}` spans are skipped.
509
- */
510
- const scanMarkers = (text) => {
511
- const re = markerPattern();
512
- const out = [];
513
- let match = re.exec(text);
514
- while (match !== null) {
515
- const inner = (match[1] ?? "").trim();
516
- const meta = classifyMarker(inner);
517
- if (meta) out.push({
518
- start: match.index,
519
- end: match.index + match[0].length,
520
- raw: match[0],
521
- inner,
522
- meta
523
- });
524
- match = re.exec(text);
422
+ //#endregion
423
+ //#region src/referenced-paths.ts
424
+ const operandsOf = (node) => node.type === "compare" ? [node.left, node.right] : [node.operand];
425
+ /** Every field path the tree reads, or `null` when an operand names something
426
+ * whose own references cannot be enumerated (a formula over other fields). */
427
+ const nodePaths = (node) => {
428
+ if (node.type === "group") {
429
+ const paths = [];
430
+ for (const child of node.children) {
431
+ const childPaths = nodePaths(child);
432
+ if (childPaths === null) return null;
433
+ paths.push(...childPaths);
434
+ }
435
+ return paths;
525
436
  }
526
- return out;
527
- };
528
- /**
529
- * Find every `{{...}}` span whose inner text is NOT a recognized directive —
530
- * near-misses an author clearly meant as markers but that every recognizer
531
- * skips, so they print literally at fill time (e.g. `{{my field}}` with a
532
- * space, `{{@clause:}}` with no name). Offsets and raw text are shaped like
533
- * {@link scanMarkers} results, minus the classified `meta`. This is the exact
534
- * complement of {@link scanMarkers}: a span is in one list or the other.
535
- */
536
- const scanInvalidMarkers = (text) => {
537
- const re = markerPattern();
538
- const out = [];
539
- let match = re.exec(text);
540
- while (match !== null) {
541
- const inner = (match[1] ?? "").trim();
542
- if (classifyMarker(inner) === null) out.push({
543
- start: match.index,
544
- end: match.index + match[0].length,
545
- raw: match[0],
546
- inner
547
- });
548
- match = re.exec(text);
437
+ const paths = [];
438
+ for (const operand of operandsOf(node)) {
439
+ if (operand.type === "formula") return null;
440
+ if (operand.type === "path") paths.push(operand.path);
549
441
  }
550
- return out;
442
+ return paths;
551
443
  };
552
- const MARKER_DEFECT_KINDS = ["unknown_directive", "bracket_index"];
553
- /** A `{{#...}}` / `{{/...}}` shape: an author reaching for a block directive. */
554
- const DIRECTIVE_SHAPE_RE = /^[#/]/u;
555
- /** Bracket indexing (`items[0].name`), which no directive kind admits. */
556
- const BRACKET_INDEX_RE = /[[\]]/u;
557
444
  /**
558
- * Name the authoring mistake behind one marker's inner text, or `null` when the
559
- * text is a recognized directive ({@link classifyMarker} accepts it) or an
560
- * unrecognizable span with no specific diagnosis (`{{my field}}`). The
561
- * recognized-directive check is what makes `DIRECTIVE_KINDS` the single
562
- * authority on which `{{#...}}` tokens exist: adding a directive to the grammar
563
- * stops it being reported here, with no list to update.
564
- */
565
- const classifyMarkerDefect = (innerRaw) => {
566
- const inner = innerRaw.trim();
567
- if (classifyMarker(inner) !== null) return null;
568
- if (DIRECTIVE_SHAPE_RE.test(inner)) return "unknown_directive";
569
- if (BRACKET_INDEX_RE.test(inner)) return "bracket_index";
570
- return null;
445
+ * Every field path an expression reads, in source order.
446
+ *
447
+ * `null` says the expression does not answer the question: it did not parse,
448
+ * or it reads an operand whose own references cannot be enumerated. A caller
449
+ * deciding what a condition depends on must not read that as "depends on
450
+ * nothing".
451
+ */
452
+ const referencedConditionPaths = (expression) => {
453
+ const node = parseCondition(expression);
454
+ return node === null ? null : nodePaths(node);
571
455
  };
572
- /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
573
- const assertNever = (value) => panic(`Unhandled template directive: ${JSON.stringify(value)}`);
574
456
  //#endregion
575
457
  //#region src/field-values.ts
576
458
  /**
577
459
  * Single source of truth for the DETERMINISTIC field-value transforms a
578
- * template fill applies — composite (parts joined by a `{{key}}` format),
579
- * formula (arithmetic over the other values), and locale-aware date rendering.
460
+ * template fill applies: formula (arithmetic over the other values) and
461
+ * locale-aware date rendering.
580
462
  *
581
463
  * Both the API fill engine (apps/api/src/handlers/docx) and the web live
582
464
  * preview (template-studio.tsx) MUST route their rendering through
@@ -646,52 +528,20 @@ const formatDate = (value, dateFormat) => {
646
528
  return getDateFormatter(dateFormat.locale, dateFormat.style).format(date);
647
529
  };
648
530
  /**
649
- * Render a `{{key}}` format over part values. Markers whose key has no part
650
- * value are left as-is (a visible authoring artifact, not a render error), so
651
- * a partially filled composite previews exactly as the fill engine renders it.
652
- */
653
- const renderComposite = (parts, format, partValues) => format.replace(markerPattern(), (raw, inner) => {
654
- const key = inner.trim();
655
- if (!parts.some((part) => part.key === key)) return raw;
656
- return partValues[key] ?? raw;
657
- });
658
- /** A plain object of part values: a non-null, non-array object. The api
659
- * composite step accepts exactly this shape (an array or primitive is not a
660
- * part-values object), so the dispatcher mirrors it. */
661
- const isPartValuesObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
662
- /** Build the `{{key}} → value` map for a composite field from its raw object
663
- * value, keeping only string part values for declared part keys. */
664
- const compositePartValues = (parts, raw) => {
665
- const partValues = {};
666
- for (const part of parts) {
667
- const value = raw[part.key];
668
- if (typeof value === "string") partValues[part.key] = value;
669
- }
670
- return partValues;
671
- };
672
- /**
673
531
  * THE deterministic field-value dispatcher: returns the string the fill engine
674
532
  * writes for `field` given the submitted `values`, or null when the field has
675
533
  * no deterministic transform (a scalar, lookup, or AI field the CALLER renders
676
534
  * itself).
677
535
  *
678
- * Dispatch order mirrors the API fill pipeline (composite → formula → date):
679
- * - composite (parts + format present) → {@link renderComposite}
680
- * - else formula present → {@link evaluateNumericExpression}, stringified
536
+ * Dispatch order mirrors the API fill pipeline (formula → date):
537
+ * - formula present → {@link evaluateNumericExpression}, stringified
681
538
  * - else date (inputType "date" + dateFormat) → {@link formatDate}
682
539
  * - else null
683
540
  *
684
- * Composite returns null when its value is not an object of part values (a
685
- * plain string passes through unchanged on the api side, so the caller's
686
- * scalar path handles it). Formula and date return null when the expression /
687
- * value does not yield a value, so the caller leaves the field as-is.
541
+ * Formula and date return null when the expression or value does not yield a
542
+ * value, so the caller leaves the field as-is.
688
543
  */
689
544
  const renderDeterministicFieldValue = (field, values) => {
690
- if (field.parts !== void 0 && field.format !== void 0) {
691
- const raw = resolvePath(field.path, values);
692
- if (!isPartValuesObject(raw)) return null;
693
- return renderComposite(field.parts, field.format, compositePartValues(field.parts, raw));
694
- }
695
545
  if (field.formula !== void 0) {
696
546
  const result = evaluateNumericExpression(field.formula, values);
697
547
  return result === void 0 ? null : String(result);
@@ -732,8 +582,10 @@ const serializePredicate = (node) => {
732
582
  const operand = serializeOperand(node.operand);
733
583
  if (node.op === "contains") {
734
584
  const value = Array.isArray(node.value) ? node.value.join(",") : node.value ?? "";
735
- return `${operand} contains ${serializeLiteral(value)}`;
585
+ return `${serializeLiteral(value)} in ${operand}`;
736
586
  }
587
+ if (node.op === "is_not_empty") return `${operand} is defined`;
588
+ if (node.op === "is_empty") return `${operand} is not defined`;
737
589
  return operand;
738
590
  };
739
591
  const serializeGroup = (group, top) => {
@@ -744,7 +596,7 @@ const serializeGroup = (group, top) => {
744
596
  }
745
597
  if (parts.length === 0) return "";
746
598
  const body = parts.length === 1 ? parts[0] ?? "" : parts.join(group.combinator === "and" ? " and " : " or ");
747
- if (group.negated) return `!(${body})`;
599
+ if (group.negated) return `not (${body})`;
748
600
  if (parts.length === 1) return body;
749
601
  return top ? body : `(${body})`;
750
602
  };
@@ -760,6 +612,858 @@ const serializeNode = (node, top) => {
760
612
  */
761
613
  const serializeCondition = (node) => serializeNode(node, true);
762
614
  //#endregion
615
+ //#region src/markers.ts
616
+ /**
617
+ * Canonical grammar for the docxtpl dialect of Jinja — the single source of
618
+ * truth shared by the fill pipeline (api), the editor highlighter (folio), and
619
+ * the read-only preview (web).
620
+ *
621
+ * Two spans carry meaning: an output marker `{{ ... }}` prints a value, and a
622
+ * tag `{% ... %}` opens or closes a block. A tag may carry a docxtpl placement
623
+ * prefix — `{%p ... %}` owns its paragraph, `{%tr ... %}` owns its table row —
624
+ * and a bare tag is placed by the leniency rules the consumer applies.
625
+ *
626
+ * Add a new directive to `MarkerMeta` and teach {@link classifyMarker} to
627
+ * recognize it. Compile-time totality checks then require its ordered runtime
628
+ * value and its block/inline disposition, while exhaustive consumer switches
629
+ * fail until they handle it.
630
+ *
631
+ * No consumer should define its own marker regex. The low-level pattern
632
+ * factories below exist so handlers that mutate raw XML/text reuse the exact
633
+ * same literals; recognizers should prefer the high-level scanner.
634
+ */
635
+ /** The loop properties addressable inside a `{% for %}` body. */
636
+ const LOOP_PROPERTIES = [
637
+ "index",
638
+ "index0",
639
+ "first",
640
+ "last",
641
+ "length"
642
+ ];
643
+ const DIRECTIVE_KINDS = [
644
+ "placeholder",
645
+ "clause",
646
+ "num",
647
+ "ref",
648
+ "loop",
649
+ "if",
650
+ "elif",
651
+ "else",
652
+ "endif",
653
+ "for",
654
+ "endfor"
655
+ ];
656
+ /** Directives that occupy their own paragraph, in canonical grammar order. */
657
+ const BLOCK_DIRECTIVE_KINDS = [
658
+ "if",
659
+ "elif",
660
+ "else",
661
+ "endif",
662
+ "for",
663
+ "endfor"
664
+ ];
665
+ const isBlockDirectiveKind = (kind) => typeof kind === "string" && BLOCK_DIRECTIVE_KINDS.some((blockKind) => blockKind === kind);
666
+ /** Callable forms inside `{{ }}` that address document machinery rather than a
667
+ * fill value. */
668
+ const FUNCTION_NAMES = [
669
+ "clause",
670
+ "num",
671
+ "ref"
672
+ ];
673
+ /**
674
+ * Every filter the dialect accepts on a fillable value. The names are the
675
+ * grammar's; which manifest property each one writes is decided by the field
676
+ * catalogue in the api, which is total over BOTH this list and the manifest
677
+ * keys, so neither side can gain an entry the other has not decided.
678
+ */
679
+ const FILTER_NAMES = [
680
+ "text",
681
+ "number",
682
+ "date",
683
+ "checkbox",
684
+ "select",
685
+ "options_from",
686
+ "label",
687
+ "hint",
688
+ "required",
689
+ "pattern",
690
+ "min",
691
+ "max",
692
+ "min_length",
693
+ "max_length",
694
+ "min_items",
695
+ "max_items",
696
+ "ai",
697
+ "lookup",
698
+ "matter",
699
+ "contact",
700
+ "party",
701
+ "attorney",
702
+ "firm",
703
+ "formula",
704
+ "condition"
705
+ ];
706
+ const isFilterName = (value) => FILTER_NAMES.some((name) => name === value);
707
+ const isFunctionName = (value) => FUNCTION_NAMES.some((name) => name === value);
708
+ const SMART_DOUBLE_QUOTES = /[“”„«»]/gu;
709
+ const SMART_SINGLE_QUOTES = /[‘’‚]/gu;
710
+ const FIXED_SPACES = /[    ]/gu;
711
+ /** Normalise one marker's inner text: typographic quotes become straight
712
+ * quotes, fixed-width spaces become ordinary ones. */
713
+ const normalizeMarkerInner = (inner) => inner.replace(SMART_DOUBLE_QUOTES, "\"").replace(SMART_SINGLE_QUOTES, "'").replace(FIXED_SPACES, " ");
714
+ /**
715
+ * What an output marker holds between its braces: anything but a brace or a
716
+ * double quote, plus whole double-quoted arguments, whose braces are content
717
+ * rather than the marker's own delimiters — `pattern("^[0-9]{5}$")` is a regex,
718
+ * not a nested marker. An apostrophe stays an ordinary character, so
719
+ * `label("Owner's")` reads as it always did.
720
+ *
721
+ * A quoted run honours `\`-escapes, the way the argument scanner reads them,
722
+ * so `label("she said \"yes\"")` is one span rather than three: the span and
723
+ * the scanner have to agree on where a literal ends or a marker the scanner
724
+ * would parse is one the recognizer never sees.
725
+ *
726
+ * The alternatives cannot overlap (the plain one excludes every character the
727
+ * quoted one may start with, and inside a quoted run the escape branch is the
728
+ * only one that starts with a backslash), so the scan stays linear on
729
+ * adversarial input: a `{{` with no closing `}}` costs one failed pass, not a
730
+ * polynomial retry.
731
+ */
732
+ const MARKER_OUTPUT_BODY = String.raw`(?:[^{}"“”„«»]|["“„«](?:\\[\s\S]|[^"”»\\])*["”»])*`;
733
+ /** What a tag holds between `{%` and `%}`: anything but a brace, and a `%`
734
+ * only where it does not close the tag. */
735
+ const MARKER_STATEMENT_BODY = String.raw`(?:[^{}%]|%(?!\}))*`;
736
+ /**
737
+ * Any marker span: an output `{{...}}` (group `output`) or a tag `{%...%}`
738
+ * (group `statement`, with the docxtpl placement in group `prefix`).
739
+ */
740
+ const markerPattern = () => new RegExp(String.raw`\{\{(?<output>${MARKER_OUTPUT_BODY})\}\}|\{%(?:(?<prefix>tr|p)(?=\s))?(?<statement>${MARKER_STATEMENT_BODY})%\}`, "gu");
741
+ /** Inline value marker, including its filter chain; group `name` is the path
742
+ * the value comes from and group `filters` the untouched chain. */
743
+ const placeholderPattern = () => new RegExp(String.raw`\{\{\s*(?<name>[\p{L}\p{N}_.-]+)\s*(?<filters>\|${MARKER_OUTPUT_BODY})?\}\}`, "gu");
744
+ /** Any character that opens a quoted function argument. */
745
+ const OPEN_QUOTE = String.raw`["'“„«‘‚]`;
746
+ /** Any character that closes one. */
747
+ const CLOSE_QUOTE = String.raw`["'”»’]`;
748
+ /** Everything a quoted argument may hold: no quote of either shape. */
749
+ const QUOTED_BODY = String.raw`[^"'“”„«»‘’‚]*`;
750
+ /** `{{ clause("Name") }}` / `{{ clause("Name", "v3") }}`. */
751
+ const clauseSlotPattern = () => new RegExp(String.raw`\{\{\s*clause\(\s*${OPEN_QUOTE}(?<name>${QUOTED_BODY})${CLOSE_QUOTE}\s*(?:,\s*${OPEN_QUOTE}(?<modifier>${QUOTED_BODY})${CLOSE_QUOTE}\s*)?\)\s*\}\}`, "gu");
752
+ const numberingPattern = (fn) => new RegExp(String.raw`\{\{\s*${fn}\(\s*${OPEN_QUOTE}(?<key>[\p{L}\p{N}_.-]+)${CLOSE_QUOTE}\s*\)\s*\}\}`, "gu");
753
+ /** `{{ num("key") }}` — the `key` group. */
754
+ const numPattern = () => numberingPattern("num");
755
+ /** `{{ ref("key") }}` — the `key` group. */
756
+ const refPattern = () => numberingPattern("ref");
757
+ /** `{{ loop.index }}` and its siblings — the `property` group. */
758
+ const loopPattern = () => /\{\{\s*loop\.(?<property>index0|index|first|last|length)\s*\}\}/gu;
759
+ /** Cheap presence test for any numbering marker (no capture). */
760
+ const hasNumberingPattern = () => /\{\{\s*(?:num|ref)\s*\(/u;
761
+ /** Cheap presence test for any tag (no capture). */
762
+ const hasBlockDirectivePattern = () => /\{%/u;
763
+ /** A whole line that is a single block tag — `prefix`, `tag` and `expr`. */
764
+ const blockDirectiveLinePattern = () => /^\s*\{%(?:(?<prefix>tr|p)(?=\s))?\s*(?<tag>if|elif|else|endif|for|endfor)\b(?<expr>(?:[^{}%]|%(?!\}))*)%\}\s*$/u;
765
+ const FIELD_PATH_RE = /^[\p{L}\p{N}_.-]+$/u;
766
+ const IDENTIFIER_RE = /^[\p{L}_][\p{L}\p{N}_-]*$/u;
767
+ const UNSAFE_FIELD_PATH_SEGMENTS = /* @__PURE__ */ new Set([
768
+ "__proto__",
769
+ "constructor",
770
+ "prototype"
771
+ ]);
772
+ /** Whether `value` is a valid field path per the marker grammar (dotted
773
+ * segments of letters/digits/underscore/dash — no brackets or spaces). */
774
+ const isFieldPath = (value) => FIELD_PATH_RE.test(value);
775
+ /** Whether `value` is safe to use as a dotted object path. */
776
+ const isSafeFieldPath = (value) => isFieldPath(value) && value.split(".").every((segment) => !UNSAFE_FIELD_PATH_SEGMENTS.has(segment));
777
+ const CLAUSE_SLOT_NAME_RE = /^[\p{L}\p{N}_.-]+$/u;
778
+ /** Whether `value` is a valid name for authoring a `clause("NAME")` slot. */
779
+ const isClauseSlotName = (value) => CLAUSE_SLOT_NAME_RE.test(value);
780
+ const isLoopProperty = (value) => LOOP_PROPERTIES.some((property) => property === value);
781
+ const NUMBER_LITERAL_RE = /^-?\d+(?:\.\d+)?$/u;
782
+ const literalFromBareToken = (raw) => {
783
+ if (raw === "true") return true;
784
+ if (raw === "false") return false;
785
+ if (NUMBER_LITERAL_RE.test(raw)) return Number(raw);
786
+ return null;
787
+ };
788
+ /**
789
+ * Scan a `"..."` / `'...'` literal starting at the opening quote, honouring
790
+ * `\`-escapes. One linear pass; an unterminated literal closes at end of
791
+ * input, matching the degrade-gracefully policy of the condition parser.
792
+ */
793
+ const scanQuoted = (text, start) => {
794
+ const quote = text[start];
795
+ let i = start + 1;
796
+ let value = "";
797
+ while (i < text.length) {
798
+ const ch = text[i];
799
+ if (ch === quote) return {
800
+ value,
801
+ end: i + 1
802
+ };
803
+ if (ch === "\\" && i + 1 < text.length) {
804
+ value += text[i + 1];
805
+ i += 2;
806
+ continue;
807
+ }
808
+ value += ch;
809
+ i += 1;
810
+ }
811
+ return {
812
+ value,
813
+ end: text.length
814
+ };
815
+ };
816
+ /**
817
+ * Scan a parenthesised argument list starting at `(`. Arguments are string,
818
+ * number or boolean literals, optionally named (`adapt=true`). Anything else
819
+ * (an identifier, an operator, a nested call) fails the scan, which is what
820
+ * makes `min(a + 1)` a rejected expression rather than a silently odd filter.
821
+ */
822
+ const scanArguments = (text, start) => {
823
+ const args = [];
824
+ let i = start + 1;
825
+ let expectValue = true;
826
+ let pendingName;
827
+ while (i < text.length) {
828
+ const ch = text[i];
829
+ if (ch === void 0) break;
830
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
831
+ i += 1;
832
+ continue;
833
+ }
834
+ if (ch === ")") {
835
+ if (pendingName !== void 0) return null;
836
+ if (!expectValue || args.length === 0) return {
837
+ args,
838
+ end: i + 1
839
+ };
840
+ return null;
841
+ }
842
+ if (ch === ",") {
843
+ if (expectValue) return null;
844
+ expectValue = true;
845
+ i += 1;
846
+ continue;
847
+ }
848
+ if (!expectValue) return null;
849
+ if (ch === "\"" || ch === "'") {
850
+ const { value, end } = scanQuoted(text, i);
851
+ args.push(pendingName === void 0 ? {
852
+ kind: "positional",
853
+ value
854
+ } : {
855
+ kind: "keyword",
856
+ name: pendingName,
857
+ value
858
+ });
859
+ pendingName = void 0;
860
+ expectValue = false;
861
+ i = end;
862
+ continue;
863
+ }
864
+ let j = i;
865
+ while (j < text.length && /[\p{L}\p{N}_.-]/u.test(text[j] ?? "")) j += 1;
866
+ if (j === i) return null;
867
+ const raw = text.slice(i, j);
868
+ if (text[j] === "=" && text[j + 1] !== "=") {
869
+ if (pendingName !== void 0 || !IDENTIFIER_RE.test(raw)) return null;
870
+ pendingName = raw;
871
+ i = j + 1;
872
+ continue;
873
+ }
874
+ const value = literalFromBareToken(raw);
875
+ if (value === null) return null;
876
+ args.push(pendingName === void 0 ? {
877
+ kind: "positional",
878
+ value
879
+ } : {
880
+ kind: "keyword",
881
+ name: pendingName,
882
+ value
883
+ });
884
+ pendingName = void 0;
885
+ expectValue = false;
886
+ i = j;
887
+ }
888
+ return null;
889
+ };
890
+ /**
891
+ * Parse the `| name | name(args)` tail of an output marker. Returns the named
892
+ * unknown filter instead of a chain when one step is not in the catalogue, so
893
+ * the rejection can quote it.
894
+ */
895
+ const scanFilterChain = (tail) => {
896
+ const filters = [];
897
+ let i = 0;
898
+ while (i < tail.length) {
899
+ while (i < tail.length && /\s/u.test(tail[i] ?? "")) i += 1;
900
+ if (i >= tail.length) break;
901
+ if (tail[i] !== "|") return null;
902
+ i += 1;
903
+ while (i < tail.length && /\s/u.test(tail[i] ?? "")) i += 1;
904
+ let j = i;
905
+ while (j < tail.length && /[\p{L}\p{N}_]/u.test(tail[j] ?? "")) j += 1;
906
+ const name = tail.slice(i, j);
907
+ if (name === "") return null;
908
+ if (!isFilterName(name)) return { unknownFilter: name };
909
+ i = j;
910
+ while (i < tail.length && /\s/u.test(tail[i] ?? "")) i += 1;
911
+ if (tail[i] === "(") {
912
+ const scanned = scanArguments(tail, i);
913
+ if (scanned === null) return null;
914
+ filters.push({
915
+ name,
916
+ args: scanned.args
917
+ });
918
+ i = scanned.end;
919
+ continue;
920
+ }
921
+ filters.push({
922
+ name,
923
+ args: []
924
+ });
925
+ }
926
+ return { filters };
927
+ };
928
+ const PREFIX_BY_TOKEN = {
929
+ p: "paragraph",
930
+ tr: "row"
931
+ };
932
+ const readPrefix = (token) => token === "p" || token === "tr" ? PREFIX_BY_TOKEN[token] : "none";
933
+ const TAG_RE = /^(?<tag>[\p{L}_][\p{L}\p{N}_]*)\b(?<rest>[\s\S]*)$/u;
934
+ const FOR_RE = /^(?<alias>[\p{L}_][\p{L}\p{N}_-]*)\s+in\s+(?<path>[\p{L}\p{N}_.-]+)\s*(?<filters>\|[\s\S]*)?$/u;
935
+ const CALL_RE = /^(?<name>[\p{L}_][\p{L}\p{N}_]*)\s*\(/u;
936
+ /** Classify a `{% ... %}` tag. */
937
+ const classifyStatement = (inner) => {
938
+ const match = TAG_RE.exec(inner);
939
+ if (!match) return null;
940
+ const tag = match.groups?.["tag"] ?? "";
941
+ const rest = (match.groups?.["rest"] ?? "").trim();
942
+ switch (tag) {
943
+ case "if": return {
944
+ kind: "if",
945
+ expr: rest
946
+ };
947
+ case "elif": return {
948
+ kind: "elif",
949
+ expr: rest
950
+ };
951
+ case "else": return rest === "" ? { kind: "else" } : null;
952
+ case "endif": return rest === "" ? { kind: "endif" } : null;
953
+ case "endfor": return rest === "" ? { kind: "endfor" } : null;
954
+ case "for": {
955
+ const loop = FOR_RE.exec(rest);
956
+ if (!loop) return null;
957
+ const tail = loop.groups?.["filters"];
958
+ const chain = tail === void 0 ? { filters: [] } : scanFilterChain(tail);
959
+ if (chain === null || "unknownFilter" in chain) return null;
960
+ return {
961
+ kind: "for",
962
+ alias: loop.groups?.["alias"] ?? "",
963
+ path: loop.groups?.["path"] ?? "",
964
+ filters: chain.filters
965
+ };
966
+ }
967
+ default: return null;
968
+ }
969
+ };
970
+ /** Classify a `{{ ... }}` output marker. */
971
+ const classifyOutput = (inner) => {
972
+ const call = CALL_RE.exec(inner);
973
+ if (call) {
974
+ const name = call.groups?.["name"] ?? "";
975
+ if (!isFunctionName(name)) return null;
976
+ const scanned = scanArguments(inner, inner.indexOf("("));
977
+ if (scanned === null) return null;
978
+ if (inner.slice(scanned.end).trim() !== "") return null;
979
+ const positional = scanned.args.filter((arg) => arg.kind === "positional" && typeof arg.value === "string");
980
+ if (positional.length !== scanned.args.length) return null;
981
+ const [first, second] = positional.map((arg) => String(arg.value));
982
+ if (first === void 0) return null;
983
+ if (name === "clause") return scanned.args.length <= 2 ? {
984
+ kind: "clause",
985
+ name: first,
986
+ version: second
987
+ } : null;
988
+ if (scanned.args.length !== 1 || !isFieldPath(first)) return null;
989
+ return name === "num" ? {
990
+ kind: "num",
991
+ key: first
992
+ } : {
993
+ kind: "ref",
994
+ key: first
995
+ };
996
+ }
997
+ const pipe = inner.indexOf("|");
998
+ const head = (pipe === -1 ? inner : inner.slice(0, pipe)).trim();
999
+ const tail = pipe === -1 ? "" : inner.slice(pipe);
1000
+ const loopMatch = /^loop\.(?<property>[\p{L}\p{N}_]+)$/u.exec(head);
1001
+ if (loopMatch) {
1002
+ const property = loopMatch.groups?.["property"] ?? "";
1003
+ return isLoopProperty(property) && tail === "" ? {
1004
+ kind: "loop",
1005
+ property
1006
+ } : null;
1007
+ }
1008
+ if (!FIELD_PATH_RE.test(head)) return null;
1009
+ if (tail === "") return {
1010
+ kind: "placeholder",
1011
+ expr: head,
1012
+ filters: []
1013
+ };
1014
+ const chain = scanFilterChain(tail);
1015
+ if (chain === null || "unknownFilter" in chain) return null;
1016
+ return {
1017
+ kind: "placeholder",
1018
+ expr: head,
1019
+ filters: chain.filters
1020
+ };
1021
+ };
1022
+ /**
1023
+ * Classify the inner text of one marker. Returns `null` when the text is not a
1024
+ * recognized directive (so a recognizer can leave stray braces alone).
1025
+ */
1026
+ const classifyMarker = (innerRaw, form = "output") => {
1027
+ const inner = normalizeMarkerInner(innerRaw).trim();
1028
+ return form === "statement" ? classifyStatement(inner) : classifyOutput(inner);
1029
+ };
1030
+ /** Every marker-shaped span in `text`, in document order, before classification. */
1031
+ const scanSpans = (text) => {
1032
+ const re = markerPattern();
1033
+ const spans = [];
1034
+ let match = re.exec(text);
1035
+ while (match !== null) {
1036
+ const output = match.groups?.["output"];
1037
+ const statement = match.groups?.["statement"];
1038
+ const raw = match[0];
1039
+ const form = output === void 0 ? "statement" : "output";
1040
+ spans.push({
1041
+ start: match.index,
1042
+ end: match.index + raw.length,
1043
+ raw,
1044
+ inner: normalizeMarkerInner(output ?? statement ?? "").trim(),
1045
+ form,
1046
+ prefix: readPrefix(match.groups?.["prefix"])
1047
+ });
1048
+ match = re.exec(text);
1049
+ }
1050
+ return spans;
1051
+ };
1052
+ /**
1053
+ * Find every recognized marker in `text`, in document order, with offsets and
1054
+ * classified metadata. Unrecognized spans are skipped.
1055
+ */
1056
+ const scanMarkers = (text) => {
1057
+ const out = [];
1058
+ for (const span of scanSpans(text)) {
1059
+ const meta = classifyMarker(span.inner, span.form);
1060
+ if (meta) out.push({
1061
+ ...span,
1062
+ meta
1063
+ });
1064
+ }
1065
+ return out;
1066
+ };
1067
+ /**
1068
+ * Rewrite every `{{ ... }}` output marker in `text` through `replace`, which
1069
+ * receives the whole marker and its raw inner text and returns the replacement.
1070
+ * Tags are left untouched. The single scan is the same one the classifier uses,
1071
+ * so a caller that rewrites marker text never needs its own regex.
1072
+ */
1073
+ const replaceOutputMarkers = (text, replace) => {
1074
+ let out = "";
1075
+ let cursor = 0;
1076
+ for (const span of scanSpans(text)) {
1077
+ if (span.form !== "output") continue;
1078
+ out += text.slice(cursor, span.start) + replace(span.raw, span.inner);
1079
+ cursor = span.end;
1080
+ }
1081
+ return out + text.slice(cursor);
1082
+ };
1083
+ /**
1084
+ * The values-map key one marker substitutes from, or `null` when the marker is
1085
+ * not a substitution target (a tag, or a `loop.*` token the loop expander
1086
+ * resolves first). One function so the key discovery writes is the key the
1087
+ * patcher reads.
1088
+ */
1089
+ const substitutionKey = (meta) => {
1090
+ switch (meta.kind) {
1091
+ case "placeholder": return meta.expr;
1092
+ case "clause": return meta.version === void 0 ? `${CLAUSE_KEY_PREFIX}${meta.name}` : `${CLAUSE_KEY_PREFIX}${meta.name}:${meta.version}`;
1093
+ case "num":
1094
+ case "ref":
1095
+ case "loop":
1096
+ case "if":
1097
+ case "elif":
1098
+ case "else":
1099
+ case "endif":
1100
+ case "for":
1101
+ case "endfor": return null;
1102
+ default: return assertNever(meta);
1103
+ }
1104
+ };
1105
+ /** Namespace of the synthetic clause-slot key, kept out of the field-path
1106
+ * charset so it can never collide with an author's path. */
1107
+ const CLAUSE_KEY_PREFIX = "@clause:";
1108
+ /** The values-map key for one clause slot. */
1109
+ const clauseSlotKey = (name, version) => version === void 0 ? `${CLAUSE_KEY_PREFIX}${name}` : `${CLAUSE_KEY_PREFIX}${name}:${version}`;
1110
+ /**
1111
+ * Find every marker-shaped span whose inner text is NOT a recognized
1112
+ * directive — near-misses an author clearly meant as markers but that every
1113
+ * recognizer skips, so they print literally at fill time. This is the exact
1114
+ * complement of {@link scanMarkers}: a span is in one list or the other.
1115
+ */
1116
+ const scanInvalidMarkers = (text) => {
1117
+ const out = [];
1118
+ for (const { prefix: _prefix, ...span } of scanSpans(text)) if (classifyMarker(span.inner, span.form) === null) out.push(span);
1119
+ return out;
1120
+ };
1121
+ const LEGACY_BLOCK_RE = /^(?<token>#if|#elseif|#else|#each|\/if|\/each)\b(?<expr>[\s\S]*)$/u;
1122
+ const LEGACY_CLAUSE_RE = /^@clause:(?<name>[^:}\s]+)(?::(?<version>[^}\s]+))?$/u;
1123
+ const LEGACY_NUM_RE = /^@(?<fn>num|ref):(?<key>[\p{L}\p{N}_.-]+)$/u;
1124
+ /** Singular of a plain-word path segment, for a generated loop alias. */
1125
+ const singularize = (segment) => {
1126
+ if (segment.endsWith("ies") && segment.length > 3) return `${segment.slice(0, -3)}y`;
1127
+ if (/(?:s|ss|sh|ch|x|z)es$/u.test(segment)) return segment.slice(0, -2);
1128
+ if (/[^s]s$/u.test(segment)) return segment.slice(0, -1);
1129
+ return segment;
1130
+ };
1131
+ /** The loop alias the codemod (and the legacy-marker message) generates for a
1132
+ * `{{#each path}}`: the singular of the last plain-word segment, else `item`. */
1133
+ const legacyLoopAlias = (path) => {
1134
+ const segment = path.split(".").at(-1) ?? "";
1135
+ if (!IDENTIFIER_RE.test(segment)) return "item";
1136
+ const alias = singularize(segment);
1137
+ return alias === "" || alias === segment ? "item" : alias;
1138
+ };
1139
+ /** Translate an old-dialect condition expression to the Jinja surface:
1140
+ * `!a` becomes `not a`, `a contains "x"` becomes `"x" in a`. */
1141
+ const translateLegacyExpression = (expr) => expr.replace(/(?<![\p{L}\p{N}_.-])(?<path>[\p{L}\p{N}_.-]+)\s+contains\s+(?<value>"[^"]*"|[\p{L}\p{N}_.-]+)/gu, (_m, path, value) => `${value.startsWith("\"") ? value : `"${value}"`} in ${path}`).replace(/!(?!=)\s*/gu, "not ");
1142
+ /**
1143
+ * The Jinja marker that replaces one old-dialect marker's inner text, or
1144
+ * `null` when the text is not old-dialect. Shared by the rejection message and
1145
+ * the codemod, so the fix an author is told to make is the fix the script
1146
+ * would have made.
1147
+ */
1148
+ const legacyMarkerReplacement = (innerRaw) => {
1149
+ const inner = normalizeMarkerInner(innerRaw).trim();
1150
+ const block = LEGACY_BLOCK_RE.exec(inner);
1151
+ if (block) {
1152
+ const token = block.groups?.["token"] ?? "";
1153
+ const expr = translateLegacyExpression((block.groups?.["expr"] ?? "").trim());
1154
+ switch (token) {
1155
+ case "#if": return `{% if ${expr} %}`;
1156
+ case "#elseif": return `{% elif ${expr} %}`;
1157
+ case "#else": return "{% else %}";
1158
+ case "/if": return "{% endif %}";
1159
+ case "#each": return `{% for ${legacyLoopAlias(expr)} in ${expr} %}`;
1160
+ case "/each": return "{% endfor %}";
1161
+ default: return null;
1162
+ }
1163
+ }
1164
+ const clause = LEGACY_CLAUSE_RE.exec(inner);
1165
+ if (clause) {
1166
+ const name = clause.groups?.["name"] ?? "";
1167
+ const version = clause.groups?.["version"];
1168
+ return version === void 0 ? `{{ clause("${name}") }}` : `{{ clause("${name}", "${version}") }}`;
1169
+ }
1170
+ const numbering = LEGACY_NUM_RE.exec(inner);
1171
+ if (numbering) return `{{ ${numbering.groups?.["fn"] ?? ""}("${numbering.groups?.["key"] ?? ""}") }}`;
1172
+ if (inner === "@index") return "{{ loop.index }}";
1173
+ if (inner === "@count") return "{{ loop.length }}";
1174
+ return null;
1175
+ };
1176
+ const MARKER_DEFECT_KINDS = [
1177
+ "legacy_marker",
1178
+ "unsupported_tag",
1179
+ "unknown_filter",
1180
+ "python_expression",
1181
+ "bracket_index"
1182
+ ];
1183
+ /** Bracket indexing (`items[0].name`), which no directive kind admits. */
1184
+ const BRACKET_INDEX_RE = /[[\]]/u;
1185
+ /** A legacy directive sigil at the head of an output marker. */
1186
+ const LEGACY_SHAPE_RE = /^[#/@]/u;
1187
+ /**
1188
+ * Arithmetic, a call, a quoted string or a Python literal inside `{{ }}`. A
1189
+ * span that merely holds a space (`{{my field}}`) is a mistyped path, not an
1190
+ * expression, and stays undiagnosed so the check reports it as an unreadable
1191
+ * marker rather than guessing at Python.
1192
+ */
1193
+ const PYTHON_SHAPE_RE = /["'()+*/%]|\bNone\b|\bTrue\b|\bFalse\b/u;
1194
+ /**
1195
+ * Name the authoring mistake behind one marker's inner text, or `null` when
1196
+ * the text is a recognized directive ({@link classifyMarker} accepts it) or an
1197
+ * unrecognizable span with no specific diagnosis (`{{my field}}`).
1198
+ */
1199
+ const classifyMarkerDefect = (innerRaw, form = "output") => {
1200
+ const inner = normalizeMarkerInner(innerRaw).trim();
1201
+ if (classifyMarker(inner, form) !== null) return null;
1202
+ if (form === "statement") {
1203
+ const tag = TAG_RE.exec(inner)?.groups?.["tag"];
1204
+ if (tag === void 0) return null;
1205
+ const legacy = legacyMarkerReplacement(inner);
1206
+ if (legacy !== null) return {
1207
+ kind: "legacy_marker",
1208
+ construct: inner,
1209
+ replacement: legacy
1210
+ };
1211
+ return {
1212
+ kind: "unsupported_tag",
1213
+ construct: tag
1214
+ };
1215
+ }
1216
+ const legacy = legacyMarkerReplacement(inner);
1217
+ if (legacy !== null) return {
1218
+ kind: "legacy_marker",
1219
+ construct: inner,
1220
+ replacement: legacy
1221
+ };
1222
+ if (LEGACY_SHAPE_RE.test(inner)) return {
1223
+ kind: "legacy_marker",
1224
+ construct: inner
1225
+ };
1226
+ if (BRACKET_INDEX_RE.test(inner)) return {
1227
+ kind: "bracket_index",
1228
+ construct: inner
1229
+ };
1230
+ const pipe = inner.indexOf("|");
1231
+ if (pipe !== -1) {
1232
+ const chain = scanFilterChain(inner.slice(pipe));
1233
+ if (chain !== null && "unknownFilter" in chain) return {
1234
+ kind: "unknown_filter",
1235
+ construct: chain.unknownFilter
1236
+ };
1237
+ }
1238
+ const call = CALL_RE.exec(inner);
1239
+ if (call) {
1240
+ const name = call.groups?.["name"] ?? "";
1241
+ if (!isFunctionName(name)) return {
1242
+ kind: "python_expression",
1243
+ construct: `${name}(...)`
1244
+ };
1245
+ return {
1246
+ kind: "python_expression",
1247
+ construct: inner
1248
+ };
1249
+ }
1250
+ const head = (pipe === -1 ? inner : inner.slice(0, pipe)).trim();
1251
+ if (head !== "" && !FIELD_PATH_RE.test(head) && PYTHON_SHAPE_RE.test(head)) return {
1252
+ kind: "python_expression",
1253
+ construct: head
1254
+ };
1255
+ return null;
1256
+ };
1257
+ /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
1258
+ const assertNever = (value) => panic(`Unhandled template directive: ${JSON.stringify(value)}`);
1259
+ //#endregion
1260
+ //#region src/loop-scope.ts
1261
+ const rowScopePaths = (rowScopes) => rowScopes.map(({ scopedPath }) => scopedPath);
1262
+ /** A path already under the innermost repeat is left alone; anything else is
1263
+ * read as a name relative to it. */
1264
+ const qualifyRowScopedPath = (path, rowPaths = []) => {
1265
+ if (rowPaths.some((rowPath) => path === rowPath || path.startsWith(`${rowPath}.`))) return path;
1266
+ const innermostRowPath = rowPaths.at(-1);
1267
+ return innermostRowPath === void 0 ? path : `${innermostRowPath}.${path}`;
1268
+ };
1269
+ /** The manifest path for a marker written inside these loops. */
1270
+ const qualifyRowScopedPlaceholder = (path, rowScopes) => {
1271
+ if (rowScopes.some(({ scopedPath }) => path === scopedPath || path.startsWith(`${scopedPath}.`))) return path;
1272
+ for (const { alias, declaredPath, scopedPath } of rowScopes.toReversed()) for (const head of [alias, declaredPath]) {
1273
+ if (path === head) return scopedPath;
1274
+ if (path.startsWith(`${head}.`)) return `${scopedPath}.${path.slice(head.length + 1)}`;
1275
+ }
1276
+ return path;
1277
+ };
1278
+ /** The manifest path for an array a `{% for %}` opens inside these loops. */
1279
+ const qualifyLoopPath = (declaredPath, rowScopes) => qualifyRowScopedPath(qualifyRowScopedPlaceholder(declaredPath, rowScopes), rowScopePaths(rowScopes));
1280
+ //#endregion
1281
+ //#region src/field-config.ts
1282
+ /**
1283
+ * The filters that describe a REPEAT rather than a value: how many rows it
1284
+ * takes and what to call it. These are the ones a `{% for %}` tag may carry,
1285
+ * since the loop path names the array, not one of its values.
1286
+ */
1287
+ const ARRAY_FILTER_NAMES = [
1288
+ "label",
1289
+ "hint",
1290
+ "required",
1291
+ "min_items",
1292
+ "max_items"
1293
+ ];
1294
+ const isArrayFilterName = (name) => ARRAY_FILTER_NAMES.some((candidate) => candidate === name);
1295
+ const NO_FILTERS = [];
1296
+ const filterCall = (name, ...args) => ({
1297
+ name,
1298
+ args
1299
+ });
1300
+ const positionalArg = (value) => ({
1301
+ kind: "positional",
1302
+ value
1303
+ });
1304
+ const keywordArg = (name, value) => ({
1305
+ kind: "keyword",
1306
+ name,
1307
+ value
1308
+ });
1309
+ /** The `date()` spec that reads back as this pair: a locale, then the style it
1310
+ * renders with. An ISO date reads the same in every language, so `iso` is
1311
+ * written as the bare style the reader canonicalizes. */
1312
+ const dateSpec = ({ locale, style }) => style === "iso" ? style : `${locale}-${style}`;
1313
+ /** The numeric bounds, in the order the reader lists them, so a chain a
1314
+ * document already carries and one this writes are the same text. */
1315
+ const VALIDATION_BOUNDS = [
1316
+ ["min", "min"],
1317
+ ["max", "max"],
1318
+ ["minLength", "min_length"],
1319
+ ["maxLength", "max_length"],
1320
+ ["minItems", "min_items"],
1321
+ ["maxItems", "max_items"]
1322
+ ];
1323
+ const writeInputType = (field) => {
1324
+ if (field.dateFormat !== void 0) return [filterCall("date", positionalArg(dateSpec(field.dateFormat)))];
1325
+ const { inputType } = field;
1326
+ if (inputType === void 0) return NO_FILTERS;
1327
+ switch (inputType) {
1328
+ case "boolean": return [filterCall("checkbox")];
1329
+ case "date": return [filterCall("date", positionalArg("iso"))];
1330
+ case "select": return [filterCall("select", ...(field.options ?? []).map((option) => positionalArg(option)))];
1331
+ case "number":
1332
+ case "text": return [filterCall(inputType)];
1333
+ default: return assertNever(inputType);
1334
+ }
1335
+ };
1336
+ const writeSource = (field) => {
1337
+ const { source } = field;
1338
+ if (source === void 0) return NO_FILTERS;
1339
+ switch (source.kind) {
1340
+ case "party": return [filterCall("party", positionalArg(source.role), positionalArg(source.field))];
1341
+ case "attorney": return [filterCall("attorney", positionalArg(source.ref), positionalArg(source.field))];
1342
+ case "contact":
1343
+ case "firm":
1344
+ case "matter": return [filterCall(source.kind, positionalArg(source.field))];
1345
+ default: return assertNever(source);
1346
+ }
1347
+ };
1348
+ const writeAi = (field) => {
1349
+ const { aiAdapt, aiPrompt, aiSeesDocument } = field;
1350
+ if (aiPrompt === void 0 && aiAdapt !== true) return NO_FILTERS;
1351
+ return [filterCall("ai", ...aiPrompt === void 0 ? [] : [positionalArg(aiPrompt)], ...aiAdapt === void 0 ? [] : [keywordArg("adapt", aiAdapt)], ...aiSeesDocument === void 0 ? [] : [keywordArg("sees_document", aiSeesDocument)])];
1352
+ };
1353
+ const writeValidation = (field) => {
1354
+ const validation = field.validation;
1355
+ if (validation === void 0) return NO_FILTERS;
1356
+ const calls = [];
1357
+ if (validation.pattern !== void 0) calls.push(filterCall("pattern", positionalArg(validation.pattern)));
1358
+ for (const [key, name] of VALIDATION_BOUNDS) {
1359
+ const bound = validation[key];
1360
+ if (bound !== void 0) calls.push(filterCall(name, positionalArg(bound)));
1361
+ }
1362
+ return calls;
1363
+ };
1364
+ /**
1365
+ * Every key of {@link MarkerFieldConfig} and the filters that write it. Total
1366
+ * over the shape, so a property cannot join it without a decision about how an
1367
+ * author writes it in the document.
1368
+ *
1369
+ * A filter several properties feed (`ai`, `select`, `date`) is written by one
1370
+ * of them and skipped by the rest; the round-trip property in the api's
1371
+ * `field-filters.property.test.ts` is what proves nothing is dropped.
1372
+ */
1373
+ const MARKER_FIELD_FILTERS = {
1374
+ path: () => NO_FILTERS,
1375
+ inputType: writeInputType,
1376
+ options: () => NO_FILTERS,
1377
+ dateFormat: () => NO_FILTERS,
1378
+ optionsFrom: ({ optionsFrom }) => optionsFrom === void 0 ? NO_FILTERS : [filterCall("options_from", positionalArg(optionsFrom))],
1379
+ label: ({ label }) => label === void 0 ? NO_FILTERS : [filterCall("label", positionalArg(label))],
1380
+ hint: ({ hint }) => hint === void 0 ? NO_FILTERS : [filterCall("hint", positionalArg(hint))],
1381
+ required: (field) => field.required === true || field.validation?.required === true ? [filterCall("required")] : NO_FILTERS,
1382
+ validation: writeValidation,
1383
+ aiPrompt: writeAi,
1384
+ aiAdapt: () => NO_FILTERS,
1385
+ aiSeesDocument: () => NO_FILTERS,
1386
+ lookup: ({ lookup }) => lookup === void 0 ? NO_FILTERS : [filterCall("lookup", positionalArg(lookup.registry), ...lookup.formats.map(({ key, template }) => keywordArg(key, template)))],
1387
+ source: writeSource,
1388
+ formula: ({ formula }) => formula === void 0 ? NO_FILTERS : [filterCall("formula", positionalArg(formula))],
1389
+ condition: ({ condition }) => condition === void 0 ? NO_FILTERS : [filterCall("condition", positionalArg(condition))]
1390
+ };
1391
+ /**
1392
+ * The filter chain that declares this field in a document, in the catalogue's
1393
+ * own order so a chain the writer produces and one an author wrote read the
1394
+ * same.
1395
+ */
1396
+ const filtersFromFieldConfig = (field) => Object.values(MARKER_FIELD_FILTERS).flatMap((write) => write(field));
1397
+ /**
1398
+ * The chain a `{% for %}` opener carries for this array: the repeat's own
1399
+ * filters and nothing else, derived from {@link ARRAY_FILTER_NAMES}, so a
1400
+ * value filter can never be written where the reader refuses it.
1401
+ */
1402
+ const arrayFiltersFromFieldConfig = (field) => filtersFromFieldConfig(field).filter(({ name }) => isArrayFilterName(name));
1403
+ //#endregion
1404
+ //#region src/render.ts
1405
+ /** The number literals the scanner reads: no exponent, no `Infinity`. */
1406
+ const WRITABLE_NUMBER = /^-?\d+(?:\.\d+)?$/u;
1407
+ /** What a tag's body cannot carry: it has no quoted-run escape hatch, so a
1408
+ * brace or a tag closer inside one would end the tag early. */
1409
+ const RESERVED_IN_TAG = /[{}]|%\}/u;
1410
+ /** The reason this literal cannot be written in a marker of this form, or
1411
+ * `null` when it can. A boolean always can; a string can in a `{{ }}` marker,
1412
+ * because the writer quotes and escapes it. */
1413
+ const unwritableMarkerLiteral = (value, form = "output") => {
1414
+ if (typeof value === "number") return WRITABLE_NUMBER.test(String(value)) ? null : "number-spelling";
1415
+ if (typeof value === "string" && form === "statement") return RESERVED_IN_TAG.test(value) ? "tag-delimiter" : null;
1416
+ return null;
1417
+ };
1418
+ /** One string literal, double-quoted, with backslashes and quotes escaped the
1419
+ * way `scanQuoted` reads them back. */
1420
+ const renderString = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
1421
+ const renderLiteral = (value) => typeof value === "string" ? renderString(value) : String(value);
1422
+ const renderArgument = (arg) => arg.kind === "positional" ? renderLiteral(arg.value) : `${arg.name}=${renderLiteral(arg.value)}`;
1423
+ /** Every value in a chain the grammar has no spelling for, with the filter it
1424
+ * was written on and why, so a caller can refuse a configuration before it
1425
+ * rewrites a document into something the scanner would read differently. A
1426
+ * repeat's chain goes in a tag, so pass `"statement"` for one. */
1427
+ const unwritableFilterValues = (filters, form = "output") => filters.flatMap(({ args, name }) => args.flatMap((arg) => {
1428
+ const reason = unwritableMarkerLiteral(arg.value, form);
1429
+ return reason === null ? [] : [{
1430
+ filter: name,
1431
+ value: arg.value,
1432
+ reason
1433
+ }];
1434
+ }));
1435
+ /**
1436
+ * One filter chain as marker text, without the leading pipe: `label("A") |
1437
+ * required`. An empty chain renders the empty string, which is what a marker
1438
+ * that configures nothing carries.
1439
+ */
1440
+ const renderFilterChain = (filters) => filters.map(({ args, name }) => args.length === 0 ? name : `${name}(${args.map(renderArgument).join(", ")})`).join(" | ");
1441
+ /** One value marker: `{{ path }}` or `{{ path | filters }}`. */
1442
+ const renderValueMarker = (path, filters) => {
1443
+ const chain = renderFilterChain(filters);
1444
+ return chain === "" ? `{{ ${path} }}` : `{{ ${path} | ${chain} }}`;
1445
+ };
1446
+ /** The docxtpl placement token a prefix is written with; `none` writes none. */
1447
+ const PREFIX_TOKEN = {
1448
+ none: "",
1449
+ paragraph: "p",
1450
+ row: "tr"
1451
+ };
1452
+ /**
1453
+ * One `{% if %}` / `{% elif %}` opener carrying its expression.
1454
+ *
1455
+ * A rule that decides whether a block shows belongs in the tag that shows it:
1456
+ * the tag is the only place a document has for it, so a builder's rule is
1457
+ * serialized here rather than stored beside the document under a name.
1458
+ */
1459
+ const renderConditionTag = ({ expression, kind, prefix }) => `{%${PREFIX_TOKEN[prefix]} ${kind} ${expression} %}`;
1460
+ /** One `{% for %}` opener, keeping its placement prefix. */
1461
+ const renderForOpener = ({ alias, filters, path, prefix }) => {
1462
+ const chain = renderFilterChain(filters);
1463
+ const head = `{%${PREFIX_TOKEN[prefix]} for ${alias} in ${path}`;
1464
+ return chain === "" ? `${head} %}` : `${head} | ${chain} %}`;
1465
+ };
1466
+ //#endregion
763
1467
  //#region src/row-blocks.ts
764
1468
  /**
765
1469
  * Row-form block markers: the lenient placement authoring agents actually
@@ -770,7 +1474,7 @@ const serializeCondition = (node) => serializeNode(node, true);
770
1474
  * an author instead types the opener in front of the first cell's text and the
771
1475
  * closer behind the last cell's text:
772
1476
  *
773
- * | {{#each deliverables}}{{deliverables.item}} | {{deliverables.fee}}{{/each}} |
1477
+ * | {% for d in deliverables %}{{ d.item }} | {{ d.fee }}{% endfor %} |
774
1478
  *
775
1479
  * Both placements mean the same thing — the `w:tr` is the unit — so this
776
1480
  * module recognizes the row form purely from text, and the consumer rewrites
@@ -781,18 +1485,18 @@ const serializeCondition = (node) => serializeNode(node, true);
781
1485
  * and neither can drift from the other's idea of what a row block is.
782
1486
  */
783
1487
  const CLOSER_OF = {
784
- each: "endeach",
1488
+ for: "endfor",
785
1489
  if: "endif"
786
1490
  };
787
- const isOpenKind = (kind) => kind === "each" || kind === "if";
1491
+ const isOpenKind = (kind) => kind === "for" || kind === "if";
788
1492
  /**
789
1493
  * Block markers in one paragraph that do not pair inside it. A pair that opens
790
- * and closes within the paragraph is an inline span (`the Buyer{{#if x}} and
791
- * spouse{{/if}}`), which the inline engine already owns; only what is left
1494
+ * and closes within the paragraph is an inline span (`the Buyer{% if x %} and
1495
+ * spouse{% endif %}`), which the inline engine already owns; only what is left
792
1496
  * over can reach across cells.
793
1497
  *
794
- * A branch marker (`{{#elseif}}`, `{{#else}}`) counts as left over unless its
795
- * `{{#if}}` also closes in this paragraph. Only the opener and the closer of a
1498
+ * A branch marker (`{% elif %}`, `{% else %}`) counts as left over unless its
1499
+ * `{% if %}` also closes in this paragraph. Only the opener and the closer of a
796
1500
  * row block are hoisted, so a branch marker stranded between them would stay
797
1501
  * buried in the cell text: a false condition would drop the whole row and the
798
1502
  * branch that should have rendered with it, and a true one would leave the
@@ -816,13 +1520,13 @@ const danglingBlockMarkers = (text) => {
816
1520
  });
817
1521
  continue;
818
1522
  }
819
- if (kind === "endeach" || kind === "endif") {
1523
+ if (kind === "endfor" || kind === "endif") {
820
1524
  const innermost = open.at(-1);
821
1525
  if (innermost && CLOSER_OF[innermost.kind] === kind) open.pop();
822
1526
  else dangling.push(marker);
823
1527
  continue;
824
1528
  }
825
- if (kind === "elseif" || kind === "else") {
1529
+ if (kind === "elif" || kind === "else") {
826
1530
  const innermost = open.at(-1);
827
1531
  if (innermost?.kind === "if") innermost.branches.push(marker);
828
1532
  else dangling.push(marker);
@@ -847,7 +1551,7 @@ const suffixesCell = (paragraphs, { marker, paragraphIndex }) => paragraphIndex
847
1551
  * paragraph, they are a matching opener/closer pair, the closer sits in a LATER
848
1552
  * cell than the opener, the opener prefixes its cell and the closer suffixes
849
1553
  * its cell. Everything else — one half of a pair, two nested pairs, a stray
850
- * `{{#else}}`, a pair wrapping the paragraphs of a single cell — is left to the
1554
+ * `{% else %}`, a pair wrapping the paragraphs of a single cell — is left to the
851
1555
  * engine's existing structure errors rather than guessed at.
852
1556
  */
853
1557
  const detectRowBlockPair = (cells) => {
@@ -878,7 +1582,7 @@ const detectRowBlockPair = (cells) => {
878
1582
  *
879
1583
  * Boolean conditions are evaluated through the canonical `@stll/conditions`
880
1584
  * AST and its single evaluator, so view filters, AI-extraction gating, and
881
- * template `{{#if ...}}` conditionals share one set of operators and semantics
1585
+ * template `{% if ... %}` conditionals share one set of operators and semantics
882
1586
  * and never drift apart. This module owns the template surface: the string
883
1587
  * parser (`./parse`), the named-condition resolver, and the no-code builder
884
1588
  * serializer (`./condition-builder`).
@@ -892,7 +1596,7 @@ const MAX_CONDITION_DEPTH = 50;
892
1596
  /**
893
1597
  * Normalize a fill-bag value into a `ConditionValue` the shared evaluator
894
1598
  * accepts: arrays become string arrays (multi-select fields hold their options
895
- * as strings, so membership tests like `parties contains "guarantor"` work
1599
+ * as strings, so membership tests like `"guarantor" in parties` work
896
1600
  * even when an element is numeric), primitives pass through, anything else is
897
1601
  * stringified.
898
1602
  */
@@ -905,7 +1609,7 @@ const toConditionValue = (raw) => {
905
1609
  * Build the operand resolver for one evaluation. Only `path` operands need
906
1610
  * resolving (the evaluator handles literals). A path is first matched against
907
1611
  * the named conditions: a hit evaluates that condition's expression — so
908
- * `{{#if isCompany}}` and `{{#if isCompany and signed}}` both work — guarded
1612
+ * `{% if isCompany %}` and `{% if isCompany and signed %}` both work — guarded
909
1613
  * against cycles (`resolved`) and runaway chains (`MAX_CONDITION_DEPTH`).
910
1614
  * Otherwise the path resolves against the fill data.
911
1615
  */
@@ -927,12 +1631,13 @@ const makeResolver = (data, namedConditions, resolved, depth) => {
927
1631
  return resolve;
928
1632
  };
929
1633
  /**
930
- * Evaluate a template `{{#if ...}}` condition. The expression is parsed into
1634
+ * Evaluate a template `{% if ... %}` condition. The expression is parsed into
931
1635
  * the canonical `@stll/conditions` AST and evaluated by the shared evaluator.
932
1636
  *
933
- * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), `contains`,
934
- * truthiness, negation (`!`), logical operators (`and`, `or`), parentheses,
935
- * dotted paths, numeric underscores, and named-condition references.
1637
+ * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), membership
1638
+ * (`"x" in path`), presence (`is defined` / `is not defined`), truthiness,
1639
+ * negation (`not`), logical operators (`and`, `or`), parentheses, dotted
1640
+ * paths, numeric underscores, and named-condition references.
936
1641
  *
937
1642
  * An empty or unparseable expression is `false` ("no condition" never gates a
938
1643
  * branch true).
@@ -943,4 +1648,4 @@ const evaluateCondition = (expression, data, namedConditions, _resolved = /* @__
943
1648
  return evaluateCondition$1(node, makeResolver(data, namedConditions, _resolved, _depth));
944
1649
  };
945
1650
  //#endregion
946
- export { BLOCK_DIRECTIVE_KINDS, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, MARKER_DEFECT_KINDS, MAX_CONDITION_DEPTH, NUMERIC_FUNCTION_NAMES, assertNever, blockDirectiveLinePattern, classifyMarker, classifyMarkerDefect, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, countPattern, detectRowBlockPair, emptyCondition, evaluateCondition, evaluateNumericExpression, formatDate, hasBlockDirectivePattern, hasNumberingPattern, indexPattern, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, markerPattern, numPattern, parseCondition, placeholderPattern, refPattern, renderComposite, renderDeterministicFieldValue, resolvePath, scanInvalidMarkers, scanMarkers, serializeCondition };
1651
+ export { ARRAY_FILTER_NAMES, BLOCK_DIRECTIVE_KINDS, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, FILTER_NAMES, LOOP_PROPERTIES, MARKER_DEFECT_KINDS, MARKER_OUTPUT_BODY, MARKER_STATEMENT_BODY, MAX_CONDITION_DEPTH, NUMERIC_FUNCTION_NAMES, arrayFiltersFromFieldConfig, assertNever, blockDirectiveLinePattern, classifyMarker, classifyMarkerDefect, clauseSlotKey, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, detectRowBlockPair, emptyCondition, evaluateCondition, evaluateNumericExpression, filtersFromFieldConfig, formatDate, hasBlockDirectivePattern, hasNumberingPattern, isArrayFilterName, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, legacyLoopAlias, legacyMarkerReplacement, loopPattern, markerPattern, normalizeMarkerInner, numPattern, parseCondition, placeholderPattern, qualifyLoopPath, qualifyRowScopedPath, qualifyRowScopedPlaceholder, refPattern, referencedConditionPaths, renderConditionTag, renderDeterministicFieldValue, renderFilterChain, renderForOpener, renderValueMarker, replaceOutputMarkers, resolvePath, rowScopePaths, scanInvalidMarkers, scanMarkers, serializeCondition, substitutionKey, translateLegacyExpression, unwritableFilterValues, unwritableMarkerLiteral };