@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/README.md CHANGED
@@ -7,7 +7,7 @@ functions usable on both backend (Bun) and frontend (browser).
7
7
 
8
8
  Boolean conditions are evaluated through the canonical
9
9
  [`@stll/conditions`](https://github.com/stella/stella/tree/main/packages/conditions)
10
- AST and its single evaluator, so template `{{#if ...}}` conditionals share one
10
+ AST and its single evaluator, so template `{% if ... %}` conditionals share one
11
11
  set of operators and semantics with the rest of the system. This package owns
12
12
  the template surface: the marker grammar, the string parser, the named-condition
13
13
  resolver, the numeric expression evaluator, and the no-code builder serializer.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,22 @@ declare const resolvePath: (path: string, data: Record<string, unknown>) => unkn
6
6
  //#region src/parse.d.ts
7
7
  declare const parseCondition: (expression: string) => ConditionNode$1 | null;
8
8
  //#endregion
9
+ //#region src/referenced-paths.d.ts
10
+ /**
11
+ * What a `{% if %}` expression reads. Structural, not evaluative: the paths a
12
+ * condition depends on, for callers that decide something about the condition
13
+ * itself rather than about a document being filled.
14
+ */
15
+ /**
16
+ * Every field path an expression reads, in source order.
17
+ *
18
+ * `null` says the expression does not answer the question: it did not parse,
19
+ * or it reads an operand whose own references cannot be enumerated. A caller
20
+ * deciding what a condition depends on must not read that as "depends on
21
+ * nothing".
22
+ */
23
+ declare const referencedConditionPaths: (expression: string) => string[] | null;
24
+ //#endregion
9
25
  //#region src/compute.d.ts
10
26
  /**
11
27
  * Arithmetic evaluator for computed template fields.
@@ -31,8 +47,8 @@ declare const evaluateNumericExpression: (expression: string, data: Record<strin
31
47
  //#region src/field-values.d.ts
32
48
  /**
33
49
  * Single source of truth for the DETERMINISTIC field-value transforms a
34
- * template fill applies — composite (parts joined by a `{{key}}` format),
35
- * formula (arithmetic over the other values), and locale-aware date rendering.
50
+ * template fill applies: formula (arithmetic over the other values) and
51
+ * locale-aware date rendering.
36
52
  *
37
53
  * Both the API fill engine (apps/api/src/handlers/docx) and the web live
38
54
  * preview (template-studio.tsx) MUST route their rendering through
@@ -67,18 +83,6 @@ type FieldDateFormat = {
67
83
  * returns null.
68
84
  */
69
85
  declare const formatDate: (value: string, dateFormat: FieldDateFormat) => string | null;
70
- /** Minimal structural shape of one composite field part: only the `key`
71
- * referenced by the join format is needed to render. Callers' richer part
72
- * types (api FieldPart, web EditablePart) structurally satisfy this. */
73
- type PartConfig = {
74
- key: string;
75
- };
76
- /**
77
- * Render a `{{key}}` format over part values. Markers whose key has no part
78
- * value are left as-is (a visible authoring artifact, not a render error), so
79
- * a partially filled composite previews exactly as the fill engine renders it.
80
- */
81
- declare const renderComposite: (parts: readonly PartConfig[], format: string, partValues: Readonly<Record<string, string>>) => string;
82
86
  /**
83
87
  * Minimal STRUCTURAL config of a field, carrying only what the deterministic
84
88
  * transforms read. Callers (api FieldMeta, web StudioField) structurally
@@ -88,8 +92,6 @@ declare const renderComposite: (parts: readonly PartConfig[], format: string, pa
88
92
  type DeterministicFieldConfig = {
89
93
  path: string;
90
94
  inputType?: string | undefined;
91
- parts?: readonly PartConfig[] | undefined;
92
- format?: string | undefined;
93
95
  formula?: string | undefined;
94
96
  dateFormat?: FieldDateFormat | undefined;
95
97
  };
@@ -99,16 +101,13 @@ type DeterministicFieldConfig = {
99
101
  * no deterministic transform (a scalar, lookup, or AI field the CALLER renders
100
102
  * itself).
101
103
  *
102
- * Dispatch order mirrors the API fill pipeline (composite → formula → date):
103
- * - composite (parts + format present) → {@link renderComposite}
104
- * - else formula present → {@link evaluateNumericExpression}, stringified
104
+ * Dispatch order mirrors the API fill pipeline (formula → date):
105
+ * - formula present → {@link evaluateNumericExpression}, stringified
105
106
  * - else date (inputType "date" + dateFormat) → {@link formatDate}
106
107
  * - else null
107
108
  *
108
- * Composite returns null when its value is not an object of part values (a
109
- * plain string passes through unchanged on the api side, so the caller's
110
- * scalar path handles it). Formula and date return null when the expression /
111
- * value does not yield a value, so the caller leaves the field as-is.
109
+ * Formula and date return null when the expression or value does not yield a
110
+ * value, so the caller leaves the field as-is.
112
111
  */
113
112
  declare const renderDeterministicFieldValue: (field: DeterministicFieldConfig, values: Record<string, unknown>) => string | null;
114
113
  //#endregion
@@ -122,22 +121,47 @@ declare const serializeCondition: (node: ConditionNode$1) => string;
122
121
  //#endregion
123
122
  //#region src/markers.d.ts
124
123
  /**
125
- * Canonical grammar for `{{...}}` template markers — the single source of truth
126
- * shared by the fill pipeline (api), the editor highlighter (folio), and the
127
- * read-only preview (web).
124
+ * Canonical grammar for the docxtpl dialect of Jinja — the single source of
125
+ * truth shared by the fill pipeline (api), the editor highlighter (folio), and
126
+ * the read-only preview (web).
127
+ *
128
+ * Two spans carry meaning: an output marker `{{ ... }}` prints a value, and a
129
+ * tag `{% ... %}` opens or closes a block. A tag may carry a docxtpl placement
130
+ * prefix — `{%p ... %}` owns its paragraph, `{%tr ... %}` owns its table row —
131
+ * and a bare tag is placed by the leniency rules the consumer applies.
128
132
  *
129
133
  * Add a new directive to `MarkerMeta` and teach {@link classifyMarker} to
130
134
  * recognize it. Compile-time totality checks then require its ordered runtime
131
135
  * value and its block/inline disposition, while exhaustive consumer switches
132
136
  * fail until they handle it.
133
137
  *
134
- * No consumer should define its own `{{...}}` regex. The low-level pattern
138
+ * No consumer should define its own marker regex. The low-level pattern
135
139
  * factories below exist so handlers that mutate raw XML/text reuse the exact
136
140
  * same literals; recognizers should prefer the high-level scanner.
137
141
  */
142
+ /** A literal an author can write inside a filter or function call. */
143
+ type MarkerLiteral = string | number | boolean;
144
+ /** One argument of a filter call: positional, or `name=value`. */
145
+ type FilterArgument = {
146
+ kind: "positional";
147
+ value: MarkerLiteral;
148
+ } | {
149
+ kind: "keyword";
150
+ name: string;
151
+ value: MarkerLiteral;
152
+ };
153
+ /** One `| name` / `| name(args)` step of an output marker's filter chain. */
154
+ type FilterCall = {
155
+ name: FilterName;
156
+ args: readonly FilterArgument[];
157
+ };
158
+ /** The loop properties addressable inside a `{% for %}` body. */
159
+ declare const LOOP_PROPERTIES: readonly ["index", "index0", "first", "last", "length"];
160
+ type LoopProperty = (typeof LOOP_PROPERTIES)[number];
138
161
  type MarkerMeta = {
139
162
  kind: "placeholder";
140
163
  expr: string;
164
+ filters: readonly FilterCall[];
141
165
  } | {
142
166
  kind: "clause";
143
167
  name: string;
@@ -149,134 +173,377 @@ type MarkerMeta = {
149
173
  kind: "ref";
150
174
  key: string;
151
175
  } | {
152
- kind: "index";
153
- } | {
154
- kind: "count";
176
+ kind: "loop";
177
+ property: LoopProperty;
155
178
  } | {
156
179
  kind: "if";
157
180
  expr: string;
158
181
  } | {
159
- kind: "elseif";
182
+ kind: "elif";
160
183
  expr: string;
161
184
  } | {
162
185
  kind: "else";
163
186
  } | {
164
187
  kind: "endif";
165
188
  } | {
166
- kind: "each";
167
- expr: string;
189
+ kind: "for";
190
+ alias: string;
191
+ path: string;
192
+ /** Filters on the loop path configure the ARRAY: how many rows it takes
193
+ * and what to call it. */
194
+ filters: readonly FilterCall[];
168
195
  } | {
169
- kind: "endeach";
196
+ kind: "endfor";
170
197
  };
171
198
  type DirectiveKind = MarkerMeta["kind"];
172
- declare const DIRECTIVE_KINDS: readonly ["placeholder", "clause", "num", "ref", "index", "count", "if", "elseif", "else", "endif", "each", "endeach"];
199
+ declare const DIRECTIVE_KINDS: readonly ["placeholder", "clause", "num", "ref", "loop", "if", "elif", "else", "endif", "for", "endfor"];
173
200
  declare const DIRECTIVE_PLACEMENT: {
174
201
  readonly placeholder: "inline";
175
202
  readonly clause: "inline";
176
203
  readonly num: "inline";
177
204
  readonly ref: "inline";
178
- readonly index: "inline";
179
- readonly count: "inline";
205
+ readonly loop: "inline";
180
206
  readonly if: "block";
181
- readonly elseif: "block";
207
+ readonly elif: "block";
182
208
  readonly else: "block";
183
209
  readonly endif: "block";
184
- readonly each: "block";
185
- readonly endeach: "block";
210
+ readonly for: "block";
211
+ readonly endfor: "block";
186
212
  };
187
213
  type BlockDirectiveKind = { [TKind in DirectiveKind]: (typeof DIRECTIVE_PLACEMENT)[TKind] extends "block" ? TKind : never; }[DirectiveKind];
188
214
  /** Directives that occupy their own paragraph, in canonical grammar order. */
189
- declare const BLOCK_DIRECTIVE_KINDS: readonly ["if", "elseif", "else", "endif", "each", "endeach"];
215
+ declare const BLOCK_DIRECTIVE_KINDS: readonly ["if", "elif", "else", "endif", "for", "endfor"];
190
216
  declare const isBlockDirectiveKind: (kind: unknown) => kind is BlockDirectiveKind;
191
- /** Any `{{...}}` marker; group 1 is the inner text (untrimmed — callers trim). */
217
+ /**
218
+ * Every filter the dialect accepts on a fillable value. The names are the
219
+ * grammar's; which manifest property each one writes is decided by the field
220
+ * catalogue in the api, which is total over BOTH this list and the manifest
221
+ * keys, so neither side can gain an entry the other has not decided.
222
+ */
223
+ declare const FILTER_NAMES: readonly ["text", "number", "date", "checkbox", "select", "options_from", "label", "hint", "required", "pattern", "min", "max", "min_length", "max_length", "min_items", "max_items", "ai", "lookup", "matter", "contact", "party", "attorney", "firm", "formula", "condition"];
224
+ type FilterName = (typeof FILTER_NAMES)[number];
225
+ /** Normalise one marker's inner text: typographic quotes become straight
226
+ * quotes, fixed-width spaces become ordinary ones. */
227
+ declare const normalizeMarkerInner: (inner: string) => string;
228
+ /**
229
+ * What an output marker holds between its braces: anything but a brace or a
230
+ * double quote, plus whole double-quoted arguments, whose braces are content
231
+ * rather than the marker's own delimiters — `pattern("^[0-9]{5}$")` is a regex,
232
+ * not a nested marker. An apostrophe stays an ordinary character, so
233
+ * `label("Owner's")` reads as it always did.
234
+ *
235
+ * A quoted run honours `\`-escapes, the way the argument scanner reads them,
236
+ * so `label("she said \"yes\"")` is one span rather than three: the span and
237
+ * the scanner have to agree on where a literal ends or a marker the scanner
238
+ * would parse is one the recognizer never sees.
239
+ *
240
+ * The alternatives cannot overlap (the plain one excludes every character the
241
+ * quoted one may start with, and inside a quoted run the escape branch is the
242
+ * only one that starts with a backslash), so the scan stays linear on
243
+ * adversarial input: a `{{` with no closing `}}` costs one failed pass, not a
244
+ * polynomial retry.
245
+ */
246
+ declare const MARKER_OUTPUT_BODY: string;
247
+ /** What a tag holds between `{%` and `%}`: anything but a brace, and a `%`
248
+ * only where it does not close the tag. */
249
+ declare const MARKER_STATEMENT_BODY: string;
250
+ /**
251
+ * Any marker span: an output `{{...}}` (group `output`) or a tag `{%...%}`
252
+ * (group `statement`, with the docxtpl placement in group `prefix`).
253
+ */
192
254
  declare const markerPattern: () => RegExp;
193
- /** Inline field / clause / numbering marker (inner allows the directive sigils). */
255
+ /** Inline value marker, including its filter chain; group `name` is the path
256
+ * the value comes from and group `filters` the untouched chain. */
194
257
  declare const placeholderPattern: () => RegExp;
195
- /** `{{@clause:Name}}` / `{{@clause:Name:v3}}` — `name` and `modifier` groups. */
258
+ /** `{{ clause("Name") }}` / `{{ clause("Name", "v3") }}`. */
196
259
  declare const clauseSlotPattern: () => RegExp;
197
- /** `{{@num:Key}}` — the `key` group. */
260
+ /** `{{ num("key") }}` — the `key` group. */
198
261
  declare const numPattern: () => RegExp;
199
- /** `{{@ref:Key}}` — the `key` group. */
262
+ /** `{{ ref("key") }}` — the `key` group. */
200
263
  declare const refPattern: () => RegExp;
201
- /** `{{@index}}` — the 1-based position within the innermost enclosing loop. */
202
- declare const indexPattern: () => RegExp;
203
- /** `{{@count}}` — the item count of the innermost enclosing loop. */
204
- declare const countPattern: () => RegExp;
264
+ /** `{{ loop.index }}` and its siblings — the `property` group. */
265
+ declare const loopPattern: () => RegExp;
205
266
  /** Cheap presence test for any numbering marker (no capture). */
206
267
  declare const hasNumberingPattern: () => RegExp;
207
- /** Cheap presence test for any block directive (no capture). */
268
+ /** Cheap presence test for any tag (no capture). */
208
269
  declare const hasBlockDirectivePattern: () => RegExp;
209
- /** A whole line that is a single block directive — `tag` and `expr` groups. */
270
+ /** A whole line that is a single block tag — `prefix`, `tag` and `expr`. */
210
271
  declare const blockDirectiveLinePattern: () => RegExp;
211
272
  /** Whether `value` is a valid field path per the marker grammar (dotted
212
273
  * segments of letters/digits/underscore/dash — no brackets or spaces). */
213
274
  declare const isFieldPath: (value: string) => boolean;
214
275
  /** Whether `value` is safe to use as a dotted object path. */
215
276
  declare const isSafeFieldPath: (value: string) => boolean;
216
- /** Whether `value` is a valid name for authoring a `{{@clause:NAME}}` slot:
217
- * the intersection of the clause-slot parse grammar and the fill pipeline's
218
- * placeholder charset, so every accepted name round-trips through fill. */
277
+ /** Whether `value` is a valid name for authoring a `clause("NAME")` slot. */
219
278
  declare const isClauseSlotName: (value: string) => boolean;
279
+ /** Which brace pair carried the marker text. */
280
+ type MarkerForm = "output" | "statement";
281
+ /** Where a tag's docxtpl prefix says the tag belongs. `"none"` is a bare tag,
282
+ * placed by the consumer's leniency rules, and every output marker. */
283
+ type MarkerPrefix = "none" | "paragraph" | "row";
220
284
  /**
221
- * Classify the inner text of a `{{...}}` marker. Returns `null` when the text is
222
- * not a recognized directive (so a recognizer can leave stray braces alone).
285
+ * Classify the inner text of one marker. Returns `null` when the text is not a
286
+ * recognized directive (so a recognizer can leave stray braces alone).
223
287
  */
224
- declare const classifyMarker: (innerRaw: string) => MarkerMeta | null;
288
+ declare const classifyMarker: (innerRaw: string, form?: MarkerForm) => MarkerMeta | null;
225
289
  type ScannedMarker = {
226
- /** Offset of `{{` in the source text. */
290
+ /** Offset of the opening brace pair in the source text. */
227
291
  start: number;
228
- /** Offset just past `}}`. */
292
+ /** Offset just past the closing brace pair. */
229
293
  end: number;
230
- /** The full matched marker, e.g. `{{@num:scope}}`. */
294
+ /** The full matched marker, e.g. `{% endfor %}`. */
231
295
  raw: string;
232
- /** The (untrimmed) inner text. */
296
+ /** The normalized, trimmed inner text. */
233
297
  inner: string;
298
+ form: MarkerForm;
299
+ /** The docxtpl placement prefix the author wrote. */
300
+ prefix: MarkerPrefix;
234
301
  meta: MarkerMeta;
235
302
  };
236
303
  /**
237
- * Find every recognized `{{...}}` marker in `text`, in document order, with
238
- * offsets and classified metadata. Unrecognized `{{...}}` spans are skipped.
304
+ * Find every recognized marker in `text`, in document order, with offsets and
305
+ * classified metadata. Unrecognized spans are skipped.
239
306
  */
240
307
  declare const scanMarkers: (text: string) => ScannedMarker[];
241
- /** A `{{...}}` span that looks like a marker but classifies to nothing. */
308
+ /**
309
+ * Rewrite every `{{ ... }}` output marker in `text` through `replace`, which
310
+ * receives the whole marker and its raw inner text and returns the replacement.
311
+ * Tags are left untouched. The single scan is the same one the classifier uses,
312
+ * so a caller that rewrites marker text never needs its own regex.
313
+ */
314
+ declare const replaceOutputMarkers: (text: string, replace: (raw: string, inner: string) => string) => string;
315
+ /**
316
+ * The values-map key one marker substitutes from, or `null` when the marker is
317
+ * not a substitution target (a tag, or a `loop.*` token the loop expander
318
+ * resolves first). One function so the key discovery writes is the key the
319
+ * patcher reads.
320
+ */
321
+ declare const substitutionKey: (meta: MarkerMeta) => string | null;
322
+ /** The values-map key for one clause slot. */
323
+ declare const clauseSlotKey: (name: string, version?: string) => string;
324
+ /** A marker-shaped span that classifies to nothing. */
242
325
  type InvalidMarker = {
243
- /** Offset of `{{` in the source text. */
326
+ /** Offset of the opening brace pair in the source text. */
244
327
  start: number;
245
- /** Offset just past `}}`. */
328
+ /** Offset just past the closing brace pair. */
246
329
  end: number;
247
330
  /** The full matched span, e.g. `{{my field}}`. */
248
331
  raw: string;
249
- /** The (trimmed) inner text. */
332
+ /** The normalized, trimmed inner text. */
250
333
  inner: string;
334
+ form: MarkerForm;
251
335
  };
252
336
  /**
253
- * Find every `{{...}}` span whose inner text is NOT a recognized directive —
254
- * near-misses an author clearly meant as markers but that every recognizer
255
- * skips, so they print literally at fill time (e.g. `{{my field}}` with a
256
- * space, `{{@clause:}}` with no name). Offsets and raw text are shaped like
257
- * {@link scanMarkers} results, minus the classified `meta`. This is the exact
337
+ * Find every marker-shaped span whose inner text is NOT a recognized
338
+ * directive — near-misses an author clearly meant as markers but that every
339
+ * recognizer skips, so they print literally at fill time. This is the exact
258
340
  * complement of {@link scanMarkers}: a span is in one list or the other.
259
341
  */
260
342
  declare const scanInvalidMarkers: (text: string) => InvalidMarker[];
343
+ /** The loop alias the codemod (and the legacy-marker message) generates for a
344
+ * `{{#each path}}`: the singular of the last plain-word segment, else `item`. */
345
+ declare const legacyLoopAlias: (path: string) => string;
346
+ /** Translate an old-dialect condition expression to the Jinja surface:
347
+ * `!a` becomes `not a`, `a contains "x"` becomes `"x" in a`. */
348
+ declare const translateLegacyExpression: (expr: string) => string;
261
349
  /**
262
- * Why a `{{...}}` span misses the grammar in a way that names the authoring
263
- * mistake rather than merely failing to classify. Consumers derive their own
264
- * warning codes from {@link MARKER_DEFECT_KINDS} so the two never drift.
350
+ * The Jinja marker that replaces one old-dialect marker's inner text, or
351
+ * `null` when the text is not old-dialect. Shared by the rejection message and
352
+ * the codemod, so the fix an author is told to make is the fix the script
353
+ * would have made.
265
354
  */
266
- type MarkerDefectKind = "unknown_directive" | "bracket_index";
267
- declare const MARKER_DEFECT_KINDS: readonly ["unknown_directive", "bracket_index"];
355
+ declare const legacyMarkerReplacement: (innerRaw: string) => string | null;
268
356
  /**
269
- * Name the authoring mistake behind one marker's inner text, or `null` when the
270
- * text is a recognized directive ({@link classifyMarker} accepts it) or an
271
- * unrecognizable span with no specific diagnosis (`{{my field}}`). The
272
- * recognized-directive check is what makes `DIRECTIVE_KINDS` the single
273
- * authority on which `{{#...}}` tokens exist: adding a directive to the grammar
274
- * stops it being reported here, with no list to update.
357
+ * Why a marker-shaped span misses the grammar in a way that names the
358
+ * authoring mistake rather than merely failing to classify. Consumers derive
359
+ * their own warning codes from {@link MARKER_DEFECT_KINDS} so the two never
360
+ * drift.
275
361
  */
276
- declare const classifyMarkerDefect: (innerRaw: string) => MarkerDefectKind | null;
362
+ type MarkerDefectKind = "legacy_marker" | "unsupported_tag" | "unknown_filter" | "python_expression" | "bracket_index";
363
+ declare const MARKER_DEFECT_KINDS: readonly ["legacy_marker", "unsupported_tag", "unknown_filter", "python_expression", "bracket_index"];
364
+ /** One diagnosis of a rejected span: the kind plus the construct to quote. */
365
+ type MarkerDefect = {
366
+ kind: MarkerDefectKind;
367
+ /** The construct the author wrote that the grammar refuses. */
368
+ construct: string;
369
+ /** The Jinja that replaces it, when the mistake has one exact fix. */
370
+ replacement?: string;
371
+ };
372
+ /**
373
+ * Name the authoring mistake behind one marker's inner text, or `null` when
374
+ * the text is a recognized directive ({@link classifyMarker} accepts it) or an
375
+ * unrecognizable span with no specific diagnosis (`{{my field}}`).
376
+ */
377
+ declare const classifyMarkerDefect: (innerRaw: string, form?: MarkerForm) => MarkerDefect | null;
277
378
  /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
278
379
  declare const assertNever: (value: never) => never;
279
380
  //#endregion
381
+ //#region src/loop-scope.d.ts
382
+ /**
383
+ * The path a marker names, once the loops around it are taken into account.
384
+ *
385
+ * `{% for attorney in attorneys %}{{ attorney.name }}{% endfor %}` writes one
386
+ * field, and the manifest calls it `attorneys.name`: the loop's alias is the
387
+ * body's name for the item, the array path is the manifest's. Discovery reads
388
+ * that mapping, and every writer that puts a configuration back into a marker
389
+ * has to reverse it, so the rule lives beside the grammar rather than in each
390
+ * of them; two readings of one loop are two different documents.
391
+ */
392
+ /** One enclosing `{% for %}`, as the body sees it and as the manifest does. */
393
+ type RowScope = {
394
+ /** The loop variable the body addresses items through. */
395
+ alias: string;
396
+ /** The array path the opener was written with, before its own enclosing
397
+ * loops were applied. */
398
+ declaredPath: string;
399
+ /** The array path the manifest speaks, enclosing loops included. */
400
+ scopedPath: string;
401
+ };
402
+ declare const rowScopePaths: (rowScopes: readonly RowScope[]) => string[];
403
+ /** A path already under the innermost repeat is left alone; anything else is
404
+ * read as a name relative to it. */
405
+ declare const qualifyRowScopedPath: (path: string, rowPaths?: readonly string[]) => string;
406
+ /** The manifest path for a marker written inside these loops. */
407
+ declare const qualifyRowScopedPlaceholder: (path: string, rowScopes: readonly RowScope[]) => string;
408
+ /** The manifest path for an array a `{% for %}` opens inside these loops. */
409
+ declare const qualifyLoopPath: (declaredPath: string, rowScopes: readonly RowScope[]) => string;
410
+ //#endregion
411
+ //#region src/field-config.d.ts
412
+ /** Where a field's value comes from, when a record supplies it. Only the shape
413
+ * the filters read: each caller's own union satisfies it. */
414
+ type MarkerFieldSource = {
415
+ kind: "matter";
416
+ field: string;
417
+ } | {
418
+ kind: "contact";
419
+ field: string;
420
+ } | {
421
+ kind: "firm";
422
+ field: string;
423
+ } | {
424
+ kind: "party";
425
+ role: string;
426
+ field: string;
427
+ } | {
428
+ kind: "attorney";
429
+ ref: string;
430
+ field: string;
431
+ };
432
+ /** The constraints a chain can express. */
433
+ type MarkerFieldValidation = {
434
+ required?: boolean | undefined;
435
+ pattern?: string | undefined;
436
+ min?: number | undefined;
437
+ max?: number | undefined;
438
+ minLength?: number | undefined;
439
+ maxLength?: number | undefined;
440
+ minItems?: number | undefined;
441
+ maxItems?: number | undefined;
442
+ };
443
+ /** A registry lookup and the renderings of its one resolved hit. */
444
+ type MarkerFieldLookup = {
445
+ registry: string;
446
+ formats: readonly {
447
+ key: string;
448
+ template: string;
449
+ }[];
450
+ };
451
+ /** The input controls a marker can name. */
452
+ type MarkerFieldInputType = "text" | "number" | "boolean" | "date" | "select";
453
+ /**
454
+ * Everything about a field that a marker's filter chain can carry. A caller's
455
+ * own field type may hold more (a derived condition AST, a discovery count);
456
+ * those keys are simply not read here.
457
+ */
458
+ type MarkerFieldConfig = {
459
+ path: string;
460
+ label?: string | undefined;
461
+ hint?: string | undefined;
462
+ inputType?: MarkerFieldInputType | undefined;
463
+ options?: readonly string[] | undefined;
464
+ optionsFrom?: string | undefined;
465
+ validation?: MarkerFieldValidation | undefined;
466
+ required?: boolean | undefined;
467
+ aiPrompt?: string | undefined;
468
+ aiAdapt?: boolean | undefined;
469
+ aiSeesDocument?: boolean | undefined;
470
+ lookup?: MarkerFieldLookup | undefined;
471
+ source?: MarkerFieldSource | undefined;
472
+ formula?: string | undefined;
473
+ condition?: string | undefined;
474
+ dateFormat?: FieldDateFormat | undefined;
475
+ };
476
+ /**
477
+ * The filters that describe a REPEAT rather than a value: how many rows it
478
+ * takes and what to call it. These are the ones a `{% for %}` tag may carry,
479
+ * since the loop path names the array, not one of its values.
480
+ */
481
+ declare const ARRAY_FILTER_NAMES: readonly ["label", "hint", "required", "min_items", "max_items"];
482
+ declare const isArrayFilterName: (name: FilterName) => boolean;
483
+ /**
484
+ * The filter chain that declares this field in a document, in the catalogue's
485
+ * own order so a chain the writer produces and one an author wrote read the
486
+ * same.
487
+ */
488
+ declare const filtersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
489
+ /**
490
+ * The chain a `{% for %}` opener carries for this array: the repeat's own
491
+ * filters and nothing else, derived from {@link ARRAY_FILTER_NAMES}, so a
492
+ * value filter can never be written where the reader refuses it.
493
+ */
494
+ declare const arrayFiltersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
495
+ //#endregion
496
+ //#region src/render.d.ts
497
+ /** Why a value has no spelling the scanner reads back as itself. */
498
+ type UnwritableReason = "tag-delimiter" | "number-spelling";
499
+ /** The reason this literal cannot be written in a marker of this form, or
500
+ * `null` when it can. A boolean always can; a string can in a `{{ }}` marker,
501
+ * because the writer quotes and escapes it. */
502
+ declare const unwritableMarkerLiteral: (value: MarkerLiteral, form?: MarkerForm) => UnwritableReason | null;
503
+ /** Every value in a chain the grammar has no spelling for, with the filter it
504
+ * was written on and why, so a caller can refuse a configuration before it
505
+ * rewrites a document into something the scanner would read differently. A
506
+ * repeat's chain goes in a tag, so pass `"statement"` for one. */
507
+ declare const unwritableFilterValues: (filters: readonly FilterCall[], form?: MarkerForm) => {
508
+ filter: string;
509
+ value: MarkerLiteral;
510
+ reason: UnwritableReason;
511
+ }[];
512
+ /**
513
+ * One filter chain as marker text, without the leading pipe: `label("A") |
514
+ * required`. An empty chain renders the empty string, which is what a marker
515
+ * that configures nothing carries.
516
+ */
517
+ declare const renderFilterChain: (filters: readonly FilterCall[]) => string;
518
+ /** One value marker: `{{ path }}` or `{{ path | filters }}`. */
519
+ declare const renderValueMarker: (path: string, filters: readonly FilterCall[]) => string;
520
+ type ConditionTagOptions = {
521
+ /** Which branch opener this is; `elif` keeps its place in the chain. */
522
+ kind: "if" | "elif";
523
+ /** The `{% if %}` expression, already serialized. */
524
+ expression: string;
525
+ /** The placement the tag already carried. */
526
+ prefix: MarkerPrefix;
527
+ };
528
+ /**
529
+ * One `{% if %}` / `{% elif %}` opener carrying its expression.
530
+ *
531
+ * A rule that decides whether a block shows belongs in the tag that shows it:
532
+ * the tag is the only place a document has for it, so a builder's rule is
533
+ * serialized here rather than stored beside the document under a name.
534
+ */
535
+ declare const renderConditionTag: ({ expression, kind, prefix }: ConditionTagOptions) => string;
536
+ type ForOpenerOptions = {
537
+ alias: string;
538
+ path: string;
539
+ filters: readonly FilterCall[];
540
+ /** The placement the opener already carried, so rewriting its filters does
541
+ * not move a row-form loop off its table row. */
542
+ prefix: MarkerPrefix;
543
+ };
544
+ /** One `{% for %}` opener, keeping its placement prefix. */
545
+ declare const renderForOpener: ({ alias, filters, path, prefix }: ForOpenerOptions) => string;
546
+ //#endregion
280
547
  //#region src/row-blocks.d.ts
281
548
  /** One end of a row block, addressed within the row that was scanned. */
282
549
  type RowBlockMarker = {
@@ -300,7 +567,7 @@ type RowBlockPair = {
300
567
  * paragraph, they are a matching opener/closer pair, the closer sits in a LATER
301
568
  * cell than the opener, the opener prefixes its cell and the closer suffixes
302
569
  * its cell. Everything else — one half of a pair, two nested pairs, a stray
303
- * `{{#else}}`, a pair wrapping the paragraphs of a single cell — is left to the
570
+ * `{% else %}`, a pair wrapping the paragraphs of a single cell — is left to the
304
571
  * engine's existing structure errors rather than guessed at.
305
572
  */
306
573
  declare const detectRowBlockPair: (cells: readonly (readonly string[])[]) => RowBlockPair | null;
@@ -308,11 +575,11 @@ declare const detectRowBlockPair: (cells: readonly (readonly string[])[]) => Row
308
575
  //#region src/index.d.ts
309
576
  type NamedCondition = {
310
577
  name: string;
311
- /** The `{{#if}}` expression string. Authoritative unless `node` is set. */
578
+ /** The `{% if %}` expression string. Authoritative unless `node` is set. */
312
579
  expression: string;
313
580
  /**
314
581
  * The canonical AST, set when the condition cannot round-trip through the
315
- * expression string — i.e. it uses a `formula` operand the `{{#if}}` grammar
582
+ * expression string — i.e. it uses a `formula` operand the `{% if %}` grammar
316
583
  * has no syntax for. When present it is evaluated directly and `expression`
317
584
  * is ignored.
318
585
  */
@@ -326,16 +593,17 @@ type NamedCondition = {
326
593
  */
327
594
  declare const MAX_CONDITION_DEPTH = 50;
328
595
  /**
329
- * Evaluate a template `{{#if ...}}` condition. The expression is parsed into
596
+ * Evaluate a template `{% if ... %}` condition. The expression is parsed into
330
597
  * the canonical `@stll/conditions` AST and evaluated by the shared evaluator.
331
598
  *
332
- * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), `contains`,
333
- * truthiness, negation (`!`), logical operators (`and`, `or`), parentheses,
334
- * dotted paths, numeric underscores, and named-condition references.
599
+ * Supports comparisons (`==`, `!=`, `>`, `<`, `>=`, `<=`), membership
600
+ * (`"x" in path`), presence (`is defined` / `is not defined`), truthiness,
601
+ * negation (`not`), logical operators (`and`, `or`), parentheses, dotted
602
+ * paths, numeric underscores, and named-condition references.
335
603
  *
336
604
  * An empty or unparseable expression is `false` ("no condition" never gates a
337
605
  * branch true).
338
606
  */
339
607
  declare const evaluateCondition: (expression: string, data: Record<string, unknown>, namedConditions?: readonly NamedCondition[], _resolved?: ReadonlySet<string>, _depth?: number) => boolean;
340
608
  //#endregion
341
- export { BLOCK_DIRECTIVE_KINDS, type BlockDirectiveKind, type CompareNode, type CompareOp, type Condition, type ConditionNode, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, type DateFormatStyle, type DeterministicFieldConfig, type DirectiveKind, type FieldDateFormat, type GroupNode, type InvalidMarker, MARKER_DEFECT_KINDS, MAX_CONDITION_DEPTH, type MarkerDefectKind, type MarkerMeta, NUMERIC_FUNCTION_NAMES, NamedCondition, type NumericFunctionName, type Operand, type PartConfig, type PredicateNode, type PredicateOp, type RowBlockMarker, type RowBlockPair, type ScannedMarker, 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 };
609
+ export { ARRAY_FILTER_NAMES, BLOCK_DIRECTIVE_KINDS, type BlockDirectiveKind, type CompareNode, type CompareOp, type Condition, type ConditionNode, type ConditionTagOptions, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, type DateFormatStyle, type DeterministicFieldConfig, type DirectiveKind, FILTER_NAMES, type FieldDateFormat, type FilterArgument, type FilterCall, type FilterName, type ForOpenerOptions, type GroupNode, type InvalidMarker, LOOP_PROPERTIES, type LoopProperty, MARKER_DEFECT_KINDS, MARKER_OUTPUT_BODY, MARKER_STATEMENT_BODY, MAX_CONDITION_DEPTH, type MarkerDefect, type MarkerDefectKind, type MarkerFieldConfig, type MarkerFieldInputType, type MarkerFieldLookup, type MarkerFieldSource, type MarkerFieldValidation, type MarkerForm, type MarkerLiteral, type MarkerMeta, type MarkerPrefix, NUMERIC_FUNCTION_NAMES, NamedCondition, type NumericFunctionName, type Operand, type PredicateNode, type PredicateOp, type RowBlockMarker, type RowBlockPair, type RowScope, type ScannedMarker, type UnwritableReason, 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 };