@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
@@ -8,6 +8,7 @@ import { setAutospacingBaseValue } from "../autospacingBase.js";
8
8
  import { buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
9
9
  import { directionFromBidi } from "../paragraphDirection.js";
10
10
  import { schema } from "../schema/index.js";
11
+ import { cascadeStyleTextFormatting } from "../styles/styleToggleCascade.js";
11
12
  import { assertValidProseMirrorDocument } from "../validation.js";
12
13
  import { resolveEffectiveTableCellFormatting } from "./effectiveTableCellFormatting.js";
13
14
  import { marksToTextFormatting } from "./fromProseDoc.js";
@@ -144,22 +145,66 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
144
145
  emitInlineNodes([node]);
145
146
  };
146
147
  let styleRunFormatting;
148
+ let paragraphStyleRunFormatting;
147
149
  let paragraphStyleFontFamily;
148
150
  if (styleResolver) {
149
151
  styleRunFormatting = styleResolver.resolveParagraphStyle(paragraph.formatting?.styleId).runFormatting;
152
+ const paragraphStyle = paragraph.formatting?.styleId ? styleResolver.getStyle(paragraph.formatting.styleId) ?? styleResolver.getDefaultParagraphStyle() : styleResolver.getDefaultParagraphStyle();
153
+ paragraphStyleRunFormatting = paragraphStyle?.type === "paragraph" ? paragraphStyle.rPr : void 0;
150
154
  paragraphStyleFontFamily = resolveParagraphStyleFontFamily(paragraph.formatting?.styleId, styleResolver);
151
155
  }
152
156
  const paragraphRunFormatting = resolveRunFormattingWithoutDefaults(paragraph.formatting?.runProperties, styleResolver);
153
157
  let inheritableParagraphRunFormatting;
154
158
  if (paragraphRunFormatting && !isTocParagraph) inheritableParagraphRunFormatting = stripParagraphMarkFormattingForBodyRuns(paragraphRunFormatting);
155
- let baseRunFormatting = mergeTextFormatting(styleRunFormatting, extraRunFormatting);
159
+ const orderedToggleFormatting = cascadeStyleTextFormatting([
160
+ {
161
+ formatting: styleResolver?.getDocDefaults()?.rPr,
162
+ type: "defaults"
163
+ },
164
+ {
165
+ formatting: extraRunFormatting,
166
+ type: "style"
167
+ },
168
+ {
169
+ formatting: paragraphStyleRunFormatting,
170
+ type: "style"
171
+ }
172
+ ], { ordinaryFormatting: mergeTextFormatting(styleRunFormatting, extraRunFormatting) });
173
+ let baseRunFormatting = orderedToggleFormatting.formatting;
156
174
  if (paragraphStyleFontFamily) baseRunFormatting = mergeTextFormatting(baseRunFormatting, { fontFamily: paragraphStyleFontFamily });
157
175
  const paragraphMarkPrecedesStyle = paragraph.formatting?.styleId !== void 0;
158
- const defaultRunFormatting = paragraphMarkPrecedesStyle ? mergeTextFormatting(inheritableParagraphRunFormatting, baseRunFormatting) : mergeTextFormatting(baseRunFormatting, inheritableParagraphRunFormatting);
176
+ const ordinaryDefaultRunFormatting = paragraphMarkPrecedesStyle ? mergeTextFormatting(inheritableParagraphRunFormatting, baseRunFormatting) : mergeTextFormatting(baseRunFormatting, inheritableParagraphRunFormatting);
177
+ const defaultToggleCascade = paragraphMarkPrecedesStyle ? cascadeStyleTextFormatting([{
178
+ cascade: orderedToggleFormatting,
179
+ type: "carried"
180
+ }], { ordinaryFormatting: ordinaryDefaultRunFormatting }) : cascadeStyleTextFormatting([{
181
+ cascade: orderedToggleFormatting,
182
+ type: "carried"
183
+ }, {
184
+ formatting: inheritableParagraphRunFormatting,
185
+ type: "direct"
186
+ }], { ordinaryFormatting: ordinaryDefaultRunFormatting });
187
+ const defaultRunFormatting = defaultToggleCascade.formatting;
159
188
  const getInheritedRunFormatting = (formatting, fieldType) => {
160
- if (fieldType === "TOC") return hasDirectRunFormatting(formatting) ? suppressParagraphMarkFormatting(baseRunFormatting, void 0, formatting) : baseRunFormatting;
161
- if (!(hasDirectRunFormatting(formatting) || formatting?.styleId !== void 0)) return defaultRunFormatting;
162
- return suppressParagraphMarkFormatting(baseRunFormatting, inheritableParagraphRunFormatting, formatting, paragraphMarkPrecedesStyle);
189
+ if (fieldType === "TOC") return {
190
+ formatting: hasDirectRunFormatting(formatting) ? suppressParagraphMarkFormatting(baseRunFormatting, void 0, formatting) : baseRunFormatting,
191
+ toggleCascade: orderedToggleFormatting
192
+ };
193
+ if (!(hasDirectRunFormatting(formatting) || formatting?.styleId !== void 0)) return {
194
+ formatting: defaultRunFormatting,
195
+ toggleCascade: defaultToggleCascade
196
+ };
197
+ const suppressedFormatting = suppressParagraphMarkFormatting(baseRunFormatting, inheritableParagraphRunFormatting, formatting, paragraphMarkPrecedesStyle);
198
+ const paragraphMarkOverrides = getParagraphMarkSuppressionOverrides({
199
+ directFormatting: formatting,
200
+ paragraphMarkFormatting: inheritableParagraphRunFormatting,
201
+ suppressedFormatting
202
+ });
203
+ return {
204
+ formatting: suppressedFormatting,
205
+ ...paragraphMarkOverrides ? { paragraphMarkOverrides } : {},
206
+ toggleCascade: orderedToggleFormatting
207
+ };
163
208
  };
164
209
  const emitTrackedChange = (change, markType, moveKind) => {
165
210
  emitInlineNodes(convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind, textBoxAnchors));
@@ -499,27 +544,37 @@ function stripParagraphMarkFormattingForBodyRuns(formatting) {
499
544
  const { fontFamily: _fontFamily, ...bodyRunFormatting } = paragraphMarkFormatting;
500
545
  return Object.keys(bodyRunFormatting).length > 0 ? bodyRunFormatting : void 0;
501
546
  }
547
+ const PARAGRAPH_MARK_BOOLEAN_KEYS = [
548
+ "bold",
549
+ "italic",
550
+ "strike",
551
+ "doubleStrike",
552
+ "allCaps",
553
+ "smallCaps",
554
+ "hidden",
555
+ "emboss",
556
+ "imprint",
557
+ "shadow",
558
+ "outline",
559
+ "rtl"
560
+ ];
502
561
  function suppressParagraphMarkFormatting(base, paragraphMark, direct, paragraphMarkPrecedesStyle = false) {
503
562
  if (!paragraphMark) return base;
504
563
  const result = (paragraphMarkPrecedesStyle ? mergeTextFormatting(paragraphMark, base) : mergeTextFormatting(base, paragraphMark)) ?? {};
505
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "bold");
506
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "italic");
507
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "strike");
508
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "doubleStrike");
509
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "allCaps");
510
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "smallCaps");
511
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "hidden");
512
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "emboss");
513
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "imprint");
514
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "shadow");
515
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "outline");
516
- suppressBooleanParagraphMark(result, base, paragraphMark, direct, "rtl");
564
+ for (const key of PARAGRAPH_MARK_BOOLEAN_KEYS) suppressBooleanParagraphMark(result, base, paragraphMark, direct, key);
517
565
  if (paragraphMark.fontSize !== void 0 && direct?.fontSize === void 0 && base?.fontSize !== void 0) result.fontSize = base.fontSize;
518
566
  if (paragraphMark.fontSizeCs !== void 0 && direct?.fontSizeCs === void 0 && base?.fontSizeCs !== void 0) result.fontSizeCs = base.fontSizeCs;
519
567
  if (paragraphMark.underline !== void 0 && direct?.underline === void 0) result.underline = { style: "none" };
520
568
  if (paragraphMark.spacing !== void 0 && direct?.spacing === void 0) result.spacing = 0;
521
569
  return Object.keys(result).length > 0 ? result : void 0;
522
570
  }
571
+ function getParagraphMarkSuppressionOverrides({ directFormatting, paragraphMarkFormatting, suppressedFormatting }) {
572
+ if (!paragraphMarkFormatting) return;
573
+ const overrides = {};
574
+ for (const key of PARAGRAPH_MARK_BOOLEAN_KEYS) if (paragraphMarkFormatting[key] !== void 0 && directFormatting?.[key] === void 0 && suppressedFormatting?.[key] === false) overrides[key] = false;
575
+ if (paragraphMarkFormatting.underline !== void 0 && directFormatting?.underline === void 0 && suppressedFormatting?.underline?.style === "none") overrides.underline = { style: "none" };
576
+ return Object.keys(overrides).length > 0 ? overrides : void 0;
577
+ }
523
578
  function suppressBooleanParagraphMark(result, base, paragraphMark, direct, key) {
524
579
  if (paragraphMark[key] === void 0 || direct?.[key] !== void 0) return;
525
580
  result[key] = base?.[key] ?? false;
@@ -536,15 +591,45 @@ function resolveTextFormatting(formatting, styleResolver) {
536
591
  */
537
592
  function resolveRunFormattingWithoutDefaults(formatting, styleResolver) {
538
593
  if (!formatting || !styleResolver) return formatting;
539
- return mergeTextFormatting(formatting.styleId ? styleResolver.getRunStyleOwnProperties(formatting.styleId) : void 0, formatting);
594
+ return cascadeStyleTextFormatting([{
595
+ formatting: formatting.styleId ? styleResolver.getRunStyleOwnProperties(formatting.styleId) : void 0,
596
+ type: "style"
597
+ }, {
598
+ formatting,
599
+ type: "direct"
600
+ }]).formatting;
540
601
  }
541
602
  function resolveParagraphDefaultTextFormatting(styleId, formatting, styleResolver, options = {}) {
542
603
  const style = styleId ? styleResolver.getStyle(styleId) ?? styleResolver.getDefaultParagraphStyle() : styleResolver.getDefaultParagraphStyle();
543
604
  const paragraphStyleRpr = style?.type === "paragraph" ? style.rPr : void 0;
544
605
  const rawParagraphMarkRpr = options.includeParagraphMarkRunProperties === false ? void 0 : formatting?.runProperties;
545
606
  const paragraphRunProperties = rawParagraphMarkRpr ? stripParagraphMarkOnlyFormatting(resolveRunFormattingWithoutDefaults(rawParagraphMarkRpr, styleResolver) ?? {}) : void 0;
546
- const bodyRunDefaults = mergeTextFormatting(mergeTextFormatting(styleResolver.getDocDefaults()?.rPr, styleResolver.getDefaultCharacterStyle()?.rPr), paragraphStyleRpr);
547
- return styleId === void 0 ? mergeTextFormatting(bodyRunDefaults, paragraphRunProperties) : mergeTextFormatting(paragraphRunProperties, bodyRunDefaults);
607
+ const orderedBodyToggleFormatting = cascadeStyleTextFormatting([
608
+ {
609
+ formatting: styleResolver.getDocDefaults()?.rPr,
610
+ type: "defaults"
611
+ },
612
+ {
613
+ formatting: paragraphStyleRpr,
614
+ type: "style"
615
+ },
616
+ {
617
+ formatting: styleResolver.getDefaultCharacterStyle()?.rPr,
618
+ type: "style"
619
+ }
620
+ ], { ordinaryFormatting: mergeTextFormatting(mergeTextFormatting(styleResolver.getDocDefaults()?.rPr, styleResolver.getDefaultCharacterStyle()?.rPr), paragraphStyleRpr) });
621
+ const bodyRunDefaults = orderedBodyToggleFormatting.formatting;
622
+ if (styleId !== void 0) return cascadeStyleTextFormatting([{
623
+ cascade: orderedBodyToggleFormatting,
624
+ type: "carried"
625
+ }], { ordinaryFormatting: mergeTextFormatting(paragraphRunProperties, bodyRunDefaults) }).formatting;
626
+ return cascadeStyleTextFormatting([{
627
+ cascade: orderedBodyToggleFormatting,
628
+ type: "carried"
629
+ }, {
630
+ formatting: paragraphRunProperties,
631
+ type: "direct"
632
+ }], { ordinaryFormatting: mergeTextFormatting(bodyRunDefaults, paragraphRunProperties) }).formatting;
548
633
  }
549
634
  function calculateRowSpans(table) {
550
635
  const result = /* @__PURE__ */ new Map();
@@ -1012,11 +1097,11 @@ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, getInheritedRunFormat
1012
1097
  * Convert a Run to ProseMirror text nodes with marks
1013
1098
  *
1014
1099
  * @param run - The run to convert
1015
- * @param styleFormatting - Text formatting from the paragraph's style (e.g., Heading1's font size/color)
1100
+ * @param resolvedStyleFormatting - Paragraph formatting and its explicit style-toggle state
1016
1101
  */
1017
- function convertRun(run, styleFormatting, styleResolver, textBoxAnchors) {
1102
+ function convertRun(run, resolvedStyleFormatting, styleResolver, textBoxAnchors) {
1018
1103
  const nodes = [];
1019
- const { marks, mergedFormatting } = buildRunMarks(run.formatting, styleFormatting, styleResolver);
1104
+ const { marks, mergedFormatting } = buildRunMarks(run.formatting, resolvedStyleFormatting, styleResolver);
1020
1105
  if (run.propertyChanges && run.propertyChanges.length > 0) marks.push(schema.mark("runPropertyChange", { changes: [...run.propertyChanges] }));
1021
1106
  for (const content of run.content) {
1022
1107
  const contentNodes = convertRunContent(content, marks, mergedFormatting, textBoxAnchors);
@@ -1024,13 +1109,34 @@ function convertRun(run, styleFormatting, styleResolver, textBoxAnchors) {
1024
1109
  }
1025
1110
  return nodes;
1026
1111
  }
1027
- function buildRunMarks(runFormatting, inheritedFormatting, styleResolver) {
1112
+ function buildRunMarks(runFormatting, inherited, styleResolver) {
1028
1113
  const styleId = runFormatting?.styleId;
1029
- const runStyleFormatting = styleId ? styleResolver?.getRunStyleOwnProperties(styleId) : void 0;
1030
- const mergedFormatting = mergeTextFormatting(mergeTextFormatting(inheritedFormatting, runStyleFormatting), runFormatting);
1031
- const marks = textFormattingToMarks(mergedFormatting);
1114
+ const characterStyleFormatting = styleId ? styleResolver?.getRunStyleOwnProperties(styleId) : styleResolver?.getDefaultCharacterStyle()?.rPr;
1115
+ let ordinaryRunStyleFormatting = inherited.formatting ? { ...inherited.formatting } : {};
1116
+ if (styleId) ordinaryRunStyleFormatting = mergeTextFormatting(inherited.formatting, characterStyleFormatting) ?? {};
1117
+ const cascadedStyleFormatting = cascadeStyleTextFormatting([{
1118
+ cascade: inherited.toggleCascade,
1119
+ type: "carried"
1120
+ }, {
1121
+ formatting: characterStyleFormatting,
1122
+ type: "style"
1123
+ }], { ordinaryFormatting: ordinaryRunStyleFormatting });
1124
+ const runStyleFormatting = cascadedStyleFormatting.formatting;
1125
+ const mergedFormatting = cascadeStyleTextFormatting([{
1126
+ cascade: cascadedStyleFormatting,
1127
+ type: "carried"
1128
+ }, {
1129
+ formatting: runFormatting,
1130
+ type: "direct"
1131
+ }], { ordinaryFormatting: mergeTextFormatting(runStyleFormatting, runFormatting) }).formatting;
1132
+ const marks = textFormattingToMarks(mergedFormatting, { overrideFormatting: getRunFormattingOverrides({
1133
+ directFormatting: runFormatting,
1134
+ effectiveStyleFormatting: runStyleFormatting,
1135
+ hasCharacterStyle: styleId !== void 0,
1136
+ paragraphMarkOverrides: inherited.paragraphMarkOverrides
1137
+ }) });
1032
1138
  if (styleId) {
1033
- const styleRPr = runStyleFormatting ? marksToTextFormatting(textFormattingToMarks(runStyleFormatting)) : void 0;
1139
+ const styleRPr = characterStyleFormatting ? marksToTextFormatting(textFormattingToMarks(characterStyleFormatting)) : void 0;
1034
1140
  marks.push(schema.mark("characterStyle", {
1035
1141
  styleId,
1036
1142
  _styleRPr: styleRPr && Object.keys(styleRPr).length > 0 ? styleRPr : null
@@ -1041,6 +1147,26 @@ function buildRunMarks(runFormatting, inheritedFormatting, styleResolver) {
1041
1147
  mergedFormatting
1042
1148
  };
1043
1149
  }
1150
+ const ORDINARY_STYLE_TOGGLE_KEYS = [
1151
+ "bold",
1152
+ "italic",
1153
+ "strike",
1154
+ "allCaps",
1155
+ "smallCaps",
1156
+ "hidden",
1157
+ "emboss",
1158
+ "imprint",
1159
+ "shadow",
1160
+ "outline"
1161
+ ];
1162
+ function getRunFormattingOverrides({ directFormatting, effectiveStyleFormatting, hasCharacterStyle, paragraphMarkOverrides }) {
1163
+ const overrides = mergeTextFormatting(paragraphMarkOverrides, directFormatting);
1164
+ if (!overrides || !directFormatting) return overrides;
1165
+ for (const key of ORDINARY_STYLE_TOGGLE_KEYS) if (directFormatting[key] === true && (!hasCharacterStyle || effectiveStyleFormatting?.[key] !== true)) Reflect.deleteProperty(overrides, key);
1166
+ if (directFormatting.boldCs === true && directFormatting.boldCs === directFormatting.bold && (!hasCharacterStyle || effectiveStyleFormatting?.boldCs === void 0)) Reflect.deleteProperty(overrides, "boldCs");
1167
+ if (directFormatting.italicCs === true && directFormatting.italicCs === directFormatting.italic && (!hasCharacterStyle || effectiveStyleFormatting?.italicCs === void 0)) Reflect.deleteProperty(overrides, "italicCs");
1168
+ return overrides;
1169
+ }
1044
1170
  /**
1045
1171
  * Convert RunContent to ProseMirror nodes
1046
1172
  */
@@ -1220,13 +1346,10 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
1220
1346
  }
1221
1347
  return nodes;
1222
1348
  }
1223
- /**
1224
- * Convert TextFormatting to ProseMirror marks
1225
- */
1226
- function textFormattingToMarks(formatting) {
1349
+ function textFormattingToMarks(formatting, options) {
1227
1350
  if (!formatting) return [];
1228
1351
  const marks = [];
1229
- const overrideAttrs = buildRunFormattingOverrideAttrs(formatting);
1352
+ const overrideAttrs = buildRunFormattingOverrideAttrs(options ? options.overrideFormatting : formatting);
1230
1353
  if (overrideAttrs) marks.push(schema.mark("runFormattingOverride", overrideAttrs));
1231
1354
  if (formatting.bold) marks.push(schema.mark("bold"));
1232
1355
  if (formatting.italic) marks.push(schema.mark("italic"));
@@ -4,38 +4,38 @@ import { createMarkExtension } from "../create.js";
4
4
  function buildRunFormattingOverrideAttrs(formatting) {
5
5
  if (!formatting) return;
6
6
  const attrs = {};
7
- if (formatting.bold === false) attrs.bold = false;
8
- if (formatting.italic === false) attrs.italic = false;
7
+ if (formatting.bold !== void 0) attrs.bold = formatting.bold;
8
+ if (formatting.italic !== void 0) attrs.italic = formatting.italic;
9
9
  if (formatting.underline?.style === "none") attrs.underline = "none";
10
- if (formatting.strike === false) attrs.strike = false;
10
+ if (formatting.strike !== void 0) attrs.strike = formatting.strike;
11
11
  if (formatting.doubleStrike === false) attrs.doubleStrike = false;
12
- if (formatting.allCaps === false) attrs.allCaps = false;
13
- if (formatting.smallCaps === false) attrs.smallCaps = false;
14
- if (formatting.hidden === false) attrs.hidden = false;
15
- if (formatting.emboss === false) attrs.emboss = false;
16
- if (formatting.imprint === false) attrs.imprint = false;
17
- if (formatting.shadow === false) attrs.shadow = false;
18
- if (formatting.outline === false) attrs.outline = false;
12
+ if (formatting.allCaps !== void 0) attrs.allCaps = formatting.allCaps;
13
+ if (formatting.smallCaps !== void 0) attrs.smallCaps = formatting.smallCaps;
14
+ if (formatting.hidden !== void 0) attrs.hidden = formatting.hidden;
15
+ if (formatting.emboss !== void 0) attrs.emboss = formatting.emboss;
16
+ if (formatting.imprint !== void 0) attrs.imprint = formatting.imprint;
17
+ if (formatting.shadow !== void 0) attrs.shadow = formatting.shadow;
18
+ if (formatting.outline !== void 0) attrs.outline = formatting.outline;
19
19
  if (formatting.rtl === false) attrs.rtl = false;
20
- if (formatting.boldCs !== void 0 && formatting.boldCs !== formatting.bold) attrs.boldCs = formatting.boldCs;
21
- if (formatting.italicCs !== void 0 && formatting.italicCs !== formatting.italic) attrs.italicCs = formatting.italicCs;
20
+ if (formatting.boldCs !== void 0) attrs.boldCs = formatting.boldCs;
21
+ if (formatting.italicCs !== void 0) attrs.italicCs = formatting.italicCs;
22
22
  if (formatting.fontSizeCs !== void 0 && formatting.fontSizeCs !== formatting.fontSize) attrs.fontSizeCs = formatting.fontSizeCs;
23
23
  if (formatting.cs !== void 0) attrs.cs = formatting.cs;
24
24
  return Object.keys(attrs).length > 0 ? attrs : void 0;
25
25
  }
26
26
  function applyRunFormattingOverrideAttrs(formatting, attrs) {
27
- if (attrs.bold === false) formatting.bold = false;
28
- if (attrs.italic === false) formatting.italic = false;
27
+ if (attrs.bold !== void 0) formatting.bold = attrs.bold;
28
+ if (attrs.italic !== void 0) formatting.italic = attrs.italic;
29
29
  if (attrs.underline === "none") formatting.underline = { style: "none" };
30
- if (attrs.strike === false) formatting.strike = false;
30
+ if (attrs.strike !== void 0) formatting.strike = attrs.strike;
31
31
  if (attrs.doubleStrike === false) formatting.doubleStrike = false;
32
- if (attrs.allCaps === false) formatting.allCaps = false;
33
- if (attrs.smallCaps === false) formatting.smallCaps = false;
34
- if (attrs.hidden === false) formatting.hidden = false;
35
- if (attrs.emboss === false) formatting.emboss = false;
36
- if (attrs.imprint === false) formatting.imprint = false;
37
- if (attrs.shadow === false) formatting.shadow = false;
38
- if (attrs.outline === false) formatting.outline = false;
32
+ if (attrs.allCaps !== void 0) formatting.allCaps = attrs.allCaps;
33
+ if (attrs.smallCaps !== void 0) formatting.smallCaps = attrs.smallCaps;
34
+ if (attrs.hidden !== void 0) formatting.hidden = attrs.hidden;
35
+ if (attrs.emboss !== void 0) formatting.emboss = attrs.emboss;
36
+ if (attrs.imprint !== void 0) formatting.imprint = attrs.imprint;
37
+ if (attrs.shadow !== void 0) formatting.shadow = attrs.shadow;
38
+ if (attrs.outline !== void 0) formatting.outline = attrs.outline;
39
39
  if (attrs.rtl === false) formatting.rtl = false;
40
40
  if (attrs.boldCs !== void 0) formatting.boldCs = attrs.boldCs;
41
41
  if (attrs.italicCs !== void 0) formatting.italicCs = attrs.italicCs;
@@ -121,7 +121,9 @@ type RunPropertyChangeMarkAttrs = {
121
121
  provenance: TrackedChangeProvenance;
122
122
  suggestionId?: string;
123
123
  };
124
- type RunFormattingOverrideAttrs = { [K in keyof Pick<document_d_exports.TextFormatting, "bold" | "italic" | "strike" | "doubleStrike" | "allCaps" | "smallCaps" | "hidden" | "emboss" | "imprint" | "shadow" | "outline" | "rtl">]?: false; } & {
124
+ type RunFormattingOverrideAttrs = { [K in keyof Pick<document_d_exports.TextFormatting, "bold" | "italic" | "strike" | "allCaps" | "smallCaps" | "hidden" | "emboss" | "imprint" | "shadow" | "outline">]?: boolean; } & {
125
+ doubleStrike?: false;
126
+ rtl?: false;
125
127
  /** Independent complex-script weight (`w:bCs`). */
126
128
  boldCs?: boolean;
127
129
  /** Force complex-script formatting for the full run (`w:cs`). */
@@ -127,7 +127,6 @@ declare class StyleResolver {
127
127
  */
128
128
  hasStyle(styleId: string): boolean;
129
129
  private findDefaultStyle;
130
- private mergeStyleIntoResult;
131
130
  }
132
131
  /**
133
132
  * Create a style resolver from document's style definitions
@@ -1,5 +1,5 @@
1
1
  import { mergeParagraphFormatting } from "../../utils/paragraphFormattingMerge.js";
2
- import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
2
+ import { cascadeStyleTextFormatting } from "./styleToggleCascade.js";
3
3
  //#region src/prosemirror/styles/styleResolver.ts
4
4
  /**
5
5
  * Word's default-template Normal style, used as a last-resort fallback for a
@@ -102,21 +102,23 @@ var StyleResolver = class {
102
102
  resolveParagraphStyleCascade(styleId, tableParagraphOverlay) {
103
103
  const result = {};
104
104
  if (this.docDefaults?.pPr) result.paragraphFormatting = { ...this.docDefaults.pPr };
105
- if (this.docDefaults?.rPr) result.runFormatting = { ...this.docDefaults.rPr };
106
105
  if (tableParagraphOverlay) {
107
106
  const merged = mergeParagraphFormatting(result.paragraphFormatting, tableParagraphOverlay);
108
107
  if (merged !== void 0) result.paragraphFormatting = merged;
109
108
  }
110
- if (!styleId) {
111
- if (this.defaultParagraphStyle) this.mergeStyleIntoResult(result, this.defaultParagraphStyle);
112
- return result;
113
- }
114
- const style = this.stylesById.get(styleId);
115
- if (!style) {
116
- if (this.defaultParagraphStyle) this.mergeStyleIntoResult(result, this.defaultParagraphStyle);
117
- return result;
109
+ const style = styleId ? this.stylesById.get(styleId) ?? this.defaultParagraphStyle : this.defaultParagraphStyle;
110
+ if (style?.pPr) {
111
+ const merged = mergeParagraphFormatting(result.paragraphFormatting, style.pPr);
112
+ if (merged) result.paragraphFormatting = merged;
118
113
  }
119
- this.mergeStyleIntoResult(result, style);
114
+ const runFormatting = cascadeStyleTextFormatting([{
115
+ formatting: this.docDefaults?.rPr,
116
+ type: "defaults"
117
+ }, {
118
+ formatting: style?.rPr,
119
+ type: "style"
120
+ }]).formatting;
121
+ if (runFormatting) result.runFormatting = runFormatting;
120
122
  return result;
121
123
  }
122
124
  /**
@@ -159,14 +161,15 @@ var StyleResolver = class {
159
161
  * @returns Resolved text formatting
160
162
  */
161
163
  resolveRunStyle(styleId) {
162
- let result = {};
163
- if (this.docDefaults?.rPr) result = { ...this.docDefaults.rPr };
164
- const defaultCharacterRpr = this.defaultCharacterStyle?.rPr;
165
- if (defaultCharacterRpr) result = mergeTextFormatting(result, defaultCharacterRpr) ?? {};
166
- const style = styleId ? this.stylesById.get(styleId) : void 0;
167
- if (!style?.rPr) return Object.keys(result).length > 0 ? result : void 0;
168
- const merged = mergeTextFormatting(result, style.rPr);
169
- return merged && Object.keys(merged).length > 0 ? merged : void 0;
164
+ const style = styleId ? this.stylesById.get(styleId) : this.defaultCharacterStyle;
165
+ const result = cascadeStyleTextFormatting([{
166
+ formatting: this.docDefaults?.rPr,
167
+ type: "defaults"
168
+ }, {
169
+ formatting: style?.rPr,
170
+ type: "style"
171
+ }]).formatting;
172
+ return result && Object.keys(result).length > 0 ? result : void 0;
170
173
  }
171
174
  /**
172
175
  * Get a character style's own properties WITHOUT docDefaults.
@@ -213,16 +216,6 @@ var StyleResolver = class {
213
216
  for (const style of this.stylesById.values()) if (style.type === type && style.default) return style;
214
217
  if (type === "paragraph") return this.stylesById.get("Normal") ?? (this.docDefaults ? void 0 : BUILTIN_NORMAL_STYLE);
215
218
  }
216
- mergeStyleIntoResult(result, style) {
217
- if (style.pPr) {
218
- const merged = mergeParagraphFormatting(result.paragraphFormatting, style.pPr);
219
- if (merged !== void 0) result.paragraphFormatting = merged;
220
- }
221
- if (style.rPr) {
222
- const merged = mergeTextFormatting(result.runFormatting, style.rPr);
223
- if (merged !== void 0) result.runFormatting = merged;
224
- }
225
- }
226
219
  };
227
220
  /**
228
221
  * Create a style resolver from document's style definitions
@@ -0,0 +1,37 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ import { STYLE_TOGGLE_KEYS } from "../../utils/textFormattingMerge.js";
3
+ //#region src/prosemirror/styles/styleToggleCascade.d.ts
4
+ type StyleToggleKey = (typeof STYLE_TOGGLE_KEYS)[number];
5
+ type ToggleState = {
6
+ value: boolean;
7
+ defaultsActive: boolean;
8
+ };
9
+ /** Internal formatting value whose toggle history survives copying and separate cascade passes. */
10
+ type StyleTextFormattingCascade = {
11
+ formatting: document_d_exports.TextFormatting | undefined;
12
+ toggleStates: ReadonlyMap<StyleToggleKey, ToggleState>;
13
+ type: "styleToggleCascade";
14
+ };
15
+ type StyleToggleLevel = {
16
+ formatting: document_d_exports.TextFormatting | undefined;
17
+ type: "defaults" | "style" | "direct";
18
+ } | {
19
+ cascade: StyleTextFormattingCascade;
20
+ type: "carried";
21
+ };
22
+ type StyleToggleCascadeOptions = {
23
+ /** Existing ordinary-property merge whose toggle fields this cascade replaces. */
24
+ ordinaryFormatting: document_d_exports.TextFormatting | undefined;
25
+ };
26
+ /**
27
+ * Resolve run formatting across OOXML style hierarchy levels.
28
+ *
29
+ * Ordinary properties retain Folio's existing low-to-high, last-defined merge. Toggle
30
+ * properties reverse inherited state when a style level states `true`; `false` leaves inherited
31
+ * style state unchanged, while direct formatting remains an absolute value. The returned value
32
+ * carries its toggle history explicitly, so paragraph and character style resolution may happen
33
+ * in separate passes without an object-identity contract.
34
+ */
35
+ declare function cascadeStyleTextFormatting(levels: readonly StyleToggleLevel[], options?: StyleToggleCascadeOptions): StyleTextFormattingCascade;
36
+ //#endregion
37
+ export { cascadeStyleTextFormatting };
@@ -0,0 +1,52 @@
1
+ import { STYLE_TOGGLE_KEYS, mergeTextFormatting } from "../../utils/textFormattingMerge.js";
2
+ //#region src/prosemirror/styles/styleToggleCascade.ts
3
+ /**
4
+ * Resolve run formatting across OOXML style hierarchy levels.
5
+ *
6
+ * Ordinary properties retain Folio's existing low-to-high, last-defined merge. Toggle
7
+ * properties reverse inherited state when a style level states `true`; `false` leaves inherited
8
+ * style state unchanged, while direct formatting remains an absolute value. The returned value
9
+ * carries its toggle history explicitly, so paragraph and character style resolution may happen
10
+ * in separate passes without an object-identity contract.
11
+ */
12
+ function cascadeStyleTextFormatting(levels, options) {
13
+ let formatting = options ? mergeTextFormatting(void 0, options.ordinaryFormatting) : void 0;
14
+ const mergeOrdinaryFormatting = options === void 0;
15
+ const toggleStates = /* @__PURE__ */ new Map();
16
+ for (const level of levels) {
17
+ if (level.type === "carried") {
18
+ if (mergeOrdinaryFormatting) formatting = mergeTextFormatting(formatting, level.cascade.formatting);
19
+ for (const [key, state] of level.cascade.toggleStates) toggleStates.set(key, state);
20
+ continue;
21
+ }
22
+ if (!level.formatting) continue;
23
+ if (mergeOrdinaryFormatting) formatting = mergeTextFormatting(formatting, level.formatting);
24
+ for (const key of STYLE_TOGGLE_KEYS) {
25
+ const value = level.formatting[key];
26
+ if (value === void 0) continue;
27
+ const inherited = toggleStates.get(key);
28
+ if (level.type === "direct") toggleStates.set(key, {
29
+ value,
30
+ defaultsActive: false
31
+ });
32
+ else if (!value) continue;
33
+ else if (level.type === "defaults") toggleStates.set(key, {
34
+ value: true,
35
+ defaultsActive: true
36
+ });
37
+ else toggleStates.set(key, {
38
+ value: inherited?.defaultsActive === true ? true : !(inherited?.value ?? false),
39
+ defaultsActive: inherited?.defaultsActive === true
40
+ });
41
+ }
42
+ }
43
+ if (!formatting && toggleStates.size > 0) formatting = {};
44
+ for (const [key, state] of toggleStates) if (formatting) formatting[key] = state.value;
45
+ return {
46
+ formatting,
47
+ toggleStates,
48
+ type: "styleToggleCascade"
49
+ };
50
+ }
51
+ //#endregion
52
+ export { cascadeStyleTextFormatting };
@@ -1,5 +1,13 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  //#region src/utils/textFormattingMerge.d.ts
3
+ declare const STYLE_TOGGLE_KEYS: readonly ["bold", "boldCs", "italic", "italicCs", "allCaps", "emboss", "imprint", "outline", "shadow", "smallCaps", "strike", "hidden"];
4
+ /**
5
+ * Merge the properties exposed by one resolved style definition.
6
+ *
7
+ * A false toggle at a child style level does not cancel the inherited state. The
8
+ * ordinary merge remains unchanged for direct formatting and non-toggle properties.
9
+ */
10
+ declare function mergeStyleTextFormatting(target: document_d_exports.TextFormatting | undefined, source: document_d_exports.TextFormatting | undefined): document_d_exports.TextFormatting | undefined;
3
11
  declare function mergeTextFormatting(target: document_d_exports.TextFormatting | undefined, source: document_d_exports.TextFormatting | undefined): document_d_exports.TextFormatting | undefined;
4
12
  //#endregion
5
- export { mergeTextFormatting };
13
+ export { STYLE_TOGGLE_KEYS, mergeStyleTextFormatting, mergeTextFormatting };
@@ -1,5 +1,36 @@
1
1
  import { mergeFontFamily } from "./fontFamilyMerge.js";
2
2
  //#region src/utils/textFormattingMerge.ts
3
+ const STYLE_TOGGLE_KEYS = [
4
+ "bold",
5
+ "boldCs",
6
+ "italic",
7
+ "italicCs",
8
+ "allCaps",
9
+ "emboss",
10
+ "imprint",
11
+ "outline",
12
+ "shadow",
13
+ "smallCaps",
14
+ "strike",
15
+ "hidden"
16
+ ];
17
+ /**
18
+ * Merge the properties exposed by one resolved style definition.
19
+ *
20
+ * A false toggle at a child style level does not cancel the inherited state. The
21
+ * ordinary merge remains unchanged for direct formatting and non-toggle properties.
22
+ */
23
+ function mergeStyleTextFormatting(target, source) {
24
+ const result = mergeTextFormatting(target, source);
25
+ if (!result || !source) return result;
26
+ for (const key of STYLE_TOGGLE_KEYS) {
27
+ if (source[key] !== false) continue;
28
+ const inherited = target?.[key];
29
+ if (inherited === void 0) Reflect.deleteProperty(result, key);
30
+ else result[key] = inherited;
31
+ }
32
+ return result;
33
+ }
3
34
  function mergeTextFormatting(target, source) {
4
35
  if (!source && !target) return;
5
36
  if (!source) return target;
@@ -28,4 +59,4 @@ function mergeTextFormatting(target, source) {
28
59
  return result;
29
60
  }
30
61
  //#endregion
31
- export { mergeTextFormatting };
62
+ export { STYLE_TOGGLE_KEYS, mergeStyleTextFormatting, mergeTextFormatting };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",