@beyondwork/docx-react-component 1.0.17 → 1.0.19

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 (74) hide show
  1. package/README.md +8 -2
  2. package/package.json +32 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +374 -4
  5. package/src/api/session-state.ts +58 -0
  6. package/src/core/commands/formatting-commands.ts +1 -0
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +5 -1
  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 +329 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +1 -1
  16. package/src/index.ts +30 -0
  17. package/src/io/docx-session.ts +260 -39
  18. package/src/io/export/serialize-main-document.ts +202 -5
  19. package/src/io/export/serialize-numbering.ts +28 -7
  20. package/src/io/normalize/normalize-text.ts +63 -25
  21. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  22. package/src/io/ooxml/parse-footnotes.ts +212 -20
  23. package/src/io/ooxml/parse-headers-footers.ts +229 -25
  24. package/src/io/ooxml/parse-inline-media.ts +16 -0
  25. package/src/io/ooxml/parse-main-document.ts +411 -6
  26. package/src/io/ooxml/parse-numbering.ts +7 -0
  27. package/src/io/ooxml/parse-settings.ts +184 -0
  28. package/src/io/ooxml/parse-shapes.ts +25 -0
  29. package/src/io/ooxml/parse-styles.ts +463 -0
  30. package/src/io/ooxml/parse-theme.ts +32 -0
  31. package/src/model/canonical-document.ts +133 -3
  32. package/src/model/cds-1.0.0.ts +13 -0
  33. package/src/model/snapshot.ts +2 -1
  34. package/src/runtime/document-layout.ts +332 -0
  35. package/src/runtime/document-navigation.ts +564 -0
  36. package/src/runtime/document-runtime.ts +265 -35
  37. package/src/runtime/document-search.ts +145 -0
  38. package/src/runtime/numbering-prefix.ts +47 -26
  39. package/src/runtime/page-layout-estimation.ts +212 -0
  40. package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
  41. package/src/runtime/session-capabilities.ts +2 -0
  42. package/src/runtime/story-context.ts +164 -0
  43. package/src/runtime/story-targeting.ts +162 -0
  44. package/src/runtime/surface-projection.ts +239 -12
  45. package/src/runtime/table-schema.ts +87 -5
  46. package/src/runtime/view-state.ts +459 -0
  47. package/src/ui/WordReviewEditor.tsx +1902 -312
  48. package/src/ui/browser-export.ts +52 -0
  49. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  50. package/src/ui/headless/selection-helpers.ts +20 -0
  51. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  52. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  53. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  54. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
  57. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  58. package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
  59. package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
  60. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
  61. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  62. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  63. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
  64. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  65. package/src/ui-tailwind/index.ts +2 -1
  66. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
  68. package/src/ui-tailwind/theme/editor-theme.css +123 -0
  69. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  70. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
  71. package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
  72. package/src/validation/compatibility-engine.ts +92 -20
  73. package/src/validation/diagnostics.ts +1 -0
  74. package/src/validation/docx-comment-proof.ts +487 -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 {
@@ -18,6 +29,7 @@ import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
18
29
 
19
30
  export interface ParsedMainDocument {
20
31
  blocks: ParsedBlockNode[];
32
+ finalSectionProperties?: SectionProperties;
21
33
  }
22
34
 
23
35
  export type ParsedBlockNode =
@@ -26,8 +38,15 @@ export type ParsedBlockNode =
26
38
  | ParsedSdtNode
27
39
  | ParsedCustomXmlNode
28
40
  | ParsedAltChunkNode
41
+ | ParsedSectionBreakNode
29
42
  | ParsedOpaqueBlockNode;
30
43
 
44
+ export interface ParsedSectionBreakNode {
45
+ type: "section_break";
46
+ sectionPropertiesXml: string;
47
+ sectionProperties: SectionProperties;
48
+ }
49
+
31
50
  export interface ParsedParagraphNode {
32
51
  type: "paragraph";
33
52
  styleId?: string;
@@ -49,6 +68,8 @@ export interface ParsedParagraphNode {
49
68
  bidi?: boolean;
50
69
  suppressLineNumbers?: boolean;
51
70
  cnfStyle?: string;
71
+ sectionProperties?: SectionProperties;
72
+ sectionPropertiesXml?: string;
52
73
  children: ParsedInlineNode[];
53
74
  rawXml: string;
54
75
  }
@@ -152,6 +173,7 @@ export interface ParsedShapeInlineNode {
152
173
  type: "shape";
153
174
  text?: string;
154
175
  geometry?: string;
176
+ isTextBox?: boolean;
155
177
  rawXml: string;
156
178
  }
157
179
 
@@ -195,6 +217,23 @@ export interface ParsedOpaqueBlockNode {
195
217
  rawXml: string;
196
218
  }
197
219
 
220
+ export interface ParsedSdtCheckboxState {
221
+ checked: boolean;
222
+ checkedChar?: string;
223
+ uncheckedChar?: string;
224
+ }
225
+
226
+ export interface ParsedSdtDatePickerState {
227
+ fullDate?: string;
228
+ dateFormat?: string;
229
+ lid?: string;
230
+ }
231
+
232
+ export interface ParsedSdtDropdownListItem {
233
+ displayText?: string;
234
+ value: string;
235
+ }
236
+
198
237
  export interface ParsedSdtNode {
199
238
  type: "sdt";
200
239
  properties: {
@@ -203,6 +242,11 @@ export interface ParsedSdtNode {
203
242
  tag?: string;
204
243
  lock?: string;
205
244
  propertiesXml?: string;
245
+ checkbox?: ParsedSdtCheckboxState;
246
+ datePicker?: ParsedSdtDatePickerState;
247
+ dropdownList?: ParsedSdtDropdownListItem[];
248
+ comboBox?: ParsedSdtDropdownListItem[];
249
+ showingPlcHdr?: boolean;
206
250
  };
207
251
  children: ParsedBlockNode[];
208
252
  rawXml: string;
@@ -290,11 +334,24 @@ export function parseMainDocumentXml(
290
334
  const bodyElement = findChildElement(documentElement, "body");
291
335
  const relationshipMap = new Map(relationships.map((relationship) => [relationship.id, relationship]));
292
336
 
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
- };
337
+ const allBlocks = bodyElement.children
338
+ .filter((node): node is XmlElementNode => node.type === "element")
339
+ .map((node) => parseBodyChild(node, xml, relationshipMap, relationships, mediaParts, sourcePartPath));
340
+
341
+ // The last body-level sectPr is the final section properties (not an intermediate section break).
342
+ // Extract it from the blocks list and store it separately.
343
+ let finalSectionProperties: SectionProperties | undefined;
344
+ const blocks: ParsedBlockNode[] = [];
345
+ for (let i = 0; i < allBlocks.length; i++) {
346
+ const block = allBlocks[i];
347
+ if (block.type === "section_break" && i === allBlocks.length - 1) {
348
+ finalSectionProperties = block.sectionProperties;
349
+ } else {
350
+ blocks.push(block);
351
+ }
352
+ }
353
+
354
+ return { blocks, finalSectionProperties };
298
355
  }
299
356
 
300
357
  function parseBodyChild(
@@ -340,6 +397,10 @@ function parseBodyChild(
340
397
  return parseAltChunkElement(node, sourceXml);
341
398
  }
342
399
 
400
+ if (nodeType === "sectPr") {
401
+ return parseSectionBreakElement(node, sourceXml);
402
+ }
403
+
343
404
  if (nodeType !== "p") {
344
405
  return {
345
406
  type: "opaque_block",
@@ -363,6 +424,8 @@ function parseBodyChild(
363
424
  let bidi: ParsedParagraphNode["bidi"];
364
425
  let suppressLineNumbers: ParsedParagraphNode["suppressLineNumbers"];
365
426
  let cnfStyle: ParsedParagraphNode["cnfStyle"];
427
+ let sectionProperties: SectionProperties | undefined;
428
+ let sectionPropertiesXml: string | undefined;
366
429
  let paragraphSupported = true;
367
430
  const children: ParsedInlineNode[] = [];
368
431
 
@@ -389,6 +452,8 @@ function parseBodyChild(
389
452
  bidi = readOnOffParagraphProperty(child, "bidi");
390
453
  suppressLineNumbers = readOnOffParagraphProperty(child, "suppressLineNumbers");
391
454
  cnfStyle = readParagraphCnfStyle(child);
455
+ sectionProperties = readSectionPropertiesFromPPr(child);
456
+ sectionPropertiesXml = readSectionPropertiesXmlFromPPr(child, sourceXml);
392
457
  paragraphSupported = paragraphSupported && supportsParagraphProperties(child);
393
458
  break;
394
459
  case "r":
@@ -452,6 +517,8 @@ function parseBodyChild(
452
517
  ...(bidi ? { bidi } : {}),
453
518
  ...(suppressLineNumbers ? { suppressLineNumbers } : {}),
454
519
  ...(cnfStyle ? { cnfStyle } : {}),
520
+ ...(sectionProperties ? { sectionProperties } : {}),
521
+ ...(sectionPropertiesXml ? { sectionPropertiesXml } : {}),
455
522
  children,
456
523
  rawXml: sourceXml.slice(node.start, node.end),
457
524
  };
@@ -692,7 +759,68 @@ function readSdtProperties(
692
759
  properties.lock = readOptionalAttribute(child, "val");
693
760
  continue;
694
761
  }
695
- if (!properties.sdtType && name !== "id" && name !== "placeholder" && name !== "showingPlcHdr") {
762
+ if (name === "showingPlcHdr") {
763
+ const val = readOptionalAttribute(child, "val");
764
+ properties.showingPlcHdr = val !== "false" && val !== "0";
765
+ continue;
766
+ }
767
+
768
+ // Checkbox (w14:checkbox)
769
+ if (name === "checkbox") {
770
+ properties.sdtType = "checkbox";
771
+ const checkedNode = findFirstDescendant(child, "checked");
772
+ const checkedVal = checkedNode ? (readOptionalAttribute(checkedNode, "val") ?? "0") : "0";
773
+ const checkedCharNode = findFirstDescendant(child, "checkedState");
774
+ const uncheckedCharNode = findFirstDescendant(child, "uncheckedState");
775
+ properties.checkbox = {
776
+ checked: checkedVal === "1" || checkedVal === "true",
777
+ ...(checkedCharNode ? { checkedChar: readOptionalAttribute(checkedCharNode, "val") } : {}),
778
+ ...(uncheckedCharNode ? { uncheckedChar: readOptionalAttribute(uncheckedCharNode, "val") } : {}),
779
+ };
780
+ continue;
781
+ }
782
+
783
+ // Date picker
784
+ if (name === "date") {
785
+ properties.sdtType = "date";
786
+ const fullDate = readOptionalAttribute(child, "fullDate");
787
+ const dateFormatNode = findFirstChild(child, "dateFormat");
788
+ const lidNode = findFirstChild(child, "lid");
789
+ properties.datePicker = {
790
+ ...(fullDate ? { fullDate } : {}),
791
+ ...(dateFormatNode ? { dateFormat: readOptionalAttribute(dateFormatNode, "val") } : {}),
792
+ ...(lidNode ? { lid: readOptionalAttribute(lidNode, "val") } : {}),
793
+ };
794
+ continue;
795
+ }
796
+
797
+ // Dropdown list
798
+ if (name === "dropDownList") {
799
+ properties.sdtType = "dropDownList";
800
+ properties.dropdownList = readSdtListItems(child);
801
+ continue;
802
+ }
803
+
804
+ // Combo box
805
+ if (name === "comboBox") {
806
+ properties.sdtType = "comboBox";
807
+ properties.comboBox = readSdtListItems(child);
808
+ continue;
809
+ }
810
+
811
+ // Plain text
812
+ if (name === "text") {
813
+ properties.sdtType = "plainText";
814
+ continue;
815
+ }
816
+
817
+ // Rich text (richText element is the default, but if explicitly present, tag it)
818
+ if (name === "richText") {
819
+ properties.sdtType = "richText";
820
+ continue;
821
+ }
822
+
823
+ if (!properties.sdtType && name !== "id" && name !== "placeholder" && name !== "showingPlcHdr" && name !== "rPr") {
696
824
  properties.sdtType = name;
697
825
  }
698
826
  }
@@ -700,6 +828,34 @@ function readSdtProperties(
700
828
  return properties;
701
829
  }
702
830
 
831
+ function readSdtListItems(node: XmlElementNode): ParsedSdtDropdownListItem[] {
832
+ const items: ParsedSdtDropdownListItem[] = [];
833
+ for (const child of node.children) {
834
+ if (child.type !== "element" || localName(child.name) !== "listItem") continue;
835
+ const value = readOptionalAttribute(child, "value") ?? "";
836
+ const displayText = readOptionalAttribute(child, "displayText");
837
+ items.push({ value, ...(displayText ? { displayText } : {}) });
838
+ }
839
+ return items;
840
+ }
841
+
842
+ function findFirstChild(node: XmlElementNode, local: string): XmlElementNode | undefined {
843
+ for (const child of node.children) {
844
+ if (child.type === "element" && localName(child.name) === local) return child;
845
+ }
846
+ return undefined;
847
+ }
848
+
849
+ function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
850
+ for (const child of node.children) {
851
+ if (child.type !== "element") continue;
852
+ if (localName(child.name) === local) return child;
853
+ const nested = findFirstDescendant(child, local);
854
+ if (nested) return nested;
855
+ }
856
+ return undefined;
857
+ }
858
+
703
859
  function readTableStyleId(node: XmlElementNode): string | undefined {
704
860
  for (const child of node.children) {
705
861
  if (child.type !== "element" || localName(child.name) !== "tblStyle") continue;
@@ -2007,3 +2163,252 @@ function decodeXmlEntities(value: string): string {
2007
2163
  }
2008
2164
  });
2009
2165
  }
2166
+
2167
+ // ---- Section properties parsing ----
2168
+
2169
+ function parseSectionBreakElement(
2170
+ node: XmlElementNode,
2171
+ sourceXml: string,
2172
+ ): ParsedSectionBreakNode {
2173
+ const props = parseSectionPropertiesFromElement(node);
2174
+ return {
2175
+ type: "section_break",
2176
+ sectionPropertiesXml: sourceXml.slice(node.start, node.end),
2177
+ sectionProperties: props,
2178
+ };
2179
+ }
2180
+
2181
+ function readSectionPropertiesFromPPr(
2182
+ pPrNode: XmlElementNode,
2183
+ ): SectionProperties | undefined {
2184
+ for (const child of pPrNode.children) {
2185
+ if (child.type === "element" && localName(child.name) === "sectPr") {
2186
+ return parseSectionPropertiesFromElement(child);
2187
+ }
2188
+ }
2189
+ return undefined;
2190
+ }
2191
+
2192
+ function readSectionPropertiesXmlFromPPr(
2193
+ pPrNode: XmlElementNode,
2194
+ sourceXml: string,
2195
+ ): string | undefined {
2196
+ for (const child of pPrNode.children) {
2197
+ if (child.type === "element" && localName(child.name) === "sectPr") {
2198
+ return sourceXml.slice(child.start, child.end);
2199
+ }
2200
+ }
2201
+ return undefined;
2202
+ }
2203
+
2204
+ export function parseSectionPropertiesFromElement(
2205
+ node: XmlElementNode,
2206
+ ): SectionProperties {
2207
+ const props: SectionProperties = {};
2208
+
2209
+ for (const child of node.children) {
2210
+ if (child.type !== "element") continue;
2211
+ const name = localName(child.name);
2212
+
2213
+ switch (name) {
2214
+ case "pgSz": {
2215
+ const w = safeParseInt(child.attributes["w:w"]);
2216
+ const h = safeParseInt(child.attributes["w:h"]);
2217
+ if (w !== undefined && h !== undefined) {
2218
+ const pageSize: PageSize = { width: w, height: h };
2219
+ const orient = child.attributes["w:orient"];
2220
+ if (orient === "landscape" || orient === "portrait") {
2221
+ pageSize.orientation = orient;
2222
+ }
2223
+ props.pageSize = pageSize;
2224
+ }
2225
+ break;
2226
+ }
2227
+ case "pgMar": {
2228
+ const top = safeParseInt(child.attributes["w:top"]);
2229
+ const right = safeParseInt(child.attributes["w:right"]);
2230
+ const bottom = safeParseInt(child.attributes["w:bottom"]);
2231
+ const left = safeParseInt(child.attributes["w:left"]);
2232
+ if (top !== undefined && right !== undefined && bottom !== undefined && left !== undefined) {
2233
+ const margins: PageMargins = { top, right, bottom, left };
2234
+ const header = safeParseInt(child.attributes["w:header"]);
2235
+ const footer = safeParseInt(child.attributes["w:footer"]);
2236
+ const gutter = safeParseInt(child.attributes["w:gutter"]);
2237
+ if (header !== undefined) margins.header = header;
2238
+ if (footer !== undefined) margins.footer = footer;
2239
+ if (gutter !== undefined) margins.gutter = gutter;
2240
+ props.pageMargins = margins;
2241
+ }
2242
+ break;
2243
+ }
2244
+ case "cols": {
2245
+ const columns: ColumnProperties = {};
2246
+ const num = safeParseInt(child.attributes["w:num"]);
2247
+ const space = safeParseInt(child.attributes["w:space"]);
2248
+ const equalWidth = child.attributes["w:equalWidth"];
2249
+ const sep = child.attributes["w:sep"];
2250
+ if (num !== undefined) columns.count = num;
2251
+ if (space !== undefined) columns.space = space;
2252
+ if (equalWidth !== undefined) columns.equalWidth = equalWidth !== "0" && equalWidth !== "false";
2253
+ if (sep === "1" || sep === "true") columns.separator = true;
2254
+ const colDefs: Array<{ width: number; space?: number }> = [];
2255
+ for (const colChild of child.children) {
2256
+ if (colChild.type === "element" && localName(colChild.name) === "col") {
2257
+ const colW = safeParseInt(colChild.attributes["w:w"]);
2258
+ const colSpace = safeParseInt(colChild.attributes["w:space"]);
2259
+ if (colW !== undefined) {
2260
+ colDefs.push(colSpace !== undefined ? { width: colW, space: colSpace } : { width: colW });
2261
+ }
2262
+ }
2263
+ }
2264
+ if (colDefs.length > 0) columns.columns = colDefs;
2265
+ if (Object.keys(columns).length > 0) props.columns = columns;
2266
+ break;
2267
+ }
2268
+ case "pgNumType": {
2269
+ const numbering: PageNumbering = {};
2270
+ const fmt = child.attributes["w:fmt"];
2271
+ const start = safeParseInt(child.attributes["w:start"]);
2272
+ const chapStyle = child.attributes["w:chapStyle"];
2273
+ const chapSep = child.attributes["w:chapSep"];
2274
+ if (fmt) numbering.format = fmt;
2275
+ if (start !== undefined) numbering.start = start;
2276
+ if (chapStyle) numbering.chapStyle = chapStyle;
2277
+ if (chapSep) numbering.chapSep = chapSep;
2278
+ if (Object.keys(numbering).length > 0) props.pageNumbering = numbering;
2279
+ break;
2280
+ }
2281
+ case "lnNumType": {
2282
+ const lineNumbering: SectionLineNumbering = {};
2283
+ const countBy = safeParseInt(child.attributes["w:countBy"]);
2284
+ const start = safeParseInt(child.attributes["w:start"]);
2285
+ const distance = safeParseInt(child.attributes["w:distance"]);
2286
+ const restart = child.attributes["w:restart"];
2287
+ if (countBy !== undefined) lineNumbering.countBy = countBy;
2288
+ if (start !== undefined) lineNumbering.start = start;
2289
+ if (distance !== undefined) lineNumbering.distance = distance;
2290
+ if (
2291
+ restart === "newPage" ||
2292
+ restart === "newSection" ||
2293
+ restart === "continuous"
2294
+ ) {
2295
+ lineNumbering.restart = restart;
2296
+ }
2297
+ if (Object.keys(lineNumbering).length > 0) {
2298
+ props.lineNumbering = lineNumbering;
2299
+ }
2300
+ break;
2301
+ }
2302
+ case "pgBorders": {
2303
+ const pageBorders: SectionPageBorders = {};
2304
+ const offsetFrom = child.attributes["w:offsetFrom"];
2305
+ const display = child.attributes["w:display"];
2306
+ const zOrder = child.attributes["w:zOrder"];
2307
+ if (offsetFrom === "page" || offsetFrom === "text") {
2308
+ pageBorders.offsetFrom = offsetFrom;
2309
+ }
2310
+ if (
2311
+ display === "allPages" ||
2312
+ display === "firstPage" ||
2313
+ display === "notFirstPage"
2314
+ ) {
2315
+ pageBorders.display = display;
2316
+ }
2317
+ if (zOrder === "front" || zOrder === "back") {
2318
+ pageBorders.zOrder = zOrder;
2319
+ }
2320
+ for (const borderChild of child.children) {
2321
+ if (borderChild.type !== "element") continue;
2322
+ const borderName = localName(borderChild.name);
2323
+ const border = parseBorderSpec(borderChild);
2324
+ if (!border) continue;
2325
+ if (
2326
+ borderName === "top" ||
2327
+ borderName === "left" ||
2328
+ borderName === "bottom" ||
2329
+ borderName === "right"
2330
+ ) {
2331
+ pageBorders[borderName] = border;
2332
+ }
2333
+ }
2334
+ if (Object.keys(pageBorders).length > 0) {
2335
+ props.pageBorders = pageBorders;
2336
+ }
2337
+ break;
2338
+ }
2339
+ case "docGrid": {
2340
+ const documentGrid: SectionDocumentGrid = {};
2341
+ const type = child.attributes["w:type"];
2342
+ const linePitch = safeParseInt(child.attributes["w:linePitch"]);
2343
+ const charSpace = safeParseInt(child.attributes["w:charSpace"]);
2344
+ if (
2345
+ type === "default" ||
2346
+ type === "lines" ||
2347
+ type === "linesAndChars" ||
2348
+ type === "snapToChars"
2349
+ ) {
2350
+ documentGrid.type = type;
2351
+ }
2352
+ if (linePitch !== undefined) documentGrid.linePitch = linePitch;
2353
+ if (charSpace !== undefined) documentGrid.charSpace = charSpace;
2354
+ if (Object.keys(documentGrid).length > 0) {
2355
+ props.documentGrid = documentGrid;
2356
+ }
2357
+ break;
2358
+ }
2359
+ case "headerReference": {
2360
+ const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
2361
+ const rId = child.attributes["r:id"];
2362
+ if (variant && rId) {
2363
+ if (!props.headerReferences) props.headerReferences = [];
2364
+ props.headerReferences.push({ variant, relationshipId: rId });
2365
+ }
2366
+ break;
2367
+ }
2368
+ case "footerReference": {
2369
+ const variant = child.attributes["w:type"] as HeaderFooterVariant | undefined;
2370
+ const rId = child.attributes["r:id"];
2371
+ if (variant && rId) {
2372
+ if (!props.footerReferences) props.footerReferences = [];
2373
+ props.footerReferences.push({ variant, relationshipId: rId });
2374
+ }
2375
+ break;
2376
+ }
2377
+ case "type": {
2378
+ const val = child.attributes["w:val"];
2379
+ if (val === "continuous" || val === "nextPage" || val === "evenPage" || val === "oddPage" || val === "nextColumn") {
2380
+ props.sectionType = val;
2381
+ }
2382
+ break;
2383
+ }
2384
+ case "titlePg": {
2385
+ const val = child.attributes["w:val"];
2386
+ props.titlePage = val !== "false" && val !== "0";
2387
+ break;
2388
+ }
2389
+ }
2390
+ }
2391
+
2392
+ return props;
2393
+ }
2394
+
2395
+ function safeParseInt(value: string | undefined): number | undefined {
2396
+ if (value === undefined) return undefined;
2397
+ const n = Number.parseInt(value, 10);
2398
+ return Number.isFinite(n) ? n : undefined;
2399
+ }
2400
+
2401
+ function parseBorderSpec(node: XmlElementNode): BorderSpec | undefined {
2402
+ const border: BorderSpec = {};
2403
+ const value = node.attributes["w:val"];
2404
+ const size = safeParseInt(node.attributes["w:sz"]);
2405
+ const space = safeParseInt(node.attributes["w:space"]);
2406
+ const color = node.attributes["w:color"];
2407
+
2408
+ if (value) border.value = value;
2409
+ if (size !== undefined) border.size = size;
2410
+ if (space !== undefined) border.space = space;
2411
+ if (color) border.color = color;
2412
+
2413
+ return Object.keys(border).length > 0 ? border : undefined;
2414
+ }
@@ -147,6 +147,11 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
147
147
  const text = textNode?.attributes["w:val"] ?? textNode?.attributes.val ?? `%${level + 1}.`;
148
148
  const paragraphStyleId =
149
149
  paragraphStyleNode?.attributes["w:val"] ?? paragraphStyleNode?.attributes.val;
150
+ const isLegalNode = findChildElementOptional(child, "isLgl");
151
+ const isLegalNumbering = isLegalNode !== undefined;
152
+ const suffixNode = findChildElementOptional(child, "suff");
153
+ const suffixVal = suffixNode?.attributes["w:val"] ?? suffixNode?.attributes.val;
154
+ const suffix = suffixVal === "space" || suffixVal === "nothing" ? suffixVal : suffixVal === "tab" ? "tab" : undefined;
150
155
 
151
156
  levels.push({
152
157
  level,
@@ -154,6 +159,8 @@ function readLevels(abstractNode: XmlElementNode): NumberingLevelDefinition[] {
154
159
  text,
155
160
  ...(startAt !== undefined ? { startAt } : {}),
156
161
  ...(paragraphStyleId ? { paragraphStyleId } : {}),
162
+ ...(isLegalNumbering ? { isLegalNumbering } : {}),
163
+ ...(suffix ? { suffix } : {}),
157
164
  });
158
165
  }
159
166