@stll/template-conditions 0.2.2 → 0.3.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
@@ -258,9 +258,53 @@ type InvalidMarker = {
258
258
  * complement of {@link scanMarkers}: a span is in one list or the other.
259
259
  */
260
260
  declare const scanInvalidMarkers: (text: string) => InvalidMarker[];
261
+ /**
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.
265
+ */
266
+ type MarkerDefectKind = "unknown_directive" | "bracket_index";
267
+ declare const MARKER_DEFECT_KINDS: readonly ["unknown_directive", "bracket_index"];
268
+ /**
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.
275
+ */
276
+ declare const classifyMarkerDefect: (innerRaw: string) => MarkerDefectKind | null;
261
277
  /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
262
278
  declare const assertNever: (value: never) => never;
263
279
  //#endregion
280
+ //#region src/row-blocks.d.ts
281
+ /** One end of a row block, addressed within the row that was scanned. */
282
+ type RowBlockMarker = {
283
+ /** Index into the row's cells. */
284
+ cellIndex: number;
285
+ /** Index into that cell's paragraphs. */
286
+ paragraphIndex: number;
287
+ marker: ScannedMarker;
288
+ };
289
+ type RowBlockPair = {
290
+ open: RowBlockMarker;
291
+ close: RowBlockMarker;
292
+ };
293
+ /**
294
+ * The one row block a table row declares in the lenient form, or `null`.
295
+ *
296
+ * `cells` is the row's cells in document order, each as its paragraph texts in
297
+ * document order.
298
+ *
299
+ * A row qualifies when exactly two block markers reach outside their own
300
+ * paragraph, they are a matching opener/closer pair, the closer sits in a LATER
301
+ * cell than the opener, the opener prefixes its cell and the closer suffixes
302
+ * 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
304
+ * engine's existing structure errors rather than guessed at.
305
+ */
306
+ declare const detectRowBlockPair: (cells: readonly (readonly string[])[]) => RowBlockPair | null;
307
+ //#endregion
264
308
  //#region src/index.d.ts
265
309
  type NamedCondition = {
266
310
  name: string;
@@ -294,4 +338,4 @@ declare const MAX_CONDITION_DEPTH = 50;
294
338
  */
295
339
  declare const evaluateCondition: (expression: string, data: Record<string, unknown>, namedConditions?: readonly NamedCondition[], _resolved?: ReadonlySet<string>, _depth?: number) => boolean;
296
340
  //#endregion
297
- 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, MAX_CONDITION_DEPTH, type MarkerMeta, NUMERIC_FUNCTION_NAMES, NamedCondition, type NumericFunctionName, type Operand, type PartConfig, type PredicateNode, type PredicateOp, type ScannedMarker, assertNever, blockDirectiveLinePattern, classifyMarker, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, countPattern, emptyCondition, evaluateCondition, evaluateNumericExpression, formatDate, hasBlockDirectivePattern, hasNumberingPattern, indexPattern, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, markerPattern, numPattern, parseCondition, placeholderPattern, refPattern, renderComposite, renderDeterministicFieldValue, resolvePath, scanInvalidMarkers, scanMarkers, serializeCondition };
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 };
package/dist/index.js CHANGED
@@ -549,6 +549,26 @@ const scanInvalidMarkers = (text) => {
549
549
  }
550
550
  return out;
551
551
  };
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
+ /**
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;
571
+ };
552
572
  /** Exhaustiveness guard — pass the discriminant in a `switch` default branch. */
553
573
  const assertNever = (value) => panic(`Unhandled template directive: ${JSON.stringify(value)}`);
554
574
  //#endregion
@@ -740,6 +760,116 @@ const serializeNode = (node, top) => {
740
760
  */
741
761
  const serializeCondition = (node) => serializeNode(node, true);
742
762
  //#endregion
763
+ //#region src/row-blocks.ts
764
+ /**
765
+ * Row-form block markers: the lenient placement authoring agents actually
766
+ * write for a "one row per item" table.
767
+ *
768
+ * The canonical placement gives a block marker its own paragraph. In a table
769
+ * that costs an extra empty-looking paragraph in the first and last cell, so
770
+ * an author instead types the opener in front of the first cell's text and the
771
+ * closer behind the last cell's text:
772
+ *
773
+ * | {{#each deliverables}}{{deliverables.item}} | {{deliverables.fee}}{{/each}} |
774
+ *
775
+ * Both placements mean the same thing — the `w:tr` is the unit — so this
776
+ * module recognizes the row form purely from text, and the consumer rewrites
777
+ * it into the canonical form before any engine runs.
778
+ *
779
+ * Text-only and structure-free on purpose: the fill/discovery pipeline feeds
780
+ * it OOXML run text, the authoring eval feeds it the cells a model produced,
781
+ * and neither can drift from the other's idea of what a row block is.
782
+ */
783
+ const CLOSER_OF = {
784
+ each: "endeach",
785
+ if: "endif"
786
+ };
787
+ const isOpenKind = (kind) => kind === "each" || kind === "if";
788
+ /**
789
+ * 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
792
+ * over can reach across cells.
793
+ *
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
796
+ * row block are hoisted, so a branch marker stranded between them would stay
797
+ * buried in the cell text: a false condition would drop the whole row and the
798
+ * branch that should have rendered with it, and a true one would leave the
799
+ * marker behind as an inline orphan. Counting it here refuses the placement
800
+ * instead.
801
+ *
802
+ * A paragraph that is nothing but a directive is the canonical form and
803
+ * belongs to the block engine, so it contributes nothing here.
804
+ */
805
+ const danglingBlockMarkers = (text) => {
806
+ if (blockDirectiveLinePattern().test(text)) return [];
807
+ const open = [];
808
+ const dangling = [];
809
+ for (const marker of scanMarkers(text)) {
810
+ const { kind } = marker.meta;
811
+ if (isOpenKind(kind)) {
812
+ open.push({
813
+ kind,
814
+ marker,
815
+ branches: []
816
+ });
817
+ continue;
818
+ }
819
+ if (kind === "endeach" || kind === "endif") {
820
+ const innermost = open.at(-1);
821
+ if (innermost && CLOSER_OF[innermost.kind] === kind) open.pop();
822
+ else dangling.push(marker);
823
+ continue;
824
+ }
825
+ if (kind === "elseif" || kind === "else") {
826
+ const innermost = open.at(-1);
827
+ if (innermost?.kind === "if") innermost.branches.push(marker);
828
+ else dangling.push(marker);
829
+ }
830
+ }
831
+ for (const { branches, marker } of open) dangling.push(marker, ...branches);
832
+ return dangling.toSorted((a, b) => a.start - b.start);
833
+ };
834
+ const firstContentParagraph = (paragraphs) => paragraphs.findIndex((text) => text.trim() !== "");
835
+ const lastContentParagraph = (paragraphs) => paragraphs.findLastIndex((text) => text.trim() !== "");
836
+ /** The marker stands in front of everything the cell says. */
837
+ const prefixesCell = (paragraphs, { marker, paragraphIndex }) => paragraphIndex === firstContentParagraph(paragraphs) && (paragraphs[paragraphIndex] ?? "").slice(0, marker.start).trim() === "";
838
+ /** The marker stands behind everything the cell says. */
839
+ const suffixesCell = (paragraphs, { marker, paragraphIndex }) => paragraphIndex === lastContentParagraph(paragraphs) && (paragraphs[paragraphIndex] ?? "").slice(marker.end).trim() === "";
840
+ /**
841
+ * The one row block a table row declares in the lenient form, or `null`.
842
+ *
843
+ * `cells` is the row's cells in document order, each as its paragraph texts in
844
+ * document order.
845
+ *
846
+ * A row qualifies when exactly two block markers reach outside their own
847
+ * paragraph, they are a matching opener/closer pair, the closer sits in a LATER
848
+ * cell than the opener, the opener prefixes its cell and the closer suffixes
849
+ * 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
851
+ * engine's existing structure errors rather than guessed at.
852
+ */
853
+ const detectRowBlockPair = (cells) => {
854
+ const located = [];
855
+ for (const [cellIndex, paragraphs] of cells.entries()) for (const [paragraphIndex, text] of paragraphs.entries()) for (const marker of danglingBlockMarkers(text)) located.push({
856
+ cellIndex,
857
+ paragraphIndex,
858
+ marker
859
+ });
860
+ if (located.length !== 2) return null;
861
+ const [open, close] = located;
862
+ if (!open || !close) return null;
863
+ const openKind = open.marker.meta.kind;
864
+ if (!isOpenKind(openKind) || close.marker.meta.kind !== CLOSER_OF[openKind]) return null;
865
+ if (open.cellIndex >= close.cellIndex) return null;
866
+ if (!prefixesCell(cells[open.cellIndex] ?? [], open) || !suffixesCell(cells[close.cellIndex] ?? [], close)) return null;
867
+ return {
868
+ open,
869
+ close
870
+ };
871
+ };
872
+ //#endregion
743
873
  //#region src/index.ts
744
874
  /**
745
875
  * Shared condition evaluation + marker grammar for DOCX templates. Small,
@@ -813,4 +943,4 @@ const evaluateCondition = (expression, data, namedConditions, _resolved = /* @__
813
943
  return evaluateCondition$1(node, makeResolver(data, namedConditions, _resolved, _depth));
814
944
  };
815
945
  //#endregion
816
- export { BLOCK_DIRECTIVE_KINDS, DATE_FORMAT_EXAMPLE_ISO, DATE_FORMAT_STYLES, DIRECTIVE_KINDS, MAX_CONDITION_DEPTH, NUMERIC_FUNCTION_NAMES, assertNever, blockDirectiveLinePattern, classifyMarker, clauseSlotPattern, conditionHasFormula, conditionNodeSchema, conditionSchema, countPattern, emptyCondition, evaluateCondition, evaluateNumericExpression, formatDate, hasBlockDirectivePattern, hasNumberingPattern, indexPattern, isBlockDirectiveKind, isClauseSlotName, isFieldPath, isSafeFieldPath, markerPattern, numPattern, parseCondition, placeholderPattern, refPattern, renderComposite, renderDeterministicFieldValue, resolvePath, scanInvalidMarkers, scanMarkers, serializeCondition };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/template-conditions",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "A document-template marker and condition engine: {{...}} marker grammar, parse, compute, serialize, field values.",
5
5
  "keywords": [
6
6
  "condition",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@stll/conditions": "^0.3.1",
53
- "better-result": "3.0.0"
53
+ "better-result": "3.0.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@stll/property-testing": "0.0.0",