@stll/folio-core 0.32.2 → 0.33.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 (222) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +649 -85
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +219 -15
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +67 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +406 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1059 -0
  37. package/dist/compare/reproducible-package.d.ts +17 -0
  38. package/dist/compare/reproducible-package.js +30 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +282 -0
  42. package/dist/compare/types.js +32 -0
  43. package/dist/compare/verification.d.ts +55 -0
  44. package/dist/compare/verification.js +146 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +39 -8
  100. package/dist/document-operations.js +169 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/packageParts.d.ts +35 -0
  104. package/dist/docx/packageParts.js +120 -0
  105. package/dist/docx/paragraphParser.js +22 -22
  106. package/dist/docx/paragraphTraversal.js +1 -1
  107. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  108. package/dist/docx/revisionIdNormalization.d.ts +13 -0
  109. package/dist/docx/revisionIdNormalization.js +103 -0
  110. package/dist/docx/rezip.js +26 -17
  111. package/dist/docx/selectiveSave.js +2 -2
  112. package/dist/docx/serializer/commentSerializer.js +6 -1
  113. package/dist/docx/serializer/paragraphSerializer.js +33 -4
  114. package/dist/docx/serializer/runSerializer.js +11 -10
  115. package/dist/docx/serializer/tableSerializer.js +2 -1
  116. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  117. package/dist/docx/server/createBilingualDocx.js +3 -2
  118. package/dist/docx/streamingXmlParser.d.ts +13 -1
  119. package/dist/docx/streamingXmlParser.js +49 -11
  120. package/dist/docx/unzip.d.ts +1 -2
  121. package/dist/docx/unzip.js +25 -6
  122. package/dist/docx/xmlParser.d.ts +3 -1
  123. package/dist/docx/xmlParser.js +33 -27
  124. package/dist/export-pdf.d.ts +43 -0
  125. package/dist/export-pdf.js +98 -0
  126. package/dist/fonts/headlessMeasure.d.ts +76 -0
  127. package/dist/fonts/headlessMeasure.js +0 -0
  128. package/dist/fonts/sfnt/parse.d.ts +51 -0
  129. package/dist/fonts/sfnt/parse.js +525 -0
  130. package/dist/fonts/sfnt/subset.d.ts +20 -0
  131. package/dist/fonts/sfnt/subset.js +350 -0
  132. package/dist/fonts/sfnt/tables.d.ts +88 -0
  133. package/dist/fonts/sfnt/tables.js +131 -0
  134. package/dist/fonts/sfnt/woff.d.ts +14 -0
  135. package/dist/fonts/sfnt/woff.js +163 -0
  136. package/dist/generated/text_shaper.js +324 -0
  137. package/dist/generated/text_shaper_bg.wasm +0 -0
  138. package/dist/headless-layout.d.ts +75 -0
  139. package/dist/headless-layout.js +350 -0
  140. package/dist/index.d.ts +9 -6
  141. package/dist/index.js +6 -3
  142. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  143. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  144. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  145. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  146. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  147. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  148. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  149. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  150. package/dist/layout-engine/measure/cache.d.ts +2 -0
  151. package/dist/layout-engine/measure/measureContainer.js +13 -0
  152. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  153. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  154. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  155. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  156. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  157. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  158. package/dist/layout-engine/types.d.ts +8 -0
  159. package/dist/layout-painter/renderPage.d.ts +29 -1
  160. package/dist/layout-painter/renderPage.js +47 -19
  161. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  162. package/dist/layout-painter/renderParagraph.js +1 -1
  163. package/dist/layout-painter/renderTable.js +37 -4
  164. package/dist/managers/editorShortcuts.d.ts +33 -1
  165. package/dist/managers/editorShortcuts.js +20 -1
  166. package/dist/markdown/renderRuns.js +7 -2
  167. package/dist/markdown/renderTable.js +5 -1
  168. package/dist/model.d.ts +3 -3
  169. package/dist/model.js +2 -2
  170. package/dist/pdf/contentStream.d.ts +73 -0
  171. package/dist/pdf/contentStream.js +143 -0
  172. package/dist/pdf/fonts.d.ts +96 -0
  173. package/dist/pdf/fonts.js +593 -0
  174. package/dist/pdf/images.d.ts +28 -0
  175. package/dist/pdf/images.js +439 -0
  176. package/dist/pdf/objects.d.ts +93 -0
  177. package/dist/pdf/objects.js +237 -0
  178. package/dist/pdf/pageSpace.d.ts +43 -0
  179. package/dist/pdf/pageSpace.js +57 -0
  180. package/dist/pdf/paint.d.ts +32 -0
  181. package/dist/pdf/paint.js +442 -0
  182. package/dist/pdf/writePdf.d.ts +56 -0
  183. package/dist/pdf/writePdf.js +345 -0
  184. package/dist/prosemirror/commands/comments.js +91 -6
  185. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  186. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  187. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  188. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  189. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  190. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  191. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  192. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  193. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  194. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  195. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  196. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  198. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  199. package/dist/prosemirror/revisionCarriers.js +19 -2
  200. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  201. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  202. package/dist/prosemirror/validation.js +55 -27
  203. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  204. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  205. package/dist/redline.js +12 -79
  206. package/dist/server.d.ts +3 -3
  207. package/dist/server.js +2 -2
  208. package/dist/shaping/placeRun.d.ts +51 -0
  209. package/dist/shaping/placeRun.js +84 -0
  210. package/dist/shaping/shaper.d.ts +92 -0
  211. package/dist/shaping/shaper.js +102 -0
  212. package/dist/types/block-id.d.ts +18 -2
  213. package/dist/types/block-id.js +49 -5
  214. package/dist/types/content.d.ts +2 -2
  215. package/dist/utils/clipboard.js +4 -3
  216. package/dist/utils/fontResolver.js +183 -0
  217. package/dist/utils/htmlComments.d.ts +17 -0
  218. package/dist/utils/htmlComments.js +22 -0
  219. package/dist/utils/scriptSegments.d.ts +12 -1
  220. package/dist/utils/scriptSegments.js +17 -1
  221. package/dist/version-comparison.d.ts +1 -1
  222. package/package.json +7 -3
@@ -0,0 +1,17 @@
1
+ //#region src/compare/reproducible-package.d.ts
2
+ /**
3
+ * The last clock in the compare path is the ZIP container itself.
4
+ *
5
+ * Every part the serializer rewrites is stored with JSZip's default entry
6
+ * date, which is `new Date()`. The XML is identical between two runs, but the
7
+ * DOS timestamp fields in the local headers are not, and they only agree when
8
+ * both runs land in the same two-second bucket — so the packages match most of
9
+ * the time and differ occasionally, which is worse than differing always.
10
+ *
11
+ * Restamping every entry from the comparison's own timestamp removes it. It
12
+ * also states the truth about the package: a generated redline is dated by the
13
+ * comparison that produced it, not by the second it happened to be written.
14
+ */
15
+ declare const withFixedPackageDates: (buffer: ArrayBuffer, date: Date) => Promise<ArrayBuffer>;
16
+ //#endregion
17
+ export { withFixedPackageDates };
@@ -0,0 +1,30 @@
1
+ import JSZip from "jszip";
2
+ //#region src/compare/reproducible-package.ts
3
+ /**
4
+ * The last clock in the compare path is the ZIP container itself.
5
+ *
6
+ * Every part the serializer rewrites is stored with JSZip's default entry
7
+ * date, which is `new Date()`. The XML is identical between two runs, but the
8
+ * DOS timestamp fields in the local headers are not, and they only agree when
9
+ * both runs land in the same two-second bucket — so the packages match most of
10
+ * the time and differ occasionally, which is worse than differing always.
11
+ *
12
+ * Restamping every entry from the comparison's own timestamp removes it. It
13
+ * also states the truth about the package: a generated redline is dated by the
14
+ * comparison that produced it, not by the second it happened to be written.
15
+ */
16
+ /** JSZip deflate level `repackDocx` writes DOCX parts at. */
17
+ const DOCX_COMPRESSION_LEVEL = 6;
18
+ const withFixedPackageDates = async (buffer, date) => {
19
+ const zip = await JSZip.loadAsync(buffer);
20
+ zip.forEach((_path, file) => {
21
+ file.date = date;
22
+ });
23
+ return await zip.generateAsync({
24
+ type: "arraybuffer",
25
+ compression: "DEFLATE",
26
+ compressionOptions: { level: DOCX_COMPRESSION_LEVEL }
27
+ });
28
+ };
29
+ //#endregion
30
+ export { withFixedPackageDates };
@@ -0,0 +1,77 @@
1
+ import { FolioAIInlineFormatting } from "../ai-edits/types.js";
2
+ import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
3
+ import { Result } from "better-result";
4
+ //#region src/compare/scenario.d.ts
5
+ type EditScriptStep = {
6
+ type: "insertParagraphAfter";
7
+ blockIndex: number;
8
+ text: string;
9
+ } | {
10
+ type: "deleteParagraph";
11
+ blockIndex: number;
12
+ } | {
13
+ type: "replaceWords";
14
+ blockIndex: number;
15
+ find: string;
16
+ replace: string;
17
+ } |
18
+ /** Delete the block at `blockIndex` and reinsert its text before `beforeBlockIndex`. */
19
+ {
20
+ type: "moveParagraph";
21
+ blockIndex: number;
22
+ beforeBlockIndex: number;
23
+ } |
24
+ /**
25
+ * Break one paragraph into two at `wordIndex`, which is an inserted
26
+ * paragraph mark and nothing else: both halves keep their words.
27
+ */
28
+ {
29
+ type: "splitParagraph";
30
+ blockIndex: number;
31
+ wordIndex: number;
32
+ } |
33
+ /**
34
+ * Join the block at `blockIndex` with the one after it, which is a deleted
35
+ * paragraph mark and nothing else.
36
+ */
37
+ {
38
+ type: "mergeParagraphs";
39
+ blockIndex: number;
40
+ } | {
41
+ type: "formatRange";
42
+ blockIndex: number;
43
+ startOffset: number;
44
+ endOffset: number;
45
+ formatting: FolioAIInlineFormatting;
46
+ } | {
47
+ type: "insertTableRow";
48
+ blockIndex: number;
49
+ cellTexts: readonly string[];
50
+ } | {
51
+ type: "deleteTableRow";
52
+ blockIndex: number;
53
+ } | {
54
+ type: "editTableCell";
55
+ blockIndex: number;
56
+ text: string;
57
+ };
58
+ type EditScript = readonly EditScriptStep[];
59
+ declare const EDIT_SCRIPT_UNRESOLVED_REASONS: readonly ["block-out-of-range", "block-not-in-table", "range-out-of-bounds", "text-not-found", "no-paragraph-boundary", "refused"];
60
+ type EditScriptUnresolvedReason = (typeof EDIT_SCRIPT_UNRESOLVED_REASONS)[number];
61
+ type EditScriptUnresolvedStep = {
62
+ step: EditScriptStep;
63
+ reason: EditScriptUnresolvedReason;
64
+ };
65
+ type EditScriptResult = {
66
+ /** The base document with every applied step written in directly. */
67
+ buffer: ArrayBuffer;
68
+ /** The steps that reached the document, in script order. */
69
+ applied: readonly EditScriptStep[];
70
+ unresolved: readonly EditScriptUnresolvedStep[];
71
+ };
72
+ /**
73
+ * Apply `script` to `base` directly and return the resulting target document.
74
+ */
75
+ declare const applyEditScript: (base: ArrayBuffer, script: EditScript) => Promise<Result<EditScriptResult, CompareDocxParseError | CompareDocxSerializeError>>;
76
+ //#endregion
77
+ export { EDIT_SCRIPT_UNRESOLVED_REASONS, EditScript, EditScriptResult, EditScriptStep, EditScriptUnresolvedReason, EditScriptUnresolvedStep, applyEditScript };
@@ -0,0 +1,263 @@
1
+ import { FolioDocxReviewer } from "../ai-edits/headless.js";
2
+ import { createFolioAITextRangeHandle } from "../ai-edits/snapshot.js";
3
+ import { CompareDocxParseError, CompareDocxSerializeError } from "./types.js";
4
+ import { Result, panic } from "better-result";
5
+ //#region src/compare/scenario.ts
6
+ /**
7
+ * Edit scripts: ground truth for the compare engine, by construction.
8
+ *
9
+ * A property test cannot assert "this redline is right" against a document it
10
+ * only guessed at. It can assert it against a document it BUILT: take a real
11
+ * base, apply a scripted edit directly (no tracked changes), and the result is
12
+ * a target whose difference from the base is known exactly. `compareDocx` then
13
+ * has to rediscover that difference, and the script says what it should have
14
+ * found.
15
+ *
16
+ * Every step is applied through the same headless applier `compareDocx` writes
17
+ * its own operations through, in `"direct"` mode — so the target is an ordinary
18
+ * document, not one carrying revisions the comparison would have to resolve
19
+ * first.
20
+ *
21
+ * Steps address blocks by their index in the base story's block list rather
22
+ * than by id, so a generator can pick targets without first parsing the
23
+ * document. A step that does not resolve is reported in `unresolved`, never
24
+ * dropped: a script silently doing less than it says would weaken every
25
+ * property built on it.
26
+ */
27
+ const EDIT_SCRIPT_UNRESOLVED_REASONS = Object.freeze([
28
+ "block-out-of-range",
29
+ "block-not-in-table",
30
+ "range-out-of-bounds",
31
+ "text-not-found",
32
+ "no-paragraph-boundary",
33
+ "refused"
34
+ ]);
35
+ const blockAt = (blocks, index) => Number.isInteger(index) && index >= 0 ? blocks.at(index) : void 0;
36
+ const planStep = ({ step, blocks, nextOperationId }) => {
37
+ const block = blockAt(blocks, step.blockIndex);
38
+ if (!block) return {
39
+ status: "unresolved",
40
+ reason: "block-out-of-range"
41
+ };
42
+ switch (step.type) {
43
+ case "insertParagraphAfter": return {
44
+ status: "planned",
45
+ operations: [{
46
+ id: nextOperationId(),
47
+ type: "insertAfterBlock",
48
+ blockId: block.id,
49
+ text: step.text
50
+ }]
51
+ };
52
+ case "deleteParagraph": return {
53
+ status: "planned",
54
+ operations: [{
55
+ id: nextOperationId(),
56
+ type: "deleteBlock",
57
+ blockId: block.id
58
+ }]
59
+ };
60
+ case "replaceWords": return block.text.includes(step.find) ? {
61
+ status: "planned",
62
+ operations: [{
63
+ id: nextOperationId(),
64
+ type: "replaceInBlock",
65
+ blockId: block.id,
66
+ find: step.find,
67
+ replace: step.replace
68
+ }]
69
+ } : {
70
+ status: "unresolved",
71
+ reason: "text-not-found"
72
+ };
73
+ case "moveParagraph": {
74
+ const destination = blockAt(blocks, step.beforeBlockIndex);
75
+ if (!destination || destination.id === block.id) return {
76
+ status: "unresolved",
77
+ reason: "block-out-of-range"
78
+ };
79
+ return {
80
+ status: "planned",
81
+ operations: [{
82
+ id: nextOperationId(),
83
+ type: "deleteBlock",
84
+ blockId: block.id
85
+ }, {
86
+ id: nextOperationId(),
87
+ type: "insertBeforeBlock",
88
+ blockId: destination.id,
89
+ text: block.text,
90
+ styleId: block.styleId ?? null,
91
+ listLevel: block.listLevel ?? null
92
+ }]
93
+ };
94
+ }
95
+ case "splitParagraph": {
96
+ const words = block.text.split(" ");
97
+ if (step.wordIndex <= 0 || step.wordIndex >= words.length) return {
98
+ status: "unresolved",
99
+ reason: "no-paragraph-boundary"
100
+ };
101
+ return {
102
+ status: "planned",
103
+ operations: [{
104
+ id: nextOperationId(),
105
+ type: "replaceBlock",
106
+ blockId: block.id,
107
+ text: words.slice(0, step.wordIndex).join(" ")
108
+ }, {
109
+ id: nextOperationId(),
110
+ type: "insertAfterBlock",
111
+ blockId: block.id,
112
+ text: words.slice(step.wordIndex).join(" "),
113
+ ...block.styleId !== void 0 && { styleId: block.styleId }
114
+ }]
115
+ };
116
+ }
117
+ case "mergeParagraphs": {
118
+ const next = blockAt(blocks, step.blockIndex + 1);
119
+ if (!next || next.table?.cellIndex !== block.table?.cellIndex) return {
120
+ status: "unresolved",
121
+ reason: "no-paragraph-boundary"
122
+ };
123
+ return {
124
+ status: "planned",
125
+ operations: [{
126
+ id: nextOperationId(),
127
+ type: "replaceBlock",
128
+ blockId: block.id,
129
+ text: `${block.text} ${next.text}`
130
+ }, {
131
+ id: nextOperationId(),
132
+ type: "deleteBlock",
133
+ blockId: next.id
134
+ }]
135
+ };
136
+ }
137
+ case "formatRange": {
138
+ const range = createFolioAITextRangeHandle({
139
+ blockId: block.id,
140
+ text: block.text,
141
+ startOffset: step.startOffset,
142
+ endOffset: step.endOffset
143
+ });
144
+ return range ? {
145
+ status: "planned",
146
+ operations: [{
147
+ id: nextOperationId(),
148
+ type: "formatRange",
149
+ range,
150
+ formatting: step.formatting
151
+ }]
152
+ } : {
153
+ status: "unresolved",
154
+ reason: "range-out-of-bounds"
155
+ };
156
+ }
157
+ case "insertTableRow": return block.table ? {
158
+ status: "planned",
159
+ operations: [{
160
+ id: nextOperationId(),
161
+ type: "insertTableRow",
162
+ blockId: block.id,
163
+ position: "after",
164
+ cellTexts: [...step.cellTexts]
165
+ }]
166
+ } : {
167
+ status: "unresolved",
168
+ reason: "block-not-in-table"
169
+ };
170
+ case "deleteTableRow": return block.table ? {
171
+ status: "planned",
172
+ operations: [{
173
+ id: nextOperationId(),
174
+ type: "deleteTableRow",
175
+ blockId: block.id
176
+ }]
177
+ } : {
178
+ status: "unresolved",
179
+ reason: "block-not-in-table"
180
+ };
181
+ case "editTableCell": return block.table ? {
182
+ status: "planned",
183
+ operations: [{
184
+ id: nextOperationId(),
185
+ type: "replaceBlock",
186
+ blockId: block.id,
187
+ text: step.text
188
+ }]
189
+ } : {
190
+ status: "unresolved",
191
+ reason: "block-not-in-table"
192
+ };
193
+ default: return panic("Unhandled edit script step", { step });
194
+ }
195
+ };
196
+ /**
197
+ * Apply `script` to `base` directly and return the resulting target document.
198
+ */
199
+ const applyEditScript = async (base, script) => {
200
+ const parsed = await Result.tryPromise({
201
+ try: async () => await FolioDocxReviewer.fromBuffer(base),
202
+ catch: (cause) => new CompareDocxParseError({
203
+ message: "The edit script's base document could not be parsed.",
204
+ side: "base",
205
+ cause
206
+ })
207
+ });
208
+ if (parsed.isErr()) return Result.err(parsed.error);
209
+ const reviewer = parsed.value;
210
+ const snapshot = reviewer.snapshot();
211
+ const unresolved = [];
212
+ const planned = [];
213
+ let operationSequence = 0;
214
+ const nextOperationId = () => `scenario-${++operationSequence}`;
215
+ for (const step of script) {
216
+ const plan = planStep({
217
+ step,
218
+ blocks: snapshot.blocks,
219
+ nextOperationId
220
+ });
221
+ if (plan.status === "unresolved") {
222
+ unresolved.push({
223
+ step,
224
+ reason: plan.reason
225
+ });
226
+ continue;
227
+ }
228
+ planned.push({
229
+ step,
230
+ operations: plan.operations
231
+ });
232
+ }
233
+ const { skipped } = reviewer.applyOperations(planned.flatMap(({ operations }) => [...operations]), {
234
+ mode: "direct",
235
+ snapshot
236
+ });
237
+ const skippedIds = new Set(skipped.map(({ id }) => id));
238
+ const applied = [];
239
+ for (const { step, operations } of planned) {
240
+ if (operations.some(({ id }) => skippedIds.has(id))) {
241
+ unresolved.push({
242
+ step,
243
+ reason: "refused"
244
+ });
245
+ continue;
246
+ }
247
+ applied.push(step);
248
+ }
249
+ const serialized = await Result.tryPromise({
250
+ try: async () => await reviewer.toBuffer(),
251
+ catch: (cause) => new CompareDocxSerializeError({
252
+ message: "The edit script's target document could not be serialized.",
253
+ cause
254
+ })
255
+ });
256
+ return serialized.isErr() ? Result.err(serialized.error) : Result.ok({
257
+ buffer: serialized.value,
258
+ applied,
259
+ unresolved
260
+ });
261
+ };
262
+ //#endregion
263
+ export { EDIT_SCRIPT_UNRESOLVED_REASONS, applyEditScript };
@@ -0,0 +1,282 @@
1
+ import { FolioAIBlockParagraphProperties, FolioAIBlockTableLocation, FolioAIEditSkippedOperation, FolioAIInlineFormatting } from "../ai-edits/types.js";
2
+ import { WordDiffGranularity } from "../ai-edits/word-diff.js";
3
+ import { FolioDocumentStoryHandle, FolioNumberingLevel } from "../ai-edits/headless.js";
4
+ import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant } from "./verification.js";
5
+ //#region src/compare/types.d.ts
6
+ /** Everything {@link compareDocx} needs; nothing it reads from the ambient clock. */
7
+ type CompareDocxOptions = {
8
+ /** Author recorded on every generated tracked change. */
9
+ author: string;
10
+ /**
11
+ * ISO-8601 date stamped on every generated tracked change. Required rather
12
+ * than defaulted so a caller cannot get a nondeterministic package by
13
+ * omission; pass the base document's own timestamp, a release date, or a
14
+ * fixed epoch.
15
+ */
16
+ timestamp: string;
17
+ /**
18
+ * What to do when the round-trip self-check cannot prove the result.
19
+ *
20
+ * `"refuse"` (default) returns a {@link CompareDocxRoundTripError}: a
21
+ * redline that reads plausibly and is wrong is worse than no redline.
22
+ * `"emit"` returns the redline anyway, with `verification` naming every
23
+ * invariant that did not hold, for a caller that would rather show its best
24
+ * attempt and say what is missing.
25
+ */
26
+ onUnverified?: "refuse" | "emit";
27
+ /**
28
+ * Token size a changed paragraph's redline is cut at: `"word"` (default)
29
+ * marks whole words, `"character"` marks the changed letters inside one.
30
+ *
31
+ * Case and whitespace normalization are not options here, though
32
+ * `diffWordSegments` offers them: a comparison that leaves a difference
33
+ * unmarked does not accept back to the target, and the round trip is the
34
+ * one thing this call promises.
35
+ */
36
+ granularity?: WordDiffGranularity;
37
+ };
38
+ /** Where one change sits in the base or target document. */
39
+ type CompareChangeLocation = {
40
+ story: FolioDocumentStoryHandle;
41
+ /** Innermost table cell, when the change is inside a table. */
42
+ cell?: FolioAIBlockTableLocation;
43
+ };
44
+ /** One run of characters whose inline formatting differs, in base-block offsets. */
45
+ type CompareFormatRange = {
46
+ startOffset: number;
47
+ endOffset: number;
48
+ /** Only the properties that differ, set to the target document's value. */
49
+ formatting: FolioAIInlineFormatting;
50
+ };
51
+ /**
52
+ * One difference between the two documents, in target-document order with
53
+ * base-only entries slotted where they sat. Every variant is plain JSON, so a
54
+ * caller can hand the list to an agent instead of the package.
55
+ *
56
+ * A `move` is reported, not represented: the tracked-change grammar has no
57
+ * "this paragraph came from there" mark that Word round-trips, so the buffer
58
+ * carries a deletion at the source and an insertion at the destination while
59
+ * the change list keeps the relocation visible.
60
+ */
61
+ type CompareChange = {
62
+ kind: "insert";
63
+ location: CompareChangeLocation;
64
+ /** Id of the inserted block in the target document. */
65
+ targetBlockId: string;
66
+ after: string;
67
+ } | {
68
+ kind: "delete";
69
+ location: CompareChangeLocation;
70
+ baseBlockId: string;
71
+ before: string;
72
+ } | {
73
+ kind: "replace";
74
+ location: CompareChangeLocation;
75
+ baseBlockId: string;
76
+ targetBlockId: string;
77
+ before: string;
78
+ after: string;
79
+ } | {
80
+ kind: "move";
81
+ location: CompareChangeLocation;
82
+ /** The block's id where it sat in the base document. */
83
+ baseBlockId: string;
84
+ /** The same content's id where it sits in the target document. */
85
+ targetBlockId: string;
86
+ text: string;
87
+ } |
88
+ /**
89
+ * One paragraph became two: a paragraph mark was inserted and no words
90
+ * changed. Reported as its own kind so a reader is not told the tail was
91
+ * newly written.
92
+ */
93
+ {
94
+ kind: "split";
95
+ location: CompareChangeLocation;
96
+ baseBlockId: string;
97
+ /** The two blocks the base block became, in target order. */
98
+ targetBlockIds: readonly string[];
99
+ text: string;
100
+ } |
101
+ /** Two paragraphs became one: a paragraph mark was deleted. */
102
+ {
103
+ kind: "merge";
104
+ location: CompareChangeLocation;
105
+ /** The two blocks that became one, in base order. */
106
+ baseBlockIds: readonly string[];
107
+ targetBlockId: string;
108
+ text: string;
109
+ } |
110
+ /**
111
+ * A paragraph property moved and no words did: a list item demoted a level,
112
+ * a paragraph restyled. Written as `w:pPrChange`, so rejecting restores the
113
+ * whole previous property set the way Word does.
114
+ */
115
+ {
116
+ kind: "paragraph-format";
117
+ location: CompareChangeLocation;
118
+ baseBlockId: string;
119
+ targetBlockId: string;
120
+ /** Only the properties that differ, set to the target document's value. */
121
+ properties: FolioAIBlockParagraphProperties;
122
+ } | {
123
+ kind: "format";
124
+ location: CompareChangeLocation;
125
+ baseBlockId: string;
126
+ targetBlockId: string;
127
+ text: string;
128
+ ranges: readonly CompareFormatRange[];
129
+ } | {
130
+ kind: "table-row-insert";
131
+ location: CompareChangeLocation;
132
+ tableIndex: number;
133
+ /** Row index in the target document's table. */
134
+ rowIndex: number;
135
+ /** The new row's cell texts, in physical cell order. */
136
+ cells: readonly string[];
137
+ targetBlockIds: readonly string[];
138
+ } |
139
+ /**
140
+ * A numbering definition that differs. It carries no `location`: numbering
141
+ * lives in the package, not in a story, and one definition governs every
142
+ * list that references it.
143
+ *
144
+ * Reported and not represented. A renumbering that FOLLOWS from an edit —
145
+ * an item inserted, so the ones below it count on — is already shown
146
+ * as-if-accepted, because labels are rendered from these definitions rather
147
+ * than stored on the paragraphs. A definition that itself changed is a
148
+ * different thing, and OOXML has no tracked-change grammar for it: Word
149
+ * does not track `numbering.xml` either.
150
+ */
151
+ {
152
+ kind: "numbering";
153
+ numId: number;
154
+ level: number;
155
+ /** `null` when the target added this level. */
156
+ before: FolioNumberingLevel | null;
157
+ /** `null` when the target dropped it. */
158
+ after: FolioNumberingLevel | null;
159
+ } |
160
+ /** A whole table the target added. */
161
+ {
162
+ kind: "table-insert";
163
+ location: CompareChangeLocation;
164
+ tableIndex: number;
165
+ /** Cell texts row by row, in physical cell order. */
166
+ rows: readonly (readonly string[])[];
167
+ targetBlockIds: readonly string[];
168
+ } |
169
+ /** A whole table the target dropped. */
170
+ {
171
+ kind: "table-delete";
172
+ location: CompareChangeLocation;
173
+ tableIndex: number;
174
+ rows: readonly (readonly string[])[];
175
+ baseBlockIds: readonly string[];
176
+ } | {
177
+ kind: "table-row-delete";
178
+ location: CompareChangeLocation;
179
+ tableIndex: number;
180
+ /** Row index in the base document's table. */
181
+ rowIndex: number;
182
+ cells: readonly string[];
183
+ baseBlockIds: readonly string[];
184
+ } | {
185
+ kind: "table-column-insert";
186
+ location: CompareChangeLocation;
187
+ tableIndex: number;
188
+ /** Grid-column index in the target table. */
189
+ columnIndex: number;
190
+ /** Newly created physical-cell text in row order. */
191
+ cells: readonly string[];
192
+ targetBlockIds: readonly string[];
193
+ } | {
194
+ kind: "table-column-delete";
195
+ location: CompareChangeLocation;
196
+ tableIndex: number;
197
+ /** Grid-column index in the base table. */
198
+ columnIndex: number;
199
+ cells: readonly string[];
200
+ baseBlockIds: readonly string[];
201
+ };
202
+ /** Why a part of the package is absent from `changes`. */
203
+ declare const COMPARE_UNSUPPORTED_REASONS: readonly ["story-missing-in-base", "story-missing-in-target", "story-not-editable"];
204
+ type CompareUnsupportedReason = (typeof COMPARE_UNSUPPORTED_REASONS)[number];
205
+ /**
206
+ * A package part the comparison did not cover. Reported rather than dropped so
207
+ * a caller can tell "no differences" from "not looked at".
208
+ */
209
+ type CompareUnsupportedPart = {
210
+ reason: CompareUnsupportedReason;
211
+ baseStory: FolioDocumentStoryHandle | null;
212
+ targetStory: FolioDocumentStoryHandle | null;
213
+ };
214
+ type CompareResult = {
215
+ /** The base package carrying the generated tracked changes. */
216
+ buffer: ArrayBuffer;
217
+ changes: readonly CompareChange[];
218
+ /**
219
+ * Whether the round trip was proven. Always `verified` unless the call asked
220
+ * for `onUnverified: "emit"`, which is the only way an unproven redline is
221
+ * returned at all.
222
+ */
223
+ verification: CompareVerification;
224
+ unsupported: readonly CompareUnsupportedPart[];
225
+ };
226
+ declare const InvalidCompareDocxOptionsError_base: import("better-result").TaggedErrorClass<"InvalidCompareDocxOptionsError">;
227
+ declare class InvalidCompareDocxOptionsError extends InvalidCompareDocxOptionsError_base<{
228
+ message: string;
229
+ option: "timestamp";
230
+ receivedValue: unknown;
231
+ }> {}
232
+ declare const CompareDocxParseError_base: import("better-result").TaggedErrorClass<"CompareDocxParseError">;
233
+ declare class CompareDocxParseError extends CompareDocxParseError_base<{
234
+ message: string;
235
+ side: "base" | "target";
236
+ cause: unknown;
237
+ }> {}
238
+ declare const CompareDocxApplyError_base: import("better-result").TaggedErrorClass<"CompareDocxApplyError">;
239
+ /**
240
+ * The applier refused at least one derived operation, so accepting the result
241
+ * would not reproduce the target. Reported instead of returning a package that
242
+ * silently under-represents the difference.
243
+ */
244
+ declare class CompareDocxApplyError extends CompareDocxApplyError_base<{
245
+ message: string;
246
+ skipped: readonly FolioAIEditSkippedOperation[];
247
+ }> {}
248
+ declare const CompareDocxRoundTripError_base: import("better-result").TaggedErrorClass<"CompareDocxRoundTripError">;
249
+ /**
250
+ * The generated tracked changes do not accept back to the target. Raised by
251
+ * the self-check {@link compareDocx} runs before returning: a redline that
252
+ * quietly loses part of the difference is worse than no redline, so the
253
+ * mismatch is surfaced instead of the document.
254
+ */
255
+ declare class CompareDocxRoundTripError extends CompareDocxRoundTripError_base<{
256
+ message: string;
257
+ story: FolioDocumentStoryHandle;
258
+ /** The invariant that did not hold, and what diverged under it. */
259
+ invariant: CompareVerificationInvariant;
260
+ cause: CompareVerificationCause;
261
+ /**
262
+ * Every invariant that failed, not only the one named above: a redline that
263
+ * loses a difference usually loses it in both directions, and a caller
264
+ * deciding what to do next wants the whole list. Each detail is structural,
265
+ * so it is safe to log or quote.
266
+ */
267
+ failures: readonly CompareVerificationFailure[];
268
+ }> {}
269
+ declare const CompareDocxOperationLimitError_base: import("better-result").TaggedErrorClass<"CompareDocxOperationLimitError">;
270
+ /** The difference needs more operations than the engine will generate. */
271
+ declare class CompareDocxOperationLimitError extends CompareDocxOperationLimitError_base<{
272
+ message: string;
273
+ limit: number;
274
+ }> {}
275
+ declare const CompareDocxSerializeError_base: import("better-result").TaggedErrorClass<"CompareDocxSerializeError">;
276
+ declare class CompareDocxSerializeError extends CompareDocxSerializeError_base<{
277
+ message: string;
278
+ cause: unknown;
279
+ }> {}
280
+ type CompareDocxError = CompareDocxApplyError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
281
+ //#endregion
282
+ export { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };