@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,620 @@
1
+ import { STROKE_DASH_FACTORS, walkRegions } from "../primitives.js";
2
+ import { panic } from "better-result";
3
+ //#region src/display-list/dom/renderDisplayListToDom.ts
4
+ /**
5
+ * The DOM backend for the display list.
6
+ *
7
+ * Every mark is positioned absolutely from the coordinates the producer
8
+ * already decided. Nothing here re-derives a position from inline flow
9
+ * (`text-align`, `text-indent`, floats, line boxes): a second opinion about
10
+ * where a glyph goes is the divergence the display list exists to remove.
11
+ *
12
+ * A glyph run is one element, and the display list places its origin. Where
13
+ * each glyph lands inside the run is the browser's, because shaping is the
14
+ * only thing that forms a ligature, applies a kern, or picks a cursive
15
+ * letter's initial, medial or final form, and none of those can be recovered
16
+ * from advances alone. The painted extent of a run can therefore differ from
17
+ * its declared extent by the shaper's rounding.
18
+ *
19
+ * That residue is measured rather than assumed. It is small when the list was
20
+ * built by the same engine that paints it, which is the editor's own path, and
21
+ * larger when a list built from font-table advances is painted by a shaper
22
+ * that kerns; the harness reports both and gates only on the first.
23
+ *
24
+ * This module takes a type-only edge to the paint IR and a runtime edge to its
25
+ * companions in `../primitives`, which carry no layout fact. It has no access
26
+ * to the layout engine, so a fact it needs and the display list does not carry
27
+ * has to be added to the contract, where the PDF backend sees it too.
28
+ *
29
+ * `DisplayList.unsupported` is not paint and is not consumed here: the caller
30
+ * surfaces it, because a page missing marks must say so.
31
+ *
32
+ * Object URLs minted for images and embedded fonts outlive this call, one per
33
+ * image and one per embedded face for the whole render. A caller that discards
34
+ * the returned elements should revoke them (`img[src^="blob:"]` and the `src`
35
+ * of each `@font-face` rule).
36
+ */
37
+ const PAGE_CLASS_NAME = "layout-page";
38
+ /**
39
+ * Anchor ids are the display list's own page indices, not `pageNumber`, which
40
+ * the engine may restart or skip. One numbering, so a link target and the id
41
+ * it addresses cannot drift apart.
42
+ */
43
+ const PAGE_ELEMENT_ID_PREFIX = "page-";
44
+ const DEGREES_PER_RADIAN = 180 / Math.PI;
45
+ const BASE64_CHUNK_SIZE = 32768;
46
+ const IMAGE_MIME_TYPES = {
47
+ png: "image/png",
48
+ jpeg: "image/jpeg"
49
+ };
50
+ /**
51
+ * The display list does not say whether an embedded face is sfnt or WOFF, so a
52
+ * data URL declares neither and the browser sniffs; an `@font-face` `src` needs
53
+ * no `format()` hint to load.
54
+ */
55
+ const FONT_MIME_TYPE = "application/octet-stream";
56
+ /**
57
+ * Which implementation draws a rect's stroke. Every pattern with a period or a
58
+ * second line goes through the `line` renderer, so a dashed cell border and a
59
+ * dashed underline cannot come out as two different dashes. `solid` has no
60
+ * pattern to get wrong, so a CSS border expresses it exactly and cheaply.
61
+ */
62
+ const RECT_STROKE_PAINTERS = {
63
+ solid: "border",
64
+ dashed: "edges",
65
+ dotted: "edges",
66
+ double: "edges",
67
+ wavy: "edges"
68
+ };
69
+ const px = (value) => `${value}px`;
70
+ /**
71
+ * `rgb()` when opaque so serialized styles stay stable and diffable against
72
+ * the PDF backend's colour dump.
73
+ */
74
+ const cssColor = ({ r, g, b, a }) => a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
75
+ const quoteFontFamily = (family) => `"${family.replaceAll("\"", "\\\"")}"`;
76
+ /**
77
+ * An embedded face wins over any installed face of the same name by being
78
+ * named first, under the id its `@font-face` rule was registered with.
79
+ */
80
+ /**
81
+ * The stack the measurer resolved, rebuilt in CSS.
82
+ *
83
+ * Every entry, not just the first: the browser picks the first family it has,
84
+ * and so did the measurement. A shorter stack here paints a different face on
85
+ * any host missing the first one.
86
+ */
87
+ const fontFamilyStack = ({ family, fallbacks, generic, embedded }) => {
88
+ const resolved = `${[family, ...fallbacks].map(quoteFontFamily).join(", ")}, ${generic}`;
89
+ return embedded === void 0 ? resolved : `${quoteFontFamily(embedded.id)}, ${resolved}`;
90
+ };
91
+ const resolveFont = (ref, fonts) => {
92
+ const face = ref < 0 ? void 0 : fonts.at(ref);
93
+ if (face === void 0) panic(`renderDisplayListToDom: font ref ${ref} is out of range (${fonts.length} faces)`);
94
+ return face;
95
+ };
96
+ const resolveImage = (ref, images) => {
97
+ const source = ref < 0 ? void 0 : images.at(ref);
98
+ if (source === void 0) panic(`renderDisplayListToDom: image ref ${ref} is out of range (${images.length} images)`);
99
+ return source;
100
+ };
101
+ const toBase64 = (bytes) => {
102
+ let binary = "";
103
+ for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK_SIZE) binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK_SIZE));
104
+ return btoa(binary);
105
+ };
106
+ /** A `Blob` part cannot be backed by shared memory, so prove it is not. */
107
+ const isBlobPart = (bytes) => bytes.buffer instanceof ArrayBuffer;
108
+ const binarySrc = (bytes, mimeType) => {
109
+ if (isBlobPart(bytes) && typeof Blob === "function" && typeof URL !== "undefined" && typeof URL.createObjectURL === "function") return URL.createObjectURL(new Blob([bytes], { type: mimeType }));
110
+ if (typeof btoa === "function") return `data:${mimeType};base64,${toBase64(bytes)}`;
111
+ panic("renderDisplayListToDom: no Blob or btoa to build a binary source from");
112
+ };
113
+ const fontFaceRule = (face, embedded, src) => `@font-face { font-family: ${quoteFontFamily(embedded.id)}; src: url("${src}"); font-weight: ${face.weight}; font-style: ${face.italic ? "italic" : "normal"}; }`;
114
+ /**
115
+ * `@font-face` rules for the faces whose bytes travel with the list. Resolved
116
+ * once per render and shared by every page: an id maps to one blob, so the
117
+ * bytes are held once and one revoke releases them.
118
+ */
119
+ const embeddedFontFaceCss = (fonts) => {
120
+ const srcById = /* @__PURE__ */ new Map();
121
+ const rules = [];
122
+ for (const face of fonts) {
123
+ const embedded = face.embedded;
124
+ if (embedded === void 0) continue;
125
+ const cached = srcById.get(embedded.id);
126
+ const src = cached ?? binarySrc(embedded.bytes, FONT_MIME_TYPE);
127
+ if (cached === void 0) srcById.set(embedded.id, src);
128
+ rules.push(fontFaceRule(face, embedded, src));
129
+ }
130
+ return rules.join("\n");
131
+ };
132
+ const createAbsoluteDiv = (context, rect) => {
133
+ const element = context.doc.createElement("div");
134
+ element.style.position = "absolute";
135
+ element.style.left = px(rect.xPx - context.originXPx);
136
+ element.style.top = px(rect.yPx - context.originYPx);
137
+ element.style.width = px(rect.widthPx);
138
+ element.style.height = px(rect.heightPx);
139
+ return element;
140
+ };
141
+ const createFilledDiv = (context, rect, fill) => {
142
+ const element = createAbsoluteDiv(context, rect);
143
+ if (fill !== void 0) element.style.backgroundColor = cssColor(fill);
144
+ return element;
145
+ };
146
+ /**
147
+ * A group that transforms its children without changing their coordinates: a
148
+ * zero-sized positioned box at the container's origin is the containing block,
149
+ * so a child keeps the coordinates it had one level up.
150
+ */
151
+ const createTransparentGroupDiv = (context) => {
152
+ const element = context.doc.createElement("div");
153
+ element.style.position = "absolute";
154
+ element.style.left = "0px";
155
+ element.style.top = "0px";
156
+ element.style.width = "0px";
157
+ element.style.height = "0px";
158
+ return element;
159
+ };
160
+ const HORIZONTAL_BAR_AXES = {
161
+ main: "to right",
162
+ cross: "to bottom"
163
+ };
164
+ const VERTICAL_BAR_AXES = {
165
+ main: "to bottom",
166
+ cross: "to right"
167
+ };
168
+ /**
169
+ * How far a stroke reaches across its path. A dash pattern stays within the
170
+ * thickness; `double` spans two lines plus the shared gap, and `wavy` the peak
171
+ * to peak amplitude plus the thickness the curve itself is drawn with.
172
+ */
173
+ const strokeCrossExtentPx = (stroke) => {
174
+ switch (stroke.pattern) {
175
+ case "solid":
176
+ case "dashed":
177
+ case "dotted": return stroke.thicknessPx;
178
+ case "double": return stroke.thicknessPx * 3;
179
+ case "wavy": return stroke.thicknessPx * 3;
180
+ default: {
181
+ const unreachable = stroke.pattern;
182
+ panic(`renderDisplayListToDom: unhandled stroke pattern ${JSON.stringify(unreachable)}`);
183
+ }
184
+ }
185
+ };
186
+ const repeatingBars = (color, axis, barPx, gapPx) => `repeating-linear-gradient(${axis}, ${color} 0px, ${color} ${barPx}px, transparent ${barPx}px, transparent ${barPx + gapPx}px)`;
187
+ const strokeBackground = (stroke, axes) => {
188
+ const color = cssColor(stroke.color);
189
+ switch (stroke.pattern) {
190
+ case "solid": return color;
191
+ case "dashed":
192
+ case "dotted": {
193
+ const { dash, gap } = STROKE_DASH_FACTORS[stroke.pattern];
194
+ return repeatingBars(color, axes.main, stroke.thicknessPx * dash, stroke.thicknessPx * gap);
195
+ }
196
+ case "double": {
197
+ const rulePercent = 100 / 3;
198
+ return `linear-gradient(${axes.cross}, ${color} 0%, ${color} ${rulePercent}%, transparent ${rulePercent}%, transparent ${100 - rulePercent}%, ${color} ${100 - rulePercent}%)`;
199
+ }
200
+ case "wavy": {
201
+ const halfPeriod = stroke.thicknessPx * 6 / 2;
202
+ return repeatingBars(color, "45deg", halfPeriod, halfPeriod);
203
+ }
204
+ default: {
205
+ const unreachable = stroke.pattern;
206
+ panic(`renderDisplayListToDom: unhandled stroke pattern ${JSON.stringify(unreachable)}`);
207
+ }
208
+ }
209
+ };
210
+ /** Strokes are centred on their path, so half the cross extent falls each side. */
211
+ const paintLineSegment = (line, context) => {
212
+ const { x1Px, y1Px, x2Px, y2Px, stroke } = line;
213
+ const crossExtentPx = strokeCrossExtentPx(stroke);
214
+ const halfCrossExtentPx = crossExtentPx / 2;
215
+ const element = context.doc.createElement("div");
216
+ element.style.position = "absolute";
217
+ element.style.boxSizing = "content-box";
218
+ if (y1Px === y2Px) {
219
+ element.style.left = px(Math.min(x1Px, x2Px) - context.originXPx);
220
+ element.style.top = px(y1Px - halfCrossExtentPx - context.originYPx);
221
+ element.style.width = px(Math.abs(x2Px - x1Px));
222
+ element.style.height = px(crossExtentPx);
223
+ element.style.background = strokeBackground(stroke, HORIZONTAL_BAR_AXES);
224
+ } else if (x1Px === x2Px) {
225
+ element.style.left = px(x1Px - halfCrossExtentPx - context.originXPx);
226
+ element.style.top = px(Math.min(y1Px, y2Px) - context.originYPx);
227
+ element.style.width = px(crossExtentPx);
228
+ element.style.height = px(Math.abs(y2Px - y1Px));
229
+ element.style.background = strokeBackground(stroke, VERTICAL_BAR_AXES);
230
+ } else {
231
+ const dx = x2Px - x1Px;
232
+ const dy = y2Px - y1Px;
233
+ element.style.left = px(x1Px - context.originXPx);
234
+ element.style.top = px(y1Px - halfCrossExtentPx - context.originYPx);
235
+ element.style.width = px(Math.hypot(dx, dy));
236
+ element.style.height = px(crossExtentPx);
237
+ element.style.transformOrigin = "0 50%";
238
+ element.style.transform = `rotate(${Math.atan2(dy, dx) * DEGREES_PER_RADIAN}deg)`;
239
+ element.style.background = strokeBackground(stroke, HORIZONTAL_BAR_AXES);
240
+ }
241
+ context.parent.append(element);
242
+ };
243
+ /**
244
+ * Reapply what the measurer added to the advances.
245
+ *
246
+ * The browser shapes the run's text and advances it by what the font says, and
247
+ * none of these three is in the font: without them the painted run is as wide
248
+ * as the glyphs alone, which is not the width the line was fitted at. The CSS
249
+ * is the same CSS the legacy painter emits, so the two agree by construction
250
+ * rather than by coincidence.
251
+ *
252
+ * Spacing goes on before the transform scales it, so the painted-pixel numbers
253
+ * the list carries are divided by the scale first.
254
+ */
255
+ const applyAdjustments = (span, adjustments) => {
256
+ if (adjustments === void 0) return;
257
+ const { letterSpacingPx, horizontalScale, wordSpacingPx } = adjustments;
258
+ if (letterSpacingPx !== 0) span.style.letterSpacing = px(letterSpacingPx / horizontalScale);
259
+ if (wordSpacingPx !== 0) span.style.wordSpacing = px(wordSpacingPx / horizontalScale);
260
+ if (horizontalScale !== 1) {
261
+ span.style.transform = `scaleX(${String(horizontalScale)})`;
262
+ span.style.transformOrigin = "left center";
263
+ }
264
+ };
265
+ /**
266
+ * Where a run's glyphs came from in the editable model, on the element that
267
+ * paints them.
268
+ *
269
+ * An editing surface maps a click and a selection through these, and the
270
+ * browser's own hit-tester resolves the offset inside the run. The story is
271
+ * written beside the range because a page is not one document: the same number
272
+ * addresses a different character in the body, in each header and footer part,
273
+ * and in each note, so a surface has to know which one it landed in before it
274
+ * can use the number at all.
275
+ */
276
+ const applyModelRange = (span, range) => {
277
+ if (range === void 0) return;
278
+ span.className = "layout-run-text";
279
+ span.dataset["pmStart"] = String(range.start);
280
+ span.dataset["pmEnd"] = String(range.end);
281
+ const { story } = range;
282
+ span.dataset["story"] = story.kind;
283
+ switch (story.kind) {
284
+ case "body": break;
285
+ case "header":
286
+ case "footer":
287
+ if (story.rId !== null) span.dataset["hfRid"] = story.rId;
288
+ break;
289
+ case "footnote":
290
+ case "endnote":
291
+ span.dataset["noteKind"] = story.kind;
292
+ span.dataset["noteId"] = String(story.id);
293
+ break;
294
+ default: panic(`renderDisplayListToDom: unhandled story ${JSON.stringify(story)}`);
295
+ }
296
+ };
297
+ const paintGlyphRun = (run, context) => {
298
+ if ([...run.text].length !== run.advancesPx.length) panic(`renderDisplayListToDom: glyph run carries ${run.advancesPx.length} advances for ${[...run.text].length} code points`);
299
+ const face = resolveFont(run.font, context.fonts);
300
+ const advanceSum = run.advancesPx.reduce((total, advance) => total + advance, 0);
301
+ const ascentPx = face.fontBoxAscentRatio * run.fontSizePx;
302
+ const descentPx = face.fontBoxDescentRatio * run.fontSizePx;
303
+ const span = context.doc.createElement("span");
304
+ span.style.position = "absolute";
305
+ span.style.left = px(run.xPx - context.originXPx);
306
+ span.style.top = px(run.baselineYPx - ascentPx - context.originYPx);
307
+ span.style.lineHeight = px(ascentPx + descentPx);
308
+ span.style.display = "inline-block";
309
+ span.style.boxSizing = "content-box";
310
+ span.style.width = px(advanceSum / (run.adjustments?.horizontalScale ?? 1));
311
+ span.style.whiteSpace = "pre";
312
+ span.style.fontFamily = fontFamilyStack(face);
313
+ span.style.fontSize = px(run.fontSizePx);
314
+ span.style.fontWeight = String(face.weight);
315
+ span.style.fontStyle = face.italic ? "italic" : "normal";
316
+ span.style.color = cssColor(run.color);
317
+ span.style.direction = run.direction;
318
+ if (run.stroke !== void 0) span.style.webkitTextStroke = `${px(run.stroke.thicknessPx)} ${cssColor(run.stroke.color)}`;
319
+ span.style.fontKerning = run.kerning ? "normal" : "none";
320
+ if (run.smallCaps) span.style.fontVariant = "small-caps";
321
+ applyAdjustments(span, run.adjustments);
322
+ span.dataset["advanceSum"] = String(advanceSum);
323
+ applyModelRange(span, run.pmRange);
324
+ if (run.collapsedEdge !== void 0) {
325
+ span.dataset[run.collapsedEdge.side === "leading" ? "collapsedLeadingSpaces" : "collapsedTrailingSpaces"] = "true";
326
+ span.dataset["collapsedSpaceAdvance"] = String(run.collapsedEdge.spaceAdvancePx);
327
+ span.style.fontSize = "0";
328
+ span.style.letterSpacing = "0";
329
+ span.style.wordSpacing = "0";
330
+ }
331
+ span.textContent = run.text;
332
+ context.parent.append(span);
333
+ };
334
+ const grownRect = (rect, amountPx) => ({
335
+ xPx: rect.xPx - amountPx / 2,
336
+ yPx: rect.yPx - amountPx / 2,
337
+ widthPx: rect.widthPx + amountPx,
338
+ heightPx: rect.heightPx + amountPx
339
+ });
340
+ const rectEdges = ({ xPx, yPx, widthPx, heightPx }) => {
341
+ const rightPx = xPx + widthPx;
342
+ const bottomPx = yPx + heightPx;
343
+ return [
344
+ {
345
+ x1Px: xPx,
346
+ y1Px: yPx,
347
+ x2Px: rightPx,
348
+ y2Px: yPx
349
+ },
350
+ {
351
+ x1Px: rightPx,
352
+ y1Px: yPx,
353
+ x2Px: rightPx,
354
+ y2Px: bottomPx
355
+ },
356
+ {
357
+ x1Px: xPx,
358
+ y1Px: bottomPx,
359
+ x2Px: rightPx,
360
+ y2Px: bottomPx
361
+ },
362
+ {
363
+ x1Px: xPx,
364
+ y1Px: yPx,
365
+ x2Px: xPx,
366
+ y2Px: bottomPx
367
+ }
368
+ ];
369
+ };
370
+ const paintRect = ({ rect, fill, stroke }, context) => {
371
+ if (stroke === void 0) {
372
+ context.parent.append(createFilledDiv(context, rect, fill));
373
+ return;
374
+ }
375
+ const painter = RECT_STROKE_PAINTERS[stroke.pattern];
376
+ switch (painter) {
377
+ case "border": {
378
+ const element = createFilledDiv(context, grownRect(rect, stroke.thicknessPx), fill);
379
+ element.style.boxSizing = "border-box";
380
+ element.style.border = `${px(stroke.thicknessPx)} solid ${cssColor(stroke.color)}`;
381
+ context.parent.append(element);
382
+ return;
383
+ }
384
+ case "edges":
385
+ context.parent.append(createFilledDiv(context, rect, fill));
386
+ for (const edge of rectEdges(rect)) paintLineSegment({
387
+ kind: "line",
388
+ ...edge,
389
+ stroke
390
+ }, context);
391
+ return;
392
+ default: panic(`renderDisplayListToDom: unhandled rect stroke painter ${JSON.stringify(painter)}`);
393
+ }
394
+ };
395
+ const paintImage = ({ image, rect, crop, opacity }, context) => {
396
+ const source = resolveImage(image, context.images);
397
+ const clip = createAbsoluteDiv(context, rect);
398
+ clip.style.overflow = "hidden";
399
+ const element = context.doc.createElement("img");
400
+ element.style.position = "absolute";
401
+ element.style.opacity = String(opacity);
402
+ element.style.left = "0px";
403
+ element.style.top = "0px";
404
+ element.style.width = px(rect.widthPx);
405
+ element.style.height = px(rect.heightPx);
406
+ if (crop !== void 0) {
407
+ const visibleWidth = 1 - crop.l - crop.r;
408
+ const visibleHeight = 1 - crop.t - crop.b;
409
+ if (visibleWidth <= 0 || visibleHeight <= 0) panic(`renderDisplayListToDom: image crop leaves nothing visible (${JSON.stringify(crop)})`);
410
+ const fullWidth = rect.widthPx / visibleWidth;
411
+ const fullHeight = rect.heightPx / visibleHeight;
412
+ element.style.left = px(-crop.l * fullWidth);
413
+ element.style.top = px(-crop.t * fullHeight);
414
+ element.style.width = px(fullWidth);
415
+ element.style.height = px(fullHeight);
416
+ }
417
+ element.alt = "";
418
+ element.src = binarySrc(source.bytes, IMAGE_MIME_TYPES[source.format]);
419
+ clip.append(element);
420
+ context.parent.append(clip);
421
+ };
422
+ const paintClipGroup = ({ rect, children, regions }, context) => {
423
+ const element = createAbsoluteDiv(context, rect);
424
+ element.style.overflow = "hidden";
425
+ context.parent.append(element);
426
+ const inner = {
427
+ ...context,
428
+ parent: element,
429
+ originXPx: rect.xPx,
430
+ originYPx: rect.yPx
431
+ };
432
+ paintRegionTree({
433
+ primitives: children,
434
+ regions
435
+ }, inner);
436
+ };
437
+ const paintRotateGroup = ({ degrees, originXPx, originYPx, children }, context) => {
438
+ const element = createTransparentGroupDiv(context);
439
+ element.style.transformOrigin = `${px(originXPx - context.originXPx)} ${px(originYPx - context.originYPx)}`;
440
+ element.style.transform = `rotate(${degrees}deg)`;
441
+ context.parent.append(element);
442
+ const inner = {
443
+ ...context,
444
+ parent: element
445
+ };
446
+ for (const child of children) paintPrimitive(child, inner);
447
+ };
448
+ const paintOpacityGroup = ({ opacity, children }, context) => {
449
+ const element = createTransparentGroupDiv(context);
450
+ element.style.opacity = String(opacity);
451
+ context.parent.append(element);
452
+ const inner = {
453
+ ...context,
454
+ parent: element
455
+ };
456
+ for (const child of children) paintPrimitive(child, inner);
457
+ };
458
+ const paintPrimitive = (primitive, context) => {
459
+ switch (primitive.kind) {
460
+ case "glyphRun": return paintGlyphRun(primitive, context);
461
+ case "rect": return paintRect(primitive, context);
462
+ case "line": return paintLineSegment(primitive, context);
463
+ case "image": return paintImage(primitive, context);
464
+ case "clipGroup": return paintClipGroup(primitive, context);
465
+ case "rotateGroup": return paintRotateGroup(primitive, context);
466
+ case "opacityGroup": return paintOpacityGroup(primitive, context);
467
+ default: panic(`renderDisplayListToDom: unhandled display primitive ${JSON.stringify(primitive)}`);
468
+ }
469
+ };
470
+ /**
471
+ * A page target loses its `yPx`: a fragment identifier addresses an element,
472
+ * not an offset within one.
473
+ */
474
+ const linkHref = (target) => {
475
+ switch (target.kind) {
476
+ case "external": return target.href;
477
+ case "page": return `#${PAGE_ELEMENT_ID_PREFIX}${target.pageIndex}`;
478
+ default: panic(`renderDisplayListToDom: unhandled link target ${JSON.stringify(target)}`);
479
+ }
480
+ };
481
+ const paintLink = ({ rect, target, tooltip }, context) => {
482
+ const element = context.doc.createElement("a");
483
+ element.style.position = "absolute";
484
+ element.style.left = px(rect.xPx - context.originXPx);
485
+ element.style.top = px(rect.yPx - context.originYPx);
486
+ element.style.width = px(rect.widthPx);
487
+ element.style.height = px(rect.heightPx);
488
+ element.href = linkHref(target);
489
+ if (tooltip !== void 0) element.title = tooltip;
490
+ context.parent.append(element);
491
+ };
492
+ const renderPage = (page, options) => {
493
+ const element = options.doc.createElement("div");
494
+ element.className = PAGE_CLASS_NAME;
495
+ element.id = `${PAGE_ELEMENT_ID_PREFIX}${options.pageIndex}`;
496
+ element.style.position = "relative";
497
+ element.style.width = px(page.widthPx);
498
+ element.style.height = px(page.heightPx);
499
+ element.style.overflow = "hidden";
500
+ if (options.pageBackground !== void 0) element.style.backgroundColor = cssColor(options.pageBackground);
501
+ if (options.fontFaceCss.length > 0) {
502
+ const style = options.doc.createElement("style");
503
+ style.textContent = options.fontFaceCss;
504
+ element.append(style);
505
+ }
506
+ const context = {
507
+ doc: options.doc,
508
+ fonts: options.fonts,
509
+ images: options.images,
510
+ parent: element,
511
+ originXPx: 0,
512
+ originYPx: 0
513
+ };
514
+ paintRegionTree(page, context);
515
+ for (const link of page.links) paintLink(link, context);
516
+ return element;
517
+ };
518
+ /**
519
+ * The element a region becomes.
520
+ *
521
+ * The class names are the ones the interaction layer has always read; naming
522
+ * them here rather than in a painter is what lets a reader keep working while
523
+ * the thing that emits them changes underneath.
524
+ */
525
+ const REGION_CLASS = {
526
+ pageContent: "layout-page-content",
527
+ headerSlot: "layout-page-header",
528
+ footerSlot: "layout-page-footer",
529
+ note: "layout-note",
530
+ paragraph: "layout-paragraph",
531
+ line: "layout-line",
532
+ emptyRun: "layout-empty-run",
533
+ tab: "layout-run-tab",
534
+ image: "layout-run-image",
535
+ table: "layout-table",
536
+ tableRow: "layout-table-row",
537
+ tableCell: "layout-table-cell",
538
+ textBox: "layout-text-box"
539
+ };
540
+ /** Model facts a surface reads off the element, as the painter wrote them. */
541
+ const applyRegionModel = (element, region) => {
542
+ const model = region.model;
543
+ if (model === void 0) return;
544
+ if (model.pmRange !== void 0) {
545
+ element.dataset["pmStart"] = String(model.pmRange.start);
546
+ element.dataset["pmEnd"] = String(model.pmRange.end);
547
+ }
548
+ if (model.blockId !== void 0) element.dataset["blockId"] = model.blockId;
549
+ if (model.commentIds !== void 0 && model.commentIds.length > 0) element.dataset["commentId"] = String(model.commentIds[0]);
550
+ if (model.rowIndex !== void 0) element.dataset["rowIndex"] = String(model.rowIndex);
551
+ if (model.columnIndex !== void 0) element.dataset["columnIndex"] = String(model.columnIndex);
552
+ const story = model.story ?? model.pmRange?.story;
553
+ if (story === void 0) return;
554
+ element.dataset["story"] = story.kind;
555
+ switch (story.kind) {
556
+ case "body": break;
557
+ case "header":
558
+ case "footer":
559
+ if (story.rId === null) break;
560
+ element.dataset["rid"] = story.rId;
561
+ element.dataset["hfRid"] = story.rId;
562
+ element.dataset["hfRId"] = story.rId;
563
+ element.dataset["hfSlotKind"] = story.kind;
564
+ element.dataset["hfKind"] = story.kind;
565
+ break;
566
+ case "footnote":
567
+ case "endnote":
568
+ element.dataset["noteKind"] = story.kind;
569
+ element.dataset["noteId"] = String(story.id);
570
+ break;
571
+ default: panic(`renderDisplayListToDom: unhandled story ${JSON.stringify(story)}`);
572
+ }
573
+ };
574
+ const paintRegionTree = ({ primitives, regions }, context) => {
575
+ let current = context;
576
+ const stack = [];
577
+ walkRegions(primitives, regions, {
578
+ onPrimitive: (primitive) => {
579
+ paintPrimitive(primitive, current);
580
+ },
581
+ enter: (region) => {
582
+ const element = current.doc.createElement("div");
583
+ element.className = REGION_CLASS[region.kind];
584
+ element.style.position = "absolute";
585
+ element.style.left = px(region.rect.xPx - current.originXPx);
586
+ element.style.top = px(region.rect.yPx - current.originYPx);
587
+ element.style.width = px(region.rect.widthPx);
588
+ element.style.height = px(region.rect.heightPx);
589
+ applyRegionModel(element, region);
590
+ current.parent.append(element);
591
+ stack.push(current);
592
+ current = {
593
+ ...current,
594
+ parent: element,
595
+ originXPx: region.rect.xPx,
596
+ originYPx: region.rect.yPx
597
+ };
598
+ },
599
+ exit: () => {
600
+ current = stack.pop() ?? context;
601
+ }
602
+ });
603
+ };
604
+ const renderDisplayPageToDom = (page, options) => renderPage(page, {
605
+ ...options,
606
+ fontFaceCss: embeddedFontFaceCss(options.fonts)
607
+ });
608
+ /** One `div.layout-page` per display page, in page order. */
609
+ const renderDisplayListToDom = (list, options) => {
610
+ const fontFaceCss = embeddedFontFaceCss(list.fonts);
611
+ return list.pages.map((page, pageIndex) => renderPage(page, {
612
+ ...options,
613
+ fonts: list.fonts,
614
+ images: list.images,
615
+ pageIndex,
616
+ fontFaceCss
617
+ }));
618
+ };
619
+ //#endregion
620
+ export { renderDisplayListToDom, renderDisplayPageToDom };
@@ -0,0 +1,22 @@
1
+ import { Page } from "../../layout-engine/types.js";
2
+ import { DisplayList } from "../types.js";
3
+ import { BuildDisplayListOptions } from "../build/buildDisplayList.js";
4
+ //#region src/display-list/editor/displayListPagePainter.d.ts
5
+ type DisplayListPagePainterOptions = BuildDisplayListOptions & {
6
+ /** Document to create elements in. */
7
+ readonly doc: Document;
8
+ };
9
+ type DisplayListPagePainter = {
10
+ /** Matches `RenderPageOptions.paintPage`. */
11
+ readonly paintPage: (request: {
12
+ readonly page: Page;
13
+ }) => HTMLElement | null;
14
+ /**
15
+ * The list as far as the painter has built it, for tests and diagnostics.
16
+ * Pages nobody painted are absent, because nobody built them.
17
+ */
18
+ readonly list: () => DisplayList;
19
+ };
20
+ declare const createDisplayListPagePainter: (options: DisplayListPagePainterOptions) => DisplayListPagePainter;
21
+ //#endregion
22
+ export { DisplayListPagePainter, DisplayListPagePainterOptions, createDisplayListPagePainter };