@stll/folio-core 0.4.0 → 0.5.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/apply.d.ts +3 -9
- package/dist/ai-edits/apply.js +94 -14
- package/dist/ai-edits/blockRange.d.ts +33 -0
- package/dist/ai-edits/blockRange.js +58 -0
- package/dist/ai-edits/headless.d.ts +33 -4
- package/dist/ai-edits/headless.js +83 -15
- package/dist/ai-edits/index.d.ts +5 -3
- package/dist/ai-edits/index.js +3 -2
- package/dist/ai-edits/snapshot.d.ts +14 -2
- package/dist/ai-edits/snapshot.js +12 -1
- package/dist/ai-edits/types.d.ts +42 -3
- package/dist/compat/eigenpal.d.ts +3 -2
- package/dist/compat/eigenpal.js +6 -5
- package/dist/controller/layoutPipeline.d.ts +1 -0
- package/dist/controller/layoutPipeline.js +35 -137
- package/dist/controller/layoutScheduler.d.ts +2 -1
- package/dist/controller/layoutScheduler.js +6 -0
- package/dist/document-operations.d.ts +122 -0
- package/dist/document-operations.js +575 -0
- package/dist/docx/blockContentParser.js +17 -7
- package/dist/docx/encryption/agileDecryption.js +2 -1
- package/dist/docx/encryption/compoundFile.js +4 -2
- package/dist/docx/footnoteParser.d.ts +1 -17
- package/dist/docx/footnoteParser.js +1 -27
- package/dist/docx/groupDrawingParser.d.ts +8 -0
- package/dist/docx/groupDrawingParser.js +131 -0
- package/dist/docx/numberingParser.d.ts +3 -1
- package/dist/docx/numberingParser.js +8 -2
- package/dist/docx/paragraphParser.js +9 -3
- package/dist/docx/runParser.js +8 -1
- package/dist/docx/selectiveXmlPatch.js +4 -10
- package/dist/docx/serializer/numberingSerializer.js +3 -1
- package/dist/docx/settingsParser.d.ts +6 -3
- package/dist/docx/settingsParser.js +2 -0
- package/dist/docx/xmlParser.js +11 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -5
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
- package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
- package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
- package/dist/layout-engine/index.d.ts +6 -5
- package/dist/layout-engine/index.js +89 -26
- package/dist/layout-engine/keep-together.d.ts +13 -8
- package/dist/layout-engine/keep-together.js +40 -25
- package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
- package/dist/layout-engine/layoutInstrumentation.js +10 -1
- package/dist/layout-engine/measure/measureBlocks.js +28 -18
- package/dist/layout-engine/measure/measureParagraph.js +61 -15
- package/dist/layout-engine/paginator.d.ts +2 -1
- package/dist/layout-engine/paginator.js +9 -3
- package/dist/layout-engine/textBoxGroup.d.ts +7 -0
- package/dist/layout-engine/textBoxGroup.js +8 -0
- package/dist/layout-engine/types.d.ts +30 -9
- package/dist/layout-engine/types.js +20 -2
- package/dist/layout-painter/renderPage.d.ts +15 -1
- package/dist/layout-painter/renderPage.js +27 -11
- package/dist/layout-painter/renderParagraph.js +18 -8
- package/dist/layout-painter/renderTable.js +10 -7
- package/dist/managers/DocumentLoaderManager.js +12 -5
- package/dist/managers/TableSelectionManager.js +1 -1
- package/dist/model.js +1 -1
- package/dist/paged-layout/pageText.d.ts +14 -0
- package/dist/paged-layout/pageText.js +21 -0
- package/dist/prosemirror/attrs/index.js +2 -0
- package/dist/prosemirror/commands/comments.js +75 -5
- package/dist/prosemirror/commands/hyperlink.js +4 -2
- package/dist/prosemirror/commands/image.js +2 -1
- package/dist/prosemirror/commands/index.js +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
- package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
- package/dist/prosemirror/conversion/toProseDoc.js +29 -8
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
- package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
- package/dist/prosemirror/findReplaceSelection.js +2 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/insertOperations.js +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +10 -5
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/utils/tabCalculator.js +4 -2
- package/dist/redline.d.ts +20 -0
- package/dist/redline.js +127 -0
- package/dist/server.d.ts +7 -3
- package/dist/server.js +6 -2
- package/dist/symbols.d.ts +35 -0
- package/dist/symbols.js +512 -0
- package/dist/utils/clipboard.js +3 -3
- package/dist/utils/fontLoader.js +3 -0
- package/dist/utils/fontResolver.js +48 -1
- package/dist/utils/zoom.d.ts +21 -0
- package/dist/utils/zoom.js +21 -0
- package/dist/version-comparison.d.ts +92 -0
- package/dist/version-comparison.js +436 -0
- package/package.json +3 -3
- package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
- package/dist/paged-layout/headerFooterMargins.js +0 -122
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { emuToPixels } from "../utils/units.js";
|
|
1
2
|
import { measuredLineAdvance } from "./lineFlow.js";
|
|
2
|
-
import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js";
|
|
3
|
+
import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
|
|
3
4
|
import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
|
|
4
5
|
import { createPaginator } from "./paginator.js";
|
|
5
6
|
import { getParagraphFragmentPmRange } from "./paragraphFragmentRange.js";
|
|
@@ -51,25 +52,42 @@ function isEmptyParagraph(block) {
|
|
|
51
52
|
if (block.runs.length === 0) return true;
|
|
52
53
|
if (block.runs.length !== 1) return false;
|
|
53
54
|
const r = block.runs[0];
|
|
54
|
-
return r?.kind === "text" &&
|
|
55
|
+
return r?.kind === "text" && r.text === "";
|
|
56
|
+
}
|
|
57
|
+
function isPaginationEmptyParagraph(block) {
|
|
58
|
+
return block.runs.every((run) => {
|
|
59
|
+
if (run.kind === "text") return run.text.trim().length === 0;
|
|
60
|
+
return run.kind === "tab" || run.kind === "lineBreak";
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function pageHasVisibleBodyContent(page, blocksById) {
|
|
64
|
+
for (const fragment of page.fragments) {
|
|
65
|
+
if (fragment.kind !== "paragraph") return true;
|
|
66
|
+
const block = blocksById.get(String(fragment.blockId));
|
|
67
|
+
if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
55
70
|
}
|
|
56
71
|
/**
|
|
57
72
|
* Get spacing before a paragraph block. Empty paragraphs whose
|
|
58
|
-
* `before`
|
|
59
|
-
*
|
|
73
|
+
* `before` came only from the implicit default paragraph style collapse to
|
|
74
|
+
* zero. An explicit `w:pStyle` selection is authored paragraph formatting,
|
|
75
|
+
* so Word keeps the selected style's spacing even when the paragraph is empty.
|
|
60
76
|
*/
|
|
61
77
|
function getSpacingBefore(block) {
|
|
62
78
|
const value = block.attrs?.spacing?.before ?? 0;
|
|
63
|
-
if (
|
|
79
|
+
if (value === 0) return 0;
|
|
80
|
+
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.before) return 0;
|
|
64
81
|
return value;
|
|
65
82
|
}
|
|
66
83
|
/**
|
|
67
|
-
* Get spacing after a paragraph block. Same
|
|
68
|
-
* as `getSpacingBefore`.
|
|
84
|
+
* Get spacing after a paragraph block. Same implicit-default-style collapse
|
|
85
|
+
* rule as `getSpacingBefore`.
|
|
69
86
|
*/
|
|
70
87
|
function getSpacingAfter(block) {
|
|
71
88
|
const value = block.attrs?.spacing?.after ?? 0;
|
|
72
|
-
if (
|
|
89
|
+
if (value === 0) return 0;
|
|
90
|
+
if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.after) return 0;
|
|
73
91
|
return value;
|
|
74
92
|
}
|
|
75
93
|
function hasWidowControl(block) {
|
|
@@ -78,9 +96,10 @@ function hasWidowControl(block) {
|
|
|
78
96
|
/**
|
|
79
97
|
* Apply contextual spacing suppression (OOXML §17.3.1.9).
|
|
80
98
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
99
|
+
* Contextual spacing applies independently to each paragraph: suppress the
|
|
100
|
+
* current paragraph's spaceAfter when it opts in, and the next paragraph's
|
|
101
|
+
* spaceBefore when it opts in, provided both paragraphs share a style. Two
|
|
102
|
+
* absent style ids both refer to the document's default paragraph style.
|
|
84
103
|
*
|
|
85
104
|
* This mutates the block attrs in-place before layout runs.
|
|
86
105
|
*/
|
|
@@ -91,16 +110,15 @@ function applyContextualSpacing(blocks) {
|
|
|
91
110
|
if (curr.kind !== "paragraph" || next.kind !== "paragraph") continue;
|
|
92
111
|
const currAttrs = curr.attrs;
|
|
93
112
|
const nextAttrs = next.attrs;
|
|
94
|
-
if (currAttrs?.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
113
|
+
if (currAttrs?.styleId !== nextAttrs?.styleId) continue;
|
|
114
|
+
if (currAttrs?.contextualSpacing && currAttrs.spacing) currAttrs.spacing = {
|
|
115
|
+
...currAttrs.spacing,
|
|
116
|
+
after: 0
|
|
117
|
+
};
|
|
118
|
+
if (nextAttrs?.contextualSpacing && nextAttrs.spacing) nextAttrs.spacing = {
|
|
119
|
+
...nextAttrs.spacing,
|
|
120
|
+
before: 0
|
|
121
|
+
};
|
|
104
122
|
}
|
|
105
123
|
for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyContextualSpacing(cell.blocks);
|
|
106
124
|
else if (block.kind === "textBox") applyContextualSpacing(block.content);
|
|
@@ -145,6 +163,7 @@ function layoutDocument(blocks, measures, options) {
|
|
|
145
163
|
const paginator = createPaginator({
|
|
146
164
|
pageSize: initialConfig.pageSize,
|
|
147
165
|
margins: initialConfig.margins,
|
|
166
|
+
...options.mirrorMargins === true ? { mirrorMargins: true } : {},
|
|
148
167
|
...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {},
|
|
149
168
|
columns: initialConfig.columns ?? DEFAULT_COLUMNS,
|
|
150
169
|
...options.footnoteReservedHeights !== void 0 ? { footnoteReservedHeights: options.footnoteReservedHeights } : {},
|
|
@@ -153,21 +172,29 @@ function layoutDocument(blocks, measures, options) {
|
|
|
153
172
|
applyContextualSpacing(blocks);
|
|
154
173
|
const keepNextChains = computeKeepNextChains(blocks);
|
|
155
174
|
const midChainIndices = getMidChainIndices(keepNextChains);
|
|
175
|
+
const blocksById = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const block of blocks) blocksById.set(String(block.id), block);
|
|
156
177
|
let sectionIdx = 0;
|
|
157
178
|
let activeSectionMarginTop = initialConfig.margins.top;
|
|
158
179
|
let activeSectionPageHeight = initialConfig.pageSize.h;
|
|
159
180
|
let activeSectionMarginBottom = initialConfig.margins.bottom;
|
|
181
|
+
let naturalPageAdvanceSinceRenderedBreak = false;
|
|
160
182
|
for (let i = 0; i < blocks.length; i++) {
|
|
161
183
|
const block = blocks[i];
|
|
162
184
|
const measure = measures[i];
|
|
185
|
+
const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true;
|
|
163
186
|
if (hasPageBreakBefore(block)) paginator.forcePageBreak();
|
|
187
|
+
else if (hasRenderedPageBreak) {
|
|
188
|
+
const state = paginator.getCurrentState();
|
|
189
|
+
if (!(block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak();
|
|
190
|
+
}
|
|
191
|
+
if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = false;
|
|
164
192
|
const chain = keepNextChains.get(i);
|
|
165
193
|
if (chain && !midChainIndices.has(i)) {
|
|
166
194
|
const chainHeight = calculateChainHeight(chain, blocks, measures);
|
|
167
|
-
|
|
168
|
-
const availableHeight = paginator.getAvailableHeight();
|
|
169
|
-
if (chainHeight <= state.contentBottom - state.topMargin && chainHeight > availableHeight && state.page.fragments.length > 0) paginator.forcePageBreak();
|
|
195
|
+
paginator.ensureFits(chainHeight);
|
|
170
196
|
}
|
|
197
|
+
const pageBeforeBlockLayout = paginator.getCurrentState().page.number;
|
|
171
198
|
switch (block.kind) {
|
|
172
199
|
case "paragraph":
|
|
173
200
|
layoutParagraph(block, measure, paginator, paginator.getContentWidth(), options.footnoteHeightById);
|
|
@@ -204,6 +231,9 @@ function layoutDocument(blocks, measures, options) {
|
|
|
204
231
|
}
|
|
205
232
|
default: break;
|
|
206
233
|
}
|
|
234
|
+
const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak";
|
|
235
|
+
if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = false;
|
|
236
|
+
else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = true;
|
|
207
237
|
}
|
|
208
238
|
if (paginator.pages.length === 0) paginator.getCurrentState();
|
|
209
239
|
return {
|
|
@@ -397,7 +427,10 @@ function layoutTable(block, measure, paginator) {
|
|
|
397
427
|
let x = paginator.getColumnX(columnIndex);
|
|
398
428
|
if (block.justification === "center") x += (paginator.columnWidth - measure.totalWidth) / 2;
|
|
399
429
|
else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth;
|
|
400
|
-
else if (block.indent
|
|
430
|
+
else if (block.indent !== void 0) {
|
|
431
|
+
const leadingCellMargin = block.rows[0]?.cells[0]?.padding?.left ?? 0;
|
|
432
|
+
x += block.indent - leadingCellMargin;
|
|
433
|
+
}
|
|
401
434
|
return x;
|
|
402
435
|
};
|
|
403
436
|
const getCurrentRowCapacity = (state = paginator.getCurrentState()) => state.rawContentBottom - state.topMargin;
|
|
@@ -531,6 +564,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
|
|
|
531
564
|
let baseY = margins.top;
|
|
532
565
|
if (floating?.horzAnchor === "page") baseX = 0;
|
|
533
566
|
if (floating?.vertAnchor === "page") baseY = 0;
|
|
567
|
+
else if (floating?.vertAnchor === "text") baseY = state.cursorY;
|
|
534
568
|
let x = paginator.getColumnX(state.columnIndex);
|
|
535
569
|
if (floating?.tblpX !== void 0) x = baseX + floating.tblpX;
|
|
536
570
|
else if (floating?.tblpXSpec) {
|
|
@@ -649,6 +683,35 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
|
|
|
649
683
|
state.page.fragments.push(fragment);
|
|
650
684
|
return;
|
|
651
685
|
}
|
|
686
|
+
if (block.position !== void 0) {
|
|
687
|
+
const state = paginator.getCurrentState();
|
|
688
|
+
const horizontal = block.position.horizontal;
|
|
689
|
+
const x = horizontal ? bandFragmentX(horizontal, {
|
|
690
|
+
pageWidth: state.page.size.w,
|
|
691
|
+
marginLeft: state.page.margins.left,
|
|
692
|
+
marginRight: state.page.margins.right,
|
|
693
|
+
boxWidth: measure.width
|
|
694
|
+
}) : paginator.getColumnX(state.columnIndex);
|
|
695
|
+
const vertical = block.position.vertical;
|
|
696
|
+
const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, {
|
|
697
|
+
pageHeight: sectionPageHeight,
|
|
698
|
+
marginTop: sectionMarginTop,
|
|
699
|
+
marginBottom: sectionMarginBottom,
|
|
700
|
+
boxHeight: measure.height
|
|
701
|
+
}) : state.cursorY + emuToPixels(vertical?.posOffset ?? 0);
|
|
702
|
+
const fragment = {
|
|
703
|
+
kind: "textBox",
|
|
704
|
+
blockId: block.id,
|
|
705
|
+
x,
|
|
706
|
+
y,
|
|
707
|
+
width: measure.width,
|
|
708
|
+
height: measure.height,
|
|
709
|
+
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
710
|
+
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
|
|
711
|
+
};
|
|
712
|
+
state.page.fragments.push(fragment);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
652
715
|
const state = paginator.ensureFits(measure.height);
|
|
653
716
|
const fragment = {
|
|
654
717
|
kind: "textBox",
|
|
@@ -715,4 +778,4 @@ function handleSectionBreak(_block, paginator, nextSectionConfig, nextSectionTyp
|
|
|
715
778
|
paginator.updateColumns(nextSectionConfig.columns ?? DEFAULT_COLUMNS);
|
|
716
779
|
}
|
|
717
780
|
//#endregion
|
|
718
|
-
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 };
|
|
781
|
+
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, tableColumnsArePinned };
|
|
@@ -2,28 +2,33 @@ import { FlowBlock, Measure } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
//#region src/layout-engine/keep-together.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* A chain of
|
|
5
|
+
* A chain of keepNext-linked paragraphs, including empty separators Word
|
|
6
|
+
* carries through to the next paragraph with visible content.
|
|
6
7
|
*/
|
|
7
8
|
type KeepNextChain = {
|
|
8
|
-
/** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last
|
|
9
|
-
endIndex: number; /** All paragraph indices in the chain. */
|
|
9
|
+
/** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last keepNext or pass-through empty member. */
|
|
10
|
+
endIndex: number; /** All keepNext and pass-through empty paragraph indices in the chain. */
|
|
10
11
|
memberIndices: number[]; /** Index of the anchor paragraph (first non-keepNext after chain), or -1 if none. */
|
|
11
12
|
anchorIndex: number;
|
|
12
13
|
};
|
|
13
14
|
/**
|
|
14
15
|
* Pre-scan blocks to find all keepNext chains.
|
|
15
16
|
*
|
|
16
|
-
* A keepNext chain
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* A keepNext chain starts with a paragraph whose keepNext=true and continues
|
|
18
|
+
* through further keepNext paragraphs and structural empty separators. The
|
|
19
|
+
* first visible non-keepNext paragraph is its anchor.
|
|
19
20
|
*
|
|
20
21
|
* Returns a map from chain start index to chain info.
|
|
21
22
|
*/
|
|
22
23
|
declare function computeKeepNextChains(blocks: FlowBlock[]): Map<number, KeepNextChain>;
|
|
23
24
|
/**
|
|
24
|
-
* Calculate the
|
|
25
|
+
* Calculate the height needed to keep consecutive paragraph boundaries from
|
|
26
|
+
* breaking across pages.
|
|
25
27
|
*
|
|
26
|
-
*
|
|
28
|
+
* Single-line and `keepLines` members are indivisible, so the reservation must
|
|
29
|
+
* continue through them to the anchor's first line. A multi-line member without
|
|
30
|
+
* `keepLines` may itself split; only its first line is needed to satisfy the
|
|
31
|
+
* preceding member's `keepNext`, and the chain can stop there.
|
|
27
32
|
*/
|
|
28
33
|
declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[]): number;
|
|
29
34
|
/**
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
//#region src/layout-engine/keep-together.ts
|
|
2
|
+
function isVisuallyEmptyParagraph(block) {
|
|
3
|
+
if (block.runs.length === 0) return true;
|
|
4
|
+
if (block.runs.length !== 1) return false;
|
|
5
|
+
const run = block.runs.at(0);
|
|
6
|
+
return run?.kind === "text" && run.text === "";
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* Pre-scan blocks to find all keepNext chains.
|
|
4
10
|
*
|
|
5
|
-
* A keepNext chain
|
|
6
|
-
*
|
|
7
|
-
*
|
|
11
|
+
* A keepNext chain starts with a paragraph whose keepNext=true and continues
|
|
12
|
+
* through further keepNext paragraphs and structural empty separators. The
|
|
13
|
+
* first visible non-keepNext paragraph is its anchor.
|
|
8
14
|
*
|
|
9
15
|
* Returns a map from chain start index to chain info.
|
|
10
16
|
*/
|
|
@@ -22,7 +28,8 @@ function computeKeepNextChains(blocks) {
|
|
|
22
28
|
const nextBlock = blocks[j];
|
|
23
29
|
if (nextBlock.kind === "sectionBreak" || nextBlock.kind === "pageBreak" || nextBlock.kind === "columnBreak") break;
|
|
24
30
|
if (nextBlock.kind !== "paragraph") break;
|
|
25
|
-
|
|
31
|
+
const nextPara = nextBlock;
|
|
32
|
+
if (nextPara.attrs?.keepNext || isVisuallyEmptyParagraph(nextPara)) {
|
|
26
33
|
memberIndices.push(j);
|
|
27
34
|
endIndex = j;
|
|
28
35
|
processed.add(j);
|
|
@@ -44,30 +51,38 @@ function computeKeepNextChains(blocks) {
|
|
|
44
51
|
return chains;
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
|
-
* Calculate the
|
|
54
|
+
* Calculate the height needed to keep consecutive paragraph boundaries from
|
|
55
|
+
* breaking across pages.
|
|
48
56
|
*
|
|
49
|
-
*
|
|
57
|
+
* Single-line and `keepLines` members are indivisible, so the reservation must
|
|
58
|
+
* continue through them to the anchor's first line. A multi-line member without
|
|
59
|
+
* `keepLines` may itself split; only its first line is needed to satisfy the
|
|
60
|
+
* preceding member's `keepNext`, and the chain can stop there.
|
|
50
61
|
*/
|
|
51
62
|
function calculateChainHeight(chain, blocks, measures) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const firstMemberIndex = chain.memberIndices.at(0);
|
|
64
|
+
if (firstMemberIndex === void 0) return 0;
|
|
65
|
+
const firstBlock = blocks[firstMemberIndex];
|
|
66
|
+
const firstMeasure = measures[firstMemberIndex];
|
|
67
|
+
if (firstBlock?.kind !== "paragraph" || firstMeasure?.kind !== "paragraph") return 0;
|
|
68
|
+
let totalHeight = (firstBlock.attrs?.spacing?.before ?? 0) + firstMeasure.totalHeight;
|
|
69
|
+
let trailingSpacing = firstBlock.attrs?.spacing?.after ?? 0;
|
|
70
|
+
const successorIndices = [...chain.memberIndices.slice(1)];
|
|
71
|
+
if (chain.anchorIndex !== -1) successorIndices.push(chain.anchorIndex);
|
|
72
|
+
for (let index = 0; index < successorIndices.length; index++) {
|
|
73
|
+
const successorIndex = successorIndices[index];
|
|
74
|
+
if (successorIndex === void 0) continue;
|
|
75
|
+
const successorBlock = blocks[successorIndex];
|
|
76
|
+
const successorMeasure = measures[successorIndex];
|
|
77
|
+
if (successorBlock?.kind !== "paragraph" || successorMeasure?.kind !== "paragraph") return totalHeight;
|
|
78
|
+
totalHeight += Math.max(trailingSpacing, successorBlock.attrs?.spacing?.before ?? 0);
|
|
79
|
+
const firstLine = successorMeasure.lines.at(0);
|
|
80
|
+
if (!firstLine) return totalHeight;
|
|
81
|
+
const isAnchor = index === successorIndices.length - 1 && chain.anchorIndex !== -1;
|
|
82
|
+
const isSplittable = successorMeasure.lines.length > 1 && !successorBlock.attrs?.keepLines;
|
|
83
|
+
if (isAnchor || isSplittable) return totalHeight + firstLine.lineHeight;
|
|
84
|
+
totalHeight += successorMeasure.totalHeight;
|
|
85
|
+
trailingSpacing = successorBlock.attrs?.spacing?.after ?? 0;
|
|
71
86
|
}
|
|
72
87
|
return totalHeight;
|
|
73
88
|
}
|
|
@@ -5,7 +5,12 @@ type LayoutRunReason = "font-ready" | "initial" | "layout-input" | "manual" | "t
|
|
|
5
5
|
type LayoutPhase = "flow-blocks" | "header-footer" | "initial-fonts" | "layout-document" | "measure-blocks" | "render-pages";
|
|
6
6
|
type HiddenEditorStateReason = "external-document" | "mount";
|
|
7
7
|
type HiddenEditorPhase = "editor-state" | "editor-view" | "to-prose-doc" | "update-state";
|
|
8
|
+
type DocumentLoadPhase = "docx-parse";
|
|
8
9
|
type LayoutInstrumentation = {
|
|
10
|
+
onDocumentLoadPhase?: (event: {
|
|
11
|
+
durationMs: number;
|
|
12
|
+
phase: DocumentLoadPhase;
|
|
13
|
+
}) => void;
|
|
9
14
|
onHiddenEditorPhase?: (event: {
|
|
10
15
|
durationMs: number;
|
|
11
16
|
phase: HiddenEditorPhase;
|
|
@@ -21,6 +26,9 @@ type LayoutInstrumentation = {
|
|
|
21
26
|
message: string;
|
|
22
27
|
reason: LayoutRunReason;
|
|
23
28
|
}) => void;
|
|
29
|
+
onLayoutStart?: (event: {
|
|
30
|
+
reason: LayoutRunReason;
|
|
31
|
+
}) => void;
|
|
24
32
|
onLayoutPhase?: (event: {
|
|
25
33
|
durationMs: number;
|
|
26
34
|
phase: LayoutPhase;
|
|
@@ -42,9 +50,11 @@ declare global {
|
|
|
42
50
|
declare function recordMeasureBlock(blockIndex: number, block: FlowBlock): void;
|
|
43
51
|
declare function recordMeasureBlockError(blockIndex: number, block: FlowBlock, error: unknown): void;
|
|
44
52
|
declare function recordLayoutComplete(reason?: LayoutRunReason): void;
|
|
53
|
+
declare function recordLayoutStart(reason: LayoutRunReason): void;
|
|
45
54
|
declare function recordLayoutError(reason: LayoutRunReason, error: unknown): void;
|
|
46
55
|
declare function recordLayoutPhase(reason: LayoutRunReason, phase: LayoutPhase, durationMs: number): void;
|
|
47
56
|
declare function recordHiddenEditorStateCreate(reason: HiddenEditorStateReason): void;
|
|
48
57
|
declare function recordHiddenEditorPhase(reason: HiddenEditorStateReason, phase: HiddenEditorPhase, durationMs: number): void;
|
|
58
|
+
declare function recordDocumentLoadPhase(phase: DocumentLoadPhase, durationMs: number): void;
|
|
49
59
|
//#endregion
|
|
50
|
-
export { HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError };
|
|
60
|
+
export { DocumentLoadPhase, HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
|
|
@@ -15,6 +15,9 @@ function recordMeasureBlockError(blockIndex, block, error) {
|
|
|
15
15
|
function recordLayoutComplete(reason = "manual") {
|
|
16
16
|
globalThis.__folioLayoutInstrumentation?.onLayoutComplete?.({ reason });
|
|
17
17
|
}
|
|
18
|
+
function recordLayoutStart(reason) {
|
|
19
|
+
globalThis.__folioLayoutInstrumentation?.onLayoutStart?.({ reason });
|
|
20
|
+
}
|
|
18
21
|
function recordLayoutError(reason, error) {
|
|
19
22
|
const message = error instanceof Error ? error.message : String(error);
|
|
20
23
|
globalThis.__folioLayoutInstrumentation?.onLayoutError?.({
|
|
@@ -39,5 +42,11 @@ function recordHiddenEditorPhase(reason, phase, durationMs) {
|
|
|
39
42
|
reason
|
|
40
43
|
});
|
|
41
44
|
}
|
|
45
|
+
function recordDocumentLoadPhase(phase, durationMs) {
|
|
46
|
+
globalThis.__folioLayoutInstrumentation?.onDocumentLoadPhase?.({
|
|
47
|
+
durationMs,
|
|
48
|
+
phase
|
|
49
|
+
});
|
|
50
|
+
}
|
|
42
51
|
//#endregion
|
|
43
|
-
export { recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError };
|
|
52
|
+
export { recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand } from "../types.js";
|
|
1
|
+
import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, tableColumnsArePinned } from "../types.js";
|
|
2
2
|
import { findClearLineY, measureParagraph } from "./measureParagraph.js";
|
|
3
3
|
import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js";
|
|
4
|
+
import { getTextBoxGroupId } from "../textBoxGroup.js";
|
|
4
5
|
import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js";
|
|
5
6
|
import { bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js";
|
|
6
7
|
//#region src/layout-engine/measure/measureBlocks.ts
|
|
@@ -80,8 +81,8 @@ function measureTableCellBlockVisualHeight(block, blockMeasure) {
|
|
|
80
81
|
const spacingAfter = paragraphBlock.attrs?.spacing?.after ?? 0;
|
|
81
82
|
return spacingBefore + maxImageHeight + spacingAfter;
|
|
82
83
|
}
|
|
83
|
-
function getTableCellVerticalBorderHeight(cell) {
|
|
84
|
-
return (cell?.borders?.top?.width ?? 0) + (cell?.borders?.bottom?.width ?? 0);
|
|
84
|
+
function getTableCellVerticalBorderHeight(cell, isFirstRow) {
|
|
85
|
+
return (isFirstRow ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0);
|
|
85
86
|
}
|
|
86
87
|
function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
87
88
|
const DEFAULT_CELL_PADDING_X = 7;
|
|
@@ -118,6 +119,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
118
119
|
while (occupied.has(colIdx)) colIdx++;
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
const columnsPinned = tableColumnsArePinned(tableBlock);
|
|
121
123
|
const rows = tableBlock.rows.map((row, rowIdx) => {
|
|
122
124
|
let columnIndex = 0;
|
|
123
125
|
const occupied = occupiedColumnsPerRow.get(rowIdx) ?? /* @__PURE__ */ new Set();
|
|
@@ -133,7 +135,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
133
135
|
const padLeft = cell.padding?.left ?? DEFAULT_CELL_PADDING_X;
|
|
134
136
|
const padRight = cell.padding?.right ?? DEFAULT_CELL_PADDING_X;
|
|
135
137
|
const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight);
|
|
136
|
-
const measureWidth = cell.noWrap ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
|
|
138
|
+
const measureWidth = cell.noWrap === true && !columnsPinned ? NO_WRAP_MEASURE_WIDTH : cellContentWidth;
|
|
137
139
|
const cellMeasure = {
|
|
138
140
|
blocks: cell.blocks.map((b) => measureBlock(b, measureWidth, void 0, void 0, fieldValues)),
|
|
139
141
|
width: cellWidth,
|
|
@@ -160,6 +162,7 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
160
162
|
}
|
|
161
163
|
const sourceRowCells = tableBlock.rows[rowIdx]?.cells;
|
|
162
164
|
let maxCellHeightWithBorders = 0;
|
|
165
|
+
let maxBorderHeight = 0;
|
|
163
166
|
for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
|
|
164
167
|
const cell = row.cells[cellIdx];
|
|
165
168
|
const sourceCell = sourceRowCells?.[cellIdx];
|
|
@@ -173,12 +176,14 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) {
|
|
|
173
176
|
const padTop = sourceCell?.padding?.top ?? DEFAULT_CELL_PADDING_Y;
|
|
174
177
|
const padBottom = sourceCell?.padding?.bottom ?? DEFAULT_CELL_PADDING_Y;
|
|
175
178
|
cell.height += padTop + padBottom;
|
|
176
|
-
|
|
179
|
+
const borderHeight = getTableCellVerticalBorderHeight(sourceCell, rowIdx === 0);
|
|
180
|
+
maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + borderHeight);
|
|
181
|
+
maxBorderHeight = Math.max(maxBorderHeight, borderHeight);
|
|
177
182
|
}
|
|
178
183
|
const explicitHeight = sourceRow?.height;
|
|
179
184
|
const heightRule = sourceRow?.heightRule;
|
|
180
185
|
if (explicitHeight && heightRule === "exact") row.height = explicitHeight;
|
|
181
|
-
else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight);
|
|
186
|
+
else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxBorderHeight);
|
|
182
187
|
else row.height = maxCellHeightWithBorders;
|
|
183
188
|
}
|
|
184
189
|
const totalHeight = rows.reduce((h, r) => h + r.height, 0);
|
|
@@ -197,6 +202,7 @@ function perBlockNumberValue(value, blockIndex, fallback) {
|
|
|
197
202
|
}
|
|
198
203
|
function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry) {
|
|
199
204
|
const zones = [];
|
|
205
|
+
const textBoxGroupAnchors = /* @__PURE__ */ new Map();
|
|
200
206
|
const defaultMarginTop = Array.isArray(marginTop) ? marginTop[0] ?? 0 : marginTop;
|
|
201
207
|
const pageHeightInput = pageGeometry?.pageHeight ?? 0;
|
|
202
208
|
const marginBottomInput = pageGeometry?.marginBottom ?? 0;
|
|
@@ -286,28 +292,31 @@ function extractFloatingZones(blocks, contentWidth, marginTop = 0, pageGeometry)
|
|
|
286
292
|
const block = blocks[blockIndex];
|
|
287
293
|
if (block.kind !== "textBox") continue;
|
|
288
294
|
const tb = block;
|
|
289
|
-
if (!floatingTextBoxReservesBand(tb)) continue;
|
|
295
|
+
if (!floatingTextBoxReservesBand(tb) || tb.position === void 0) continue;
|
|
290
296
|
const v = tb.position?.vertical;
|
|
291
|
-
if (!isPageFrameRelativeAnchor(v?.relativeTo)) continue;
|
|
292
297
|
const height = measureTextBoxBlock(tb).height;
|
|
293
298
|
const distTop = tb.distTop ?? 0;
|
|
294
299
|
const distBottom = tb.distBottom ?? 0;
|
|
295
300
|
const blockMarginTop = perBlockNumberValue(marginTop, blockIndex, defaultMarginTop);
|
|
296
|
-
const
|
|
301
|
+
const pageFrameRelative = isPageFrameRelativeAnchor(v?.relativeTo);
|
|
302
|
+
const rawTop = pageFrameRelative ? bandTopContentY(v, {
|
|
297
303
|
pageHeight: perBlockNumberValue(pageHeightInput, blockIndex, defaultPageHeight),
|
|
298
304
|
marginTop: blockMarginTop,
|
|
299
305
|
marginBottom: perBlockNumberValue(marginBottomInput, blockIndex, defaultMarginBottom),
|
|
300
306
|
boxHeight: height
|
|
301
|
-
});
|
|
307
|
+
}) : emuToPixels(v?.posOffset ?? 0);
|
|
302
308
|
const bottomY = rawTop + height + distBottom;
|
|
303
309
|
if (bottomY <= 0) continue;
|
|
310
|
+
const groupId = getTextBoxGroupId(tb);
|
|
311
|
+
const anchorBlockIndex = groupId ? textBoxGroupAnchors.get(groupId) ?? blockIndex : blockIndex;
|
|
312
|
+
if (groupId && !textBoxGroupAnchors.has(groupId)) textBoxGroupAnchors.set(groupId, blockIndex);
|
|
304
313
|
zones.push({
|
|
305
314
|
leftMargin: 0,
|
|
306
315
|
rightMargin: 0,
|
|
307
316
|
topY: Math.max(0, rawTop - distTop),
|
|
308
317
|
bottomY,
|
|
309
|
-
anchorBlockIndex
|
|
310
|
-
isMarginRelative: true,
|
|
318
|
+
anchorBlockIndex,
|
|
319
|
+
...pageFrameRelative ? { isMarginRelative: true } : {},
|
|
311
320
|
fullWidthBlock: true
|
|
312
321
|
});
|
|
313
322
|
}
|
|
@@ -367,14 +376,15 @@ function measureTextBoxBlock(tb, fieldValues) {
|
|
|
367
376
|
};
|
|
368
377
|
}
|
|
369
378
|
/**
|
|
370
|
-
* `true` for a section break that starts a new page
|
|
371
|
-
* `oddPage
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* band that lands right after the break.
|
|
379
|
+
* `true` for a section break that explicitly starts a new page
|
|
380
|
+
* (`nextPage`/`evenPage`/`oddPage`). An absent type follows
|
|
381
|
+
* `scheduleSectionBreak` and stays continuous, so measurement must retain any
|
|
382
|
+
* active exclusion zones across it. Used by `measureBlocks` to reset the
|
|
383
|
+
* running Y for a page-pinned band that lands right after the break.
|
|
384
|
+
* eigenpal #694.
|
|
375
385
|
*/
|
|
376
386
|
function isNextPageSectionBreak(block) {
|
|
377
|
-
return block.kind === "sectionBreak" && block.type !== "continuous";
|
|
387
|
+
return block.kind === "sectionBreak" && block.type !== void 0 && block.type !== "continuous";
|
|
378
388
|
}
|
|
379
389
|
/**
|
|
380
390
|
* Measure all blocks with floating image support.
|