@stll/folio-core 0.1.3 → 0.3.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/clean-text.d.ts +21 -1
- package/dist/ai-edits/clean-text.js +69 -1
- package/dist/ai-edits/headless.d.ts +49 -22
- package/dist/ai-edits/headless.js +101 -77
- package/dist/ai-edits/index.d.ts +3 -1
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/read.d.ts +43 -0
- package/dist/ai-edits/read.js +91 -0
- package/dist/docx/index.d.ts +2 -0
- package/dist/docx/index.js +2 -0
- package/dist/docx/parser.js +1 -1
- package/dist/docx/wrapTypes.d.ts +8 -1
- package/dist/docx/wrapTypes.js +0 -8
- package/dist/i18n/messages/catalogs.gen.d.ts +9899 -0
- package/dist/i18n/messages/catalogs.gen.js +9831 -0
- package/dist/i18n/messages/messages.gen.d.ts +585 -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/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/measureParagraph.js +40 -5
- package/dist/layout-engine/pmPageIndex.d.ts +24 -0
- package/dist/layout-engine/pmPageIndex.js +37 -0
- 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 +1 -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/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/server.d.ts +2 -1
- 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/package.json +10 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/layout-bridge/cellSelectionHighlight.ts
|
|
2
|
+
/**
|
|
3
|
+
* Apply the `.layout-table-cell-selected` class to painted layout cells
|
|
4
|
+
* matching a CellSelection in the PM state. Clears the class everywhere
|
|
5
|
+
* (within scope) first so toggling off (or moving to a TextSelection) erases
|
|
6
|
+
* prior highlights.
|
|
7
|
+
*
|
|
8
|
+
* Duck-types CellSelection via `$anchorCell` / `forEachCell` rather than
|
|
9
|
+
* `instanceof` to dodge bundling issues across `prosemirror-tables` copies
|
|
10
|
+
* (the same trick used inline in the selection-overlay update).
|
|
11
|
+
*/
|
|
12
|
+
function applyCellSelectionHighlight(pagesContainer, state, options = {}) {
|
|
13
|
+
const scope = options.scope ?? "body";
|
|
14
|
+
const scopeClass = scope === "body" ? "layout-page-content" : `layout-page-${scope}`;
|
|
15
|
+
const scopeSelector = `.${scopeClass} .layout-table-cell`;
|
|
16
|
+
const prevSelected = pagesContainer.querySelectorAll(`.${scopeClass} .layout-table-cell-selected`);
|
|
17
|
+
for (const el of Array.from(prevSelected)) el.classList.remove("layout-table-cell-selected");
|
|
18
|
+
const sel = state.selection;
|
|
19
|
+
if (!("$anchorCell" in sel && typeof sel.forEachCell === "function") || !sel.forEachCell) return;
|
|
20
|
+
const selectedRanges = [];
|
|
21
|
+
sel.forEachCell((node, pos) => {
|
|
22
|
+
selectedRanges.push([pos, pos + node.nodeSize]);
|
|
23
|
+
});
|
|
24
|
+
const allCells = pagesContainer.querySelectorAll(scopeSelector);
|
|
25
|
+
for (const cellEl of Array.from(allCells)) {
|
|
26
|
+
const pmStartAttr = cellEl.dataset["pmStart"];
|
|
27
|
+
if (pmStartAttr === void 0) continue;
|
|
28
|
+
const pmPos = Number(pmStartAttr);
|
|
29
|
+
for (const [start, end] of selectedRanges) if (pmPos >= start && pmPos < end) {
|
|
30
|
+
cellEl.classList.add("layout-table-cell-selected");
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { applyCellSelectionHighlight };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EditorView } from "prosemirror-view";
|
|
2
|
+
|
|
3
|
+
//#region src/layout-bridge/headerFooterLayout.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Drop the cached HF host + span lists. Hosts/painters call this after
|
|
6
|
+
* a repaint (or HF mode toggle) so the next caret / selection compute
|
|
7
|
+
* re-walks the DOM. Public so adapters can call it from their painter
|
|
8
|
+
* commit signal.
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
declare function invalidateHfDomCache(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Viewport-relative caret rect for a persistent HF EditorView's selection
|
|
15
|
+
* head. Resolves against the painter's `data-pm-start`/`data-pm-end` spans
|
|
16
|
+
* inside `.layout-page-header` / `.layout-page-footer`. The same HF doc is
|
|
17
|
+
* painted on every page (multi-page docs, titlePg), so this walks every
|
|
18
|
+
* candidate host and picks the one whose spans bracket the PM head; falls
|
|
19
|
+
* back to the first so empty paragraphs still resolve to a paragraph anchor.
|
|
20
|
+
*
|
|
21
|
+
* Public so the React + Vue adapters can share a single implementation
|
|
22
|
+
* (`packages/{react,vue}` adapters used to carry byte-identical copies).
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
declare function computeHfCaretRectFromView(view: EditorView, section: "header" | "footer", doc?: globalThis.Document): {
|
|
27
|
+
top: number;
|
|
28
|
+
left: number;
|
|
29
|
+
height: number;
|
|
30
|
+
} | null;
|
|
31
|
+
/**
|
|
32
|
+
* Selection-rect set for a non-empty HF selection, projected against the
|
|
33
|
+
* painted HF spans. Mirror of `computeSelectionRectsFromDom` but scoped to
|
|
34
|
+
* `.layout-page-header` / `.layout-page-footer` instead of the body. Used
|
|
35
|
+
* so the painter draws a visible highlight when the user drag-selects
|
|
36
|
+
* inside a header/footer in edit mode.
|
|
37
|
+
*
|
|
38
|
+
* Returns viewport-relative `{top, left, width, height}` rects. Empty
|
|
39
|
+
* array when selection is collapsed or no painted spans overlap the range.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
declare function computeHfSelectionRectsFromView(view: EditorView, section: "header" | "footer", doc?: globalThis.Document): Array<{
|
|
44
|
+
top: number;
|
|
45
|
+
left: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
}>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { computeHfCaretRectFromView, computeHfSelectionRectsFromView, invalidateHfDomCache };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region src/layout-bridge/headerFooterLayout.ts
|
|
2
|
+
const hfDomCache = {
|
|
3
|
+
header: null,
|
|
4
|
+
footer: null
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Drop the cached HF host + span lists. Hosts/painters call this after
|
|
8
|
+
* a repaint (or HF mode toggle) so the next caret / selection compute
|
|
9
|
+
* re-walks the DOM. Public so adapters can call it from their painter
|
|
10
|
+
* commit signal.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
function invalidateHfDomCache() {
|
|
15
|
+
hfDomCache.header = null;
|
|
16
|
+
hfDomCache.footer = null;
|
|
17
|
+
}
|
|
18
|
+
function getHfDomSnapshot(section, doc) {
|
|
19
|
+
const hosts = doc.querySelectorAll(`.layout-page-${section}`);
|
|
20
|
+
if (hosts.length === 0) return null;
|
|
21
|
+
const win = doc.defaultView;
|
|
22
|
+
const vpCenter = win ? win.innerHeight / 2 : 0;
|
|
23
|
+
let host = hosts[0];
|
|
24
|
+
if (!host) return null;
|
|
25
|
+
let bestDist = Infinity;
|
|
26
|
+
for (const h of Array.from(hosts)) {
|
|
27
|
+
const r = h.getBoundingClientRect();
|
|
28
|
+
const dist = Math.abs((r.top + r.bottom) / 2 - vpCenter);
|
|
29
|
+
if (dist < bestDist) {
|
|
30
|
+
bestDist = dist;
|
|
31
|
+
host = h;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const cached = hfDomCache[section];
|
|
35
|
+
if (cached && cached.host === host && cached.host.isConnected) return cached;
|
|
36
|
+
const spans = Array.from(host.querySelectorAll("span[data-pm-start][data-pm-end]"));
|
|
37
|
+
const ranged = Array.from(host.querySelectorAll("[data-pm-start][data-pm-end]"));
|
|
38
|
+
const snapshot = {
|
|
39
|
+
host,
|
|
40
|
+
spans,
|
|
41
|
+
ranged
|
|
42
|
+
};
|
|
43
|
+
hfDomCache[section] = snapshot;
|
|
44
|
+
return snapshot;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Viewport-relative caret rect for a persistent HF EditorView's selection
|
|
48
|
+
* head. Resolves against the painter's `data-pm-start`/`data-pm-end` spans
|
|
49
|
+
* inside `.layout-page-header` / `.layout-page-footer`. The same HF doc is
|
|
50
|
+
* painted on every page (multi-page docs, titlePg), so this walks every
|
|
51
|
+
* candidate host and picks the one whose spans bracket the PM head; falls
|
|
52
|
+
* back to the first so empty paragraphs still resolve to a paragraph anchor.
|
|
53
|
+
*
|
|
54
|
+
* Public so the React + Vue adapters can share a single implementation
|
|
55
|
+
* (`packages/{react,vue}` adapters used to carry byte-identical copies).
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
function computeHfCaretRectFromView(view, section, doc = globalThis.document) {
|
|
60
|
+
const sel = view.state.selection;
|
|
61
|
+
if (!sel.empty) return null;
|
|
62
|
+
const pmPos = sel.head;
|
|
63
|
+
const snapshot = getHfDomSnapshot(section, doc);
|
|
64
|
+
if (!snapshot) return null;
|
|
65
|
+
const { host, spans } = snapshot;
|
|
66
|
+
for (const span of spans) {
|
|
67
|
+
const start = Number(span.dataset["pmStart"]);
|
|
68
|
+
const end = Number(span.dataset["pmEnd"]);
|
|
69
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
|
|
70
|
+
if (pmPos >= start && pmPos <= end) {
|
|
71
|
+
const range = host.ownerDocument.createRange();
|
|
72
|
+
const walker = host.ownerDocument.createTreeWalker(span, NodeFilter.SHOW_TEXT);
|
|
73
|
+
let remaining = pmPos - start;
|
|
74
|
+
let textNode = walker.nextNode();
|
|
75
|
+
while (textNode) {
|
|
76
|
+
const len = textNode.data.length;
|
|
77
|
+
if (remaining <= len) {
|
|
78
|
+
try {
|
|
79
|
+
range.setStart(textNode, remaining);
|
|
80
|
+
range.setEnd(textNode, remaining);
|
|
81
|
+
const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
|
|
82
|
+
if (rect && rect.height > 0) return {
|
|
83
|
+
top: rect.top,
|
|
84
|
+
left: rect.left,
|
|
85
|
+
height: rect.height
|
|
86
|
+
};
|
|
87
|
+
} catch {}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
remaining -= len;
|
|
91
|
+
textNode = walker.nextNode();
|
|
92
|
+
}
|
|
93
|
+
const spanRect = span.getBoundingClientRect();
|
|
94
|
+
const ratio = (pmPos - start) / Math.max(1, end - start);
|
|
95
|
+
return {
|
|
96
|
+
top: spanRect.top,
|
|
97
|
+
left: spanRect.left + spanRect.width * ratio,
|
|
98
|
+
height: spanRect.height
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const anchor = host.querySelector(`[data-pm-start="${pmPos}"]`);
|
|
103
|
+
if (anchor) {
|
|
104
|
+
const rect = anchor.getBoundingClientRect();
|
|
105
|
+
return {
|
|
106
|
+
top: rect.top,
|
|
107
|
+
left: rect.left + 1,
|
|
108
|
+
height: rect.height || 16
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const ranged = snapshot.ranged;
|
|
112
|
+
let bestEl = null;
|
|
113
|
+
let bestSpan = Infinity;
|
|
114
|
+
for (const el of ranged) {
|
|
115
|
+
const start = Number(el.dataset["pmStart"]);
|
|
116
|
+
const end = Number(el.dataset["pmEnd"]);
|
|
117
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) continue;
|
|
118
|
+
if (pmPos < start || pmPos > end) continue;
|
|
119
|
+
const span = end - start;
|
|
120
|
+
if (span < bestSpan) {
|
|
121
|
+
bestSpan = span;
|
|
122
|
+
bestEl = el;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (bestEl) {
|
|
126
|
+
const rect = bestEl.getBoundingClientRect();
|
|
127
|
+
const atEnd = pmPos >= Number(bestEl.dataset["pmEnd"]);
|
|
128
|
+
return {
|
|
129
|
+
top: rect.top,
|
|
130
|
+
left: atEnd ? rect.right : rect.left + 1,
|
|
131
|
+
height: rect.height || 16
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
let trailingEl = null;
|
|
135
|
+
let trailingStart = -Infinity;
|
|
136
|
+
for (const el of ranged) {
|
|
137
|
+
const start = Number(el.dataset["pmStart"]);
|
|
138
|
+
if (!Number.isFinite(start)) continue;
|
|
139
|
+
if (start > pmPos) continue;
|
|
140
|
+
if (start > trailingStart) {
|
|
141
|
+
trailingStart = start;
|
|
142
|
+
trailingEl = el;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (trailingEl) {
|
|
146
|
+
const rect = trailingEl.getBoundingClientRect();
|
|
147
|
+
return {
|
|
148
|
+
top: rect.top,
|
|
149
|
+
left: rect.right,
|
|
150
|
+
height: rect.height || 16
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const hostRect = host.getBoundingClientRect();
|
|
154
|
+
return {
|
|
155
|
+
top: hostRect.top + 2,
|
|
156
|
+
left: hostRect.left + 2,
|
|
157
|
+
height: 16
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Selection-rect set for a non-empty HF selection, projected against the
|
|
162
|
+
* painted HF spans. Mirror of `computeSelectionRectsFromDom` but scoped to
|
|
163
|
+
* `.layout-page-header` / `.layout-page-footer` instead of the body. Used
|
|
164
|
+
* so the painter draws a visible highlight when the user drag-selects
|
|
165
|
+
* inside a header/footer in edit mode.
|
|
166
|
+
*
|
|
167
|
+
* Returns viewport-relative `{top, left, width, height}` rects. Empty
|
|
168
|
+
* array when selection is collapsed or no painted spans overlap the range.
|
|
169
|
+
*
|
|
170
|
+
* @public
|
|
171
|
+
*/
|
|
172
|
+
function computeHfSelectionRectsFromView(view, section, doc = globalThis.document) {
|
|
173
|
+
const sel = view.state.selection;
|
|
174
|
+
if (sel.empty) return [];
|
|
175
|
+
const from = sel.from;
|
|
176
|
+
const to = sel.to;
|
|
177
|
+
const out = [];
|
|
178
|
+
const snapshot = getHfDomSnapshot(section, doc);
|
|
179
|
+
if (!snapshot) return out;
|
|
180
|
+
const { host, spans } = snapshot;
|
|
181
|
+
for (const spanEl of spans) {
|
|
182
|
+
const pmStart = Number(spanEl.dataset["pmStart"]);
|
|
183
|
+
const pmEnd = Number(spanEl.dataset["pmEnd"]);
|
|
184
|
+
if (!Number.isFinite(pmStart) || !Number.isFinite(pmEnd)) continue;
|
|
185
|
+
if (pmEnd <= from || pmStart >= to) continue;
|
|
186
|
+
if (spanEl.classList.contains("layout-run-tab")) {
|
|
187
|
+
const rect = spanEl.getBoundingClientRect();
|
|
188
|
+
out.push({
|
|
189
|
+
top: rect.top,
|
|
190
|
+
left: rect.left,
|
|
191
|
+
width: rect.width,
|
|
192
|
+
height: rect.height
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
let textNode = null;
|
|
197
|
+
if (spanEl.firstChild?.nodeType === Node.TEXT_NODE) textNode = spanEl.firstChild;
|
|
198
|
+
else if (spanEl.firstChild?.nodeType === Node.ELEMENT_NODE && spanEl.firstChild.tagName === "A" && spanEl.firstChild.firstChild?.nodeType === Node.TEXT_NODE) textNode = spanEl.firstChild.firstChild;
|
|
199
|
+
if (!textNode) continue;
|
|
200
|
+
const startChar = Math.max(0, from - pmStart);
|
|
201
|
+
const endChar = Math.min(textNode.length, to - pmStart);
|
|
202
|
+
if (startChar >= endChar) continue;
|
|
203
|
+
const range = host.ownerDocument.createRange();
|
|
204
|
+
range.setStart(textNode, startChar);
|
|
205
|
+
range.setEnd(textNode, endChar);
|
|
206
|
+
for (const rect of Array.from(range.getClientRects())) out.push({
|
|
207
|
+
top: rect.top,
|
|
208
|
+
left: rect.left,
|
|
209
|
+
width: rect.width,
|
|
210
|
+
height: rect.height
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { computeHfCaretRectFromView, computeHfSelectionRectsFromView, invalidateHfDomCache };
|
|
@@ -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 };
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 };
|