@stll/folio-core 0.28.0 → 0.28.1

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 (28) hide show
  1. package/dist/docx/styleParser.js +15 -14
  2. package/dist/layout-bridge/convert/toFlowBlocks.js +74 -23
  3. package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
  4. package/dist/layout-engine/footnoteColumnReflow.js +74 -0
  5. package/dist/layout-engine/index.d.ts +3 -5
  6. package/dist/layout-engine/index.js +36 -15
  7. package/dist/layout-engine/measure/cache.d.ts +1 -1
  8. package/dist/layout-engine/measure/cache.js +11 -2
  9. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  10. package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
  11. package/dist/layout-engine/paginator.d.ts +6 -0
  12. package/dist/layout-engine/paginator.js +30 -11
  13. package/dist/layout-engine/types.d.ts +11 -6
  14. package/dist/prosemirror/attrs/index.js +3 -2
  15. package/dist/prosemirror/commands/formatPainter.js +45 -1
  16. package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
  17. package/dist/prosemirror/conversion/fromProseDoc.js +59 -44
  18. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
  19. package/dist/prosemirror/conversion/toProseDoc.js +156 -33
  20. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +22 -22
  21. package/dist/prosemirror/schema/marks.d.ts +3 -1
  22. package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
  23. package/dist/prosemirror/styles/styleResolver.js +22 -29
  24. package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
  25. package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
  26. package/dist/utils/textFormattingMerge.d.ts +9 -1
  27. package/dist/utils/textFormattingMerge.js +32 -1
  28. package/package.json +1 -1
@@ -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 footnoteHeight = options.footnoteReservedHeights?.get(pageNumber) ?? 0;
103
- const pageContentBottom = contentBottom - footnoteHeight;
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
- ...footnoteHeight > 0 ? { footnoteReservedHeight: footnoteHeight } : {},
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.page.fragments.push(fragment);
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.footnoteHeight === 0 ? 12 : 0;
233
- state.footnoteHeight += additionalHeight + separatorOverhead;
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 footnoteHeight = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
279
+ const footnoteHeightFloor = options.footnoteReservedHeights?.get(current.page.number) ?? 0;
265
280
  current.page.size = { ...pageSize };
266
281
  current.page.margins = pageMargins;
267
- if (footnoteHeight > 0) current.page.footnoteReservedHeight = footnoteHeight;
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 = footnoteHeight;
276
- current.contentBottom = rawContentBottom - footnoteHeight;
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
- /** Per-page footnote reserved heights (pageNumber → height in pixels). */
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. This single-pass approach avoids the static-
1237
- * reservation + iterative-convergence loop that produced oscillation
1238
- * (and either body-overflow into the footer or large empty gaps
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. */
@@ -77,11 +77,10 @@ const TEXT_BOX_TRACKED_CHANGE_TYPES = [
77
77
  "moveTo"
78
78
  ];
79
79
  const HARD_BREAK_TYPES = ["column"];
80
- const RUN_FORMATTING_OVERRIDE_FALSE_KEYS = [
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",
@@ -659,6 +659,7 @@ const readRunFormattingOverrideMarkAttrs = (mark) => {
659
659
  const attrs = attrsRecord(mark.attrs);
660
660
  const issues = [];
661
661
  expectMarkType(mark, "runFormattingOverride", issues);
662
+ for (const key of RUN_FORMATTING_OVERRIDE_BOOLEAN_KEYS) optionalBoolean(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
662
663
  for (const key of RUN_FORMATTING_OVERRIDE_FALSE_KEYS) optionalFalse(attrs, key, `runFormattingOverride.attrs.${key}`, issues);
663
664
  optionalBoolean(attrs, "boldCs", "runFormattingOverride.attrs.boldCs", issues);
664
665
  optionalBoolean(attrs, "italicCs", "runFormattingOverride.attrs.italicCs", issues);
@@ -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 captured;
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
- declare function marksToTextFormatting(marks: readonly Mark[]): document_d_exports.TextFormatting;
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
@@ -8,6 +8,7 @@ import { applyRunFormattingOverrideAttrs } from "../extensions/marks/RunFormatti
8
8
  import { directionToBidi } from "../paragraphDirection.js";
9
9
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
10
10
  import { parseShapeGeometryAdjustments } from "../shapeGeometryAdjustments.js";
11
+ import { cascadeStyleTextFormatting } from "../styles/styleToggleCascade.js";
11
12
  import { expectTextBoxAnchorAttrs } from "../textBoxAnchorAttrs.js";
12
13
  import { assertValidProseMirrorDocument } from "../validation.js";
13
14
  import { runShadingAttrsToShading } from "./runShadingMark.js";
@@ -647,6 +648,7 @@ function paragraphAttrsToFormatting(attrs) {
647
648
  */
648
649
  function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false) {
649
650
  const content = [];
651
+ const inheritedFormatting = paragraph.type.name === "paragraph" ? expectParagraphAttrs(paragraph).defaultTextFormatting ?? void 0 : void 0;
650
652
  const sortedEmptyHyperlinks = (emptyHyperlinks ?? []).map((attrs, order) => ({
651
653
  attrs,
652
654
  order
@@ -763,7 +765,12 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
763
765
  const changeMark = insertionMark ?? deletionMark;
764
766
  if (!changeMark) return;
765
767
  const changeAttrs = expectTrackedChangeMarkAttrs(changeMark);
766
- const run = createTrackedChangeRun(node, node.marks.filter((m) => m.type.name !== "insertion" && m.type.name !== "deletion"));
768
+ const otherMarks = node.marks.filter((m) => m.type.name !== "insertion" && m.type.name !== "deletion");
769
+ const run = createTrackedChangeRun({
770
+ inheritedFormatting,
771
+ marks: otherMarks,
772
+ node
773
+ });
767
774
  if (!run) return;
768
775
  const trackedContent = linkMark ? {
769
776
  ...createHyperlink(linkMark),
@@ -809,7 +816,11 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
809
816
  currentHyperlink = createHyperlink(linkMark);
810
817
  currentHyperlinkKey = linkKey;
811
818
  }
812
- addNodeToHyperlink(currentHyperlink, node);
819
+ addNodeToHyperlink({
820
+ hyperlink: currentHyperlink,
821
+ inheritedFormatting,
822
+ node
823
+ });
813
824
  return;
814
825
  }
815
826
  if (currentHyperlink) flushCurrentInline();
@@ -818,7 +829,11 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
818
829
  if (currentRun && currentMarksKey === marksKey) appendTextToRun(currentRun, node.text || "");
819
830
  else {
820
831
  if (currentRun) content.push(currentRun);
821
- currentRun = createRunFromText(node.text || "", node.marks);
832
+ currentRun = createRunFromText({
833
+ inheritedFormatting,
834
+ marks: node.marks,
835
+ text: node.text || ""
836
+ });
822
837
  currentMarksKey = marksKey;
823
838
  }
824
839
  } else if (node.type.name === "symbol") {
@@ -880,11 +895,11 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
880
895
  });
881
896
  return content;
882
897
  }
883
- function createTrackedChangeRun(node, marks) {
898
+ function createTrackedChangeRun({ inheritedFormatting, marks, node }) {
884
899
  const noteRefMark = marks.find((mark) => mark.type.name === "footnoteRef");
885
900
  if (noteRefMark) return createNoteReferenceRun(noteRefMark, marks);
886
901
  if (node.isText) {
887
- const formatting = marksToTextFormatting(marks);
902
+ const formatting = marksToTextFormatting(marks, { inheritedFormatting });
888
903
  const run = {
889
904
  type: "run",
890
905
  content: node.text ? [{
@@ -989,10 +1004,7 @@ function createHyperlink(linkMark) {
989
1004
  if (attrs.rId) hyperlink.rId = attrs.rId;
990
1005
  return hyperlink;
991
1006
  }
992
- /**
993
- * Add a node to a hyperlink
994
- */
995
- function addNodeToHyperlink(hyperlink, node) {
1007
+ function addNodeToHyperlink({ hyperlink, inheritedFormatting, node }) {
996
1008
  const noteRefMark = node.marks.find((m) => m.type.name === "footnoteRef");
997
1009
  if (noteRefMark) {
998
1010
  hyperlink.children.push(createNoteReferenceRun(noteRefMark, node.marks));
@@ -1000,7 +1012,11 @@ function addNodeToHyperlink(hyperlink, node) {
1000
1012
  }
1001
1013
  const nonLinkMarks = node.marks.filter((m) => m.type.name !== "hyperlink");
1002
1014
  if (node.isText && node.text) {
1003
- const run = createRunFromText(node.text, nonLinkMarks);
1015
+ const run = createRunFromText({
1016
+ inheritedFormatting,
1017
+ marks: nonLinkMarks,
1018
+ text: node.text
1019
+ });
1004
1020
  hyperlink.children.push(run);
1005
1021
  return;
1006
1022
  }
@@ -1044,11 +1060,8 @@ function createNoteReferenceRun(noteRefMark, marks) {
1044
1060
  if (vertAlign) run.formatting = { vertAlign };
1045
1061
  return run;
1046
1062
  }
1047
- /**
1048
- * Create a Run from text and marks
1049
- */
1050
- function createRunFromText(text, marks) {
1051
- const formatting = getRunFormattingFromMarks(marks);
1063
+ function createRunFromText({ inheritedFormatting, marks, text }) {
1064
+ const formatting = getRunFormattingFromMarks(marks, { inheritedFormatting });
1052
1065
  const run = {
1053
1066
  type: "run",
1054
1067
  content: [{
@@ -1082,9 +1095,9 @@ function restoreRunPropertyChanges(run, marks) {
1082
1095
  if (changes.length === 0) return;
1083
1096
  run.propertyChanges = [...changes];
1084
1097
  }
1085
- function getRunFormattingFromMarks(marks) {
1098
+ function getRunFormattingFromMarks(marks, options) {
1086
1099
  if (!marks || marks.length === 0) return;
1087
- const formatting = marksToTextFormatting(marks);
1100
+ const formatting = marksToTextFormatting(marks, options);
1088
1101
  return Object.keys(formatting).length > 0 ? formatting : void 0;
1089
1102
  }
1090
1103
  /**
@@ -1354,11 +1367,9 @@ function createShapeRun(node) {
1354
1367
  }]
1355
1368
  };
1356
1369
  }
1357
- /**
1358
- * Convert ProseMirror marks to TextFormatting
1359
- */
1360
- function marksToTextFormatting(marks) {
1370
+ function marksToTextFormatting(marks, options) {
1361
1371
  const formatting = {};
1372
+ let directOverrideFormatting;
1362
1373
  let characterStyleRPr;
1363
1374
  let runFormattingOverrideMark;
1364
1375
  for (const mark of marks) switch (mark.type.name) {
@@ -1483,8 +1494,18 @@ function marksToTextFormatting(marks) {
1483
1494
  }
1484
1495
  default: break;
1485
1496
  }
1486
- if (runFormattingOverrideMark) applyRunFormattingOverrideAttrs(formatting, expectRunFormattingOverrideMarkAttrs(runFormattingOverrideMark));
1487
- if (characterStyleRPr) return subtractCharacterStyleFormatting(formatting, characterStyleRPr);
1497
+ if (runFormattingOverrideMark) {
1498
+ const overrideAttrs = expectRunFormattingOverrideMarkAttrs(runFormattingOverrideMark);
1499
+ applyRunFormattingOverrideAttrs(formatting, overrideAttrs);
1500
+ directOverrideFormatting = {};
1501
+ applyRunFormattingOverrideAttrs(directOverrideFormatting, overrideAttrs);
1502
+ }
1503
+ if (characterStyleRPr) return subtractCharacterStyleFormatting({
1504
+ directOverrideFormatting,
1505
+ formatting,
1506
+ inheritedFormatting: options?.inheritedFormatting,
1507
+ styleRPr: characterStyleRPr
1508
+ });
1488
1509
  return formatting;
1489
1510
  }
1490
1511
  /**
@@ -1509,35 +1530,29 @@ const NEGATABLE_STYLE_KEYS = [
1509
1530
  ["outline", null],
1510
1531
  ["rtl", null]
1511
1532
  ];
1512
- /**
1513
- * Drop run formatting values that the run's character style already provides
1514
- * (the `w:rStyle` reference re-imposes them on load), so a styled run
1515
- * serializes back to a style reference instead of baked direct formatting.
1516
- *
1517
- * `styleRPr` is the load-time snapshot from the characterStyle mark, captured
1518
- * in the same normal form this module produces from marks, so value equality
1519
- * is a faithful "came from the style and is unchanged" check. Values that
1520
- * differ (user edits, direct overrides from the source document) stay as
1521
- * direct formatting, which wins over the style per the OOXML cascade.
1522
- *
1523
- * When the user *removes* a boolean property the style supplied (e.g. toggling
1524
- * off bold on text whose `w:rStyle` is bold), that key is no longer present in
1525
- * `formatting`, so the subtraction loop above never sees it. Without a counter,
1526
- * the run keeps the style reference and Word re-applies the removed formatting
1527
- * on load. The second pass emits an explicit negative override (`false`) for
1528
- * each negatable boolean the style set to `true` but the run no longer carries.
1529
- */
1530
- function subtractCharacterStyleFormatting(formatting, styleRPr) {
1533
+ function subtractCharacterStyleFormatting({ directOverrideFormatting, formatting, inheritedFormatting, styleRPr }) {
1531
1534
  const result = {};
1535
+ const effectiveStyleFormatting = cascadeStyleTextFormatting([{
1536
+ formatting: inheritedFormatting,
1537
+ type: "direct"
1538
+ }, {
1539
+ formatting: styleRPr,
1540
+ type: "style"
1541
+ }]).formatting;
1532
1542
  for (const key of Object.keys(formatting)) {
1533
1543
  const value = formatting[key];
1534
1544
  if (value === void 0) continue;
1535
- const styleValue = key === "styleId" ? void 0 : styleRPr[key];
1545
+ if (directOverrideFormatting?.[key] !== void 0) {
1546
+ result[key] = value;
1547
+ continue;
1548
+ }
1549
+ const styleValue = key === "styleId" ? void 0 : effectiveStyleFormatting?.[key];
1550
+ if ((key === "boldCs" || key === "italicCs") && styleValue === false && value === true) continue;
1536
1551
  if (styleValue !== void 0 && JSON.stringify(value) === JSON.stringify(styleValue)) continue;
1537
1552
  result[key] = value;
1538
1553
  }
1539
1554
  for (const [key, csKey] of NEGATABLE_STYLE_KEYS) {
1540
- if (styleRPr[key] !== true || formatting[key] !== void 0) continue;
1555
+ if (effectiveStyleFormatting?.[key] !== true || formatting[key] !== void 0) continue;
1541
1556
  result[key] = false;
1542
1557
  if (csKey !== null) result[csKey] = false;
1543
1558
  }
@@ -22,7 +22,10 @@ declare function standaloneTableCellToProseMirror(cell: document_d_exports.Table
22
22
  /**
23
23
  * Convert TextFormatting to ProseMirror marks
24
24
  */
25
- declare function textFormattingToMarks(formatting: document_d_exports.TextFormatting | undefined): ReturnType<typeof schema.mark>[];
25
+ type TextFormattingToMarksOptions = {
26
+ overrideFormatting: document_d_exports.TextFormatting | undefined;
27
+ };
28
+ declare function textFormattingToMarks(formatting: document_d_exports.TextFormatting | undefined, options?: TextFormattingToMarksOptions): ReturnType<typeof schema.mark>[];
26
29
  /**
27
30
  * Convert HeaderFooter content (array of Paragraph/Table blocks) to a ProseMirror document.
28
31
  * Used for editing headers/footers in their own ProseMirror editor and for