@beyondwork/docx-react-component 1.0.1 → 1.0.3

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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +325 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1506 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,982 @@
1
+ import type {
2
+ AltChunkNode,
3
+ BorderSpec,
4
+ CustomXmlNode,
5
+ DocumentRootNode,
6
+ InlineNode,
7
+ MediaCatalog,
8
+ ParagraphNode,
9
+ PreservationStore,
10
+ SdtNode,
11
+ TableNode,
12
+ TableCellNode,
13
+ TextMark,
14
+ } from "../../model/canonical-document.ts";
15
+ import type { OpcRelationship } from "../ooxml/part-manifest.ts";
16
+ import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts";
17
+ import { getOpaqueFragment } from "../../preservation/store.ts";
18
+ import { retainRelationshipsForFragment } from "../../preservation/relationship-retention.ts";
19
+ import { serializeParagraphNumberingProperties } from "./serialize-numbering.ts";
20
+
21
+ const HYPERLINK_RELATIONSHIP_TYPE =
22
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
23
+
24
+ export interface SerializedMainDocument {
25
+ documentXml: string;
26
+ relationships: OpcRelationship[];
27
+ paragraphBoundaries: RevisionParagraphBoundary[];
28
+ }
29
+
30
+ export interface SerializeMainDocumentOptions {
31
+ documentAttributes?: Record<string, string>;
32
+ media?: MediaCatalog;
33
+ }
34
+
35
+ interface SerializationState {
36
+ nextHyperlinkRelationshipIndex: number;
37
+ relationships: OpcRelationship[];
38
+ existingRelationshipMap: Map<string, OpcRelationship>;
39
+ retainedRelationshipIds: Set<string>;
40
+ media: MediaCatalog;
41
+ preservation: PreservationStore;
42
+ }
43
+
44
+ interface InlineSerializationResult {
45
+ xml: string;
46
+ cursor: number;
47
+ boundaries: Map<number, number>;
48
+ }
49
+
50
+ interface ParagraphSerializationResult {
51
+ xml: string;
52
+ nextCursor: number;
53
+ boundary: RevisionParagraphBoundary;
54
+ }
55
+
56
+ export function serializeMainDocument(
57
+ content: DocumentRootNode,
58
+ preservation: PreservationStore = { opaqueFragments: {}, packageParts: {} },
59
+ existingRelationships: readonly OpcRelationship[] = [],
60
+ options: SerializeMainDocumentOptions = {},
61
+ ): SerializedMainDocument {
62
+ const nextRelationshipIndex =
63
+ existingRelationships.reduce((maxIndex, relationship) => {
64
+ const match = /^rIdHyperlink(\d+)$/.exec(relationship.id);
65
+ return match ? Math.max(maxIndex, Number.parseInt(match[1] ?? "0", 10)) : maxIndex;
66
+ }, 0) + 1;
67
+ const state: SerializationState = {
68
+ nextHyperlinkRelationshipIndex: nextRelationshipIndex,
69
+ relationships: existingRelationships.filter(
70
+ (relationship) => relationship.type !== HYPERLINK_RELATIONSHIP_TYPE,
71
+ ).map(cloneRelationship),
72
+ existingRelationshipMap: new Map(
73
+ existingRelationships.map((relationship) => [relationship.id, cloneRelationship(relationship)]),
74
+ ),
75
+ retainedRelationshipIds: new Set(
76
+ existingRelationships
77
+ .filter((relationship) => relationship.type !== HYPERLINK_RELATIONSHIP_TYPE)
78
+ .map((relationship) => relationship.id),
79
+ ),
80
+ media: options.media ?? { items: {} },
81
+ preservation,
82
+ };
83
+ const documentOpen = `<w:document${serializeDocumentAttributes(options.documentAttributes, content)}>`;
84
+ const prefix = [
85
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
86
+ documentOpen,
87
+ ` <w:body>`,
88
+ ].join("\n");
89
+ const suffix = `</w:body>\n</w:document>`;
90
+ const bodyPieces: string[] = [];
91
+ const paragraphBoundaries: RevisionParagraphBoundary[] = [];
92
+ let bodyLength = 0;
93
+ let sectionPropertiesXml = "<w:sectPr/>";
94
+ let cursor = 0;
95
+ let paragraphIndex = -1;
96
+ let previousWasParagraph = false;
97
+
98
+ for (const block of content.children) {
99
+ if (block.type === "paragraph") {
100
+ if (previousWasParagraph) {
101
+ cursor += 1;
102
+ }
103
+
104
+ paragraphIndex += 1;
105
+ const serializedParagraph = serializeParagraph(
106
+ block,
107
+ state,
108
+ cursor,
109
+ paragraphIndex,
110
+ );
111
+ const paragraphOffset = prefix.length + bodyLength;
112
+ bodyPieces.push(serializedParagraph.xml);
113
+ bodyLength += serializedParagraph.xml.length;
114
+ paragraphBoundaries.push(
115
+ offsetParagraphBoundary(serializedParagraph.boundary, paragraphOffset),
116
+ );
117
+ cursor = serializedParagraph.nextCursor;
118
+ previousWasParagraph = true;
119
+ continue;
120
+ }
121
+
122
+ if (block.type === "table") {
123
+ const tableXml = serializeTableNode(block, state);
124
+ bodyPieces.push(tableXml);
125
+ bodyLength += tableXml.length;
126
+ cursor += 1;
127
+ previousWasParagraph = false;
128
+ continue;
129
+ }
130
+
131
+ if (block.type === "sdt") {
132
+ const sdtXml = serializeSdtNode(block, state);
133
+ bodyPieces.push(sdtXml);
134
+ bodyLength += sdtXml.length;
135
+ previousWasParagraph = false;
136
+ continue;
137
+ }
138
+
139
+ if (block.type === "custom_xml") {
140
+ const customXml = serializeCustomXmlNode(block, state);
141
+ bodyPieces.push(customXml);
142
+ bodyLength += customXml.length;
143
+ previousWasParagraph = false;
144
+ continue;
145
+ }
146
+
147
+ if (block.type === "alt_chunk") {
148
+ const altChunkXml = serializeAltChunkNode(block);
149
+ bodyPieces.push(altChunkXml);
150
+ bodyLength += altChunkXml.length;
151
+ cursor += 1;
152
+ previousWasParagraph = false;
153
+ continue;
154
+ }
155
+
156
+ const blockXml = serializeOpaqueBlock(block, state);
157
+ if (looksLikeSectionPropertiesXml(blockXml)) {
158
+ sectionPropertiesXml = blockXml;
159
+ } else {
160
+ bodyPieces.push(blockXml);
161
+ bodyLength += blockXml.length;
162
+ }
163
+ cursor += 1;
164
+ previousWasParagraph = false;
165
+ }
166
+
167
+ const bodyXml = bodyPieces.join("");
168
+ const documentXml = `${prefix}${bodyXml || "<w:p><w:r><w:t></w:t></w:r></w:p>"}${sectionPropertiesXml}${suffix}`;
169
+
170
+ return {
171
+ documentXml,
172
+ relationships: state.relationships,
173
+ paragraphBoundaries,
174
+ };
175
+ }
176
+
177
+ function serializeOpaqueBlock(
178
+ block: Extract<DocumentRootNode["children"][number], { type: "opaque_block" }>,
179
+ state: SerializationState,
180
+ ): string {
181
+ return lookupOpaqueXml(block.fragmentId, state);
182
+ }
183
+
184
+ function serializeTableNode(
185
+ table: TableNode,
186
+ state: SerializationState,
187
+ ): string {
188
+ const propertiesXml = table.propertiesXml ?? buildTablePropertiesXml(table);
189
+ const gridXml =
190
+ table.gridColumns.length > 0
191
+ ? `<w:tblGrid>${table.gridColumns
192
+ .map((width) => `<w:gridCol w:w="${width}"/>`)
193
+ .join("")}</w:tblGrid>`
194
+ : "";
195
+ const rowsXml = table.rows
196
+ .map((row) => {
197
+ const rowPropertiesXml = row.propertiesXml ?? "";
198
+ const cellsXml = row.cells
199
+ .map((cell) => serializeTableCellNode(cell, state))
200
+ .join("");
201
+ return `<w:tr>${rowPropertiesXml}${cellsXml}</w:tr>`;
202
+ })
203
+ .join("");
204
+ return `<w:tbl>${propertiesXml}${gridXml}${rowsXml}</w:tbl>`;
205
+ }
206
+
207
+ function serializeTableCellNode(
208
+ cell: TableCellNode,
209
+ state: SerializationState,
210
+ ): string {
211
+ const propertiesXml = cell.propertiesXml ?? buildCellPropertiesXml(cell);
212
+ const blocksXml = cell.children
213
+ .map((child) => serializeBlockNode(child, state))
214
+ .join("");
215
+ return `<w:tc>${propertiesXml}${blocksXml || "<w:p/>"}</w:tc>`;
216
+ }
217
+
218
+ function serializeBlockNode(
219
+ block: DocumentRootNode["children"][number],
220
+ state: SerializationState,
221
+ ): string {
222
+ switch (block.type) {
223
+ case "paragraph":
224
+ return serializeTableCellParagraph(block, state);
225
+ case "table":
226
+ return serializeTableNode(block, state);
227
+ case "sdt":
228
+ return serializeSdtNode(block, state);
229
+ case "custom_xml":
230
+ return serializeCustomXmlNode(block, state);
231
+ case "alt_chunk":
232
+ return serializeAltChunkNode(block);
233
+ case "opaque_block":
234
+ return lookupOpaqueXml(block.fragmentId, state);
235
+ case "section_break":
236
+ return block.propertiesXml ?? "<w:sectPr/>";
237
+ }
238
+ }
239
+
240
+ function buildCellPropertiesXml(cell: TableCellNode): string {
241
+ const children: string[] = [];
242
+ if (cell.gridSpan && cell.gridSpan > 1) {
243
+ children.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
244
+ }
245
+ if (cell.verticalMerge) {
246
+ children.push(
247
+ cell.verticalMerge === "restart"
248
+ ? `<w:vMerge w:val="restart"/>`
249
+ : `<w:vMerge/>`,
250
+ );
251
+ }
252
+ return children.length > 0 ? `<w:tcPr>${children.join("")}</w:tcPr>` : "";
253
+ }
254
+
255
+ function serializeSdtNode(
256
+ block: SdtNode,
257
+ state: SerializationState,
258
+ ): string {
259
+ const propertiesXml = block.properties.propertiesXml ?? buildSdtPropertiesXml(block);
260
+ const childrenXml = block.children.map((child) => serializeBlockNode(child, state)).join("") || "<w:p/>";
261
+ return `<w:sdt>${propertiesXml}<w:sdtContent>${childrenXml}</w:sdtContent></w:sdt>`;
262
+ }
263
+
264
+ function serializeCustomXmlNode(
265
+ block: CustomXmlNode,
266
+ state: SerializationState,
267
+ ): string {
268
+ const attrs: string[] = [];
269
+ if (block.uri) {
270
+ attrs.push(`w:uri="${escapeAttribute(block.uri)}"`);
271
+ }
272
+ if (block.element) {
273
+ attrs.push(`w:element="${escapeAttribute(block.element)}"`);
274
+ }
275
+ const attrXml = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
276
+ const childrenXml = block.children.map((child) => serializeBlockNode(child, state)).join("");
277
+ return `<w:customXml${attrXml}>${childrenXml || "<w:p/>"}</w:customXml>`;
278
+ }
279
+
280
+ function serializeAltChunkNode(
281
+ block: AltChunkNode,
282
+ ): string {
283
+ return `<w:altChunk r:id="${escapeAttribute(block.relationshipId)}"/>`;
284
+ }
285
+
286
+ function buildSdtPropertiesXml(block: SdtNode): string {
287
+ const children: string[] = [];
288
+ if (block.properties.alias) {
289
+ children.push(`<w:alias w:val="${escapeAttribute(block.properties.alias)}"/>`);
290
+ }
291
+ if (block.properties.tag) {
292
+ children.push(`<w:tag w:val="${escapeAttribute(block.properties.tag)}"/>`);
293
+ }
294
+ if (block.properties.lock) {
295
+ children.push(`<w:lock w:val="${escapeAttribute(block.properties.lock)}"/>`);
296
+ }
297
+ if (block.properties.sdtType) {
298
+ children.push(`<w:${block.properties.sdtType}/>`);
299
+ }
300
+ return children.length > 0 ? `<w:sdtPr>${children.join("")}</w:sdtPr>` : "<w:sdtPr/>";
301
+ }
302
+
303
+ function serializeTableCellParagraph(
304
+ paragraph: ParagraphNode,
305
+ state: SerializationState,
306
+ ): string {
307
+ let xml = "<w:p>";
308
+ const paragraphPropertiesXml = buildParagraphPropertiesXml(paragraph);
309
+ if (paragraphPropertiesXml.length > 0) {
310
+ xml += paragraphPropertiesXml;
311
+ }
312
+ const childrenXml = paragraph.children.map((child) => serializeTableInlineNode(child, state)).join("");
313
+ xml += childrenXml || "<w:r><w:t></w:t></w:r>";
314
+ xml += "</w:p>";
315
+ return xml;
316
+ }
317
+
318
+ function serializeTableInlineNode(
319
+ node: InlineNode,
320
+ state: SerializationState,
321
+ ): string {
322
+ switch (node.type) {
323
+ case "text": {
324
+ const marks = node.marks;
325
+ const properties = serializeRunPropertiesFromMarks(marks);
326
+ const preserve = requiresPreservedSpace(node.text) ? ` xml:space="preserve"` : "";
327
+ return `<w:r>${properties}<w:t${preserve}>${escapeXml(node.text)}</w:t></w:r>`;
328
+ }
329
+ case "tab":
330
+ return "<w:r><w:tab/></w:r>";
331
+ case "column_break":
332
+ return "<w:r><w:br w:type=\"column\"/></w:r>";
333
+ case "hard_break":
334
+ return "<w:r><w:br/></w:r>";
335
+ case "symbol": {
336
+ const properties = serializeRunPropertiesFromMarks(node.marks);
337
+ const fontAttribute = node.font ? ` w:font="${escapeAttribute(node.font)}"` : "";
338
+ return `<w:r>${properties}<w:sym${fontAttribute} w:char="${escapeAttribute(node.char)}"/></w:r>`;
339
+ }
340
+ case "image":
341
+ return serializeImageNode(node, state);
342
+ case "opaque_inline":
343
+ return lookupOpaqueXml(node.fragmentId, state);
344
+ case "chart_preview":
345
+ case "smartart_preview":
346
+ case "shape":
347
+ case "wordart":
348
+ case "vml_shape":
349
+ return node.rawXml;
350
+ case "hyperlink": {
351
+ const hyperlinkOpen = node.href.startsWith("#")
352
+ ? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
353
+ : (() => {
354
+ const relationshipId = `rIdHyperlink${state.nextHyperlinkRelationshipIndex}`;
355
+ state.nextHyperlinkRelationshipIndex += 1;
356
+ state.relationships.push({
357
+ id: relationshipId,
358
+ type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
359
+ target: node.href,
360
+ targetMode: "external",
361
+ });
362
+ state.retainedRelationshipIds.add(relationshipId);
363
+ return `<w:hyperlink r:id="${relationshipId}">`;
364
+ })();
365
+ const childrenXml = node.children.map((child) => serializeTableInlineNode(child, state)).join("");
366
+ return `${hyperlinkOpen}${childrenXml}</w:hyperlink>`;
367
+ }
368
+ }
369
+ }
370
+
371
+ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
372
+ const children: string[] = [];
373
+
374
+ if (paragraph.styleId) {
375
+ children.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
376
+ }
377
+ if (paragraph.keepNext) {
378
+ children.push("<w:keepNext/>");
379
+ }
380
+ if (paragraph.keepLines) {
381
+ children.push("<w:keepLines/>");
382
+ }
383
+ if (paragraph.pageBreakBefore) {
384
+ children.push("<w:pageBreakBefore/>");
385
+ }
386
+ if (paragraph.widowControl) {
387
+ children.push("<w:widowControl/>");
388
+ }
389
+ if (paragraph.outlineLevel !== undefined) {
390
+ children.push(`<w:outlineLvl w:val="${paragraph.outlineLevel}"/>`);
391
+ }
392
+ if (paragraph.numbering) {
393
+ children.push(serializeParagraphNumberingProperties(paragraph.numbering));
394
+ }
395
+ if (paragraph.spacing) {
396
+ const s = paragraph.spacing;
397
+ const attrs: string[] = [];
398
+ if (s.before !== undefined) attrs.push(`w:before="${s.before}"`);
399
+ if (s.after !== undefined) attrs.push(`w:after="${s.after}"`);
400
+ if (s.line !== undefined) attrs.push(`w:line="${s.line}"`);
401
+ if (s.lineRule !== undefined) attrs.push(`w:lineRule="${s.lineRule}"`);
402
+ if (attrs.length > 0) children.push(`<w:spacing ${attrs.join(" ")}/>`);
403
+ }
404
+ if (paragraph.indentation) {
405
+ const ind = paragraph.indentation;
406
+ const attrs: string[] = [];
407
+ if (ind.left !== undefined) attrs.push(`w:left="${ind.left}"`);
408
+ if (ind.right !== undefined) attrs.push(`w:right="${ind.right}"`);
409
+ if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${ind.firstLine}"`);
410
+ if (ind.hanging !== undefined) attrs.push(`w:hanging="${ind.hanging}"`);
411
+ if (attrs.length > 0) children.push(`<w:ind ${attrs.join(" ")}/>`);
412
+ }
413
+ if (paragraph.alignment) {
414
+ children.push(`<w:jc w:val="${paragraph.alignment}"/>`);
415
+ }
416
+ if (paragraph.borders) {
417
+ const bordersXml = serializeParagraphBorders(paragraph.borders);
418
+ if (bordersXml) {
419
+ children.push(bordersXml);
420
+ }
421
+ }
422
+ if (paragraph.shading) {
423
+ const shadingXml = serializeParagraphShading(paragraph.shading);
424
+ if (shadingXml) {
425
+ children.push(shadingXml);
426
+ }
427
+ }
428
+ if (paragraph.bidi) {
429
+ children.push("<w:bidi/>");
430
+ }
431
+ if (paragraph.suppressLineNumbers) {
432
+ children.push("<w:suppressLineNumbers/>");
433
+ }
434
+ if (paragraph.cnfStyle) {
435
+ children.push(`<w:cnfStyle w:val="${escapeAttribute(paragraph.cnfStyle)}"/>`);
436
+ }
437
+ if (paragraph.tabStops && paragraph.tabStops.length > 0) {
438
+ const tabsXml = paragraph.tabStops.map((tab) => {
439
+ const leaderAttr = tab.leader ? ` w:leader="${tab.leader}"` : "";
440
+ return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
441
+ }).join("");
442
+ children.push(`<w:tabs>${tabsXml}</w:tabs>`);
443
+ }
444
+
445
+ if (children.length === 0) return "";
446
+ return `<w:pPr>${children.join("")}</w:pPr>`;
447
+ }
448
+
449
+ function buildTablePropertiesXml(table: TableNode): string {
450
+ const children: string[] = [];
451
+ if (table.styleId) {
452
+ children.push(`<w:tblStyle w:val="${escapeAttribute(table.styleId)}"/>`);
453
+ }
454
+ if (table.tblLook) {
455
+ const attrs: string[] = [];
456
+ if (table.tblLook.val) {
457
+ attrs.push(`w:val="${escapeAttribute(table.tblLook.val)}"`);
458
+ }
459
+ for (const [key, attr] of [
460
+ ["firstRow", "w:firstRow"],
461
+ ["lastRow", "w:lastRow"],
462
+ ["firstColumn", "w:firstColumn"],
463
+ ["lastColumn", "w:lastColumn"],
464
+ ["noHBand", "w:noHBand"],
465
+ ["noVBand", "w:noVBand"],
466
+ ] as const) {
467
+ const value = table.tblLook[key];
468
+ if (value !== undefined) {
469
+ attrs.push(`${attr}="${value ? "1" : "0"}"`);
470
+ }
471
+ }
472
+ if (attrs.length > 0) {
473
+ children.push(`<w:tblLook ${attrs.join(" ")}/>`);
474
+ }
475
+ }
476
+ return children.length > 0 ? `<w:tblPr>${children.join("")}</w:tblPr>` : "";
477
+ }
478
+
479
+ function serializeParagraphBorders(borders: ParagraphNode["borders"]): string {
480
+ if (!borders) {
481
+ return "";
482
+ }
483
+ const parts: string[] = [];
484
+ for (const [name, border] of [
485
+ ["top", borders.top],
486
+ ["left", borders.left],
487
+ ["bottom", borders.bottom],
488
+ ["right", borders.right],
489
+ ["bar", borders.bar],
490
+ ["between", borders.between],
491
+ ] as const) {
492
+ const xml = serializeBorder(name, border);
493
+ if (xml) {
494
+ parts.push(xml);
495
+ }
496
+ }
497
+ return parts.length > 0 ? `<w:pBdr>${parts.join("")}</w:pBdr>` : "";
498
+ }
499
+
500
+ function serializeBorder(name: string, border: BorderSpec | undefined): string {
501
+ if (!border) {
502
+ return "";
503
+ }
504
+ const attrs: string[] = [];
505
+ if (border.value) attrs.push(`w:val="${escapeAttribute(border.value)}"`);
506
+ if (border.size !== undefined) attrs.push(`w:sz="${border.size}"`);
507
+ if (border.space !== undefined) attrs.push(`w:space="${border.space}"`);
508
+ if (border.color) attrs.push(`w:color="${escapeAttribute(border.color)}"`);
509
+ return attrs.length > 0 ? `<w:${name} ${attrs.join(" ")}/>` : "";
510
+ }
511
+
512
+ function serializeParagraphShading(shading: ParagraphNode["shading"]): string {
513
+ if (!shading) {
514
+ return "";
515
+ }
516
+ const attrs: string[] = [];
517
+ if (shading.val) attrs.push(`w:val="${escapeAttribute(shading.val)}"`);
518
+ if (shading.color) attrs.push(`w:color="${escapeAttribute(shading.color)}"`);
519
+ if (shading.fill) attrs.push(`w:fill="${escapeAttribute(shading.fill)}"`);
520
+ return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
521
+ }
522
+
523
+ function serializeRunPropertiesFromMarks(marks: TextMark[] | undefined): string {
524
+ return serializeRunProperties(marks);
525
+ }
526
+
527
+ function serializeParagraph(
528
+ paragraph: ParagraphNode,
529
+ state: SerializationState,
530
+ cursor: number,
531
+ paragraphIndex: number,
532
+ ): ParagraphSerializationResult {
533
+ let xml = "<w:p>";
534
+ const boundaries = new Map<number, number>();
535
+ const paragraphStart = 0;
536
+ const paragraphStartTagEnd = xml.length;
537
+ boundaries.set(cursor, paragraphStartTagEnd);
538
+
539
+ let paragraphPropertiesStart: number | undefined;
540
+ let paragraphPropertiesEnd: number | undefined;
541
+
542
+ const paragraphPropertiesXml = buildParagraphPropertiesXml(paragraph);
543
+ if (paragraphPropertiesXml.length > 0) {
544
+ paragraphPropertiesStart = xml.length;
545
+ xml += paragraphPropertiesXml;
546
+ paragraphPropertiesEnd = xml.length;
547
+ }
548
+
549
+ const children = serializeParagraphChildren(paragraph.children, state, cursor, xml.length);
550
+ xml += children.xml;
551
+ const contentEmpty = children.xml.length === 0;
552
+ if (contentEmpty) {
553
+ xml += "<w:r><w:t></w:t></w:r>";
554
+ }
555
+ const paragraphEndTagStart = xml.length;
556
+ xml += "</w:p>";
557
+
558
+ if (!children.boundaries.has(children.cursor)) {
559
+ children.boundaries.set(children.cursor, paragraphEndTagStart);
560
+ }
561
+
562
+ return {
563
+ xml,
564
+ nextCursor: children.cursor,
565
+ boundary: {
566
+ paragraphIndex,
567
+ start: cursor,
568
+ end: children.cursor,
569
+ boundaries: children.boundaries,
570
+ paragraphStart,
571
+ paragraphStartTagEnd,
572
+ paragraphEndTagStart,
573
+ paragraphEnd: xml.length,
574
+ ...(paragraphPropertiesStart !== undefined
575
+ ? { paragraphPropertiesStart }
576
+ : {}),
577
+ ...(paragraphPropertiesEnd !== undefined ? { paragraphPropertiesEnd } : {}),
578
+ },
579
+ };
580
+ }
581
+
582
+ function serializeParagraphChildren(
583
+ children: InlineNode[],
584
+ state: SerializationState,
585
+ cursor: number,
586
+ xmlOffset: number,
587
+ ): InlineSerializationResult {
588
+ const pieces: string[] = [];
589
+ const boundaries = new Map<number, number>();
590
+ let nextCursor = cursor;
591
+ let nextOffset = xmlOffset;
592
+ boundaries.set(nextCursor, nextOffset);
593
+
594
+ for (const child of children) {
595
+ const result = serializeInlineNode(child, state, nextCursor, nextOffset);
596
+ pieces.push(result.xml);
597
+ for (const [position, index] of result.boundaries) {
598
+ boundaries.set(position, index);
599
+ }
600
+ nextCursor = result.cursor;
601
+ nextOffset += result.xml.length;
602
+ }
603
+
604
+ return {
605
+ xml: pieces.join(""),
606
+ cursor: nextCursor,
607
+ boundaries,
608
+ };
609
+ }
610
+
611
+ type RunPiece =
612
+ | { kind: "text"; text: string; marks?: TextMark[] }
613
+ | { kind: "tab" }
614
+ | { kind: "hard_break" };
615
+
616
+ function serializeRunPiece(piece: RunPiece): string {
617
+ switch (piece.kind) {
618
+ case "text":
619
+ return serializeText(piece.text);
620
+ case "tab":
621
+ return "<w:tab/>";
622
+ case "hard_break":
623
+ return "<w:br/>";
624
+ }
625
+ }
626
+
627
+ function serializeText(text: string): string {
628
+ const preserve = requiresPreservedSpace(text) ? ` xml:space="preserve"` : "";
629
+ return `<w:t${preserve}>${escapeXml(text)}</w:t>`;
630
+ }
631
+
632
+ function serializeInlineNode(
633
+ node: InlineNode,
634
+ state: SerializationState,
635
+ cursor: number,
636
+ xmlOffset: number,
637
+ ): InlineSerializationResult {
638
+ switch (node.type) {
639
+ case "text": {
640
+ const xml = serializeRun({
641
+ kind: "text",
642
+ text: node.text,
643
+ ...(node.marks && node.marks.length > 0 ? { marks: node.marks } : {}),
644
+ });
645
+ const boundaries = new Map<number, number>();
646
+ boundaries.set(cursor, xmlOffset);
647
+ boundaries.set(cursor + Array.from(node.text).length, xmlOffset + xml.length);
648
+ return {
649
+ xml,
650
+ cursor: cursor + Array.from(node.text).length,
651
+ boundaries,
652
+ };
653
+ }
654
+ case "tab": {
655
+ const xml = serializeRun({ kind: "tab" });
656
+ const boundaries = new Map<number, number>();
657
+ boundaries.set(cursor, xmlOffset);
658
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
659
+ return {
660
+ xml,
661
+ cursor: cursor + 1,
662
+ boundaries,
663
+ };
664
+ }
665
+ case "column_break": {
666
+ const xml = `<w:r><w:br w:type="column"/></w:r>`;
667
+ const boundaries = new Map<number, number>();
668
+ boundaries.set(cursor, xmlOffset);
669
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
670
+ return {
671
+ xml,
672
+ cursor: cursor + 1,
673
+ boundaries,
674
+ };
675
+ }
676
+ case "hard_break": {
677
+ const xml = serializeRun({ kind: "hard_break" });
678
+ const boundaries = new Map<number, number>();
679
+ boundaries.set(cursor, xmlOffset);
680
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
681
+ return {
682
+ xml,
683
+ cursor: cursor + 1,
684
+ boundaries,
685
+ };
686
+ }
687
+ case "symbol": {
688
+ const xml = serializeTableInlineNode(node, state);
689
+ const boundaries = new Map<number, number>();
690
+ boundaries.set(cursor, xmlOffset);
691
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
692
+ return {
693
+ xml,
694
+ cursor: cursor + 1,
695
+ boundaries,
696
+ };
697
+ }
698
+ case "image": {
699
+ const xml = serializeImageNode(node, state);
700
+ const boundaries = new Map<number, number>();
701
+ boundaries.set(cursor, xmlOffset);
702
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
703
+ return {
704
+ xml,
705
+ cursor: cursor + 1,
706
+ boundaries,
707
+ };
708
+ }
709
+ case "opaque_inline": {
710
+ const xml = lookupOpaqueXml(node.fragmentId, state);
711
+ const boundaries = new Map<number, number>();
712
+ boundaries.set(cursor, xmlOffset);
713
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
714
+ return {
715
+ xml,
716
+ cursor: cursor + 1,
717
+ boundaries,
718
+ };
719
+ }
720
+ case "chart_preview":
721
+ case "smartart_preview":
722
+ case "shape":
723
+ case "wordart":
724
+ case "vml_shape": {
725
+ // Reattach original XML unchanged for lossless round-trip.
726
+ const xml = node.rawXml;
727
+ const boundaries = new Map<number, number>();
728
+ boundaries.set(cursor, xmlOffset);
729
+ boundaries.set(cursor + 1, xmlOffset + xml.length);
730
+ return {
731
+ xml,
732
+ cursor: cursor + 1,
733
+ boundaries,
734
+ };
735
+ }
736
+ case "hyperlink": {
737
+ const hyperlinkOpen = node.href.startsWith("#")
738
+ ? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
739
+ : (() => {
740
+ const relationshipId = `rIdHyperlink${state.nextHyperlinkRelationshipIndex}`;
741
+ state.nextHyperlinkRelationshipIndex += 1;
742
+ state.relationships.push({
743
+ id: relationshipId,
744
+ type: HYPERLINK_RELATIONSHIP_TYPE,
745
+ target: node.href,
746
+ targetMode: "external",
747
+ });
748
+ state.retainedRelationshipIds.add(relationshipId);
749
+ return `<w:hyperlink r:id="${relationshipId}">`;
750
+ })();
751
+ const hyperlinkClose = "</w:hyperlink>";
752
+ const boundaries = new Map<number, number>();
753
+ let nextCursor = cursor;
754
+ let nextOffset = xmlOffset + hyperlinkOpen.length;
755
+ boundaries.set(cursor, nextOffset);
756
+ const children: string[] = [];
757
+
758
+ for (const child of node.children) {
759
+ const result = serializeInlineNode(child, state, nextCursor, nextOffset);
760
+ children.push(result.xml);
761
+ for (const [position, index] of result.boundaries) {
762
+ boundaries.set(position, index);
763
+ }
764
+ nextCursor = result.cursor;
765
+ nextOffset += result.xml.length;
766
+ }
767
+
768
+ boundaries.set(nextCursor, nextOffset);
769
+ return {
770
+ xml: `${hyperlinkOpen}${children.join("")}${hyperlinkClose}`,
771
+ cursor: nextCursor,
772
+ boundaries,
773
+ };
774
+ }
775
+ }
776
+ }
777
+
778
+ function serializeImageNode(
779
+ node: Extract<InlineNode, { type: "image" }>,
780
+ state: SerializationState,
781
+ ): string {
782
+ const placementXml = typeof node.placementXml === "string" ? node.placementXml.trim() : "";
783
+ if (placementXml.length > 0) {
784
+ return placementXml.startsWith("<w:r") ? placementXml : `<w:r>${placementXml}</w:r>`;
785
+ }
786
+
787
+ const mediaItem = state.media.items[node.mediaId];
788
+ if (mediaItem?.relationshipId && state.existingRelationshipMap.has(mediaItem.relationshipId)) {
789
+ const altText = node.altText ?? mediaItem.altText ?? mediaItem.filename;
790
+ return `<w:r><w:drawing><wp:inline xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="9525" cy="9525"/><wp:docPr id="1" name="${escapeAttribute(mediaItem.filename)}" descr="${escapeAttribute(altText ?? "")}"/><wp:cNvGraphicFramePr/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="${escapeAttribute(mediaItem.filename)}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${escapeAttribute(mediaItem.relationshipId)}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="9525" cy="9525"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
791
+ }
792
+
793
+ return serializeRun({
794
+ kind: "text",
795
+ text: node.altText ?? "[Image]",
796
+ });
797
+ }
798
+
799
+ function serializeRun(piece: RunPiece): string {
800
+ const marks = piece.kind === "text" ? piece.marks : undefined;
801
+ const properties = serializeRunProperties(marks);
802
+ const content = serializeRunPiece(piece);
803
+ return `<w:r>${properties}${content}</w:r>`;
804
+ }
805
+
806
+ function serializeRunProperties(marks: TextMark[] | undefined): string {
807
+ if (!marks || marks.length === 0) {
808
+ return "";
809
+ }
810
+
811
+ const markParts: string[] = [];
812
+ for (const mark of marks) {
813
+ switch (mark.type) {
814
+ case "bold":
815
+ markParts.push("<w:b/>");
816
+ break;
817
+ case "italic":
818
+ markParts.push("<w:i/>");
819
+ break;
820
+ case "underline":
821
+ markParts.push(`<w:u w:val="single"/>`);
822
+ break;
823
+ case "strikethrough":
824
+ markParts.push("<w:strike/>");
825
+ break;
826
+ case "doubleStrikethrough":
827
+ markParts.push("<w:dstrike/>");
828
+ break;
829
+ case "vanish":
830
+ markParts.push("<w:vanish/>");
831
+ break;
832
+ case "lang":
833
+ markParts.push(`<w:lang w:val="${escapeAttribute(mark.val)}"/>`);
834
+ break;
835
+ case "backgroundColor":
836
+ markParts.push(
837
+ `<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`,
838
+ );
839
+ break;
840
+ case "charSpacing":
841
+ markParts.push(`<w:spacing w:val="${mark.val}"/>`);
842
+ break;
843
+ case "kerning":
844
+ markParts.push(`<w:kern w:val="${mark.val}"/>`);
845
+ break;
846
+ case "emboss":
847
+ markParts.push("<w:emboss/>");
848
+ break;
849
+ case "imprint":
850
+ markParts.push("<w:imprint/>");
851
+ break;
852
+ case "shadow":
853
+ markParts.push("<w:shadow/>");
854
+ break;
855
+ case "position":
856
+ markParts.push(`<w:position w:val="${mark.val}"/>`);
857
+ break;
858
+ case "textFill":
859
+ markParts.push(mark.xml);
860
+ break;
861
+ }
862
+ }
863
+
864
+ const children = markParts.join("");
865
+ return children.length > 0 ? `<w:rPr>${children}</w:rPr>` : "";
866
+ }
867
+
868
+ function requiresPreservedSpace(text: string): boolean {
869
+ return /^\s/.test(text) || /\s$/.test(text) || text.includes(" ");
870
+ }
871
+
872
+ function lookupOpaqueXml(fragmentId: string, state: SerializationState): string {
873
+ const fragment = getOpaqueFragment(state.preservation, fragmentId);
874
+ if (!fragment || fragment.payloadKind !== "xml-subtree") {
875
+ throw new Error(`Missing preserved OOXML fragment ${fragmentId} during serialization.`);
876
+ }
877
+
878
+ retainRelationshipsForFragment(
879
+ fragment,
880
+ state.relationships,
881
+ state.existingRelationshipMap,
882
+ state.retainedRelationshipIds,
883
+ );
884
+ return fragment.payloadReference;
885
+ }
886
+
887
+ function cloneRelationship(relationship: OpcRelationship): OpcRelationship {
888
+ return { ...relationship };
889
+ }
890
+
891
+ function looksLikeSectionPropertiesXml(xml: string): boolean {
892
+ return /^<[^>]*:?sectPr(?:\s|>|\/)/.test(xml.trim());
893
+ }
894
+
895
+ function escapeXml(value: string): string {
896
+ return value
897
+ .replace(/&/g, "&amp;")
898
+ .replace(/</g, "&lt;")
899
+ .replace(/>/g, "&gt;");
900
+ }
901
+
902
+ function escapeAttribute(value: string): string {
903
+ return escapeXml(value).replace(/"/g, "&quot;");
904
+ }
905
+
906
+ function offsetParagraphBoundary(
907
+ boundary: RevisionParagraphBoundary,
908
+ offset: number,
909
+ ): RevisionParagraphBoundary {
910
+ return {
911
+ ...boundary,
912
+ boundaries: new Map(
913
+ [...boundary.boundaries.entries()].map(([position, index]) => [
914
+ position,
915
+ index + offset,
916
+ ]),
917
+ ),
918
+ paragraphStart: boundary.paragraphStart + offset,
919
+ paragraphStartTagEnd: boundary.paragraphStartTagEnd + offset,
920
+ paragraphEndTagStart: boundary.paragraphEndTagStart + offset,
921
+ paragraphEnd: boundary.paragraphEnd + offset,
922
+ ...(boundary.paragraphPropertiesStart !== undefined
923
+ ? { paragraphPropertiesStart: boundary.paragraphPropertiesStart + offset }
924
+ : {}),
925
+ ...(boundary.paragraphPropertiesEnd !== undefined
926
+ ? { paragraphPropertiesEnd: boundary.paragraphPropertiesEnd + offset }
927
+ : {}),
928
+ ...(boundary.paragraphRunPropertiesStart !== undefined
929
+ ? { paragraphRunPropertiesStart: boundary.paragraphRunPropertiesStart + offset }
930
+ : {}),
931
+ ...(boundary.paragraphRunPropertiesEnd !== undefined
932
+ ? { paragraphRunPropertiesEnd: boundary.paragraphRunPropertiesEnd + offset }
933
+ : {}),
934
+ };
935
+ }
936
+
937
+ function serializeDocumentAttributes(
938
+ attributes: Record<string, string> | undefined,
939
+ content?: DocumentRootNode,
940
+ ): string {
941
+ const merged = {
942
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
943
+ "xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
944
+ ...(content && documentNeedsW14Namespace(content)
945
+ ? { "xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml" }
946
+ : {}),
947
+ ...(attributes ?? {}),
948
+ };
949
+
950
+ return Object.entries(merged)
951
+ .map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`)
952
+ .join("");
953
+ }
954
+
955
+ function documentNeedsW14Namespace(content: DocumentRootNode): boolean {
956
+ const blockQueue = [...content.children];
957
+ while (blockQueue.length > 0) {
958
+ const block = blockQueue.shift();
959
+ if (!block) {
960
+ continue;
961
+ }
962
+ if (block.type === "paragraph") {
963
+ for (const child of block.children) {
964
+ if (
965
+ (child.type === "text" || child.type === "symbol") &&
966
+ child.marks?.some((mark) => mark.type === "textFill")
967
+ ) {
968
+ return true;
969
+ }
970
+ }
971
+ continue;
972
+ }
973
+ if (block.type === "table") {
974
+ for (const row of block.rows) {
975
+ for (const cell of row.cells) {
976
+ blockQueue.push(...cell.children);
977
+ }
978
+ }
979
+ }
980
+ }
981
+ return false;
982
+ }