@beyondwork/docx-react-component 1.0.1 → 1.0.2

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 +76 -46
  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 +320 -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 +1504 -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,217 @@
1
+ import type {
2
+ FootnoteCollection,
3
+ FootnoteDefinition,
4
+ InlineNode,
5
+ ParagraphNode,
6
+ TextMark,
7
+ } from "../../model/canonical-document.ts";
8
+
9
+ export const WORD_FOOTNOTES_CONTENT_TYPE =
10
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
11
+ export const WORD_ENDNOTES_CONTENT_TYPE =
12
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml";
13
+
14
+ const W_NS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"`;
15
+ const R_NS = `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`;
16
+
17
+ /**
18
+ * Serialize the footnotes portion of a FootnoteCollection to footnotes.xml.
19
+ * Includes the required separator and continuation-separator stubs.
20
+ */
21
+ export function serializeFootnotesXml(collection: FootnoteCollection): string {
22
+ const entries = Object.values(collection.footnotes).sort(compareNoteIds);
23
+ const body = [
24
+ serializeSeparatorStub("footnote", "-1", "separator"),
25
+ serializeSeparatorStub("footnote", "0", "continuationSeparator"),
26
+ ...entries.map((entry) => serializeNoteDefinition("footnote", entry)),
27
+ ].join("");
28
+
29
+ return [
30
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
31
+ `<w:footnotes ${W_NS} ${R_NS}>${body}</w:footnotes>`,
32
+ ].join("\n");
33
+ }
34
+
35
+ /**
36
+ * Serialize the endnotes portion of a FootnoteCollection to endnotes.xml.
37
+ * Includes the required separator and continuation-separator stubs.
38
+ */
39
+ export function serializeEndnotesXml(collection: FootnoteCollection): string {
40
+ const entries = Object.values(collection.endnotes).sort(compareNoteIds);
41
+ const body = [
42
+ serializeSeparatorStub("endnote", "-1", "separator"),
43
+ serializeSeparatorStub("endnote", "0", "continuationSeparator"),
44
+ ...entries.map((entry) => serializeNoteDefinition("endnote", entry)),
45
+ ].join("");
46
+
47
+ return [
48
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
49
+ `<w:endnotes ${W_NS} ${R_NS}>${body}</w:endnotes>`,
50
+ ].join("\n");
51
+ }
52
+
53
+ // ---- Internal serialization ----
54
+
55
+ function serializeSeparatorStub(
56
+ kind: "footnote" | "endnote",
57
+ id: string,
58
+ type: "separator" | "continuationSeparator",
59
+ ): string {
60
+ const tag = kind === "footnote" ? "w:footnote" : "w:endnote";
61
+ return `<${tag} w:type="${type}" w:id="${id}"><w:p/></${tag}>`;
62
+ }
63
+
64
+ function serializeNoteDefinition(
65
+ kind: "footnote" | "endnote",
66
+ definition: FootnoteDefinition,
67
+ ): string {
68
+ const tag = kind === "footnote" ? "w:footnote" : "w:endnote";
69
+ const blocks = definition.blocks
70
+ .map((block) => {
71
+ if (block.type === "paragraph") {
72
+ return serializeParagraph(block);
73
+ }
74
+ // opaque_block: emit empty paragraph
75
+ return `<w:p/>`;
76
+ })
77
+ .join("");
78
+
79
+ const body = blocks || `<w:p><w:r><w:t></w:t></w:r></w:p>`;
80
+ return `<${tag} w:id="${escapeAttribute(definition.noteId)}">${body}</${tag}>`;
81
+ }
82
+
83
+ function serializeParagraph(paragraph: ParagraphNode): string {
84
+ let xml = "<w:p>";
85
+
86
+ const propertiesXml = buildParagraphPropertiesXml(paragraph);
87
+ if (propertiesXml) {
88
+ xml += propertiesXml;
89
+ }
90
+
91
+ const childrenXml = paragraph.children
92
+ .map((child) => serializeInlineNode(child))
93
+ .join("");
94
+ xml += childrenXml || "<w:r><w:t></w:t></w:r>";
95
+ xml += "</w:p>";
96
+
97
+ return xml;
98
+ }
99
+
100
+ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
101
+ const parts: string[] = [];
102
+
103
+ if (paragraph.styleId) {
104
+ parts.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
105
+ }
106
+ if (paragraph.alignment) {
107
+ parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
108
+ }
109
+
110
+ return parts.length > 0 ? `<w:pPr>${parts.join("")}</w:pPr>` : "";
111
+ }
112
+
113
+ function serializeInlineNode(node: InlineNode): string {
114
+ switch (node.type) {
115
+ case "text": {
116
+ const properties = buildRunPropertiesXml(node.marks);
117
+ const preserve = requiresPreservedSpace(node.text)
118
+ ? ` xml:space="preserve"`
119
+ : "";
120
+ return `<w:r>${properties}<w:t${preserve}>${escapeXml(node.text)}</w:t></w:r>`;
121
+ }
122
+ case "tab":
123
+ return "<w:r><w:tab/></w:r>";
124
+ case "hard_break":
125
+ return "<w:r><w:br/></w:r>";
126
+ case "footnote_ref": {
127
+ const refElement =
128
+ node.noteKind === "footnote"
129
+ ? `<w:footnoteReference w:id="${escapeAttribute(node.noteId)}"/>`
130
+ : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
131
+ const styleVal =
132
+ node.noteKind === "footnote"
133
+ ? "FootnoteReference"
134
+ : "EndnoteReference";
135
+ return `<w:r><w:rPr><w:rStyle w:val="${styleVal}"/></w:rPr>${refElement}</w:r>`;
136
+ }
137
+ case "opaque_inline":
138
+ return "";
139
+ case "hyperlink": {
140
+ return node.children
141
+ .map((child) => {
142
+ if (child.type === "text") {
143
+ const preserve = requiresPreservedSpace(child.text)
144
+ ? ` xml:space="preserve"`
145
+ : "";
146
+ return `<w:r><w:t${preserve}>${escapeXml(child.text)}</w:t></w:r>`;
147
+ }
148
+ if (child.type === "tab") return "<w:r><w:tab/></w:r>";
149
+ if (child.type === "hard_break") return "<w:r><w:br/></w:r>";
150
+ return "";
151
+ })
152
+ .join("");
153
+ }
154
+ default:
155
+ return "";
156
+ }
157
+ }
158
+
159
+ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
160
+ if (!marks || marks.length === 0) {
161
+ return "";
162
+ }
163
+
164
+ const parts: string[] = [];
165
+ for (const mark of marks) {
166
+ switch (mark.type) {
167
+ case "bold":
168
+ parts.push("<w:b/>");
169
+ break;
170
+ case "italic":
171
+ parts.push("<w:i/>");
172
+ break;
173
+ case "underline":
174
+ parts.push("<w:u w:val=\"single\"/>");
175
+ break;
176
+ case "strikethrough":
177
+ parts.push("<w:strike/>");
178
+ break;
179
+ case "doubleStrikethrough":
180
+ parts.push("<w:dstrike/>");
181
+ break;
182
+ default:
183
+ break;
184
+ }
185
+ }
186
+
187
+ return parts.length > 0 ? `<w:rPr>${parts.join("")}</w:rPr>` : "";
188
+ }
189
+
190
+ function compareNoteIds(
191
+ left: FootnoteDefinition,
192
+ right: FootnoteDefinition,
193
+ ): number {
194
+ return Number.parseInt(left.noteId, 10) - Number.parseInt(right.noteId, 10);
195
+ }
196
+
197
+ function requiresPreservedSpace(text: string): boolean {
198
+ return (
199
+ text.length > 0 &&
200
+ (text[0] === " " || text[text.length - 1] === " " || text.includes(" "))
201
+ );
202
+ }
203
+
204
+ function escapeXml(text: string): string {
205
+ return text
206
+ .replace(/&/g, "&amp;")
207
+ .replace(/</g, "&lt;")
208
+ .replace(/>/g, "&gt;");
209
+ }
210
+
211
+ function escapeAttribute(value: string): string {
212
+ return value
213
+ .replace(/&/g, "&amp;")
214
+ .replace(/</g, "&lt;")
215
+ .replace(/>/g, "&gt;")
216
+ .replace(/"/g, "&quot;");
217
+ }
@@ -0,0 +1,200 @@
1
+ import type {
2
+ FooterDocument,
3
+ HeaderDocument,
4
+ InlineNode,
5
+ ParagraphNode,
6
+ TextMark,
7
+ } from "../../model/canonical-document.ts";
8
+
9
+ export const WORD_HEADER_CONTENT_TYPE =
10
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
11
+ export const WORD_FOOTER_CONTENT_TYPE =
12
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml";
13
+
14
+ const W_NS = `xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"`;
15
+ const R_NS = `xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`;
16
+
17
+ /**
18
+ * Serialize a HeaderDocument into a headerN.xml string.
19
+ */
20
+ export function serializeHeaderXml(header: HeaderDocument): string {
21
+ const body = serializeBlocks(header.blocks);
22
+ return [
23
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
24
+ `<w:hdr ${W_NS} ${R_NS}>${body}</w:hdr>`,
25
+ ].join("\n");
26
+ }
27
+
28
+ /**
29
+ * Serialize a FooterDocument into a footerN.xml string.
30
+ */
31
+ export function serializeFooterXml(footer: FooterDocument): string {
32
+ const body = serializeBlocks(footer.blocks);
33
+ return [
34
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
35
+ `<w:ftr ${W_NS} ${R_NS}>${body}</w:ftr>`,
36
+ ].join("\n");
37
+ }
38
+
39
+ // ---- Internal serialization ----
40
+
41
+ function serializeBlocks(
42
+ blocks: HeaderDocument["blocks"] | FooterDocument["blocks"],
43
+ ): string {
44
+ if (blocks.length === 0) {
45
+ // A valid header/footer must have at least one paragraph
46
+ return `<w:p><w:r><w:t></w:t></w:r></w:p>`;
47
+ }
48
+
49
+ return blocks
50
+ .map((block) => {
51
+ if (block.type === "paragraph") {
52
+ return serializeParagraph(block);
53
+ }
54
+ // opaque_block: emit empty paragraph to preserve structure
55
+ return `<w:p/>`;
56
+ })
57
+ .join("");
58
+ }
59
+
60
+ function serializeParagraph(paragraph: ParagraphNode): string {
61
+ let xml = "<w:p>";
62
+
63
+ const propertiesXml = buildParagraphPropertiesXml(paragraph);
64
+ if (propertiesXml) {
65
+ xml += propertiesXml;
66
+ }
67
+
68
+ const childrenXml = paragraph.children
69
+ .map((child) => serializeInlineNode(child))
70
+ .join("");
71
+ xml += childrenXml || "<w:r><w:t></w:t></w:r>";
72
+ xml += "</w:p>";
73
+
74
+ return xml;
75
+ }
76
+
77
+ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
78
+ const parts: string[] = [];
79
+
80
+ if (paragraph.styleId) {
81
+ parts.push(`<w:pStyle w:val="${escapeAttribute(paragraph.styleId)}"/>`);
82
+ }
83
+ if (paragraph.alignment) {
84
+ parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
85
+ }
86
+
87
+ return parts.length > 0 ? `<w:pPr>${parts.join("")}</w:pPr>` : "";
88
+ }
89
+
90
+ function serializeInlineNode(node: InlineNode): string {
91
+ switch (node.type) {
92
+ case "text": {
93
+ const properties = buildRunPropertiesXml(node.marks);
94
+ const preserve = requiresPreservedSpace(node.text)
95
+ ? ` xml:space="preserve"`
96
+ : "";
97
+ return `<w:r>${properties}<w:t${preserve}>${escapeXml(node.text)}</w:t></w:r>`;
98
+ }
99
+ case "tab":
100
+ return "<w:r><w:tab/></w:r>";
101
+ case "hard_break":
102
+ return "<w:r><w:br/></w:r>";
103
+ case "footnote_ref": {
104
+ const refElement =
105
+ node.noteKind === "footnote"
106
+ ? `<w:footnoteReference w:id="${escapeAttribute(node.noteId)}"/>`
107
+ : `<w:endnoteReference w:id="${escapeAttribute(node.noteId)}"/>`;
108
+ return `<w:r><w:rPr><w:rStyle w:val="${node.noteKind === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr>${refElement}</w:r>`;
109
+ }
110
+ case "opaque_inline":
111
+ // Cannot reproduce opaque inline content without original XML; emit empty
112
+ return "";
113
+ case "hyperlink": {
114
+ const childrenXml = node.children
115
+ .map((child) => {
116
+ switch (child.type) {
117
+ case "text": {
118
+ const properties = buildRunPropertiesXml(undefined);
119
+ const preserve = requiresPreservedSpace(child.text)
120
+ ? ` xml:space="preserve"`
121
+ : "";
122
+ return `<w:r>${properties}<w:t${preserve}>${escapeXml(child.text)}</w:t></w:r>`;
123
+ }
124
+ case "tab":
125
+ return "<w:r><w:tab/></w:r>";
126
+ case "hard_break":
127
+ return "<w:r><w:br/></w:r>";
128
+ default:
129
+ return "";
130
+ }
131
+ })
132
+ .join("");
133
+ // Hyperlinks in headers/footers typically use bookmark anchors or external URLs.
134
+ // Emit as a plain run since we don't retain the relationship ID here.
135
+ return childrenXml;
136
+ }
137
+ case "image":
138
+ case "field":
139
+ case "bookmark_start":
140
+ case "bookmark_end":
141
+ case "column_break":
142
+ case "symbol":
143
+ // These node types are not parsed from headers/footers by parse-headers-footers.ts
144
+ return "";
145
+ }
146
+ }
147
+
148
+ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
149
+ if (!marks || marks.length === 0) {
150
+ return "";
151
+ }
152
+
153
+ const parts: string[] = [];
154
+ for (const mark of marks) {
155
+ switch (mark.type) {
156
+ case "bold":
157
+ parts.push("<w:b/>");
158
+ break;
159
+ case "italic":
160
+ parts.push("<w:i/>");
161
+ break;
162
+ case "underline":
163
+ parts.push("<w:u w:val=\"single\"/>");
164
+ break;
165
+ case "strikethrough":
166
+ parts.push("<w:strike/>");
167
+ break;
168
+ case "doubleStrikethrough":
169
+ parts.push("<w:dstrike/>");
170
+ break;
171
+ default:
172
+ // Other mark types not parsed from headers/footers
173
+ break;
174
+ }
175
+ }
176
+
177
+ return parts.length > 0 ? `<w:rPr>${parts.join("")}</w:rPr>` : "";
178
+ }
179
+
180
+ function requiresPreservedSpace(text: string): boolean {
181
+ return (
182
+ text.length > 0 &&
183
+ (text[0] === " " || text[text.length - 1] === " " || text.includes(" "))
184
+ );
185
+ }
186
+
187
+ function escapeXml(text: string): string {
188
+ return text
189
+ .replace(/&/g, "&amp;")
190
+ .replace(/</g, "&lt;")
191
+ .replace(/>/g, "&gt;");
192
+ }
193
+
194
+ function escapeAttribute(value: string): string {
195
+ return value
196
+ .replace(/&/g, "&amp;")
197
+ .replace(/</g, "&lt;")
198
+ .replace(/>/g, "&gt;")
199
+ .replace(/"/g, "&quot;");
200
+ }