@stll/folio-core 0.32.1 → 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 +692 -75
  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 +235 -7
  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 +42 -9
  100. package/dist/document-operations.js +171 -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 +4 -4
  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,345 @@
1
+ import { getShaper } from "../shaping/shaper.js";
2
+ import { collectResources, renderContentStream } from "./contentStream.js";
3
+ import { prepareFonts } from "./fonts.js";
4
+ import { decodeImage } from "./images.js";
5
+ import { createPdfDocument, pdfArray, pdfAsciiString, pdfDict, pdfFlateStream, pdfName, pdfNull, pdfNumber, pdfNumberArray, pdfStream, pdfTextString } from "./objects.js";
6
+ import { displayPointToPdf, pxToPt } from "./pageSpace.js";
7
+ import { collectUsage, paintPage } from "./paint.js";
8
+ import { Result, TaggedError } from "better-result";
9
+ import { createHash } from "node:crypto";
10
+ //#region src/pdf/writePdf.ts
11
+ /**
12
+ * The PDF backend's entry point.
13
+ *
14
+ * Two calls with the same display list and the same timestamp produce the
15
+ * same bytes. That is a hard requirement rather than a nicety: it is what
16
+ * makes an export diffable, cacheable and comparable against a golden file,
17
+ * and every design decision below that looks fussy (one number formatter,
18
+ * resource names from sorted keys, a `/ID` hashed from the body, a fixed
19
+ * deflate level) exists to keep it true. Nothing here reads a clock or a
20
+ * random source.
21
+ */
22
+ var WritePdfError = class extends TaggedError("WritePdfError") {};
23
+ const DEFAULT_PRODUCER = "folio";
24
+ /** How many code points the strict-coverage refusal names before counting. */
25
+ const UNENCODABLE_SAMPLE = 5;
26
+ const HEX_DIGITS = 4;
27
+ const formatCodePoint = (codePoint) => `U+${codePoint.toString(16).toUpperCase().padStart(HEX_DIGITS, "0")}`;
28
+ const describeUnencodable = (points) => {
29
+ const named = points.slice(0, UNENCODABLE_SAMPLE).map(({ codePoint }) => formatCodePoint(codePoint)).join(", ");
30
+ const rest = points.length - Math.min(points.length, UNENCODABLE_SAMPLE);
31
+ const suffix = rest === 0 ? "" : `, and ${String(rest)} more`;
32
+ return `no supplied face can encode ${String(points.length)} code point${points.length === 1 ? "" : "s"}: ${named}${suffix}`;
33
+ };
34
+ /** Every `/ProcSet` folio's own output can need. */
35
+ const PROC_SET = [
36
+ "PDF",
37
+ "Text",
38
+ "ImageB",
39
+ "ImageC",
40
+ "ImageI"
41
+ ];
42
+ const pad = (value, width) => String(value).padStart(width, "0");
43
+ const YEAR_DIGITS = 4;
44
+ const FIELD_DIGITS = 2;
45
+ /**
46
+ * `D:YYYYMMDDHHmmSS+00'00'`. The instant is written in UTC whatever offset
47
+ * the caller's ISO string carried: one instant has one representation here,
48
+ * so two callers who name the same moment get the same bytes.
49
+ */
50
+ const formatPdfDate = (timestamp) => {
51
+ const milliseconds = Date.parse(timestamp);
52
+ if (Number.isNaN(milliseconds)) return Result.err(new WritePdfError({ message: `timestamp is not an ISO-8601 instant: ${timestamp}` }));
53
+ const date = new Date(milliseconds);
54
+ return Result.ok(`D:${pad(date.getUTCFullYear(), YEAR_DIGITS)}${pad(date.getUTCMonth() + 1, FIELD_DIGITS)}${pad(date.getUTCDate(), FIELD_DIGITS)}${pad(date.getUTCHours(), FIELD_DIGITS)}${pad(date.getUTCMinutes(), FIELD_DIGITS)}${pad(date.getUTCSeconds(), FIELD_DIGITS)}+00'00'`);
55
+ };
56
+ /**
57
+ * A `/URI` is a byte string, so anything above ASCII is percent-encoded as
58
+ * UTF-8 rather than written raw.
59
+ */
60
+ const asciiUri = (href) => {
61
+ const LAST_ASCII = 126;
62
+ let out = "";
63
+ for (const character of href) out += (character.codePointAt(0) ?? 0) <= LAST_ASCII ? character : encodeURIComponent(character);
64
+ return out;
65
+ };
66
+ const buildImageObject = (document, samples) => {
67
+ const alphaRef = samples.alpha === void 0 ? void 0 : document.add(pdfFlateStream([
68
+ ["Type", pdfName("XObject")],
69
+ ["Subtype", pdfName("Image")],
70
+ ["Width", pdfNumber(samples.widthPx)],
71
+ ["Height", pdfNumber(samples.heightPx)],
72
+ ["ColorSpace", pdfName("DeviceGray")],
73
+ ["BitsPerComponent", pdfNumber(samples.alpha.bitsPerComponent)]
74
+ ], samples.alpha.data));
75
+ const entries = [
76
+ ["Type", pdfName("XObject")],
77
+ ["Subtype", pdfName("Image")],
78
+ ["Width", pdfNumber(samples.widthPx)],
79
+ ["Height", pdfNumber(samples.heightPx)],
80
+ ["ColorSpace", samples.colorSpace],
81
+ ["BitsPerComponent", pdfNumber(samples.bitsPerComponent)],
82
+ ["Decode", samples.decode === void 0 ? void 0 : pdfNumberArray(samples.decode)],
83
+ ["SMask", alphaRef]
84
+ ];
85
+ return samples.encoding === "jpeg" ? document.add(pdfStream(pdfDict([...entries, ["Filter", pdfName("DCTDecode")]]), samples.data)) : document.add(pdfFlateStream(entries, samples.data));
86
+ };
87
+ const buildResources = ({ resources, fontRefs, imageRefs }) => {
88
+ const fontEntries = [...resources.fontNames].map(([resourceIndex, name]) => [name, fontRefs.get(resourceIndex)]);
89
+ const imageEntries = [...resources.imageNames].map(([imageIndex, name]) => [name, imageRefs.get(imageIndex)]);
90
+ const stateEntries = [...resources.extGStateNames].map(([alpha, name]) => [name, pdfDict([
91
+ ["Type", pdfName("ExtGState")],
92
+ ["ca", pdfNumber(Number(alpha))],
93
+ ["CA", pdfNumber(Number(alpha))]
94
+ ])]);
95
+ return pdfDict([
96
+ ["ProcSet", pdfArray(PROC_SET.map(pdfName))],
97
+ ["Font", fontEntries.length === 0 ? void 0 : pdfDict(fontEntries)],
98
+ ["XObject", imageEntries.length === 0 ? void 0 : pdfDict(imageEntries)],
99
+ ["ExtGState", stateEntries.length === 0 ? void 0 : pdfDict(stateEntries)]
100
+ ]);
101
+ };
102
+ /**
103
+ * `/XYZ` with a null x and zoom: the reader scrolls the point to the top of
104
+ * the window and leaves the horizontal position and magnification alone.
105
+ */
106
+ const buildDestination = ({ pageRefs, pages, pageIndex, yPx }) => {
107
+ const pageRef = pageRefs[pageIndex];
108
+ const page = pages[pageIndex];
109
+ if (pageRef === void 0 || page === void 0) return Result.err(new WritePdfError({ message: `destination names page ${String(pageIndex)}, which does not exist` }));
110
+ const point = displayPointToPdf(page.heightPx, 0, yPx);
111
+ return Result.ok(pdfArray([
112
+ pageRef,
113
+ pdfName("XYZ"),
114
+ pdfNull,
115
+ pdfNumber(point.y),
116
+ pdfNull
117
+ ]));
118
+ };
119
+ const buildAnnotation = ({ document, link, page, pages, pageRefs }) => {
120
+ const topLeft = displayPointToPdf(page.heightPx, link.rect.xPx, link.rect.yPx);
121
+ const bottomRight = displayPointToPdf(page.heightPx, link.rect.xPx + link.rect.widthPx, link.rect.yPx + link.rect.heightPx);
122
+ let action;
123
+ let destination;
124
+ switch (link.target.kind) {
125
+ case "external":
126
+ action = pdfDict([["S", pdfName("URI")], ["URI", pdfAsciiString(asciiUri(link.target.href))]]);
127
+ break;
128
+ case "page": {
129
+ const built = buildDestination({
130
+ pageRefs,
131
+ pages,
132
+ pageIndex: link.target.pageIndex,
133
+ yPx: link.target.yPx
134
+ });
135
+ if (built.isErr()) return Result.err(built.error);
136
+ destination = built.value;
137
+ break;
138
+ }
139
+ default: {
140
+ const unreachable = link.target;
141
+ return Result.err(new WritePdfError({ message: `unhandled link target: ${JSON.stringify(unreachable)}` }));
142
+ }
143
+ }
144
+ return Result.ok(document.add(pdfDict([
145
+ ["Type", pdfName("Annot")],
146
+ ["Subtype", pdfName("Link")],
147
+ ["Rect", pdfNumberArray([
148
+ Math.min(topLeft.x, bottomRight.x),
149
+ Math.min(topLeft.y, bottomRight.y),
150
+ Math.max(topLeft.x, bottomRight.x),
151
+ Math.max(topLeft.y, bottomRight.y)
152
+ ])],
153
+ ["Border", pdfNumberArray([
154
+ 0,
155
+ 0,
156
+ 0
157
+ ])],
158
+ ["Contents", link.tooltip === void 0 ? void 0 : pdfTextString(link.tooltip)],
159
+ ["A", action],
160
+ ["Dest", destination]
161
+ ])));
162
+ };
163
+ const buildOutlineTree = (entries, document) => {
164
+ const roots = [];
165
+ const ancestors = [];
166
+ for (const entry of entries) {
167
+ const node = {
168
+ entry,
169
+ children: [],
170
+ ref: document.allocate()
171
+ };
172
+ while ((ancestors.at(-1)?.entry.level ?? -1) >= entry.level) ancestors.pop();
173
+ const parent = ancestors.at(-1);
174
+ if (parent === void 0) roots.push(node);
175
+ else parent.children.push(node);
176
+ ancestors.push(node);
177
+ }
178
+ return roots;
179
+ };
180
+ /** Every node is open, so a node's `/Count` is its whole subtree. */
181
+ const openDescendantCount = (nodes) => nodes.reduce((total, node) => total + 1 + openDescendantCount(node.children), 0);
182
+ const defineOutlineNodes = ({ document, nodes, parentRef, pages, pageRefs }) => {
183
+ for (const [index, node] of nodes.entries()) {
184
+ const destination = buildDestination({
185
+ pageRefs,
186
+ pages,
187
+ pageIndex: node.entry.pageIndex,
188
+ yPx: node.entry.yPx
189
+ });
190
+ if (destination.isErr()) return destination.error;
191
+ const childCount = openDescendantCount(node.children);
192
+ document.define(node.ref, pdfDict([
193
+ ["Title", pdfTextString(node.entry.title)],
194
+ ["Parent", parentRef],
195
+ ["Prev", nodes[index - 1]?.ref],
196
+ ["Next", nodes[index + 1]?.ref],
197
+ ["First", node.children.at(0)?.ref],
198
+ ["Last", node.children.at(-1)?.ref],
199
+ ["Count", childCount === 0 ? void 0 : pdfNumber(childCount)],
200
+ ["Dest", destination.value]
201
+ ]));
202
+ const error = defineOutlineNodes({
203
+ document,
204
+ nodes: node.children,
205
+ parentRef: node.ref,
206
+ pages,
207
+ pageRefs
208
+ });
209
+ if (error !== null) return error;
210
+ }
211
+ return null;
212
+ };
213
+ /**
214
+ * Writes the display list as a PDF.
215
+ *
216
+ * Asynchronous for one reason: a run in a script that shapes needs the shaper,
217
+ * and the shaper is fetched the first time a document actually contains one. A
218
+ * document in Latin, Cyrillic or Greek never awaits anything but the resolved
219
+ * promise this returns.
220
+ */
221
+ const writePdf = async (list, options) => {
222
+ if (list.pages.length === 0) return Result.err(new WritePdfError({ message: "a PDF needs at least one page" }));
223
+ const stamp = formatPdfDate(options.timestamp);
224
+ if (stamp.isErr()) return Result.err(stamp.error);
225
+ const usage = collectUsage(list.pages, list.fonts.length, list.images.length);
226
+ if (usage.isErr()) return Result.err(new WritePdfError({
227
+ message: usage.error.message,
228
+ cause: usage.error
229
+ }));
230
+ const document = createPdfDocument();
231
+ const catalogRef = document.allocate();
232
+ const pagesRef = document.allocate();
233
+ const infoRef = document.allocate();
234
+ const pageRefs = list.pages.map(() => document.allocate());
235
+ const shaper = usage.value.shapedRunsByFont.size === 0 ? null : await getShaper();
236
+ const { byFontIndex, fontRefByResourceIndex, substitutions, unencodable } = prepareFonts({
237
+ document,
238
+ faces: list.fonts,
239
+ usedCodePoints: usage.value.codePointsByFont,
240
+ shapedRuns: usage.value.shapedRunsByFont,
241
+ shaper,
242
+ source: options.fonts
243
+ });
244
+ if (options.strictGlyphCoverage === true && unencodable.length > 0) return Result.err(new WritePdfError({ message: describeUnencodable(unencodable) }));
245
+ const imageRefs = /* @__PURE__ */ new Map();
246
+ for (const imageIndex of [...usage.value.imageIndices].sort((left, right) => left - right)) {
247
+ const source = list.images[imageIndex];
248
+ if (source === void 0) return Result.err(new WritePdfError({ message: `image ${String(imageIndex)} is outside the image table` }));
249
+ const decoded = decodeImage(source);
250
+ if (decoded.isErr()) return Result.err(new WritePdfError({
251
+ message: `image ${String(imageIndex)} could not be decoded: ${decoded.error.message}`,
252
+ cause: decoded.error
253
+ }));
254
+ imageRefs.set(imageIndex, buildImageObject(document, decoded.value));
255
+ }
256
+ const outlineRoots = buildOutlineTree(list.outline, document);
257
+ const outlineRef = outlineRoots.length === 0 ? void 0 : document.allocate();
258
+ for (const [pageIndex, page] of list.pages.entries()) {
259
+ const pageRef = pageRefs[pageIndex];
260
+ if (pageRef === void 0) return Result.err(new WritePdfError({ message: `page ${String(pageIndex)} lost its object` }));
261
+ const parts = paintPage({
262
+ page,
263
+ fonts: byFontIndex
264
+ });
265
+ const resources = collectResources(parts);
266
+ const contentRef = document.add(pdfFlateStream([], new TextEncoder().encode(renderContentStream(parts, resources))));
267
+ const annotationRefs = [];
268
+ for (const link of page.links) {
269
+ const annotation = buildAnnotation({
270
+ document,
271
+ link,
272
+ page,
273
+ pages: list.pages,
274
+ pageRefs
275
+ });
276
+ if (annotation.isErr()) return Result.err(annotation.error);
277
+ annotationRefs.push(annotation.value);
278
+ }
279
+ document.define(pageRef, pdfDict([
280
+ ["Type", pdfName("Page")],
281
+ ["Parent", pagesRef],
282
+ ["MediaBox", pdfNumberArray([
283
+ 0,
284
+ 0,
285
+ pxToPt(page.widthPx),
286
+ pxToPt(page.heightPx)
287
+ ])],
288
+ ["Resources", buildResources({
289
+ resources,
290
+ fontRefs: fontRefByResourceIndex,
291
+ imageRefs
292
+ })],
293
+ ["Contents", contentRef],
294
+ ["Annots", annotationRefs.length === 0 ? void 0 : pdfArray(annotationRefs)]
295
+ ]));
296
+ }
297
+ if (outlineRef !== void 0) {
298
+ const error = defineOutlineNodes({
299
+ document,
300
+ nodes: outlineRoots,
301
+ parentRef: outlineRef,
302
+ pages: list.pages,
303
+ pageRefs
304
+ });
305
+ if (error !== null) return Result.err(error);
306
+ document.define(outlineRef, pdfDict([
307
+ ["Type", pdfName("Outlines")],
308
+ ["First", outlineRoots.at(0)?.ref],
309
+ ["Last", outlineRoots.at(-1)?.ref],
310
+ ["Count", pdfNumber(openDescendantCount(outlineRoots))]
311
+ ]));
312
+ }
313
+ document.define(pagesRef, pdfDict([
314
+ ["Type", pdfName("Pages")],
315
+ ["Kids", pdfArray(pageRefs)],
316
+ ["Count", pdfNumber(pageRefs.length)]
317
+ ]));
318
+ document.define(catalogRef, pdfDict([
319
+ ["Type", pdfName("Catalog")],
320
+ ["Pages", pagesRef],
321
+ ["Outlines", outlineRef],
322
+ ["PageMode", outlineRef === void 0 ? void 0 : pdfName("UseOutlines")]
323
+ ]));
324
+ const { metadata } = list;
325
+ document.define(infoRef, pdfDict([
326
+ ["Title", metadata.title === void 0 ? void 0 : pdfTextString(metadata.title)],
327
+ ["Author", metadata.author === void 0 ? void 0 : pdfTextString(metadata.author)],
328
+ ["Subject", metadata.subject === void 0 ? void 0 : pdfTextString(metadata.subject)],
329
+ ["Keywords", metadata.keywords === void 0 ? void 0 : pdfTextString(metadata.keywords)],
330
+ ["Producer", pdfTextString(options.producer ?? DEFAULT_PRODUCER)],
331
+ ["CreationDate", pdfAsciiString(stamp.value)],
332
+ ["ModDate", pdfAsciiString(stamp.value)]
333
+ ]));
334
+ return Result.ok({
335
+ bytes: document.serialize({
336
+ rootRef: catalogRef,
337
+ infoRef,
338
+ idHex: (body) => createHash("sha256").update(body).digest("hex").toUpperCase()
339
+ }),
340
+ substitutions,
341
+ unencodable
342
+ });
343
+ };
344
+ //#endregion
345
+ export { WritePdfError, writePdf };
@@ -4,9 +4,11 @@ import { markStructuralChange } from "../extensions/features/ParagraphChangeTrac
4
4
  import { getFolioNodeRevisionCarriers } from "../revisionCarriers.js";
5
5
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
6
6
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
7
+ import { holdsNoContent } from "../zeroWidthAnchors.js";
7
8
  import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
8
9
  import { hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge } from "./tableCellMergeResolution.js";
9
10
  import { TableMap, removeRow } from "prosemirror-tables";
11
+ import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
10
12
  //#region src/prosemirror/commands/comments.ts
11
13
  /**
12
14
  * Add a comment mark to the current selection.
@@ -150,10 +152,33 @@ function resolveChange(from, to, mode, revisionIds) {
150
152
  continue;
151
153
  }
152
154
  const joinPos = mappedPos + paragraph.nodeSize;
153
- if (joinPos >= tr.doc.content.size) continue;
155
+ const nextNode = joinPos < tr.doc.content.size ? tr.doc.nodeAt(joinPos) : null;
156
+ if (!(nextNode?.type.name === paragraph.type.name && !(carriesSection(paragraph) && carriesSection(nextNode)))) {
157
+ const resolved = tr.doc.resolve(mappedPos);
158
+ const previous = resolved.nodeBefore;
159
+ if ((!carriesSection(paragraph) || previous?.type.name === paragraph.type.name && !carriesSection(previous)) && holdsNoContent(paragraph) && resolved.parent.childCount > 1) {
160
+ if (carriesSection(paragraph) && previous) tr.setNodeMarkup(mappedPos - previous.nodeSize, void 0, {
161
+ ...previous.attrs,
162
+ sectionBreakType: paragraph.attrs["sectionBreakType"],
163
+ _sectionProperties: paragraph.attrs["_sectionProperties"]
164
+ });
165
+ tr.delete(mappedPos, mappedPos + paragraph.nodeSize);
166
+ continue;
167
+ }
168
+ tr.setNodeAttribute(mappedPos, "pPrMark", null);
169
+ continue;
170
+ }
171
+ const formattingOwner = holdsNoContent(paragraph) ? nextNode : paragraph;
172
+ const sectionOwner = carriesSection(paragraph) ? paragraph : nextNode;
173
+ const joinedAttrs = {
174
+ ...formattingOwner.attrs,
175
+ pPrMark: nextNode.attrs["pPrMark"],
176
+ sectionBreakType: sectionOwner.attrs["sectionBreakType"],
177
+ _sectionProperties: sectionOwner.attrs["_sectionProperties"]
178
+ };
154
179
  try {
155
180
  tr.join(joinPos);
156
- tr.setNodeAttribute(mappedPos, "pPrMark", null);
181
+ tr.setNodeMarkup(mappedPos, void 0, joinedAttrs);
157
182
  } catch {}
158
183
  }
159
184
  tableRowStructuralOps.sort((left, right) => right.rowPos - left.rowPos);
@@ -164,6 +189,12 @@ function resolveChange(from, to, mode, revisionIds) {
164
189
  if (!row || row.type.name !== "tableRow") continue;
165
190
  if (op.action === "clear") {
166
191
  tr.setNodeAttribute(mappedPos, op.attrName, null);
192
+ clearTableRowContentMarks({
193
+ tr,
194
+ rowPos: mappedPos,
195
+ markType: keepType,
196
+ revision: op.revision
197
+ });
167
198
  resolvedTableRowStructure = true;
168
199
  continue;
169
200
  }
@@ -229,7 +260,8 @@ function collectTableRowStructuralOp(node, rowPos, mode, revisionSet) {
229
260
  return {
230
261
  rowPos,
231
262
  attrName,
232
- action: attrName === "trIns" === (mode === "accept") ? "clear" : "remove"
263
+ action: attrName === "trIns" === (mode === "accept") ? "clear" : "remove",
264
+ revision: marker
233
265
  };
234
266
  }
235
267
  return null;
@@ -237,6 +269,43 @@ function collectTableRowStructuralOp(node, rowPos, mode, revisionSet) {
237
269
  function isTableRowRevisionAttr(value) {
238
270
  return typeof value === "object" && value !== null && "revisionId" in value && typeof value.revisionId === "number";
239
271
  }
272
+ /**
273
+ * Whether an inline revision mark inside a row belongs to that row's own
274
+ * structural revision. Folio writes both halves under one id; Word mints a
275
+ * fresh `w:id` per element, so an author + timestamp match counts too. A third
276
+ * party's edit inside the same row matches neither and survives untouched.
277
+ */
278
+ function markBelongsToRowRevision(mark, revision) {
279
+ if (mark.attrs["revisionId"] === revision.revisionId) return true;
280
+ return revision.author !== void 0 && mark.attrs["author"] === revision.author && (mark.attrs["date"] ?? null) === (revision.date ?? null);
281
+ }
282
+ /**
283
+ * Clear the run-level revision marks a row's structural revision wrote.
284
+ *
285
+ * A tracked row insertion or deletion is marked twice — on the row and around
286
+ * every run in its cells — so resolving one half and leaving the other would
287
+ * hand back a row whose text still reads as inserted (or struck through) after
288
+ * the change was accepted. Both halves resolve in the caller's transaction.
289
+ */
290
+ function clearTableRowContentMarks({ tr, rowPos, markType, revision }) {
291
+ const row = tr.doc.nodeAt(rowPos);
292
+ if (!row || !markType) return;
293
+ const contentFrom = rowPos + 1;
294
+ const removals = [];
295
+ row.descendants((node, offset) => {
296
+ if (!node.isInline) return true;
297
+ for (const mark of node.marks) if (mark.type === markType && markBelongsToRowRevision(mark, revision)) {
298
+ const from = contentFrom + offset;
299
+ removals.push({
300
+ from,
301
+ to: from + node.nodeSize,
302
+ mark
303
+ });
304
+ }
305
+ return false;
306
+ });
307
+ for (const removal of removals) tr.removeMark(removal.from, removal.to, removal.mark);
308
+ }
240
309
  function collectTableCellStructuralOps(node, cellPos, mode, revisionSet) {
241
310
  const operations = [];
242
311
  const marker = node.attrs["cellMarker"];
@@ -319,7 +388,7 @@ function collectPPrMarkOp(node, pos, from, to, mode, revisionSet) {
319
388
  if (revisionSet !== null && !revisionSet.has(pPrMark.info.id)) return null;
320
389
  return {
321
390
  paragraphPos: pos,
322
- action: pPrMark.kind === "ins" === (mode === "accept") ? "clear" : "join"
391
+ action: paragraphMarkWasAdded(pPrMark.kind) === (mode === "accept") ? "clear" : "join"
323
392
  };
324
393
  }
325
394
  function rangeCoversParagraphBoundary(from, to, pos, node) {
@@ -374,10 +443,26 @@ function isPPrMarkAttr(value) {
374
443
  if (typeof value !== "object" || value === null) return false;
375
444
  const kind = value.kind;
376
445
  const info = value.info;
377
- if (kind !== "ins" && kind !== "del") return false;
446
+ if (!PARAGRAPH_MARK_CHANGE_KINDS.some((allowed) => allowed === kind)) return false;
378
447
  if (typeof info !== "object" || info === null) return false;
379
448
  return typeof info.id === "number";
380
449
  }
450
+ /**
451
+ * Whether the mark says the paragraph break was ADDED. A relocation's
452
+ * destination break was added exactly as an insertion's was, and its source
453
+ * break went exactly as a deletion's did; the kinds differ so a reader is told
454
+ * the two ends belong together, not because they resolve differently.
455
+ */
456
+ const paragraphMarkWasAdded = (kind) => kind === "ins" || kind === "moveTo";
457
+ /**
458
+ * Whether the paragraph's mark carries a section's properties.
459
+ *
460
+ * A section break lives on a paragraph mark, so the paragraph is where the
461
+ * section ends. Removing it removes the section: a document that had two ends
462
+ * up with one, and every setting the dropped section carried — page size,
463
+ * margins, its header and footer references — goes with it.
464
+ */
465
+ const carriesSection = (paragraph) => paragraph.attrs["sectionBreakType"] != null || paragraph.attrs["_sectionProperties"] != null;
381
466
  function readRevisionInfo(info) {
382
467
  const revision = {};
383
468
  if (typeof info?.author === "string") revision.author = info.author;
@@ -417,7 +502,7 @@ function findParagraphBoundaryChangeAtPosition(state, pos) {
417
502
  if (node.type.name !== "paragraph") return null;
418
503
  const paragraphPos = $pos.before($pos.depth);
419
504
  const pPrMark = node.attrs["pPrMark"];
420
- if (isPPrMarkAttr(pPrMark)) return toParagraphBoundaryChange(node, paragraphPos, pPrMark.kind === "ins" ? "insertion" : "deletion", pPrMark.info);
505
+ if (isPPrMarkAttr(pPrMark)) return toParagraphBoundaryChange(node, paragraphPos, paragraphMarkWasAdded(pPrMark.kind) ? "insertion" : "deletion", pPrMark.info);
421
506
  const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
422
507
  if (!Array.isArray(propertyChanges)) return null;
423
508
  for (const change of propertyChanges) {
@@ -666,25 +666,26 @@ function paragraphAttrsToFormatting(attrs) {
666
666
  return f;
667
667
  }
668
668
  function createTrackedRunWrapper(type, info, child) {
669
+ const content = child ? [child] : [];
669
670
  if (type === "insertion") return {
670
671
  type,
671
672
  info,
672
- content: [child]
673
+ content
673
674
  };
674
675
  if (type === "deletion") return {
675
676
  type,
676
677
  info,
677
- content: [child]
678
+ content
678
679
  };
679
680
  if (type === "moveFrom") return {
680
681
  type,
681
682
  info,
682
- content: [child]
683
+ content
683
684
  };
684
685
  return {
685
686
  type,
686
687
  info,
687
- content: [child]
688
+ content
688
689
  };
689
690
  }
690
691
  function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false) {
@@ -824,16 +825,19 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
824
825
  const trackedChangeKey = `${type}:${JSON.stringify(info)}`;
825
826
  if (linkMark) {
826
827
  const linkKey = getLinkKey(linkMark);
827
- if (!currentTrackedChange || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
828
+ if (!currentTrackedChange || currentTrackedChange.type !== "hyperlink" || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
828
829
  const hyperlink = createHyperlink(linkMark);
829
830
  const wrapper = createTrackedRunWrapper(type, info, hyperlink);
830
831
  content.push(wrapper);
831
832
  currentTrackedChange = {
833
+ type: "hyperlink",
832
834
  key: trackedChangeKey,
835
+ wrapper,
833
836
  hyperlink,
834
837
  hyperlinkKey: linkKey
835
838
  };
836
839
  }
840
+ if (currentTrackedChange.type !== "hyperlink") panic("A tracked hyperlink lost its serialization parent");
837
841
  if (node.type.name === "bookmarkBoundary") {
838
842
  addNodeToHyperlink({
839
843
  hyperlink: currentTrackedChange.hyperlink,
@@ -850,13 +854,43 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
850
854
  if (run) currentTrackedChange.hyperlink.children.push(run);
851
855
  return;
852
856
  }
853
- currentTrackedChange = void 0;
857
+ if (!currentTrackedChange || currentTrackedChange.type !== "direct" || currentTrackedChange.key !== trackedChangeKey) {
858
+ const wrapper = createTrackedRunWrapper(type, info);
859
+ content.push(wrapper);
860
+ currentTrackedChange = {
861
+ type: "direct",
862
+ key: trackedChangeKey,
863
+ wrapper
864
+ };
865
+ }
866
+ if (node.type.name === "bookmarkBoundary") {
867
+ const attrs = expectBookmarkBoundaryAttrs(node);
868
+ const boundary = attrs.type === "start" ? {
869
+ type: "bookmarkStart",
870
+ id: attrs.id,
871
+ name: attrs.name,
872
+ ...attrs.colFirst !== void 0 ? { colFirst: attrs.colFirst } : {},
873
+ ...attrs.colLast !== void 0 ? { colLast: attrs.colLast } : {}
874
+ } : {
875
+ type: "bookmarkEnd",
876
+ id: attrs.id
877
+ };
878
+ currentTrackedChange.wrapper.content.push(boundary);
879
+ return;
880
+ }
881
+ if (node.type.name === "field" || node.type.name === "structuredField") {
882
+ currentTrackedChange.wrapper.content.push(createFieldFromNode(node, {
883
+ marks: otherMarks,
884
+ textBoxAnchorMarkers
885
+ }));
886
+ return;
887
+ }
854
888
  const run = createTrackedChangeRun({
855
889
  inheritedFormatting,
856
890
  marks: otherMarks,
857
891
  node
858
892
  });
859
- if (run) content.push(createTrackedRunWrapper(type, info, run));
893
+ if (run) currentTrackedChange.wrapper.content.push(run);
860
894
  return;
861
895
  }
862
896
  if (noteRefMark && !linkMark) {