@beyondwork/docx-react-component 1.0.18 → 1.0.20

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 (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BorderSpec,
2
3
  TextMark,
3
4
  ParagraphBorders,
4
5
  ParagraphShading,
@@ -6,6 +7,16 @@ import type {
6
7
  ParagraphIndentation,
7
8
  TabStop,
8
9
  TableLook,
10
+ SectionProperties,
11
+ PageSize,
12
+ PageMargins,
13
+ ColumnProperties,
14
+ PageNumbering,
15
+ HeaderFooterReference,
16
+ HeaderFooterVariant,
17
+ SectionDocumentGrid,
18
+ SectionLineNumbering,
19
+ SectionPageBorders,
9
20
  } from "../../model/canonical-document.ts";
10
21
  import type { OpcRelationship } from "./part-manifest.ts";
11
22
  import {
@@ -15,9 +26,12 @@ import {
15
26
  import { toCanonicalNumberingInstanceId } from "./parse-numbering.ts";
16
27
  import { parseComplexContentXml } from "./parse-complex-content.ts";
17
28
  import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
29
+ import { classifyFieldInstruction } from "./parse-fields.ts";
30
+ import { resolveHighlightColor } from "./highlight-colors.ts";
18
31
 
19
32
  export interface ParsedMainDocument {
20
33
  blocks: ParsedBlockNode[];
34
+ finalSectionProperties?: SectionProperties;
21
35
  }
22
36
 
23
37
  export type ParsedBlockNode =
@@ -26,8 +40,15 @@ export type ParsedBlockNode =
26
40
  | ParsedSdtNode
27
41
  | ParsedCustomXmlNode
28
42
  | ParsedAltChunkNode
43
+ | ParsedSectionBreakNode
29
44
  | ParsedOpaqueBlockNode;
30
45
 
46
+ export interface ParsedSectionBreakNode {
47
+ type: "section_break";
48
+ sectionPropertiesXml: string;
49
+ sectionProperties: SectionProperties;
50
+ }
51
+
31
52
  export interface ParsedParagraphNode {
32
53
  type: "paragraph";
33
54
  styleId?: string;
@@ -37,6 +58,7 @@ export interface ParsedParagraphNode {
37
58
  };
38
59
  alignment?: "left" | "center" | "right" | "both" | "distribute";
39
60
  spacing?: ParagraphSpacing;
61
+ contextualSpacing?: boolean;
40
62
  indentation?: ParagraphIndentation;
41
63
  tabStops?: TabStop[];
42
64
  keepNext?: boolean;
@@ -49,6 +71,8 @@ export interface ParsedParagraphNode {
49
71
  bidi?: boolean;
50
72
  suppressLineNumbers?: boolean;
51
73
  cnfStyle?: string;
74
+ sectionProperties?: SectionProperties;
75
+ sectionPropertiesXml?: string;
52
76
  children: ParsedInlineNode[];
53
77
  rawXml: string;
54
78
  }
@@ -69,7 +93,10 @@ export type ParsedInlineNode =
69
93
  | ParsedVmlShapeInlineNode
70
94
  | ParsedBookmarkStartInlineNode
71
95
  | ParsedBookmarkEndInlineNode
72
- | ParsedFieldInlineNode;
96
+ | ParsedFootnoteRefInlineNode
97
+ | ParsedFieldInlineNode
98
+ | ParsedPermStartInlineNode
99
+ | ParsedPermEndInlineNode;
73
100
 
74
101
  export interface ParsedTextNode {
75
102
  type: "text";
@@ -104,6 +131,8 @@ export interface ParsedImageNode {
104
131
  contentType?: string;
105
132
  filename?: string;
106
133
  altText?: string;
134
+ widthEmu?: number;
135
+ heightEmu?: number;
107
136
  placementXml?: string;
108
137
  display?: "inline" | "floating";
109
138
  floating?: {
@@ -152,6 +181,7 @@ export interface ParsedShapeInlineNode {
152
181
  type: "shape";
153
182
  text?: string;
154
183
  geometry?: string;
184
+ isTextBox?: boolean;
155
185
  rawXml: string;
156
186
  }
157
187
 
@@ -182,11 +212,32 @@ export interface ParsedBookmarkEndInlineNode {
182
212
  rawXml: string;
183
213
  }
184
214
 
215
+ export interface ParsedFootnoteRefInlineNode {
216
+ type: "footnote_ref";
217
+ noteId: string;
218
+ noteKind: "footnote" | "endnote";
219
+ }
220
+
185
221
  export interface ParsedFieldInlineNode {
186
222
  type: "field";
187
- fieldType: "simple";
223
+ fieldType: "simple" | "complex";
188
224
  instruction: string;
189
- contentXml: string;
225
+ contentXml?: string;
226
+ children?: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
227
+ rawXml: string;
228
+ }
229
+
230
+ export interface ParsedPermStartInlineNode {
231
+ type: "perm_start";
232
+ rangeId: string;
233
+ editorGroup?: string;
234
+ editor?: string;
235
+ rawXml: string;
236
+ }
237
+
238
+ export interface ParsedPermEndInlineNode {
239
+ type: "perm_end";
240
+ rangeId: string;
190
241
  rawXml: string;
191
242
  }
192
243
 
@@ -195,6 +246,23 @@ export interface ParsedOpaqueBlockNode {
195
246
  rawXml: string;
196
247
  }
197
248
 
249
+ export interface ParsedSdtCheckboxState {
250
+ checked: boolean;
251
+ checkedChar?: string;
252
+ uncheckedChar?: string;
253
+ }
254
+
255
+ export interface ParsedSdtDatePickerState {
256
+ fullDate?: string;
257
+ dateFormat?: string;
258
+ lid?: string;
259
+ }
260
+
261
+ export interface ParsedSdtDropdownListItem {
262
+ displayText?: string;
263
+ value: string;
264
+ }
265
+
198
266
  export interface ParsedSdtNode {
199
267
  type: "sdt";
200
268
  properties: {
@@ -203,6 +271,11 @@ export interface ParsedSdtNode {
203
271
  tag?: string;
204
272
  lock?: string;
205
273
  propertiesXml?: string;
274
+ checkbox?: ParsedSdtCheckboxState;
275
+ datePicker?: ParsedSdtDatePickerState;
276
+ dropdownList?: ParsedSdtDropdownListItem[];
277
+ comboBox?: ParsedSdtDropdownListItem[];
278
+ showingPlcHdr?: boolean;
206
279
  };
207
280
  children: ParsedBlockNode[];
208
281
  rawXml: string;
@@ -290,11 +363,24 @@ export function parseMainDocumentXml(
290
363
  const bodyElement = findChildElement(documentElement, "body");
291
364
  const relationshipMap = new Map(relationships.map((relationship) => [relationship.id, relationship]));
292
365
 
293
- return {
294
- blocks: bodyElement.children
295
- .filter((node): node is XmlElementNode => node.type === "element")
296
- .map((node) => parseBodyChild(node, xml, relationshipMap, relationships, mediaParts, sourcePartPath)),
297
- };
366
+ const allBlocks = bodyElement.children
367
+ .filter((node): node is XmlElementNode => node.type === "element")
368
+ .map((node) => parseBodyChild(node, xml, relationshipMap, relationships, mediaParts, sourcePartPath));
369
+
370
+ // The last body-level sectPr is the final section properties (not an intermediate section break).
371
+ // Extract it from the blocks list and store it separately.
372
+ let finalSectionProperties: SectionProperties | undefined;
373
+ const blocks: ParsedBlockNode[] = [];
374
+ for (let i = 0; i < allBlocks.length; i++) {
375
+ const block = allBlocks[i];
376
+ if (block.type === "section_break" && i === allBlocks.length - 1) {
377
+ finalSectionProperties = block.sectionProperties;
378
+ } else {
379
+ blocks.push(block);
380
+ }
381
+ }
382
+
383
+ return { blocks, finalSectionProperties };
298
384
  }
299
385
 
300
386
  function parseBodyChild(
@@ -340,6 +426,10 @@ function parseBodyChild(
340
426
  return parseAltChunkElement(node, sourceXml);
341
427
  }
342
428
 
429
+ if (nodeType === "sectPr") {
430
+ return parseSectionBreakElement(node, sourceXml);
431
+ }
432
+
343
433
  if (nodeType !== "p") {
344
434
  return {
345
435
  type: "opaque_block",
@@ -351,6 +441,7 @@ function parseBodyChild(
351
441
  let numbering: ParsedParagraphNode["numbering"];
352
442
  let alignment: ParsedParagraphNode["alignment"];
353
443
  let spacing: ParsedParagraphNode["spacing"];
444
+ let contextualSpacing: ParsedParagraphNode["contextualSpacing"];
354
445
  let indentation: ParsedParagraphNode["indentation"];
355
446
  let tabStops: ParsedParagraphNode["tabStops"];
356
447
  let keepNext: ParsedParagraphNode["keepNext"];
@@ -363,8 +454,15 @@ function parseBodyChild(
363
454
  let bidi: ParsedParagraphNode["bidi"];
364
455
  let suppressLineNumbers: ParsedParagraphNode["suppressLineNumbers"];
365
456
  let cnfStyle: ParsedParagraphNode["cnfStyle"];
457
+ let sectionProperties: SectionProperties | undefined;
458
+ let sectionPropertiesXml: string | undefined;
366
459
  let paragraphSupported = true;
367
460
  const children: ParsedInlineNode[] = [];
461
+ let activeComplexField: {
462
+ instruction: string;
463
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
464
+ mode: "instruction" | "result";
465
+ } | null = null;
368
466
 
369
467
  for (const child of node.children) {
370
468
  if (child.type !== "element") {
@@ -377,6 +475,7 @@ function parseBodyChild(
377
475
  numbering = readParagraphNumbering(child);
378
476
  alignment = readParagraphAlignment(child);
379
477
  spacing = readParagraphSpacing(child);
478
+ contextualSpacing = readOptionalOnOffParagraphProperty(child, "contextualSpacing");
380
479
  indentation = readParagraphIndentation(child);
381
480
  tabStops = readParagraphTabStops(child);
382
481
  keepNext = readOnOffParagraphProperty(child, "keepNext");
@@ -389,36 +488,153 @@ function parseBodyChild(
389
488
  bidi = readOnOffParagraphProperty(child, "bidi");
390
489
  suppressLineNumbers = readOnOffParagraphProperty(child, "suppressLineNumbers");
391
490
  cnfStyle = readParagraphCnfStyle(child);
491
+ sectionProperties = readSectionPropertiesFromPPr(child);
492
+ sectionPropertiesXml = readSectionPropertiesXmlFromPPr(child, sourceXml);
392
493
  paragraphSupported = paragraphSupported && supportsParagraphProperties(child);
393
494
  break;
394
495
  case "r":
395
- children.push(...parseRun(child, sourceXml, relationships, mediaParts, sourcePartPath));
496
+ activeComplexField = appendParagraphRunNodes(
497
+ child,
498
+ children,
499
+ activeComplexField,
500
+ sourceXml,
501
+ relationships,
502
+ mediaParts,
503
+ sourcePartPath,
504
+ );
396
505
  break;
397
506
  case "hyperlink": {
507
+ flushActiveComplexField(children, () => {
508
+ activeComplexField = null;
509
+ }, activeComplexField);
398
510
  const hyperlink = parseHyperlink(child, sourceXml, relationshipMap);
399
511
  children.push(hyperlink);
400
512
  break;
401
513
  }
402
514
  case "ins":
403
515
  case "del": {
516
+ flushActiveComplexField(children, () => {
517
+ activeComplexField = null;
518
+ }, activeComplexField);
404
519
  children.push(...parseRevisionContainer(child, sourceXml, relationshipMap));
405
520
  break;
406
521
  }
407
522
  case "commentRangeStart":
408
523
  case "commentRangeEnd":
409
524
  break;
410
- case "bookmarkStart":
411
- case "bookmarkEnd":
412
- case "fldSimple":
413
- case "permStart":
414
- case "permEnd":
525
+ case "bookmarkStart": {
526
+ const bkId = child.attributes["w:id"] ?? child.attributes.id ?? "";
527
+ const bkName = child.attributes["w:name"] ?? child.attributes.name ?? "";
528
+ if (bkId) {
529
+ const bookmarkNode = {
530
+ type: "bookmark_start",
531
+ bookmarkId: bkId,
532
+ name: bkName,
533
+ rawXml: sourceXml.slice(child.start, child.end),
534
+ } as ParsedBookmarkStartInlineNode;
535
+ flushActiveComplexField(children, () => {
536
+ activeComplexField = null;
537
+ }, activeComplexField);
538
+ children.push(bookmarkNode);
539
+ } else {
540
+ flushActiveComplexField(children, () => {
541
+ activeComplexField = null;
542
+ }, activeComplexField);
543
+ children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
544
+ }
545
+ break;
546
+ }
547
+ case "bookmarkEnd": {
548
+ const bkEndId = child.attributes["w:id"] ?? child.attributes.id ?? "";
549
+ if (bkEndId) {
550
+ const bookmarkNode = {
551
+ type: "bookmark_end",
552
+ bookmarkId: bkEndId,
553
+ rawXml: sourceXml.slice(child.start, child.end),
554
+ } as ParsedBookmarkEndInlineNode;
555
+ flushActiveComplexField(children, () => {
556
+ activeComplexField = null;
557
+ }, activeComplexField);
558
+ children.push(bookmarkNode);
559
+ } else {
560
+ flushActiveComplexField(children, () => {
561
+ activeComplexField = null;
562
+ }, activeComplexField);
563
+ children.push({ type: "opaque_inline", rawXml: sourceXml.slice(child.start, child.end) });
564
+ }
565
+ break;
566
+ }
567
+ case "fldSimple": {
568
+ flushActiveComplexField(children, () => {
569
+ activeComplexField = null;
570
+ }, activeComplexField);
571
+ const fieldInstr = child.attributes["w:instr"] ?? child.attributes.instr ?? "";
572
+ const fieldContentXml = child.children
573
+ .filter((c): c is XmlElementNode => c.type === "element")
574
+ .map((c) => sourceXml.slice(c.start, c.end))
575
+ .join("");
576
+ children.push({
577
+ type: "field",
578
+ fieldType: "simple",
579
+ instruction: fieldInstr,
580
+ contentXml: fieldContentXml,
581
+ rawXml: sourceXml.slice(child.start, child.end),
582
+ } as ParsedFieldInlineNode);
583
+ break;
584
+ }
415
585
  case "proofErr":
586
+ flushActiveComplexField(children, () => {
587
+ activeComplexField = null;
588
+ }, activeComplexField);
416
589
  children.push({
417
590
  type: "opaque_inline",
418
591
  rawXml: sourceXml.slice(child.start, child.end),
419
592
  });
420
593
  break;
594
+ case "permStart":
595
+ flushActiveComplexField(children, () => {
596
+ activeComplexField = null;
597
+ }, activeComplexField);
598
+ children.push(parsePermStartNode(child, sourceXml));
599
+ break;
600
+ case "permEnd":
601
+ flushActiveComplexField(children, () => {
602
+ activeComplexField = null;
603
+ }, activeComplexField);
604
+ children.push(parsePermEndNode(child, sourceXml));
605
+ break;
606
+ case "footnoteReference": {
607
+ flushActiveComplexField(children, () => {
608
+ activeComplexField = null;
609
+ }, activeComplexField);
610
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
611
+ if (noteId) {
612
+ children.push({
613
+ type: "footnote_ref",
614
+ noteId,
615
+ noteKind: "footnote",
616
+ });
617
+ }
618
+ break;
619
+ }
620
+ case "endnoteReference": {
621
+ flushActiveComplexField(children, () => {
622
+ activeComplexField = null;
623
+ }, activeComplexField);
624
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
625
+ if (noteId) {
626
+ children.push({
627
+ type: "footnote_ref",
628
+ noteId,
629
+ noteKind: "endnote",
630
+ });
631
+ }
632
+ break;
633
+ }
421
634
  default:
635
+ flushActiveComplexField(children, () => {
636
+ activeComplexField = null;
637
+ }, activeComplexField);
422
638
  children.push({
423
639
  type: "opaque_inline",
424
640
  rawXml: sourceXml.slice(child.start, child.end),
@@ -427,6 +643,10 @@ function parseBodyChild(
427
643
  }
428
644
  }
429
645
 
646
+ flushActiveComplexField(children, () => {
647
+ activeComplexField = null;
648
+ }, activeComplexField);
649
+
430
650
  if (!paragraphSupported) {
431
651
  return {
432
652
  type: "opaque_block",
@@ -440,6 +660,7 @@ function parseBodyChild(
440
660
  ...(numbering ? { numbering } : {}),
441
661
  ...(alignment ? { alignment } : {}),
442
662
  ...(spacing ? { spacing } : {}),
663
+ ...(contextualSpacing !== undefined ? { contextualSpacing } : {}),
443
664
  ...(indentation ? { indentation } : {}),
444
665
  ...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
445
666
  ...(keepNext ? { keepNext } : {}),
@@ -452,11 +673,121 @@ function parseBodyChild(
452
673
  ...(bidi ? { bidi } : {}),
453
674
  ...(suppressLineNumbers ? { suppressLineNumbers } : {}),
454
675
  ...(cnfStyle ? { cnfStyle } : {}),
676
+ ...(sectionProperties ? { sectionProperties } : {}),
677
+ ...(sectionPropertiesXml ? { sectionPropertiesXml } : {}),
455
678
  children,
456
679
  rawXml: sourceXml.slice(node.start, node.end),
457
680
  };
458
681
  }
459
682
 
683
+ function appendParagraphRunNodes(
684
+ node: XmlElementNode,
685
+ children: ParsedInlineNode[],
686
+ activeComplexField: {
687
+ instruction: string;
688
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
689
+ mode: "instruction" | "result";
690
+ } | null,
691
+ sourceXml: string,
692
+ relationships: readonly OpcRelationship[],
693
+ mediaParts: ReadonlyMap<string, InlineMediaPart>,
694
+ sourcePartPath: string,
695
+ ): {
696
+ instruction: string;
697
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
698
+ mode: "instruction" | "result";
699
+ } | null {
700
+ const hasFieldMarkers = node.children.some(
701
+ (child) =>
702
+ child.type === "element" &&
703
+ (localName(child.name) === "fldChar" || localName(child.name) === "instrText"),
704
+ );
705
+
706
+ if (activeComplexField?.mode === "result" && !hasFieldMarkers) {
707
+ const run = parseRunContentOnly(node, sourceXml);
708
+ if (
709
+ run.supported &&
710
+ run.nodes.every(
711
+ (child) =>
712
+ child.type === "text" || child.type === "hard_break" || child.type === "tab",
713
+ )
714
+ ) {
715
+ activeComplexField.children.push(...run.nodes);
716
+ return activeComplexField;
717
+ }
718
+ }
719
+
720
+ if (!hasFieldMarkers) {
721
+ children.push(...parseRun(node, sourceXml, relationships, mediaParts, sourcePartPath));
722
+ return activeComplexField;
723
+ }
724
+
725
+ for (const child of node.children) {
726
+ if (child.type !== "element") {
727
+ continue;
728
+ }
729
+
730
+ const name = localName(child.name);
731
+ if (name === "fldChar") {
732
+ const fldType = child.attributes["w:fldCharType"] ?? child.attributes.fldCharType;
733
+ if (fldType === "begin") {
734
+ activeComplexField = { instruction: "", children: [], mode: "instruction" };
735
+ } else if (fldType === "separate" && activeComplexField) {
736
+ activeComplexField.mode = "result";
737
+ } else if (fldType === "end" && activeComplexField) {
738
+ flushActiveComplexField(children, () => {
739
+ activeComplexField = null;
740
+ }, activeComplexField);
741
+ }
742
+ continue;
743
+ }
744
+
745
+ if (name === "instrText") {
746
+ const instruction = child.children
747
+ .filter((entry): entry is XmlTextNode => entry.type === "text")
748
+ .map((entry) => entry.text)
749
+ .join("");
750
+ if (activeComplexField) {
751
+ activeComplexField.instruction += instruction;
752
+ } else if (instruction.trim().length > 0) {
753
+ children.push({
754
+ type: "field",
755
+ fieldType: "complex",
756
+ instruction,
757
+ children: [],
758
+ rawXml: sourceXml.slice(node.start, node.end),
759
+ });
760
+ }
761
+ continue;
762
+ }
763
+ }
764
+
765
+ return activeComplexField;
766
+ }
767
+
768
+ function flushActiveComplexField(
769
+ children: ParsedInlineNode[],
770
+ reset: () => void,
771
+ activeComplexField: {
772
+ instruction: string;
773
+ children: Array<ParsedTextNode | ParsedBreakNode | ParsedTabNode>;
774
+ mode: "instruction" | "result";
775
+ } | null,
776
+ ): void {
777
+ if (!activeComplexField || activeComplexField.instruction.trim().length === 0) {
778
+ return;
779
+ }
780
+
781
+ children.push({
782
+ type: "field",
783
+ fieldType: "complex",
784
+ instruction: activeComplexField.instruction,
785
+ children: activeComplexField.children,
786
+ rawXml: "",
787
+ });
788
+ reset();
789
+ }
790
+
460
791
  function parseTableElement(
461
792
  node: XmlElementNode,
462
793
  sourceXml: string,
@@ -692,7 +1023,68 @@ function readSdtProperties(
692
1023
  properties.lock = readOptionalAttribute(child, "val");
693
1024
  continue;
694
1025
  }
695
- if (!properties.sdtType && name !== "id" && name !== "placeholder" && name !== "showingPlcHdr") {
1026
+ if (name === "showingPlcHdr") {
1027
+ const val = readOptionalAttribute(child, "val");
1028
+ properties.showingPlcHdr = val !== "false" && val !== "0";
1029
+ continue;
1030
+ }
1031
+
1032
+ // Checkbox (w14:checkbox)
1033
+ if (name === "checkbox") {
1034
+ properties.sdtType = "checkbox";
1035
+ const checkedNode = findFirstDescendant(child, "checked");
1036
+ const checkedVal = checkedNode ? (readOptionalAttribute(checkedNode, "val") ?? "0") : "0";
1037
+ const checkedCharNode = findFirstDescendant(child, "checkedState");
1038
+ const uncheckedCharNode = findFirstDescendant(child, "uncheckedState");
1039
+ properties.checkbox = {
1040
+ checked: checkedVal === "1" || checkedVal === "true",
1041
+ ...(checkedCharNode ? { checkedChar: readOptionalAttribute(checkedCharNode, "val") } : {}),
1042
+ ...(uncheckedCharNode ? { uncheckedChar: readOptionalAttribute(uncheckedCharNode, "val") } : {}),
1043
+ };
1044
+ continue;
1045
+ }
1046
+
1047
+ // Date picker
1048
+ if (name === "date") {
1049
+ properties.sdtType = "date";
1050
+ const fullDate = readOptionalAttribute(child, "fullDate");
1051
+ const dateFormatNode = findFirstChild(child, "dateFormat");
1052
+ const lidNode = findFirstChild(child, "lid");
1053
+ properties.datePicker = {
1054
+ ...(fullDate ? { fullDate } : {}),
1055
+ ...(dateFormatNode ? { dateFormat: readOptionalAttribute(dateFormatNode, "val") } : {}),
1056
+ ...(lidNode ? { lid: readOptionalAttribute(lidNode, "val") } : {}),
1057
+ };
1058
+ continue;
1059
+ }
1060
+
1061
+ // Dropdown list
1062
+ if (name === "dropDownList") {
1063
+ properties.sdtType = "dropDownList";
1064
+ properties.dropdownList = readSdtListItems(child);
1065
+ continue;
1066
+ }
1067
+
1068
+ // Combo box
1069
+ if (name === "comboBox") {
1070
+ properties.sdtType = "comboBox";
1071
+ properties.comboBox = readSdtListItems(child);
1072
+ continue;
1073
+ }
1074
+
1075
+ // Plain text
1076
+ if (name === "text") {
1077
+ properties.sdtType = "plainText";
1078
+ continue;
1079
+ }
1080
+
1081
+ // Rich text (richText element is the default, but if explicitly present, tag it)
1082
+ if (name === "richText") {
1083
+ properties.sdtType = "richText";
1084
+ continue;
1085
+ }
1086
+
1087
+ if (!properties.sdtType && name !== "id" && name !== "placeholder" && name !== "showingPlcHdr" && name !== "rPr") {
696
1088
  properties.sdtType = name;
697
1089
  }
698
1090
  }
@@ -700,6 +1092,34 @@ function readSdtProperties(
700
1092
  return properties;
701
1093
  }
702
1094
 
1095
+ function readSdtListItems(node: XmlElementNode): ParsedSdtDropdownListItem[] {
1096
+ const items: ParsedSdtDropdownListItem[] = [];
1097
+ for (const child of node.children) {
1098
+ if (child.type !== "element" || localName(child.name) !== "listItem") continue;
1099
+ const value = readOptionalAttribute(child, "value") ?? "";
1100
+ const displayText = readOptionalAttribute(child, "displayText");
1101
+ items.push({ value, ...(displayText ? { displayText } : {}) });
1102
+ }
1103
+ return items;
1104
+ }
1105
+
1106
+ function findFirstChild(node: XmlElementNode, local: string): XmlElementNode | undefined {
1107
+ for (const child of node.children) {
1108
+ if (child.type === "element" && localName(child.name) === local) return child;
1109
+ }
1110
+ return undefined;
1111
+ }
1112
+
1113
+ function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
1114
+ for (const child of node.children) {
1115
+ if (child.type !== "element") continue;
1116
+ if (localName(child.name) === local) return child;
1117
+ const nested = findFirstDescendant(child, local);
1118
+ if (nested) return nested;
1119
+ }
1120
+ return undefined;
1121
+ }
1122
+
703
1123
  function readTableStyleId(node: XmlElementNode): string | undefined {
704
1124
  for (const child of node.children) {
705
1125
  if (child.type !== "element" || localName(child.name) !== "tblStyle") continue;
@@ -762,9 +1182,35 @@ function readTableGridColumns(node: XmlElementNode): number[] {
762
1182
  */
763
1183
  function tableRequiresOpaquePreservation(rawXml: string): boolean {
764
1184
  // Safe table-local content now includes hyperlinks, bookmarks, comments,
765
- // nested tables, floating images, and VML preview atoms because the parser
766
- // and serializer can preserve them without degrading the whole table.
767
- return /<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|fldChar|fldSimple|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml);
1185
+ // nested tables, floating images, VML preview atoms, and bounded field
1186
+ // families already owned by the current field slice. Risky table-local
1187
+ // semantics still fail closed to preserve-only.
1188
+ if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|gridAfter|gridBefore|tblCellSpacing)\b/.test(rawXml)) {
1189
+ return true;
1190
+ }
1191
+
1192
+ const fldSimpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
1193
+ const complexInstructions = [...rawXml.matchAll(/<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu)]
1194
+ .map((match) => decodeXmlEntities(match[1] ?? ""));
1195
+ for (const instruction of [...fldSimpleInstructions, ...complexInstructions]) {
1196
+ const classification = classifyFieldInstruction(instruction);
1197
+ if (!isSafeMainStoryTableFieldFamily(classification.family)) {
1198
+ return true;
1199
+ }
1200
+ }
1201
+
1202
+ return false;
1203
+ }
1204
+
1205
+ function isSafeMainStoryTableFieldFamily(family: string): boolean {
1206
+ return (
1207
+ family === "REF" ||
1208
+ family === "PAGEREF" ||
1209
+ family === "NOTEREF" ||
1210
+ family === "TOC" ||
1211
+ family === "PAGE" ||
1212
+ family === "NUMPAGES"
1213
+ );
768
1214
  }
769
1215
 
770
1216
  function readCellGridSpan(node: XmlElementNode): number | undefined {
@@ -935,6 +1381,18 @@ function readOnOffParagraphProperty(node: XmlElementNode, name: string): boolean
935
1381
  return val !== "false" && val !== "0" && val !== "off" ? true : undefined;
936
1382
  }
937
1383
 
1384
+ function readOptionalOnOffParagraphProperty(
1385
+ node: XmlElementNode,
1386
+ name: string,
1387
+ ): boolean | undefined {
1388
+ const propNode = node.children.find(
1389
+ (child): child is XmlElementNode => child.type === "element" && localName(child.name) === name,
1390
+ );
1391
+ if (!propNode) return undefined;
1392
+ const val = (propNode.attributes["w:val"] ?? propNode.attributes.val ?? "true").toLowerCase();
1393
+ return val !== "false" && val !== "0" && val !== "off";
1394
+ }
1395
+
938
1396
  function readParagraphOutlineLevel(node: XmlElementNode): number | undefined {
939
1397
  const propNode = node.children.find(
940
1398
  (child): child is XmlElementNode => child.type === "element" && localName(child.name) === "outlineLvl",
@@ -1199,6 +1657,8 @@ function parseRun(
1199
1657
  contentType: media.contentType,
1200
1658
  filename: media.filename,
1201
1659
  ...(media.altText ? { altText: media.altText } : {}),
1660
+ ...(media.widthEmu !== undefined ? { widthEmu: media.widthEmu } : {}),
1661
+ ...(media.heightEmu !== undefined ? { heightEmu: media.heightEmu } : {}),
1202
1662
  placementXml,
1203
1663
  ...(media.display ? { display: media.display } : {}),
1204
1664
  ...(media.floating ? { floating: media.floating } : {}),
@@ -1231,6 +1691,28 @@ function parseRun(
1231
1691
  }
1232
1692
  case "commentReference":
1233
1693
  break;
1694
+ case "footnoteReference": {
1695
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
1696
+ if (noteId) {
1697
+ result.push({
1698
+ type: "footnote_ref",
1699
+ noteId,
1700
+ noteKind: "footnote",
1701
+ });
1702
+ }
1703
+ break;
1704
+ }
1705
+ case "endnoteReference": {
1706
+ const noteId = child.attributes["w:id"] ?? child.attributes.id ?? "";
1707
+ if (noteId) {
1708
+ result.push({
1709
+ type: "footnote_ref",
1710
+ noteId,
1711
+ noteKind: "endnote",
1712
+ });
1713
+ }
1714
+ break;
1715
+ }
1234
1716
  case "lastRenderedPageBreak":
1235
1717
  case "proofErr":
1236
1718
  result.push({
@@ -1310,8 +1792,6 @@ function parseRevisionContainer(
1310
1792
  case "commentRangeEnd":
1311
1793
  case "bookmarkStart":
1312
1794
  case "bookmarkEnd":
1313
- case "permStart":
1314
- case "permEnd":
1315
1795
  case "proofErr":
1316
1796
  case "lastRenderedPageBreak":
1317
1797
  return [
@@ -1320,6 +1800,10 @@ function parseRevisionContainer(
1320
1800
  rawXml: sourceXml.slice(node.start, node.end),
1321
1801
  },
1322
1802
  ];
1803
+ case "permStart":
1804
+ return [parsePermStartNode(node, sourceXml)];
1805
+ case "permEnd":
1806
+ return [parsePermEndNode(node, sourceXml)];
1323
1807
  default:
1324
1808
  return [
1325
1809
  {
@@ -1541,6 +2025,11 @@ function readRunMarks(node: XmlElementNode, sourceXml: string): MarksParseResult
1541
2025
  marks.push(backgroundColorMark);
1542
2026
  }
1543
2027
 
2028
+ const highlightMark = readRunHighlight(properties);
2029
+ if (highlightMark) {
2030
+ marks.push(highlightMark);
2031
+ }
2032
+
1544
2033
  const charSpacingMark = readNumericRunMark(properties, "spacing", "charSpacing");
1545
2034
  if (charSpacingMark) {
1546
2035
  marks.push(charSpacingMark);
@@ -1653,6 +2142,28 @@ function readRunBackgroundColor(properties: XmlElementNode): TextMark | undefine
1653
2142
  return { type: "backgroundColor", color: fill };
1654
2143
  }
1655
2144
 
2145
+ function readRunHighlight(properties: XmlElementNode): TextMark | undefined {
2146
+ const highlightNode = properties.children.find(
2147
+ (child): child is XmlElementNode =>
2148
+ child.type === "element" && localName(child.name) === "highlight",
2149
+ );
2150
+ if (!highlightNode) {
2151
+ return undefined;
2152
+ }
2153
+
2154
+ const highlightValue = highlightNode.attributes["w:val"] ?? highlightNode.attributes.val;
2155
+ const resolvedHighlight = resolveHighlightColor(highlightValue);
2156
+ if (!resolvedHighlight) {
2157
+ return undefined;
2158
+ }
2159
+
2160
+ return {
2161
+ type: "highlight",
2162
+ color: resolvedHighlight.color,
2163
+ val: resolvedHighlight.val,
2164
+ };
2165
+ }
2166
+
1656
2167
  function readNumericRunMark(
1657
2168
  properties: XmlElementNode,
1658
2169
  elementName: "spacing" | "kern" | "position",
@@ -2007,3 +2518,279 @@ function decodeXmlEntities(value: string): string {
2007
2518
  }
2008
2519
  });
2009
2520
  }
2521
+
2522
+ // ---- Section properties parsing ----
2523
+
2524
+ function parseSectionBreakElement(
2525
+ node: XmlElementNode,
2526
+ sourceXml: string,
2527
+ ): ParsedSectionBreakNode {
2528
+ const props = parseSectionPropertiesFromElement(node);
2529
+ return {
2530
+ type: "section_break",
2531
+ sectionPropertiesXml: sourceXml.slice(node.start, node.end),
2532
+ sectionProperties: props,
2533
+ };
2534
+ }
2535
+
2536
+ function readSectionPropertiesFromPPr(
2537
+ pPrNode: XmlElementNode,
2538
+ ): SectionProperties | undefined {
2539
+ for (const child of pPrNode.children) {
2540
+ if (child.type === "element" && localName(child.name) === "sectPr") {
2541
+ return parseSectionPropertiesFromElement(child);
2542
+ }
2543
+ }
2544
+ return undefined;
2545
+ }
2546
+
2547
+ function readSectionPropertiesXmlFromPPr(
2548
+ pPrNode: XmlElementNode,
2549
+ sourceXml: string,
2550
+ ): string | undefined {
2551
+ for (const child of pPrNode.children) {
2552
+ if (child.type === "element" && localName(child.name) === "sectPr") {
2553
+ return sourceXml.slice(child.start, child.end);
2554
+ }
2555
+ }
2556
+ return undefined;
2557
+ }
2558
+
2559
+ export function parseSectionPropertiesFromElement(
2560
+ node: XmlElementNode,
2561
+ ): SectionProperties {
2562
+ const props: SectionProperties = {};
2563
+
2564
+ for (const child of node.children) {
2565
+ if (child.type !== "element") continue;
2566
+ const name = localName(child.name);
2567
+
2568
+ switch (name) {
2569
+ case "pgSz": {
2570
+ const w = safeParseInt(child.attributes["w:w"]);
2571
+ const h = safeParseInt(child.attributes["w:h"]);
2572
+ if (w !== undefined && h !== undefined) {
2573
+ const pageSize: PageSize = { width: w, height: h };
2574
+ const orient = child.attributes["w:orient"];
2575
+ if (orient === "landscape" || orient === "portrait") {
2576
+ pageSize.orientation = orient;
2577
+ }
2578
+ props.pageSize = pageSize;
2579
+ }
2580
+ break;
2581
+ }
2582
+ case "pgMar": {
2583
+ const top = safeParseInt(child.attributes["w:top"]);
2584
+ const right = safeParseInt(child.attributes["w:right"]);
2585
+ const bottom = safeParseInt(child.attributes["w:bottom"]);
2586
+ const left = safeParseInt(child.attributes["w:left"]);
2587
+ if (top !== undefined && right !== undefined && bottom !== undefined && left !== undefined) {
2588
+ const margins: PageMargins = { top, right, bottom, left };
2589
+ const header = safeParseInt(child.attributes["w:header"]);
2590
+ const footer = safeParseInt(child.attributes["w:footer"]);
2591
+ const gutter = safeParseInt(child.attributes["w:gutter"]);
2592
+ if (header !== undefined) margins.header = header;
2593
+ if (footer !== undefined) margins.footer = footer;
2594
+ if (gutter !== undefined) margins.gutter = gutter;
2595
+ props.pageMargins = margins;
2596
+ }
2597
+ break;
2598
+ }
2599
+ case "cols": {
2600
+ const columns: ColumnProperties = {};
2601
+ const num = safeParseInt(child.attributes["w:num"]);
2602
+ const space = safeParseInt(child.attributes["w:space"]);
2603
+ const equalWidth = child.attributes["w:equalWidth"];
2604
+ const sep = child.attributes["w:sep"];
2605
+ if (num !== undefined) columns.count = num;
2606
+ if (space !== undefined) columns.space = space;
2607
+ if (equalWidth !== undefined) columns.equalWidth = equalWidth !== "0" && equalWidth !== "false";
2608
+ if (sep === "1" || sep === "true") columns.separator = true;
2609
+ const colDefs: Array<{ width: number; space?: number }> = [];
2610
+ for (const colChild of child.children) {
2611
+ if (colChild.type === "element" && localName(colChild.name) === "col") {
2612
+ const colW = safeParseInt(colChild.attributes["w:w"]);
2613
+ const colSpace = safeParseInt(colChild.attributes["w:space"]);
2614
+ if (colW !== undefined) {
2615
+ colDefs.push(colSpace !== undefined ? { width: colW, space: colSpace } : { width: colW });
2616
+ }
2617
+ }
2618
+ }
2619
+ if (colDefs.length > 0) columns.columns = colDefs;
2620
+ if (Object.keys(columns).length > 0) props.columns = columns;
2621
+ break;
2622
+ }
2623
+ case "pgNumType": {
2624
+ const numbering: PageNumbering = {};
2625
+ const fmt = child.attributes["w:fmt"];
2626
+ const start = safeParseInt(child.attributes["w:start"]);
2627
+ const chapStyle = child.attributes["w:chapStyle"];
2628
+ const chapSep = child.attributes["w:chapSep"];
2629
+ if (fmt) numbering.format = fmt;
2630
+ if (start !== undefined) numbering.start = start;
2631
+ if (chapStyle) numbering.chapStyle = chapStyle;
2632
+ if (chapSep) numbering.chapSep = chapSep;
2633
+ if (Object.keys(numbering).length > 0) props.pageNumbering = numbering;
2634
+ break;
2635
+ }
2636
+ case "lnNumType": {
2637
+ const lineNumbering: SectionLineNumbering = {};
2638
+ const countBy = safeParseInt(child.attributes["w:countBy"]);
2639
+ const start = safeParseInt(child.attributes["w:start"]);
2640
+ const distance = safeParseInt(child.attributes["w:distance"]);
2641
+ const restart = child.attributes["w:restart"];
2642
+ if (countBy !== undefined) lineNumbering.countBy = countBy;
2643
+ if (start !== undefined) lineNumbering.start = start;
2644
+ if (distance !== undefined) lineNumbering.distance = distance;
2645
+ if (
2646
+ restart === "newPage" ||
2647
+ restart === "newSection" ||
2648
+ restart === "continuous"
2649
+ ) {
2650
+ lineNumbering.restart = restart;
2651
+ }
2652
+ if (Object.keys(lineNumbering).length > 0) {
2653
+ props.lineNumbering = lineNumbering;
2654
+ }
2655
+ break;
2656
+ }
2657
+ case "pgBorders": {
2658
+ const pageBorders: SectionPageBorders = {};
2659
+ const offsetFrom = child.attributes["w:offsetFrom"];
2660
+ const display = child.attributes["w:display"];
2661
+ const zOrder = child.attributes["w:zOrder"];
2662
+ if (offsetFrom === "page" || offsetFrom === "text") {
2663
+ pageBorders.offsetFrom = offsetFrom;
2664
+ }
2665
+ if (
2666
+ display === "allPages" ||
2667
+ display === "firstPage" ||
2668
+ display === "notFirstPage"
2669
+ ) {
2670
+ pageBorders.display = display;
2671
+ }
2672
+ if (zOrder === "front" || zOrder === "back") {
2673
+ pageBorders.zOrder = zOrder;
2674
+ }
2675
+ for (const borderChild of child.children) {
2676
+ if (borderChild.type !== "element") continue;
2677
+ const borderName = localName(borderChild.name);
2678
+ const border = parseBorderSpec(borderChild);
2679
+ if (!border) continue;
2680
+ if (
2681
+ borderName === "top" ||
2682
+ borderName === "left" ||
2683
+ borderName === "bottom" ||
2684
+ borderName === "right"
2685
+ ) {
2686
+ pageBorders[borderName] = border;
2687
+ }
2688
+ }
2689
+ if (Object.keys(pageBorders).length > 0) {
2690
+ props.pageBorders = pageBorders;
2691
+ }
2692
+ break;
2693
+ }
2694
+ case "docGrid": {
2695
+ const documentGrid: SectionDocumentGrid = {};
2696
+ const type = child.attributes["w:type"];
2697
+ const linePitch = safeParseInt(child.attributes["w:linePitch"]);
2698
+ const charSpace = safeParseInt(child.attributes["w:charSpace"]);
2699
+ if (
2700
+ type === "default" ||
2701
+ type === "lines" ||
2702
+ type === "linesAndChars" ||
2703
+ type === "snapToChars"
2704
+ ) {
2705
+ documentGrid.type = type;
2706
+ }
2707
+ if (linePitch !== undefined) documentGrid.linePitch = linePitch;
2708
+ if (charSpace !== undefined) documentGrid.charSpace = charSpace;
2709
+ if (Object.keys(documentGrid).length > 0) {
2710
+ props.documentGrid = documentGrid;
2711
+ }
2712
+ break;
2713
+ }
2714
+ case "headerReference": {
2715
+ const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
2716
+ const rId = child.attributes["r:id"];
2717
+ if (variant && rId) {
2718
+ if (!props.headerReferences) props.headerReferences = [];
2719
+ props.headerReferences.push({ variant, relationshipId: rId });
2720
+ }
2721
+ break;
2722
+ }
2723
+ case "footerReference": {
2724
+ const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
2725
+ const rId = child.attributes["r:id"];
2726
+ if (variant && rId) {
2727
+ if (!props.footerReferences) props.footerReferences = [];
2728
+ props.footerReferences.push({ variant, relationshipId: rId });
2729
+ }
2730
+ break;
2731
+ }
2732
+ case "type": {
2733
+ const val = child.attributes["w:val"];
2734
+ if (val === "continuous" || val === "nextPage" || val === "evenPage" || val === "oddPage" || val === "nextColumn") {
2735
+ props.sectionType = val;
2736
+ }
2737
+ break;
2738
+ }
2739
+ case "titlePg": {
2740
+ const val = child.attributes["w:val"];
2741
+ props.titlePage = val !== "false" && val !== "0";
2742
+ break;
2743
+ }
2744
+ }
2745
+ }
2746
+
2747
+ return props;
2748
+ }
2749
+
2750
+ function safeParseInt(value: string | undefined): number | undefined {
2751
+ if (value === undefined) return undefined;
2752
+ const n = Number.parseInt(value, 10);
2753
+ return Number.isFinite(n) ? n : undefined;
2754
+ }
2755
+
2756
+ function parseBorderSpec(node: XmlElementNode): BorderSpec | undefined {
2757
+ const border: BorderSpec = {};
2758
+ const value = node.attributes["w:val"];
2759
+ const size = safeParseInt(node.attributes["w:sz"]);
2760
+ const space = safeParseInt(node.attributes["w:space"]);
2761
+ const color = node.attributes["w:color"];
2762
+
2763
+ if (value) border.value = value;
2764
+ if (size !== undefined) border.size = size;
2765
+ if (space !== undefined) border.space = space;
2766
+ if (color) border.color = color;
2767
+
2768
+ return Object.keys(border).length > 0 ? border : undefined;
2769
+ }
2770
+
2771
+ function parsePermStartNode(
2772
+ node: XmlElementNode,
2773
+ sourceXml: string,
2774
+ ): ParsedPermStartInlineNode {
2775
+ const rangeId = node.attributes["w:id"] ?? node.attributes.id ?? "";
2776
+ const edGrp = node.attributes["w:edGrp"] ?? node.attributes.edGrp;
2777
+ const ed = node.attributes["w:ed"] ?? node.attributes.ed;
2778
+ return {
2779
+ type: "perm_start",
2780
+ rangeId,
2781
+ ...(edGrp ? { editorGroup: edGrp } : {}),
2782
+ ...(ed ? { editor: ed } : {}),
2783
+ rawXml: sourceXml.slice(node.start, node.end),
2784
+ };
2785
+ }
2786
+
2787
+ function parsePermEndNode(
2788
+ node: XmlElementNode,
2789
+ sourceXml: string,
2790
+ ): ParsedPermEndInlineNode {
2791
+ return {
2792
+ type: "perm_end",
2793
+ rangeId: node.attributes["w:id"] ?? node.attributes.id ?? "",
2794
+ rawXml: sourceXml.slice(node.start, node.end),
2795
+ };
2796
+ }