@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.
Files changed (56) hide show
  1. package/dist/docx/styleParser.js +15 -14
  2. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
  3. package/dist/layout-bridge/convert/toFlowBlocks.js +135 -118
  4. package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
  5. package/dist/layout-engine/footnoteColumnReflow.js +74 -0
  6. package/dist/layout-engine/index.d.ts +3 -5
  7. package/dist/layout-engine/index.js +36 -15
  8. package/dist/layout-engine/measure/cache.d.ts +1 -1
  9. package/dist/layout-engine/measure/cache.js +11 -2
  10. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  11. package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
  12. package/dist/layout-engine/paginator.d.ts +6 -0
  13. package/dist/layout-engine/paginator.js +30 -11
  14. package/dist/layout-engine/types.d.ts +11 -6
  15. package/dist/markdown/renderParagraph.js +7 -2
  16. package/dist/prosemirror/attrs/index.js +5 -3
  17. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  18. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  19. package/dist/prosemirror/commands/formatPainter.js +45 -1
  20. package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
  21. package/dist/prosemirror/conversion/fromProseDoc.js +224 -91
  22. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
  23. package/dist/prosemirror/conversion/toProseDoc.js +325 -94
  24. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  25. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  26. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  27. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  28. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  29. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  30. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
  31. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  32. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  33. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  34. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  35. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  36. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  37. package/dist/prosemirror/listMarker.d.ts +58 -0
  38. package/dist/prosemirror/listMarker.js +185 -0
  39. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  40. package/dist/prosemirror/numberedRefFields.js +276 -0
  41. package/dist/prosemirror/paraText.js +56 -17
  42. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  43. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  44. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  45. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  46. package/dist/prosemirror/schema/index.d.ts +2 -2
  47. package/dist/prosemirror/schema/marks.d.ts +3 -1
  48. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  49. package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
  50. package/dist/prosemirror/styles/styleResolver.js +22 -29
  51. package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
  52. package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
  53. package/dist/prosemirror/validation.js +93 -0
  54. package/dist/utils/textFormattingMerge.d.ts +9 -1
  55. package/dist/utils/textFormattingMerge.js +32 -1
  56. package/package.json +1 -1
@@ -1,23 +1,24 @@
1
1
  import { isValidHexColor } from "../utils/colorResolver.js";
2
2
  import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
3
- import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
3
+ import { mergeStyleTextFormatting } from "../utils/textFormattingMerge.js";
4
4
  import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
5
5
  import { resolveThemeFontRef } from "./themeParser.js";
6
6
  import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
7
7
  //#region src/docx/styleParser.ts
8
+ const findLastRunToggle = (rPr, localName) => findChildren(rPr, "w", localName).at(-1) ?? null;
8
9
  /**
9
10
  * Parse text formatting properties (w:rPr)
10
11
  */
11
12
  function parseRunProperties(rPr, theme) {
12
13
  if (!rPr) return;
13
14
  const formatting = {};
14
- const b = findChild(rPr, "w", "b");
15
+ const b = findLastRunToggle(rPr, "b");
15
16
  if (b) formatting.bold = parseBooleanElement(b);
16
- const bCs = findChild(rPr, "w", "bCs");
17
+ const bCs = findLastRunToggle(rPr, "bCs");
17
18
  if (bCs) formatting.boldCs = parseBooleanElement(bCs);
18
- const i = findChild(rPr, "w", "i");
19
+ const i = findLastRunToggle(rPr, "i");
19
20
  if (i) formatting.italic = parseBooleanElement(i);
20
- const iCs = findChild(rPr, "w", "iCs");
21
+ const iCs = findLastRunToggle(rPr, "iCs");
21
22
  if (iCs) formatting.italicCs = parseBooleanElement(iCs);
22
23
  const u = findChild(rPr, "w", "u");
23
24
  if (u) {
@@ -29,7 +30,7 @@ function parseRunProperties(rPr, theme) {
29
30
  if (colorVal || themeColor) formatting.underline.color = parseColorValue(colorVal, themeColor, getAttribute(u, "w", "themeTint"), getAttribute(u, "w", "themeShade"));
30
31
  }
31
32
  }
32
- const strike = findChild(rPr, "w", "strike");
33
+ const strike = findLastRunToggle(rPr, "strike");
33
34
  if (strike) formatting.strike = parseBooleanElement(strike);
34
35
  const dstrike = findChild(rPr, "w", "dstrike");
35
36
  if (dstrike) formatting.doubleStrike = parseBooleanElement(dstrike);
@@ -38,11 +39,11 @@ function parseRunProperties(rPr, theme) {
38
39
  const val = getAttribute(vertAlign, "w", "val");
39
40
  if (val === "superscript" || val === "subscript" || val === "baseline") formatting.vertAlign = val;
40
41
  }
41
- const smallCaps = findChild(rPr, "w", "smallCaps");
42
+ const smallCaps = findLastRunToggle(rPr, "smallCaps");
42
43
  if (smallCaps) formatting.smallCaps = parseBooleanElement(smallCaps);
43
- const caps = findChild(rPr, "w", "caps");
44
+ const caps = findLastRunToggle(rPr, "caps");
44
45
  if (caps) formatting.allCaps = parseBooleanElement(caps);
45
- const vanish = findChild(rPr, "w", "vanish");
46
+ const vanish = findLastRunToggle(rPr, "vanish");
46
47
  if (vanish) formatting.hidden = parseBooleanElement(vanish);
47
48
  const color = findChild(rPr, "w", "color");
48
49
  if (color) formatting.color = parseColorValue(getAttribute(color, "w", "val"), getAttribute(color, "w", "themeColor"), getAttribute(color, "w", "themeTint"), getAttribute(color, "w", "themeShade"));
@@ -154,13 +155,13 @@ function parseRunProperties(rPr, theme) {
154
155
  const val = narrowEnum(getAttribute(em, "w", "val"), EmphasisMarkSchema);
155
156
  if (val) formatting.emphasisMark = val;
156
157
  }
157
- const emboss = findChild(rPr, "w", "emboss");
158
+ const emboss = findLastRunToggle(rPr, "emboss");
158
159
  if (emboss) formatting.emboss = parseBooleanElement(emboss);
159
- const imprint = findChild(rPr, "w", "imprint");
160
+ const imprint = findLastRunToggle(rPr, "imprint");
160
161
  if (imprint) formatting.imprint = parseBooleanElement(imprint);
161
- const outline = findChild(rPr, "w", "outline");
162
+ const outline = findLastRunToggle(rPr, "outline");
162
163
  if (outline) formatting.outline = parseBooleanElement(outline);
163
- const shadow = findChild(rPr, "w", "shadow");
164
+ const shadow = findLastRunToggle(rPr, "shadow");
164
165
  if (shadow) formatting.shadow = parseBooleanElement(shadow);
165
166
  const rtl = findChild(rPr, "w", "rtl");
166
167
  if (rtl) formatting.rtl = parseBooleanElement(rtl);
@@ -807,7 +808,7 @@ function resolveStyleInheritance(style, styleMap, visited = /* @__PURE__ */ new
807
808
  const resolved = { ...style };
808
809
  const mergedPPr = mergeParagraphFormatting(resolvedParent.pPr, style.pPr);
809
810
  if (mergedPPr) resolved.pPr = mergedPPr;
810
- const mergedRPr = mergeTextFormatting(resolvedParent.rPr, style.rPr);
811
+ const mergedRPr = mergeStyleTextFormatting(resolvedParent.rPr, style.rPr);
811
812
  if (mergedRPr) resolved.rPr = mergedRPr;
812
813
  if (style.type === "table") {
813
814
  if (resolvedParent.tblPr || style.tblPr) resolved.tblPr = {
@@ -1,5 +1,6 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
2
  import { BorderStyle, FlowBlock, ParagraphAttrs } from "../../layout-engine/types.js";
3
+ import { formatCounter, resolveListTemplate } from "../../prosemirror/listMarker.js";
3
4
  import { Node } from "prosemirror-model";
4
5
  //#region src/layout-bridge/convert/toFlowBlocks.d.ts
5
6
  /**
@@ -59,8 +60,6 @@ type ToFlowBlocksOptions = {
59
60
  /** Line pitch for the final body section, whose properties live outside the PM body. */
60
61
  finalSectionDocumentGridLinePitchTwips?: number;
61
62
  };
62
- declare function formatCounter(value: number, format: document_d_exports.NumberFormat | undefined): string;
63
- declare function resolveListTemplate(template: string, counters: number[], levelFormats: document_d_exports.NumberFormat[] | undefined, forceDecimal?: boolean): string;
64
63
  /**
65
64
  * Reset the block ID counter (useful for testing).
66
65
  */
@@ -1,15 +1,17 @@
1
1
  import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
2
2
  import { resolveDocumentGridLinePitch } from "../../docx/documentGrid.js";
3
- import { formatOoxmlCounter } from "../../docx/ooxmlCounterFormatter.js";
4
3
  import { setHyperlinkInstanceIndex } from "../../layout-engine/measure/hyperlinkInstance.js";
5
4
  import { setParagraphFrame } from "../../layout-engine/paragraphFrame.js";
6
5
  import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
7
6
  import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
8
7
  import { getPageNumbering } from "../../paged-layout/sectionGeometry.js";
9
- import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
8
+ import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectSymbolAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
10
9
  import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
11
10
  import { runShadingAttrsToShading } from "../../prosemirror/conversion/runShadingMark.js";
11
+ import { advanceListMarker, advanceVisibleListMarker, cloneListCounterState, formatCounter, resolveListTemplate } from "../../prosemirror/listMarker.js";
12
+ import { resolveNumberedRefFields } from "../../prosemirror/numberedRefFields.js";
12
13
  import { directionToBidi } from "../../prosemirror/paragraphDirection.js";
14
+ import { cascadeStyleTextFormatting } from "../../prosemirror/styles/styleToggleCascade.js";
13
15
  import { assertValidProseMirrorDocument } from "../../prosemirror/validation.js";
14
16
  import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
15
17
  import { resolveThemeFont } from "../../utils/fontResolver.js";
@@ -67,79 +69,12 @@ let blockIdCounter = 0;
67
69
  function nextBlockId() {
68
70
  return `block-${++blockIdCounter}`;
69
71
  }
70
- function formatNumberedMarker(counters, level) {
71
- const parts = [];
72
- for (let i = 0; i <= level; i += 1) {
73
- const value = counters[i] ?? 0;
74
- if (!Number.isFinite(value) || value <= 0) break;
75
- parts.push(value);
76
- }
77
- if (parts.length === 0) return "1.";
78
- return `${parts.join(".")}.`;
79
- }
80
- function formatCounter(value, format) {
81
- return formatOoxmlCounter(value, format);
82
- }
83
- function resolveListTemplate(template, counters, levelFormats, forceDecimal = false) {
84
- return template.replace(/%(?<digit>\d)(?<punct>[.):\]])?/gu, (...args) => {
85
- const { digit, punct = "" } = args.at(-1);
86
- const index = Number.parseInt(digit, 10) - 1;
87
- if (index < 0) return "";
88
- const counter = counters[index];
89
- if (counter === void 0 || Number.isNaN(counter)) return "";
90
- const formatted = formatCounter(counter, forceDecimal ? "decimal" : levelFormats?.[index]);
91
- return formatted ? `${formatted}${punct}` : "";
92
- });
93
- }
94
- function getLastListCounters(listCounters) {
95
- let lastCounters;
96
- for (const counters of listCounters.values()) lastCounters = counters;
97
- return lastCounters;
98
- }
99
72
  function applyMarkerAllCaps(marker, allCaps) {
100
73
  if (marker === null || !allCaps) return marker;
101
74
  return marker.toLocaleUpperCase();
102
75
  }
103
- function computeListMarker(pmAttrs, listCounters, abstractCounters, seenNumIds) {
104
- const numId = pmAttrs.numPr?.numId;
105
- if (numId === void 0 || numId === 0) {
106
- if (pmAttrs.listMarker?.includes("%") && !pmAttrs.listIsBullet) {
107
- const counters = getLastListCounters(listCounters);
108
- if (counters) return resolveListTemplate(pmAttrs.listMarker, counters, pmAttrs.listLevelNumFmts, pmAttrs.listIsLegal);
109
- }
110
- return null;
111
- }
112
- if (pmAttrs.listIsBullet) return convertBulletToUnicode(pmAttrs.listMarker || "");
113
- const level = pmAttrs.numPr?.ilvl ?? 0;
114
- const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => NaN);
115
- const abstractNumId = pmAttrs.listAbstractNumId;
116
- if (level > 0) {
117
- const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId);
118
- if (counters.slice(0, level).every((counter) => !Number.isFinite(counter))) for (let i = 0; i < level; i += 1) {
119
- const latestCounter = latestAbstractCounters?.[i];
120
- counters[i] = latestCounter !== void 0 && Number.isFinite(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
121
- }
122
- }
123
- const seenKey = `${numId}:${level}`;
124
- if (!seenNumIds.has(seenKey)) {
125
- seenNumIds.add(seenKey);
126
- if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1;
127
- }
128
- if (!Number.isFinite(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
129
- counters[level] = (counters[level] ?? 0) + 1;
130
- for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
131
- const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0;
132
- if (childAdvances > 0 && level + 1 < counters.length) {
133
- const childCounter = counters[level + 1];
134
- counters[level + 1] = (childCounter === void 0 || !Number.isFinite(childCounter) ? 0 : childCounter) + childAdvances;
135
- }
136
- listCounters.set(numId, counters);
137
- if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]);
138
- const levelFormats = pmAttrs.listLevelNumFmts ?? (pmAttrs.listNumFmt ? [pmAttrs.listNumFmt] : void 0);
139
- if (pmAttrs.listMarker && pmAttrs.listMarker.includes("%")) return resolveListTemplate(pmAttrs.listMarker, counters, levelFormats, pmAttrs.listIsLegal);
140
- if (pmAttrs.listMarker) return pmAttrs.listMarker;
141
- if ((levelFormats?.[level] ?? pmAttrs.listNumFmt) === "none" || pmAttrs.listMarker === "") return null;
142
- return formatNumberedMarker(counters, level);
76
+ function computeListMarker(pmAttrs, state) {
77
+ return advanceListMarker(pmAttrs, state);
143
78
  }
144
79
  /**
145
80
  * Reset the block ID counter (useful for testing).
@@ -348,17 +283,49 @@ function mergeRunFormatting(paraDefaults, formatting) {
348
283
  if (merged.letterSpacing === 0) delete merged.letterSpacing;
349
284
  return merged;
350
285
  }
286
+ /** Restore character-style toggle values that plain visual marks cannot represent. */
287
+ function applyCharacterStyleToggleFormatting({ formatting, marks, paraDefaults }) {
288
+ const characterStyleMark = marks.find((mark) => mark.type.name === "characterStyle");
289
+ if (!characterStyleMark) return;
290
+ const styleRPr = expectCharacterStyleMarkAttrs(characterStyleMark)._styleRPr;
291
+ if (!styleRPr) return;
292
+ const effectiveStyleFormatting = cascadeStyleTextFormatting([{
293
+ formatting: {
294
+ bold: paraDefaults.bold ?? false,
295
+ boldCs: paraDefaults.complexScriptBold ?? false,
296
+ italic: paraDefaults.italic ?? false,
297
+ italicCs: paraDefaults.complexScriptItalic ?? false
298
+ },
299
+ type: "direct"
300
+ }, {
301
+ formatting: styleRPr,
302
+ type: "style"
303
+ }]).formatting;
304
+ if (styleRPr.bold !== void 0 && formatting.bold === void 0) formatting.bold = effectiveStyleFormatting?.bold ?? false;
305
+ if (styleRPr.boldCs !== void 0 && formatting.complexScriptBold === void 0) formatting.complexScriptBold = effectiveStyleFormatting?.boldCs ?? false;
306
+ if (styleRPr.italic !== void 0 && formatting.italic === void 0) formatting.italic = effectiveStyleFormatting?.italic ?? false;
307
+ if (styleRPr.italicCs !== void 0 && formatting.complexScriptItalic === void 0) formatting.complexScriptItalic = effectiveStyleFormatting?.italicCs ?? false;
308
+ if (styleRPr.allCaps === true && formatting.allCaps === void 0) formatting.allCaps = false;
309
+ if (styleRPr.emboss === true && formatting.emboss === void 0) formatting.emboss = false;
310
+ if (styleRPr.imprint === true && formatting.imprint === void 0) formatting.imprint = false;
311
+ if (styleRPr.outline === true && formatting.textOutline === void 0) formatting.textOutline = false;
312
+ if (styleRPr.shadow === true && formatting.textShadow === void 0) formatting.textShadow = false;
313
+ if (styleRPr.smallCaps === true && formatting.smallCaps === void 0) formatting.smallCaps = false;
314
+ if (styleRPr.strike === true && formatting.strike === void 0) formatting.strike = false;
315
+ if (styleRPr.hidden === true && formatting.hidden === void 0) formatting.hidden = false;
316
+ }
351
317
  function applyRunFormattingOverrides(formatting, attrs) {
352
- if (attrs.bold === false) formatting.bold = false;
353
- if (attrs.italic === false) formatting.italic = false;
318
+ if (attrs.bold !== void 0) formatting.bold = attrs.bold;
319
+ if (attrs.italic !== void 0) formatting.italic = attrs.italic;
354
320
  if (attrs.underline === "none") formatting.underline = false;
355
- if (attrs.strike === false) formatting.strike = false;
356
- if (attrs.allCaps === false) formatting.allCaps = false;
357
- if (attrs.smallCaps === false) formatting.smallCaps = false;
358
- if (attrs.emboss === false) formatting.emboss = false;
359
- if (attrs.imprint === false) formatting.imprint = false;
360
- if (attrs.shadow === false) formatting.textShadow = false;
361
- if (attrs.outline === false) formatting.textOutline = false;
321
+ if (attrs.strike !== void 0) formatting.strike = attrs.strike;
322
+ if (attrs.allCaps !== void 0) formatting.allCaps = attrs.allCaps;
323
+ if (attrs.smallCaps !== void 0) formatting.smallCaps = attrs.smallCaps;
324
+ if (attrs.hidden !== void 0) formatting.hidden = attrs.hidden;
325
+ if (attrs.emboss !== void 0) formatting.emboss = attrs.emboss;
326
+ if (attrs.imprint !== void 0) formatting.imprint = attrs.imprint;
327
+ if (attrs.shadow !== void 0) formatting.textShadow = attrs.shadow;
328
+ if (attrs.outline !== void 0) formatting.textOutline = attrs.outline;
362
329
  if (attrs.rtl === false) formatting.rtl = false;
363
330
  if (attrs.boldCs !== void 0) formatting.complexScriptBold = attrs.boldCs;
364
331
  if (attrs.italicCs !== void 0) formatting.complexScriptItalic = attrs.italicCs;
@@ -482,6 +449,7 @@ function paragraphToRuns(node, startPos, _options) {
482
449
  const inTocParagraph = pmAttrs._tableOfContentsLevel !== void 0 || tableOfContentsStyleLevel({ styleId: paragraphStyleId }) !== void 0;
483
450
  let leadingRenderedPageBreakPending = pmAttrs.renderedPageBreakBefore === true;
484
451
  function pushRunsForChild(child, childPos) {
452
+ if (child.type.name === "bookmarkBoundary") return;
485
453
  if (child.type.name === "renderedPageBreak") {
486
454
  if (leadingRenderedPageBreakPending) {
487
455
  leadingRenderedPageBreakPending = false;
@@ -497,6 +465,11 @@ function paragraphToRuns(node, startPos, _options) {
497
465
  if (child.type.name !== "sdt") leadingRenderedPageBreakPending = false;
498
466
  if (child.isText && child.text) {
499
467
  const formatting = extractRunFormatting(child.marks, theme);
468
+ applyCharacterStyleToggleFormatting({
469
+ formatting,
470
+ marks: child.marks,
471
+ paraDefaults
472
+ });
500
473
  if (inTocParagraph) stripTocHyperlinkStyle(formatting);
501
474
  const run = {
502
475
  kind: "text",
@@ -513,6 +486,11 @@ function paragraphToRuns(node, startPos, _options) {
513
486
  const text = decodeOoxmlSymbolCharacter(attrs.char);
514
487
  if (text === null) return;
515
488
  const formatting = extractRunFormatting(child.marks, theme);
489
+ applyCharacterStyleToggleFormatting({
490
+ formatting,
491
+ marks: child.marks,
492
+ paraDefaults
493
+ });
516
494
  if (inTocParagraph) stripTocHyperlinkStyle(formatting);
517
495
  runs.push({
518
496
  kind: "text",
@@ -534,6 +512,11 @@ function paragraphToRuns(node, startPos, _options) {
534
512
  }
535
513
  if (child.type.name === "tab") {
536
514
  const formatting = extractRunFormatting(child.marks, theme);
515
+ applyCharacterStyleToggleFormatting({
516
+ formatting,
517
+ marks: child.marks,
518
+ paraDefaults
519
+ });
537
520
  const run = {
538
521
  kind: "tab",
539
522
  ...mergeRunFormatting(paraDefaults, formatting),
@@ -552,7 +535,7 @@ function paragraphToRuns(node, startPos, _options) {
552
535
  runs.push(run);
553
536
  return;
554
537
  }
555
- if (child.type.name === "field") {
538
+ if (child.type.name === "field" || child.type.name === "structuredField") {
556
539
  const attrs = expectFieldAttrs(child);
557
540
  const ft = attrs.fieldType;
558
541
  let mappedType = "OTHER";
@@ -561,13 +544,18 @@ function paragraphToRuns(node, startPos, _options) {
561
544
  else if (ft === "DATE") mappedType = "DATE";
562
545
  else if (ft === "TIME") mappedType = "TIME";
563
546
  const extractedFieldFormatting = extractRunFormatting(child.marks, theme);
547
+ applyCharacterStyleToggleFormatting({
548
+ formatting: extractedFieldFormatting,
549
+ marks: child.marks,
550
+ paraDefaults
551
+ });
564
552
  if (inTocParagraph) stripTocHyperlinkStyle(extractedFieldFormatting);
565
553
  const fieldFormatting = markDefaultBlackTextColorSource(extractedFieldFormatting, paraDefaults);
566
554
  const run = {
567
555
  kind: "field",
568
556
  fieldType: mappedType,
569
557
  instruction: attrs.instruction,
570
- fallback: attrs.displayText || "",
558
+ fallback: _options.numberedRefResults?.get(child) ?? attrs.displayText ?? "",
571
559
  ...attrs.fldLock ? { fldLock: true } : {},
572
560
  pmStart: childPos,
573
561
  pmEnd: childPos + child.nodeSize,
@@ -658,18 +646,18 @@ function toPreviousListAttrs(previousFormatting) {
658
646
  if (listMarkerSuffix !== void 0) attrs.listMarkerSuffix = listMarkerSuffix;
659
647
  return attrs;
660
648
  }
661
- function resolveDeletedListMarker(previousListAttrs, listCounters, listAbstractCounters, listSeenNumIds) {
662
- if (listCounters && previousListAttrs.numPr) {
663
- const marker = computeListMarker(previousListAttrs, listCounters, listAbstractCounters ?? /* @__PURE__ */ new Map(), listSeenNumIds ?? /* @__PURE__ */ new Set());
649
+ function resolveDeletedListMarker(previousListAttrs, listCounterState) {
650
+ if (listCounterState && previousListAttrs.numPr) {
651
+ const marker = computeListMarker(previousListAttrs, listCounterState);
664
652
  if (marker) return marker;
665
653
  }
666
654
  if (previousListAttrs.listMarker) return previousListAttrs.listIsBullet ? convertBulletToUnicode(previousListAttrs.listMarker) : previousListAttrs.listMarker;
667
655
  if (previousListAttrs.listIsBullet) return "•";
668
656
  return null;
669
657
  }
670
- function applyDeletedListMarkerAttrs(attrs, change, listCounters, listAbstractCounters, listSeenNumIds, theme) {
658
+ function applyDeletedListMarkerAttrs(attrs, change, listCounterState, theme) {
671
659
  const previousListAttrs = toPreviousListAttrs(change.previousFormatting);
672
- const marker = resolveDeletedListMarker(previousListAttrs, listCounters, listAbstractCounters, listSeenNumIds);
660
+ const marker = resolveDeletedListMarker(previousListAttrs, listCounterState);
673
661
  if (!marker) return;
674
662
  attrs.listMarker = marker;
675
663
  attrs.listMarkerRevision = toListMarkerRevision("del", change.info);
@@ -679,7 +667,7 @@ function applyDeletedListMarkerAttrs(attrs, change, listCounters, listAbstractCo
679
667
  if (previousListAttrs.listMarkerAlignment) attrs.listMarkerAlignment = previousListAttrs.listMarkerAlignment;
680
668
  if (previousListAttrs.listMarkerSuffix) attrs.listMarkerSuffix = previousListAttrs.listMarkerSuffix;
681
669
  }
682
- function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounters, listSeenNumIds, defaultTabStopTwips, originalListCounters, originalListAbstractCounters, originalListSeenNumIds) {
670
+ function convertParagraphAttrs(pmAttrs, { theme, listCounterStreams, defaultTabStopTwips }) {
683
671
  const attrs = {};
684
672
  if (pmAttrs.alignment) {
685
673
  const align = pmAttrs.alignment;
@@ -798,15 +786,7 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
798
786
  if (numberingInsertionChange) attrs.listMarkerRevision = toListMarkerRevision("ins", numberingInsertionChange.info);
799
787
  }
800
788
  }
801
- const useOriginalStream = attrs.listMarkerRevision?.kind === "del" && originalListCounters !== void 0;
802
- const streamCounters = useOriginalStream ? originalListCounters : listCounters;
803
- const resolvedMarker = applyMarkerAllCaps(streamCounters ? computeListMarker(pmAttrs, streamCounters, (useOriginalStream ? originalListAbstractCounters : listAbstractCounters) ?? /* @__PURE__ */ new Map(), (useOriginalStream ? originalListSeenNumIds : listSeenNumIds) ?? /* @__PURE__ */ new Set()) : null, pmAttrs.listMarkerAllCaps);
804
- const numberedNumId = pmAttrs.numPr?.numId;
805
- if (attrs.listMarkerRevision === void 0 && !pmAttrs.listIsBullet && originalListCounters !== void 0 && numberedNumId !== void 0 && numberedNumId !== 0) computeListMarker(pmAttrs, originalListCounters, originalListAbstractCounters ?? /* @__PURE__ */ new Map(), originalListSeenNumIds ?? /* @__PURE__ */ new Set());
806
- else if (changedNumberingChange !== void 0 && originalListCounters !== void 0) {
807
- const previousListAttrs = toPreviousListAttrs(changedNumberingChange.previousFormatting);
808
- if (previousListAttrs.numPr && !previousListAttrs.listIsBullet) computeListMarker(previousListAttrs, originalListCounters, originalListAbstractCounters ?? /* @__PURE__ */ new Map(), originalListSeenNumIds ?? /* @__PURE__ */ new Set());
809
- }
789
+ const resolvedMarker = applyMarkerAllCaps(advanceVisibleListMarker(pmAttrs, listCounterStreams).marker, pmAttrs.listMarkerAllCaps);
810
790
  if (resolvedMarker !== null) attrs.listMarker = resolvedMarker;
811
791
  else if (pmAttrs.listMarker) attrs.listMarker = pmAttrs.listIsBullet ? convertBulletToUnicode(pmAttrs.listMarker) : pmAttrs.listMarker;
812
792
  if (pmAttrs.listIsBullet !== void 0) attrs.listIsBullet = pmAttrs.listIsBullet;
@@ -817,7 +797,13 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
817
797
  if (pmAttrs.listMarkerSecondSlotOffsetTwips !== void 0) attrs.listMarkerSecondSlotOffsetTwips = pmAttrs.listMarkerSecondSlotOffsetTwips;
818
798
  if (!pmAttrs.numPr) {
819
799
  const numberingRemovedChange = propertyChanges.find(isRemovedNumberingChange);
820
- if (numberingRemovedChange) applyDeletedListMarkerAttrs(attrs, numberingRemovedChange, originalListCounters, originalListAbstractCounters, originalListSeenNumIds, theme);
800
+ if (numberingRemovedChange) {
801
+ applyDeletedListMarkerAttrs(attrs, numberingRemovedChange, void 0, theme);
802
+ if (resolvedMarker !== null) {
803
+ attrs.listMarker = resolvedMarker;
804
+ attrs.listMarkerRevision = toListMarkerRevision("del", numberingRemovedChange.info);
805
+ }
806
+ }
821
807
  }
822
808
  if (defaultTabStopTwips !== void 0) attrs.defaultTabStopTwips = defaultTabStopTwips;
823
809
  const dtf = pmAttrs.defaultTextFormatting;
@@ -855,7 +841,11 @@ function hasOnlyVisuallyEmptyTextRuns(runs) {
855
841
  function convertParagraph(node, startPos, options) {
856
842
  const pmAttrs = expectParagraphAttrs(node);
857
843
  const runs = paragraphToRuns(node, startPos, options);
858
- const attrs = convertParagraphAttrs(pmAttrs, options.theme, options.listCounters, options.listAbstractCounters, options.listSeenNumIds, options.defaultTabStopTwips, options.originalListCounters, options.originalListAbstractCounters, options.originalListSeenNumIds);
844
+ const attrs = convertParagraphAttrs(pmAttrs, {
845
+ theme: options.theme,
846
+ listCounterStreams: options.listCounterStreams,
847
+ defaultTabStopTwips: options.defaultTabStopTwips
848
+ });
859
849
  if (options.lineBreakRules) attrs.lineBreakRules = options.lineBreakRules;
860
850
  if (options.justificationCompatibility) attrs.justificationCompatibility = options.justificationCompatibility;
861
851
  if (options.automaticHyphenation) attrs.automaticHyphenation = options.automaticHyphenation;
@@ -902,27 +892,25 @@ function convertParagraph(node, startPos, options) {
902
892
  return block;
903
893
  }
904
894
  /**
905
- * Word keeps terminal empty body paragraphs after a final table as editable
906
- * anchors, but they do not create a page of their own. Preserve every block
907
- * and PM range while collapsing only the contiguous, run-free suffix; empty
908
- * paragraphs elsewhere still retain their normal line height.
895
+ * Word keeps a final empty body paragraph after a table as an editable anchor,
896
+ * but that final anchor does not create a page of its own. Earlier authored
897
+ * empty paragraphs retain their height and may carry the document onto a blank
898
+ * page. Preserve every block and PM range while collapsing only the final one.
909
899
  */
910
900
  function isPaintlessTerminalParagraph(block) {
911
901
  if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
912
902
  const attrs = block.attrs;
913
903
  return !(attrs?.listMarker !== void 0 && !attrs.listMarkerHidden || attrs?.borders?.top || attrs?.borders?.bottom || attrs?.borders?.left || attrs?.borders?.right || attrs?.borders?.between || attrs?.borders?.bar || attrs?.shading || attrs?.spacingExplicit?.before || attrs?.spacingExplicit?.after || attrs?.pageBreakBefore || attrs?.renderedPageBreakBefore);
914
904
  }
915
- function suppressTerminalEmptyParagraphsAfterTable(blocks) {
905
+ function suppressFinalEmptyParagraphAfterTable(blocks) {
916
906
  let suffixStart = blocks.length;
917
907
  while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
918
908
  if (suffixStart === blocks.length || suffixStart === 0 || blocks[suffixStart - 1]?.kind !== "table") return;
919
- for (let index = suffixStart; index < blocks.length; index += 1) {
920
- const block = blocks[index];
921
- if (isPaintlessTerminalParagraph(block)) block.attrs = {
922
- ...block.attrs,
923
- suppressEmptyParagraphHeight: true
924
- };
925
- }
909
+ const finalBlock = blocks.at(-1);
910
+ if (isPaintlessTerminalParagraph(finalBlock)) finalBlock.attrs = {
911
+ ...finalBlock.attrs,
912
+ suppressEmptyParagraphHeight: true
913
+ };
926
914
  }
927
915
  function suppressFinalParagraphInRepeatedEmptySuffix(blocks) {
928
916
  let suffixStart = blocks.length;
@@ -1232,6 +1220,11 @@ function convertTextBoxNode(node, startPos, opts) {
1232
1220
  if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId);
1233
1221
  return textBox;
1234
1222
  }
1223
+ function getLastMapKey(map) {
1224
+ let lastKey;
1225
+ for (const key of map.keys()) lastKey = key;
1226
+ return lastKey;
1227
+ }
1235
1228
  /**
1236
1229
  * Convert a ProseMirror document to FlowBlock array.
1237
1230
  *
@@ -1241,16 +1234,40 @@ function convertTextBoxNode(node, startPos, opts) {
1241
1234
  function toFlowBlocks(doc, options = {}) {
1242
1235
  assertValidProseMirrorDocument(doc, "Cannot layout invalid ProseMirror document");
1243
1236
  resetBlockIdCounter();
1237
+ const listCounters = options.listCounters ?? /* @__PURE__ */ new Map();
1238
+ const originalListCounters = options.originalListCounters ?? /* @__PURE__ */ new Map();
1239
+ const lastAdvancedNumId = getLastMapKey(listCounters);
1240
+ const lastAdvancedOriginalNumId = getLastMapKey(originalListCounters);
1241
+ const listCounterState = {
1242
+ counters: listCounters,
1243
+ abstractCounters: options.listAbstractCounters ?? /* @__PURE__ */ new Map(),
1244
+ seenLevels: options.listSeenNumIds ?? /* @__PURE__ */ new Set(),
1245
+ ...lastAdvancedNumId !== void 0 ? { lastAdvancedNumId } : {}
1246
+ };
1247
+ const originalListCounterState = {
1248
+ counters: originalListCounters,
1249
+ abstractCounters: options.originalListAbstractCounters ?? /* @__PURE__ */ new Map(),
1250
+ seenLevels: options.originalListSeenNumIds ?? /* @__PURE__ */ new Set(),
1251
+ ...lastAdvancedOriginalNumId !== void 0 ? { lastAdvancedNumId: lastAdvancedOriginalNumId } : {}
1252
+ };
1244
1253
  const opts = {
1245
1254
  ...options,
1246
1255
  defaultFont: options.defaultFont ?? DEFAULT_FONT,
1247
1256
  defaultSize: options.defaultSize ?? DEFAULT_SIZE,
1248
- listCounters: options.listCounters ?? /* @__PURE__ */ new Map(),
1249
- listAbstractCounters: options.listAbstractCounters ?? /* @__PURE__ */ new Map(),
1250
- listSeenNumIds: options.listSeenNumIds ?? /* @__PURE__ */ new Set(),
1251
- originalListCounters: options.originalListCounters ?? /* @__PURE__ */ new Map(),
1252
- originalListAbstractCounters: options.originalListAbstractCounters ?? /* @__PURE__ */ new Map(),
1253
- originalListSeenNumIds: options.originalListSeenNumIds ?? /* @__PURE__ */ new Set()
1257
+ listCounters: listCounterState.counters,
1258
+ listAbstractCounters: listCounterState.abstractCounters,
1259
+ listSeenNumIds: listCounterState.seenLevels,
1260
+ originalListCounters: originalListCounterState.counters,
1261
+ originalListAbstractCounters: originalListCounterState.abstractCounters,
1262
+ originalListSeenNumIds: originalListCounterState.seenLevels,
1263
+ listCounterStreams: {
1264
+ final: listCounterState,
1265
+ original: originalListCounterState
1266
+ },
1267
+ numberedRefResults: resolveNumberedRefFields(doc, {
1268
+ listCounterState: cloneListCounterState(listCounterState),
1269
+ originalListCounterState: cloneListCounterState(originalListCounterState)
1270
+ })
1254
1271
  };
1255
1272
  const blocks = [];
1256
1273
  const offset = 0;
@@ -1477,7 +1494,7 @@ function toFlowBlocks(doc, options = {}) {
1477
1494
  visit(node, offset + nodeOffset);
1478
1495
  });
1479
1496
  reserveLeadingEmptyOutlineHeight(blocks);
1480
- suppressTerminalEmptyParagraphsAfterTable(blocks);
1497
+ suppressFinalEmptyParagraphAfterTable(blocks);
1481
1498
  suppressFinalParagraphInRepeatedEmptySuffix(blocks);
1482
1499
  return groupParagraphFrames(applySectionDocumentGrid(mergeRunInParagraphs(blocks), opts.finalSectionDocumentGridLinePitchTwips), nextBlockId);
1483
1500
  }
@@ -0,0 +1,15 @@
1
+ import { Layout } from "./types.js";
2
+ //#region src/layout-engine/footnoteColumnReflow.d.ts
3
+ type ReflowFootnoteColumnsOptions = {
4
+ initialLayout: Layout;
5
+ initialReserveFloors?: ReadonlyMap<number, number>;
6
+ runLayout: (reserveFloors: Map<number, number>) => Layout;
7
+ };
8
+ /**
9
+ * Re-run a multi-column layout until its body flow clears the shared footnote band.
10
+ * Reserve floors follow the current page-to-footnote assignment. Repeated floor states
11
+ * and persistent overlap use a stable fallback layout instead of throwing.
12
+ */
13
+ declare function reflowFootnoteColumns({ initialLayout, initialReserveFloors, runLayout }: ReflowFootnoteColumnsOptions): Layout;
14
+ //#endregion
15
+ export { reflowFootnoteColumns };
@@ -0,0 +1,74 @@
1
+ //#region src/layout-engine/footnoteColumnReflow.ts
2
+ function isFlowFragment(fragment) {
3
+ switch (fragment.kind) {
4
+ case "paragraph": return true;
5
+ case "table": return !fragment.isFloating;
6
+ case "image": return !fragment.isAnchored;
7
+ case "textBox": return !fragment.isPositioned;
8
+ default: return fragment;
9
+ }
10
+ }
11
+ function hasFootnoteColumnOverlap(layout) {
12
+ for (const page of layout.pages) {
13
+ if ((page.columns?.count ?? 1) < 2 || !page.footnoteReservedHeight) continue;
14
+ const bodyBottom = page.size.h - page.margins.bottom - page.footnoteReservedHeight;
15
+ if (page.fragments.some((fragment) => isFlowFragment(fragment) && fragment.y + fragment.height > bodyBottom)) return true;
16
+ }
17
+ return false;
18
+ }
19
+ function growFootnoteReserveFloors(layout, initialReserveFloors) {
20
+ const next = /* @__PURE__ */ new Map();
21
+ for (const page of layout.pages) {
22
+ if ((page.footnoteIds?.length ?? 0) === 0) continue;
23
+ const observed = page.footnoteReservedHeight ?? 0;
24
+ const callerFloor = initialReserveFloors?.get(page.number) ?? 0;
25
+ const floor = Math.max(callerFloor, observed);
26
+ if (floor > 0) next.set(page.number, floor);
27
+ }
28
+ return next;
29
+ }
30
+ function reserveFloorFingerprint(floors) {
31
+ return JSON.stringify([...floors.entries()].sort(([leftPage], [rightPage]) => leftPage - rightPage));
32
+ }
33
+ function clearOrphanedFootnoteReservations(layout) {
34
+ let pagesChanged = false;
35
+ const pages = layout.pages.map((page) => {
36
+ if ((page.footnoteIds?.length ?? 0) > 0 || page.footnoteReservedHeight === void 0) return page;
37
+ pagesChanged = true;
38
+ const cleanedPage = { ...page };
39
+ delete cleanedPage.footnoteReservedHeight;
40
+ return cleanedPage;
41
+ });
42
+ if (!pagesChanged) return layout;
43
+ return {
44
+ ...layout,
45
+ pages
46
+ };
47
+ }
48
+ function getPassBudget(layout) {
49
+ const fragmentCount = layout.pages.reduce((count, page) => count + page.fragments.length, 0);
50
+ const footnoteCount = new Set(layout.pages.flatMap((page) => page.footnoteIds ?? [])).size;
51
+ return layout.pages.length + fragmentCount + footnoteCount + 1;
52
+ }
53
+ /**
54
+ * Re-run a multi-column layout until its body flow clears the shared footnote band.
55
+ * Reserve floors follow the current page-to-footnote assignment. Repeated floor states
56
+ * and persistent overlap use a stable fallback layout instead of throwing.
57
+ */
58
+ function reflowFootnoteColumns({ initialLayout, initialReserveFloors, runLayout }) {
59
+ let layout = initialLayout;
60
+ if (!hasFootnoteColumnOverlap(layout)) return clearOrphanedFootnoteReservations(layout);
61
+ const seenReserveStates = /* @__PURE__ */ new Set();
62
+ const passBudget = getPassBudget(layout);
63
+ for (let pass = 0; pass < passBudget; pass += 1) {
64
+ const reserveFloors = growFootnoteReserveFloors(layout, initialReserveFloors);
65
+ const reserveState = reserveFloorFingerprint(reserveFloors);
66
+ if (seenReserveStates.has(reserveState)) return clearOrphanedFootnoteReservations(layout);
67
+ seenReserveStates.add(reserveState);
68
+ layout = runLayout(reserveFloors);
69
+ if (!hasFootnoteColumnOverlap(layout)) return clearOrphanedFootnoteReservations(layout);
70
+ }
71
+ return clearOrphanedFootnoteReservations(layout);
72
+ }
73
+ //#endregion
74
+ export { reflowFootnoteColumns };
@@ -32,11 +32,9 @@ declare function applyContextualSpacing(blocks: FlowBlock[]): void;
32
32
  /**
33
33
  * Layout a document: convert blocks + measures into pages with positioned fragments.
34
34
  *
35
- * Algorithm:
36
- * 1. Walk blocks in order with their corresponding measures
37
- * 2. For each block, create appropriate fragment(s)
38
- * 3. Use paginator to manage page/column state
39
- * 4. Handle page breaks, section breaks, and keepNext chains
35
+ * A reference discovered after the first column has already been placed can shrink the
36
+ * shared body band below earlier-column fragments. Retry those documents with the observed
37
+ * reservations as page floors so every column participates in the same footnote geometry.
40
38
  */
41
39
  declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], options: LayoutOptions): Layout;
42
40
  /**