@beyondwork/docx-react-component 1.0.38 → 1.0.39

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 (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -8,6 +8,7 @@ import type {
8
8
  ParagraphIndentation,
9
9
  TabStop,
10
10
  } from "../../model/canonical-document.ts";
11
+ import { readRunProperties } from "./parse-run-formatting.ts";
11
12
 
12
13
  export interface ParsedParagraphNumberingReference {
13
14
  paragraphIndex: number;
@@ -48,9 +49,32 @@ export function parseNumberingXml(xml: string): NumberingCatalog {
48
49
  }
49
50
 
50
51
  const abstractNumberingId = toCanonicalAbstractNumberingId(rawId);
52
+ const nsidEl = findChildElementOptional(child, "nsid");
53
+ const nsid = nsidEl ? (nsidEl.attributes["w:val"] ?? nsidEl.attributes.val) : undefined;
54
+ const mltEl = findChildElementOptional(child, "multiLevelType");
55
+ const mltRaw = mltEl ? (mltEl.attributes["w:val"] ?? mltEl.attributes.val) : undefined;
56
+ const multiLevelType =
57
+ mltRaw === "singleLevel" || mltRaw === "multilevel" || mltRaw === "hybridMultilevel"
58
+ ? mltRaw
59
+ : undefined;
60
+ const tmplEl = findChildElementOptional(child, "tmpl");
61
+ const tplc = tmplEl ? (tmplEl.attributes["w:val"] ?? tmplEl.attributes.val) : undefined;
62
+ const styleLinkEl = findChildElementOptional(child, "styleLink");
63
+ const styleLink = styleLinkEl
64
+ ? (styleLinkEl.attributes["w:val"] ?? styleLinkEl.attributes.val)
65
+ : undefined;
66
+ const numStyleLinkEl = findChildElementOptional(child, "numStyleLink");
67
+ const numStyleLink = numStyleLinkEl
68
+ ? (numStyleLinkEl.attributes["w:val"] ?? numStyleLinkEl.attributes.val)
69
+ : undefined;
51
70
  abstractDefinitions[abstractNumberingId] = {
52
71
  abstractNumberingId,
53
72
  levels: readLevels(child),
73
+ ...(nsid ? { nsid } : {}),
74
+ ...(multiLevelType ? { multiLevelType } : {}),
75
+ ...(tplc ? { tplc } : {}),
76
+ ...(styleLink ? { styleLink } : {}),
77
+ ...(numStyleLink ? { numStyleLink } : {}),
54
78
  };
55
79
  break;
56
80
  }
@@ -219,6 +243,11 @@ function readLevelDefinition(
219
243
  ? "tab"
220
244
  : undefined;
221
245
  const paragraphGeometry = readLevelParagraphGeometry(levelNode);
246
+ const lvlRestartNode = findChildElementOptional(levelNode, "lvlRestart");
247
+ const rawRestart = lvlRestartNode?.attributes["w:val"] ?? lvlRestartNode?.attributes.val;
248
+ const restartAfterLevel = rawRestart !== undefined ? parseInteger(rawRestart) : undefined;
249
+ const rPrNode = findChildElementOptional(levelNode, "rPr");
250
+ const runProperties = readRunProperties(rPrNode);
222
251
 
223
252
  return {
224
253
  level,
@@ -229,6 +258,8 @@ function readLevelDefinition(
229
258
  ...(isLegalNumbering ? { isLegalNumbering: true } : {}),
230
259
  ...(suffix ? { suffix } : {}),
231
260
  ...(paragraphGeometry ? { paragraphGeometry } : {}),
261
+ ...(runProperties ? { runProperties } : {}),
262
+ ...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
232
263
  };
233
264
  }
234
265
 
@@ -263,6 +294,11 @@ function readLevelOverrideDefinition(
263
294
  ? "tab"
264
295
  : undefined;
265
296
  const paragraphGeometry = readLevelParagraphGeometry(levelNode);
297
+ const lvlRestartNode = findChildElementOptional(levelNode, "lvlRestart");
298
+ const rawRestart = lvlRestartNode?.attributes["w:val"] ?? lvlRestartNode?.attributes.val;
299
+ const restartAfterLevel = rawRestart !== undefined ? parseInteger(rawRestart) : undefined;
300
+ const rPrNode = findChildElementOptional(levelNode, "rPr");
301
+ const runProperties = readRunProperties(rPrNode);
266
302
 
267
303
  const hasExplicitFields =
268
304
  startAt !== undefined ||
@@ -271,7 +307,9 @@ function readLevelOverrideDefinition(
271
307
  paragraphStyleId !== undefined ||
272
308
  isLegalNumbering !== undefined ||
273
309
  suffix !== undefined ||
274
- paragraphGeometry !== undefined;
310
+ paragraphGeometry !== undefined ||
311
+ runProperties !== undefined ||
312
+ restartAfterLevel !== undefined;
275
313
 
276
314
  if (!hasExplicitFields) {
277
315
  return undefined;
@@ -286,6 +324,8 @@ function readLevelOverrideDefinition(
286
324
  ...(isLegalNumbering !== undefined ? { isLegalNumbering } : {}),
287
325
  ...(suffix ? { suffix } : {}),
288
326
  ...(paragraphGeometry ? { paragraphGeometry } : {}),
327
+ ...(runProperties ? { runProperties } : {}),
328
+ ...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
289
329
  };
290
330
  }
291
331
 
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Read `<w:pPr>` (paragraph properties) into a `CanonicalParagraphFormatting` value.
3
+ *
4
+ * Returns `undefined` when the input is `undefined` or when the pPr is empty (no recognized
5
+ * child produces a field). Delegates the paragraph-mark `<w:rPr>` to `readRunProperties`
6
+ * so every rPr in the project is parsed through one code path.
7
+ */
8
+
9
+ import type {
10
+ CanonicalParagraphFormatting,
11
+ ParagraphBorders,
12
+ ParagraphIndentation,
13
+ ParagraphShading,
14
+ ParagraphSpacing,
15
+ TabStop,
16
+ } from "../../model/canonical-document.ts";
17
+ import { readRunProperties } from "./parse-run-formatting.ts";
18
+ import { findChildOptional, localName, readIntAttr, readIntVal, readOnOff } from "./xml-attr-helpers.ts";
19
+ import type { XmlElementNode } from "./xml-element.ts";
20
+
21
+ function readSpacing(node: XmlElementNode): ParagraphSpacing | undefined {
22
+ const out: ParagraphSpacing = {};
23
+ const before = readIntAttr(node, "w:before");
24
+ const after = readIntAttr(node, "w:after");
25
+ const line = readIntAttr(node, "w:line");
26
+ const rule = node.attributes["w:lineRule"] ?? node.attributes.lineRule;
27
+ if (before !== undefined) out.before = before;
28
+ if (after !== undefined) out.after = after;
29
+ if (line !== undefined) out.line = line;
30
+ if (rule) {
31
+ const n = rule.toLowerCase();
32
+ if (n === "auto" || n === "exact") out.lineRule = n;
33
+ else if (n === "atleast") out.lineRule = "atLeast";
34
+ }
35
+ return Object.keys(out).length > 0 ? out : undefined;
36
+ }
37
+
38
+ function readIndent(node: XmlElementNode): ParagraphIndentation | undefined {
39
+ const out: ParagraphIndentation = {};
40
+ const left = readIntAttr(node, "w:left") ?? readIntAttr(node, "w:start");
41
+ const right = readIntAttr(node, "w:right") ?? readIntAttr(node, "w:end");
42
+ const firstLine = readIntAttr(node, "w:firstLine");
43
+ const hanging = readIntAttr(node, "w:hanging");
44
+ if (left !== undefined) out.left = left;
45
+ if (right !== undefined) out.right = right;
46
+ if (firstLine !== undefined) out.firstLine = firstLine;
47
+ if (hanging !== undefined) out.hanging = hanging;
48
+ return Object.keys(out).length > 0 ? out : undefined;
49
+ }
50
+
51
+ function readTabStops(node: XmlElementNode): TabStop[] | undefined {
52
+ const tabs: TabStop[] = [];
53
+ for (const c of node.children) {
54
+ if (c.type !== "element" || localName(c.name) !== "tab") continue;
55
+ const pos = Number.parseInt(c.attributes["w:pos"] ?? c.attributes.pos ?? "", 10);
56
+ if (!Number.isFinite(pos)) continue;
57
+ const val = (c.attributes["w:val"] ?? c.attributes.val ?? "left").toLowerCase();
58
+ const leader = (c.attributes["w:leader"] ?? c.attributes.leader ?? "none").toLowerCase();
59
+ const align: TabStop["align"] =
60
+ val === "center" || val === "right" || val === "decimal" || val === "bar" || val === "num" || val === "clear"
61
+ ? (val as TabStop["align"])
62
+ : "left";
63
+ const mappedLeader: TabStop["leader"] | undefined =
64
+ leader === "dot" || leader === "hyphen" || leader === "underscore" || leader === "heavy"
65
+ ? (leader as TabStop["leader"])
66
+ : leader === "middledot"
67
+ ? "middleDot"
68
+ : undefined;
69
+ tabs.push({
70
+ position: pos,
71
+ align,
72
+ ...(mappedLeader && mappedLeader !== "none" ? { leader: mappedLeader } : {}),
73
+ });
74
+ }
75
+ return tabs.length > 0 ? tabs : undefined;
76
+ }
77
+
78
+ function readBorders(node: XmlElementNode): ParagraphBorders | undefined {
79
+ const out: ParagraphBorders = {};
80
+ const sides = ["top", "bottom", "left", "right", "between", "bar"] as const;
81
+ for (const side of sides) {
82
+ const child = findChildOptional(node, side);
83
+ if (!child) continue;
84
+ const val = child.attributes["w:val"] ?? child.attributes.val;
85
+ const sz = readIntAttr(child, "w:sz");
86
+ const space = readIntAttr(child, "w:space");
87
+ const color = child.attributes["w:color"] ?? child.attributes.color;
88
+ out[side] = {
89
+ ...(val ? { value: val } : {}),
90
+ ...(sz !== undefined ? { size: sz } : {}),
91
+ ...(space !== undefined ? { space } : {}),
92
+ ...(color ? { color } : {}),
93
+ };
94
+ }
95
+ return Object.keys(out).length > 0 ? out : undefined;
96
+ }
97
+
98
+ function readShading(node: XmlElementNode): ParagraphShading | undefined {
99
+ const val = node.attributes["w:val"] ?? node.attributes.val;
100
+ const fill = node.attributes["w:fill"] ?? node.attributes.fill;
101
+ const color = node.attributes["w:color"] ?? node.attributes.color;
102
+ if (!val && !fill && !color) return undefined;
103
+ return {
104
+ ...(val ? { val } : {}),
105
+ ...(fill ? { fill } : {}),
106
+ ...(color ? { color } : {}),
107
+ };
108
+ }
109
+
110
+ export function readParagraphProperties(
111
+ node: XmlElementNode | undefined,
112
+ ): CanonicalParagraphFormatting | undefined {
113
+ if (!node) return undefined;
114
+
115
+ const out: CanonicalParagraphFormatting = {};
116
+
117
+ const spacingNode = findChildOptional(node, "spacing");
118
+ if (spacingNode) {
119
+ const spacing = readSpacing(spacingNode);
120
+ if (spacing) out.spacing = spacing;
121
+ }
122
+
123
+ const indNode = findChildOptional(node, "ind");
124
+ if (indNode) {
125
+ const ind = readIndent(indNode);
126
+ if (ind) out.indentation = ind;
127
+ }
128
+
129
+ const jcNode = findChildOptional(node, "jc");
130
+ if (jcNode) {
131
+ const raw = jcNode.attributes["w:val"] ?? jcNode.attributes.val;
132
+ if (
133
+ raw === "left" ||
134
+ raw === "center" ||
135
+ raw === "right" ||
136
+ raw === "both" ||
137
+ raw === "distribute" ||
138
+ raw === "start" ||
139
+ raw === "end"
140
+ ) {
141
+ out.alignment = raw;
142
+ }
143
+ }
144
+
145
+ const bordersNode = findChildOptional(node, "pBdr");
146
+ if (bordersNode) {
147
+ const borders = readBorders(bordersNode);
148
+ if (borders) out.borders = borders;
149
+ }
150
+
151
+ const shdNode = findChildOptional(node, "shd");
152
+ if (shdNode) {
153
+ const shading = readShading(shdNode);
154
+ if (shading) out.shading = shading;
155
+ }
156
+
157
+ const tabsNode = findChildOptional(node, "tabs");
158
+ if (tabsNode) {
159
+ const tabs = readTabStops(tabsNode);
160
+ if (tabs) out.tabStops = tabs;
161
+ }
162
+
163
+ const keepNext = readOnOff(findChildOptional(node, "keepNext"));
164
+ if (keepNext !== undefined) out.keepNext = keepNext;
165
+ const keepLines = readOnOff(findChildOptional(node, "keepLines"));
166
+ if (keepLines !== undefined) out.keepLines = keepLines;
167
+ const widow = readOnOff(findChildOptional(node, "widowControl"));
168
+ if (widow !== undefined) out.widowControl = widow;
169
+ const pbb = readOnOff(findChildOptional(node, "pageBreakBefore"));
170
+ if (pbb !== undefined) out.pageBreakBefore = pbb;
171
+ const ctx = readOnOff(findChildOptional(node, "contextualSpacing"));
172
+ if (ctx !== undefined) out.contextualSpacing = ctx;
173
+ const bidi = readOnOff(findChildOptional(node, "bidi"));
174
+ if (bidi !== undefined) out.bidi = bidi;
175
+ const suppressLn = readOnOff(findChildOptional(node, "suppressLineNumbers"));
176
+ if (suppressLn !== undefined) out.suppressLineNumbers = suppressLn;
177
+ const suppressAh = readOnOff(findChildOptional(node, "suppressAutoHyphens"));
178
+ if (suppressAh !== undefined) out.suppressAutoHyphens = suppressAh;
179
+
180
+ const outline = readIntVal(findChildOptional(node, "outlineLvl"));
181
+ if (outline !== undefined) out.outlineLevel = outline;
182
+
183
+ const rPrNode = findChildOptional(node, "rPr");
184
+ const markRpr = readRunProperties(rPrNode);
185
+ if (markRpr) out.paragraphMarkRunProperties = markRpr;
186
+
187
+ return Object.keys(out).length > 0 ? out : undefined;
188
+ }
@@ -0,0 +1,129 @@
1
+ import type { CanonicalRunFormatting } from "../../model/canonical-document.ts";
2
+ import { findChildOptional, readIntVal, readOnOff, readStringAttr } from "./xml-attr-helpers.ts";
3
+ import type { XmlElementNode } from "./xml-element.ts";
4
+
5
+ /**
6
+ * Read `<w:rPr>` (run properties) into a `CanonicalRunFormatting` value.
7
+ *
8
+ * Returns `undefined` when the input is `undefined` or when the rPr is empty
9
+ * (i.e., no recognized child produces a field). `colorHex` may carry the
10
+ * sentinel `"auto"`; `fontFamily` is the alias of the first-available
11
+ * `fontFamily{Ascii,HAnsi,EastAsia,Cs}`.
12
+ *
13
+ * Caller-side: route every `<w:rPr>` from styles.xml, numbering.xml, and
14
+ * the paragraph mark in document.xml through this helper so the cascade
15
+ * sees the same shape.
16
+ */
17
+ export function readRunProperties(
18
+ node: XmlElementNode | undefined,
19
+ ): CanonicalRunFormatting | undefined {
20
+ if (!node) return undefined;
21
+
22
+ const rPr: CanonicalRunFormatting = {};
23
+
24
+ const bold = readOnOff(findChildOptional(node, "b"));
25
+ if (bold !== undefined) rPr.bold = bold;
26
+
27
+ const italic = readOnOff(findChildOptional(node, "i"));
28
+ if (italic !== undefined) rPr.italic = italic;
29
+
30
+ const strike = readOnOff(findChildOptional(node, "strike"));
31
+ if (strike !== undefined) rPr.strikethrough = strike;
32
+
33
+ const dstrike = readOnOff(findChildOptional(node, "dstrike"));
34
+ if (dstrike !== undefined) rPr.doubleStrikethrough = dstrike;
35
+
36
+ const vanish = readOnOff(findChildOptional(node, "vanish"));
37
+ if (vanish !== undefined) rPr.vanish = vanish;
38
+
39
+ const allCaps = readOnOff(findChildOptional(node, "caps"));
40
+ if (allCaps !== undefined) rPr.allCaps = allCaps;
41
+
42
+ const smallCaps = readOnOff(findChildOptional(node, "smallCaps"));
43
+ if (smallCaps !== undefined) rPr.smallCaps = smallCaps;
44
+
45
+ const u = findChildOptional(node, "u");
46
+ if (u) {
47
+ const val =
48
+ u.attributes["w:val"] ?? u.attributes["val"] ?? "single";
49
+ if (val === "none") {
50
+ rPr.underline = "none";
51
+ } else if (
52
+ val === "single" ||
53
+ val === "double" ||
54
+ val === "thick" ||
55
+ val === "dotted" ||
56
+ val === "dash" ||
57
+ val === "wave"
58
+ ) {
59
+ rPr.underline = val;
60
+ } else {
61
+ // Unknown underline type — coerce to "single"
62
+ rPr.underline = "single";
63
+ }
64
+ }
65
+
66
+ const vertAlign = findChildOptional(node, "vertAlign");
67
+ if (vertAlign) {
68
+ const val =
69
+ vertAlign.attributes["w:val"] ?? vertAlign.attributes["val"];
70
+ if (
71
+ val === "superscript" ||
72
+ val === "subscript" ||
73
+ val === "baseline"
74
+ ) {
75
+ rPr.verticalAlign = val;
76
+ }
77
+ }
78
+
79
+ const rFonts = findChildOptional(node, "rFonts");
80
+ if (rFonts) {
81
+ const ascii = rFonts.attributes["w:ascii"] ?? rFonts.attributes.ascii;
82
+ const hAnsi = rFonts.attributes["w:hAnsi"] ?? rFonts.attributes.hAnsi;
83
+ const eastAsia = rFonts.attributes["w:eastAsia"] ?? rFonts.attributes.eastAsia;
84
+ const cs = rFonts.attributes["w:cs"] ?? rFonts.attributes.cs;
85
+ if (ascii) rPr.fontFamilyAscii = ascii;
86
+ if (hAnsi) rPr.fontFamilyHAnsi = hAnsi;
87
+ if (eastAsia) rPr.fontFamilyEastAsia = eastAsia;
88
+ if (cs) rPr.fontFamilyCs = cs;
89
+ const primary = ascii ?? hAnsi ?? eastAsia ?? cs;
90
+ if (primary) rPr.fontFamily = primary;
91
+ }
92
+
93
+ const sz = readIntVal(findChildOptional(node, "sz"));
94
+ if (sz !== undefined) rPr.fontSizeHalfPoints = sz;
95
+
96
+ const szCs = readIntVal(findChildOptional(node, "szCs"));
97
+ if (szCs !== undefined) rPr.fontSizeCsHalfPoints = szCs;
98
+
99
+ const color = findChildOptional(node, "color");
100
+ if (color) {
101
+ const val = color.attributes["w:val"] ?? color.attributes["val"];
102
+ const theme =
103
+ color.attributes["w:themeColor"] ?? color.attributes["themeColor"];
104
+ if (val) rPr.colorHex = val;
105
+ if (theme) rPr.colorThemeSlot = theme;
106
+ }
107
+
108
+ const highlight = findChildOptional(node, "highlight");
109
+ if (highlight) {
110
+ const val =
111
+ highlight.attributes["w:val"] ?? highlight.attributes["val"];
112
+ if (val) rPr.highlight = val;
113
+ }
114
+
115
+ const spacing = readIntVal(findChildOptional(node, "spacing"));
116
+ if (spacing !== undefined) rPr.characterSpacingTwips = spacing;
117
+
118
+ const rStyleNode = findChildOptional(node, "rStyle");
119
+ const rStyle = rStyleNode ? readStringAttr(rStyleNode, "w:val") : undefined;
120
+ if (rStyle) rPr.characterStyleId = rStyle;
121
+
122
+ const lang = findChildOptional(node, "lang");
123
+ if (lang) {
124
+ const val = lang.attributes["w:val"] ?? lang.attributes["val"];
125
+ if (val) rPr.languageCode = val;
126
+ }
127
+
128
+ return Object.keys(rPr).length > 0 ? rPr : undefined;
129
+ }
@@ -10,6 +10,7 @@
10
10
  import type {
11
11
  CellShading,
12
12
  CharacterStyleDefinition,
13
+ DocumentDefaults,
13
14
  LatentStyleDefinition,
14
15
  ParagraphStyleDefinition,
15
16
  StylesCatalog,
@@ -37,6 +38,8 @@ import {
37
38
  readTableWidth,
38
39
  } from "./parse-tables.ts";
39
40
  import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
41
+ import { readRunProperties } from "./parse-run-formatting.ts";
42
+ import { readParagraphProperties } from "./parse-paragraph-formatting.ts";
40
43
 
41
44
  // ---------------------------------------------------------------------------
42
45
  // Inline XML node types (same pattern as parse-numbering.ts)
@@ -116,11 +119,28 @@ export function parseStylesXml(xml: string): ParseStylesResult {
116
119
  const characters: Record<string, CharacterStyleDefinition> = {};
117
120
  const tables: Record<string, TableStyleDefinition> = {};
118
121
  const latentStyles: Record<string, LatentStyleDefinition> = {};
122
+ let docDefaults: DocumentDefaults | undefined;
119
123
 
120
124
  for (const child of stylesElement.children) {
121
125
  if (child.type !== "element") continue;
122
126
  const local = localName(child.name);
123
127
 
128
+ if (local === "docDefaults") {
129
+ const pPrDefault = findChildElementOptional(child, "pPrDefault");
130
+ const rPrDefault = findChildElementOptional(child, "rPrDefault");
131
+ const pPrNode = pPrDefault ? findChildElementOptional(pPrDefault, "pPr") : undefined;
132
+ const rPrNode = rPrDefault ? findChildElementOptional(rPrDefault, "rPr") : undefined;
133
+ const paragraph = readParagraphProperties(pPrNode);
134
+ const run = readRunProperties(rPrNode);
135
+ if (paragraph || run) {
136
+ docDefaults = {
137
+ ...(paragraph ? { paragraph } : {}),
138
+ ...(run ? { run } : {}),
139
+ };
140
+ }
141
+ continue;
142
+ }
143
+
124
144
  if (local === "style") {
125
145
  const styleType = child.attributes["w:type"] ?? child.attributes.type;
126
146
  const styleId = child.attributes["w:styleId"] ?? child.attributes.styleId;
@@ -135,6 +155,10 @@ export function parseStylesXml(xml: string): ParseStylesResult {
135
155
  const nextStyle = readLinkedStyleId(child, "next");
136
156
  const outlineLevel = readParagraphStyleOutlineLevel(child);
137
157
  const numbering = readParagraphStyleNumbering(child);
158
+ const pPrNode = findChildElementOptional(child, "pPr");
159
+ const rPrNode = findChildElementOptional(child, "rPr");
160
+ const paragraphProperties = readParagraphProperties(pPrNode);
161
+ const runProperties = readRunProperties(rPrNode);
138
162
  paragraphs[styleId] = {
139
163
  styleId,
140
164
  displayName,
@@ -144,16 +168,21 @@ export function parseStylesXml(xml: string): ParseStylesResult {
144
168
  ...(nextStyle ? { nextStyle } : {}),
145
169
  ...(outlineLevel !== undefined ? { outlineLevel } : {}),
146
170
  ...(numbering ? { numbering } : {}),
171
+ ...(paragraphProperties ? { paragraphProperties } : {}),
172
+ ...(runProperties ? { runProperties } : {}),
147
173
  };
148
174
  break;
149
175
  }
150
176
  case "character": {
177
+ const rPrNode = findChildElementOptional(child, "rPr");
178
+ const runProperties = readRunProperties(rPrNode);
151
179
  characters[styleId] = {
152
180
  styleId,
153
181
  displayName,
154
182
  kind: "character",
155
183
  isDefault,
156
184
  ...(basedOn ? { basedOn } : {}),
185
+ ...(runProperties ? { runProperties } : {}),
157
186
  };
158
187
  break;
159
188
  }
@@ -194,6 +223,8 @@ export function parseStylesXml(xml: string): ParseStylesResult {
194
223
  characters,
195
224
  tables,
196
225
  ...(hasLatent ? { latentStyles } : {}),
226
+ fromPackage: true,
227
+ ...(docDefaults ? { docDefaults } : {}),
197
228
  },
198
229
  fromPackage: true,
199
230
  diagnostics,
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared XML helpers for OOXML parsers.
3
+ *
4
+ * These functions factor out the common pattern of reading namespaced-prefixed
5
+ * attributes (e.g., `w:val`) with fallback to the unprefixed form, handling
6
+ * the OOXML `ST_OnOff` toggle semantics (missing child = undefined, present
7
+ * with no val = true, `val="0"|"false"|"off"` = false), and reading integers.
8
+ *
9
+ * Used by parse-run-formatting.ts, parse-paragraph-formatting.ts, and (later)
10
+ * parse-styles.ts, parse-numbering.ts.
11
+ */
12
+
13
+ import type { XmlElementNode } from "./xml-element.ts";
14
+
15
+ export function localName(name: string): string {
16
+ const sep = name.indexOf(":");
17
+ return sep >= 0 ? name.slice(sep + 1) : name;
18
+ }
19
+
20
+ export function findChildOptional(
21
+ node: XmlElementNode,
22
+ local: string,
23
+ ): XmlElementNode | undefined {
24
+ return node.children.find(
25
+ (c): c is XmlElementNode => c.type === "element" && localName(c.name) === local,
26
+ );
27
+ }
28
+
29
+ /** ST_OnOff: missing child → undefined; present bare or w:val="1|true|on" → true; w:val="0|false|off" → false. */
30
+ export function readOnOff(node: XmlElementNode | undefined): boolean | undefined {
31
+ if (!node) return undefined;
32
+ const raw = node.attributes["w:val"] ?? node.attributes.val;
33
+ if (raw === undefined) return true;
34
+ const n = raw.toLowerCase();
35
+ if (n === "0" || n === "false" || n === "off") return false;
36
+ if (n === "1" || n === "true" || n === "on") return true;
37
+ return true;
38
+ }
39
+
40
+ /** Read the child's `w:val` attribute as an int. Returns undefined if missing or not a finite integer. */
41
+ export function readIntVal(node: XmlElementNode | undefined): number | undefined {
42
+ if (!node) return undefined;
43
+ const raw = node.attributes["w:val"] ?? node.attributes.val;
44
+ if (raw === undefined) return undefined;
45
+ const v = Number.parseInt(raw, 10);
46
+ return Number.isFinite(v) ? v : undefined;
47
+ }
48
+
49
+ /** Read an arbitrary attribute from a node as an int, with namespace fallback. */
50
+ export function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
51
+ const raw = node.attributes[attr] ?? node.attributes[attr.replace(/^w:/, "")];
52
+ if (raw === undefined) return undefined;
53
+ const v = Number.parseInt(raw, 10);
54
+ return Number.isFinite(v) ? v : undefined;
55
+ }
56
+
57
+ /** Read an arbitrary attribute from a node as a string, with namespace fallback. */
58
+ export function readStringAttr(node: XmlElementNode, attr: string): string | undefined {
59
+ return node.attributes[attr] ?? node.attributes[attr.replace(/^w:/, "")];
60
+ }
@@ -0,0 +1,19 @@
1
+ export interface XmlElementNode {
2
+ type: "element";
3
+ name: string;
4
+ attributes: Record<string, string>;
5
+ children: Array<XmlElementNode | XmlTextNode>;
6
+ /** Optional source offset (start) — parsers that track offsets may populate. */
7
+ start?: number;
8
+ /** Optional source offset (end) — parsers that track offsets may populate. */
9
+ end?: number;
10
+ }
11
+
12
+ export interface XmlTextNode {
13
+ type: "text";
14
+ text: string;
15
+ start?: number;
16
+ end?: number;
17
+ }
18
+
19
+ export type XmlNode = XmlElementNode | XmlTextNode;