@stll/folio-core 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/docx/index.d.ts +2 -0
  2. package/dist/docx/index.js +2 -0
  3. package/dist/docx/wrapTypes.d.ts +8 -1
  4. package/dist/docx/wrapTypes.js +0 -8
  5. package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
  6. package/dist/i18n/messages/catalogs.gen.js +2742 -0
  7. package/dist/i18n/messages/messages.gen.d.ts +166 -0
  8. package/dist/i18n/messages/messages.gen.js +0 -0
  9. package/dist/i18n/messages.d.ts +13 -0
  10. package/dist/i18n/messages.js +39 -0
  11. package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
  12. package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
  13. package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
  14. package/dist/layout-bridge/headerFooterLayout.js +216 -0
  15. package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
  16. package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
  17. package/dist/layout-bridge/sectionColumns.d.ts +11 -0
  18. package/dist/layout-bridge/sectionColumns.js +21 -0
  19. package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
  20. package/dist/layout-bridge/tableInsertHover.js +102 -0
  21. package/dist/layout-engine/index.d.ts +2 -1
  22. package/dist/layout-engine/index.js +2 -1
  23. package/dist/layout-engine/measure/measureParagraph.js +40 -5
  24. package/dist/layout-engine/pmPageIndex.d.ts +24 -0
  25. package/dist/layout-engine/pmPageIndex.js +37 -0
  26. package/dist/layout-painter/imageLayout.d.ts +101 -0
  27. package/dist/layout-painter/imageLayout.js +161 -0
  28. package/dist/layout-painter/renderPage.d.ts +16 -1
  29. package/dist/layout-painter/renderPage.js +28 -1
  30. package/dist/layout-painter/renderParagraph.js +1 -1
  31. package/dist/layout-painter/sdtBoundary.d.ts +24 -1
  32. package/dist/layout-painter/sdtBoundary.js +40 -1
  33. package/dist/managers/AutoSaveManager.d.ts +89 -0
  34. package/dist/managers/AutoSaveManager.js +279 -0
  35. package/dist/managers/ClipboardManager.d.ts +31 -0
  36. package/dist/managers/ClipboardManager.js +147 -0
  37. package/dist/paged-layout/rangeProjection.d.ts +23 -1
  38. package/dist/paged-layout/rangeProjection.js +34 -1
  39. package/dist/prosemirror/cellDragSelection.d.ts +34 -0
  40. package/dist/prosemirror/cellDragSelection.js +69 -0
  41. package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
  42. package/dist/prosemirror/commands/sectionBreak.js +54 -0
  43. package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
  44. package/dist/prosemirror/commentIdAllocator.js +35 -0
  45. package/dist/prosemirror/commentOps.d.ts +35 -0
  46. package/dist/prosemirror/commentOps.js +104 -0
  47. package/dist/prosemirror/conversion/toProseDoc.js +29 -6
  48. package/dist/prosemirror/extensions/index.d.ts +3 -0
  49. package/dist/prosemirror/extensions/index.js +3 -0
  50. package/dist/prosemirror/imageCommit.d.ts +42 -0
  51. package/dist/prosemirror/imageCommit.js +118 -0
  52. package/dist/prosemirror/paraText.d.ts +26 -0
  53. package/dist/prosemirror/paraText.js +73 -0
  54. package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
  55. package/dist/prosemirror/plugins/templateDirectives.js +40 -1
  56. package/dist/prosemirror/queries.d.ts +46 -0
  57. package/dist/prosemirror/queries.js +74 -0
  58. package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
  59. package/dist/prosemirror/styles/styleResolver.js +34 -0
  60. package/dist/prosemirror/tableResize.d.ts +49 -0
  61. package/dist/prosemirror/tableResize.js +162 -0
  62. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
  63. package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
  64. package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
  65. package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
  66. package/dist/style-engine/index.d.ts +2 -2
  67. package/dist/style-engine/styleEngine.d.ts +14 -1
  68. package/dist/style-engine/styleEngine.js +15 -0
  69. package/dist/utils/colorResolver.d.ts +8 -1
  70. package/dist/utils/colorResolver.js +12 -1
  71. package/dist/utils/findVerticalScrollParent.d.ts +20 -0
  72. package/dist/utils/findVerticalScrollParent.js +30 -0
  73. package/dist/utils/fontResolver.d.ts +22 -3
  74. package/dist/utils/fontResolver.js +216 -31
  75. package/package.json +9 -1
@@ -0,0 +1,102 @@
1
+ //#region src/layout-bridge/tableInsertHover.ts
2
+ /**
3
+ * Table-insert "+" hover hit-test.
4
+ *
5
+ * Pure DOM logic for the floating row/column insert button that shows
6
+ * when the mouse is near the left or top edge of a layout table. Lives
7
+ * in core so React + Vue + any future adapter can share the hit-test
8
+ * and just wire up their own UI rendering of the button.
9
+ *
10
+ * The function is gated by `hfEditMode`: tables in inactive HF/body
11
+ * regions don't surface the affordance. A header table only matches
12
+ * when the user is editing the header; a body table only matches when
13
+ * not in any HF edit mode.
14
+ * @packageDocumentation
15
+ * @public
16
+ */
17
+ const TABLE_INSERT_EDGE_PROXIMITY = 30;
18
+ const TABLE_INSERT_HIDE_DELAY_MS = 200;
19
+ const ROW_BUTTON_OFFSET_X = 24;
20
+ const ROW_BUTTON_OFFSET_Y = 10;
21
+ const COL_BUTTON_OFFSET_X = 10;
22
+ const COL_BUTTON_OFFSET_Y = 24;
23
+ /**
24
+ * Detect whether a mousemove should surface a row/column insert "+" button.
25
+ *
26
+ * Returns the button anchor + target cell PM position, or `null` if the
27
+ * mouse isn't near a row's left edge or a column's top edge — or if the
28
+ * relevant table belongs to an inactive HF/body region.
29
+ */
30
+ function detectTableInsertHover(input) {
31
+ const { mouseX, mouseY, pagesContainer, target, hfEditMode, edgeProximity = 30 } = input;
32
+ const tableSelector = ".layout-table";
33
+ const rowSelector = ".layout-table-row";
34
+ const cellSelector = ".layout-table-cell";
35
+ const headerSelector = ".layout-page-header";
36
+ const footerSelector = ".layout-page-footer";
37
+ const tableIsActive = (t) => {
38
+ const inHeader = !!t.closest(headerSelector);
39
+ const inFooter = !!t.closest(footerSelector);
40
+ if (!inHeader && !inFooter) return !hfEditMode;
41
+ if (inHeader) return hfEditMode === "header";
42
+ return hfEditMode === "footer";
43
+ };
44
+ let tableEl = target.closest(tableSelector);
45
+ if (tableEl && !tableIsActive(tableEl)) tableEl = null;
46
+ if (!tableEl) {
47
+ const tables = pagesContainer.querySelectorAll(tableSelector);
48
+ for (const t of Array.from(tables)) {
49
+ if (!tableIsActive(t)) continue;
50
+ const r = t.getBoundingClientRect();
51
+ const nearLeft = mouseX >= r.left - edgeProximity && mouseX < r.left;
52
+ const nearTop = mouseY >= r.top - edgeProximity && mouseY < r.top;
53
+ const withinX = mouseX >= r.left - edgeProximity && mouseX <= r.right;
54
+ const withinY = mouseY >= r.top - edgeProximity && mouseY <= r.bottom;
55
+ if (nearLeft && withinY || nearTop && withinX) {
56
+ tableEl = t;
57
+ break;
58
+ }
59
+ }
60
+ }
61
+ if (!tableEl) return null;
62
+ const tableRect = tableEl.getBoundingClientRect();
63
+ const nearLeftEdge = mouseX < tableRect.left + edgeProximity && mouseX >= tableRect.left - edgeProximity;
64
+ const nearTopEdge = mouseY < tableRect.top + edgeProximity && mouseY >= tableRect.top - edgeProximity;
65
+ if (!nearLeftEdge && !nearTopEdge) return null;
66
+ const rows = tableEl.querySelectorAll(`:scope > ${rowSelector}`);
67
+ if (rows.length === 0) return null;
68
+ const getCellPmPos = (el) => el ? Number(el.dataset["pmStart"]) || 0 : 0;
69
+ if (nearLeftEdge) for (const row of Array.from(rows)) {
70
+ const rowRect = row.getBoundingClientRect();
71
+ if (mouseY >= rowRect.top && mouseY <= rowRect.bottom) {
72
+ const pmPos = getCellPmPos(row.querySelector(cellSelector));
73
+ if (!pmPos) break;
74
+ return {
75
+ type: "row",
76
+ clientX: tableRect.left - ROW_BUTTON_OFFSET_X,
77
+ clientY: rowRect.top + rowRect.height / 2 - ROW_BUTTON_OFFSET_Y,
78
+ cellPmPos: pmPos
79
+ };
80
+ }
81
+ }
82
+ const firstRow = rows[0];
83
+ if (nearTopEdge && firstRow) {
84
+ const cells = firstRow.querySelectorAll(`:scope > ${cellSelector}`);
85
+ for (const cell of Array.from(cells)) {
86
+ const cellRect = cell.getBoundingClientRect();
87
+ if (mouseX >= cellRect.left && mouseX <= cellRect.right) {
88
+ const pmPos = getCellPmPos(cell);
89
+ if (!pmPos) break;
90
+ return {
91
+ type: "column",
92
+ clientX: cellRect.left + cellRect.width / 2 - COL_BUTTON_OFFSET_X,
93
+ clientY: tableRect.top - COL_BUTTON_OFFSET_Y,
94
+ cellPmPos: pmPos
95
+ };
96
+ }
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ //#endregion
102
+ export { TABLE_INSERT_EDGE_PROXIMITY, TABLE_INSERT_HIDE_DELAY_MS, detectTableInsertHover };
@@ -2,6 +2,7 @@ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, Co
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { BreakDecision, SectionState, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
5
+ import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
5
6
 
6
7
  //#region src/layout-engine/index.d.ts
7
8
  type SectionLayoutConfig = {
@@ -41,4 +42,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
41
42
  */
42
43
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
43
44
  //#endregion
44
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
45
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
@@ -6,6 +6,7 @@ import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
6
6
  import { buildTableRowBreakInfo, snapRowBreak } from "./tableRowBreak.js";
7
7
  import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
8
8
  import { applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
9
+ import { assertExhaustiveFlowBlock, findPageIndexContainingPmPos } from "./pmPageIndex.js";
9
10
  import { panic } from "better-result";
10
11
  //#region src/layout-engine/index.ts
11
12
  /**
@@ -688,4 +689,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
688
689
  paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
689
690
  }
690
691
  //#endregion
691
- export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
692
+ export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
@@ -1,8 +1,9 @@
1
1
  import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js";
2
- import "../../utils/fontResolver.js";
2
+ import { CJK_FALLBACK_FONT_FAMILY, isCjkFont } from "../../utils/fontResolver.js";
3
3
  import { buildRunFontStyle, ptToPx } from "./measureHelpers.js";
4
4
  import { calculateTabWidth, pixelsToTwips } from "../../prosemirror/utils/tabCalculator.js";
5
5
  import { inlineImageBoundingBox } from "../../utils/rotationBoundingBox.js";
6
+ import { hasCjk } from "../../utils/scriptSegments.js";
6
7
  import { measuredLineAdvance } from "../lineFlow.js";
7
8
  import { isFloatingImageRun } from "../types.js";
8
9
  import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
@@ -47,6 +48,35 @@ function runToFontStyle(run) {
47
48
  return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
48
49
  }
49
50
  /**
51
+ * Line-HEIGHT style for a text run. Word derives a CJK line's height from an
52
+ * East-Asian face, not the run's ascii font: real documents routinely put CJK
53
+ * glyphs in runs whose ascii/eastAsia fonts are Latin (e.g. `w:eastAsia`
54
+ * "Century"), and Word font-links those glyphs to the default East-Asian face
55
+ * and uses its taller single-line height (≈1.303 vs the ≈1.15 Latin default).
56
+ *
57
+ * When the run holds CJK text, swap `fontFamily` to the face whose
58
+ * `singleLineRatio` Word would actually use: the run's `w:eastAsia` font when
59
+ * it is a real CJK face, the run's own ascii font when THAT is a CJK face, or
60
+ * `CJK_FALLBACK_FONT_FAMILY` otherwise. Non-CJK runs return `baseStyle`
61
+ * unchanged, so Latin-only documents are unaffected by construction.
62
+ *
63
+ * The result feeds `updateMaxFont` (line height) ONLY — width measurement
64
+ * keeps the base style so wrapping is untouched.
65
+ */
66
+ function cjkLineHeightStyle(run, baseStyle) {
67
+ if (!run.text || !hasCjk(run.text)) return baseStyle;
68
+ const eastAsia = baseStyle.eastAsiaFontFamily;
69
+ if (eastAsia !== void 0 && isCjkFont(eastAsia)) return {
70
+ ...baseStyle,
71
+ fontFamily: eastAsia
72
+ };
73
+ if (isCjkFont(baseStyle.fontFamily ?? DEFAULT_FONT_FAMILY)) return baseStyle;
74
+ return {
75
+ ...baseStyle,
76
+ fontFamily: CJK_FALLBACK_FONT_FAMILY
77
+ };
78
+ }
79
+ /**
50
80
  * Calculate typography metrics from font size and spacing settings
51
81
  *
52
82
  * @param fontSize - Font size in points
@@ -537,7 +567,11 @@ function measureParagraph(block, maxWidth, options) {
537
567
  if (!currentLine.maxFontMetrics || fontSize > currentLine.maxFontSize) {
538
568
  currentLine.maxFontSize = fontSize;
539
569
  currentLine.maxFontMetrics = getFontMetrics(style);
570
+ return;
540
571
  }
572
+ if (fontSize !== currentLine.maxFontSize || !isCjkFont(style.fontFamily ?? "")) return;
573
+ const metrics = getFontMetrics(style);
574
+ if (metrics.singleLineRatio > currentLine.maxFontMetrics.singleLineRatio) currentLine.maxFontMetrics = metrics;
541
575
  };
542
576
  for (let runIndex = 0; runIndex < runs.length; runIndex++) {
543
577
  const run = runs[runIndex];
@@ -640,7 +674,8 @@ function measureParagraph(block, maxWidth, options) {
640
674
  const textRun = run;
641
675
  const text = textRun.text;
642
676
  const style = runToFontStyle(textRun);
643
- updateMaxFont(style);
677
+ const lineHeightStyle = cjkLineHeightStyle(textRun, style);
678
+ updateMaxFont(lineHeightStyle);
644
679
  if (!text || text.length === 0) {
645
680
  currentLine.toRun = runIndex;
646
681
  currentLine.toChar = 0;
@@ -664,7 +699,7 @@ function measureParagraph(block, maxWidth, options) {
664
699
  if (bestEnd === 0) {
665
700
  if (currentLine.width > 0) {
666
701
  startNewLine(runIndex, charIndex + chunkStart);
667
- updateMaxFont(style);
702
+ updateMaxFont(lineHeightStyle);
668
703
  continue;
669
704
  }
670
705
  bestEnd = 1;
@@ -677,7 +712,7 @@ function measureParagraph(block, maxWidth, options) {
677
712
  chunkStart = chunkEnd;
678
713
  if (chunkStart < word.length) {
679
714
  startNewLine(runIndex, charIndex + chunkStart);
680
- updateMaxFont(style);
715
+ updateMaxFont(lineHeightStyle);
681
716
  }
682
717
  }
683
718
  charIndex = nextBreak;
@@ -687,7 +722,7 @@ function measureParagraph(block, maxWidth, options) {
687
722
  const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + WIDTH_TOLERANCE ? rawGlueWidth : 0;
688
723
  if (currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
689
724
  startNewLine(runIndex, charIndex);
690
- updateMaxFont(style);
725
+ updateMaxFont(lineHeightStyle);
691
726
  }
692
727
  currentLine.width += wordWidth;
693
728
  currentLine.toRun = runIndex;
@@ -0,0 +1,24 @@
1
+ import { Layout } from "./types.js";
2
+
3
+ //#region src/layout-engine/pmPageIndex.d.ts
4
+ /**
5
+ * Exhaustiveness guard for `FlowBlock`-shaped switches. Call from the
6
+ * `default` arm with the still-typed value; TypeScript will refuse to
7
+ * compile if any variant of `FlowBlock` was missed. The thrown error
8
+ * names the calling site so runtime failures (e.g. an old adapter
9
+ * compiled against a newer core) point future debuggers at the contract.
10
+ */
11
+ declare function assertExhaustiveFlowBlock(block: never, site: string): never;
12
+ /**
13
+ * Page index (0-based) whose layout fragments cover `pmPos`, or null if none.
14
+ * Used when the painted DOM may not yet have `[data-pm-start]` for this position (virtualization).
15
+ *
16
+ * Range semantics: `[pmStart, pmEnd)` — half-open, matching ProseMirror's
17
+ * `pos + nodeSize` convention. Boundary positions belong to the next fragment,
18
+ * so when a fragment ends at the same position the next one starts, the next
19
+ * fragment wins (avoids returning the previous page for the start of the
20
+ * next paragraph).
21
+ */
22
+ declare function findPageIndexContainingPmPos(layout: Layout, pmPos: number): number | null;
23
+ //#endregion
24
+ export { assertExhaustiveFlowBlock, findPageIndexContainingPmPos };
@@ -0,0 +1,37 @@
1
+ //#region src/layout-engine/pmPageIndex.ts
2
+ /**
3
+ * Exhaustiveness guard for `FlowBlock`-shaped switches. Call from the
4
+ * `default` arm with the still-typed value; TypeScript will refuse to
5
+ * compile if any variant of `FlowBlock` was missed. The thrown error
6
+ * names the calling site so runtime failures (e.g. an old adapter
7
+ * compiled against a newer core) point future debuggers at the contract.
8
+ */
9
+ function assertExhaustiveFlowBlock(block, site) {
10
+ const kind = block.kind ?? "<unknown>";
11
+ throw new Error(`${site}: unhandled FlowBlock kind "${kind}". Add the case alongside the other FlowBlock switches (see types.ts).`);
12
+ }
13
+ /**
14
+ * Page index (0-based) whose layout fragments cover `pmPos`, or null if none.
15
+ * Used when the painted DOM may not yet have `[data-pm-start]` for this position (virtualization).
16
+ *
17
+ * Range semantics: `[pmStart, pmEnd)` — half-open, matching ProseMirror's
18
+ * `pos + nodeSize` convention. Boundary positions belong to the next fragment,
19
+ * so when a fragment ends at the same position the next one starts, the next
20
+ * fragment wins (avoids returning the previous page for the start of the
21
+ * next paragraph).
22
+ */
23
+ function findPageIndexContainingPmPos(layout, pmPos) {
24
+ for (let pi = 0; pi < layout.pages.length; pi++) {
25
+ const page = layout.pages[pi];
26
+ if (!page) continue;
27
+ for (const frag of page.fragments) {
28
+ if (frag.pmStart == null) continue;
29
+ const start = frag.pmStart;
30
+ const end = frag.pmEnd ?? start + 1;
31
+ if (pmPos >= start && pmPos < end) return pi;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ //#endregion
37
+ export { assertExhaustiveFlowBlock, findPageIndexContainingPmPos };
@@ -0,0 +1,101 @@
1
+ import { FlowBlock, Measure } from "../layout-engine/types.js";
2
+ import { BlockLookup } from "./index.js";
3
+ import { WrapType } from "../docx/wrapTypes.js";
4
+ import { ImageAttrs } from "../prosemirror/schema/nodes.js";
5
+
6
+ //#region src/layout-painter/imageLayout.d.ts
7
+ /**
8
+ * Anchored wrap-type targets — the OOXML wrap types except `inline`.
9
+ * Defined locally because the fork's `ImageExtension` does not export the
10
+ * layout-target vocabulary the way upstream does.
11
+ */
12
+ type AnchorWrapType = Exclude<WrapType, "inline">;
13
+ /**
14
+ * User-facing layout choices that mirror Word's Wrap Text menu. `squareLeft`
15
+ * floats the image left (text wraps on the right); `squareRight` floats it
16
+ * right; `inline` flows it as a glyph; the remaining targets are 1:1 with
17
+ * their OOXML wrap types.
18
+ */
19
+ type ImageLayoutTarget = AnchorWrapType | "squareLeft" | "squareRight" | "inline";
20
+ declare const LAYOUT_IMAGE_CLASSES: {
21
+ /** Inline image rendered inside `.layout-line`. */readonly runImage: "layout-run-image"; /** Block (centered, topAndBottom) image. */
22
+ readonly blockImage: "layout-block-image"; /** Anchored image rendered in the page-level floating layer. */
23
+ readonly pageFloatingImage: "layout-page-floating-image"; /** Anchored image rendered inside a table cell's floating layer. */
24
+ readonly cellFloatingImage: "layout-cell-floating-image";
25
+ readonly pageContent: "layout-page-content";
26
+ readonly paragraph: "layout-paragraph";
27
+ };
28
+ /**
29
+ * Walk up from a click target to the nearest rendered image element, returning
30
+ * just the element (no PM position parsing). Used by the left-click selection
31
+ * path in both adapters — `data-pm-start` is read separately by callers that
32
+ * need the position. Returns the element when the click was directly on an
33
+ * inline `<img.layout-run-image>` OR inside one of the container classes
34
+ * registered in `LAYOUT_IMAGE_CLASSES`.
35
+ */
36
+ declare function findImageElement(target: EventTarget | null): HTMLElement | null;
37
+ /**
38
+ * Capture the rendered position of an inline image as EMUs, normalised to
39
+ * unzoomed coordinates. Returns horizontal offset relative to the page
40
+ * content area (column origin) and vertical offset relative to the
41
+ * containing paragraph — matches the OOXML attrs the resolver writes
42
+ * (`relativeFrom: 'column'` / `relativeFrom: 'paragraph'`).
43
+ *
44
+ * `zoom` defaults to 1; pass the editor's current zoom factor when the
45
+ * pages container has a CSS scale applied so `getBoundingClientRect` deltas
46
+ * are converted back to authored-pixel space before going to EMU.
47
+ *
48
+ * Returns undefined for non-inline images or detached DOM.
49
+ */
50
+ declare function captureInlinePositionEmu(imageEl: HTMLElement, zoom?: number): {
51
+ horizontalEmu: number;
52
+ verticalEmu: number;
53
+ } | undefined;
54
+ /**
55
+ * Map an image's current OOXML attrs onto the menu's choice vocabulary so the
56
+ * menu can highlight the active option. Returns null for `topAndBottom` and
57
+ * any unknown wrap type — those don't have a directional menu entry.
58
+ *
59
+ * `cssFloat` accepts both `null` and `undefined` so framework adapters can
60
+ * use either as their "unset" sentinel without an extra normalisation step.
61
+ */
62
+ declare function deriveLayoutChoice(wrapType: WrapType, cssFloat?: ImageAttrs["cssFloat"] | null): ImageLayoutTarget | null;
63
+ /**
64
+ * Hint to the framework's icon registry for which Material Symbol — or
65
+ * equivalent — to render alongside each option. Bindings own the icon
66
+ * component itself.
67
+ */
68
+ type ImageLayoutIconHint = "inline" | "squareLeft" | "squareRight" | "behind" | "inFront";
69
+ type ImageLayoutOptionDef = {
70
+ /** Choice value — what gets dispatched on click. */choice: ImageLayoutTarget; /** i18n key under `imageWrap.menu.*`. */
71
+ i18nLabelKey: string; /** i18n key under `imageWrap.menuDesc.*`. */
72
+ i18nDescKey: string; /** Hint for the framework's icon registry. */
73
+ iconHint: ImageLayoutIconHint;
74
+ };
75
+ /** Mirrors Word's Wrap Text menu — five directional options. */
76
+ declare const IMAGE_LAYOUT_OPTIONS: readonly ImageLayoutOptionDef[];
77
+ /**
78
+ * Whether a given option is enabled for an image with the given current wrap
79
+ * type. Every option stays clickable — picking the option that matches the
80
+ * current state is a no-op (the PM command early-returns), which matches
81
+ * Word's behavior. Kept around for forward-compatibility (e.g. future
82
+ * read-only mode), but currently always returns true.
83
+ */
84
+ declare function isImageLayoutOptionEnabled(_option: ImageLayoutOptionDef, _currentWrapType: WrapType): boolean;
85
+ /**
86
+ * Translate the legacy toolbar wrap-type vocabulary (`wrapLeft` / `wrapRight`
87
+ * / `square` / `tight` / `through` / `topAndBottom` / `behind` / `inFront` /
88
+ * `inline`) into an `ImageLayoutTarget` so toolbar dispatch shares the same PM
89
+ * command path as the right-click menu.
90
+ *
91
+ * Returns `undefined` for unknown values; callers should treat that as
92
+ * "no-op".
93
+ */
94
+ declare function toolbarValueToLayoutTarget(value: string): ImageLayoutTarget | undefined;
95
+ /**
96
+ * Build the painter's `block.id → { block, measure }` lookup from the parallel
97
+ * blocks/measures arrays. Shared by both adapters' paint step.
98
+ */
99
+ declare function buildBlockLookup(blocks: FlowBlock[], measures: Measure[]): BlockLookup;
100
+ //#endregion
101
+ export { AnchorWrapType, IMAGE_LAYOUT_OPTIONS, ImageLayoutIconHint, ImageLayoutOptionDef, ImageLayoutTarget, LAYOUT_IMAGE_CLASSES, buildBlockLookup, captureInlinePositionEmu, deriveLayoutChoice, findImageElement, isImageLayoutOptionEnabled, toolbarValueToLayoutTarget };
@@ -0,0 +1,161 @@
1
+ import { pixelsToEmu } from "../utils/units.js";
2
+ //#region src/layout-painter/imageLayout.ts
3
+ const LAYOUT_IMAGE_CLASSES = {
4
+ /** Inline image rendered inside `.layout-line`. */
5
+ runImage: "layout-run-image",
6
+ /** Block (centered, topAndBottom) image. */
7
+ blockImage: "layout-block-image",
8
+ /** Anchored image rendered in the page-level floating layer. */
9
+ pageFloatingImage: "layout-page-floating-image",
10
+ /** Anchored image rendered inside a table cell's floating layer. */
11
+ cellFloatingImage: "layout-cell-floating-image",
12
+ pageContent: "layout-page-content",
13
+ paragraph: "layout-paragraph"
14
+ };
15
+ const IMAGE_HIT_SELECTOR = [
16
+ `.${LAYOUT_IMAGE_CLASSES.pageFloatingImage}`,
17
+ `.${LAYOUT_IMAGE_CLASSES.cellFloatingImage}`,
18
+ `.${LAYOUT_IMAGE_CLASSES.blockImage}`,
19
+ `.${LAYOUT_IMAGE_CLASSES.runImage}`
20
+ ].join(", ");
21
+ /**
22
+ * Walk up from a click target to the nearest rendered image element, returning
23
+ * just the element (no PM position parsing). Used by the left-click selection
24
+ * path in both adapters — `data-pm-start` is read separately by callers that
25
+ * need the position. Returns the element when the click was directly on an
26
+ * inline `<img.layout-run-image>` OR inside one of the container classes
27
+ * registered in `LAYOUT_IMAGE_CLASSES`.
28
+ */
29
+ function findImageElement(target) {
30
+ if (!(target instanceof HTMLElement)) return null;
31
+ if (target.tagName === "IMG" && target.classList.contains(LAYOUT_IMAGE_CLASSES.runImage)) return target;
32
+ const container = target.closest(IMAGE_HIT_SELECTOR);
33
+ if (container && container.dataset["pmStart"]) return container;
34
+ return null;
35
+ }
36
+ /**
37
+ * Capture the rendered position of an inline image as EMUs, normalised to
38
+ * unzoomed coordinates. Returns horizontal offset relative to the page
39
+ * content area (column origin) and vertical offset relative to the
40
+ * containing paragraph — matches the OOXML attrs the resolver writes
41
+ * (`relativeFrom: 'column'` / `relativeFrom: 'paragraph'`).
42
+ *
43
+ * `zoom` defaults to 1; pass the editor's current zoom factor when the
44
+ * pages container has a CSS scale applied so `getBoundingClientRect` deltas
45
+ * are converted back to authored-pixel space before going to EMU.
46
+ *
47
+ * Returns undefined for non-inline images or detached DOM.
48
+ */
49
+ function captureInlinePositionEmu(imageEl, zoom = 1) {
50
+ if (!imageEl.classList.contains(LAYOUT_IMAGE_CLASSES.runImage)) return;
51
+ const pageContent = imageEl.closest(`.${LAYOUT_IMAGE_CLASSES.pageContent}`);
52
+ const paragraph = imageEl.closest(`.${LAYOUT_IMAGE_CLASSES.paragraph}`);
53
+ if (!pageContent || !paragraph) return;
54
+ const imgRect = imageEl.getBoundingClientRect();
55
+ const pageRect = pageContent.getBoundingClientRect();
56
+ const paraRect = paragraph.getBoundingClientRect();
57
+ const safeZoom = zoom > 0 ? zoom : 1;
58
+ return {
59
+ horizontalEmu: Math.round(pixelsToEmu((imgRect.left - pageRect.left) / safeZoom)),
60
+ verticalEmu: Math.round(pixelsToEmu((imgRect.top - paraRect.top) / safeZoom))
61
+ };
62
+ }
63
+ /**
64
+ * Map an image's current OOXML attrs onto the menu's choice vocabulary so the
65
+ * menu can highlight the active option. Returns null for `topAndBottom` and
66
+ * any unknown wrap type — those don't have a directional menu entry.
67
+ *
68
+ * `cssFloat` accepts both `null` and `undefined` so framework adapters can
69
+ * use either as their "unset" sentinel without an extra normalisation step.
70
+ */
71
+ function deriveLayoutChoice(wrapType, cssFloat) {
72
+ if (wrapType === "inline") return "inline";
73
+ if (wrapType === "behind") return "behind";
74
+ if (wrapType === "inFront") return "inFront";
75
+ if (wrapType === "square" || wrapType === "tight" || wrapType === "through") return cssFloat === "right" ? "squareRight" : "squareLeft";
76
+ return null;
77
+ }
78
+ /** Mirrors Word's Wrap Text menu — five directional options. */
79
+ const IMAGE_LAYOUT_OPTIONS = [
80
+ {
81
+ choice: "inline",
82
+ i18nLabelKey: "inLineWithText",
83
+ i18nDescKey: "inLineWithText",
84
+ iconHint: "inline"
85
+ },
86
+ {
87
+ choice: "squareLeft",
88
+ i18nLabelKey: "squareLeft",
89
+ i18nDescKey: "squareLeft",
90
+ iconHint: "squareLeft"
91
+ },
92
+ {
93
+ choice: "squareRight",
94
+ i18nLabelKey: "squareRight",
95
+ i18nDescKey: "squareRight",
96
+ iconHint: "squareRight"
97
+ },
98
+ {
99
+ choice: "behind",
100
+ i18nLabelKey: "behindText",
101
+ i18nDescKey: "behindText",
102
+ iconHint: "behind"
103
+ },
104
+ {
105
+ choice: "inFront",
106
+ i18nLabelKey: "inFrontOfText",
107
+ i18nDescKey: "inFrontOfText",
108
+ iconHint: "inFront"
109
+ }
110
+ ];
111
+ /**
112
+ * Whether a given option is enabled for an image with the given current wrap
113
+ * type. Every option stays clickable — picking the option that matches the
114
+ * current state is a no-op (the PM command early-returns), which matches
115
+ * Word's behavior. Kept around for forward-compatibility (e.g. future
116
+ * read-only mode), but currently always returns true.
117
+ */
118
+ function isImageLayoutOptionEnabled(_option, _currentWrapType) {
119
+ return true;
120
+ }
121
+ /**
122
+ * Translate the legacy toolbar wrap-type vocabulary (`wrapLeft` / `wrapRight`
123
+ * / `square` / `tight` / `through` / `topAndBottom` / `behind` / `inFront` /
124
+ * `inline`) into an `ImageLayoutTarget` so toolbar dispatch shares the same PM
125
+ * command path as the right-click menu.
126
+ *
127
+ * Returns `undefined` for unknown values; callers should treat that as
128
+ * "no-op".
129
+ */
130
+ function toolbarValueToLayoutTarget(value) {
131
+ switch (value) {
132
+ case "inline": return "inline";
133
+ case "square":
134
+ case "tight":
135
+ case "through": return "squareLeft";
136
+ case "topAndBottom": return "topAndBottom";
137
+ case "behind": return "behind";
138
+ case "inFront": return "inFront";
139
+ case "wrapRight": return "squareLeft";
140
+ case "wrapLeft": return "squareRight";
141
+ default: return;
142
+ }
143
+ }
144
+ /**
145
+ * Build the painter's `block.id → { block, measure }` lookup from the parallel
146
+ * blocks/measures arrays. Shared by both adapters' paint step.
147
+ */
148
+ function buildBlockLookup(blocks, measures) {
149
+ const lookup = /* @__PURE__ */ new Map();
150
+ for (let i = 0; i < blocks.length; i++) {
151
+ const block = blocks[i];
152
+ const measure = measures[i];
153
+ if (block && measure) lookup.set(String(block.id), {
154
+ block,
155
+ measure
156
+ });
157
+ }
158
+ return lookup;
159
+ }
160
+ //#endregion
161
+ export { IMAGE_LAYOUT_OPTIONS, LAYOUT_IMAGE_CLASSES, buildBlockLookup, captureInlinePositionEmu, deriveLayoutChoice, findImageElement, isImageLayoutOptionEnabled, toolbarValueToLayoutTarget };
@@ -242,6 +242,21 @@ type PainterPaintedDetail = {
242
242
  container: HTMLElement;
243
243
  pageCount: number;
244
244
  };
245
+ /**
246
+ * Force every virtualized page shell in `container` to be fully rendered.
247
+ *
248
+ * Virtualization keeps off-screen pages as empty shells, so cloning the pages
249
+ * container for print (or any DOM snapshot) yields blank pages past the visible
250
+ * band. Callers that need every page populated — print, export-to-HTML, pdf
251
+ * snapshot — should call this first.
252
+ *
253
+ * No-op for small documents (rendered eagerly) or containers that were never
254
+ * managed by `renderPages`. Returns the number of shells populated by this
255
+ * call (useful for tests).
256
+ *
257
+ * Ported from eigenpal/docx-editor `layout-painter/renderPage/virtualization.ts`.
258
+ */
259
+ declare function renderAllPagesNow(container: HTMLElement): number;
245
260
  /**
246
261
  * Find the page shell whose layout fragments cover (or are nearest
247
262
  * before) a given ProseMirror position. Returns null if the
@@ -261,4 +276,4 @@ declare function findPageShellForPmPos(container: HTMLElement, pmPos: number): {
261
276
  isExact: boolean;
262
277
  } | null;
263
278
  //#endregion
264
- export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
279
+ export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -1668,6 +1668,33 @@ function populatePageShell(shell, pageDataMap, totalPages, options) {
1668
1668
  if (container instanceof HTMLElement) emitPainterPainted(container);
1669
1669
  }
1670
1670
  /**
1671
+ * Force every virtualized page shell in `container` to be fully rendered.
1672
+ *
1673
+ * Virtualization keeps off-screen pages as empty shells, so cloning the pages
1674
+ * container for print (or any DOM snapshot) yields blank pages past the visible
1675
+ * band. Callers that need every page populated — print, export-to-HTML, pdf
1676
+ * snapshot — should call this first.
1677
+ *
1678
+ * No-op for small documents (rendered eagerly) or containers that were never
1679
+ * managed by `renderPages`. Returns the number of shells populated by this
1680
+ * call (useful for tests).
1681
+ *
1682
+ * Ported from eigenpal/docx-editor `layout-painter/renderPage/virtualization.ts`.
1683
+ */
1684
+ function renderAllPagesNow(container) {
1685
+ const state = container.__pageRenderState;
1686
+ if (!state) return 0;
1687
+ const { pageStates, totalPages, currentOptions, pageDataMap } = state;
1688
+ let populated = 0;
1689
+ for (const { element } of pageStates) {
1690
+ const data = pageDataMap.get(element);
1691
+ if (!data || data.rendered) continue;
1692
+ populatePageShell(element, pageDataMap, totalPages, currentOptions);
1693
+ populated++;
1694
+ }
1695
+ return populated;
1696
+ }
1697
+ /**
1671
1698
  * Surgically replace only the content area of a rendered page shell.
1672
1699
  * Preserves header/footer elements to avoid blinking.
1673
1700
  */
@@ -1792,4 +1819,4 @@ function depopulatePageShell(shell, pageDataMap) {
1792
1819
  }
1793
1820
  }
1794
1821
  //#endregion
1795
- export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
1822
+ export { PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft };
@@ -3,9 +3,9 @@ import { resolveFontFamily } from "../utils/fontResolver.js";
3
3
  import "../utils/fontWeights.js";
4
4
  import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
5
5
  import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
6
+ import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
6
7
  import { isFloatingImageRun } from "../layout-engine/types.js";
7
8
  import { getListMarkerInlineWidth } from "../layout-engine/measure/listMarkerWidth.js";
8
- import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
9
9
  import { applySdtDataAttrs } from "./sdtBoundary.js";
10
10
  import { applyImageVisualAttrs, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
11
11
  import { ommlToMathml } from "../docx/mathToMathml.js";