@beyondwork/docx-react-component 1.0.18 → 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 +24 -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
@@ -8,6 +8,7 @@ import type {
8
8
  ParagraphNode,
9
9
  PreservationStore,
10
10
  SdtNode,
11
+ SectionProperties,
11
12
  TableNode,
12
13
  TableCellNode,
13
14
  TextMark,
@@ -30,6 +31,7 @@ export interface SerializedMainDocument {
30
31
  export interface SerializeMainDocumentOptions {
31
32
  documentAttributes?: Record<string, string>;
32
33
  media?: MediaCatalog;
34
+ finalSectionProperties?: SectionProperties;
33
35
  }
34
36
 
35
37
  interface SerializationState {
@@ -90,7 +92,9 @@ export function serializeMainDocument(
90
92
  const bodyPieces: string[] = [];
91
93
  const paragraphBoundaries: RevisionParagraphBoundary[] = [];
92
94
  let bodyLength = 0;
93
- let sectionPropertiesXml = "<w:sectPr/>";
95
+ let sectionPropertiesXml = options.finalSectionProperties
96
+ ? serializeSectionPropertiesXml(options.finalSectionProperties)
97
+ : "<w:sectPr/>";
94
98
  let cursor = 0;
95
99
  let paragraphIndex = -1;
96
100
  let previousWasParagraph = false;
@@ -154,9 +158,20 @@ export function serializeMainDocument(
154
158
  }
155
159
 
156
160
  if (block.type === "section_break") {
157
- if (block.propertiesXml) {
158
- sectionPropertiesXml = block.propertiesXml;
161
+ // Inline section breaks must be emitted as a paragraph with <w:sectPr>
162
+ // in its <w:pPr> element (OOXML compliance). The body-level <w:sectPr>
163
+ // is reserved for the final section only.
164
+ let inlineSectPr: string;
165
+ if (block.sectionPropertiesXml ?? block.propertiesXml) {
166
+ inlineSectPr = block.sectionPropertiesXml ?? block.propertiesXml!;
167
+ } else if (block.sectionProperties) {
168
+ inlineSectPr = serializeSectionPropertiesXml(block.sectionProperties);
169
+ } else {
170
+ inlineSectPr = "<w:sectPr/>";
159
171
  }
172
+ const sectionParagraphXml = `<w:p><w:pPr>${inlineSectPr}</w:pPr></w:p>`;
173
+ bodyPieces.push(sectionParagraphXml);
174
+ bodyLength += sectionParagraphXml.length;
160
175
  cursor += 1;
161
176
  previousWasParagraph = false;
162
177
  continue;
@@ -242,7 +257,11 @@ function serializeBlockNode(
242
257
  case "opaque_block":
243
258
  return lookupOpaqueXml(block.fragmentId, state);
244
259
  case "section_break":
245
- return block.propertiesXml ?? "<w:sectPr/>";
260
+ if (block.sectionPropertiesXml ?? block.propertiesXml) {
261
+ return block.sectionPropertiesXml ?? block.propertiesXml!;
262
+ }
263
+ if (block.sectionProperties) return serializeSectionPropertiesXml(block.sectionProperties);
264
+ return "<w:sectPr/>";
246
265
  }
247
266
  }
248
267
 
@@ -303,7 +322,40 @@ function buildSdtPropertiesXml(block: SdtNode): string {
303
322
  if (block.properties.lock) {
304
323
  children.push(`<w:lock w:val="${escapeAttribute(block.properties.lock)}"/>`);
305
324
  }
306
- if (block.properties.sdtType) {
325
+ if (block.properties.showingPlcHdr) {
326
+ children.push(`<w:showingPlcHdr/>`);
327
+ }
328
+ if (block.properties.checkbox) {
329
+ const cb = block.properties.checkbox;
330
+ const cbParts: string[] = [];
331
+ cbParts.push(`<w14:checked w14:val="${cb.checked ? "1" : "0"}"/>`);
332
+ if (cb.checkedChar) cbParts.push(`<w14:checkedState w14:val="${escapeAttribute(cb.checkedChar)}"/>`);
333
+ if (cb.uncheckedChar) cbParts.push(`<w14:uncheckedState w14:val="${escapeAttribute(cb.uncheckedChar)}"/>`);
334
+ children.push(`<w14:checkbox>${cbParts.join("")}</w14:checkbox>`);
335
+ } else if (block.properties.datePicker) {
336
+ const dp = block.properties.datePicker;
337
+ const dateAttrs = dp.fullDate ? ` w:fullDate="${escapeAttribute(dp.fullDate)}"` : "";
338
+ const dpParts: string[] = [];
339
+ if (dp.dateFormat) dpParts.push(`<w:dateFormat w:val="${escapeAttribute(dp.dateFormat)}"/>`);
340
+ if (dp.lid) dpParts.push(`<w:lid w:val="${escapeAttribute(dp.lid)}"/>`);
341
+ children.push(dpParts.length > 0 ? `<w:date${dateAttrs}>${dpParts.join("")}</w:date>` : `<w:date${dateAttrs}/>`);
342
+ } else if (block.properties.dropdownList) {
343
+ const items = block.properties.dropdownList.map((item) => {
344
+ const dt = item.displayText ? ` w:displayText="${escapeAttribute(item.displayText)}"` : "";
345
+ return `<w:listItem${dt} w:value="${escapeAttribute(item.value)}"/>`;
346
+ }).join("");
347
+ children.push(`<w:dropDownList>${items}</w:dropDownList>`);
348
+ } else if (block.properties.comboBox) {
349
+ const items = block.properties.comboBox.map((item) => {
350
+ const dt = item.displayText ? ` w:displayText="${escapeAttribute(item.displayText)}"` : "";
351
+ return `<w:listItem${dt} w:value="${escapeAttribute(item.value)}"/>`;
352
+ }).join("");
353
+ children.push(`<w:comboBox>${items}</w:comboBox>`);
354
+ } else if (block.properties.sdtType === "plainText") {
355
+ children.push(`<w:text/>`);
356
+ } else if (block.properties.sdtType === "richText") {
357
+ children.push(`<w:richText/>`);
358
+ } else if (block.properties.sdtType) {
307
359
  children.push(`<w:${block.properties.sdtType}/>`);
308
360
  }
309
361
  return children.length > 0 ? `<w:sdtPr>${children.join("")}</w:sdtPr>` : "<w:sdtPr/>";
@@ -1049,6 +1101,151 @@ function serializeDocumentAttributes(
1049
1101
  .join("");
1050
1102
  }
1051
1103
 
1104
+ export function serializeSectionPropertiesXml(props: SectionProperties): string {
1105
+ const children: string[] = [];
1106
+
1107
+ // Header references
1108
+ if (props.headerReferences) {
1109
+ for (const ref of props.headerReferences) {
1110
+ children.push(`<w:headerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1111
+ }
1112
+ }
1113
+
1114
+ // Footer references
1115
+ if (props.footerReferences) {
1116
+ for (const ref of props.footerReferences) {
1117
+ children.push(`<w:footerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1118
+ }
1119
+ }
1120
+
1121
+ // Section type
1122
+ if (props.sectionType) {
1123
+ children.push(`<w:type w:val="${escapeAttribute(props.sectionType)}"/>`);
1124
+ }
1125
+
1126
+ // Page size
1127
+ if (props.pageSize) {
1128
+ let pgSz = `<w:pgSz w:w="${props.pageSize.width}" w:h="${props.pageSize.height}"`;
1129
+ if (props.pageSize.orientation) {
1130
+ pgSz += ` w:orient="${props.pageSize.orientation}"`;
1131
+ }
1132
+ pgSz += "/>";
1133
+ children.push(pgSz);
1134
+ }
1135
+
1136
+ // Page margins
1137
+ if (props.pageMargins) {
1138
+ let pgMar = `<w:pgMar w:top="${props.pageMargins.top}" w:right="${props.pageMargins.right}" w:bottom="${props.pageMargins.bottom}" w:left="${props.pageMargins.left}"`;
1139
+ if (props.pageMargins.header !== undefined) pgMar += ` w:header="${props.pageMargins.header}"`;
1140
+ if (props.pageMargins.footer !== undefined) pgMar += ` w:footer="${props.pageMargins.footer}"`;
1141
+ if (props.pageMargins.gutter !== undefined) pgMar += ` w:gutter="${props.pageMargins.gutter}"`;
1142
+ pgMar += "/>";
1143
+ children.push(pgMar);
1144
+ }
1145
+
1146
+ // Columns
1147
+ if (props.columns) {
1148
+ let cols = "<w:cols";
1149
+ if (props.columns.count !== undefined) cols += ` w:num="${props.columns.count}"`;
1150
+ if (props.columns.space !== undefined) cols += ` w:space="${props.columns.space}"`;
1151
+ if (props.columns.equalWidth !== undefined) cols += ` w:equalWidth="${props.columns.equalWidth ? "1" : "0"}"`;
1152
+ if (props.columns.separator) cols += ` w:sep="1"`;
1153
+ if (props.columns.columns && props.columns.columns.length > 0) {
1154
+ cols += ">";
1155
+ for (const col of props.columns.columns) {
1156
+ let colXml = `<w:col w:w="${col.width}"`;
1157
+ if (col.space !== undefined) colXml += ` w:space="${col.space}"`;
1158
+ colXml += "/>";
1159
+ cols += colXml;
1160
+ }
1161
+ cols += "</w:cols>";
1162
+ } else {
1163
+ cols += "/>";
1164
+ }
1165
+ children.push(cols);
1166
+ }
1167
+
1168
+ // Page numbering
1169
+ if (props.pageNumbering) {
1170
+ let pgNum = "<w:pgNumType";
1171
+ if (props.pageNumbering.format) pgNum += ` w:fmt="${escapeAttribute(props.pageNumbering.format)}"`;
1172
+ if (props.pageNumbering.start !== undefined) pgNum += ` w:start="${props.pageNumbering.start}"`;
1173
+ if (props.pageNumbering.chapStyle) pgNum += ` w:chapStyle="${escapeAttribute(props.pageNumbering.chapStyle)}"`;
1174
+ if (props.pageNumbering.chapSep) pgNum += ` w:chapSep="${escapeAttribute(props.pageNumbering.chapSep)}"`;
1175
+ pgNum += "/>";
1176
+ children.push(pgNum);
1177
+ }
1178
+
1179
+ if (props.lineNumbering) {
1180
+ let lineNumbering = "<w:lnNumType";
1181
+ if (props.lineNumbering.countBy !== undefined) {
1182
+ lineNumbering += ` w:countBy="${props.lineNumbering.countBy}"`;
1183
+ }
1184
+ if (props.lineNumbering.start !== undefined) {
1185
+ lineNumbering += ` w:start="${props.lineNumbering.start}"`;
1186
+ }
1187
+ if (props.lineNumbering.distance !== undefined) {
1188
+ lineNumbering += ` w:distance="${props.lineNumbering.distance}"`;
1189
+ }
1190
+ if (props.lineNumbering.restart) {
1191
+ lineNumbering += ` w:restart="${escapeAttribute(props.lineNumbering.restart)}"`;
1192
+ }
1193
+ lineNumbering += "/>";
1194
+ children.push(lineNumbering);
1195
+ }
1196
+
1197
+ if (props.pageBorders) {
1198
+ const attrs: string[] = [];
1199
+ if (props.pageBorders.offsetFrom) {
1200
+ attrs.push(`w:offsetFrom="${escapeAttribute(props.pageBorders.offsetFrom)}"`);
1201
+ }
1202
+ if (props.pageBorders.display) {
1203
+ attrs.push(`w:display="${escapeAttribute(props.pageBorders.display)}"`);
1204
+ }
1205
+ if (props.pageBorders.zOrder) {
1206
+ attrs.push(`w:zOrder="${escapeAttribute(props.pageBorders.zOrder)}"`);
1207
+ }
1208
+ const borderXml = [
1209
+ serializeBorder("top", props.pageBorders.top),
1210
+ serializeBorder("left", props.pageBorders.left),
1211
+ serializeBorder("bottom", props.pageBorders.bottom),
1212
+ serializeBorder("right", props.pageBorders.right),
1213
+ ].filter((entry) => entry.length > 0);
1214
+ if (attrs.length > 0 || borderXml.length > 0) {
1215
+ children.push(
1216
+ `<w:pgBorders${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${borderXml.join("")}</w:pgBorders>`,
1217
+ );
1218
+ }
1219
+ }
1220
+
1221
+ // Title page
1222
+ if (props.titlePage) {
1223
+ children.push("<w:titlePg/>");
1224
+ }
1225
+
1226
+ if (props.documentGrid) {
1227
+ const attrs: string[] = [];
1228
+ if (props.documentGrid.type) {
1229
+ attrs.push(`w:type="${escapeAttribute(props.documentGrid.type)}"`);
1230
+ }
1231
+ if (props.documentGrid.linePitch !== undefined) {
1232
+ attrs.push(`w:linePitch="${props.documentGrid.linePitch}"`);
1233
+ }
1234
+ if (props.documentGrid.charSpace !== undefined) {
1235
+ attrs.push(`w:charSpace="${props.documentGrid.charSpace}"`);
1236
+ }
1237
+ if (attrs.length > 0) {
1238
+ children.push(`<w:docGrid ${attrs.join(" ")}/>`);
1239
+ }
1240
+ }
1241
+
1242
+ if (children.length === 0) {
1243
+ return "<w:sectPr/>";
1244
+ }
1245
+
1246
+ return `<w:sectPr>${children.join("")}</w:sectPr>`;
1247
+ }
1248
+
1052
1249
  function documentNeedsW14Namespace(content: DocumentRootNode): boolean {
1053
1250
  const blockQueue = [...content.children];
1054
1251
  while (blockQueue.length > 0) {
@@ -1,15 +1,23 @@
1
1
  import type { NumberingCatalog, ParagraphNode } from "../../model/canonical-document.ts";
2
+ import {
3
+ isSyntheticDocxNullAbstractDefinition,
4
+ isSyntheticDocxNullNumberingInstance,
5
+ } from "../ooxml/numbering-sentinels.ts";
2
6
 
3
7
  export const WORD_NUMBERING_CONTENT_TYPE =
4
8
  "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
5
9
 
6
10
  export function serializeNumberingXml(catalog: NumberingCatalog): string {
7
- const abstractDefinitions = Object.values(catalog.abstractDefinitions).sort((left, right) =>
8
- compareSerializedIds(left.abstractNumberingId, right.abstractNumberingId),
9
- );
10
- const instances = Object.values(catalog.instances).sort((left, right) =>
11
- compareSerializedIds(left.numberingInstanceId, right.numberingInstanceId),
12
- );
11
+ const abstractDefinitions = Object.values(catalog.abstractDefinitions)
12
+ .filter((definition) => !isSyntheticDocxNullAbstractDefinition(definition))
13
+ .sort((left, right) =>
14
+ compareSerializedIds(left.abstractNumberingId, right.abstractNumberingId),
15
+ );
16
+ const instances = Object.values(catalog.instances)
17
+ .filter((instance) => !isSyntheticDocxNullNumberingInstance(instance))
18
+ .sort((left, right) =>
19
+ compareSerializedIds(left.numberingInstanceId, right.numberingInstanceId),
20
+ );
13
21
 
14
22
  const body = [
15
23
  ...abstractDefinitions.map((definition) => serializeAbstractDefinition(definition)),
@@ -22,6 +30,17 @@ export function serializeNumberingXml(catalog: NumberingCatalog): string {
22
30
  ].join("\n");
23
31
  }
24
32
 
33
+ export function hasSerializableNumberingEntries(catalog: NumberingCatalog): boolean {
34
+ return (
35
+ Object.values(catalog.abstractDefinitions).some(
36
+ (definition) => !isSyntheticDocxNullAbstractDefinition(definition),
37
+ ) ||
38
+ Object.values(catalog.instances).some(
39
+ (instance) => !isSyntheticDocxNullNumberingInstance(instance),
40
+ )
41
+ );
42
+ }
43
+
25
44
  export function serializeParagraphNumberingProperties(
26
45
  numbering: ParagraphNode["numbering"],
27
46
  ): string {
@@ -51,10 +70,12 @@ function serializeLevel(level: NumberingCatalog["abstractDefinitions"][string]["
51
70
  const paragraphStyle = level.paragraphStyleId
52
71
  ? `<w:pStyle w:val="${escapeAttribute(level.paragraphStyleId)}"/>`
53
72
  : "";
73
+ const isLegal = level.isLegalNumbering ? "<w:isLgl/>" : "";
74
+ const suffix = level.suffix ? `<w:suff w:val="${escapeAttribute(level.suffix)}"/>` : "";
54
75
 
55
76
  return `<w:lvl w:ilvl="${level.level}">${start}<w:numFmt w:val="${escapeAttribute(
56
77
  level.format,
57
- )}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}</w:lvl>`;
78
+ )}"/><w:lvlText w:val="${escapeAttribute(level.text)}"/>${paragraphStyle}${isLegal}${suffix}</w:lvl>`;
58
79
  }
59
80
 
60
81
  function serializeInstance(instance: NumberingCatalog["instances"][string]): string {
@@ -11,6 +11,7 @@ import type {
11
11
  OpaqueInlineNode,
12
12
  ParagraphNode,
13
13
  PreservationStore,
14
+ SectionBreakNode,
14
15
  TableCellNode,
15
16
  TableNode,
16
17
  TableRowNode,
@@ -27,6 +28,7 @@ import type {
27
28
  ParsedImageNode,
28
29
  ParsedMainDocument,
29
30
  ParsedParagraphNode,
31
+ ParsedSectionBreakNode,
30
32
  ParsedSdtNode,
31
33
  ParsedTableBlockNode,
32
34
  ParsedTableCellNode,
@@ -38,6 +40,7 @@ export interface NormalizedTextDocument {
38
40
  media: MediaCatalog;
39
41
  preservation: PreservationStore;
40
42
  diagnostics: DiagnosticStore;
43
+ finalSectionProperties?: ParsedMainDocument["finalSectionProperties"];
41
44
  }
42
45
 
43
46
  interface NormalizationState {
@@ -72,17 +75,19 @@ export function normalizeParsedTextDocument(
72
75
  },
73
76
  };
74
77
 
75
- const children = document.blocks.map((block, index) => {
76
- if (
77
- index > 0 &&
78
- document.blocks[index - 1]?.type === "paragraph" &&
79
- block.type === "paragraph"
80
- ) {
81
- state.cursor += 1;
82
- }
78
+ const children: BlockNode[] = [];
79
+ let previousParagraph = false;
83
80
 
84
- return normalizeBlock(block, state, packagePartName);
85
- });
81
+ for (const block of document.blocks) {
82
+ const normalizedBlocks = normalizeBlocks(block, state, packagePartName);
83
+ for (const normalizedBlock of normalizedBlocks) {
84
+ if (previousParagraph && normalizedBlock.type === "paragraph") {
85
+ state.cursor += 1;
86
+ }
87
+ children.push(normalizedBlock);
88
+ previousParagraph = normalizedBlock.type === "paragraph";
89
+ }
90
+ }
86
91
 
87
92
  return {
88
93
  content: {
@@ -92,41 +97,53 @@ export function normalizeParsedTextDocument(
92
97
  media: state.media,
93
98
  preservation: state.preservation,
94
99
  diagnostics: state.diagnostics,
100
+ ...(document.finalSectionProperties !== undefined
101
+ ? { finalSectionProperties: document.finalSectionProperties }
102
+ : {}),
95
103
  };
96
104
  }
97
105
 
98
- function normalizeBlock(
106
+ function normalizeBlocks(
99
107
  block: ParsedBlockNode,
100
108
  state: NormalizationState,
101
109
  packagePartName: string,
102
- ): BlockNode {
110
+ ): BlockNode[] {
103
111
  if (block.type === "opaque_block") {
104
112
  const opaque = recordOpaqueFragment("opaque_block", block.rawXml, state, packagePartName);
105
113
  state.cursor += 1;
106
- return {
107
- type: "opaque_block",
108
- fragmentId: opaque.fragmentId,
109
- warningId: opaque.warningId,
110
- };
114
+ return [
115
+ {
116
+ type: "opaque_block",
117
+ fragmentId: opaque.fragmentId,
118
+ warningId: opaque.warningId,
119
+ },
120
+ ];
111
121
  }
112
122
 
113
123
  if (block.type === "table") {
114
- return normalizeTable(block, state, packagePartName);
124
+ return [normalizeTable(block, state, packagePartName)];
115
125
  }
116
126
 
117
127
  if (block.type === "sdt") {
118
- return normalizeSdt(block, state, packagePartName);
128
+ return [normalizeSdt(block, state, packagePartName)];
119
129
  }
120
130
 
121
131
  if (block.type === "custom_xml") {
122
- return normalizeCustomXml(block, state, packagePartName);
132
+ return [normalizeCustomXml(block, state, packagePartName)];
123
133
  }
124
134
 
125
135
  if (block.type === "alt_chunk") {
126
- return normalizeAltChunk(block, state);
136
+ return [normalizeAltChunk(block, state)];
137
+ }
138
+
139
+ if (block.type === "section_break") {
140
+ return [normalizeSectionBreak(block)];
127
141
  }
128
142
 
129
- return normalizeParagraph(block, state, packagePartName);
143
+ const normalizedParagraph = normalizeParagraph(block, state, packagePartName);
144
+ return block.sectionProperties
145
+ ? [normalizedParagraph, normalizeInlineSectionBreak(block)]
146
+ : [normalizedParagraph];
130
147
  }
131
148
 
132
149
  function normalizeParagraph(
@@ -190,7 +207,7 @@ function normalizeTableCell(
190
207
  ): TableCellNode {
191
208
  const children: BlockNode[] = [];
192
209
  for (const block of cell.children) {
193
- children.push(normalizeBlock(block, state, packagePartName));
210
+ children.push(...normalizeBlocks(block, state, packagePartName));
194
211
  }
195
212
  // Ensure at least one child (OOXML requires at least one <w:p> per cell)
196
213
  if (children.length === 0) {
@@ -213,7 +230,7 @@ function normalizeSdt(
213
230
  return {
214
231
  type: "sdt",
215
232
  properties: { ...block.properties },
216
- children: block.children.map((child) => normalizeBlock(child, state, packagePartName)),
233
+ children: block.children.flatMap((child) => normalizeBlocks(child, state, packagePartName)),
217
234
  };
218
235
  }
219
236
 
@@ -226,7 +243,27 @@ function normalizeCustomXml(
226
243
  type: "custom_xml",
227
244
  ...(block.uri ? { uri: block.uri } : {}),
228
245
  ...(block.element ? { element: block.element } : {}),
229
- children: block.children.map((child) => normalizeBlock(child, state, packagePartName)),
246
+ children: block.children.flatMap((child) => normalizeBlocks(child, state, packagePartName)),
247
+ };
248
+ }
249
+
250
+ function normalizeSectionBreak(block: ParsedSectionBreakNode): SectionBreakNode {
251
+ return {
252
+ type: "section_break",
253
+ sectionPropertiesXml: block.sectionPropertiesXml,
254
+ sectionProperties: block.sectionProperties,
255
+ };
256
+ }
257
+
258
+ function normalizeInlineSectionBreak(
259
+ paragraph: ParsedParagraphNode,
260
+ ): SectionBreakNode {
261
+ return {
262
+ type: "section_break",
263
+ ...(paragraph.sectionPropertiesXml
264
+ ? { sectionPropertiesXml: paragraph.sectionPropertiesXml }
265
+ : {}),
266
+ sectionProperties: paragraph.sectionProperties!,
230
267
  };
231
268
  }
232
269
 
@@ -324,6 +361,7 @@ function normalizeInlineChildren(
324
361
  type: "shape",
325
362
  ...(node.text ? { text: node.text } : {}),
326
363
  ...(node.geometry ? { geometry: node.geometry } : {}),
364
+ ...(node.isTextBox ? { isTextBox: true } : {}),
327
365
  rawXml: node.rawXml,
328
366
  });
329
367
  state.cursor += 1;
@@ -0,0 +1,44 @@
1
+ import type { NumberingCatalog } from "../../model/canonical-document.ts";
2
+
3
+ export const DOCX_NULL_NUMBERING_INSTANCE_ID = "num:0";
4
+ export const DOCX_NULL_ABSTRACT_NUMBERING_ID = "abstract-num:__docx-import-null__";
5
+
6
+ export function createSyntheticDocxNullNumberingCatalog(): Pick<
7
+ NumberingCatalog,
8
+ "abstractDefinitions" | "instances"
9
+ > {
10
+ return {
11
+ abstractDefinitions: {
12
+ [DOCX_NULL_ABSTRACT_NUMBERING_ID]: {
13
+ abstractNumberingId: DOCX_NULL_ABSTRACT_NUMBERING_ID,
14
+ levels: Array.from({ length: 9 }, (_unused, level) => ({
15
+ level,
16
+ format: "none",
17
+ text: "",
18
+ })),
19
+ },
20
+ },
21
+ instances: {
22
+ [DOCX_NULL_NUMBERING_INSTANCE_ID]: {
23
+ numberingInstanceId: DOCX_NULL_NUMBERING_INSTANCE_ID,
24
+ abstractNumberingId: DOCX_NULL_ABSTRACT_NUMBERING_ID,
25
+ overrides: [],
26
+ },
27
+ },
28
+ };
29
+ }
30
+
31
+ export function isSyntheticDocxNullAbstractDefinition(
32
+ definition: NumberingCatalog["abstractDefinitions"][string],
33
+ ): boolean {
34
+ return definition.abstractNumberingId === DOCX_NULL_ABSTRACT_NUMBERING_ID;
35
+ }
36
+
37
+ export function isSyntheticDocxNullNumberingInstance(
38
+ instance: NumberingCatalog["instances"][string],
39
+ ): boolean {
40
+ return (
41
+ instance.numberingInstanceId === DOCX_NULL_NUMBERING_INSTANCE_ID &&
42
+ instance.abstractNumberingId === DOCX_NULL_ABSTRACT_NUMBERING_ID
43
+ );
44
+ }