@stll/folio-core 0.28.0 → 0.29.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/styleParser.js +15 -14
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +135 -118
- package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
- package/dist/layout-engine/footnoteColumnReflow.js +74 -0
- package/dist/layout-engine/index.d.ts +3 -5
- package/dist/layout-engine/index.js +36 -15
- package/dist/layout-engine/measure/cache.d.ts +1 -1
- package/dist/layout-engine/measure/cache.js +11 -2
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
- package/dist/layout-engine/paginator.d.ts +6 -0
- package/dist/layout-engine/paginator.js +30 -11
- package/dist/layout-engine/types.d.ts +11 -6
- package/dist/markdown/renderParagraph.js +7 -2
- package/dist/prosemirror/attrs/index.js +5 -3
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
- package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
- package/dist/prosemirror/commands/formatPainter.js +45 -1
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +224 -91
- package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/toProseDoc.js +325 -94
- package/dist/prosemirror/extensions/StarterKit.js +11 -3
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
- package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
- package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
- package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
- package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
- package/dist/prosemirror/listMarker.d.ts +58 -0
- package/dist/prosemirror/listMarker.js +185 -0
- package/dist/prosemirror/numberedRefFields.d.ts +24 -0
- package/dist/prosemirror/numberedRefFields.js +276 -0
- package/dist/prosemirror/paraText.js +56 -17
- package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
- package/dist/prosemirror/plugins/pmTextScan.js +28 -7
- package/dist/prosemirror/plugins/templateDirectives.js +2 -2
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +3 -1
- package/dist/prosemirror/schema/nodes.d.ts +13 -1
- package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
- package/dist/prosemirror/styles/styleResolver.js +22 -29
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
- package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
- package/dist/prosemirror/validation.js +93 -0
- package/dist/utils/textFormattingMerge.d.ts +9 -1
- package/dist/utils/textFormattingMerge.js +32 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emuToPixels } from "../utils/units.js";
|
|
2
|
+
import { reflowFootnoteColumns } from "./footnoteColumnReflow.js";
|
|
2
3
|
import { resolveSectionHeaderFooterRefs } from "./headerFooterRefs.js";
|
|
3
4
|
import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
|
|
4
5
|
import { measuredLineAdvance } from "./lineFlow.js";
|
|
@@ -140,13 +141,23 @@ function applyContextualSpacing(blocks) {
|
|
|
140
141
|
/**
|
|
141
142
|
* Layout a document: convert blocks + measures into pages with positioned fragments.
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* 3. Use paginator to manage page/column state
|
|
147
|
-
* 4. Handle page breaks, section breaks, and keepNext chains
|
|
144
|
+
* A reference discovered after the first column has already been placed can shrink the
|
|
145
|
+
* shared body band below earlier-column fragments. Retry those documents with the observed
|
|
146
|
+
* reservations as page floors so every column participates in the same footnote geometry.
|
|
148
147
|
*/
|
|
149
148
|
function layoutDocument(blocks, measures, options) {
|
|
149
|
+
const layout = layoutDocumentPass(blocks, measures, options);
|
|
150
|
+
if (!options.footnoteHeightById) return layout;
|
|
151
|
+
return reflowFootnoteColumns({
|
|
152
|
+
initialLayout: layout,
|
|
153
|
+
...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
|
|
154
|
+
runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
|
|
155
|
+
...options,
|
|
156
|
+
footnoteReservedHeights: reserveFloors
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function layoutDocumentPass(blocks, measures, options) {
|
|
150
161
|
if (blocks.length !== measures.length) panic(`layoutDocument: expected one measure per block (blocks=${blocks.length}, measures=${measures.length})`);
|
|
151
162
|
const pageSize = options.pageSize;
|
|
152
163
|
const baseMargins = {
|
|
@@ -333,6 +344,10 @@ function getLineFootnoteRefs(block, fromRun, toRun, fnHeights) {
|
|
|
333
344
|
height
|
|
334
345
|
};
|
|
335
346
|
}
|
|
347
|
+
function projectedFootnoteReserveGrowth(state, additionalDemandHeight) {
|
|
348
|
+
const projectedDemand = state.footnoteDemandHeight + additionalDemandHeight;
|
|
349
|
+
return Math.max(state.footnoteHeightFloor, projectedDemand) - state.footnoteHeight;
|
|
350
|
+
}
|
|
336
351
|
function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeightById, suppressSpaceBefore }) {
|
|
337
352
|
const lines = measure.lines;
|
|
338
353
|
if (lines.length === 0) {
|
|
@@ -370,7 +385,7 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
370
385
|
if (currentLineIndex === 0 && state.trailingSpacing > 0 && state.cursorY !== state.topMargin) {
|
|
371
386
|
const firstLine = lines[0];
|
|
372
387
|
const firstLineRefs = getLineFootnoteRefs(block, firstLine.fromRun, firstLine.toRun, footnoteHeightById);
|
|
373
|
-
const firstLineHeight = measuredLineAdvance(firstLine) + firstLineRefs.height;
|
|
388
|
+
const firstLineHeight = measuredLineAdvance(firstLine) + projectedFootnoteReserveGrowth(state, firstLineRefs.height);
|
|
374
389
|
const collapsedLead = collapseParagraphSpacing({
|
|
375
390
|
before: spaceBefore,
|
|
376
391
|
after: state.trailingSpacing
|
|
@@ -399,7 +414,8 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
399
414
|
}
|
|
400
415
|
const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
|
|
401
416
|
const totalWithLine = linesHeight + lineAdvance;
|
|
402
|
-
|
|
417
|
+
const footnoteGrowth = projectedFootnoteReserveGrowth(state, linesFnHeight + lineRefs.height);
|
|
418
|
+
if (totalWithLine + firstFragmentSpaceBefore + footnoteGrowth <= availableHeight || fittingLines === 0) {
|
|
403
419
|
linesHeight = totalWithLine;
|
|
404
420
|
linesFnHeight += lineRefs.height;
|
|
405
421
|
for (const id of lineRefs.ids) linesFnIds.push(id);
|
|
@@ -449,8 +465,10 @@ function layoutParagraph({ block, measure, paginator, contentWidth, footnoteHeig
|
|
|
449
465
|
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
450
466
|
};
|
|
451
467
|
if (linesFnHeight > 0) {
|
|
452
|
-
paginator.
|
|
453
|
-
|
|
468
|
+
const stateBefore = paginator.getCurrentState();
|
|
469
|
+
paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateBefore, linesFnHeight));
|
|
470
|
+
const stateAfter = paginator.getCurrentState();
|
|
471
|
+
if (stateAfter.footnoteDemandHeight === 0) paginator.ensureFits(effectiveSpaceBefore + linesHeight + projectedFootnoteReserveGrowth(stateAfter, linesFnHeight + 12));
|
|
454
472
|
}
|
|
455
473
|
fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
|
|
456
474
|
if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
|
|
@@ -650,8 +668,9 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
650
668
|
fragmentFootnoteIds.push(id);
|
|
651
669
|
rowFootnoteHeight += footnoteHeightById?.get(id) ?? 0;
|
|
652
670
|
}
|
|
653
|
-
const
|
|
654
|
-
|
|
671
|
+
const candidateFootnoteHeight = fragmentFootnoteHeight + rowFootnoteHeight;
|
|
672
|
+
const footnoteGrowth = projectedFootnoteReserveGrowth(state, candidateFootnoteHeight + (state.footnoteDemandHeight === 0 && candidateFootnoteHeight > 0 ? 12 : 0));
|
|
673
|
+
if (rowsHeight + rowHeight + normalHeaderOverhead + footnoteGrowth <= availableHeight) {
|
|
655
674
|
rowsHeight += rowHeight;
|
|
656
675
|
fragmentFootnoteHeight += rowFootnoteHeight;
|
|
657
676
|
fittingRows++;
|
|
@@ -780,7 +799,7 @@ function layoutFloatingTable(block, measure, paginator, contentWidth) {
|
|
|
780
799
|
isFloating: true,
|
|
781
800
|
...block.sdtGroups ? { sdtGroups: block.sdtGroups } : {}
|
|
782
801
|
};
|
|
783
|
-
|
|
802
|
+
paginator.addUnflowedFragment(fragment);
|
|
784
803
|
}
|
|
785
804
|
/**
|
|
786
805
|
* Layout an image block onto pages.
|
|
@@ -825,7 +844,7 @@ function layoutAnchoredImage(block, measure, paginator) {
|
|
|
825
844
|
isAnchored: true,
|
|
826
845
|
zIndex: anchor.behindDoc ? -1 : 1
|
|
827
846
|
};
|
|
828
|
-
|
|
847
|
+
paginator.addUnflowedFragment(fragment);
|
|
829
848
|
}
|
|
830
849
|
/**
|
|
831
850
|
* Layout a text box block onto pages.
|
|
@@ -853,10 +872,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
|
|
|
853
872
|
y: state.topMargin + bandTop,
|
|
854
873
|
width: measure.width,
|
|
855
874
|
height: measure.height,
|
|
875
|
+
isPositioned: true,
|
|
856
876
|
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
857
877
|
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
|
|
858
878
|
};
|
|
859
|
-
|
|
879
|
+
paginator.addUnflowedFragment(fragment);
|
|
860
880
|
return;
|
|
861
881
|
}
|
|
862
882
|
if (block.position !== void 0) {
|
|
@@ -882,10 +902,11 @@ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPag
|
|
|
882
902
|
y,
|
|
883
903
|
width: measure.width,
|
|
884
904
|
height: measure.height,
|
|
905
|
+
isPositioned: true,
|
|
885
906
|
...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
|
|
886
907
|
...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
|
|
887
908
|
};
|
|
888
|
-
|
|
909
|
+
paginator.addUnflowedFragment(fragment);
|
|
889
910
|
return;
|
|
890
911
|
}
|
|
891
912
|
const state = paginator.ensureFits(measure.height);
|
|
@@ -54,7 +54,7 @@ declare function setFontCacheSize(size: number): void;
|
|
|
54
54
|
* Get current font metrics cache size
|
|
55
55
|
*/
|
|
56
56
|
declare function getFontCacheSize(): number;
|
|
57
|
-
/** Serialize the complete paragraph measurement contract into a cache key. */
|
|
57
|
+
/** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
|
|
58
58
|
declare function hashParagraphBlock(block: ParagraphBlock): string;
|
|
59
59
|
/**
|
|
60
60
|
* Get cached paragraph measurement or return undefined
|
|
@@ -188,11 +188,20 @@ const runMeasureCacheInput = (run) => {
|
|
|
188
188
|
default: return run;
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
|
-
|
|
191
|
+
const paragraphAttrsMeasureCacheInput = (attrs) => {
|
|
192
|
+
if (attrs === void 0) return void 0;
|
|
193
|
+
const { snapToGrid, ...measurementAttrs } = attrs;
|
|
194
|
+
if (measurementAttrs.documentGridLinePitch === void 0) return measurementAttrs;
|
|
195
|
+
return {
|
|
196
|
+
...measurementAttrs,
|
|
197
|
+
snapToGrid: snapToGrid !== false
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
/** Serialize the complete, semantically normalized paragraph measurement contract into a cache key. */
|
|
192
201
|
function hashParagraphBlock(block) {
|
|
193
202
|
return JSON.stringify({
|
|
194
203
|
lineBreakProviderGeneration: getLineBreakProviderGeneration(),
|
|
195
|
-
attrs: block.attrs,
|
|
204
|
+
attrs: paragraphAttrsMeasureCacheInput(block.attrs),
|
|
196
205
|
runs: block.runs.map(runMeasureCacheInput)
|
|
197
206
|
});
|
|
198
207
|
}
|
|
@@ -27,7 +27,9 @@ declare function resolveListMarkerFont(block: ParagraphBlock): {
|
|
|
27
27
|
* - hanging indent — body wraps at `indentLeft`, marker sits at
|
|
28
28
|
* `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
|
|
29
29
|
* - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
|
|
30
|
-
* one space glyph, `tab` (default) → grow to the next tab stop.
|
|
30
|
+
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
31
|
+
* indent remains the minimum footprint so body text cannot enter the
|
|
32
|
+
* marker slot.
|
|
31
33
|
* - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
|
|
32
34
|
* `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
|
|
33
35
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
@@ -50,7 +50,9 @@ function resolveListMarkerFont(block) {
|
|
|
50
50
|
* - hanging indent — body wraps at `indentLeft`, marker sits at
|
|
51
51
|
* `indentLeft - hanging`. Width is `hanging` so the marker fills the slot.
|
|
52
52
|
* - `w:suff` (§17.9.25): `nothing` → natural width, `space` → natural +
|
|
53
|
-
* one space glyph, `tab` (default) → grow to the next tab stop.
|
|
53
|
+
* one space glyph, `tab` (default) → grow to the next tab stop. A hanging
|
|
54
|
+
* indent remains the minimum footprint so body text cannot enter the
|
|
55
|
+
* marker slot.
|
|
54
56
|
* - `w:tabs` on the paragraph: non-`clear`/non-`bar` stops past the marker.
|
|
55
57
|
* `bar` (§17.3.1.37) is a vertical line and doesn't advance the cursor.
|
|
56
58
|
* - default tab grid: stops at multiples of `DEFAULT_TAB_STOP_TWIPS`,
|
|
@@ -67,12 +69,12 @@ function getListMarkerInlineWidth(block) {
|
|
|
67
69
|
const naturalWidth = measureTextWidth(attrs.listMarker, style);
|
|
68
70
|
const markerEndOffset = getMarkerEndOffset(naturalWidth, attrs.listMarkerAlignment);
|
|
69
71
|
const suffix = attrs.listMarkerSuffix ?? "tab";
|
|
70
|
-
|
|
71
|
-
if (suffix === "
|
|
72
|
+
const hanging = attrs.indent?.hanging ?? 0;
|
|
73
|
+
if (suffix === "nothing") return Math.max(hanging, markerEndOffset);
|
|
74
|
+
if (suffix === "space") return Math.max(hanging, markerEndOffset + measureTextWidth(" ", style));
|
|
72
75
|
const indent = attrs.indent;
|
|
73
76
|
const indentLeft = indent?.left ?? 0;
|
|
74
77
|
const firstLine = indent?.firstLine ?? 0;
|
|
75
|
-
const hanging = indent?.hanging ?? 0;
|
|
76
78
|
const markerStartPx = hanging > 0 ? indentLeft - hanging : indentLeft + firstLine;
|
|
77
79
|
const minBodyStart = markerStartPx + markerEndOffset;
|
|
78
80
|
const customTabs = (attrs.tabs ?? []).filter((t) => t.val !== "clear" && t.val !== "bar").map((t) => t.pos * TWIPS_TO_PX);
|
|
@@ -22,6 +22,10 @@ type PageState = {
|
|
|
22
22
|
rawContentBottom: number;
|
|
23
23
|
/** Total height reserved for footnotes on this page (grows as refs are placed). */
|
|
24
24
|
footnoteHeight: number;
|
|
25
|
+
/** Static reservation supplied by a layout retry. */
|
|
26
|
+
footnoteHeightFloor: number;
|
|
27
|
+
/** Footnote demand discovered while placing reference-bearing content. */
|
|
28
|
+
footnoteDemandHeight: number;
|
|
25
29
|
/** Accumulated trailing spacing (space after previous block). */
|
|
26
30
|
trailingSpacing: number;
|
|
27
31
|
};
|
|
@@ -98,6 +102,8 @@ declare function createPaginator(options: PaginatorOptions): {
|
|
|
98
102
|
x: number;
|
|
99
103
|
y: number;
|
|
100
104
|
};
|
|
105
|
+
/** Add an already-positioned fragment without advancing normal flow. */
|
|
106
|
+
addUnflowedFragment: (fragment: Fragment) => PageState;
|
|
101
107
|
/** Reserve additional footnote area on the current page. */
|
|
102
108
|
addFootnoteHeight: (additionalHeight: number, footnoteIds?: number[]) => void;
|
|
103
109
|
/** Force a page break. */
|
|
@@ -99,8 +99,8 @@ function createPaginator(options) {
|
|
|
99
99
|
const pageMargins = getPageMargins(pageNumber, logicalNumber);
|
|
100
100
|
const topMargin = pageMargins.top;
|
|
101
101
|
const contentBottom = pageSize.h - pageMargins.bottom;
|
|
102
|
-
const
|
|
103
|
-
const pageContentBottom = contentBottom -
|
|
102
|
+
const footnoteHeightFloor = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
|
|
103
|
+
const pageContentBottom = contentBottom - footnoteHeightFloor;
|
|
104
104
|
const page = {
|
|
105
105
|
number: pageNumber,
|
|
106
106
|
logicalNumber,
|
|
@@ -108,7 +108,7 @@ function createPaginator(options) {
|
|
|
108
108
|
fragments: [],
|
|
109
109
|
margins: pageMargins,
|
|
110
110
|
size: { ...pageSize },
|
|
111
|
-
...
|
|
111
|
+
...footnoteHeightFloor > 0 ? { footnoteReservedHeight: footnoteHeightFloor } : {},
|
|
112
112
|
...columns.count > 1 ? { columns: { ...columns } } : {}
|
|
113
113
|
};
|
|
114
114
|
applySectionMetadata(page);
|
|
@@ -119,7 +119,9 @@ function createPaginator(options) {
|
|
|
119
119
|
topMargin,
|
|
120
120
|
contentBottom: pageContentBottom,
|
|
121
121
|
rawContentBottom: contentBottom,
|
|
122
|
-
footnoteHeight,
|
|
122
|
+
footnoteHeight: footnoteHeightFloor,
|
|
123
|
+
footnoteHeightFloor,
|
|
124
|
+
footnoteDemandHeight: 0,
|
|
123
125
|
trailingSpacing: 0
|
|
124
126
|
};
|
|
125
127
|
pages.push(page);
|
|
@@ -197,7 +199,7 @@ function createPaginator(options) {
|
|
|
197
199
|
const y = state.cursorY + actualSpaceBefore;
|
|
198
200
|
fragment.x = x;
|
|
199
201
|
fragment.y = y;
|
|
200
|
-
state
|
|
202
|
+
commitFragment(state, fragment);
|
|
201
203
|
state.cursorY = y + height;
|
|
202
204
|
state.trailingSpacing = spaceAfter;
|
|
203
205
|
return {
|
|
@@ -206,6 +208,18 @@ function createPaginator(options) {
|
|
|
206
208
|
y
|
|
207
209
|
};
|
|
208
210
|
}
|
|
211
|
+
function commitFragment(state, fragment) {
|
|
212
|
+
if (sectionStartPending && state.page.sectionIndex !== currentSectionIndex) {
|
|
213
|
+
if (currentPageNumbering.type === "restart") nextLogicalPageNumber = currentPageNumbering.start + 1;
|
|
214
|
+
sectionStartPending = false;
|
|
215
|
+
}
|
|
216
|
+
state.page.fragments.push(fragment);
|
|
217
|
+
}
|
|
218
|
+
function addUnflowedFragment(fragment) {
|
|
219
|
+
const state = getCurrentState();
|
|
220
|
+
commitFragment(state, fragment);
|
|
221
|
+
return state;
|
|
222
|
+
}
|
|
209
223
|
/**
|
|
210
224
|
* Reserve additional footnote area on the current page.
|
|
211
225
|
*
|
|
@@ -229,8 +243,9 @@ function createPaginator(options) {
|
|
|
229
243
|
function addFootnoteHeight(additionalHeight, footnoteIds) {
|
|
230
244
|
if (!Number.isFinite(additionalHeight) || additionalHeight <= 0) return;
|
|
231
245
|
const state = getCurrentState();
|
|
232
|
-
const separatorOverhead = state.
|
|
233
|
-
state.
|
|
246
|
+
const separatorOverhead = state.footnoteDemandHeight === 0 ? 12 : 0;
|
|
247
|
+
state.footnoteDemandHeight += additionalHeight + separatorOverhead;
|
|
248
|
+
state.footnoteHeight = Math.max(state.footnoteHeightFloor, state.footnoteDemandHeight);
|
|
234
249
|
state.contentBottom = state.rawContentBottom - state.footnoteHeight;
|
|
235
250
|
state.page.footnoteReservedHeight = state.footnoteHeight;
|
|
236
251
|
if (footnoteIds && footnoteIds.length > 0) {
|
|
@@ -261,10 +276,10 @@ function createPaginator(options) {
|
|
|
261
276
|
const pageMargins = getPageMargins(current.page.number, current.page.logicalNumber);
|
|
262
277
|
const topMargin = pageMargins.top;
|
|
263
278
|
const rawContentBottom = pageSize.h - pageMargins.bottom;
|
|
264
|
-
const
|
|
279
|
+
const footnoteHeightFloor = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
|
|
265
280
|
current.page.size = { ...pageSize };
|
|
266
281
|
current.page.margins = pageMargins;
|
|
267
|
-
if (
|
|
282
|
+
if (footnoteHeightFloor > 0) current.page.footnoteReservedHeight = footnoteHeightFloor;
|
|
268
283
|
else delete current.page.footnoteReservedHeight;
|
|
269
284
|
if (columns.count > 1) current.page.columns = { ...columns };
|
|
270
285
|
else delete current.page.columns;
|
|
@@ -272,8 +287,10 @@ function createPaginator(options) {
|
|
|
272
287
|
current.cursorY = topMargin;
|
|
273
288
|
current.columnIndex = 0;
|
|
274
289
|
current.rawContentBottom = rawContentBottom;
|
|
275
|
-
current.footnoteHeight =
|
|
276
|
-
current.
|
|
290
|
+
current.footnoteHeight = footnoteHeightFloor;
|
|
291
|
+
current.footnoteHeightFloor = footnoteHeightFloor;
|
|
292
|
+
current.footnoteDemandHeight = 0;
|
|
293
|
+
current.contentBottom = rawContentBottom - footnoteHeightFloor;
|
|
277
294
|
current.trailingSpacing = 0;
|
|
278
295
|
columnRegionTop = topMargin;
|
|
279
296
|
return true;
|
|
@@ -370,6 +387,8 @@ function createPaginator(options) {
|
|
|
370
387
|
ensureFits,
|
|
371
388
|
/** Add a fragment to current page. */
|
|
372
389
|
addFragment,
|
|
390
|
+
/** Add an already-positioned fragment without advancing normal flow. */
|
|
391
|
+
addUnflowedFragment,
|
|
373
392
|
/** Reserve additional footnote area on the current page. */
|
|
374
393
|
addFootnoteHeight,
|
|
375
394
|
/** Force a page break. */
|
|
@@ -1059,6 +1059,8 @@ type TextBoxFragment = FragmentBase & {
|
|
|
1059
1059
|
kind: "textBox";
|
|
1060
1060
|
/** Height of the text box. */
|
|
1061
1061
|
height: number;
|
|
1062
|
+
/** True if this text box is positioned outside normal body flow. */
|
|
1063
|
+
isPositioned?: true;
|
|
1062
1064
|
};
|
|
1063
1065
|
/**
|
|
1064
1066
|
* Union of all fragment types.
|
|
@@ -1226,18 +1228,21 @@ type LayoutOptions = {
|
|
|
1226
1228
|
evenAndOddHeaders?: boolean;
|
|
1227
1229
|
/** Swap left/right margins on even physical pages. */
|
|
1228
1230
|
mirrorMargins?: boolean;
|
|
1229
|
-
/**
|
|
1231
|
+
/**
|
|
1232
|
+
* Per-page footnote reserved heights (pageNumber → height in pixels).
|
|
1233
|
+
* When `footnoteHeightById` is also supplied, a value is a retry floor only
|
|
1234
|
+
* while that page has assigned footnote IDs; without the dynamic height map,
|
|
1235
|
+
* the value remains a static reservation.
|
|
1236
|
+
*/
|
|
1230
1237
|
footnoteReservedHeights?: Map<number, number>;
|
|
1231
1238
|
/**
|
|
1232
1239
|
* Footnote content heights keyed by internal footnote id (the OOXML
|
|
1233
1240
|
* `<w:footnoteReference w:id>`). When provided, the layout engine
|
|
1234
1241
|
* tracks footnote demand per body line: each line carrying a fn ref
|
|
1235
1242
|
* grows its page's reservation by that fn's height before the next
|
|
1236
|
-
* line is fitted.
|
|
1237
|
-
* reservation
|
|
1238
|
-
*
|
|
1239
|
-
* above the fn area) on documents with multiple long footnotes per
|
|
1240
|
-
* page.
|
|
1243
|
+
* line is fitted. Multi-column pages may retry with the observed
|
|
1244
|
+
* reservation as a shared floor when a later column invalidates
|
|
1245
|
+
* earlier-column placement.
|
|
1241
1246
|
*/
|
|
1242
1247
|
footnoteHeightById?: Map<number, number>;
|
|
1243
1248
|
/** Header/footer references for each document section, by section index. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveListTemplate } from "../
|
|
1
|
+
import { resolveListTemplate } from "../prosemirror/listMarker.js";
|
|
2
2
|
import { isHeadingStyle, parseHeadingLevel } from "./headings.js";
|
|
3
3
|
import { renderParagraphInline } from "./renderRuns.js";
|
|
4
4
|
//#region src/markdown/renderParagraph.ts
|
|
@@ -58,7 +58,12 @@ function resolveTemplateMarker(ctx, list) {
|
|
|
58
58
|
for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0;
|
|
59
59
|
ctx.listCounters.set(list.numId, counters);
|
|
60
60
|
const levelFormats = list.levelNumFmts ?? (list.numFmt ? [list.numFmt] : void 0);
|
|
61
|
-
return resolveListTemplate(
|
|
61
|
+
return resolveListTemplate({
|
|
62
|
+
template: list.marker,
|
|
63
|
+
counters,
|
|
64
|
+
levelFormats,
|
|
65
|
+
forceDecimal: list.isLegal
|
|
66
|
+
}).trim();
|
|
62
67
|
}
|
|
63
68
|
//#endregion
|
|
64
69
|
export { renderParagraph };
|
|
@@ -77,11 +77,10 @@ const TEXT_BOX_TRACKED_CHANGE_TYPES = [
|
|
|
77
77
|
"moveTo"
|
|
78
78
|
];
|
|
79
79
|
const HARD_BREAK_TYPES = ["column"];
|
|
80
|
-
const
|
|
80
|
+
const RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS = [
|
|
81
81
|
"bold",
|
|
82
82
|
"italic",
|
|
83
83
|
"strike",
|
|
84
|
-
"doubleStrike",
|
|
85
84
|
"allCaps",
|
|
86
85
|
"smallCaps",
|
|
87
86
|
"hidden",
|
|
@@ -90,6 +89,7 @@ const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = [
|
|
|
90
89
|
"shadow",
|
|
91
90
|
"outline"
|
|
92
91
|
];
|
|
92
|
+
const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = ["doubleStrike", "rtl"];
|
|
93
93
|
const SECTION_ORIENTATIONS = ["portrait", "landscape"];
|
|
94
94
|
const SECTION_START_TYPES = [
|
|
95
95
|
"continuous",
|
|
@@ -371,10 +371,11 @@ const expectImageAttrs = (node) => expectCachedNodeAttrs(node, imageAttrsCache,
|
|
|
371
371
|
const readFieldAttrs = (node) => {
|
|
372
372
|
const attrs = attrsRecord(node.attrs);
|
|
373
373
|
const issues = [];
|
|
374
|
-
|
|
374
|
+
expectNodeTypeOneOf(node, ["field", "structuredField"], issues);
|
|
375
375
|
requiredOneOf(attrs, "fieldType", "field.attrs.fieldType", issues, FIELD_TYPE_VALUES);
|
|
376
376
|
requiredString(attrs, "instruction", "field.attrs.instruction", issues);
|
|
377
377
|
requiredString(attrs, "displayText", "field.attrs.displayText", issues);
|
|
378
|
+
optionalString(attrs, "_numberedRefBaseline", "field.attrs._numberedRefBaseline", issues);
|
|
378
379
|
requiredOneOf(attrs, "fieldKind", "field.attrs.fieldKind", issues, FIELD_KINDS);
|
|
379
380
|
optionalBoolean(attrs, "fldLock", "field.attrs.fldLock", issues);
|
|
380
381
|
optionalBoolean(attrs, "dirty", "field.attrs.dirty", issues);
|
|
@@ -659,6 +660,7 @@ const readRunFormattingOverrideMarkAttrs = (mark) => {
|
|
|
659
660
|
const attrs = attrsRecord(mark.attrs);
|
|
660
661
|
const issues = [];
|
|
661
662
|
expectMarkType(mark, "runFormattingOverride", issues);
|
|
663
|
+
for (const key of RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS) optionalBoolean(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
|
|
662
664
|
for (const key of RUN_FORMATTING_OVERRIDE_FALSE_KEYS) optionalFalse(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
|
|
663
665
|
optionalBoolean(attrs, "boldCs", "runFormattingOverride.attrs.boldCs", issues);
|
|
664
666
|
optionalBoolean(attrs, "italicCs", "runFormattingOverride.attrs.italicCs", issues);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BookmarkBoundaryAttrs } from "./schema/nodes.js";
|
|
2
|
+
import { ReadProseMirrorAttrsResult } from "./attrs/index.js";
|
|
3
|
+
import { Node } from "prosemirror-model";
|
|
4
|
+
//#region src/prosemirror/bookmarkBoundaryAttrs.d.ts
|
|
5
|
+
declare const readBookmarkBoundaryAttrs: (node: Node) => ReadProseMirrorAttrsResult<BookmarkBoundaryAttrs>;
|
|
6
|
+
declare const expectBookmarkBoundaryAttrs: (node: Node) => BookmarkBoundaryAttrs;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { expectBookmarkBoundaryAttrs, readBookmarkBoundaryAttrs };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { panic } from "better-result";
|
|
2
|
+
//#region src/prosemirror/bookmarkBoundaryAttrs.ts
|
|
3
|
+
const attrsCache = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
const readBookmarkBoundaryAttrs = (node) => {
|
|
5
|
+
const issues = [];
|
|
6
|
+
if (node.type.name !== "bookmarkBoundary") issues.push({
|
|
7
|
+
path: "bookmarkBoundary.type.name",
|
|
8
|
+
message: `Expected bookmarkBoundary, got ${node.type.name}.`
|
|
9
|
+
});
|
|
10
|
+
const type = node.attrs["type"];
|
|
11
|
+
if (type !== "start" && type !== "end") issues.push({
|
|
12
|
+
path: "bookmarkBoundary.attrs.type",
|
|
13
|
+
message: "Expected \"start\" or \"end\"."
|
|
14
|
+
});
|
|
15
|
+
const id = node.attrs["id"];
|
|
16
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id < 0) issues.push({
|
|
17
|
+
path: "bookmarkBoundary.attrs.id",
|
|
18
|
+
message: "Expected a non-negative integer."
|
|
19
|
+
});
|
|
20
|
+
const name = node.attrs["name"];
|
|
21
|
+
if (type === "start" && (typeof name !== "string" || name.length === 0)) issues.push({
|
|
22
|
+
path: "bookmarkBoundary.attrs.name",
|
|
23
|
+
message: "Expected a non-empty string for a bookmark start."
|
|
24
|
+
});
|
|
25
|
+
const colFirst = node.attrs["colFirst"];
|
|
26
|
+
if (colFirst !== void 0 && colFirst !== null && (typeof colFirst !== "number" || !Number.isInteger(colFirst) || colFirst < 0)) issues.push({
|
|
27
|
+
path: "bookmarkBoundary.attrs.colFirst",
|
|
28
|
+
message: "Expected a non-negative integer."
|
|
29
|
+
});
|
|
30
|
+
const colLast = node.attrs["colLast"];
|
|
31
|
+
if (colLast !== void 0 && colLast !== null && (typeof colLast !== "number" || !Number.isInteger(colLast) || colLast < 0)) issues.push({
|
|
32
|
+
path: "bookmarkBoundary.attrs.colLast",
|
|
33
|
+
message: "Expected a non-negative integer."
|
|
34
|
+
});
|
|
35
|
+
if (issues.length > 0 || typeof id !== "number") return {
|
|
36
|
+
ok: false,
|
|
37
|
+
issues
|
|
38
|
+
};
|
|
39
|
+
if (type === "start" && typeof name === "string") return {
|
|
40
|
+
ok: true,
|
|
41
|
+
value: {
|
|
42
|
+
type,
|
|
43
|
+
id,
|
|
44
|
+
name,
|
|
45
|
+
...typeof colFirst === "number" ? { colFirst } : {},
|
|
46
|
+
...typeof colLast === "number" ? { colLast } : {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
value: {
|
|
52
|
+
type: "end",
|
|
53
|
+
id
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const expectBookmarkBoundaryAttrs = (node) => {
|
|
58
|
+
const cached = attrsCache.get(node);
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
const result = readBookmarkBoundaryAttrs(node);
|
|
61
|
+
if (!result.ok) panic(`Invalid ProseMirror bookmark boundary attrs:\n${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("\n")}`);
|
|
62
|
+
attrsCache.set(node, result.value);
|
|
63
|
+
return result.value;
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
export { expectBookmarkBoundaryAttrs, readBookmarkBoundaryAttrs };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { expectCharacterStyleMarkAttrs, expectParagraphAttrs } from "../attrs/index.js";
|
|
1
2
|
//#region src/prosemirror/commands/formatPainter.ts
|
|
2
3
|
/**
|
|
3
4
|
* Character-formatting marks the painter copies. Structural marks — comments,
|
|
@@ -64,6 +65,40 @@ function sanitizeOverrideMark(mark) {
|
|
|
64
65
|
if (Object.keys(kept).length === 0) return null;
|
|
65
66
|
return mark.type.create(kept);
|
|
66
67
|
}
|
|
68
|
+
const PAINTABLE_STYLE_TOGGLE_MARKS = [
|
|
69
|
+
["allCaps", "allCaps"],
|
|
70
|
+
["bold", "bold"],
|
|
71
|
+
["emboss", "emboss"],
|
|
72
|
+
["imprint", "imprint"],
|
|
73
|
+
["italic", "italic"],
|
|
74
|
+
["outline", "textOutline"],
|
|
75
|
+
["shadow", "textShadow"],
|
|
76
|
+
["smallCaps", "smallCaps"],
|
|
77
|
+
["strike", "strike"]
|
|
78
|
+
];
|
|
79
|
+
const PAINTABLE_COMPLEX_SCRIPT_TOGGLES = ["boldCs", "italicCs"];
|
|
80
|
+
function materializeStyleToggleNegatives({ inheritedFormatting, marks }) {
|
|
81
|
+
const characterStyle = marks.find((mark) => mark.type.name === "characterStyle");
|
|
82
|
+
if (!characterStyle) return [...marks];
|
|
83
|
+
const styleRPr = expectCharacterStyleMarkAttrs(characterStyle)._styleRPr;
|
|
84
|
+
if (!styleRPr) return [...marks];
|
|
85
|
+
const negativeAttrs = {};
|
|
86
|
+
for (const [key, markName] of PAINTABLE_STYLE_TOGGLE_MARKS) if (styleRPr[key] === true && !marks.some((mark) => mark.type.name === markName)) negativeAttrs[key] = false;
|
|
87
|
+
for (const key of PAINTABLE_COMPLEX_SCRIPT_TOGGLES) {
|
|
88
|
+
if (styleRPr[key] === void 0) continue;
|
|
89
|
+
const inheritedValue = inheritedFormatting?.[key] ?? false;
|
|
90
|
+
if (!(styleRPr[key] === true ? !inheritedValue : false)) negativeAttrs[key] = false;
|
|
91
|
+
}
|
|
92
|
+
if (Object.keys(negativeAttrs).length === 0) return [...marks];
|
|
93
|
+
const existingOverride = marks.find((mark) => mark.type.name === "runFormattingOverride");
|
|
94
|
+
const overrideType = existingOverride?.type ?? characterStyle.type.schema.marks["runFormattingOverride"];
|
|
95
|
+
if (!overrideType) return [...marks];
|
|
96
|
+
const override = overrideType.create({
|
|
97
|
+
...negativeAttrs,
|
|
98
|
+
...existingOverride?.attrs
|
|
99
|
+
});
|
|
100
|
+
return existingOverride ? marks.map((mark) => mark === existingOverride ? override : mark) : [...marks, override];
|
|
101
|
+
}
|
|
67
102
|
/**
|
|
68
103
|
* Marks of the first text node inside [from, to). A word processor's format
|
|
69
104
|
* brush copies from the start of the source selection, so a mixed selection
|
|
@@ -81,6 +116,12 @@ function firstTextMarks(state, from, to) {
|
|
|
81
116
|
});
|
|
82
117
|
return marks ?? [];
|
|
83
118
|
}
|
|
119
|
+
function paragraphDefaultTextFormatting(position) {
|
|
120
|
+
for (let depth = position.depth; depth >= 0; depth -= 1) {
|
|
121
|
+
const ancestor = position.node(depth);
|
|
122
|
+
if (ancestor.type.name === "paragraph") return expectParagraphAttrs(ancestor).defaultTextFormatting;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
84
125
|
/**
|
|
85
126
|
* Capture the paintable character marks active over the current selection.
|
|
86
127
|
* Returns the marks (with their attrs) to hand to `applyFormatMarks`. An empty
|
|
@@ -100,7 +141,10 @@ function captureFormatMarks(state) {
|
|
|
100
141
|
}
|
|
101
142
|
captured.push(mark);
|
|
102
143
|
}
|
|
103
|
-
return
|
|
144
|
+
return materializeStyleToggleNegatives({
|
|
145
|
+
inheritedFormatting: paragraphDefaultTextFormatting($from),
|
|
146
|
+
marks: captured
|
|
147
|
+
});
|
|
104
148
|
}
|
|
105
149
|
/**
|
|
106
150
|
* Apply captured marks onto the current selection's range: clear every paintable
|
|
@@ -8,7 +8,10 @@ declare function fromProseDoc(pmDoc: Node, baseDocument?: document_d_exports.Doc
|
|
|
8
8
|
/**
|
|
9
9
|
* Convert ProseMirror marks to TextFormatting
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
type MarksToTextFormattingOptions = {
|
|
12
|
+
inheritedFormatting: document_d_exports.TextFormatting | undefined;
|
|
13
|
+
};
|
|
14
|
+
declare function marksToTextFormatting(marks: readonly Mark[], options?: MarksToTextFormattingOptions): document_d_exports.TextFormatting;
|
|
12
15
|
declare const standaloneTableCellFromProseMirror: (node: Node) => document_d_exports.TableCell;
|
|
13
16
|
/**
|
|
14
17
|
* Update a Document with content from a ProseMirror document
|