@stll/folio-core 0.1.2 → 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.
- package/dist/docx/headerFooterParser.js +3 -0
- package/dist/docx/headerFooterVerbatim.d.ts +19 -0
- package/dist/docx/headerFooterVerbatim.js +25 -0
- package/dist/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/metafileRaster.d.ts +30 -0
- package/dist/docx/metafileRaster.js +80 -0
- package/dist/docx/paragraphParser.js +5 -1
- package/dist/docx/parser.d.ts +12 -2
- package/dist/docx/parser.js +31 -1
- package/dist/docx/serializer/headerFooterSerializer.js +3 -0
- package/dist/docx/styleParser.js +2 -2
- package/dist/docx/tableParser.js +2 -3
- package/dist/docx/unzip.js +7 -1
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/docx/xmlParser.d.ts +7 -1
- package/dist/docx/xmlParser.js +17 -1
- package/dist/i18n/messages/catalogs.gen.d.ts +2776 -0
- package/dist/i18n/messages/catalogs.gen.js +2742 -0
- package/dist/i18n/messages/messages.gen.d.ts +166 -0
- package/dist/i18n/messages/messages.gen.js +0 -0
- package/dist/i18n/messages.d.ts +13 -0
- package/dist/i18n/messages.js +39 -0
- package/dist/layout-bridge/cellSelectionHighlight.d.ts +18 -0
- package/dist/layout-bridge/cellSelectionHighlight.js +36 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -2
- package/dist/layout-bridge/convert/headerFooterLayout.js +39 -15
- package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
- package/dist/layout-bridge/headerFooterLayout.d.ts +50 -0
- package/dist/layout-bridge/headerFooterLayout.js +216 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.d.ts +33 -0
- package/dist/layout-bridge/measuring/measureBlocksPipeline.js +0 -0
- package/dist/layout-bridge/sectionColumns.d.ts +11 -0
- package/dist/layout-bridge/sectionColumns.js +21 -0
- package/dist/layout-bridge/tableInsertHover.d.ts +42 -0
- package/dist/layout-bridge/tableInsertHover.js +102 -0
- package/dist/layout-engine/index.d.ts +2 -1
- package/dist/layout-engine/index.js +2 -1
- package/dist/layout-engine/measure/lineBreaks.d.ts +10 -0
- package/dist/layout-engine/measure/lineBreaks.js +30 -0
- package/dist/layout-engine/measure/measureContainer.js +1 -1
- package/dist/layout-engine/measure/measureParagraph.js +63 -22
- package/dist/layout-engine/pmPageIndex.d.ts +24 -0
- package/dist/layout-engine/pmPageIndex.js +37 -0
- package/dist/layout-engine/types.d.ts +2 -1
- package/dist/layout-painter/imageLayout.d.ts +101 -0
- package/dist/layout-painter/imageLayout.js +161 -0
- package/dist/layout-painter/renderPage.d.ts +16 -1
- package/dist/layout-painter/renderPage.js +28 -1
- package/dist/layout-painter/renderParagraph.js +2 -1
- package/dist/layout-painter/sdtBoundary.d.ts +24 -1
- package/dist/layout-painter/sdtBoundary.js +40 -1
- package/dist/managers/AutoSaveManager.d.ts +89 -0
- package/dist/managers/AutoSaveManager.js +279 -0
- package/dist/managers/ClipboardManager.d.ts +31 -0
- package/dist/managers/ClipboardManager.js +147 -0
- package/dist/paged-layout/paragraphFlash.d.ts +17 -0
- package/dist/paged-layout/paragraphFlash.js +56 -0
- package/dist/paged-layout/paragraphFlashTypes.d.ts +18 -0
- package/dist/paged-layout/paragraphFlashTypes.js +0 -0
- package/dist/paged-layout/rangeProjection.d.ts +23 -1
- package/dist/paged-layout/rangeProjection.js +34 -1
- package/dist/prosemirror/cellDragSelection.d.ts +34 -0
- package/dist/prosemirror/cellDragSelection.js +69 -0
- package/dist/prosemirror/commands/sectionBreak.d.ts +15 -0
- package/dist/prosemirror/commands/sectionBreak.js +54 -0
- package/dist/prosemirror/commentIdAllocator.d.ts +32 -0
- package/dist/prosemirror/commentIdAllocator.js +35 -0
- package/dist/prosemirror/commentOps.d.ts +35 -0
- package/dist/prosemirror/commentOps.js +104 -0
- package/dist/prosemirror/conversion/toProseDoc.js +29 -6
- package/dist/prosemirror/extensions/index.d.ts +3 -0
- package/dist/prosemirror/extensions/index.js +3 -0
- package/dist/prosemirror/imageCommit.d.ts +42 -0
- package/dist/prosemirror/imageCommit.js +118 -0
- package/dist/prosemirror/paraText.d.ts +26 -0
- package/dist/prosemirror/paraText.js +73 -0
- package/dist/prosemirror/plugins/templateDirectives.d.ts +21 -1
- package/dist/prosemirror/plugins/templateDirectives.js +40 -1
- package/dist/prosemirror/queries.d.ts +46 -0
- package/dist/prosemirror/queries.js +74 -0
- package/dist/prosemirror/styles/styleResolver.d.ts +43 -1
- package/dist/prosemirror/styles/styleResolver.js +34 -0
- package/dist/prosemirror/tableResize.d.ts +49 -0
- package/dist/prosemirror/tableResize.js +162 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +106 -0
- package/dist/prosemirror/utils/extractTrackedChanges.js +379 -0
- package/dist/prosemirror/utils/visualLineNavigation.d.ts +27 -0
- package/dist/prosemirror/utils/visualLineNavigation.js +217 -0
- package/dist/style-engine/index.d.ts +2 -2
- package/dist/style-engine/styleEngine.d.ts +14 -1
- package/dist/style-engine/styleEngine.js +15 -0
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +12 -1
- package/dist/utils/findVerticalScrollParent.d.ts +20 -0
- package/dist/utils/findVerticalScrollParent.js +30 -0
- package/dist/utils/fontResolver.d.ts +22 -3
- package/dist/utils/fontResolver.js +216 -31
- package/dist/utils/headerFooter.js +2 -0
- package/package.json +9 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/layout-bridge/measuring/measureBlocksPipeline.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Floating-aware block measurement pipeline (adapter-facing surface).
|
|
4
|
+
*
|
|
5
|
+
* Upstream exposes a generic `measureBlocksWithFloats(blocks, contentWidth,
|
|
6
|
+
* measureBlock, pageGeometry)` orchestrator: it pre-scans blocks for floating
|
|
7
|
+
* exclusion zones, groups co-located floats, then walks the blocks calling a
|
|
8
|
+
* caller-supplied `measureBlock` with the active zones + cumulative Y.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Page geometry (CSS px) used to resolve page/margin-relative anchored objects
|
|
15
|
+
* into content-area coordinates — currently the vertical anchor of a top-pinned
|
|
16
|
+
* `topAndBottom` band. Structurally identical to the painter's `PageGeometry`,
|
|
17
|
+
* declared here rather than imported because layout-bridge must not import from
|
|
18
|
+
* layout-painter (arch boundary); both paths resolve to identical positions.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
type FloatPageGeometry = {
|
|
23
|
+
pageWidth: number;
|
|
24
|
+
pageHeight: number;
|
|
25
|
+
marginLeft: number;
|
|
26
|
+
marginTop: number;
|
|
27
|
+
marginRight: number;
|
|
28
|
+
marginBottom: number;
|
|
29
|
+
contentWidth: number;
|
|
30
|
+
contentHeight: number;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { FloatPageGeometry };
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { ColumnLayout } from "../layout-engine/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/layout-bridge/sectionColumns.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Extract column layout from section properties.
|
|
7
|
+
* Returns undefined for single-column (default) to avoid unnecessary paginator overhead.
|
|
8
|
+
*/
|
|
9
|
+
declare function getColumns(sectionProps: document_d_exports.SectionProperties | null | undefined): ColumnLayout | undefined;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { getColumns };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { twipsToPixels } from "../paged-layout/sectionGeometry.js";
|
|
2
|
+
//#region src/layout-bridge/sectionColumns.ts
|
|
3
|
+
/** Default column spacing per OOXML spec: 720 twips (0.5 inch). */
|
|
4
|
+
const DEFAULT_COLUMN_SPACE_TWIPS = 720;
|
|
5
|
+
/**
|
|
6
|
+
* Extract column layout from section properties.
|
|
7
|
+
* Returns undefined for single-column (default) to avoid unnecessary paginator overhead.
|
|
8
|
+
*/
|
|
9
|
+
function getColumns(sectionProps) {
|
|
10
|
+
const count = sectionProps?.columnCount ?? 1;
|
|
11
|
+
if (count <= 1) return void 0;
|
|
12
|
+
const columns = {
|
|
13
|
+
count,
|
|
14
|
+
gap: twipsToPixels(sectionProps?.columnSpace ?? DEFAULT_COLUMN_SPACE_TWIPS),
|
|
15
|
+
equalWidth: sectionProps?.equalWidth ?? true
|
|
16
|
+
};
|
|
17
|
+
if (sectionProps?.separator !== void 0) columns.separator = sectionProps.separator;
|
|
18
|
+
return columns;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { getColumns };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/layout-bridge/tableInsertHover.d.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
|
+
declare const TABLE_INSERT_EDGE_PROXIMITY = 30;
|
|
18
|
+
declare const TABLE_INSERT_HIDE_DELAY_MS = 200;
|
|
19
|
+
type TableInsertHoverHit = {
|
|
20
|
+
type: "row" | "column"; /** Client-coordinate anchor for the button. Caller converts to its UI frame. */
|
|
21
|
+
clientX: number;
|
|
22
|
+
clientY: number; /** PM position of the cell the button targets. */
|
|
23
|
+
cellPmPos: number;
|
|
24
|
+
};
|
|
25
|
+
type TableInsertHoverInput = {
|
|
26
|
+
mouseX: number;
|
|
27
|
+
mouseY: number;
|
|
28
|
+
pagesContainer: HTMLElement; /** Element under the cursor at the time of the event (e.target). */
|
|
29
|
+
target: HTMLElement;
|
|
30
|
+
hfEditMode: "header" | "footer" | null; /** Override edge proximity in pixels (defaults to TABLE_INSERT_EDGE_PROXIMITY). */
|
|
31
|
+
edgeProximity?: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Detect whether a mousemove should surface a row/column insert "+" button.
|
|
35
|
+
*
|
|
36
|
+
* Returns the button anchor + target cell PM position, or `null` if the
|
|
37
|
+
* mouse isn't near a row's left edge or a column's top edge — or if the
|
|
38
|
+
* relevant table belongs to an inactive HF/body region.
|
|
39
|
+
*/
|
|
40
|
+
declare function detectTableInsertHover(input: TableInsertHoverInput): TableInsertHoverHit | null;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { TABLE_INSERT_EDGE_PROXIMITY, TABLE_INSERT_HIDE_DELAY_MS, TableInsertHoverHit, TableInsertHoverInput, detectTableInsertHover };
|
|
@@ -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 };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/layout-engine/measure/lineBreaks.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Indices in `text` where a line may end. Latin text breaks after
|
|
4
|
+
* whitespace or hyphen; CJK text can break after every ideograph so a
|
|
5
|
+
* partial line is filled instead of pushing the whole run to the next line.
|
|
6
|
+
*/
|
|
7
|
+
declare function findWordBreaks(text: string): number[];
|
|
8
|
+
declare function isBreakChar(char: string | undefined): boolean;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { findWordBreaks, isBreakChar };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isCjkCodePoint } from "../../utils/scriptSegments.js";
|
|
2
|
+
//#region src/layout-engine/measure/lineBreaks.ts
|
|
3
|
+
/**
|
|
4
|
+
* Indices in `text` where a line may end. Latin text breaks after
|
|
5
|
+
* whitespace or hyphen; CJK text can break after every ideograph so a
|
|
6
|
+
* partial line is filled instead of pushing the whole run to the next line.
|
|
7
|
+
*/
|
|
8
|
+
function findWordBreaks(text) {
|
|
9
|
+
const breaks = [];
|
|
10
|
+
for (let index = 0; index < text.length;) {
|
|
11
|
+
const codePoint = text.codePointAt(index);
|
|
12
|
+
if (codePoint === void 0) break;
|
|
13
|
+
const charLength = codePoint > 65535 ? 2 : 1;
|
|
14
|
+
const char = text[index];
|
|
15
|
+
if (char === " " || char === "-" || char === " ") breaks.push(index + charLength);
|
|
16
|
+
else if (isCjkCodePoint(codePoint)) breaks.push(index + charLength);
|
|
17
|
+
index += charLength;
|
|
18
|
+
}
|
|
19
|
+
return breaks;
|
|
20
|
+
}
|
|
21
|
+
function isBreakChar(char) {
|
|
22
|
+
if (char === void 0) return false;
|
|
23
|
+
if (char === " " || char === "-" || char === " ") return true;
|
|
24
|
+
const codePoint = char.codePointAt(0);
|
|
25
|
+
if (codePoint === void 0) return false;
|
|
26
|
+
if (char >= "\udc00" && char <= "\udfff") return true;
|
|
27
|
+
return isCjkCodePoint(codePoint);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { findWordBreaks, isBreakChar };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setMeasureProvider } from "./measureProvider.js";
|
|
2
2
|
import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
|
|
3
|
-
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
4
3
|
import { hasCjk, isCjkCodePoint, segmentByScript } from "../../utils/scriptSegments.js";
|
|
4
|
+
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
5
5
|
import { canPrefetchMeasurement, prefetchMeasurement } from "./measureWorker.js";
|
|
6
6
|
import { WORKER_FONT_FINGERPRINT_TEXT, countCodePoints } from "./measureWorkerProtocol.js";
|
|
7
7
|
import { panic } from "better-result";
|
|
@@ -1,13 +1,15 @@
|
|
|
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";
|
|
9
10
|
import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
|
|
10
11
|
import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
|
|
12
|
+
import { findWordBreaks, isBreakChar } from "./lineBreaks.js";
|
|
11
13
|
//#region src/layout-engine/measure/measureParagraph.ts
|
|
12
14
|
/**
|
|
13
15
|
* Paragraph measurement module
|
|
@@ -46,6 +48,35 @@ function runToFontStyle(run) {
|
|
|
46
48
|
return buildRunFontStyle(run, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE);
|
|
47
49
|
}
|
|
48
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
|
+
/**
|
|
49
80
|
* Calculate typography metrics from font size and spacing settings
|
|
50
81
|
*
|
|
51
82
|
* @param fontSize - Font size in points
|
|
@@ -207,18 +238,34 @@ function isEmptyTextRun(run) {
|
|
|
207
238
|
* desync measurer and painter on tab advance for paragraphs with a tab
|
|
208
239
|
* preceding a floating image.
|
|
209
240
|
*/
|
|
241
|
+
function isBlockLayoutImageRun(run) {
|
|
242
|
+
return run.wrapType === "topAndBottom" || run.displayMode === "block";
|
|
243
|
+
}
|
|
210
244
|
function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
211
245
|
let width = 0;
|
|
212
246
|
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
213
247
|
const next = runs[i];
|
|
214
248
|
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
249
|
+
if (isImageRun(next)) {
|
|
250
|
+
if (isBlockLayoutImageRun(next)) break;
|
|
251
|
+
if (!isFloatingImageRun(next)) width += inlineImageBoundingBox(next).width || 0;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
215
254
|
if (isTextRun(next)) width += measureTextWidth(next.text || "", runToFontStyle(next));
|
|
216
255
|
else if (isFieldRun(next)) width += measureTextWidth(fieldMeasureText(next, fieldValues), runToFontStyle(next));
|
|
217
|
-
else if (isImageRun(next) && !isFloatingImageRun(next)) width += next.width || 0;
|
|
218
256
|
else if (isMathRun(next)) width += measureTextWidth(next.plainText || "[equation]", runToFontStyle(next));
|
|
219
257
|
}
|
|
220
258
|
return width;
|
|
221
259
|
}
|
|
260
|
+
function hasFollowingTabOnLine(runs, tabIndex) {
|
|
261
|
+
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
262
|
+
const next = runs[i];
|
|
263
|
+
if (!next || isLineBreakRun(next)) break;
|
|
264
|
+
if (isImageRun(next) && isBlockLayoutImageRun(next)) break;
|
|
265
|
+
if (isTabRun(next)) return true;
|
|
266
|
+
}
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
222
269
|
/**
|
|
223
270
|
* Width of the inline content preceding the first `.` in the runs that follow
|
|
224
271
|
* a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
|
|
@@ -249,21 +296,6 @@ function measureDecimalPrefixWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
|
249
296
|
if (decimalIndex === -1 || !firstRun) return 0;
|
|
250
297
|
return measureTextWidth(text.slice(0, decimalIndex), runToFontStyle(firstRun));
|
|
251
298
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Find word break points in text
|
|
254
|
-
* Returns array of indices where words end (after space/punctuation)
|
|
255
|
-
*/
|
|
256
|
-
function findWordBreaks(text) {
|
|
257
|
-
const breaks = [];
|
|
258
|
-
for (let i = 0; i < text.length; i++) {
|
|
259
|
-
const char = text[i];
|
|
260
|
-
if (char === " " || char === "-" || char === " ") breaks.push(i + 1);
|
|
261
|
-
}
|
|
262
|
-
return breaks;
|
|
263
|
-
}
|
|
264
|
-
function isBreakChar(char) {
|
|
265
|
-
return char === " " || char === "-" || char === " ";
|
|
266
|
-
}
|
|
267
299
|
function isSpaceOrTab(char) {
|
|
268
300
|
return char === " " || char === " ";
|
|
269
301
|
}
|
|
@@ -535,7 +567,11 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
535
567
|
if (!currentLine.maxFontMetrics || fontSize > currentLine.maxFontSize) {
|
|
536
568
|
currentLine.maxFontSize = fontSize;
|
|
537
569
|
currentLine.maxFontMetrics = getFontMetrics(style);
|
|
570
|
+
return;
|
|
538
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;
|
|
539
575
|
};
|
|
540
576
|
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
|
|
541
577
|
const run = runs[runIndex];
|
|
@@ -551,13 +587,17 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
551
587
|
const followingWidth = measureInlineWidthAfterTab(runs, runIndex, options?.fieldValues);
|
|
552
588
|
const decimalPrefixWidth = measureDecimalPrefixWidthAfterTab(runs, runIndex, options?.fieldValues);
|
|
553
589
|
const lineX = currentLine.width + currentLine.leftOffset;
|
|
554
|
-
const
|
|
590
|
+
const isFirstLine = lines.length === 0;
|
|
591
|
+
const contentX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + lineX;
|
|
592
|
+
let tabWidth = calculateTabWidth(contentX, {
|
|
555
593
|
...attrs?.tabs !== void 0 ? { explicitStops: attrs.tabs } : {},
|
|
556
594
|
leftIndent: pixelsToTwips(indentLeft)
|
|
557
595
|
}, {
|
|
558
596
|
followingWidth,
|
|
559
597
|
decimalPrefixWidth
|
|
560
598
|
}).width;
|
|
599
|
+
const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
|
|
600
|
+
if (!hasFollowingTabOnLine(runs, runIndex) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
|
|
561
601
|
if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
|
|
562
602
|
startNewLine(runIndex, 0);
|
|
563
603
|
updateMaxFont(style);
|
|
@@ -634,7 +674,8 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
634
674
|
const textRun = run;
|
|
635
675
|
const text = textRun.text;
|
|
636
676
|
const style = runToFontStyle(textRun);
|
|
637
|
-
|
|
677
|
+
const lineHeightStyle = cjkLineHeightStyle(textRun, style);
|
|
678
|
+
updateMaxFont(lineHeightStyle);
|
|
638
679
|
if (!text || text.length === 0) {
|
|
639
680
|
currentLine.toRun = runIndex;
|
|
640
681
|
currentLine.toChar = 0;
|
|
@@ -658,7 +699,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
658
699
|
if (bestEnd === 0) {
|
|
659
700
|
if (currentLine.width > 0) {
|
|
660
701
|
startNewLine(runIndex, charIndex + chunkStart);
|
|
661
|
-
updateMaxFont(
|
|
702
|
+
updateMaxFont(lineHeightStyle);
|
|
662
703
|
continue;
|
|
663
704
|
}
|
|
664
705
|
bestEnd = 1;
|
|
@@ -671,7 +712,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
671
712
|
chunkStart = chunkEnd;
|
|
672
713
|
if (chunkStart < word.length) {
|
|
673
714
|
startNewLine(runIndex, charIndex + chunkStart);
|
|
674
|
-
updateMaxFont(
|
|
715
|
+
updateMaxFont(lineHeightStyle);
|
|
675
716
|
}
|
|
676
717
|
}
|
|
677
718
|
charIndex = nextBreak;
|
|
@@ -681,7 +722,7 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
681
722
|
const glueWidth = rawGlueWidth > 0 && wordWidth + rawGlueWidth <= getPostWrapAvailableWidth() + WIDTH_TOLERANCE ? rawGlueWidth : 0;
|
|
682
723
|
if (currentLine.width > 0 && currentLine.width + wordWidth + glueWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
|
|
683
724
|
startNewLine(runIndex, charIndex);
|
|
684
|
-
updateMaxFont(
|
|
725
|
+
updateMaxFont(lineHeightStyle);
|
|
685
726
|
}
|
|
686
727
|
currentLine.width += wordWidth;
|
|
687
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 };
|
|
@@ -365,7 +365,8 @@ type ParagraphAttrs = {
|
|
|
365
365
|
*/
|
|
366
366
|
type ParagraphBlock = {
|
|
367
367
|
kind: "paragraph";
|
|
368
|
-
id: BlockId;
|
|
368
|
+
id: BlockId; /** Stable Word `w14:paraId` / PM `paraId`, when available. */
|
|
369
|
+
paraId?: string;
|
|
369
370
|
runs: Run[];
|
|
370
371
|
attrs?: ParagraphAttrs;
|
|
371
372
|
/** Names of bookmarks anchored to this paragraph; used to map a bookmark to
|
|
@@ -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 };
|