@stll/folio-core 0.58.4 → 0.59.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.
Files changed (101) hide show
  1. package/dist/ai-edits/apply.d.ts +11 -12
  2. package/dist/ai-edits/apply.js +179 -168
  3. package/dist/ai-edits/headless.d.ts +15 -13
  4. package/dist/ai-edits/headless.js +40 -13
  5. package/dist/ai-edits/index.d.ts +3 -3
  6. package/dist/ai-edits/newListNumbering.d.ts +3 -9
  7. package/dist/ai-edits/newListNumbering.js +36 -41
  8. package/dist/ai-edits/referenceClassification.d.ts +22 -0
  9. package/dist/ai-edits/referenceClassification.js +21 -0
  10. package/dist/ai-edits/snapshot.js +39 -40
  11. package/dist/ai-edits/types.d.ts +24 -32
  12. package/dist/compare/column-alignment.d.ts +4 -4
  13. package/dist/compare/compare.d.ts +3 -7
  14. package/dist/compare/compare.js +25 -27
  15. package/dist/compare/content-alignment.d.ts +9 -9
  16. package/dist/compare/content-types.d.ts +23 -10
  17. package/dist/compare/content-types.js +14 -0
  18. package/dist/compare/content.d.ts +16 -17
  19. package/dist/compare/content.js +30 -7
  20. package/dist/compare/formatting.d.ts +3 -3
  21. package/dist/compare/import-style-closure.d.ts +22 -0
  22. package/dist/compare/import-style-closure.js +33 -0
  23. package/dist/compare/inserted-resource-snapshot.d.ts +15 -0
  24. package/dist/compare/inserted-resource-snapshot.js +63 -0
  25. package/dist/compare/plan.js +20 -13
  26. package/dist/compare/scenario.js +12 -2
  27. package/dist/compare/style-resources.d.ts +8 -2
  28. package/dist/compare/style-resources.js +63 -14
  29. package/dist/compare/verification.d.ts +15 -3
  30. package/dist/compare/verification.js +47 -8
  31. package/dist/compat/eigenpal.d.ts +4 -4
  32. package/dist/controller/canonicalListRendering.js +13 -22
  33. package/dist/controller/canonicalPublicOperations.js +1 -1
  34. package/dist/controller/canonicalStructure.js +45 -21
  35. package/dist/document-operations.d.ts +4 -9
  36. package/dist/document-operations.js +45 -41
  37. package/dist/docx/attributeRemainder.d.ts +1 -1
  38. package/dist/docx/index.d.ts +1 -1
  39. package/dist/docx/listMarkerComputation.js +9 -14
  40. package/dist/docx/listTemplate.d.ts +22 -0
  41. package/dist/docx/listTemplate.js +40 -0
  42. package/dist/docx/numberingParser.js +2 -2
  43. package/dist/docx/numberingReferenceNormalization.js +3 -3
  44. package/dist/docx/paragraphParser.js +3 -4
  45. package/dist/docx/rezip.js +2 -2
  46. package/dist/docx/server/materializeYjsDocx.js +2 -2
  47. package/dist/docx/server/migrateYjsAttrSchema.js +5 -5
  48. package/dist/generated/text_shaper.js +8 -4
  49. package/dist/generated/text_shaper_bg.wasm +0 -0
  50. package/dist/index.d.ts +4 -4
  51. package/dist/internal/paragraphFormattingSerialization.d.ts +1 -8
  52. package/dist/internal/paragraphFormattingSerialization.js +6 -20
  53. package/dist/layout-bridge/convert/flowConversionShared.d.ts +1 -1
  54. package/dist/layout-bridge/convert/listMarkers.d.ts +1 -2
  55. package/dist/layout-bridge/convert/listMarkers.js +3 -11
  56. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +1 -1
  57. package/dist/layout-bridge/convert/paragraphAttrs.js +8 -4
  58. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +2 -1
  59. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -1
  60. package/dist/markdown/renderParagraph.js +29 -3
  61. package/dist/prosemirror/commands/comments.js +5 -32
  62. package/dist/prosemirror/commands/paragraphBookmarkJoin.d.ts +7 -1
  63. package/dist/prosemirror/commands/paragraphBookmarkJoin.js +35 -1
  64. package/dist/prosemirror/commands/propertyChangeScope.js +6 -0
  65. package/dist/prosemirror/commands/resolveParagraphProperties.js +10 -5
  66. package/dist/prosemirror/conversion/fromProseDoc.js +23 -9
  67. package/dist/prosemirror/conversion/toProseDoc.js +31 -7
  68. package/dist/prosemirror/extensions/core/ParagraphExtension.js +29 -14
  69. package/dist/prosemirror/extensions/features/ListExtension.js +51 -21
  70. package/dist/prosemirror/listInstanceReferences.js +21 -15
  71. package/dist/prosemirror/listLabels.d.ts +4 -3
  72. package/dist/prosemirror/listLabels.js +18 -51
  73. package/dist/prosemirror/listMarker.d.ts +2 -19
  74. package/dist/prosemirror/listMarker.js +28 -57
  75. package/dist/prosemirror/listNumbering.js +16 -6
  76. package/dist/prosemirror/listRendering.d.ts +4 -3
  77. package/dist/prosemirror/listRendering.js +22 -3
  78. package/dist/prosemirror/listRenderingAttrs.d.ts +2 -1
  79. package/dist/prosemirror/listRenderingAttrs.js +6 -11
  80. package/dist/prosemirror/numberedRefFields.js +9 -5
  81. package/dist/prosemirror/numberingAttr.d.ts +19 -1
  82. package/dist/prosemirror/numberingAttr.js +16 -2
  83. package/dist/prosemirror/paragraphIndentation.js +5 -2
  84. package/dist/prosemirror/paragraphMarkJoin.js +17 -3
  85. package/dist/prosemirror/paragraphPropertyCarry.js +5 -6
  86. package/dist/prosemirror/plugins/transactionInvariants.js +6 -2
  87. package/dist/prosemirror/schema/nodes.d.ts +2 -1
  88. package/dist/prosemirror/selectionState.js +5 -1
  89. package/dist/prosemirror/storyListNumbering.js +11 -12
  90. package/dist/prosemirror/styles/paragraphStyleCascade.js +2 -6
  91. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +11 -4
  92. package/dist/prosemirror/styles/resolvedStyleAttrs.js +28 -18
  93. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  94. package/dist/prosemirror/yjsDocumentMetadata.js +42 -10
  95. package/dist/redline.d.ts +17 -1
  96. package/dist/redline.js +118 -57
  97. package/dist/server.d.ts +3 -3
  98. package/dist/server.js +2 -2
  99. package/dist/version-comparison.d.ts +1 -2
  100. package/dist/version-comparison.js +1 -2
  101. package/package.json +4 -3
@@ -3,8 +3,10 @@ import { outlineLevelFromAttrValue } from "../prosemirror/outlineLevelAttr.js";
3
3
  import { paragraphIndentationEqual } from "../prosemirror/paragraphIndentation.js";
4
4
  import { PARAGRAPH_ALIGNMENT_VALUES } from "../types/documentEnumValues.js";
5
5
  import { alignFolioContentStructure, contentBlocksShareContainer, createFolioContentAlignmentWorkSession } from "./content-alignment.js";
6
+ import { sameFolioContentStatedNumbering } from "./content-types.js";
6
7
  import { inlineFormattingSegments } from "./formatting.js";
7
8
  import { Result, TaggedError, panic } from "better-result";
9
+ import { isNumberingReference } from "@stll/docx-core/model";
8
10
  //#region src/compare/content.ts
9
11
  /**
10
12
  * Pure comparison of two ordered, representation-neutral content snapshots.
@@ -68,10 +70,29 @@ const limitExceeded = ({ input, limit, maximum, actual, blockIndex, field }) =>
68
70
  ...field !== void 0 && { field }
69
71
  });
70
72
  const isFiniteInteger = (value) => typeof value === "number" && Number.isSafeInteger(value);
73
+ const isNonNegativeInteger = (value) => isFiniteInteger(value) && value >= 0;
71
74
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
72
75
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
76
+ const validStatedNumbering = (value) => {
77
+ if (!isRecord(value) || typeof value["kind"] !== "string") return false;
78
+ switch (value["kind"]) {
79
+ case "inherit":
80
+ case "none": return true;
81
+ case "levelOnly": return isFiniteInteger(value["ilvl"]) && value["ilvl"] >= 0;
82
+ case "reference": return isNonNegativeInteger(value["numId"]) && isNumberingReference(value["numId"]) && (value["ilvl"] === void 0 || isFiniteInteger(value["ilvl"]) && value["ilvl"] >= 0);
83
+ default: return false;
84
+ }
85
+ };
73
86
  const hasRecordShape = (value) => isRecord(value);
74
- const PARAGRAPH_ALIGNMENTS = new Set(PARAGRAPH_ALIGNMENT_VALUES);
87
+ const PARAGRAPH_ALIGNMENTS$1 = new Set(PARAGRAPH_ALIGNMENT_VALUES);
88
+ const isParagraphContentBlock = (block) => {
89
+ switch (block.kind) {
90
+ case "paragraph":
91
+ case "heading":
92
+ case "listItem": return "statedNumbering" in block;
93
+ default: return false;
94
+ }
95
+ };
75
96
  const validateRunFormatting = (block, side, blockIndex) => {
76
97
  for (const [runIndex, run] of (block.previewRuns ?? []).entries()) {
77
98
  if (run.directFormatting !== void 0 && !isRecord(run.directFormatting)) return invalidInput(side, `blocks[${String(blockIndex)}].previewRuns[${String(runIndex)}].directFormatting`, "Direct inline formatting must be an object.", blockIndex);
@@ -100,8 +121,12 @@ const validateRunFormatting = (block, side, blockIndex) => {
100
121
  const validateParagraphFormatting = (block, side, blockIndex) => {
101
122
  for (const property of ["styleId", "displayLabel"]) if (block[property] !== void 0 && typeof block[property] !== "string") return invalidInput(side, `blocks[${String(blockIndex)}].${property}`, "Optional block labels and style ids must be strings.", blockIndex);
102
123
  if (block.headingLevel !== void 0 && (!isFiniteInteger(block.headingLevel) || block.headingLevel < 1)) return invalidInput(side, `blocks[${String(blockIndex)}].headingLevel`, "Heading levels must be positive integers.", blockIndex);
103
- if (block.listLevel !== void 0 && (!isFiniteInteger(block.listLevel) || block.listLevel < 0)) return invalidInput(side, `blocks[${String(blockIndex)}].listLevel`, "List levels must be non-negative integers.", blockIndex);
104
- if (block.directAlignment !== void 0 && !PARAGRAPH_ALIGNMENTS.has(block.directAlignment)) return invalidInput(side, `blocks[${String(blockIndex)}].directAlignment`, "Paragraph alignment is not recognized.", blockIndex);
124
+ if (block.kind === "diagnostic") {
125
+ for (const field of ["statedNumbering", "listReference"]) if (Object.hasOwn(block, field)) return invalidInput(side, `blocks[${String(blockIndex)}].${field}`, "Diagnostics cannot state paragraph numbering.", blockIndex);
126
+ return null;
127
+ }
128
+ if (!isParagraphContentBlock(block) || !validStatedNumbering(block.statedNumbering)) return invalidInput(side, `blocks[${String(blockIndex)}].statedNumbering`, "Stated paragraph numbering must be a recognized authored override or inherit.", blockIndex);
129
+ if (block.directAlignment !== void 0 && !PARAGRAPH_ALIGNMENTS$1.has(block.directAlignment)) return invalidInput(side, `blocks[${String(blockIndex)}].directAlignment`, "Paragraph alignment is not recognized.", blockIndex);
105
130
  if (block.directOutlineLevel !== void 0 && outlineLevelFromAttrValue(block.directOutlineLevel) === null) return invalidInput(side, `blocks[${String(blockIndex)}].directOutlineLevel`, "Direct outline level is not recognized.", blockIndex);
106
131
  const spacing = block.directSpacing;
107
132
  if (spacing === void 0) return null;
@@ -411,13 +436,11 @@ const withTextOffsets = (segments) => {
411
436
  };
412
437
  const paragraphSpacingEqual = (base, revised) => base?.spaceBefore === revised?.spaceBefore && base?.spaceAfter === revised?.spaceAfter && base?.lineSpacing === revised?.lineSpacing && base?.lineSpacingRule === revised?.lineSpacingRule && base?.beforeAutospacing === revised?.beforeAutospacing && base?.afterAutospacing === revised?.afterAutospacing;
413
438
  const changedFolioContentParagraphFormatting = (base, revised) => {
439
+ if (!isParagraphContentBlock(base) || !isParagraphContentBlock(revised)) return null;
414
440
  const patch = {};
415
441
  if ((base.styleId ?? null) !== (revised.styleId ?? null)) patch.styleId = revised.styleId ?? null;
416
442
  if (!sameFolioContentOutlineLevel(base.directOutlineLevel, revised.directOutlineLevel)) patch.outlineLevel = revised.directOutlineLevel ?? null;
417
- const listReferenceChanged = base.listReference?.numId !== revised.listReference?.numId;
418
- const targetOmitsLevelOnChangedReference = listReferenceChanged && revised.listReference !== void 0 && revised.listLevel === void 0;
419
- if (base.listLevel !== revised.listLevel || targetOmitsLevelOnChangedReference) patch.listLevel = revised.listLevel ?? null;
420
- if (listReferenceChanged || base.listLevel !== revised.listLevel && revised.listLevel === void 0 && revised.listReference !== void 0) patch.listReference = revised.listReference ?? null;
443
+ if (!sameFolioContentStatedNumbering(base.statedNumbering, revised.statedNumbering)) patch.numbering = revised.statedNumbering;
421
444
  if (base.directAlignment !== revised.directAlignment) patch.alignment = revised.directAlignment ?? null;
422
445
  if (!paragraphSpacingEqual(base.directSpacing, revised.directSpacing)) patch.spacing = revised.directSpacing ?? null;
423
446
  if (!paragraphIndentationEqual(base.directIndentation, revised.directIndentation)) patch.indentation = revised.directIndentation ?? null;
@@ -1,8 +1,8 @@
1
- import { FolioContentBlock, FolioContentInlineComparisonResult } from "./content-types.js";
1
+ import { FolioContentBlockIdentity, FolioContentInlineComparisonResult } from "./content-types.js";
2
2
  //#region src/compare/formatting.d.ts
3
3
  type InlineFormattingSegmentsOptions = {
4
- baseBlock: FolioContentBlock;
5
- targetBlock: FolioContentBlock;
4
+ baseBlock: FolioContentBlockIdentity;
5
+ targetBlock: FolioContentBlockIdentity;
6
6
  maxSegments: number;
7
7
  };
8
8
  /** Malformed run streams take precedence over the output budget. */
@@ -0,0 +1,22 @@
1
+ import { FolioAIEditSnapshot } from "../ai-edits/types.js";
2
+ import { ImportReferencedStyleDefinitionsResult } from "./style-resources.js";
3
+ import { FolioDocxReviewer } from "../ai-edits/headless.js";
4
+ //#region src/compare/import-style-closure.d.ts
5
+ type ImportStyleClosureWithNumberingOptions = {
6
+ destination: FolioDocxReviewer;
7
+ source: FolioDocxReviewer;
8
+ snapshots: readonly FolioAIEditSnapshot[];
9
+ importedHeaderFooterSnapshots: readonly FolioAIEditSnapshot[];
10
+ numberingReferences: readonly {
11
+ numId: number;
12
+ level: number;
13
+ }[];
14
+ };
15
+ /** Import numbering before styles bind to it, then rebind the source snapshots once. */
16
+ declare const importStyleClosureWithNumbering: ({ destination, source, snapshots, importedHeaderFooterSnapshots, numberingReferences }: ImportStyleClosureWithNumberingOptions) => {
17
+ styleImport: ImportReferencedStyleDefinitionsResult;
18
+ numberingStage: "unchanged" | "staged" | "conflict";
19
+ snapshots: FolioAIEditSnapshot[];
20
+ };
21
+ //#endregion
22
+ export { importStyleClosureWithNumbering };
@@ -0,0 +1,33 @@
1
+ import { getFolioDocxComparisonAccess } from "../ai-edits/headless.js";
2
+ import { remapFolioAIEditSnapshotNumberingReferences, remapFolioAIEditSnapshotStyleReferences } from "../ai-edits/snapshot.js";
3
+ import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
4
+ //#region src/compare/import-style-closure.ts
5
+ /** Import numbering before styles bind to it, then rebind the source snapshots once. */
6
+ const importStyleClosureWithNumbering = ({ destination, source, snapshots, importedHeaderFooterSnapshots, numberingReferences }) => {
7
+ const access = getFolioDocxComparisonAccess(destination);
8
+ const numbering = getFolioDocxComparisonAccess(source).numberingDefinitions();
9
+ const numberingReferenceMap = access.planTargetNumberingReferences(numbering, numberingReferences);
10
+ const numberingStage = numberingReferenceMap === null ? "conflict" : access.stageTargetNumbering(numbering, numberingReferences, numberingReferenceMap);
11
+ const styleImport = access.stageTargetStyles({
12
+ source,
13
+ snapshots,
14
+ importedHeaderFooterSnapshots,
15
+ ...numberingReferenceMap !== null && { numberingReferenceMap }
16
+ });
17
+ return {
18
+ styleImport,
19
+ numberingStage,
20
+ snapshots: snapshots.map((snapshot) => {
21
+ const styleRebound = styleImport.status === "unalignable" ? snapshot : remapFolioAIEditSnapshotStyleReferences({
22
+ snapshot,
23
+ styleIdMap: styleImport.styleIdMap,
24
+ defaultParagraphStyleId: styleImport.defaultParagraphStyleId,
25
+ importedStyleResolver: createStyleResolver(styleImport.styles),
26
+ reconcileAuthoredFormatting: true
27
+ });
28
+ return numberingReferenceMap === null ? styleRebound : remapFolioAIEditSnapshotNumberingReferences(styleRebound, numberingReferenceMap);
29
+ })
30
+ };
31
+ };
32
+ //#endregion
33
+ export { importStyleClosureWithNumbering };
@@ -0,0 +1,15 @@
1
+ import { FolioAIBlock, FolioAIEditSnapshot } from "../ai-edits/types.js";
2
+ //#region src/compare/inserted-resource-snapshot.d.ts
3
+ type CreateInsertedResourceProjectionOptions = {
4
+ base: FolioAIEditSnapshot;
5
+ revised: FolioAIEditSnapshot;
6
+ };
7
+ /** Select resource nodes with their identities from the full revised snapshot. */
8
+ declare const createInsertedResourceProjection: ({ base, revised }: CreateInsertedResourceProjectionOptions) => {
9
+ snapshot: FolioAIEditSnapshot;
10
+ revisedBlockIds: string[];
11
+ };
12
+ /** Subset projection IDs never participate in rebinding the full revised snapshot. */
13
+ declare const rebindInsertedResourceBlocks: (projection: ReturnType<typeof createInsertedResourceProjection>, rebound: FolioAIEditSnapshot) => Map<string, FolioAIBlock>;
14
+ //#endregion
15
+ export { createInsertedResourceProjection, rebindInsertedResourceBlocks };
@@ -0,0 +1,63 @@
1
+ import { createFolioAIEditSnapshotWithStyleResolver, sourceDocumentOf, styleResolverOf } from "../ai-edits/snapshot.js";
2
+ import { alignFolioBlocks } from "../version-comparison.js";
3
+ import { panic } from "better-result";
4
+ import { Fragment } from "prosemirror-model";
5
+ //#region src/compare/inserted-resource-snapshot.ts
6
+ /** Select resource nodes with their identities from the full revised snapshot. */
7
+ const createInsertedResourceProjection = ({ base, revised }) => {
8
+ const document = sourceDocumentOf(revised);
9
+ const insertions = alignFolioBlocks(base.blocks, revised.blocks).flatMap((event) => {
10
+ if (event.type === "pair" || event.type === "baseOnly") return [];
11
+ const anchor = revised.anchors[event.block.id] ?? panic("An inserted block lost its source anchor");
12
+ const node = document.nodeAt(anchor.from) ?? panic("An inserted block lost its source paragraph");
13
+ return [{
14
+ from: anchor.from,
15
+ to: anchor.to,
16
+ node
17
+ }];
18
+ }).toSorted((left, right) => left.from - right.from);
19
+ const carriers = [];
20
+ let projectionFrom = 0;
21
+ for (const insertion of insertions) {
22
+ if (insertion.from < (carriers.at(-1)?.to ?? -1)) continue;
23
+ carriers.push({
24
+ ...insertion,
25
+ projectionFrom
26
+ });
27
+ projectionFrom += insertion.node.nodeSize;
28
+ }
29
+ const revisedIdsByProjectionPosition = /* @__PURE__ */ new Map();
30
+ let carrierIndex = 0;
31
+ for (const block of revised.blocks) {
32
+ const anchor = revised.anchors[block.id];
33
+ if (anchor === void 0) continue;
34
+ while (anchor.from >= (carriers.at(carrierIndex)?.to ?? Number.POSITIVE_INFINITY)) carrierIndex++;
35
+ const carrier = carriers.at(carrierIndex);
36
+ if (carrier === void 0 || anchor.from < carrier.from || anchor.to > carrier.to) continue;
37
+ revisedIdsByProjectionPosition.set(carrier.projectionFrom + anchor.from - carrier.from, block.id);
38
+ }
39
+ const snapshot = createFolioAIEditSnapshotWithStyleResolver(document.copy(Fragment.from(carriers.map(({ node }) => node))), styleResolverOf(revised));
40
+ if (snapshot.blocks.length !== revisedIdsByProjectionPosition.size) return panic("A redline resource projection must preserve its maximal carriers' blocks");
41
+ return {
42
+ snapshot,
43
+ revisedBlockIds: snapshot.blocks.map((block) => {
44
+ const anchor = snapshot.anchors[block.id] ?? panic("A projected resource block lost its anchor");
45
+ return revisedIdsByProjectionPosition.get(anchor.from) ?? panic("A projected resource block lost its carried source identity");
46
+ })
47
+ };
48
+ };
49
+ /** Subset projection IDs never participate in rebinding the full revised snapshot. */
50
+ const rebindInsertedResourceBlocks = (projection, rebound) => {
51
+ if (rebound.blocks.length !== projection.revisedBlockIds.length) return panic("A redline resource import changed the carried insertion count");
52
+ const blocks = /* @__PURE__ */ new Map();
53
+ for (const [index, id] of projection.revisedBlockIds.entries()) {
54
+ const block = rebound.blocks.at(index) ?? panic("A redline import lost a carried insertion");
55
+ blocks.set(id, {
56
+ ...block,
57
+ id
58
+ });
59
+ }
60
+ return blocks;
61
+ };
62
+ //#endregion
63
+ export { createInsertedResourceProjection, rebindInsertedResourceBlocks };
@@ -224,8 +224,7 @@ const shareAContainer = (left, right) => {
224
224
  const toFolioAIBlockParagraphProperties = (properties) => ({
225
225
  ...properties.styleId !== void 0 && { styleId: properties.styleId },
226
226
  ...properties.outlineLevel !== void 0 && { outlineLevel: properties.outlineLevel },
227
- ...properties.listLevel !== void 0 && { listLevel: properties.listLevel },
228
- ...properties.listReference !== void 0 && { numbering: properties.listReference },
227
+ ...properties.numbering !== void 0 && { numbering: properties.numbering },
229
228
  ...properties.alignment !== void 0 && { alignment: properties.alignment },
230
229
  ...properties.spacing !== void 0 && { spacing: properties.spacing },
231
230
  ...properties.indentation !== void 0 && { indentation: properties.indentation }
@@ -489,8 +488,7 @@ const withTrailingDeletionRules = ({ baseSnapshot, targetSnapshot, operations, t
489
488
  text: insert.text,
490
489
  ...insert.moveId !== void 0 && { moveId: insert.moveId },
491
490
  styleId: insert.styleId ?? null,
492
- listLevel: insert.listLevel ?? null,
493
- numbering: insert.numbering ?? null,
491
+ ...insert.numbering !== void 0 && { numbering: insert.numbering },
494
492
  indentation: insert.indentation ?? null,
495
493
  ...insert.lineBreakMode !== void 0 && { lineBreakMode: insert.lineBreakMode },
496
494
  ...insert.hardPageBreak !== void 0 && { hardPageBreak: insert.hardPageBreak },
@@ -592,17 +590,26 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations,
592
590
  */
593
591
  const moveIdOf = (baseBlockId) => `move-${baseBlockId}`;
594
592
  const pushInsertOperation = (block, anchorId) => {
593
+ let paragraph;
594
+ switch (block.kind) {
595
+ case "paragraph":
596
+ case "heading":
597
+ case "listItem":
598
+ paragraph = block;
599
+ break;
600
+ case "diagnostic": return;
601
+ default: return panic("Unhandled comparison block kind", { block });
602
+ }
595
603
  const moveSourceId = moveSourceByTargetBlockId.get(block.id);
596
- const structuralBoundary = block.structuralBoundaries?.at(0);
597
- const hardPageBreak = block.text.length === 0 && block.structuralBoundaries?.length === 1 && structuralBoundary?.type === "pageBreak" && structuralBoundary.offset === 0 ? { ...structuralBoundary.clear !== void 0 && { clear: structuralBoundary.clear } } : void 0;
604
+ const structuralBoundary = paragraph.structuralBoundaries?.at(0);
605
+ const hardPageBreak = block.text.length === 0 && paragraph.structuralBoundaries?.length === 1 && structuralBoundary?.type === "pageBreak" && structuralBoundary.offset === 0 ? { ...structuralBoundary.clear !== void 0 && { clear: structuralBoundary.clear } } : void 0;
598
606
  const paragraphProperties = {
599
- styleId: block.styleId ?? null,
600
- outlineLevel: block.directOutlineLevel ?? null,
601
- listLevel: block.listLevel ?? null,
602
- numbering: block.listReference ?? null,
603
- alignment: block.directAlignment ?? null,
604
- spacing: block.directSpacing ?? null,
605
- indentation: block.directIndentation ?? null
607
+ styleId: paragraph.styleId ?? null,
608
+ outlineLevel: paragraph.directOutlineLevel ?? null,
609
+ numbering: paragraph.statedNumbering,
610
+ alignment: paragraph.directAlignment ?? null,
611
+ spacing: paragraph.directSpacing ?? null,
612
+ indentation: paragraph.directIndentation ?? null
606
613
  };
607
614
  const shared = {
608
615
  text: block.text,
@@ -38,6 +38,15 @@ const EDIT_SCRIPT_UNRESOLVED_REASONS = Object.freeze([
38
38
  "refused"
39
39
  ]);
40
40
  const blockAt = (blocks, index) => Number.isInteger(index) && index >= 0 ? blocks.at(index) : void 0;
41
+ const statedNumberingOf = (block) => {
42
+ switch (block.kind) {
43
+ case "paragraph":
44
+ case "heading":
45
+ case "listItem": return block.statedNumbering;
46
+ case "diagnostic": return;
47
+ default: return panic("Unhandled scenario block kind", { block });
48
+ }
49
+ };
41
50
  const planStep = ({ step, blocks, nextOperationId }) => {
42
51
  const block = blockAt(blocks, step.blockIndex);
43
52
  if (!block) return {
@@ -81,6 +90,7 @@ const planStep = ({ step, blocks, nextOperationId }) => {
81
90
  status: "unresolved",
82
91
  reason: "block-out-of-range"
83
92
  };
93
+ const statedNumbering = statedNumberingOf(block);
84
94
  return {
85
95
  status: "planned",
86
96
  operations: [{
@@ -93,7 +103,7 @@ const planStep = ({ step, blocks, nextOperationId }) => {
93
103
  blockId: destination.id,
94
104
  text: block.text,
95
105
  styleId: block.styleId ?? null,
96
- listLevel: block.listLevel ?? null
106
+ ...statedNumbering !== void 0 && { numbering: statedNumbering }
97
107
  }]
98
108
  };
99
109
  }
@@ -243,7 +253,7 @@ const applyEditScript = async (base, script) => {
243
253
  operations: planned.flatMap(({ operations }) => [...operations])
244
254
  },
245
255
  snapshot,
246
- undefinedStyles: "keep"
256
+ undefinedReferences: "keep"
247
257
  });
248
258
  const skippedIds = new Set(skipped.map(({ id }) => id));
249
259
  const applied = [];
@@ -8,6 +8,7 @@ type ImportReferencedStyleDefinitionsOptions = {
8
8
  referencedStyleIds: readonly string[];
9
9
  reservedStyleIds?: readonly string[];
10
10
  materializeDefaultParagraphStyle?: boolean;
11
+ numberingReferenceMap?: ReadonlyMap<number, number>;
11
12
  };
12
13
  type ImportReferencedStyleDefinitionsResult = {
13
14
  status: "unchanged" | "imported";
@@ -19,12 +20,17 @@ type ImportReferencedStyleDefinitionsResult = {
19
20
  status: "unalignable";
20
21
  detail: string;
21
22
  };
23
+ /** Numbering resources in the same transitive closure the style importer owns. */
24
+ declare const styleClosureNumberingReferences: (styles: document_d_exports.StyleDefinitions | undefined, referencedStyleIds: readonly string[]) => {
25
+ numId: number;
26
+ level: number;
27
+ }[];
22
28
  /**
23
29
  * Add the target style definitions required by a comparison without changing
24
30
  * existing package resources. A referenced style is only portable when both
25
31
  * packages have the same document defaults and theme: those resources take
26
32
  * part in OOXML style resolution even when a style definition itself is new.
27
33
  */
28
- declare const importReferencedStyleDefinitions: ({ sourceStyles, destinationStyles, sourceTheme, destinationTheme, referencedStyleIds, reservedStyleIds, materializeDefaultParagraphStyle }: ImportReferencedStyleDefinitionsOptions) => ImportReferencedStyleDefinitionsResult;
34
+ declare const importReferencedStyleDefinitions: ({ sourceStyles: originalSourceStyles, destinationStyles, sourceTheme, destinationTheme, referencedStyleIds, reservedStyleIds, materializeDefaultParagraphStyle, numberingReferenceMap }: ImportReferencedStyleDefinitionsOptions) => ImportReferencedStyleDefinitionsResult;
29
35
  //#endregion
30
- export { ImportReferencedStyleDefinitionsOptions, ImportReferencedStyleDefinitionsResult, importReferencedStyleDefinitions };
36
+ export { ImportReferencedStyleDefinitionsOptions, ImportReferencedStyleDefinitionsResult, importReferencedStyleDefinitions, styleClosureNumberingReferences };
@@ -2,12 +2,34 @@ import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
2
2
  import { canonicalJson } from "../utils/canonicalJson.js";
3
3
  import { resolveColorToHex } from "../utils/colorResolver.js";
4
4
  import { resolveThemeFont } from "../utils/fontResolver.js";
5
+ import { paragraphNumberingReference } from "@stll/docx-core/model";
5
6
  //#region src/compare/style-resources.ts
6
7
  const styleDependencies = ({ basedOn, link, next }) => [
7
8
  basedOn,
8
9
  link,
9
10
  next
10
11
  ].filter((styleId) => styleId !== void 0);
12
+ /** Numbering resources in the same transitive closure the style importer owns. */
13
+ const styleClosureNumberingReferences = (styles, referencedStyleIds) => {
14
+ const definitions = new Map(styles?.styles.map((style) => [style.styleId, style]));
15
+ const pending = [...referencedStyleIds];
16
+ const seen = /* @__PURE__ */ new Set();
17
+ const references = [];
18
+ while (pending.length > 0) {
19
+ const id = pending.pop();
20
+ if (id === void 0 || seen.has(id)) continue;
21
+ seen.add(id);
22
+ const style = definitions.get(id);
23
+ if (style === void 0) continue;
24
+ const numbering = style.pPr?.numPr;
25
+ if (numbering?.kind === "reference") references.push({
26
+ numId: numbering.numId,
27
+ level: numbering.ilvl ?? 0
28
+ });
29
+ pending.push(...styleDependencies(style));
30
+ }
31
+ return references;
32
+ };
11
33
  const themesMatch = (sourceTheme, destinationTheme) => canonicalJson(sourceTheme) === canonicalJson(destinationTheme);
12
34
  const hasThemeReference = (value) => {
13
35
  if (Array.isArray(value)) return value.some(hasThemeReference);
@@ -205,7 +227,25 @@ const remapImportedStyle = ({ style, styleIdMap, isolatedDefaultStyleIds }) => {
205
227
  * packages have the same document defaults and theme: those resources take
206
228
  * part in OOXML style resolution even when a style definition itself is new.
207
229
  */
208
- const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sourceTheme, destinationTheme, referencedStyleIds, reservedStyleIds = [], materializeDefaultParagraphStyle = false }) => {
230
+ const importReferencedStyleDefinitions = ({ sourceStyles: originalSourceStyles, destinationStyles, sourceTheme, destinationTheme, referencedStyleIds, reservedStyleIds = [], materializeDefaultParagraphStyle = false, numberingReferenceMap }) => {
231
+ const sourceStyles = originalSourceStyles && numberingReferenceMap && numberingReferenceMap.size > 0 ? {
232
+ ...originalSourceStyles,
233
+ styles: originalSourceStyles.styles.map((style) => {
234
+ const numbering = style.pPr?.numPr;
235
+ const numId = numbering?.kind === "reference" ? numberingReferenceMap.get(numbering.numId) : void 0;
236
+ if (numId === void 0 || numbering?.kind !== "reference") return style;
237
+ return {
238
+ ...style,
239
+ pPr: {
240
+ ...style.pPr,
241
+ numPr: paragraphNumberingReference({
242
+ numId,
243
+ ...numbering.ilvl !== void 0 && { ilvl: numbering.ilvl }
244
+ })
245
+ }
246
+ };
247
+ })
248
+ } : originalSourceStyles;
209
249
  const requested = [...new Set(referencedStyleIds)].toSorted();
210
250
  if (requested.length === 0 && !materializeDefaultParagraphStyle) return {
211
251
  status: "unchanged",
@@ -238,7 +278,7 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
238
278
  const source = sourceById.get(styleId);
239
279
  if (!source) return {
240
280
  status: "unalignable",
241
- detail: "a referenced target style is missing"
281
+ detail: `referenced target style ${styleId} is missing`
242
282
  };
243
283
  required.add(styleId);
244
284
  pending.push(...styleDependencies(source));
@@ -257,7 +297,7 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
257
297
  const destination = destinationById.get(styleId);
258
298
  if (!source) return {
259
299
  status: "unalignable",
260
- detail: "a referenced target style is missing"
300
+ detail: `referenced target style ${styleId} is missing`
261
301
  };
262
302
  if (destination !== void 0 && canonicalJson(source) !== canonicalJson(destination) || !destination && (reserved.has(styleId) || source.default)) {
263
303
  const alias = nextAvailableStyleAlias({
@@ -275,7 +315,14 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
275
315
  const contextMatches = canonicalJson(sourceStyles.docDefaults) === canonicalJson(destinationStyles.docDefaults) && themesMatch(sourceTheme, destinationTheme);
276
316
  const materializeThemeReferences = !themesMatch(sourceTheme, destinationTheme);
277
317
  const additions = sourceStyles.styles.filter(({ styleId }) => required.has(styleId) && (!destinationById.has(styleId) || styleIdMap.get(styleId) !== styleId));
278
- if (contextMatches) {
318
+ const needsDefaultParagraphStyle = materializeDefaultParagraphStyle && !effectiveDefaultParagraphFormattingMatches({
319
+ sourceStyles,
320
+ destinationStyles,
321
+ sourceTheme,
322
+ destinationTheme,
323
+ materializeThemeReferences
324
+ });
325
+ if (contextMatches && !needsDefaultParagraphStyle) {
279
326
  if (additions.length === 0) return {
280
327
  status: "unchanged",
281
328
  styles: destinationStyles,
@@ -298,6 +345,14 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
298
345
  }
299
346
  const materializedStyles = [];
300
347
  for (const style of additions) {
348
+ if (contextMatches) {
349
+ materializedStyles.push(remapImportedStyle({
350
+ style,
351
+ styleIdMap,
352
+ isolatedDefaultStyleIds
353
+ }));
354
+ continue;
355
+ }
301
356
  const result = materializeEffectiveStyle({
302
357
  style,
303
358
  styles: sourceStyles,
@@ -306,7 +361,7 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
306
361
  });
307
362
  if (result.status === "unalignable") return {
308
363
  status: "unalignable",
309
- detail: "a referenced style has formatting that cannot be materialized"
364
+ detail: `referenced style ${style.styleId} has formatting that cannot be materialized`
310
365
  };
311
366
  materializedStyles.push(remapImportedStyle({
312
367
  style: result.style,
@@ -316,13 +371,7 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
316
371
  }
317
372
  let defaultParagraphStyleId;
318
373
  let materializedDefaultParagraphStyle;
319
- if (materializeDefaultParagraphStyle && !effectiveDefaultParagraphFormattingMatches({
320
- sourceStyles,
321
- destinationStyles,
322
- sourceTheme,
323
- destinationTheme,
324
- materializeThemeReferences
325
- })) {
374
+ if (needsDefaultParagraphStyle) {
326
375
  defaultParagraphStyleId = nextAvailableStyleAlias({
327
376
  taken,
328
377
  sequence: aliasSequence
@@ -373,7 +422,7 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
373
422
  rPr: candidateRunFormatting.formatting
374
423
  })) return {
375
424
  status: "unalignable",
376
- detail: "a referenced style differs after materializing its formatting"
425
+ detail: `referenced style ${style.styleId} differs after materializing its formatting`
377
426
  };
378
427
  }
379
428
  if (defaultParagraphStyleId !== void 0) {
@@ -407,4 +456,4 @@ const importReferencedStyleDefinitions = ({ sourceStyles, destinationStyles, sou
407
456
  };
408
457
  };
409
458
  //#endregion
410
- export { importReferencedStyleDefinitions };
459
+ export { importReferencedStyleDefinitions, styleClosureNumberingReferences };
@@ -1,4 +1,4 @@
1
- import { FolioAIBlock } from "../ai-edits/types.js";
1
+ import { FolioAIDiagnosticBlock, FolioAIParagraphBlock } from "../ai-edits/types.js";
2
2
  import { FolioDocumentStoryHandle } from "../ai-edits/headless.js";
3
3
  import { ParagraphMarkChangeKind } from "@stll/docx-core/model";
4
4
  //#region src/compare/verification.d.ts
@@ -10,7 +10,7 @@ type CompareVerificationInvariant = (typeof COMPARE_VERIFICATION_INVARIANTS)[num
10
10
  * that fits is the one reported, because a block in the wrong container is a
11
11
  * different finding from a block whose words are wrong even when both are true.
12
12
  */
13
- declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "inline-structure", "table-geometry", "section-properties", "style", "list-level", "alignment", "spacing", "indentation", "inline-formatting", "whitespace", "text"];
13
+ declare const COMPARE_VERIFICATION_CAUSES: readonly ["invisible-structure", "block-count", "container", "inline-structure", "table-geometry", "section-properties", "style", "list-level", "numbering-source", "alignment", "spacing", "indentation", "inline-formatting", "whitespace", "text"];
14
14
  type CompareVerificationCause = (typeof COMPARE_VERIFICATION_CAUSES)[number];
15
15
  /** One invariant that did not hold, in one story. */
16
16
  type CompareVerificationFailure = {
@@ -33,7 +33,19 @@ type CompareVerification = {
33
33
  status: "unverified";
34
34
  failures: readonly CompareVerificationFailure[];
35
35
  };
36
- type ProjectedBlock = Pick<FolioAIBlock, "kind" | "headingLevel" | "text" | "table" | "styleId" | "directOutlineLevel" | "listLevel" | "listReference" | "directAlignment" | "directSpacing" | "directIndentation" | "structuralBoundaries">;
36
+ type ProjectedBlockFields = Omit<Pick<FolioAIParagraphBlock, "kind" | "headingLevel" | "text" | "table" | "styleId">, "kind">;
37
+ type ProjectedBlock = ProjectedBlockFields & ({
38
+ kind: FolioAIDiagnosticBlock["kind"];
39
+ } | {
40
+ kind: FolioAIParagraphBlock["kind"];
41
+ statedNumbering: FolioAIParagraphBlock["statedNumbering"];
42
+ listReference?: FolioAIParagraphBlock["listReference"];
43
+ structuralBoundaries?: FolioAIParagraphBlock["structuralBoundaries"];
44
+ directOutlineLevel?: FolioAIParagraphBlock["directOutlineLevel"];
45
+ directAlignment?: FolioAIParagraphBlock["directAlignment"];
46
+ directSpacing?: FolioAIParagraphBlock["directSpacing"];
47
+ directIndentation?: FolioAIParagraphBlock["directIndentation"];
48
+ });
37
49
  type ClassifyOptions<T> = {
38
50
  invariant: CompareVerificationInvariant;
39
51
  story: FolioDocumentStoryHandle;
@@ -1,6 +1,8 @@
1
1
  import { paragraphIndentationEqual } from "../prosemirror/paragraphIndentation.js";
2
2
  import { paragraphSpacingEqual } from "../prosemirror/paragraphSpacing.js";
3
+ import { sameFolioContentStatedNumbering } from "./content-types.js";
3
4
  import { sameFolioContentOutlineLevel } from "./content.js";
5
+ import { panic } from "better-result";
4
6
  import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
5
7
  //#region src/compare/verification.ts
6
8
  /**
@@ -38,6 +40,7 @@ const COMPARE_VERIFICATION_CAUSES = Object.freeze([
38
40
  "section-properties",
39
41
  "style",
40
42
  "list-level",
43
+ "numbering-source",
41
44
  "alignment",
42
45
  "spacing",
43
46
  "indentation",
@@ -65,7 +68,43 @@ const sameStructuralBoundaries = (left, right) => {
65
68
  return true;
66
69
  };
67
70
  const sameClassification = (left, right) => left.kind === right.kind && left.headingLevel === right.headingLevel;
68
- const sameProjectedBlock = (left, right) => sameContainer(left.table, right.table) && sameClassification(left, right) && sameStructuralBoundaries(left.structuralBoundaries, right.structuralBoundaries) && left.styleId === right.styleId && sameFolioContentOutlineLevel(left.directOutlineLevel, right.directOutlineLevel) && left.listLevel === right.listLevel && left.listReference?.numId === right.listReference?.numId && left.listReference?.level === right.listReference?.level && left.directAlignment === right.directAlignment && paragraphSpacingEqual(left.directSpacing, right.directSpacing) && paragraphIndentationEqual(left.directIndentation, right.directIndentation) && left.text === right.text;
71
+ const sameProjectedNumbering = (left, right) => {
72
+ switch (left.kind) {
73
+ case "diagnostic": return right.kind === "diagnostic";
74
+ case "paragraph":
75
+ case "heading":
76
+ case "listItem": return right.kind !== "diagnostic" && sameFolioContentStatedNumbering(left.statedNumbering, right.statedNumbering);
77
+ default: return panic("Unhandled projected block kind", { block: left });
78
+ }
79
+ };
80
+ const sameProjectedListReference = (left, right) => {
81
+ switch (left.kind) {
82
+ case "diagnostic": return right.kind === "diagnostic";
83
+ case "paragraph":
84
+ case "heading":
85
+ case "listItem": return right.kind !== "diagnostic" && left.listReference?.numId === right.listReference?.numId && left.listReference?.level === right.listReference?.level;
86
+ default: return panic("Unhandled projected block kind", { block: left });
87
+ }
88
+ };
89
+ const sameProjectedStructuralBoundaries = (left, right) => {
90
+ switch (left.kind) {
91
+ case "diagnostic": return right.kind === "diagnostic";
92
+ case "paragraph":
93
+ case "heading":
94
+ case "listItem": return right.kind !== "diagnostic" && sameStructuralBoundaries(left.structuralBoundaries, right.structuralBoundaries);
95
+ default: return panic("Unhandled projected block kind", { block: left });
96
+ }
97
+ };
98
+ const sameProjectedParagraphFormatting = (left, right, compare) => {
99
+ switch (left.kind) {
100
+ case "diagnostic": return right.kind === "diagnostic";
101
+ case "paragraph":
102
+ case "heading":
103
+ case "listItem": return right.kind !== "diagnostic" && compare(left, right);
104
+ default: return panic("Unhandled projected block kind", { block: left });
105
+ }
106
+ };
107
+ const sameProjectedBlock = (left, right) => sameContainer(left.table, right.table) && sameClassification(left, right) && sameProjectedStructuralBoundaries(left, right) && left.styleId === right.styleId && sameProjectedParagraphFormatting(left, right, (a, b) => sameFolioContentOutlineLevel(a.directOutlineLevel, b.directOutlineLevel)) && sameProjectedListReference(left, right) && sameProjectedNumbering(left, right) && sameProjectedParagraphFormatting(left, right, (a, b) => a.directAlignment === b.directAlignment) && sameProjectedParagraphFormatting(left, right, (a, b) => paragraphSpacingEqual(a.directSpacing, b.directSpacing)) && sameProjectedParagraphFormatting(left, right, (a, b) => paragraphIndentationEqual(a.directIndentation, b.directIndentation)) && left.text === right.text;
69
108
  const containerKind = (container) => container ? "cell" : "body";
70
109
  const collapseWhitespace = (text) => text.replace(/\s+/gu, " ").trim();
71
110
  const sameBlockProjection = (left, right) => left.length === right.length && left.every((entry, index) => {
@@ -180,15 +219,15 @@ const classifyProjectionMismatch = ({ invariant, story, actual, expected }) => {
180
219
  if (divergence.actual === null || divergence.expected === null) return failure("block-count", `${actual.length > expected.length ? "more" : "fewer"} blocks than expected (${counts}), diverging ${at}`);
181
220
  const { actual: left, expected: right } = divergence;
182
221
  if (!sameContainer(left.table, right.table)) return failure("container", `a block sits in a ${containerKind(left.table)} where it is expected in a ${containerKind(right.table)}, ${at} (${counts})`);
183
- if (!sameStructuralBoundaries(left.structuralBoundaries, right.structuralBoundaries)) return failure("inline-structure", `a block's zero-width inline structure does not match ${at} (${counts})`);
222
+ if (left.kind === right.kind && !sameProjectedStructuralBoundaries(left, right)) return failure("inline-structure", `a block's zero-width inline structure does not match ${at} (${counts})`);
184
223
  if (left.text === right.text && left.styleId !== right.styleId) return failure("style", `the paragraph style did not move ${at} (${counts})`);
185
- if (left.text === right.text && !sameFolioContentOutlineLevel(left.directOutlineLevel, right.directOutlineLevel)) return failure("style", `the direct outline level did not move ${at} (${counts})`);
186
- if (left.text === right.text && left.listLevel !== right.listLevel) return failure("list-level", `the list level did not move ${at} (${counts})`);
187
- if (left.text === right.text && (left.listReference?.numId !== right.listReference?.numId || left.listReference?.level !== right.listReference?.level)) return failure("list-level", `the numbering reference did not move ${at} (${counts})`);
224
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedParagraphFormatting(left, right, (a, b) => sameFolioContentOutlineLevel(a.directOutlineLevel, b.directOutlineLevel))) return failure("style", `the direct outline level did not move ${at} (${counts})`);
225
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedListReference(left, right)) return failure("list-level", `the numbering reference did not move ${at} (${counts})`);
226
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedNumbering(left, right)) return failure("numbering-source", `the authored numbering state did not move ${at} (${counts})`);
188
227
  if (left.text === right.text && !sameClassification(left, right)) return left.kind === "heading" || right.kind === "heading" ? failure("style", `the heading classification did not move ${at} (${counts})`) : failure("list-level", `the list membership did not move ${at} (${counts})`);
189
- if (left.text === right.text && left.directAlignment !== right.directAlignment) return failure("alignment", `the direct paragraph alignment did not move ${at} (${counts})`);
190
- if (left.text === right.text && !paragraphSpacingEqual(left.directSpacing, right.directSpacing)) return failure("spacing", `the direct paragraph spacing did not move ${at} (${counts})`);
191
- if (left.text === right.text && !paragraphIndentationEqual(left.directIndentation, right.directIndentation)) return failure("indentation", `the direct paragraph indentation did not move ${at} (${counts})`);
228
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedParagraphFormatting(left, right, (a, b) => a.directAlignment === b.directAlignment)) return failure("alignment", `the direct paragraph alignment did not move ${at} (${counts})`);
229
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedParagraphFormatting(left, right, (a, b) => paragraphSpacingEqual(a.directSpacing, b.directSpacing))) return failure("spacing", `the direct paragraph spacing did not move ${at} (${counts})`);
230
+ if (left.kind === right.kind && left.text === right.text && !sameProjectedParagraphFormatting(left, right, (a, b) => paragraphIndentationEqual(a.directIndentation, b.directIndentation))) return failure("indentation", `the direct paragraph indentation did not move ${at} (${counts})`);
192
231
  if (collapseWhitespace(left.text) === collapseWhitespace(right.text)) return failure("whitespace", `a block's text differs only in whitespace ${at} (${counts})`);
193
232
  if (actual.length !== expected.length) return failure("block-count", `${counts}, first differing ${at}`);
194
233
  return failure("text", `a ${containerKind(left.table)} block's text does not match ${at}, length ${String(left.text.length)} against ${String(right.text.length)} (${counts})`);