@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,819 @@
1
+ import { getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont } from "../../layout-engine/measure/listMarkerWidth.js";
2
+ import { DEFAULT_FONT_FAMILY, buildRunFontStyle, ptToPx } from "../../layout-engine/measure/measureHelpers.js";
3
+ import { getFontMetrics, measureTextWidth } from "../../layout-engine/measure/measureProvider.js";
4
+ import { calculateTabWidth, pixelsToTwips } from "../../layout-engine/measure/tabCalculator.js";
5
+ import { countCompressibleSpaces, toPaintedText } from "../../layout-engine/measure/textMeasurementPolicy.js";
6
+ import { isFloatingImageRun } from "../../layout-engine/types.js";
7
+ import { resolveParagraphBorderHorizontalOutsets } from "../../layout-painter/borderStroke.js";
8
+ import { getAutomaticTextColorForBackground } from "../../layout-painter/documentColors.js";
9
+ import { getLeaderChar, getRenderableTextColor, sliceRunsForLine, splitCollapsibleLineEdgeSpaces, splitTextRunsByEastAsia, startsAfterSoftWrap } from "../../layout-painter/renderParagraph.js";
10
+ import { getHorizontalScaleFactor } from "../../utils/horizontalScale.js";
11
+ import { resolvePhysicalParagraphInlineLayout } from "../../utils/paragraphInlineLayout.js";
12
+ import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
13
+ import { sanitizeExternalUrl } from "../../utils/urlSecurity.js";
14
+ import { HIT_REGION_KINDS } from "../primitives.js";
15
+ import { trackedChangeColor } from "./buildContext.js";
16
+ import { DOC_CANVAS_TEXT, parseDisplayColor } from "./colors.js";
17
+ import { buildGlyphs, glyphRunText } from "./glyphs.js";
18
+ import { paintImage } from "./imagePrimitives.js";
19
+ import { decorationPatternForStyle, resolveBorderStroke } from "./strokes.js";
20
+ import { strikethroughCenterYPx, underlineCenterYPx, underlineThicknessPx } from "./textDecorations.js";
21
+ import { UNSUPPORTED_CONSTRUCT } from "./unsupported.js";
22
+ //#region src/display-list/build/paragraphPrimitives.ts
23
+ /**
24
+ * Paragraph fragments → primitives.
25
+ *
26
+ * This is where the display list earns its keep. The DOM painter never
27
+ * computes a line's start x: it sets `text-align` on the fragment, a
28
+ * `text-indent` on the first line, margins for float offsets, and lets the
29
+ * browser place the glyphs (`renderParagraph.ts:2957` onwards). A PDF has no
30
+ * such engine, so alignment, first-line and hanging indent, list-marker
31
+ * footprint, float offsets and `floatSkipBefore` are all resolved here into one
32
+ * number per line, and justification is resolved into the advances themselves
33
+ * rather than deferred to a backend.
34
+ *
35
+ * Two cursors run side by side. `layoutX` is the measurer's content-area
36
+ * coordinate, which is what tab stops are measured from and what line breaking
37
+ * was decided on; `paintX` is where the glyphs actually land after alignment
38
+ * moved the line. They differ by a constant per line, and conflating them would
39
+ * put a right-aligned line's tabs on the wrong stops.
40
+ */
41
+ /** Word's default hyperlink colour, applied when the run carries no colour of its own. */
42
+ const HYPERLINK_COLOR = {
43
+ r: 5,
44
+ g: 99,
45
+ b: 193,
46
+ a: 1
47
+ };
48
+ /** `renderParagraph.ts:603-616`: a raised run paints 0.4em up, a lowered one 0.2em down. */
49
+ const SUPERSCRIPT_RISE_RATIO = .4;
50
+ const SUBSCRIPT_DROP_RATIO = .2;
51
+ /** Matches `RIGHT_EDGE_EPSILON_PX` in the painter and `WIDTH_TOLERANCE` in the measurer. */
52
+ const WIDTH_EPSILON_PX = .5;
53
+ /** `w:pBdr` bar borders hang this far left of the text (`renderParagraph.ts:3072`). */
54
+ const BAR_BORDER_OFFSET_PX = 8;
55
+ const AUTOMATIC_TEXT_COLOR_VALUES = /* @__PURE__ */ new Set(["auto", "windowtext"]);
56
+ /** `w:color w:val="auto"` and its `windowtext` spelling, normalized as the painter normalizes them. */
57
+ const isAutomaticTextColor = (color) => AUTOMATIC_TEXT_COLOR_VALUES.has(color.trim().toLowerCase().replace(/^#/u, ""));
58
+ const isNoteReferenceRun = (run) => run.footnoteRefId !== void 0 || run.endnoteRefId !== void 0;
59
+ /**
60
+ * The colour a run's glyphs are actually painted in.
61
+ *
62
+ * `applyRunStyles` (`renderParagraph.ts:250-258`) sets no inline `color` for a
63
+ * black or `auto` run, on purpose: the editor's `--doc-canvas-text` then adapts
64
+ * to dark mode. A backend with no stylesheet would paint nothing at all, so
65
+ * black is re-materialized here. This is the one place that knows the dropped
66
+ * value was black; drop the fallback and every default-coloured run in the
67
+ * export turns invisible.
68
+ */
69
+ const resolveRunColor = (run, context) => {
70
+ const explicit = getRenderableTextColor(run);
71
+ let color = explicit === void 0 ? void 0 : parseDisplayColor(explicit);
72
+ const background = run.highlight ?? run.shading;
73
+ const isTracked = run.isInsertion === true || run.isDeletion === true;
74
+ const hasComments = run.commentIds !== void 0 && run.commentIds.length > 0;
75
+ if (background && explicit === void 0 && !isTracked && !hasComments) color = parseDisplayColor(getAutomaticTextColorForBackground(background));
76
+ if (isTracked) color = trackedChangeColor(context.authorColors, run.changeAuthor, run.isSuggestion);
77
+ if (run.hyperlink && run.hyperlink.noDefaultStyle !== true) {
78
+ const direct = run.color?.trim();
79
+ color = (direct && !isAutomaticTextColor(direct) && run.textColorSource === "direct" ? parseDisplayColor(direct) : void 0) ?? color ?? HYPERLINK_COLOR;
80
+ }
81
+ return color ?? DOC_CANVAS_TEXT;
82
+ };
83
+ const runFontStyle = (run) => buildRunFontStyle(run, DEFAULT_FONT_FAMILY, 11);
84
+ /**
85
+ * Field text, resolved without a clock. `evaluateFieldInstruction` needs a
86
+ * `now`, which a pure builder cannot supply, so the cached `w:fldSimple` result
87
+ * Word already wrote wins; only the two pagination fields, whose values the
88
+ * layout itself carries, are recomputed.
89
+ */
90
+ const resolveFieldText = (run, context) => {
91
+ if (run.fieldType === "PAGE") return String(context.pageNumber);
92
+ if (run.fieldType === "NUMPAGES") return String(context.totalPages);
93
+ return run.fallback ?? "";
94
+ };
95
+ const paintableText = (run, context) => {
96
+ switch (run.kind) {
97
+ case "text": return toPaintedText(run.text);
98
+ case "field": return toPaintedText(resolveFieldText(run, context));
99
+ case "math": return toPaintedText(run.plainText || "[equation]");
100
+ }
101
+ };
102
+ const convertTabStop = (stop) => ({
103
+ val: stop.val,
104
+ pos: stop.pos,
105
+ ...stop.leader === void 0 ? {} : { leader: stop.leader }
106
+ });
107
+ const horizontalLine = (xPx, widthPx, yPx, stroke) => ({
108
+ kind: "line",
109
+ x1Px: xPx,
110
+ y1Px: yPx,
111
+ x2Px: xPx + widthPx,
112
+ y2Px: yPx,
113
+ stroke
114
+ });
115
+ const bordersEqual = (a, b) => {
116
+ if (!a && !b) return true;
117
+ if (!a || !b) return false;
118
+ return a.style === b.style && a.width === b.width && a.color === b.color;
119
+ };
120
+ const bordersFormGroup = (a, b) => Boolean(a) && Boolean(b) && bordersEqual(a?.top, b?.top) && bordersEqual(a?.bottom, b?.bottom) && bordersEqual(a?.left, b?.left) && bordersEqual(a?.right, b?.right) && bordersEqual(a?.between, b?.between);
121
+ /** Paint the paragraph's own background and border box, before any line. */
122
+ const paintParagraphChrome = ({ fragment, block, context, prevBorders, nextBorders }) => {
123
+ const primitives = [];
124
+ const { indentLeft, indentRight } = resolvePhysicalParagraphInlineLayout(block);
125
+ const shading = block.attrs?.shading;
126
+ if (shading) {
127
+ const fill = parseDisplayColor(shading);
128
+ if (fill) primitives.push({
129
+ kind: "rect",
130
+ rect: {
131
+ xPx: fragment.x,
132
+ yPx: fragment.y,
133
+ widthPx: fragment.width,
134
+ heightPx: fragment.height
135
+ },
136
+ fill
137
+ });
138
+ else context.unsupported.report(UNSUPPORTED_CONSTRUCT.unresolvedColor, context.pageIndex, `paragraph shading ${shading}`);
139
+ }
140
+ const borders = block.attrs?.borders;
141
+ if (!borders) return primitives;
142
+ const topBorder = bordersFormGroup(prevBorders, borders) ? borders.between : borders.top;
143
+ const bottomBorder = bordersFormGroup(borders, nextBorders) ? void 0 : borders.bottom;
144
+ const outsets = resolveParagraphBorderHorizontalOutsets(borders, topBorder !== void 0 || bottomBorder !== void 0);
145
+ const boxLeft = fragment.x + indentLeft - outsets.left;
146
+ const boxRight = fragment.x + fragment.width - (indentRight - outsets.right);
147
+ const boxTop = fragment.y - (topBorder?.space ?? 0) - (topBorder?.width ?? 0);
148
+ const boxBottom = fragment.y + fragment.height + (bottomBorder?.space ?? 0) + (bottomBorder?.width ?? 0);
149
+ const push = (border, line) => {
150
+ if (!border) return;
151
+ const { stroke, unresolvedColor } = resolveBorderStroke(border);
152
+ if (unresolvedColor !== void 0) context.unsupported.report(UNSUPPORTED_CONSTRUCT.unresolvedColor, context.pageIndex, `paragraph border colour ${unresolvedColor}`);
153
+ if (stroke) primitives.push(line(stroke));
154
+ };
155
+ push(topBorder, (stroke) => ({
156
+ kind: "line",
157
+ x1Px: boxLeft,
158
+ y1Px: boxTop + stroke.thicknessPx / 2,
159
+ x2Px: boxRight,
160
+ y2Px: boxTop + stroke.thicknessPx / 2,
161
+ stroke
162
+ }));
163
+ push(bottomBorder, (stroke) => ({
164
+ kind: "line",
165
+ x1Px: boxLeft,
166
+ y1Px: boxBottom - stroke.thicknessPx / 2,
167
+ x2Px: boxRight,
168
+ y2Px: boxBottom - stroke.thicknessPx / 2,
169
+ stroke
170
+ }));
171
+ push(borders.left, (stroke) => ({
172
+ kind: "line",
173
+ x1Px: boxLeft + stroke.thicknessPx / 2,
174
+ y1Px: boxTop,
175
+ x2Px: boxLeft + stroke.thicknessPx / 2,
176
+ y2Px: boxBottom,
177
+ stroke
178
+ }));
179
+ push(borders.right, (stroke) => ({
180
+ kind: "line",
181
+ x1Px: boxRight - stroke.thicknessPx / 2,
182
+ y1Px: boxTop,
183
+ x2Px: boxRight - stroke.thicknessPx / 2,
184
+ y2Px: boxBottom,
185
+ stroke
186
+ }));
187
+ push(borders.bar, (stroke) => ({
188
+ kind: "line",
189
+ x1Px: fragment.x - BAR_BORDER_OFFSET_PX + stroke.thicknessPx / 2,
190
+ y1Px: fragment.y,
191
+ x2Px: fragment.x - BAR_BORDER_OFFSET_PX + stroke.thicknessPx / 2,
192
+ y2Px: fragment.y + fragment.height,
193
+ stroke
194
+ }));
195
+ return primitives;
196
+ };
197
+ const resolveLineGeometry = ({ fragment, line, lineTopYPx, alignment, isRtl, indentLeft, indentRight, firstLineOffsetPx, markerInlineWidthPx, isFirstLine }) => {
198
+ const leftOffset = line.leftOffset ?? 0;
199
+ const rightOffset = line.rightOffset ?? 0;
200
+ const firstLineExtra = isFirstLine ? firstLineOffsetPx + markerInlineWidthPx : 0;
201
+ const layoutStartXPx = indentLeft + leftOffset + (isRtl ? 0 : firstLineExtra);
202
+ const layoutRightEdgeXPx = fragment.width - indentRight - rightOffset;
203
+ const leftEdge = fragment.x + layoutStartXPx;
204
+ const rightEdge = fragment.x + layoutRightEdgeXPx - (isRtl ? firstLineExtra : 0);
205
+ let paintStartXPx;
206
+ switch (alignment) {
207
+ case "right":
208
+ paintStartXPx = rightEdge - line.width;
209
+ break;
210
+ case "center":
211
+ paintStartXPx = leftEdge + (rightEdge - leftEdge - line.width) / 2;
212
+ break;
213
+ case "left":
214
+ case "justify":
215
+ paintStartXPx = isRtl ? rightEdge - line.width : leftEdge;
216
+ break;
217
+ }
218
+ const baselineYPx = lineTopYPx + (line.lineHeight - (line.ascent + line.descent)) / 2 + line.ascent;
219
+ return {
220
+ paintStartXPx,
221
+ layoutStartXPx,
222
+ layoutRightEdgeXPx,
223
+ baselineYPx,
224
+ lineTopYPx
225
+ };
226
+ };
227
+ /**
228
+ * Resolve `renderParagraph.ts:2404-2437` into one number.
229
+ *
230
+ * The painter has three branches (explicit contraction via `word-spacing`, an
231
+ * explicit expansion for tab-bearing lines, and CSS `text-align: justify` for
232
+ * the rest) that all mean the same thing: spread the difference between the
233
+ * measured line and its capacity across the compressible spaces. A display list
234
+ * has no CSS branch, so all three collapse to the delta.
235
+ */
236
+ const resolveJustification = ({ line, alignment, availableWidthPx, firstLineOffsetPx, indentLeft, hasVisibleMarker, isFirstLine, isLastLine, paragraphEndsWithLineBreak, shrinkableSpaces }) => {
237
+ if (alignment !== "justify" || shrinkableSpaces === 0) return { spaceDeltaPx: 0 };
238
+ const firstLineIndentPx = isFirstLine ? firstLineOffsetPx : 0;
239
+ const positiveIndentPx = Math.max(0, firstLineIndentPx);
240
+ const hangingPx = Math.max(0, -firstLineIndentPx);
241
+ const hangingExpansionPx = hasVisibleMarker ? Math.min(hangingPx, Math.max(0, indentLeft)) : hangingPx;
242
+ const capacityPx = availableWidthPx - positiveIndentPx + hangingExpansionPx;
243
+ const overfullPx = line.width - capacityPx;
244
+ const finalContractionPx = line.justificationPaint?.type === "space-contraction" ? line.justificationPaint.contractionPx : void 0;
245
+ if (isLastLine && finalContractionPx !== void 0 && finalContractionPx > WIDTH_EPSILON_PX && overfullPx > WIDTH_EPSILON_PX) return { spaceDeltaPx: -(finalContractionPx ?? 0) / shrinkableSpaces };
246
+ if (!isLastLine || paragraphEndsWithLineBreak) {
247
+ if (Math.abs(overfullPx) <= WIDTH_EPSILON_PX) return { spaceDeltaPx: 0 };
248
+ return { spaceDeltaPx: -overfullPx / shrinkableSpaces };
249
+ }
250
+ return { spaceDeltaPx: 0 };
251
+ };
252
+ /**
253
+ * The editable-model range a run's glyphs came from, or `undefined` when they
254
+ * have no counterpart there.
255
+ *
256
+ * Every story qualifies, named. A header, footer or note run's positions belong
257
+ * to that story's own document, and a range that said only "start 12" could be
258
+ * used against the body and address unrelated content; one that says which
259
+ * document it came from can be routed into it instead.
260
+ */
261
+ const modelRangeOf = (run, context) => run.pmStart === void 0 || run.pmEnd === void 0 ? void 0 : {
262
+ start: run.pmStart,
263
+ end: run.pmEnd,
264
+ story: context.story
265
+ };
266
+ /**
267
+ * One `glyphRun` plus everything painted around it: the run's background rect
268
+ * first, then the glyphs, then the decorations whose geometry CSS would have
269
+ * derived from the font.
270
+ */
271
+ const emitGlyphRun = ({ sink, context, run, glyphs, style, paintXPx, baselineYPx, lineTopYPx, lineHeightPx, isRtl, pmRange, collapsedEdge }) => {
272
+ if (glyphs.text.length === 0) return;
273
+ const fontSizePx = ptToPx(style.fontSize ?? 11);
274
+ const color = resolveRunColor(run, context);
275
+ let runBaselineYPx = baselineYPx - (run.positionPx ?? 0);
276
+ if (run.superscript) runBaselineYPx -= fontSizePx * SUPERSCRIPT_RISE_RATIO;
277
+ else if (run.subscript) runBaselineYPx += fontSizePx * SUBSCRIPT_DROP_RATIO;
278
+ const background = run.highlight ?? run.shading;
279
+ if (background && glyphs.widthPx > 0) {
280
+ const fill = parseDisplayColor(background);
281
+ if (fill) {
282
+ const metrics = getFontMetrics(style);
283
+ sink.backgrounds.push({
284
+ kind: "rect",
285
+ rect: {
286
+ xPx: paintXPx,
287
+ yPx: runBaselineYPx - metrics.fontBoxAscent,
288
+ widthPx: glyphs.widthPx,
289
+ heightPx: metrics.fontBoxAscent + metrics.fontBoxDescent
290
+ },
291
+ fill
292
+ });
293
+ } else context.unsupported.report(UNSUPPORTED_CONSTRUCT.unresolvedColor, context.pageIndex, `run background ${background}`);
294
+ }
295
+ const font = context.fonts.intern({
296
+ fontFamily: style.fontFamily ?? "Calibri",
297
+ ...style.alternateFontFamily === void 0 ? {} : { alternateFontFamily: style.alternateFontFamily },
298
+ ...style.bold === void 0 ? {} : { bold: style.bold },
299
+ ...style.italic === void 0 ? {} : { italic: style.italic },
300
+ measureStyle: style
301
+ });
302
+ const stroke = run.textOutline ? {
303
+ color,
304
+ thicknessPx: 1,
305
+ pattern: "solid"
306
+ } : void 0;
307
+ sink.glyphs.push({
308
+ kind: "glyphRun",
309
+ font,
310
+ fontSizePx,
311
+ color,
312
+ xPx: paintXPx,
313
+ baselineYPx: runBaselineYPx,
314
+ ...glyphRunText(glyphs),
315
+ direction: isRtl ? "rtl" : "ltr",
316
+ ...stroke === void 0 ? {} : { stroke },
317
+ ...pmRange === void 0 ? {} : { pmRange },
318
+ ...collapsedEdge === void 0 ? {} : { collapsedEdge }
319
+ });
320
+ emitDecorations({
321
+ sink,
322
+ run,
323
+ color,
324
+ context,
325
+ xPx: paintXPx,
326
+ widthPx: glyphs.widthPx,
327
+ baselineYPx: runBaselineYPx,
328
+ fontSizePx
329
+ });
330
+ if (run.hyperlink) {
331
+ const target = resolveLinkTarget(run.hyperlink.href, context);
332
+ if (target) context.links.push({
333
+ rect: {
334
+ xPx: paintXPx,
335
+ yPx: lineTopYPx,
336
+ widthPx: glyphs.widthPx,
337
+ heightPx: lineHeightPx
338
+ },
339
+ target,
340
+ ...run.hyperlink.tooltip === void 0 ? {} : { tooltip: run.hyperlink.tooltip }
341
+ });
342
+ }
343
+ reportRunEffects(run, context);
344
+ };
345
+ const emitDecorations = ({ sink, run, color, context, xPx, widthPx, baselineYPx, fontSizePx }) => {
346
+ if (widthPx <= 0) return;
347
+ const thicknessPx = underlineThicknessPx(fontSizePx);
348
+ const trackedColor = run.isInsertion || run.isDeletion ? trackedChangeColor(context.authorColors, run.changeAuthor, run.isSuggestion) : void 0;
349
+ if (Boolean(run.underline) && !isNoteReferenceRun(run) || run.isInsertion) {
350
+ const authored = typeof run.underline === "object" ? run.underline : void 0;
351
+ const authoredColor = authored?.color === void 0 ? void 0 : parseDisplayColor(authored.color);
352
+ const pattern = run.isSuggestion ? "dotted" : decorationPatternForStyle(authored?.style);
353
+ sink.decorations.push(horizontalLine(xPx, widthPx, underlineCenterYPx(baselineYPx, fontSizePx), {
354
+ color: trackedColor ?? authoredColor ?? color,
355
+ thicknessPx,
356
+ pattern
357
+ }));
358
+ }
359
+ if (run.strike || run.isDeletion) sink.decorations.push(horizontalLine(xPx, widthPx, strikethroughCenterYPx(baselineYPx, fontSizePx), {
360
+ color: trackedColor ?? color,
361
+ thicknessPx,
362
+ pattern: run.isSuggestion ? "dotted" : "solid"
363
+ }));
364
+ };
365
+ /**
366
+ * Run-level typography the display list cannot express. The glyphs and their
367
+ * advances are still correct; only the treatment is missing, so this reports
368
+ * rather than skips.
369
+ */
370
+ const reportRunEffects = (run, context) => {
371
+ const { unsupported, pageIndex } = context;
372
+ if (run.smallCaps) unsupported.report(UNSUPPORTED_CONSTRUCT.smallCaps, pageIndex, "w:smallCaps is carried on the run and painted by the DOM backend; the PDF backend paints full-size glyphs for it");
373
+ if (getHorizontalScaleFactor(run.horizontalScale) !== 1) unsupported.report(UNSUPPORTED_CONSTRUCT.horizontalScale, pageIndex, `w:w ${String(run.horizontalScale)}% is carried on the run and painted by the DOM backend; the PDF backend paints unnarrowed glyphs at the measured advances`);
374
+ if (run.textEffect) unsupported.report(UNSUPPORTED_CONSTRUCT.textEffect, pageIndex, `w:effect ${run.textEffect}`);
375
+ if (run.emphasisMark) unsupported.report(UNSUPPORTED_CONSTRUCT.emphasisMark, pageIndex, `w:em ${run.emphasisMark}`);
376
+ if (run.emboss || run.imprint || run.textShadow) unsupported.report(UNSUPPORTED_CONSTRUCT.textShadowEffect, pageIndex, "w:emboss / w:imprint / w:shadow paint as plain glyphs");
377
+ };
378
+ const resolveLinkTarget = (href, context) => {
379
+ if (href.startsWith("#")) return context.bookmarkTargets.get(href.slice(1));
380
+ const safe = sanitizeExternalUrl(href);
381
+ return safe === void 0 ? void 0 : {
382
+ kind: "external",
383
+ href: safe
384
+ };
385
+ };
386
+ const paintLine = ({ block, line, geometry, context, alignment, isRtl, isFirstLine, isLastLine, paragraphEndsWithLineBreak, availableWidthPx, indentLeft, firstLineOffsetPx, markerInlineWidthPx }) => {
387
+ const sink = {
388
+ backgrounds: [],
389
+ glyphs: [],
390
+ decorations: []
391
+ };
392
+ const tabRegions = [];
393
+ const { runs, collapsedLeadingRuns, collapsedTrailingRuns } = splitCollapsibleLineEdgeSpaces(splitTextRunsByEastAsia(sliceRunsForLine(block, line)), startsAfterSoftWrap(block, line));
394
+ const isCollapsedEdgeRun = (run) => collapsedLeadingRuns.has(run) || collapsedTrailingRuns.has(run);
395
+ /**
396
+ * What a collapsed line-edge run is worth to a caret.
397
+ *
398
+ * The run paints at no width, so there is nothing for a caret to step over
399
+ * and an editing surface steps by this instead: one space in the run's own
400
+ * style, which is what the painter measured for the same purpose.
401
+ */
402
+ const collapsedEdgeOf = (run, style, leading) => isCollapsedEdgeRun(run) ? {
403
+ side: leading.has(run) ? "leading" : "trailing",
404
+ spaceAdvancePx: measureTextWidth(" ", style)
405
+ } : void 0;
406
+ const hasVisibleMarker = Boolean(block.attrs?.listMarker) && block.attrs?.listMarkerHidden !== true;
407
+ let shrinkableSpaces = 0;
408
+ for (const run of runs) if (run.kind === "text") {
409
+ if (!isCollapsedEdgeRun(run)) shrinkableSpaces += countCompressibleSpaces(run.text);
410
+ } else if (run.kind === "field") shrinkableSpaces += countCompressibleSpaces(resolveFieldText(run, context));
411
+ const { spaceDeltaPx } = resolveJustification({
412
+ line,
413
+ alignment,
414
+ availableWidthPx,
415
+ firstLineOffsetPx,
416
+ indentLeft,
417
+ hasVisibleMarker: hasVisibleMarker && isFirstLine,
418
+ isFirstLine,
419
+ isLastLine,
420
+ paragraphEndsWithLineBreak,
421
+ shrinkableSpaces
422
+ });
423
+ if (isFirstLine && hasVisibleMarker) paintListMarker({
424
+ sink,
425
+ block,
426
+ context,
427
+ paintXPx: geometry.paintStartXPx - markerInlineWidthPx + getListMarkerVisualOffset(block),
428
+ baselineYPx: geometry.baselineYPx,
429
+ isRtl
430
+ });
431
+ let layoutXPx = geometry.layoutStartXPx;
432
+ const alignShiftPx = geometry.paintStartXPx - geometry.layoutStartXPx;
433
+ const paintXOf = () => layoutXPx + alignShiftPx;
434
+ const tabContext = {
435
+ ...block.attrs?.tabs === void 0 ? {} : { explicitStops: block.attrs.tabs.map(convertTabStop) },
436
+ ...block.attrs?.defaultTabStopTwips === void 0 ? {} : { defaultTabInterval: block.attrs.defaultTabStopTwips },
437
+ leftIndent: pixelsToTwips(block.attrs?.indent?.left ?? 0)
438
+ };
439
+ for (let index = 0; index < runs.length; index += 1) {
440
+ const run = runs[index];
441
+ switch (run.kind) {
442
+ case "text": {
443
+ if (run.hidden) {
444
+ context.unsupported.report(UNSUPPORTED_CONSTRUCT.hiddenRun, context.pageIndex, "w:vanish text is suppressed, as Word's print view suppresses it");
445
+ break;
446
+ }
447
+ const style = runFontStyle(run);
448
+ const glyphs = buildGlyphs({
449
+ text: toPaintedText(run.text),
450
+ style,
451
+ allCaps: run.allCaps === true,
452
+ spaceDeltaPx,
453
+ collapsed: isCollapsedEdgeRun(run)
454
+ });
455
+ const pmRange = modelRangeOf(run, context);
456
+ const collapsedEdge = collapsedEdgeOf(run, style, collapsedLeadingRuns);
457
+ emitGlyphRun({
458
+ sink,
459
+ context,
460
+ run,
461
+ glyphs,
462
+ style,
463
+ paintXPx: paintXOf(),
464
+ baselineYPx: geometry.baselineYPx,
465
+ lineTopYPx: geometry.lineTopYPx,
466
+ lineHeightPx: line.lineHeight,
467
+ isRtl,
468
+ ...pmRange === void 0 ? {} : { pmRange },
469
+ ...collapsedEdge === void 0 ? {} : { collapsedEdge }
470
+ });
471
+ layoutXPx += glyphs.widthPx;
472
+ break;
473
+ }
474
+ case "field": {
475
+ const style = runFontStyle(run);
476
+ const glyphs = buildGlyphs({
477
+ text: paintableText(run, context),
478
+ style,
479
+ allCaps: run.allCaps === true,
480
+ spaceDeltaPx,
481
+ collapsed: false
482
+ });
483
+ emitGlyphRun({
484
+ sink,
485
+ context,
486
+ run: {
487
+ ...run,
488
+ kind: "text",
489
+ text: glyphs.text
490
+ },
491
+ glyphs,
492
+ style,
493
+ paintXPx: paintXOf(),
494
+ baselineYPx: geometry.baselineYPx,
495
+ lineTopYPx: geometry.lineTopYPx,
496
+ lineHeightPx: line.lineHeight,
497
+ isRtl
498
+ });
499
+ layoutXPx += glyphs.widthPx;
500
+ break;
501
+ }
502
+ case "math": {
503
+ const style = runFontStyle(run);
504
+ const glyphs = buildGlyphs({
505
+ text: paintableText(run, context),
506
+ style,
507
+ allCaps: false,
508
+ spaceDeltaPx: 0,
509
+ collapsed: false
510
+ });
511
+ emitGlyphRun({
512
+ sink,
513
+ context,
514
+ run: {
515
+ ...run,
516
+ kind: "text",
517
+ text: glyphs.text
518
+ },
519
+ glyphs,
520
+ style,
521
+ paintXPx: paintXOf(),
522
+ baselineYPx: geometry.baselineYPx,
523
+ lineTopYPx: geometry.lineTopYPx,
524
+ lineHeightPx: line.lineHeight,
525
+ isRtl
526
+ });
527
+ context.unsupported.report(UNSUPPORTED_CONSTRUCT.mathRun, context.pageIndex, "OMML painted as renderMathFallback's plain text, not as typeset math");
528
+ layoutXPx += glyphs.widthPx;
529
+ break;
530
+ }
531
+ case "tab": {
532
+ const widthPx = paintTab({
533
+ sink,
534
+ context,
535
+ runs,
536
+ index,
537
+ run,
538
+ tabContext,
539
+ layoutXPx,
540
+ paintXPx: paintXOf(),
541
+ baselineYPx: geometry.baselineYPx,
542
+ layoutRightEdgeXPx: geometry.layoutRightEdgeXPx
543
+ });
544
+ tabRegions.push({
545
+ kind: HIT_REGION_KINDS.tab,
546
+ rect: {
547
+ xPx: paintXOf(),
548
+ yPx: geometry.lineTopYPx,
549
+ widthPx,
550
+ heightPx: line.lineHeight
551
+ },
552
+ model: {
553
+ blockId: String(block.id),
554
+ ...run.pmStart === void 0 || run.pmEnd === void 0 ? {} : { pmRange: {
555
+ start: run.pmStart,
556
+ end: run.pmEnd,
557
+ story: context.story
558
+ } }
559
+ }
560
+ });
561
+ layoutXPx += widthPx;
562
+ break;
563
+ }
564
+ case "image": {
565
+ if (isFloatingImageRun(run)) break;
566
+ const boundingBox = inlineImageBoundingBox(run);
567
+ const primitives = paintImage({
568
+ source: run,
569
+ rect: {
570
+ xPx: paintXOf(),
571
+ yPx: geometry.baselineYPx - boundingBox.height,
572
+ widthPx: boundingBox.width,
573
+ heightPx: boundingBox.height
574
+ },
575
+ context,
576
+ label: "inline image"
577
+ });
578
+ sink.glyphs.push(...primitives);
579
+ if (run.displayMode !== "block" && run.wrapType !== "topAndBottom") layoutXPx += boundingBox.width;
580
+ break;
581
+ }
582
+ case "lineBreak":
583
+ case "renderedPageBreak": break;
584
+ }
585
+ }
586
+ return {
587
+ primitives: [
588
+ ...sink.backgrounds,
589
+ ...sink.glyphs,
590
+ ...sink.decorations
591
+ ],
592
+ tabRegions
593
+ };
594
+ };
595
+ const paintListMarker = ({ sink, block, context, paintXPx, baselineYPx, isRtl }) => {
596
+ const marker = block.attrs?.listMarker;
597
+ if (marker === void 0 || marker.length === 0) return;
598
+ const formatting = resolveListMarkerFont(block);
599
+ const style = formatting;
600
+ const glyphs = buildGlyphs({
601
+ text: toPaintedText(marker.replaceAll(" ", " ")),
602
+ style,
603
+ allCaps: false,
604
+ spaceDeltaPx: 0,
605
+ collapsed: false
606
+ });
607
+ const revision = block.attrs?.listMarkerRevision;
608
+ const color = revision ? trackedChangeColor(context.authorColors, revision.author, void 0) : DOC_CANVAS_TEXT;
609
+ const font = context.fonts.intern({
610
+ fontFamily: formatting.fontFamily,
611
+ ...formatting.alternateFontFamily === void 0 ? {} : { alternateFontFamily: formatting.alternateFontFamily },
612
+ ...formatting.bold === void 0 ? {} : { bold: formatting.bold },
613
+ ...formatting.italic === void 0 ? {} : { italic: formatting.italic },
614
+ measureStyle: style
615
+ });
616
+ const fontSizePx = ptToPx(formatting.fontSize);
617
+ sink.glyphs.push({
618
+ kind: "glyphRun",
619
+ font,
620
+ fontSizePx,
621
+ color,
622
+ xPx: paintXPx,
623
+ baselineYPx,
624
+ ...glyphRunText(glyphs),
625
+ direction: formatting.rtl ?? isRtl ? "rtl" : "ltr"
626
+ });
627
+ if (revision) {
628
+ const stroke = {
629
+ color,
630
+ thicknessPx: underlineThicknessPx(fontSizePx),
631
+ pattern: "solid"
632
+ };
633
+ sink.decorations.push(revision.kind === "ins" ? horizontalLine(paintXPx, glyphs.widthPx, underlineCenterYPx(baselineYPx, fontSizePx), stroke) : horizontalLine(paintXPx, glyphs.widthPx, strikethroughCenterYPx(baselineYPx, fontSizePx), stroke));
634
+ }
635
+ };
636
+ /**
637
+ * A tab's advance, plus its leader glyphs when the stop sets one.
638
+ *
639
+ * The end-aligned clamp mirrors `measureParagraph.ts:1795-1802` rather than the
640
+ * painter's flex "right anchor": both put the trailing content flush against
641
+ * the line's right edge, but the measurer's version is the one the line width
642
+ * was decided on, and in an explicit-x world it is just arithmetic.
643
+ */
644
+ const paintTab = ({ sink, context, runs, index, run, tabContext, layoutXPx, paintXPx, baselineYPx, layoutRightEdgeXPx }) => {
645
+ let followingWidthPx = 0;
646
+ let followingText = "";
647
+ for (let next = index + 1; next < runs.length; next += 1) {
648
+ const following = runs[next];
649
+ if (following.kind === "tab" || following.kind === "lineBreak") break;
650
+ if (following.kind === "text" || following.kind === "field" || following.kind === "math") {
651
+ const text = paintableText(following, context);
652
+ followingText += text;
653
+ followingWidthPx += measureTextWidth(following.kind === "text" && following.allCaps ? text.toLocaleUpperCase() : text, runFontStyle(following));
654
+ } else if (following.kind === "image" && !isFloatingImageRun(following)) followingWidthPx += inlineImageBoundingBox(following).width;
655
+ }
656
+ const decimalIndex = followingText.indexOf(".");
657
+ const decimalPrefixWidthPx = decimalIndex === -1 ? 0 : measureDecimalPrefixWidth(runs, index, decimalIndex, context);
658
+ const result = calculateTabWidth(layoutXPx, tabContext, {
659
+ followingWidth: followingWidthPx,
660
+ ...decimalPrefixWidthPx > 0 ? { decimalPrefixWidth: decimalPrefixWidthPx } : {}
661
+ });
662
+ let widthPx = result.width;
663
+ if (result.alignment === "end" && layoutXPx + widthPx + followingWidthPx > layoutRightEdgeXPx + WIDTH_EPSILON_PX) widthPx = Math.max(1, layoutRightEdgeXPx - layoutXPx - followingWidthPx);
664
+ const leaderChar = result.leader === void 0 ? null : getLeaderChar(result.leader);
665
+ if (leaderChar !== null && widthPx > 0) {
666
+ const style = runFontStyle(run);
667
+ const unitWidthPx = measureTextWidth(leaderChar, style);
668
+ if (unitWidthPx > 0) {
669
+ const count = Math.floor(widthPx / unitWidthPx);
670
+ if (count > 0) {
671
+ const glyphs = buildGlyphs({
672
+ text: leaderChar.repeat(count),
673
+ style,
674
+ allCaps: false,
675
+ spaceDeltaPx: 0,
676
+ collapsed: false
677
+ });
678
+ const font = context.fonts.intern({
679
+ fontFamily: style.fontFamily ?? "Calibri",
680
+ ...style.bold === void 0 ? {} : { bold: style.bold },
681
+ ...style.italic === void 0 ? {} : { italic: style.italic },
682
+ measureStyle: style
683
+ });
684
+ const clip = {
685
+ xPx: paintXPx,
686
+ yPx: baselineYPx - ptToPx(style.fontSize ?? 11),
687
+ widthPx,
688
+ heightPx: ptToPx(style.fontSize ?? 11) * 2
689
+ };
690
+ sink.glyphs.push({
691
+ kind: "clipGroup",
692
+ rect: clip,
693
+ regions: [],
694
+ children: [{
695
+ kind: "glyphRun",
696
+ font,
697
+ fontSizePx: ptToPx(style.fontSize ?? 11),
698
+ color: run.color === void 0 ? DOC_CANVAS_TEXT : parseDisplayColor(run.color) ?? DOC_CANVAS_TEXT,
699
+ xPx: paintXPx,
700
+ baselineYPx,
701
+ ...glyphRunText(glyphs),
702
+ direction: "ltr"
703
+ }]
704
+ });
705
+ }
706
+ }
707
+ }
708
+ return widthPx;
709
+ };
710
+ const measureDecimalPrefixWidth = (runs, tabIndex, decimalIndex, context) => {
711
+ let width = 0;
712
+ let consumed = 0;
713
+ for (let index = tabIndex + 1; index < runs.length && consumed < decimalIndex; index += 1) {
714
+ const run = runs[index];
715
+ if (run.kind === "tab" || run.kind === "lineBreak") break;
716
+ if (run.kind !== "text" && run.kind !== "field" && run.kind !== "math") continue;
717
+ const text = paintableText(run, context);
718
+ const take = Math.min(text.length, decimalIndex - consumed);
719
+ if (take > 0) width += measureTextWidth(text.slice(0, take), runFontStyle(run));
720
+ consumed += text.length;
721
+ }
722
+ return width;
723
+ };
724
+ /** Every primitive a paragraph fragment paints, back to front. */
725
+ const paintParagraphFragment = (options) => {
726
+ const { fragment, block, measure, context, composer } = options;
727
+ composer.push(paintParagraphChrome(options));
728
+ const { alignment, indentLeft, indentRight, isRtl } = resolvePhysicalParagraphInlineLayout(block);
729
+ const indent = block.attrs?.indent;
730
+ const firstLineOffsetPx = (indent?.firstLine ?? 0) - (indent?.hanging ?? 0);
731
+ const markerInlineWidthPx = getListMarkerInlineWidth(block);
732
+ const paragraphEndsWithLineBreak = block.runs.at(-1)?.kind === "lineBreak";
733
+ const totalLines = measure.lines.length;
734
+ if (isRtl) context.unsupported.report(UNSUPPORTED_CONSTRUCT.bidiReordering, context.pageIndex, "runs of an RTL paragraph are placed in logical order; the builder runs no bidi algorithm");
735
+ let cursorYPx = fragment.y;
736
+ for (let offset = 0; offset < fragment.toLine - fragment.fromLine; offset += 1) {
737
+ const lineIndex = fragment.fromLine + offset;
738
+ const line = measure.lines[lineIndex];
739
+ if (!line) continue;
740
+ cursorYPx += line.floatSkipBefore ?? 0;
741
+ const isFirstLine = lineIndex === 0 && fragment.continuesFromPrev !== true;
742
+ const geometry = resolveLineGeometry({
743
+ fragment,
744
+ line,
745
+ lineTopYPx: cursorYPx,
746
+ alignment,
747
+ isRtl,
748
+ indentLeft,
749
+ indentRight,
750
+ firstLineOffsetPx,
751
+ markerInlineWidthPx,
752
+ isFirstLine
753
+ });
754
+ const lineRuns = sliceRunsForLine(block, line);
755
+ composer.region({
756
+ kind: lineRegionKind(lineRuns),
757
+ rect: {
758
+ xPx: fragment.x,
759
+ yPx: geometry.lineTopYPx,
760
+ widthPx: fragment.width,
761
+ heightPx: line.lineHeight
762
+ },
763
+ model: lineModel({
764
+ block,
765
+ line,
766
+ runs: lineRuns,
767
+ context
768
+ })
769
+ }, () => {
770
+ const painted = paintLine({
771
+ block,
772
+ line,
773
+ geometry,
774
+ context,
775
+ alignment,
776
+ isRtl,
777
+ isFirstLine,
778
+ isLastLine: lineIndex === totalLines - 1,
779
+ paragraphEndsWithLineBreak,
780
+ availableWidthPx: fragment.width - indentLeft - indentRight - (line.leftOffset ?? 0) - (line.rightOffset ?? 0),
781
+ indentLeft,
782
+ firstLineOffsetPx,
783
+ markerInlineWidthPx
784
+ });
785
+ composer.push(painted.primitives);
786
+ for (const tab of painted.tabRegions) composer.region(tab, () => void 0);
787
+ });
788
+ cursorYPx += line.lineHeight;
789
+ }
790
+ };
791
+ /**
792
+ * A line with no text of its own is where a caret lands in an empty paragraph,
793
+ * and the surface has always needed to tell the two apart: one has characters
794
+ * to put the caret between, the other has only a position.
795
+ */
796
+ const lineRegionKind = (runs) => runs.some((run) => run.kind === "text" && run.text.length > 0 || run.kind === "field" || run.kind === "math" || run.kind === "image" || run.kind === "tab") ? HIT_REGION_KINDS.line : HIT_REGION_KINDS.emptyRun;
797
+ const lineModel = ({ block, line, runs, context }) => {
798
+ const positions = runs.flatMap((run) => run.kind === "text" && run.pmStart !== void 0 && run.pmEnd !== void 0 ? [{
799
+ start: run.pmStart,
800
+ end: run.pmEnd
801
+ }] : []);
802
+ const commentIds = [...new Set(runs.flatMap((run) => run.kind === "text" ? run.commentIds ?? [] : []))];
803
+ const trailingRun = block.runs.at(-1);
804
+ const trailingBreakEnd = runs.length === 0 && line.fromRun > block.runs.length - 1 && trailingRun?.kind === "lineBreak" ? trailingRun.pmEnd : void 0;
805
+ const emptyStart = trailingBreakEnd ?? (block.pmStart === void 0 ? void 0 : block.pmStart + 1);
806
+ const start = positions.at(0)?.start ?? emptyStart;
807
+ const end = positions.at(-1)?.end ?? (trailingBreakEnd === void 0 ? block.pmEnd ?? emptyStart : trailingBreakEnd);
808
+ return {
809
+ blockId: String(block.id),
810
+ ...start === void 0 || end === void 0 ? {} : { pmRange: {
811
+ start,
812
+ end,
813
+ story: context.story
814
+ } },
815
+ ...commentIds.length === 0 ? {} : { commentIds }
816
+ };
817
+ };
818
+ //#endregion
819
+ export { paintParagraphFragment };