@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.
Files changed (99) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +33 -4
  6. package/dist/ai-edits/headless.js +83 -15
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -137
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.js +2 -1
  22. package/dist/docx/encryption/compoundFile.js +4 -2
  23. package/dist/docx/footnoteParser.d.ts +1 -17
  24. package/dist/docx/footnoteParser.js +1 -27
  25. package/dist/docx/groupDrawingParser.d.ts +8 -0
  26. package/dist/docx/groupDrawingParser.js +131 -0
  27. package/dist/docx/numberingParser.d.ts +3 -1
  28. package/dist/docx/numberingParser.js +8 -2
  29. package/dist/docx/paragraphParser.js +9 -3
  30. package/dist/docx/runParser.js +8 -1
  31. package/dist/docx/selectiveXmlPatch.js +4 -10
  32. package/dist/docx/serializer/numberingSerializer.js +3 -1
  33. package/dist/docx/settingsParser.d.ts +6 -3
  34. package/dist/docx/settingsParser.js +2 -0
  35. package/dist/docx/xmlParser.js +11 -4
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.js +6 -5
  38. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  39. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  40. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -7
  41. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -7
  42. package/dist/layout-bridge/convert/toFlowBlocks.js +56 -19
  43. package/dist/layout-engine/index.d.ts +6 -5
  44. package/dist/layout-engine/index.js +89 -26
  45. package/dist/layout-engine/keep-together.d.ts +13 -8
  46. package/dist/layout-engine/keep-together.js +40 -25
  47. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  48. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  49. package/dist/layout-engine/measure/measureBlocks.js +28 -18
  50. package/dist/layout-engine/measure/measureParagraph.js +61 -15
  51. package/dist/layout-engine/paginator.d.ts +2 -1
  52. package/dist/layout-engine/paginator.js +9 -3
  53. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  54. package/dist/layout-engine/textBoxGroup.js +8 -0
  55. package/dist/layout-engine/types.d.ts +30 -9
  56. package/dist/layout-engine/types.js +20 -2
  57. package/dist/layout-painter/renderPage.d.ts +15 -1
  58. package/dist/layout-painter/renderPage.js +27 -11
  59. package/dist/layout-painter/renderParagraph.js +18 -8
  60. package/dist/layout-painter/renderTable.js +10 -7
  61. package/dist/managers/DocumentLoaderManager.js +12 -5
  62. package/dist/managers/TableSelectionManager.js +1 -1
  63. package/dist/model.js +1 -1
  64. package/dist/paged-layout/pageText.d.ts +14 -0
  65. package/dist/paged-layout/pageText.js +21 -0
  66. package/dist/prosemirror/attrs/index.js +2 -0
  67. package/dist/prosemirror/commands/comments.js +75 -5
  68. package/dist/prosemirror/commands/hyperlink.js +4 -2
  69. package/dist/prosemirror/commands/image.js +2 -1
  70. package/dist/prosemirror/commands/index.js +1 -1
  71. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  72. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  73. package/dist/prosemirror/conversion/fromProseDoc.js +14 -3
  74. package/dist/prosemirror/conversion/toProseDoc.js +29 -8
  75. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -0
  76. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  77. package/dist/prosemirror/extensions/nodes/TableExtension.js +10 -11
  78. package/dist/prosemirror/findReplaceSelection.js +2 -1
  79. package/dist/prosemirror/index.js +1 -1
  80. package/dist/prosemirror/insertOperations.js +1 -1
  81. package/dist/prosemirror/schema/nodes.d.ts +10 -5
  82. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  83. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  84. package/dist/redline.d.ts +20 -0
  85. package/dist/redline.js +127 -0
  86. package/dist/server.d.ts +7 -3
  87. package/dist/server.js +6 -2
  88. package/dist/symbols.d.ts +35 -0
  89. package/dist/symbols.js +512 -0
  90. package/dist/utils/clipboard.js +3 -3
  91. package/dist/utils/fontLoader.js +3 -0
  92. package/dist/utils/fontResolver.js +48 -1
  93. package/dist/utils/zoom.d.ts +21 -0
  94. package/dist/utils/zoom.js +21 -0
  95. package/dist/version-comparison.d.ts +92 -0
  96. package/dist/version-comparison.js +436 -0
  97. package/package.json +3 -3
  98. package/dist/paged-layout/headerFooterMargins.d.ts +0 -75
  99. package/dist/paged-layout/headerFooterMargins.js +0 -122
@@ -16,20 +16,149 @@ const FOOTNOTE_FONT_SIZE = 8;
16
16
  * area.
17
17
  */
18
18
  function collectFootnoteRefs(blocks) {
19
+ return collectNoteRefs(blocks, "footnoteRefId").map(({ noteId, pmPos }) => ({
20
+ footnoteId: noteId,
21
+ pmPos
22
+ }));
23
+ }
24
+ /**
25
+ * Scan FlowBlocks for runs with endnoteRefId set, in document order.
26
+ * Same recursive walk as `collectFootnoteRefs`.
27
+ */
28
+ function collectEndnoteRefs(blocks) {
29
+ return collectNoteRefs(blocks, "endnoteRefId").map(({ noteId, pmPos }) => ({
30
+ endnoteId: noteId,
31
+ pmPos
32
+ }));
33
+ }
34
+ function collectNoteRefs(blocks, idKey) {
19
35
  const refs = [];
20
36
  const walk = (containerBlocks) => {
21
- for (const block of containerBlocks) if (block.kind === "paragraph") {
22
- for (const run of block.runs) if (run.kind === "text" && run.footnoteRefId !== void 0) refs.push({
23
- footnoteId: run.footnoteRefId,
37
+ for (const block of containerBlocks) if (block.kind === "paragraph") for (const run of block.runs) {
38
+ if (run.kind !== "text") continue;
39
+ const noteId = run[idKey];
40
+ if (noteId !== void 0) refs.push({
41
+ noteId,
24
42
  pmPos: run.pmStart ?? 0
25
43
  });
26
- } else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks);
44
+ }
45
+ else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks);
27
46
  else if (block.kind === "textBox") walk(block.content);
28
47
  };
29
48
  walk(blocks);
30
49
  return refs;
31
50
  }
32
51
  /**
52
+ * Assign sequential display numbers (1, 2, 3, …) to notes in order of first
53
+ * reference. Word numbers footnotes/endnotes by reference order, not by their
54
+ * `w:id` values, which may be non-contiguous or out of document order. Only
55
+ * `normal` notes are numbered; separator/continuation notes and refs to
56
+ * missing notes consume no number.
57
+ */
58
+ function computeNoteDisplayNumbers(notes, refNoteIds) {
59
+ const numberable = /* @__PURE__ */ new Set();
60
+ for (const note of notes) if (note.noteType === "normal") numberable.add(note.id);
61
+ const displayNumbers = /* @__PURE__ */ new Map();
62
+ let nextNumber = 1;
63
+ for (const noteId of refNoteIds) {
64
+ if (displayNumbers.has(noteId) || !numberable.has(noteId)) continue;
65
+ displayNumbers.set(noteId, nextNumber);
66
+ nextNumber++;
67
+ }
68
+ return displayNumbers;
69
+ }
70
+ /**
71
+ * Rewrite body reference-marker run text from the raw `w:id` (which the PM
72
+ * doc stores as the marker text; the save path serializes from the mark
73
+ * attrs, never from this text) to the sequential display number, so the
74
+ * inline marker matches the number rendered in the note area. Must run
75
+ * before measurement so marker widths match the painted digits. Untouched
76
+ * blocks/runs are returned by reference so the painter's fingerprinting and
77
+ * the incremental measure path see them unchanged. Runs keep their PM range,
78
+ * so click-to-position still resolves into the marker (same contract as the
79
+ * template preview substitution).
80
+ */
81
+ function remapNoteMarkerText(blocks, maps) {
82
+ if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteNumbers?.size ?? 0) === 0) return blocks;
83
+ let changed = false;
84
+ const next = blocks.map((block) => {
85
+ const remapped = remapNoteMarkerBlock(block, maps);
86
+ changed ||= remapped !== block;
87
+ return remapped;
88
+ });
89
+ return changed ? next : blocks;
90
+ }
91
+ function remapNoteMarkerBlock(block, maps) {
92
+ if (block.kind === "paragraph") return remapNoteMarkerParagraph(block, maps);
93
+ if (block.kind === "table") {
94
+ let tableChanged = false;
95
+ const rows = block.rows.map((row) => {
96
+ let rowChanged = false;
97
+ const cells = row.cells.map((cell) => {
98
+ let cellChanged = false;
99
+ const cellBlocks = cell.blocks.map((cellBlock) => {
100
+ const remapped = remapNoteMarkerBlock(cellBlock, maps);
101
+ cellChanged ||= remapped !== cellBlock;
102
+ return remapped;
103
+ });
104
+ return cellChanged ? {
105
+ ...cell,
106
+ blocks: cellBlocks
107
+ } : cell;
108
+ });
109
+ rowChanged = cells.some((cell, index) => cell !== row.cells[index]);
110
+ tableChanged ||= rowChanged;
111
+ return rowChanged ? {
112
+ ...row,
113
+ cells
114
+ } : row;
115
+ });
116
+ return tableChanged ? {
117
+ ...block,
118
+ rows
119
+ } : block;
120
+ }
121
+ if (block.kind === "textBox") {
122
+ let boxChanged = false;
123
+ const content = block.content.map((paragraph) => {
124
+ const remapped = remapNoteMarkerParagraph(paragraph, maps);
125
+ boxChanged ||= remapped !== paragraph;
126
+ return remapped;
127
+ });
128
+ return boxChanged ? {
129
+ ...block,
130
+ content
131
+ } : block;
132
+ }
133
+ return block;
134
+ }
135
+ function remapNoteMarkerParagraph(block, maps) {
136
+ let changed = false;
137
+ const runs = block.runs.map((run) => {
138
+ const remapped = remapNoteMarkerRun(run, maps);
139
+ changed ||= remapped !== run;
140
+ return remapped;
141
+ });
142
+ return changed ? {
143
+ ...block,
144
+ runs
145
+ } : block;
146
+ }
147
+ function remapNoteMarkerRun(run, maps) {
148
+ if (run.kind !== "text") return run;
149
+ const displayNumber = getRunDisplayNumber(run, maps);
150
+ if (displayNumber === void 0) return run;
151
+ const text = String(displayNumber);
152
+ return run.text === text ? run : {
153
+ ...run,
154
+ text
155
+ };
156
+ }
157
+ function getRunDisplayNumber(run, maps) {
158
+ if (run.footnoteRefId !== void 0) return maps.footnoteNumbers?.get(run.footnoteRefId);
159
+ if (run.endnoteRefId !== void 0) return maps.endnoteNumbers?.get(run.endnoteRefId);
160
+ }
161
+ /**
33
162
  * After layout, determine which footnotes appear on which pages.
34
163
  * Checks each page's fragments to see if any footnoteRef PM positions fall within.
35
164
  *
@@ -279,16 +408,11 @@ function buildFootnoteContentMap(footnotes, footnoteRefs, contentWidth, options
279
408
  const contentMap = /* @__PURE__ */ new Map();
280
409
  const footnoteById = /* @__PURE__ */ new Map();
281
410
  for (const fn of footnotes) if (fn.noteType === "normal") footnoteById.set(fn.id, fn);
282
- let displayNumber = 1;
283
- const seen = /* @__PURE__ */ new Set();
284
- for (const ref of footnoteRefs) {
285
- if (seen.has(ref.footnoteId)) continue;
286
- seen.add(ref.footnoteId);
287
- const footnote = footnoteById.get(ref.footnoteId);
411
+ const displayNumbers = computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId));
412
+ for (const [footnoteId, displayNumber] of displayNumbers) {
413
+ const footnote = footnoteById.get(footnoteId);
288
414
  if (!footnote) continue;
289
- const content = convertFootnoteToContent(footnote, displayNumber, contentWidth, options);
290
- contentMap.set(ref.footnoteId, content);
291
- displayNumber++;
415
+ contentMap.set(footnoteId, convertFootnoteToContent(footnote, displayNumber, contentWidth, options));
292
416
  }
293
417
  return contentMap;
294
418
  }
@@ -313,4 +437,4 @@ function calculateFootnoteReservedHeights(pageFootnoteMap, footnoteContentMap) {
313
437
  return reserved;
314
438
  }
315
439
  //#endregion
316
- export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectFootnoteRefs, convertFootnoteToContent, mapFootnotesToPages };
440
+ export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, convertFootnoteToContent, mapFootnotesToPages, remapNoteMarkerText };
@@ -20,13 +20,11 @@ declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures
20
20
  visualBottom: number;
21
21
  };
22
22
  /**
23
- * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender`
24
- * to push body margins clear of HF overflow. Excludes anchored/floating objects
25
- * (full-page letterheads, watermarks): Word positions them on the page instead
26
- * of in the header/footer flow, so they must not reserve body push-down. Keeping
27
- * them in `visualBottom` is still correct for the renderer (and for the
28
- * page-hash invalidation signal), but the margin extender needs the flow-only
29
- * extent.
23
+ * Compute flow-only header/footer bounds, excluding anchored/floating objects
24
+ * such as full-page letterheads and watermarks. Word positions those objects on
25
+ * the page independently from header/footer flow. Keeping them in
26
+ * `visualBottom` is still correct for rendering and page-hash invalidation;
27
+ * these bounds separately describe the in-flow portion for API consumers.
30
28
  */
31
29
  declare function calculateHeaderFooterMarginPushBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
32
30
  top: number;
@@ -225,13 +225,11 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
225
225
  };
226
226
  }
227
227
  /**
228
- * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender`
229
- * to push body margins clear of HF overflow. Excludes anchored/floating objects
230
- * (full-page letterheads, watermarks): Word positions them on the page instead
231
- * of in the header/footer flow, so they must not reserve body push-down. Keeping
232
- * them in `visualBottom` is still correct for the renderer (and for the
233
- * page-hash invalidation signal), but the margin extender needs the flow-only
234
- * extent.
228
+ * Compute flow-only header/footer bounds, excluding anchored/floating objects
229
+ * such as full-page letterheads and watermarks. Word positions those objects on
230
+ * the page independently from header/footer flow. Keeping them in
231
+ * `visualBottom` is still correct for rendering and page-hash invalidation;
232
+ * these bounds separately describe the in-flow portion for API consumers.
235
233
  */
236
234
  function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics) {
237
235
  let top = 0;
@@ -5,6 +5,7 @@ import { DEFAULT_TEXTBOX_MARGINS } from "../../layout-engine/types.js";
5
5
  import { resolveColor, resolveHighlightToCss } from "../../utils/colorResolver.js";
6
6
  import { resolveShadingFill } from "../../utils/formatToStyle.js";
7
7
  import { convertBulletToUnicode } from "../../docx/bulletMarkers.js";
8
+ import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js";
8
9
  import { directionIsRtl } from "../../prosemirror/paragraphDirection.js";
9
10
  import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js";
10
11
  import { autospacingMatchesBase } from "../../prosemirror/autospacingBase.js";
@@ -100,7 +101,7 @@ function toLetter(value, upper) {
100
101
  return String.fromCodePoint(baseCodePoint + zeroBased % 26).repeat(Math.floor(zeroBased / 26) + 1);
101
102
  }
102
103
  function formatCounter(value, format) {
103
- if (value <= 0) return "";
104
+ if (!Number.isFinite(value)) return "";
104
105
  switch (format) {
105
106
  case "upperRoman": return toRoman(value, true);
106
107
  case "lowerRoman": return toRoman(value, false);
@@ -119,7 +120,9 @@ function resolveListTemplate(template, counters, levelFormats, forceDecimal = fa
119
120
  const { digit, punct = "" } = args.at(-1);
120
121
  const index = Number.parseInt(digit, 10) - 1;
121
122
  if (index < 0) return "";
122
- const formatted = formatCounter(counters[index] ?? 0, forceDecimal ? "decimal" : levelFormats?.[index]);
123
+ const counter = counters[index];
124
+ if (counter === void 0 || Number.isNaN(counter)) return "";
125
+ const formatted = formatCounter(counter, forceDecimal ? "decimal" : levelFormats?.[index]);
123
126
  return formatted ? `${formatted}${punct}` : "";
124
127
  });
125
128
  }
@@ -143,22 +146,28 @@ function computeListMarker(pmAttrs, listCounters, abstractCounters, seenNumIds)
143
146
  }
144
147
  if (pmAttrs.listIsBullet) return convertBulletToUnicode(pmAttrs.listMarker || "");
145
148
  const level = pmAttrs.numPr?.ilvl ?? 0;
146
- const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => 0);
149
+ const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => NaN);
147
150
  const abstractNumId = pmAttrs.listAbstractNumId;
148
- if (abstractNumId !== void 0 && level > 0) {
149
- const latestAbstractCounters = abstractCounters.get(abstractNumId);
150
- const missingParentCounters = counters.slice(0, level).every((value) => value === 0);
151
- if (latestAbstractCounters && missingParentCounters) for (let i = 0; i < level; i += 1) counters[i] = latestAbstractCounters[i] ?? 0;
151
+ if (level > 0) {
152
+ const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId);
153
+ if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) {
154
+ const latestCounter = latestAbstractCounters?.[i];
155
+ counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1;
156
+ }
152
157
  }
153
158
  const seenKey = `${numId}:${level}`;
154
159
  if (!seenNumIds.has(seenKey)) {
155
160
  seenNumIds.add(seenKey);
156
161
  if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1;
162
+ else if (Number.isNaN(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1;
157
163
  }
158
164
  counters[level] = (counters[level] ?? 0) + 1;
159
- for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0;
165
+ for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN;
160
166
  const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0;
161
- if (childAdvances > 0 && level + 1 < counters.length) counters[level + 1] = (counters[level + 1] ?? 0) + childAdvances;
167
+ if (childAdvances > 0 && level + 1 < counters.length) {
168
+ const childCounter = counters[level + 1];
169
+ counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
170
+ }
162
171
  listCounters.set(numId, counters);
163
172
  if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]);
164
173
  const levelFormats = pmAttrs.listLevelNumFmts ?? (pmAttrs.listNumFmt ? [pmAttrs.listNumFmt] : void 0);
@@ -234,7 +243,7 @@ function extractRunFormatting(marks, theme) {
234
243
  }
235
244
  case "characterSpacing": {
236
245
  const attrs = expectCharacterSpacingMarkAttrs(mark);
237
- if (attrs.spacing !== void 0 && attrs.spacing !== 0) formatting.letterSpacing = twipsToPixels(attrs.spacing);
246
+ if (attrs.spacing !== void 0) formatting.letterSpacing = twipsToPixels(attrs.spacing);
238
247
  if (attrs.position !== void 0 && attrs.position !== 0) formatting.positionPx = halfPointsToPixels(attrs.position);
239
248
  if (attrs.scale !== void 0 && attrs.scale !== 100) formatting.horizontalScale = attrs.scale;
240
249
  if (attrs.kerning !== void 0 && attrs.kerning > 0) formatting.kerningMinPt = halfPointsToPoints(attrs.kerning);
@@ -336,10 +345,12 @@ function markDefaultBlackTextColorSource(formatting, paraDefaults) {
336
345
  };
337
346
  }
338
347
  function mergeRunFormatting(paraDefaults, formatting) {
339
- return {
348
+ const merged = {
340
349
  ...paraDefaults,
341
350
  ...markDefaultBlackTextColorSource(formatting, paraDefaults)
342
351
  };
352
+ if (merged.letterSpacing === 0) delete merged.letterSpacing;
353
+ return merged;
343
354
  }
344
355
  function applyRunFormattingOverrides(formatting, attrs) {
345
356
  if (attrs.bold === false) formatting.bold = false;
@@ -485,9 +496,10 @@ function paragraphToRuns(node, startPos, _options) {
485
496
  return;
486
497
  }
487
498
  if (child.type.name === "tab") {
499
+ const formatting = extractRunFormatting(child.marks, theme);
488
500
  const run = {
489
501
  kind: "tab",
490
- ...mergeRunFormatting(paraDefaults, extractRunFormatting(child.marks, theme)),
502
+ ...mergeRunFormatting(paraDefaults, formatting),
491
503
  pmStart: childPos,
492
504
  pmEnd: childPos + child.nodeSize
493
505
  };
@@ -496,7 +508,8 @@ function paragraphToRuns(node, startPos, _options) {
496
508
  }
497
509
  if (child.type.name === "image") {
498
510
  const attrs = expectImageAttrs(child);
499
- const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, _options.pageContentHeight);
511
+ if (!attrs.src) return;
512
+ const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, _options.pageContentHeight);
500
513
  const trackedFmt = extractRunFormatting(child.marks, theme);
501
514
  const run = buildImageRun(attrs, constrained, childPos, childPos + child.nodeSize, trackedFmt);
502
515
  runs.push(run);
@@ -613,6 +626,8 @@ function toPreviousListAttrs(previousFormatting) {
613
626
  if (isNumberFormat(listNumFmt)) attrs.listNumFmt = listNumFmt;
614
627
  const listLevelNumFmts = readNumberFormats(previousFormatting["listLevelNumFmts"]);
615
628
  if (listLevelNumFmts) attrs.listLevelNumFmts = listLevelNumFmts;
629
+ const listLevelStarts = previousFormatting["listLevelStarts"];
630
+ if (Array.isArray(listLevelStarts) && listLevelStarts.every((value) => typeof value === "number")) attrs.listLevelStarts = listLevelStarts;
616
631
  const listAbstractNumId = previousFormatting["listAbstractNumId"];
617
632
  if (typeof listAbstractNumId === "number") attrs.listAbstractNumId = listAbstractNumId;
618
633
  const listStartOverride = previousFormatting["listStartOverride"];
@@ -669,9 +684,10 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
669
684
  if (autoAfter) attrs.spacing.after = 14;
670
685
  else if (typeof spaceAfter === "number") attrs.spacing.after = twipsToPixels(spaceAfter);
671
686
  const pmSpacingExplicit = pmAttrs.spacingExplicit;
687
+ const spacingFromDocDefaults = pmAttrs.spacingFromDocDefaults;
672
688
  const explicit = {};
673
- if (autoBefore || pmSpacingExplicit?.before) explicit.before = true;
674
- if (autoAfter || pmSpacingExplicit?.after) explicit.after = true;
689
+ if (autoBefore || pmSpacingExplicit?.before || spacingFromDocDefaults?.before) explicit.before = true;
690
+ if (autoAfter || pmSpacingExplicit?.after || spacingFromDocDefaults?.after) explicit.after = true;
675
691
  if (explicit.before !== void 0 || explicit.after !== void 0) attrs.spacingExplicit = explicit;
676
692
  if (typeof lineSpacing === "number") if (pmAttrs.lineSpacingRule === "exact" || pmAttrs.lineSpacingRule === "atLeast") {
677
693
  attrs.spacing.line = twipsToPixels(lineSpacing);
@@ -730,6 +746,7 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
730
746
  return tabStop;
731
747
  });
732
748
  if (pmAttrs.pageBreakBefore) attrs.pageBreakBefore = true;
749
+ if (pmAttrs.renderedPageBreakBefore) attrs.renderedPageBreakBefore = true;
733
750
  if (pmAttrs.keepNext) attrs.keepNext = true;
734
751
  if (pmAttrs.keepLines) attrs.keepLines = true;
735
752
  if (pmAttrs.widowControl === false) attrs.widowControl = false;
@@ -809,6 +826,17 @@ function convertParagraph(node, startPos, options) {
809
826
  const pmAttrs = expectParagraphAttrs(node);
810
827
  const runs = paragraphToRuns(node, startPos, options);
811
828
  const attrs = convertParagraphAttrs(pmAttrs, options.theme, options.listCounters, options.listAbstractCounters, options.listSeenNumIds, options.defaultTabStopTwips, options.originalListCounters, options.originalListAbstractCounters, options.originalListSeenNumIds);
829
+ const defaultTextFormatting = pmAttrs.defaultTextFormatting;
830
+ if (runs.length === 0) {
831
+ const paragraphMarkFormatting = pmAttrs._originalFormatting?.runProperties;
832
+ if (paragraphMarkFormatting?.fontSize !== void 0) attrs.defaultFontSize = paragraphMarkFormatting.fontSize / 2;
833
+ const paragraphMarkFontFamily = paragraphMarkFormatting?.fontFamily?.ascii ?? paragraphMarkFormatting?.fontFamily?.hAnsi;
834
+ if (paragraphMarkFontFamily) attrs.defaultFontFamily = paragraphMarkFontFamily;
835
+ }
836
+ if (runs.length === 0 && defaultTextFormatting?.hidden === true) {
837
+ attrs.suppressEmptyParagraphHeight = true;
838
+ if (attrs.listMarker !== void 0) attrs.listMarkerHidden = true;
839
+ }
812
840
  const bookmarkNames = pmAttrs.bookmarks?.map((b) => b.name);
813
841
  return {
814
842
  kind: "paragraph",
@@ -893,6 +921,11 @@ function convertTableCell(node, startPos, options, tableCellMargins) {
893
921
  } else if (child.type.name === "table") blocks.push(convertTable(child, offset, options));
894
922
  offset += child.nodeSize;
895
923
  });
924
+ const trailingBlock = blocks.at(-1);
925
+ if (blocks.length > 1 && trailingBlock?.kind === "paragraph" && trailingBlock.runs.every((run) => run.kind === "text" && run.text.length === 0)) trailingBlock.attrs = {
926
+ ...trailingBlock.attrs,
927
+ suppressEmptyParagraphHeight: true
928
+ };
896
929
  const attrs = expectTableCellAttrs(node);
897
930
  const margins = attrs.margins;
898
931
  const resolvePaddingSide = (side, cellTwips, tableTwips) => {
@@ -993,6 +1026,7 @@ function convertTable(node, startPos, options) {
993
1026
  if (columnWidths) tableBlock.columnWidths = columnWidths;
994
1027
  if (width !== void 0) tableBlock.width = width;
995
1028
  if (widthType !== void 0) tableBlock.widthType = widthType;
1029
+ if (originalFormatting?.layout !== void 0) tableBlock.layout = originalFormatting.layout;
996
1030
  if (justification) tableBlock.justification = justification;
997
1031
  if (indentPx !== void 0) tableBlock.indent = indentPx;
998
1032
  if (floatingPx) tableBlock.floating = floatingPx;
@@ -1006,7 +1040,7 @@ function convertImage(node, startPos, pageContentHeight) {
1006
1040
  const attrs = expectImageAttrs(node);
1007
1041
  const wrapType = attrs.wrapType;
1008
1042
  const shouldAnchor = wrapType === "behind" || wrapType === "inFront";
1009
- const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, pageContentHeight);
1043
+ const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, pageContentHeight);
1010
1044
  const imgBlock = {
1011
1045
  kind: "image",
1012
1046
  id: nextBlockId(),
@@ -1075,6 +1109,7 @@ function convertTextBoxNode(node, startPos, opts) {
1075
1109
  if (attrs.distLeft !== void 0) textBox.distLeft = attrs.distLeft;
1076
1110
  if (attrs.distRight !== void 0) textBox.distRight = attrs.distRight;
1077
1111
  if (attrs.position !== void 0) textBox.position = attrs.position;
1112
+ if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId);
1078
1113
  return textBox;
1079
1114
  }
1080
1115
  /**
@@ -1235,14 +1270,16 @@ function toFlowBlocks(doc, options = {}) {
1235
1270
  trackedPush(convertTextBoxNode(node, pos, opts));
1236
1271
  break;
1237
1272
  case "horizontalRule":
1238
- case "pageBreak":
1239
- trackedPush({
1273
+ case "pageBreak": {
1274
+ const pb = {
1240
1275
  kind: "pageBreak",
1241
1276
  id: nextBlockId(),
1242
1277
  pmStart: pos,
1243
1278
  pmEnd: pos + node.nodeSize
1244
- });
1279
+ };
1280
+ trackedPush(pb);
1245
1281
  break;
1282
+ }
1246
1283
  default: break;
1247
1284
  }
1248
1285
  };
@@ -1,4 +1,4 @@
1
- import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js";
1
+ import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js";
2
2
  import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js";
3
3
  import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
4
4
  import { BreakDecision, SectionState, applyPendingToActive, createInitialSectionState, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, scheduleSectionBreak } from "./section-breaks.js";
@@ -20,9 +20,10 @@ declare function collectSectionConfigs(blocks: FlowBlock[], initialConfig: Secti
20
20
  /**
21
21
  * Apply contextual spacing suppression (OOXML §17.3.1.9).
22
22
  *
23
- * When two consecutive paragraph blocks both have `contextualSpacing: true`
24
- * and share the same `styleId`, the spaceAfter of the first paragraph and
25
- * the spaceBefore of the second paragraph are suppressed (set to 0).
23
+ * Contextual spacing applies independently to each paragraph: suppress the
24
+ * current paragraph's spaceAfter when it opts in, and the next paragraph's
25
+ * spaceBefore when it opts in, provided both paragraphs share a style. Two
26
+ * absent style ids both refer to the document's default paragraph style.
26
27
  *
27
28
  * This mutates the block attrs in-place before layout runs.
28
29
  */
@@ -42,4 +43,4 @@ declare function layoutDocument(blocks: FlowBlock[], measures: Measure[], option
42
43
  */
43
44
  declare function getHeaderRowsHeight(measure: TableMeasure, headerRowCount: number): number;
44
45
  //#endregion
45
- export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak };
46
+ export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned };