@stll/template-conditions 0.5.3 → 0.6.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.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { CompareNode, CompareOp, Condition, ConditionNode, ConditionNode as ConditionNode$1, GroupNode, Operand, PredicateNode, PredicateOp, conditionHasFormula, conditionNodeSchema, conditionSchema, emptyCondition } from "@stll/conditions";
2
2
  //#region src/path.d.ts
3
3
  /** Resolve a dotted path like `company.name` against data. */
4
- declare const resolvePath: (path: string, data: Record<string, unknown>) => unknown;
4
+ export declare const resolvePath: (path: string, data: Record<string, unknown>) => unknown;
5
5
  //#endregion
6
6
  //#region src/parse.d.ts
7
7
  /** Bare words the tokenizer reads as operators or literals; a data path can
8
8
  * never be one of them. */
9
- declare const CONDITION_RESERVED_WORDS: readonly ["and", "or", "not", "in", "is", "defined", "true", "false"];
10
- declare const parseCondition: (expression: string) => ConditionNode$1 | null;
9
+ export declare const CONDITION_RESERVED_WORDS: readonly ["and", "or", "not", "in", "is", "defined", "true", "false"];
10
+ export declare const parseCondition: (expression: string) => ConditionNode$1 | null;
11
11
  //#endregion
12
12
  //#region src/referenced-paths.d.ts
13
13
  /**
@@ -23,7 +23,7 @@ declare const parseCondition: (expression: string) => ConditionNode$1 | null;
23
23
  * deciding what a condition depends on must not read that as "depends on
24
24
  * nothing".
25
25
  */
26
- declare const referencedConditionPaths: (expression: string) => string[] | null;
26
+ export declare const referencedConditionPaths: (expression: string) => string[] | null;
27
27
  //#endregion
28
28
  //#region src/compute.d.ts
29
29
  /**
@@ -43,9 +43,9 @@ declare const referencedConditionPaths: (expression: string) => string[] | null;
43
43
  * non-numeric variable, division producing a non-finite result) so callers
44
44
  * can fall back to leaving the field unfilled rather than emitting `NaN`.
45
45
  */
46
- declare const NUMERIC_FUNCTION_NAMES: readonly ["min", "max", "round", "abs", "floor", "ceil"];
46
+ export declare const NUMERIC_FUNCTION_NAMES: readonly ["min", "max", "round", "abs", "floor", "ceil"];
47
47
  type NumericFunctionName = (typeof NUMERIC_FUNCTION_NAMES)[number];
48
- declare const evaluateNumericExpression: (expression: string, data: Record<string, unknown>) => number | undefined;
48
+ export declare const evaluateNumericExpression: (expression: string, data: Record<string, unknown>) => number | undefined;
49
49
  //#endregion
50
50
  //#region src/field-values.d.ts
51
51
  /**
@@ -67,10 +67,10 @@ declare const evaluateNumericExpression: (expression: string, data: Record<strin
67
67
  * STRUCTURAL shapes so the package stays free of api/web imports; callers pass
68
68
  * their own field objects, which structurally satisfy these.
69
69
  */
70
- declare const DATE_FORMAT_STYLES: readonly ["long", "medium", "short", "iso"];
70
+ export declare const DATE_FORMAT_STYLES: readonly ["long", "medium", "short", "iso"];
71
71
  type DateFormatStyle = (typeof DATE_FORMAT_STYLES)[number];
72
72
  /** Stable exemplar for date-format configuration previews. */
73
- declare const DATE_FORMAT_EXAMPLE_ISO = "2028-06-13";
73
+ export declare const DATE_FORMAT_EXAMPLE_ISO = "2028-06-13";
74
74
  /** Locale-aware date rendering config. "iso" leaves the submitted value as-is;
75
75
  * the other styles map to `Intl.DateTimeFormat` `dateStyle`. */
76
76
  type FieldDateFormat = {
@@ -85,7 +85,7 @@ type FieldDateFormat = {
85
85
  * 2028". The "iso" style returns the value unchanged; an invalid calendar date
86
86
  * returns null.
87
87
  */
88
- declare const formatDate: (value: string, dateFormat: FieldDateFormat) => string | null;
88
+ export declare const formatDate: (value: string, dateFormat: FieldDateFormat) => string | null;
89
89
  /**
90
90
  * Minimal STRUCTURAL config of a field, carrying only what the deterministic
91
91
  * transforms read. Callers (api FieldMeta, web StudioField) structurally
@@ -112,7 +112,7 @@ type DeterministicFieldConfig = {
112
112
  * Formula and date return null when the expression or value does not yield a
113
113
  * value, so the caller leaves the field as-is.
114
114
  */
115
- declare const renderDeterministicFieldValue: (field: DeterministicFieldConfig, values: Record<string, unknown>) => string | null;
115
+ export declare const renderDeterministicFieldValue: (field: DeterministicFieldConfig, values: Record<string, unknown>) => string | null;
116
116
  //#endregion
117
117
  //#region src/condition-builder.d.ts
118
118
  /**
@@ -120,7 +120,7 @@ declare const renderDeterministicFieldValue: (field: DeterministicFieldConfig, v
120
120
  * Returns an empty string for an empty tree (caller treats that as "no
121
121
  * condition" / always-visible).
122
122
  */
123
- declare const serializeCondition: (node: ConditionNode$1) => string;
123
+ export declare const serializeCondition: (node: ConditionNode$1) => string;
124
124
  //#endregion
125
125
  //#region src/markers.d.ts
126
126
  /**
@@ -159,7 +159,7 @@ type FilterCall = {
159
159
  args: readonly FilterArgument[];
160
160
  };
161
161
  /** The loop properties addressable inside a `{% for %}` body. */
162
- declare const LOOP_PROPERTIES: readonly ["index", "index0", "first", "last", "length"];
162
+ export declare const LOOP_PROPERTIES: readonly ["index", "index0", "first", "last", "length"];
163
163
  type LoopProperty = (typeof LOOP_PROPERTIES)[number];
164
164
  type MarkerMeta = {
165
165
  kind: "placeholder";
@@ -181,9 +181,14 @@ type MarkerMeta = {
181
181
  } | {
182
182
  kind: "if";
183
183
  expr: string;
184
+ /** Filters on a tag whose whole body is one field path configure THAT
185
+ * boolean: a question the document only asks has no value marker to
186
+ * carry a chain. A tag holding a real expression carries none. */
187
+ filters: readonly FilterCall[];
184
188
  } | {
185
189
  kind: "elif";
186
190
  expr: string;
191
+ filters: readonly FilterCall[];
187
192
  } | {
188
193
  kind: "else";
189
194
  } | {
@@ -199,7 +204,7 @@ type MarkerMeta = {
199
204
  kind: "endfor";
200
205
  };
201
206
  type DirectiveKind = MarkerMeta["kind"];
202
- declare const DIRECTIVE_KINDS: readonly ["placeholder", "clause", "num", "ref", "loop", "if", "elif", "else", "endif", "for", "endfor"];
207
+ export declare const DIRECTIVE_KINDS: readonly ["placeholder", "clause", "num", "ref", "loop", "if", "elif", "else", "endif", "for", "endfor"];
203
208
  declare const DIRECTIVE_PLACEMENT: {
204
209
  readonly placeholder: "inline";
205
210
  readonly clause: "inline";
@@ -215,19 +220,19 @@ declare const DIRECTIVE_PLACEMENT: {
215
220
  };
216
221
  type BlockDirectiveKind = { [TKind in DirectiveKind]: (typeof DIRECTIVE_PLACEMENT)[TKind] extends "block" ? TKind : never; }[DirectiveKind];
217
222
  /** Directives that occupy their own paragraph, in canonical grammar order. */
218
- declare const BLOCK_DIRECTIVE_KINDS: readonly ["if", "elif", "else", "endif", "for", "endfor"];
219
- declare const isBlockDirectiveKind: (kind: unknown) => kind is BlockDirectiveKind;
223
+ export declare const BLOCK_DIRECTIVE_KINDS: readonly ["if", "elif", "else", "endif", "for", "endfor"];
224
+ export declare const isBlockDirectiveKind: (kind: unknown) => kind is BlockDirectiveKind;
220
225
  /**
221
226
  * Every filter the dialect accepts on a fillable value. The names are the
222
227
  * grammar's; which manifest property each one writes is decided by the field
223
228
  * catalogue in the api, which is total over BOTH this list and the manifest
224
229
  * keys, so neither side can gain an entry the other has not decided.
225
230
  */
226
- 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"];
231
+ export 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"];
227
232
  type FilterName = (typeof FILTER_NAMES)[number];
228
233
  /** Normalise one marker's inner text: typographic quotes become straight
229
234
  * quotes, fixed-width spaces become ordinary ones. */
230
- declare const normalizeMarkerInner: (inner: string) => string;
235
+ export declare const normalizeMarkerInner: (inner: string) => string;
231
236
  /**
232
237
  * What an output marker holds between its braces: anything but a brace or a
233
238
  * double quote, plus whole double-quoted arguments, whose braces are content
@@ -246,39 +251,39 @@ declare const normalizeMarkerInner: (inner: string) => string;
246
251
  * adversarial input: a `{{` with no closing `}}` costs one failed pass, not a
247
252
  * polynomial retry.
248
253
  */
249
- declare const MARKER_OUTPUT_BODY: string;
254
+ export declare const MARKER_OUTPUT_BODY: string;
250
255
  /** What a tag holds between `{%` and `%}`: anything but a brace, and a `%`
251
256
  * only where it does not close the tag. */
252
- declare const MARKER_STATEMENT_BODY: string;
257
+ export declare const MARKER_STATEMENT_BODY: string;
253
258
  /**
254
259
  * Any marker span: an output `{{...}}` (group `output`) or a tag `{%...%}`
255
260
  * (group `statement`, with the docxtpl placement in group `prefix`).
256
261
  */
257
- declare const markerPattern: () => RegExp;
262
+ export declare const markerPattern: () => RegExp;
258
263
  /** Inline value marker, including its filter chain; group `name` is the path
259
264
  * the value comes from and group `filters` the untouched chain. */
260
- declare const placeholderPattern: () => RegExp;
265
+ export declare const placeholderPattern: () => RegExp;
261
266
  /** `{{ clause("Name") }}` / `{{ clause("Name", "v3") }}`. */
262
- declare const clauseSlotPattern: () => RegExp;
267
+ export declare const clauseSlotPattern: () => RegExp;
263
268
  /** `{{ num("key") }}` — the `key` group. */
264
- declare const numPattern: () => RegExp;
269
+ export declare const numPattern: () => RegExp;
265
270
  /** `{{ ref("key") }}` — the `key` group. */
266
- declare const refPattern: () => RegExp;
271
+ export declare const refPattern: () => RegExp;
267
272
  /** `{{ loop.index }}` and its siblings — the `property` group. */
268
- declare const loopPattern: () => RegExp;
273
+ export declare const loopPattern: () => RegExp;
269
274
  /** Cheap presence test for any numbering marker (no capture). */
270
- declare const hasNumberingPattern: () => RegExp;
275
+ export declare const hasNumberingPattern: () => RegExp;
271
276
  /** Cheap presence test for any tag (no capture). */
272
- declare const hasBlockDirectivePattern: () => RegExp;
277
+ export declare const hasBlockDirectivePattern: () => RegExp;
273
278
  /** A whole line that is a single block tag — `prefix`, `tag` and `expr`. */
274
- declare const blockDirectiveLinePattern: () => RegExp;
279
+ export declare const blockDirectiveLinePattern: () => RegExp;
275
280
  /** Whether `value` is a valid field path per the marker grammar (dotted
276
281
  * segments of letters/digits/underscore/dash — no brackets or spaces). */
277
- declare const isFieldPath: (value: string) => boolean;
282
+ export declare const isFieldPath: (value: string) => boolean;
278
283
  /** Whether `value` is safe to use as a dotted object path. */
279
- declare const isSafeFieldPath: (value: string) => boolean;
284
+ export declare const isSafeFieldPath: (value: string) => boolean;
280
285
  /** Whether `value` is a valid name for authoring a `clause("NAME")` slot. */
281
- declare const isClauseSlotName: (value: string) => boolean;
286
+ export declare const isClauseSlotName: (value: string) => boolean;
282
287
  /** Which brace pair carried the marker text. */
283
288
  type MarkerForm = "output" | "statement";
284
289
  /** Where a tag's docxtpl prefix says the tag belongs. `"none"` is a bare tag,
@@ -288,7 +293,7 @@ type MarkerPrefix = "none" | "paragraph" | "row";
288
293
  * Classify the inner text of one marker. Returns `null` when the text is not a
289
294
  * recognized directive (so a recognizer can leave stray braces alone).
290
295
  */
291
- declare const classifyMarker: (innerRaw: string, form?: MarkerForm) => MarkerMeta | null;
296
+ export declare const classifyMarker: (innerRaw: string, form?: MarkerForm) => MarkerMeta | null;
292
297
  type ScannedMarker = {
293
298
  /** Offset of the opening brace pair in the source text. */
294
299
  start: number;
@@ -307,23 +312,23 @@ type ScannedMarker = {
307
312
  * Find every recognized marker in `text`, in document order, with offsets and
308
313
  * classified metadata. Unrecognized spans are skipped.
309
314
  */
310
- declare const scanMarkers: (text: string) => ScannedMarker[];
315
+ export declare const scanMarkers: (text: string) => ScannedMarker[];
311
316
  /**
312
317
  * Rewrite every `{{ ... }}` output marker in `text` through `replace`, which
313
318
  * receives the whole marker and its raw inner text and returns the replacement.
314
319
  * Tags are left untouched. The single scan is the same one the classifier uses,
315
320
  * so a caller that rewrites marker text never needs its own regex.
316
321
  */
317
- declare const replaceOutputMarkers: (text: string, replace: (raw: string, inner: string) => string) => string;
322
+ export declare const replaceOutputMarkers: (text: string, replace: (raw: string, inner: string) => string) => string;
318
323
  /**
319
324
  * The values-map key one marker substitutes from, or `null` when the marker is
320
325
  * not a substitution target (a tag, or a `loop.*` token the loop expander
321
326
  * resolves first). One function so the key discovery writes is the key the
322
327
  * patcher reads.
323
328
  */
324
- declare const substitutionKey: (meta: MarkerMeta) => string | null;
329
+ export declare const substitutionKey: (meta: MarkerMeta) => string | null;
325
330
  /** The values-map key for one clause slot. */
326
- declare const clauseSlotKey: (name: string, version?: string) => string;
331
+ export declare const clauseSlotKey: (name: string, version?: string) => string;
327
332
  /** A marker-shaped span that classifies to nothing. */
328
333
  type InvalidMarker = {
329
334
  /** Offset of the opening brace pair in the source text. */
@@ -342,20 +347,20 @@ type InvalidMarker = {
342
347
  * recognizer skips, so they print literally at fill time. This is the exact
343
348
  * complement of {@link scanMarkers}: a span is in one list or the other.
344
349
  */
345
- declare const scanInvalidMarkers: (text: string) => InvalidMarker[];
350
+ export declare const scanInvalidMarkers: (text: string) => InvalidMarker[];
346
351
  /** The loop alias the codemod (and the legacy-marker message) generates for a
347
352
  * `{{#each path}}`: the singular of the last plain-word segment, else `item`. */
348
- declare const legacyLoopAlias: (path: string) => string;
353
+ export declare const legacyLoopAlias: (path: string) => string;
349
354
  /** Translate an old-dialect condition expression to the Jinja surface:
350
355
  * `!a` becomes `not a`, `a contains "x"` becomes `"x" in a`. */
351
- declare const translateLegacyExpression: (expr: string) => string;
356
+ export declare const translateLegacyExpression: (expr: string) => string;
352
357
  /**
353
358
  * The Jinja marker that replaces one old-dialect marker's inner text, or
354
359
  * `null` when the text is not old-dialect. Shared by the rejection message and
355
360
  * the codemod, so the fix an author is told to make is the fix the script
356
361
  * would have made.
357
362
  */
358
- declare const legacyMarkerReplacement: (innerRaw: string) => string | null;
363
+ export declare const legacyMarkerReplacement: (innerRaw: string) => string | null;
359
364
  /**
360
365
  * Why a marker-shaped span misses the grammar in a way that names the
361
366
  * authoring mistake rather than merely failing to classify. Consumers derive
@@ -363,7 +368,7 @@ declare const legacyMarkerReplacement: (innerRaw: string) => string | null;
363
368
  * drift.
364
369
  */
365
370
  type MarkerDefectKind = "legacy_marker" | "unsupported_tag" | "unknown_filter" | "python_expression" | "bracket_index";
366
- declare const MARKER_DEFECT_KINDS: readonly ["legacy_marker", "unsupported_tag", "unknown_filter", "python_expression", "bracket_index"];
371
+ export declare const MARKER_DEFECT_KINDS: readonly ["legacy_marker", "unsupported_tag", "unknown_filter", "python_expression", "bracket_index"];
367
372
  /** One diagnosis of a rejected span: the kind plus the construct to quote. */
368
373
  type MarkerDefect = {
369
374
  kind: MarkerDefectKind;
@@ -377,9 +382,9 @@ type MarkerDefect = {
377
382
  * the text is a recognized directive ({@link classifyMarker} accepts it) or an
378
383
  * unrecognizable span with no specific diagnosis (`{{my field}}`).
379
384
  */
380
- declare const classifyMarkerDefect: (innerRaw: string, form?: MarkerForm) => MarkerDefect | null;
385
+ export declare const classifyMarkerDefect: (innerRaw: string, form?: MarkerForm) => MarkerDefect | null;
381
386
  /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
382
- declare const assertNever: (value: never) => never;
387
+ export declare const assertNever: (value: never) => never;
383
388
  //#endregion
384
389
  //#region src/loop-scope.d.ts
385
390
  /**
@@ -402,14 +407,14 @@ type RowScope = {
402
407
  /** The array path the manifest speaks, enclosing loops included. */
403
408
  scopedPath: string;
404
409
  };
405
- declare const rowScopePaths: (rowScopes: readonly RowScope[]) => string[];
410
+ export declare const rowScopePaths: (rowScopes: readonly RowScope[]) => string[];
406
411
  /** A path already under the innermost repeat is left alone; anything else is
407
412
  * read as a name relative to it. */
408
- declare const qualifyRowScopedPath: (path: string, rowPaths?: readonly string[]) => string;
413
+ export declare const qualifyRowScopedPath: (path: string, rowPaths?: readonly string[]) => string;
409
414
  /** The manifest path for a marker written inside these loops. */
410
- declare const qualifyRowScopedPlaceholder: (path: string, rowScopes: readonly RowScope[]) => string;
415
+ export declare const qualifyRowScopedPlaceholder: (path: string, rowScopes: readonly RowScope[]) => string;
411
416
  /** The manifest path for an array a `{% for %}` opens inside these loops. */
412
- declare const qualifyLoopPath: (declaredPath: string, rowScopes: readonly RowScope[]) => string;
417
+ export declare const qualifyLoopPath: (declaredPath: string, rowScopes: readonly RowScope[]) => string;
413
418
  //#endregion
414
419
  //#region src/field-config.d.ts
415
420
  /** Where a field's value comes from, when a record supplies it. Only the shape
@@ -481,20 +486,20 @@ type MarkerFieldConfig = {
481
486
  * takes and what to call it. These are the ones a `{% for %}` tag may carry,
482
487
  * since the loop path names the array, not one of its values.
483
488
  */
484
- declare const ARRAY_FILTER_NAMES: readonly ["label", "hint", "required", "min_items", "max_items"];
485
- declare const isArrayFilterName: (name: FilterName) => boolean;
489
+ export declare const ARRAY_FILTER_NAMES: readonly ["label", "hint", "required", "min_items", "max_items"];
490
+ export declare const isArrayFilterName: (name: FilterName) => boolean;
486
491
  /**
487
492
  * The filter chain that declares this field in a document, in the catalogue's
488
493
  * own order so a chain the writer produces and one an author wrote read the
489
494
  * same.
490
495
  */
491
- declare const filtersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
496
+ export declare const filtersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
492
497
  /**
493
498
  * The chain a `{% for %}` opener carries for this array: the repeat's own
494
499
  * filters and nothing else, derived from {@link ARRAY_FILTER_NAMES}, so a
495
500
  * value filter can never be written where the reader refuses it.
496
501
  */
497
- declare const arrayFiltersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
502
+ export declare const arrayFiltersFromFieldConfig: (field: MarkerFieldConfig) => FilterCall[];
498
503
  //#endregion
499
504
  //#region src/render.d.ts
500
505
  /** Why a value has no spelling the scanner reads back as itself. */
@@ -502,12 +507,12 @@ type UnwritableReason = "tag-delimiter" | "number-spelling";
502
507
  /** The reason this literal cannot be written in a marker of this form, or
503
508
  * `null` when it can. A boolean always can; a string can in a `{{ }}` marker,
504
509
  * because the writer quotes and escapes it. */
505
- declare const unwritableMarkerLiteral: (value: MarkerLiteral, form?: MarkerForm) => UnwritableReason | null;
510
+ export declare const unwritableMarkerLiteral: (value: MarkerLiteral, form?: MarkerForm) => UnwritableReason | null;
506
511
  /** Every value in a chain the grammar has no spelling for, with the filter it
507
512
  * was written on and why, so a caller can refuse a configuration before it
508
513
  * rewrites a document into something the scanner would read differently. A
509
514
  * repeat's chain goes in a tag, so pass `"statement"` for one. */
510
- declare const unwritableFilterValues: (filters: readonly FilterCall[], form?: MarkerForm) => {
515
+ export declare const unwritableFilterValues: (filters: readonly FilterCall[], form?: MarkerForm) => {
511
516
  filter: string;
512
517
  value: MarkerLiteral;
513
518
  reason: UnwritableReason;
@@ -517,25 +522,32 @@ declare const unwritableFilterValues: (filters: readonly FilterCall[], form?: Ma
517
522
  * required`. An empty chain renders the empty string, which is what a marker
518
523
  * that configures nothing carries.
519
524
  */
520
- declare const renderFilterChain: (filters: readonly FilterCall[]) => string;
525
+ export declare const renderFilterChain: (filters: readonly FilterCall[]) => string;
521
526
  /** One value marker: `{{ path }}` or `{{ path | filters }}`. */
522
- declare const renderValueMarker: (path: string, filters: readonly FilterCall[]) => string;
527
+ export declare const renderValueMarker: (path: string, filters: readonly FilterCall[]) => string;
523
528
  type ConditionTagOptions = {
524
529
  /** Which branch opener this is; `elif` keeps its place in the chain. */
525
530
  kind: "if" | "elif";
526
531
  /** The `{% if %}` expression, already serialized. */
527
532
  expression: string;
533
+ /** The chain the boolean this tag names carries. Only a tag whose whole
534
+ * expression is one field path reads one back, so an expression tag writes
535
+ * none. */
536
+ filters: readonly FilterCall[];
528
537
  /** The placement the tag already carried. */
529
538
  prefix: MarkerPrefix;
530
539
  };
531
540
  /**
532
- * One `{% if %}` / `{% elif %}` opener carrying its expression.
541
+ * One `{% if %}` / `{% elif %}` opener carrying its expression, and the field
542
+ * configuration of the boolean it asks for.
533
543
  *
534
544
  * A rule that decides whether a block shows belongs in the tag that shows it:
535
545
  * the tag is the only place a document has for it, so a builder's rule is
536
- * serialized here rather than stored beside the document under a name.
546
+ * serialized here rather than stored beside the document under a name. The
547
+ * same is true of what to call the question and how to decide it: a condition
548
+ * the document never prints has no value marker to hold that chain either.
537
549
  */
538
- declare const renderConditionTag: ({ expression, kind, prefix }: ConditionTagOptions) => string;
550
+ export declare const renderConditionTag: ({ expression, filters, kind, prefix }: ConditionTagOptions) => string;
539
551
  type ForOpenerOptions = {
540
552
  alias: string;
541
553
  path: string;
@@ -545,7 +557,7 @@ type ForOpenerOptions = {
545
557
  prefix: MarkerPrefix;
546
558
  };
547
559
  /** One `{% for %}` opener, keeping its placement prefix. */
548
- declare const renderForOpener: ({ alias, filters, path, prefix }: ForOpenerOptions) => string;
560
+ export declare const renderForOpener: ({ alias, filters, path, prefix }: ForOpenerOptions) => string;
549
561
  //#endregion
550
562
  //#region src/row-blocks.d.ts
551
563
  /** One end of a row block, addressed within the row that was scanned. */
@@ -573,10 +585,10 @@ type RowBlockPair = {
573
585
  * `{% else %}`, a pair wrapping the paragraphs of a single cell — is left to the
574
586
  * engine's existing structure errors rather than guessed at.
575
587
  */
576
- declare const detectRowBlockPair: (cells: readonly (readonly string[])[]) => RowBlockPair | null;
588
+ export declare const detectRowBlockPair: (cells: readonly (readonly string[])[]) => RowBlockPair | null;
577
589
  //#endregion
578
590
  //#region src/index.d.ts
579
- type NamedCondition = {
591
+ export type NamedCondition = {
580
592
  name: string;
581
593
  /** The `{% if %}` expression string. Authoritative unless `node` is set. */
582
594
  expression: string;
@@ -594,7 +606,7 @@ type NamedCondition = {
594
606
  * Prevents stack overflow when a long chain of named
595
607
  * conditions (A→B→C→...→Z) is evaluated without cycles.
596
608
  */
597
- declare const MAX_CONDITION_DEPTH = 50;
609
+ export declare const MAX_CONDITION_DEPTH = 50;
598
610
  /**
599
611
  * Evaluate a template `{% if ... %}` condition. The expression is parsed into
600
612
  * the canonical `@stll/conditions` AST and evaluated by the shared evaluator.
@@ -607,6 +619,6 @@ declare const MAX_CONDITION_DEPTH = 50;
607
619
  * An empty or unparseable expression is `false` ("no condition" never gates a
608
620
  * branch true).
609
621
  */
610
- declare const evaluateCondition: (expression: string, data: Record<string, unknown>, namedConditions?: readonly NamedCondition[], _resolved?: ReadonlySet<string>, _depth?: number) => boolean;
622
+ export declare const evaluateCondition: (expression: string, data: Record<string, unknown>, namedConditions?: readonly NamedCondition[], _resolved?: ReadonlySet<string>, _depth?: number) => boolean;
611
623
  //#endregion
612
- export { ARRAY_FILTER_NAMES, BLOCK_DIRECTIVE_KINDS, type BlockDirectiveKind, CONDITION_RESERVED_WORDS, 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 };
624
+ export { type BlockDirectiveKind, type CompareNode, type CompareOp, type Condition, type ConditionNode, type ConditionTagOptions, type DateFormatStyle, type DeterministicFieldConfig, type DirectiveKind, type FieldDateFormat, type FilterArgument, type FilterCall, type FilterName, type ForOpenerOptions, type GroupNode, type InvalidMarker, type LoopProperty, type MarkerDefect, type MarkerDefectKind, type MarkerFieldConfig, type MarkerFieldInputType, type MarkerFieldLookup, type MarkerFieldSource, type MarkerFieldValidation, type MarkerForm, type MarkerLiteral, type MarkerMeta, type MarkerPrefix, type NumericFunctionName, type Operand, type PredicateNode, type PredicateOp, type RowBlockMarker, type RowBlockPair, type RowScope, type ScannedMarker, type UnwritableReason, conditionHasFormula, conditionNodeSchema, conditionSchema, emptyCondition };
package/dist/index.js CHANGED
@@ -945,6 +945,32 @@ const readPrefix = (token) => token === "p" || token === "tr" ? PREFIX_BY_TOKEN[
945
945
  const TAG_RE = /^(?<tag>[\p{L}_][\p{L}\p{N}_]*)\b(?<rest>[\s\S]*)$/u;
946
946
  const FOR_RE = /^(?<alias>[\p{L}_][\p{L}\p{N}_-]*)\s+in\s+(?<path>[\p{L}\p{N}_.-]+)\s*(?<filters>\|[\s\S]*)?$/u;
947
947
  const CALL_RE = /^(?<name>[\p{L}_][\p{L}\p{N}_]*)\s*\(/u;
948
+ const CONDITION_RE = /^(?<path>[\p{L}\p{N}_.-]+)\s*(?<filters>\|[\s\S]*)$/u;
949
+ /**
950
+ * Read an `{% if %}` / `{% elif %}` body: one field path plus the chain that
951
+ * configures the boolean it names, or an expression the author wrote.
952
+ *
953
+ * A tail the filter catalogue does not read is part of that expression
954
+ * (`items|length > 0`), never a configuration, so the body stays verbatim
955
+ * rather than being rejected: a tag the grammar refuses would take its whole
956
+ * block with it.
957
+ */
958
+ const readConditionBody = (rest) => {
959
+ const match = CONDITION_RE.exec(rest);
960
+ if (!match) return {
961
+ expr: rest,
962
+ filters: []
963
+ };
964
+ const chain = scanFilterChain(match.groups?.["filters"] ?? "");
965
+ if (chain === null || "unknownFilter" in chain || chain.filters.length === 0) return {
966
+ expr: rest,
967
+ filters: []
968
+ };
969
+ return {
970
+ expr: match.groups?.["path"] ?? rest,
971
+ filters: chain.filters
972
+ };
973
+ };
948
974
  /** Classify a `{% ... %}` tag. */
949
975
  const classifyStatement = (inner) => {
950
976
  const match = TAG_RE.exec(inner);
@@ -952,14 +978,22 @@ const classifyStatement = (inner) => {
952
978
  const tag = match.groups?.["tag"] ?? "";
953
979
  const rest = (match.groups?.["rest"] ?? "").trim();
954
980
  switch (tag) {
955
- case "if": return {
956
- kind: "if",
957
- expr: rest
958
- };
959
- case "elif": return {
960
- kind: "elif",
961
- expr: rest
962
- };
981
+ case "if": {
982
+ const { expr, filters } = readConditionBody(rest);
983
+ return {
984
+ kind: "if",
985
+ expr,
986
+ filters
987
+ };
988
+ }
989
+ case "elif": {
990
+ const { expr, filters } = readConditionBody(rest);
991
+ return {
992
+ kind: "elif",
993
+ expr,
994
+ filters
995
+ };
996
+ }
963
997
  case "else": return rest === "" ? { kind: "else" } : null;
964
998
  case "endif": return rest === "" ? { kind: "endif" } : null;
965
999
  case "endfor": return rest === "" ? { kind: "endfor" } : null;
@@ -1466,13 +1500,20 @@ const PREFIX_TOKEN = {
1466
1500
  row: "tr"
1467
1501
  };
1468
1502
  /**
1469
- * One `{% if %}` / `{% elif %}` opener carrying its expression.
1503
+ * One `{% if %}` / `{% elif %}` opener carrying its expression, and the field
1504
+ * configuration of the boolean it asks for.
1470
1505
  *
1471
1506
  * A rule that decides whether a block shows belongs in the tag that shows it:
1472
1507
  * the tag is the only place a document has for it, so a builder's rule is
1473
- * serialized here rather than stored beside the document under a name.
1508
+ * serialized here rather than stored beside the document under a name. The
1509
+ * same is true of what to call the question and how to decide it: a condition
1510
+ * the document never prints has no value marker to hold that chain either.
1474
1511
  */
1475
- const renderConditionTag = ({ expression, kind, prefix }) => `{%${PREFIX_TOKEN[prefix]} ${kind} ${expression} %}`;
1512
+ const renderConditionTag = ({ expression, filters, kind, prefix }) => {
1513
+ const chain = renderFilterChain(filters);
1514
+ const head = `{%${PREFIX_TOKEN[prefix]} ${kind} ${expression}`;
1515
+ return chain === "" ? `${head} %}` : `${head} | ${chain} %}`;
1516
+ };
1476
1517
  /** One `{% for %}` opener, keeping its placement prefix. */
1477
1518
  const renderForOpener = ({ alias, filters, path, prefix }) => {
1478
1519
  const chain = renderFilterChain(filters);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/template-conditions",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "A document-template marker and condition engine: {{...}} marker grammar, parse, compute, serialize, field values.",
5
5
  "keywords": [
6
6
  "condition",
@@ -49,7 +49,7 @@
49
49
  "prepack": "bun run build"
50
50
  },
51
51
  "dependencies": {
52
- "@stll/conditions": "^0.3.1",
52
+ "@stll/conditions": "^0.4.0",
53
53
  "better-result": "3.0.1",
54
54
  "temporal-polyfill": "1.0.4"
55
55
  },
@@ -58,7 +58,7 @@
58
58
  "@stll/typescript-config": "0.0.0",
59
59
  "bun-types": "1.4.2",
60
60
  "fast-check": "^4.9.0",
61
- "tsdown": "0.22.14"
61
+ "tsdown": "0.23.0"
62
62
  },
63
63
  "main": "./dist/index.js",
64
64
  "types": "./dist/index.d.ts"