@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,8 +1,12 @@
1
1
  import type {
2
+ BlockNode,
2
3
  FooterDocument,
3
4
  HeaderDocument,
4
5
  InlineNode,
5
6
  ParagraphNode,
7
+ TableCellNode,
8
+ TableNode,
9
+ TableRowNode,
6
10
  TextMark,
7
11
  } from "../../model/canonical-document.ts";
8
12
 
@@ -13,6 +17,17 @@ export const WORD_FOOTER_CONTENT_TYPE =
13
17
 
14
18
  const W_NS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"`;
15
19
  const R_NS = `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`;
20
+ const MC_NS = `xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"`;
21
+ const O_NS = `xmlns:o="urn:schemas-microsoft-com:office:office"`;
22
+ const V_NS = `xmlns:v="urn:schemas-microsoft-com:vml"`;
23
+ const WP_NS = `xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"`;
24
+ const WPS_NS = `xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"`;
25
+ const A_NS = `xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"`;
26
+ const W10_NS = `xmlns:w10="urn:schemas-microsoft-com:office:word"`;
27
+ const W14_NS = `xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"`;
28
+ const W15_NS = `xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"`;
29
+ const WP14_NS = `xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"`;
30
+ const EXTENDED_ROOT_ATTRS = `${MC_NS} ${O_NS} ${V_NS} ${WP_NS} ${WPS_NS} ${A_NS} ${W10_NS} ${W14_NS} ${W15_NS} ${WP14_NS} mc:Ignorable="wps wp14 w14 w15"`;
16
31
 
17
32
  /**
18
33
  * Serialize a HeaderDocument into a headerN.xml string.
@@ -21,7 +36,7 @@ export function serializeHeaderXml(header: HeaderDocument): string {
21
36
  const body = serializeBlocks(header.blocks);
22
37
  return [
23
38
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
24
- `<w:hdr ${W_NS} ${R_NS}>${body}</w:hdr>`,
39
+ `<w:hdr ${W_NS} ${R_NS} ${EXTENDED_ROOT_ATTRS}>${body}</w:hdr>`,
25
40
  ].join("\n");
26
41
  }
27
42
 
@@ -32,7 +47,7 @@ export function serializeFooterXml(footer: FooterDocument): string {
32
47
  const body = serializeBlocks(footer.blocks);
33
48
  return [
34
49
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
35
- `<w:ftr ${W_NS} ${R_NS}>${body}</w:ftr>`,
50
+ `<w:ftr ${W_NS} ${R_NS} ${EXTENDED_ROOT_ATTRS}>${body}</w:ftr>`,
36
51
  ].join("\n");
37
52
  }
38
53
 
@@ -51,6 +66,12 @@ function serializeBlocks(
51
66
  if (block.type === "paragraph") {
52
67
  return serializeParagraph(block);
53
68
  }
69
+ if (block.type === "table") {
70
+ return serializeTable(block);
71
+ }
72
+ if (block.type === "opaque_block" && typeof block.rawXml === "string") {
73
+ return block.rawXml;
74
+ }
54
75
  throw new Error(`Cannot safely serialize ${block.type} content in header/footer sub-parts.`);
55
76
  })
56
77
  .join("");
@@ -73,15 +94,88 @@ function serializeParagraph(paragraph: ParagraphNode): string {
73
94
  return xml;
74
95
  }
75
96
 
97
+ function serializeTable(table: TableNode): string {
98
+ let xml = "<w:tbl>";
99
+ if (table.propertiesXml) {
100
+ xml += table.propertiesXml;
101
+ }
102
+ if (table.gridColumns.length > 0) {
103
+ xml += "<w:tblGrid>";
104
+ for (const width of table.gridColumns) {
105
+ xml += `<w:gridCol w:w="${width}"/>`;
106
+ }
107
+ xml += "</w:tblGrid>";
108
+ }
109
+ for (const row of table.rows) {
110
+ xml += serializeTableRow(row);
111
+ }
112
+ xml += "</w:tbl>";
113
+ return xml;
114
+ }
115
+
116
+ function serializeTableRow(row: TableRowNode): string {
117
+ let xml = "<w:tr>";
118
+ if (row.propertiesXml) {
119
+ xml += wrapPropertiesXml("w:trPr", row.propertiesXml);
120
+ }
121
+ for (const cell of row.cells) {
122
+ xml += serializeTableCell(cell);
123
+ }
124
+ xml += "</w:tr>";
125
+ return xml;
126
+ }
127
+
128
+ function serializeTableCell(cell: TableCellNode): string {
129
+ let xml = "<w:tc>";
130
+ const propertiesXml = buildTableCellPropertiesXml(cell);
131
+ if (propertiesXml) {
132
+ xml += propertiesXml;
133
+ }
134
+ for (const child of cell.children) {
135
+ if (child.type === "paragraph") {
136
+ xml += serializeParagraph(child);
137
+ } else {
138
+ throw new Error(`Cannot safely serialize ${child.type} content in header/footer table cells.`);
139
+ }
140
+ }
141
+ xml += "</w:tc>";
142
+ return xml;
143
+ }
144
+
76
145
  function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
77
146
  const parts: string[] = [];
78
147
 
79
148
  if (paragraph.styleId) {
80
149
  parts.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
81
150
  }
151
+ if (paragraph.spacing) {
152
+ const s = paragraph.spacing;
153
+ const attrs: string[] = [];
154
+ if (s.before !== undefined) attrs.push(`w:before="${s.before}"`);
155
+ if (s.after !== undefined) attrs.push(`w:after="${s.after}"`);
156
+ if (s.line !== undefined) attrs.push(`w:line="${s.line}"`);
157
+ if (s.lineRule) attrs.push(`w:lineRule="${escapeAttribute(s.lineRule)}"`);
158
+ if (attrs.length > 0) parts.push(`<w:spacing ${attrs.join(" ")}/>`);
159
+ }
160
+ if (paragraph.indentation) {
161
+ const ind = paragraph.indentation;
162
+ const attrs: string[] = [];
163
+ if (ind.left !== undefined) attrs.push(`w:left="${ind.left}"`);
164
+ if (ind.right !== undefined) attrs.push(`w:right="${ind.right}"`);
165
+ if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${ind.firstLine}"`);
166
+ if (ind.hanging !== undefined) attrs.push(`w:hanging="${ind.hanging}"`);
167
+ if (attrs.length > 0) parts.push(`<w:ind ${attrs.join(" ")}/>`);
168
+ }
82
169
  if (paragraph.alignment) {
83
170
  parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
84
171
  }
172
+ if (paragraph.tabStops && paragraph.tabStops.length > 0) {
173
+ const tabsXml = paragraph.tabStops.map((tab) => {
174
+ const leaderAttr = tab.leader ? ` w:leader="${escapeAttribute(tab.leader)}"` : "";
175
+ return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
176
+ }).join("");
177
+ parts.push(`<w:tabs>${tabsXml}</w:tabs>`);
178
+ }
85
179
 
86
180
  return parts.length > 0 ? `<w:pPr>${parts.join("")}</w:pPr>` : "";
87
181
  }
@@ -106,25 +200,51 @@ function serializeInlineNode(node: InlineNode): string {
106
200
  : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
107
201
  return `<w:r><w:rPr><w:rStyle w:val="${node.noteKind === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr>${refElement}</w:r>`;
108
202
  }
109
- case "opaque_inline":
110
- throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
111
- case "hyperlink":
112
- case "image":
113
- case "field":
114
203
  case "bookmark_start":
204
+ return `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}" w:name="${escapeAttribute(node.name)}"/>`;
115
205
  case "bookmark_end":
116
- case "column_break":
117
- case "symbol":
206
+ return `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
207
+ case "field":
208
+ if (node.children && node.children.length > 0) {
209
+ const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
210
+ if (node.fieldType === "complex") {
211
+ return (
212
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
213
+ `<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
214
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
215
+ childrenXml +
216
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
217
+ );
218
+ }
219
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
220
+ }
221
+ if (node.fieldType === "simple") {
222
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
223
+ }
224
+ return `<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>`;
225
+ case "hyperlink":
226
+ return serializeHyperlinkNode(node);
118
227
  case "chart_preview":
119
228
  case "smartart_preview":
120
229
  case "shape":
121
230
  case "wordart":
122
231
  case "vml_shape":
232
+ return wrapInlineRawXml(node.rawXml);
233
+ case "opaque_inline":
234
+ throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
235
+ case "image":
236
+ case "column_break":
237
+ case "symbol":
123
238
  default:
124
239
  throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
125
240
  }
126
241
  }
127
242
 
243
+ function wrapInlineRawXml(rawXml: string): string {
244
+ const trimmed = rawXml.trimStart();
245
+ return trimmed.startsWith("<w:r") ? rawXml : `<w:r>${rawXml}</w:r>`;
246
+ }
247
+
128
248
  function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
129
249
  if (!marks || marks.length === 0) {
130
250
  return "";
@@ -148,8 +268,31 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
148
268
  case "doubleStrikethrough":
149
269
  parts.push("<w:dstrike/>");
150
270
  break;
271
+ case "fontFamily":
272
+ parts.push(`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`);
273
+ break;
274
+ case "fontSize":
275
+ parts.push(`<w:sz w:val="${mark.val}"/>`);
276
+ break;
277
+ case "textColor":
278
+ parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
279
+ break;
280
+ case "highlight":
281
+ parts.push(`<w:highlight w:val="${escapeAttribute(mark.val)}"/>`);
282
+ break;
283
+ case "backgroundColor":
284
+ parts.push(`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`);
285
+ break;
286
+ case "smallCaps":
287
+ parts.push("<w:smallCaps/>");
288
+ break;
289
+ case "allCaps":
290
+ parts.push("<w:caps/>");
291
+ break;
151
292
  default:
152
- throw new Error(`Cannot safely serialize ${mark.type} marks in header/footer sub-parts.`);
293
+ // Marks outside the secondary-story contract are silently dropped
294
+ // to avoid export failure on content the parser extracted safely.
295
+ break;
153
296
  }
154
297
  }
155
298
 
@@ -177,3 +320,53 @@ function escapeAttribute(value: string): string {
177
320
  .replace(/>/g, "&gt;")
178
321
  .replace(/"/g, "&quot;");
179
322
  }
323
+
324
+ function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>): string {
325
+ const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
326
+ if (node.href.startsWith("#")) {
327
+ return `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
328
+ }
329
+ if (!/^rId[A-Za-z0-9._-]+$/u.test(node.href)) {
330
+ throw new Error("Cannot safely serialize URL-backed header/footer hyperlinks without relationship context.");
331
+ }
332
+ return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
333
+ }
334
+
335
+ function wrapPropertiesXml(tagName: "w:trPr" | "w:tcPr", xml: string): string {
336
+ const trimmed = xml.trim();
337
+ if (trimmed.startsWith(`<${tagName}`)) {
338
+ return trimmed;
339
+ }
340
+ return `<${tagName}>${trimmed}</${tagName}>`;
341
+ }
342
+
343
+ function extractWrappedChildren(tagName: "w:tcPr", xml: string | undefined): string {
344
+ if (!xml) {
345
+ return "";
346
+ }
347
+ const trimmed = xml.trim();
348
+ const wrapped = new RegExp(`^<${tagName}\\b[^>]*>([\\s\\S]*)</${tagName}>$`, "u").exec(trimmed);
349
+ return wrapped?.[1] ?? trimmed;
350
+ }
351
+
352
+ function buildTableCellPropertiesXml(cell: TableCellNode): string {
353
+ const innerXml = extractWrappedChildren("w:tcPr", cell.propertiesXml)
354
+ .replace(/<w:gridSpan\b[^>]*\/>/gu, "")
355
+ .replace(/<w:vMerge\b[^>]*\/>/gu, "")
356
+ .trim();
357
+ const parts: string[] = [];
358
+ if (cell.gridSpan && cell.gridSpan > 1) {
359
+ parts.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
360
+ }
361
+ if (cell.verticalMerge) {
362
+ parts.push(
363
+ cell.verticalMerge === "restart"
364
+ ? `<w:vMerge w:val="restart"/>`
365
+ : `<w:vMerge/>`,
366
+ );
367
+ }
368
+ if (innerXml.length > 0) {
369
+ parts.push(innerXml);
370
+ }
371
+ return parts.length > 0 ? `<w:tcPr>${parts.join("")}</w:tcPr>` : "";
372
+ }
@@ -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/>";
@@ -355,7 +407,7 @@ function serializeTableInlineNode(
355
407
  case "shape":
356
408
  case "wordart":
357
409
  case "vml_shape":
358
- return node.rawXml;
410
+ return wrapInlineRawXml(node.rawXml);
359
411
  case "hyperlink": {
360
412
  const hyperlinkOpen = node.href.startsWith("#")
361
413
  ? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
@@ -374,8 +426,24 @@ function serializeTableInlineNode(
374
426
  const childrenXml = node.children.map((child) => serializeTableInlineNode(child, state)).join("");
375
427
  return `${hyperlinkOpen}${childrenXml}</w:hyperlink>`;
376
428
  }
377
- case "field":
429
+ case "field": {
430
+ if (node.children && node.children.length > 0) {
431
+ const childrenXml = node.children
432
+ .map((child) => serializeTableInlineNode(child, state))
433
+ .join("");
434
+ if (node.fieldType === "complex") {
435
+ return (
436
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
437
+ `<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
438
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
439
+ childrenXml +
440
+ `<w:r><w:fldChar w:fldCharType="end"/></w:r>`
441
+ );
442
+ }
443
+ return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
444
+ }
378
445
  return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
446
+ }
379
447
  case "bookmark_start":
380
448
  return (
381
449
  `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}"` +
@@ -432,6 +500,13 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
432
500
  if (s.lineRule !== undefined) attrs.push(`w:lineRule="${s.lineRule}"`);
433
501
  if (attrs.length > 0) children.push(`<w:spacing ${attrs.join(" ")}/>`);
434
502
  }
503
+ if (paragraph.contextualSpacing !== undefined) {
504
+ children.push(
505
+ paragraph.contextualSpacing
506
+ ? "<w:contextualSpacing/>"
507
+ : `<w:contextualSpacing w:val="0"/>`,
508
+ );
509
+ }
435
510
  if (paragraph.indentation) {
436
511
  const ind = paragraph.indentation;
437
512
  const attrs: string[] = [];
@@ -754,7 +829,7 @@ function serializeInlineNode(
754
829
  case "wordart":
755
830
  case "vml_shape": {
756
831
  // Reattach original XML unchanged for lossless round-trip.
757
- const xml = node.rawXml;
832
+ const xml = wrapInlineRawXml(node.rawXml);
758
833
  const boundaries = new Map<number, number>();
759
834
  boundaries.set(cursor, xmlOffset);
760
835
  boundaries.set(cursor + 1, xmlOffset + xml.length);
@@ -804,6 +879,55 @@ function serializeInlineNode(
804
879
  };
805
880
  }
806
881
  case "field": {
882
+ if (node.children && node.children.length > 0) {
883
+ const boundaries = new Map<number, number>();
884
+ boundaries.set(cursor, xmlOffset);
885
+ let nextCursor = cursor;
886
+ let nextOffset = xmlOffset;
887
+
888
+ if (node.fieldType === "complex") {
889
+ const beginXml =
890
+ `<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
891
+ `<w:r><w:instrText xml:space="preserve"> ${escapeXml(node.instruction)} </w:instrText></w:r>` +
892
+ `<w:r><w:fldChar w:fldCharType="separate"/></w:r>`;
893
+ nextOffset += beginXml.length;
894
+
895
+ const children: string[] = [beginXml];
896
+ for (const child of node.children) {
897
+ const result = serializeInlineNode(child, state, nextCursor, nextOffset);
898
+ children.push(result.xml);
899
+ for (const [position, index] of result.boundaries) {
900
+ boundaries.set(position, index);
901
+ }
902
+ nextCursor = result.cursor;
903
+ nextOffset += result.xml.length;
904
+ }
905
+ const endXml = `<w:r><w:fldChar w:fldCharType="end"/></w:r>`;
906
+ children.push(endXml);
907
+ nextOffset += endXml.length;
908
+ boundaries.set(nextCursor, nextOffset);
909
+ return { xml: children.join(""), cursor: nextCursor, boundaries };
910
+ }
911
+
912
+ // Simple field with children
913
+ const openXml = `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">`;
914
+ nextOffset += openXml.length;
915
+ const children: string[] = [openXml];
916
+ for (const child of node.children) {
917
+ const result = serializeInlineNode(child, state, nextCursor, nextOffset);
918
+ children.push(result.xml);
919
+ for (const [position, index] of result.boundaries) {
920
+ boundaries.set(position, index);
921
+ }
922
+ nextCursor = result.cursor;
923
+ nextOffset += result.xml.length;
924
+ }
925
+ children.push("</w:fldSimple>");
926
+ nextOffset += "</w:fldSimple>".length;
927
+ boundaries.set(nextCursor, nextOffset);
928
+ return { xml: children.join(""), cursor: nextCursor, boundaries };
929
+ }
930
+
807
931
  const xml = `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
808
932
  const boundaries = new Map<number, number>();
809
933
  boundaries.set(cursor, xmlOffset);
@@ -914,6 +1038,9 @@ function serializeRunProperties(marks: TextMark[] | undefined): string {
914
1038
  case "lang":
915
1039
  markParts.push(`<w:lang w:val="${escapeAttribute(mark.val)}"/>`);
916
1040
  break;
1041
+ case "highlight":
1042
+ markParts.push(`<w:highlight w:val="${escapeAttribute(mark.val)}"/>`);
1043
+ break;
917
1044
  case "backgroundColor":
918
1045
  markParts.push(
919
1046
  `<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`,
@@ -1049,6 +1176,156 @@ function serializeDocumentAttributes(
1049
1176
  .join("");
1050
1177
  }
1051
1178
 
1179
+ export function serializeSectionPropertiesXml(props: SectionProperties): string {
1180
+ const children: string[] = [];
1181
+
1182
+ // Header references
1183
+ if (props.headerReferences) {
1184
+ for (const ref of props.headerReferences) {
1185
+ children.push(`<w:headerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1186
+ }
1187
+ }
1188
+
1189
+ // Footer references
1190
+ if (props.footerReferences) {
1191
+ for (const ref of props.footerReferences) {
1192
+ children.push(`<w:footerReference w:type="${escapeAttribute(ref.variant)}" r:id="${escapeAttribute(ref.relationshipId)}"/>`);
1193
+ }
1194
+ }
1195
+
1196
+ // Section type
1197
+ if (props.sectionType) {
1198
+ children.push(`<w:type w:val="${escapeAttribute(props.sectionType)}"/>`);
1199
+ }
1200
+
1201
+ // Page size
1202
+ if (props.pageSize) {
1203
+ let pgSz = `<w:pgSz w:w="${props.pageSize.width}" w:h="${props.pageSize.height}"`;
1204
+ if (props.pageSize.orientation) {
1205
+ pgSz += ` w:orient="${props.pageSize.orientation}"`;
1206
+ }
1207
+ pgSz += "/>";
1208
+ children.push(pgSz);
1209
+ }
1210
+
1211
+ // Page margins
1212
+ if (props.pageMargins) {
1213
+ let pgMar = `<w:pgMar w:top="${props.pageMargins.top}" w:right="${props.pageMargins.right}" w:bottom="${props.pageMargins.bottom}" w:left="${props.pageMargins.left}"`;
1214
+ if (props.pageMargins.header !== undefined) pgMar += ` w:header="${props.pageMargins.header}"`;
1215
+ if (props.pageMargins.footer !== undefined) pgMar += ` w:footer="${props.pageMargins.footer}"`;
1216
+ if (props.pageMargins.gutter !== undefined) pgMar += ` w:gutter="${props.pageMargins.gutter}"`;
1217
+ pgMar += "/>";
1218
+ children.push(pgMar);
1219
+ }
1220
+
1221
+ // Columns
1222
+ if (props.columns) {
1223
+ let cols = "<w:cols";
1224
+ if (props.columns.count !== undefined) cols += ` w:num="${props.columns.count}"`;
1225
+ if (props.columns.space !== undefined) cols += ` w:space="${props.columns.space}"`;
1226
+ if (props.columns.equalWidth !== undefined) cols += ` w:equalWidth="${props.columns.equalWidth ? "1" : "0"}"`;
1227
+ if (props.columns.separator) cols += ` w:sep="1"`;
1228
+ if (props.columns.columns && props.columns.columns.length > 0) {
1229
+ cols += ">";
1230
+ for (const col of props.columns.columns) {
1231
+ let colXml = `<w:col w:w="${col.width}"`;
1232
+ if (col.space !== undefined) colXml += ` w:space="${col.space}"`;
1233
+ colXml += "/>";
1234
+ cols += colXml;
1235
+ }
1236
+ cols += "</w:cols>";
1237
+ } else {
1238
+ cols += "/>";
1239
+ }
1240
+ children.push(cols);
1241
+ }
1242
+
1243
+ // Page numbering
1244
+ if (props.pageNumbering) {
1245
+ let pgNum = "<w:pgNumType";
1246
+ if (props.pageNumbering.format) pgNum += ` w:fmt="${escapeAttribute(props.pageNumbering.format)}"`;
1247
+ if (props.pageNumbering.start !== undefined) pgNum += ` w:start="${props.pageNumbering.start}"`;
1248
+ if (props.pageNumbering.chapStyle) pgNum += ` w:chapStyle="${escapeAttribute(props.pageNumbering.chapStyle)}"`;
1249
+ if (props.pageNumbering.chapSep) pgNum += ` w:chapSep="${escapeAttribute(props.pageNumbering.chapSep)}"`;
1250
+ pgNum += "/>";
1251
+ children.push(pgNum);
1252
+ }
1253
+
1254
+ if (props.lineNumbering) {
1255
+ let lineNumbering = "<w:lnNumType";
1256
+ if (props.lineNumbering.countBy !== undefined) {
1257
+ lineNumbering += ` w:countBy="${props.lineNumbering.countBy}"`;
1258
+ }
1259
+ if (props.lineNumbering.start !== undefined) {
1260
+ lineNumbering += ` w:start="${props.lineNumbering.start}"`;
1261
+ }
1262
+ if (props.lineNumbering.distance !== undefined) {
1263
+ lineNumbering += ` w:distance="${props.lineNumbering.distance}"`;
1264
+ }
1265
+ if (props.lineNumbering.restart) {
1266
+ lineNumbering += ` w:restart="${escapeAttribute(props.lineNumbering.restart)}"`;
1267
+ }
1268
+ lineNumbering += "/>";
1269
+ children.push(lineNumbering);
1270
+ }
1271
+
1272
+ if (props.pageBorders) {
1273
+ const attrs: string[] = [];
1274
+ if (props.pageBorders.offsetFrom) {
1275
+ attrs.push(`w:offsetFrom="${escapeAttribute(props.pageBorders.offsetFrom)}"`);
1276
+ }
1277
+ if (props.pageBorders.display) {
1278
+ attrs.push(`w:display="${escapeAttribute(props.pageBorders.display)}"`);
1279
+ }
1280
+ if (props.pageBorders.zOrder) {
1281
+ attrs.push(`w:zOrder="${escapeAttribute(props.pageBorders.zOrder)}"`);
1282
+ }
1283
+ const borderXml = [
1284
+ serializeBorder("top", props.pageBorders.top),
1285
+ serializeBorder("left", props.pageBorders.left),
1286
+ serializeBorder("bottom", props.pageBorders.bottom),
1287
+ serializeBorder("right", props.pageBorders.right),
1288
+ ].filter((entry) => entry.length > 0);
1289
+ if (attrs.length > 0 || borderXml.length > 0) {
1290
+ children.push(
1291
+ `<w:pgBorders${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${borderXml.join("")}</w:pgBorders>`,
1292
+ );
1293
+ }
1294
+ }
1295
+
1296
+ // Title page
1297
+ if (props.titlePage) {
1298
+ children.push("<w:titlePg/>");
1299
+ }
1300
+
1301
+ if (props.documentGrid) {
1302
+ const attrs: string[] = [];
1303
+ if (props.documentGrid.type) {
1304
+ attrs.push(`w:type="${escapeAttribute(props.documentGrid.type)}"`);
1305
+ }
1306
+ if (props.documentGrid.linePitch !== undefined) {
1307
+ attrs.push(`w:linePitch="${props.documentGrid.linePitch}"`);
1308
+ }
1309
+ if (props.documentGrid.charSpace !== undefined) {
1310
+ attrs.push(`w:charSpace="${props.documentGrid.charSpace}"`);
1311
+ }
1312
+ if (attrs.length > 0) {
1313
+ children.push(`<w:docGrid ${attrs.join(" ")}/>`);
1314
+ }
1315
+ }
1316
+
1317
+ if (children.length === 0) {
1318
+ return "<w:sectPr/>";
1319
+ }
1320
+
1321
+ return `<w:sectPr>${children.join("")}</w:sectPr>`;
1322
+ }
1323
+
1324
+ function wrapInlineRawXml(rawXml: string): string {
1325
+ const trimmed = rawXml.trimStart();
1326
+ return trimmed.startsWith("<w:r") ? rawXml : `<w:r>${rawXml}</w:r>`;
1327
+ }
1328
+
1052
1329
  function documentNeedsW14Namespace(content: DocumentRootNode): boolean {
1053
1330
  const blockQueue = [...content.children];
1054
1331
  while (blockQueue.length > 0) {