@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 +45 -1
- package/dist/index.js +131 -1
- package/package.json +2 -2
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.
|
|
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.
|
|
53
|
+
"better-result": "3.0.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@stll/property-testing": "0.0.0",
|