@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.
- package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
- package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
- package/dist/ai-edits/apply.d.ts +49 -3
- package/dist/ai-edits/apply.js +692 -75
- package/dist/ai-edits/blockRange.d.ts +7 -3
- package/dist/ai-edits/blockRange.js +14 -6
- package/dist/ai-edits/headless.d.ts +39 -5
- package/dist/ai-edits/headless.js +76 -17
- package/dist/ai-edits/index.d.ts +6 -6
- package/dist/ai-edits/index.js +3 -3
- package/dist/ai-edits/read.d.ts +4 -0
- package/dist/ai-edits/read.js +30 -6
- package/dist/ai-edits/scoped-reading.js +4 -3
- package/dist/ai-edits/snapshot.d.ts +42 -2
- package/dist/ai-edits/snapshot.js +127 -62
- package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
- package/dist/ai-edits/table-row-column-mutations.js +98 -6
- package/dist/ai-edits/table-targets.d.ts +12 -3
- package/dist/ai-edits/table-targets.js +15 -5
- package/dist/ai-edits/types.d.ts +235 -7
- package/dist/ai-edits/word-diff.d.ts +54 -11
- package/dist/ai-edits/word-diff.js +221 -61
- package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
- package/dist/compare/__fixtures__/body-sequence.js +67 -0
- package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
- package/dist/compare/__fixtures__/nested-table.js +50 -0
- package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
- package/dist/compare/__fixtures__/numbered-list.js +85 -0
- package/dist/compare/column-alignment.d.ts +27 -0
- package/dist/compare/column-alignment.js +162 -0
- package/dist/compare/compare.d.ts +107 -0
- package/dist/compare/compare.js +406 -0
- package/dist/compare/formatting.d.ts +29 -0
- package/dist/compare/formatting.js +84 -0
- package/dist/compare/plan.d.ts +22 -0
- package/dist/compare/plan.js +1059 -0
- package/dist/compare/reproducible-package.d.ts +17 -0
- package/dist/compare/reproducible-package.js +30 -0
- package/dist/compare/scenario.d.ts +77 -0
- package/dist/compare/scenario.js +263 -0
- package/dist/compare/types.d.ts +282 -0
- package/dist/compare/types.js +32 -0
- package/dist/compare/verification.d.ts +55 -0
- package/dist/compare/verification.js +146 -0
- package/dist/compat/eigenpal.d.ts +9 -6
- package/dist/compat/eigenpal.js +6 -3
- package/dist/controller/layoutPipeline.d.ts +13 -0
- package/dist/controller/layoutPipeline.js +15 -1
- package/dist/display-list/build/buildContext.d.ts +39 -0
- package/dist/display-list/build/buildContext.js +31 -0
- package/dist/display-list/build/buildDisplayList.d.ts +79 -0
- package/dist/display-list/build/buildDisplayList.js +283 -0
- package/dist/display-list/build/colors.d.ts +20 -0
- package/dist/display-list/build/colors.js +136 -0
- package/dist/display-list/build/floatingImages.d.ts +24 -0
- package/dist/display-list/build/floatingImages.js +41 -0
- package/dist/display-list/build/fontTable.d.ts +31 -0
- package/dist/display-list/build/fontTable.js +97 -0
- package/dist/display-list/build/furniture.d.ts +86 -0
- package/dist/display-list/build/furniture.js +98 -0
- package/dist/display-list/build/glyphs.d.ts +55 -0
- package/dist/display-list/build/glyphs.js +103 -0
- package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
- package/dist/display-list/build/headerFooterPrimitives.js +40 -0
- package/dist/display-list/build/imagePrimitives.d.ts +35 -0
- package/dist/display-list/build/imagePrimitives.js +264 -0
- package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
- package/dist/display-list/build/pageBorderPrimitives.js +111 -0
- package/dist/display-list/build/pageFurniture.d.ts +35 -0
- package/dist/display-list/build/pageFurniture.js +141 -0
- package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
- package/dist/display-list/build/paragraphPrimitives.js +819 -0
- package/dist/display-list/build/regions.d.ts +46 -0
- package/dist/display-list/build/regions.js +56 -0
- package/dist/display-list/build/storyPrimitives.d.ts +29 -0
- package/dist/display-list/build/storyPrimitives.js +279 -0
- package/dist/display-list/build/strokes.d.ts +19 -0
- package/dist/display-list/build/strokes.js +78 -0
- package/dist/display-list/build/tablePrimitives.d.ts +38 -0
- package/dist/display-list/build/tablePrimitives.js +409 -0
- package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
- package/dist/display-list/build/textBoxPrimitives.js +129 -0
- package/dist/display-list/build/textDecorations.d.ts +29 -0
- package/dist/display-list/build/textDecorations.js +29 -0
- package/dist/display-list/build/unsupported.d.ts +57 -0
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
- package/dist/display-list/build/watermarkPrimitives.js +128 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
- package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
- package/dist/display-list/editor/displayListPagePainter.js +63 -0
- package/dist/display-list/editor/pageRenderer.d.ts +24 -0
- package/dist/display-list/editor/pageRenderer.js +36 -0
- package/dist/display-list/primitives.d.ts +95 -0
- package/dist/display-list/primitives.js +127 -0
- package/dist/display-list/types.d.ts +472 -0
- package/dist/display-list/types.js +0 -0
- package/dist/document-operations.d.ts +42 -9
- package/dist/document-operations.js +171 -16
- package/dist/document-stories.d.ts +16 -0
- package/dist/document-stories.js +48 -4
- package/dist/docx/packageParts.d.ts +35 -0
- package/dist/docx/packageParts.js +120 -0
- package/dist/docx/paragraphParser.js +22 -22
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/renderedPageBreakNormalization.js +3 -1
- package/dist/docx/revisionIdNormalization.d.ts +13 -0
- package/dist/docx/revisionIdNormalization.js +103 -0
- package/dist/docx/rezip.js +26 -17
- package/dist/docx/selectiveSave.js +2 -2
- package/dist/docx/serializer/commentSerializer.js +6 -1
- package/dist/docx/serializer/paragraphSerializer.js +33 -4
- package/dist/docx/serializer/runSerializer.js +11 -10
- package/dist/docx/serializer/tableSerializer.js +2 -1
- package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
- package/dist/docx/server/createBilingualDocx.js +3 -2
- package/dist/docx/streamingXmlParser.d.ts +13 -1
- package/dist/docx/streamingXmlParser.js +49 -11
- package/dist/docx/unzip.d.ts +1 -2
- package/dist/docx/unzip.js +25 -6
- package/dist/docx/xmlParser.d.ts +3 -1
- package/dist/docx/xmlParser.js +33 -27
- package/dist/export-pdf.d.ts +43 -0
- package/dist/export-pdf.js +98 -0
- package/dist/fonts/headlessMeasure.d.ts +76 -0
- package/dist/fonts/headlessMeasure.js +0 -0
- package/dist/fonts/sfnt/parse.d.ts +51 -0
- package/dist/fonts/sfnt/parse.js +525 -0
- package/dist/fonts/sfnt/subset.d.ts +20 -0
- package/dist/fonts/sfnt/subset.js +350 -0
- package/dist/fonts/sfnt/tables.d.ts +88 -0
- package/dist/fonts/sfnt/tables.js +131 -0
- package/dist/fonts/sfnt/woff.d.ts +14 -0
- package/dist/fonts/sfnt/woff.js +163 -0
- package/dist/generated/text_shaper.js +324 -0
- package/dist/generated/text_shaper_bg.wasm +0 -0
- package/dist/headless-layout.d.ts +75 -0
- package/dist/headless-layout.js +350 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
- package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
- package/dist/layout-engine/measure/advanceComposition.js +148 -0
- package/dist/layout-engine/measure/cache.d.ts +2 -0
- package/dist/layout-engine/measure/measureContainer.js +13 -0
- package/dist/layout-engine/measure/measureParagraph.js +1 -2
- package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
- package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
- package/dist/layout-engine/tableIndentCompatibility.js +15 -0
- package/dist/layout-engine/types.d.ts +8 -0
- package/dist/layout-painter/renderPage.d.ts +29 -1
- package/dist/layout-painter/renderPage.js +47 -19
- package/dist/layout-painter/renderParagraph.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/renderTable.js +37 -4
- package/dist/managers/editorShortcuts.d.ts +33 -1
- package/dist/managers/editorShortcuts.js +20 -1
- package/dist/markdown/renderRuns.js +7 -2
- package/dist/markdown/renderTable.js +5 -1
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/pdf/contentStream.d.ts +73 -0
- package/dist/pdf/contentStream.js +143 -0
- package/dist/pdf/fonts.d.ts +96 -0
- package/dist/pdf/fonts.js +593 -0
- package/dist/pdf/images.d.ts +28 -0
- package/dist/pdf/images.js +439 -0
- package/dist/pdf/objects.d.ts +93 -0
- package/dist/pdf/objects.js +237 -0
- package/dist/pdf/pageSpace.d.ts +43 -0
- package/dist/pdf/pageSpace.js +57 -0
- package/dist/pdf/paint.d.ts +32 -0
- package/dist/pdf/paint.js +442 -0
- package/dist/pdf/writePdf.d.ts +56 -0
- package/dist/pdf/writePdf.js +345 -0
- package/dist/prosemirror/commands/comments.js +91 -6
- package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
- package/dist/prosemirror/conversion/toProseDoc.js +41 -4
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
- package/dist/prosemirror/plugins/suggestionMode.js +1 -0
- package/dist/prosemirror/revisionCarriers.js +19 -2
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
- package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
- package/dist/prosemirror/validation.js +55 -27
- package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
- package/dist/prosemirror/zeroWidthAnchors.js +23 -0
- package/dist/redline.js +12 -79
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/shaping/placeRun.d.ts +51 -0
- package/dist/shaping/placeRun.js +84 -0
- package/dist/shaping/shaper.d.ts +92 -0
- package/dist/shaping/shaper.js +102 -0
- package/dist/types/block-id.d.ts +18 -2
- package/dist/types/block-id.js +49 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/clipboard.js +4 -3
- package/dist/utils/fontResolver.js +183 -0
- package/dist/utils/htmlComments.d.ts +17 -0
- package/dist/utils/htmlComments.js +22 -0
- package/dist/utils/scriptSegments.d.ts +12 -1
- package/dist/utils/scriptSegments.js +17 -1
- package/dist/version-comparison.d.ts +1 -1
- 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 };
|