@stll/folio-core 0.32.2 → 0.33.1

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 (227) 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 +652 -86
  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 +56 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +136 -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 +412 -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 +1141 -0
  37. package/dist/compare/reproducible-package.d.ts +21 -0
  38. package/dist/compare/reproducible-package.js +47 -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 +297 -0
  42. package/dist/compare/types.js +42 -0
  43. package/dist/compare/verification.d.ts +83 -0
  44. package/dist/compare/verification.js +189 -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/hyperlinkParser.d.ts +9 -1
  104. package/dist/docx/hyperlinkParser.js +19 -13
  105. package/dist/docx/packageParts.d.ts +35 -0
  106. package/dist/docx/packageParts.js +120 -0
  107. package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
  108. package/dist/docx/paraIdRangeNormalization.js +64 -0
  109. package/dist/docx/paragraphParser.js +101 -26
  110. package/dist/docx/paragraphTraversal.js +1 -1
  111. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  112. package/dist/docx/revisionIdNormalization.d.ts +27 -0
  113. package/dist/docx/revisionIdNormalization.js +121 -0
  114. package/dist/docx/rezip.d.ts +6 -0
  115. package/dist/docx/rezip.js +51 -19
  116. package/dist/docx/selectiveSave.js +2 -2
  117. package/dist/docx/serializer/commentSerializer.js +6 -1
  118. package/dist/docx/serializer/paragraphSerializer.js +73 -20
  119. package/dist/docx/serializer/runSerializer.js +11 -10
  120. package/dist/docx/serializer/tableSerializer.js +26 -10
  121. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  122. package/dist/docx/server/createBilingualDocx.js +3 -2
  123. package/dist/docx/streamingXmlParser.d.ts +13 -1
  124. package/dist/docx/streamingXmlParser.js +49 -11
  125. package/dist/docx/unzip.d.ts +1 -2
  126. package/dist/docx/unzip.js +25 -6
  127. package/dist/docx/xmlParser.d.ts +3 -1
  128. package/dist/docx/xmlParser.js +33 -27
  129. package/dist/export-pdf.d.ts +43 -0
  130. package/dist/export-pdf.js +98 -0
  131. package/dist/fonts/headlessMeasure.d.ts +76 -0
  132. package/dist/fonts/headlessMeasure.js +0 -0
  133. package/dist/fonts/sfnt/parse.d.ts +51 -0
  134. package/dist/fonts/sfnt/parse.js +525 -0
  135. package/dist/fonts/sfnt/subset.d.ts +20 -0
  136. package/dist/fonts/sfnt/subset.js +350 -0
  137. package/dist/fonts/sfnt/tables.d.ts +88 -0
  138. package/dist/fonts/sfnt/tables.js +131 -0
  139. package/dist/fonts/sfnt/woff.d.ts +14 -0
  140. package/dist/fonts/sfnt/woff.js +163 -0
  141. package/dist/generated/text_shaper.js +324 -0
  142. package/dist/generated/text_shaper_bg.wasm +0 -0
  143. package/dist/headless-layout.d.ts +75 -0
  144. package/dist/headless-layout.js +350 -0
  145. package/dist/index.d.ts +9 -6
  146. package/dist/index.js +6 -3
  147. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  148. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  149. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  150. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  151. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  152. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  153. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  154. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  155. package/dist/layout-engine/measure/cache.d.ts +2 -0
  156. package/dist/layout-engine/measure/measureContainer.js +13 -0
  157. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  158. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  159. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  160. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  161. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  162. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  163. package/dist/layout-engine/types.d.ts +8 -0
  164. package/dist/layout-painter/renderPage.d.ts +29 -1
  165. package/dist/layout-painter/renderPage.js +47 -19
  166. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  167. package/dist/layout-painter/renderParagraph.js +1 -1
  168. package/dist/layout-painter/renderTable.js +37 -4
  169. package/dist/managers/editorShortcuts.d.ts +33 -1
  170. package/dist/managers/editorShortcuts.js +20 -1
  171. package/dist/markdown/renderRuns.js +7 -2
  172. package/dist/markdown/renderTable.js +5 -1
  173. package/dist/model.d.ts +3 -3
  174. package/dist/model.js +2 -2
  175. package/dist/pdf/contentStream.d.ts +73 -0
  176. package/dist/pdf/contentStream.js +143 -0
  177. package/dist/pdf/fonts.d.ts +96 -0
  178. package/dist/pdf/fonts.js +593 -0
  179. package/dist/pdf/images.d.ts +28 -0
  180. package/dist/pdf/images.js +439 -0
  181. package/dist/pdf/objects.d.ts +93 -0
  182. package/dist/pdf/objects.js +237 -0
  183. package/dist/pdf/pageSpace.d.ts +43 -0
  184. package/dist/pdf/pageSpace.js +57 -0
  185. package/dist/pdf/paint.d.ts +32 -0
  186. package/dist/pdf/paint.js +442 -0
  187. package/dist/pdf/writePdf.d.ts +56 -0
  188. package/dist/pdf/writePdf.js +345 -0
  189. package/dist/prosemirror/commands/comments.js +96 -6
  190. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  191. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  192. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  193. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  194. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  195. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  196. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  198. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  199. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  200. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  201. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  202. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  203. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  204. package/dist/prosemirror/revisionCarriers.js +19 -2
  205. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  206. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  207. package/dist/prosemirror/validation.js +55 -27
  208. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  209. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  210. package/dist/redline.js +12 -79
  211. package/dist/server.d.ts +3 -3
  212. package/dist/server.js +2 -2
  213. package/dist/shaping/placeRun.d.ts +51 -0
  214. package/dist/shaping/placeRun.js +84 -0
  215. package/dist/shaping/shaper.d.ts +92 -0
  216. package/dist/shaping/shaper.js +102 -0
  217. package/dist/types/block-id.d.ts +18 -2
  218. package/dist/types/block-id.js +49 -5
  219. package/dist/types/content.d.ts +2 -2
  220. package/dist/utils/clipboard.js +4 -3
  221. package/dist/utils/fontResolver.js +183 -0
  222. package/dist/utils/htmlComments.d.ts +17 -0
  223. package/dist/utils/htmlComments.js +22 -0
  224. package/dist/utils/scriptSegments.d.ts +12 -1
  225. package/dist/utils/scriptSegments.js +17 -1
  226. package/dist/version-comparison.d.ts +1 -1
  227. 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,36 @@ 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
+ const sectionCanMoveBack = !carriesSection(paragraph) || previous?.type.name === paragraph.type.name && !carriesSection(previous);
160
+ const endsItsContainer = resolved.index() === resolved.parent.childCount - 1;
161
+ const canGo = op.markWasAdded || !endsItsContainer;
162
+ if (sectionCanMoveBack && holdsNoContent(paragraph) && canGo && resolved.parent.childCount > 1) {
163
+ if (carriesSection(paragraph) && previous) tr.setNodeMarkup(mappedPos - previous.nodeSize, void 0, {
164
+ ...previous.attrs,
165
+ sectionBreakType: paragraph.attrs["sectionBreakType"],
166
+ _sectionProperties: paragraph.attrs["_sectionProperties"]
167
+ });
168
+ tr.delete(mappedPos, mappedPos + paragraph.nodeSize);
169
+ continue;
170
+ }
171
+ tr.setNodeAttribute(mappedPos, "pPrMark", null);
172
+ continue;
173
+ }
174
+ const formattingOwner = holdsNoContent(paragraph) ? nextNode : paragraph;
175
+ const sectionOwner = carriesSection(paragraph) ? paragraph : nextNode;
176
+ const joinedAttrs = {
177
+ ...formattingOwner.attrs,
178
+ pPrMark: nextNode.attrs["pPrMark"],
179
+ sectionBreakType: sectionOwner.attrs["sectionBreakType"],
180
+ _sectionProperties: sectionOwner.attrs["_sectionProperties"]
181
+ };
154
182
  try {
155
183
  tr.join(joinPos);
156
- tr.setNodeAttribute(mappedPos, "pPrMark", null);
184
+ tr.setNodeMarkup(mappedPos, void 0, joinedAttrs);
157
185
  } catch {}
158
186
  }
159
187
  tableRowStructuralOps.sort((left, right) => right.rowPos - left.rowPos);
@@ -164,6 +192,12 @@ function resolveChange(from, to, mode, revisionIds) {
164
192
  if (!row || row.type.name !== "tableRow") continue;
165
193
  if (op.action === "clear") {
166
194
  tr.setNodeAttribute(mappedPos, op.attrName, null);
195
+ clearTableRowContentMarks({
196
+ tr,
197
+ rowPos: mappedPos,
198
+ markType: keepType,
199
+ revision: op.revision
200
+ });
167
201
  resolvedTableRowStructure = true;
168
202
  continue;
169
203
  }
@@ -229,7 +263,8 @@ function collectTableRowStructuralOp(node, rowPos, mode, revisionSet) {
229
263
  return {
230
264
  rowPos,
231
265
  attrName,
232
- action: attrName === "trIns" === (mode === "accept") ? "clear" : "remove"
266
+ action: attrName === "trIns" === (mode === "accept") ? "clear" : "remove",
267
+ revision: marker
233
268
  };
234
269
  }
235
270
  return null;
@@ -237,6 +272,43 @@ function collectTableRowStructuralOp(node, rowPos, mode, revisionSet) {
237
272
  function isTableRowRevisionAttr(value) {
238
273
  return typeof value === "object" && value !== null && "revisionId" in value && typeof value.revisionId === "number";
239
274
  }
275
+ /**
276
+ * Whether an inline revision mark inside a row belongs to that row's own
277
+ * structural revision. Folio writes both halves under one id; Word mints a
278
+ * fresh `w:id` per element, so an author + timestamp match counts too. A third
279
+ * party's edit inside the same row matches neither and survives untouched.
280
+ */
281
+ function markBelongsToRowRevision(mark, revision) {
282
+ if (mark.attrs["revisionId"] === revision.revisionId) return true;
283
+ return revision.author !== void 0 && mark.attrs["author"] === revision.author && (mark.attrs["date"] ?? null) === (revision.date ?? null);
284
+ }
285
+ /**
286
+ * Clear the run-level revision marks a row's structural revision wrote.
287
+ *
288
+ * A tracked row insertion or deletion is marked twice — on the row and around
289
+ * every run in its cells — so resolving one half and leaving the other would
290
+ * hand back a row whose text still reads as inserted (or struck through) after
291
+ * the change was accepted. Both halves resolve in the caller's transaction.
292
+ */
293
+ function clearTableRowContentMarks({ tr, rowPos, markType, revision }) {
294
+ const row = tr.doc.nodeAt(rowPos);
295
+ if (!row || !markType) return;
296
+ const contentFrom = rowPos + 1;
297
+ const removals = [];
298
+ row.descendants((node, offset) => {
299
+ if (!node.isInline) return true;
300
+ for (const mark of node.marks) if (mark.type === markType && markBelongsToRowRevision(mark, revision)) {
301
+ const from = contentFrom + offset;
302
+ removals.push({
303
+ from,
304
+ to: from + node.nodeSize,
305
+ mark
306
+ });
307
+ }
308
+ return false;
309
+ });
310
+ for (const removal of removals) tr.removeMark(removal.from, removal.to, removal.mark);
311
+ }
240
312
  function collectTableCellStructuralOps(node, cellPos, mode, revisionSet) {
241
313
  const operations = [];
242
314
  const marker = node.attrs["cellMarker"];
@@ -317,9 +389,11 @@ function collectPPrMarkOp(node, pos, from, to, mode, revisionSet) {
317
389
  const pPrMark = node.attrs["pPrMark"];
318
390
  if (!isPPrMarkAttr(pPrMark)) return null;
319
391
  if (revisionSet !== null && !revisionSet.has(pPrMark.info.id)) return null;
392
+ const markWasAdded = paragraphMarkWasAdded(pPrMark.kind);
320
393
  return {
321
394
  paragraphPos: pos,
322
- action: pPrMark.kind === "ins" === (mode === "accept") ? "clear" : "join"
395
+ action: markWasAdded === (mode === "accept") ? "clear" : "join",
396
+ markWasAdded
323
397
  };
324
398
  }
325
399
  function rangeCoversParagraphBoundary(from, to, pos, node) {
@@ -374,10 +448,26 @@ function isPPrMarkAttr(value) {
374
448
  if (typeof value !== "object" || value === null) return false;
375
449
  const kind = value.kind;
376
450
  const info = value.info;
377
- if (kind !== "ins" && kind !== "del") return false;
451
+ if (!PARAGRAPH_MARK_CHANGE_KINDS.some((allowed) => allowed === kind)) return false;
378
452
  if (typeof info !== "object" || info === null) return false;
379
453
  return typeof info.id === "number";
380
454
  }
455
+ /**
456
+ * Whether the mark says the paragraph break was ADDED. A relocation's
457
+ * destination break was added exactly as an insertion's was, and its source
458
+ * break went exactly as a deletion's did; the kinds differ so a reader is told
459
+ * the two ends belong together, not because they resolve differently.
460
+ */
461
+ const paragraphMarkWasAdded = (kind) => kind === "ins" || kind === "moveTo";
462
+ /**
463
+ * Whether the paragraph's mark carries a section's properties.
464
+ *
465
+ * A section break lives on a paragraph mark, so the paragraph is where the
466
+ * section ends. Removing it removes the section: a document that had two ends
467
+ * up with one, and every setting the dropped section carried — page size,
468
+ * margins, its header and footer references — goes with it.
469
+ */
470
+ const carriesSection = (paragraph) => paragraph.attrs["sectionBreakType"] != null || paragraph.attrs["_sectionProperties"] != null;
381
471
  function readRevisionInfo(info) {
382
472
  const revision = {};
383
473
  if (typeof info?.author === "string") revision.author = info.author;
@@ -417,7 +507,7 @@ function findParagraphBoundaryChangeAtPosition(state, pos) {
417
507
  if (node.type.name !== "paragraph") return null;
418
508
  const paragraphPos = $pos.before($pos.depth);
419
509
  const pPrMark = node.attrs["pPrMark"];
420
- if (isPPrMarkAttr(pPrMark)) return toParagraphBoundaryChange(node, paragraphPos, pPrMark.kind === "ins" ? "insertion" : "deletion", pPrMark.info);
510
+ if (isPPrMarkAttr(pPrMark)) return toParagraphBoundaryChange(node, paragraphPos, paragraphMarkWasAdded(pPrMark.kind) ? "insertion" : "deletion", pPrMark.info);
421
511
  const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
422
512
  if (!Array.isArray(propertyChanges)) return null;
423
513
  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) {