@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.
- package/README.md +44 -104
- package/package.json +50 -30
- package/src/README.md +85 -0
- package/src/api/README.md +22 -0
- package/src/api/public-types.ts +525 -0
- package/src/compare/diff-engine.ts +530 -0
- package/src/compare/export-redlines.ts +162 -0
- package/src/compare/snapshot.ts +37 -0
- package/src/component-inventory.md +99 -0
- package/src/core/README.md +10 -0
- package/src/core/commands/README.md +3 -0
- package/src/core/commands/formatting-commands.ts +161 -0
- package/src/core/commands/image-commands.ts +144 -0
- package/src/core/commands/index.ts +1013 -0
- package/src/core/commands/list-commands.ts +370 -0
- package/src/core/commands/review-commands.ts +108 -0
- package/src/core/commands/text-commands.ts +119 -0
- package/src/core/schema/README.md +3 -0
- package/src/core/schema/text-schema.ts +512 -0
- package/src/core/selection/README.md +3 -0
- package/src/core/selection/mapping.ts +238 -0
- package/src/core/selection/review-anchors.ts +94 -0
- package/src/core/state/README.md +3 -0
- package/src/core/state/editor-state.ts +580 -0
- package/src/core/state/text-transaction.ts +276 -0
- package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
- package/src/formats/xlsx/io/parse-sheet.ts +289 -0
- package/src/formats/xlsx/io/parse-styles.ts +57 -0
- package/src/formats/xlsx/io/parse-workbook.ts +75 -0
- package/src/formats/xlsx/io/xlsx-session.ts +306 -0
- package/src/formats/xlsx/model/cell.ts +189 -0
- package/src/formats/xlsx/model/sheet.ts +244 -0
- package/src/formats/xlsx/model/styles.ts +118 -0
- package/src/formats/xlsx/model/workbook.ts +449 -0
- package/src/index.ts +45 -0
- package/src/io/README.md +10 -0
- package/src/io/docx-session.ts +1763 -0
- package/src/io/export/README.md +3 -0
- package/src/io/export/export-session.ts +165 -0
- package/src/io/export/minimal-docx.ts +115 -0
- package/src/io/export/reattach-preserved-parts.ts +54 -0
- package/src/io/export/serialize-comments.ts +876 -0
- package/src/io/export/serialize-footnotes.ts +217 -0
- package/src/io/export/serialize-headers-footers.ts +200 -0
- package/src/io/export/serialize-main-document.ts +982 -0
- package/src/io/export/serialize-numbering.ts +97 -0
- package/src/io/export/serialize-revisions.ts +389 -0
- package/src/io/export/serialize-runtime-revisions.ts +265 -0
- package/src/io/export/serialize-tables.ts +147 -0
- package/src/io/export/split-review-boundaries.ts +194 -0
- package/src/io/normalize/README.md +3 -0
- package/src/io/normalize/normalize-text.ts +437 -0
- package/src/io/ooxml/README.md +3 -0
- package/src/io/ooxml/parse-comments.ts +779 -0
- package/src/io/ooxml/parse-complex-content.ts +287 -0
- package/src/io/ooxml/parse-fields.ts +438 -0
- package/src/io/ooxml/parse-footnotes.ts +403 -0
- package/src/io/ooxml/parse-headers-footers.ts +483 -0
- package/src/io/ooxml/parse-inline-media.ts +431 -0
- package/src/io/ooxml/parse-main-document.ts +1846 -0
- package/src/io/ooxml/parse-numbering.ts +425 -0
- package/src/io/ooxml/parse-revisions.ts +658 -0
- package/src/io/ooxml/parse-shapes.ts +271 -0
- package/src/io/ooxml/parse-tables.ts +568 -0
- package/src/io/ooxml/parse-theme.ts +314 -0
- package/src/io/ooxml/part-manifest.ts +136 -0
- package/src/io/ooxml/revision-boundaries.ts +351 -0
- package/src/io/opc/README.md +3 -0
- package/src/io/opc/corrupt-package.ts +166 -0
- package/src/io/opc/docx-package.ts +74 -0
- package/src/io/opc/package-reader.ts +325 -0
- package/src/io/opc/package-writer.ts +273 -0
- package/src/legal/bookmarks.ts +196 -0
- package/src/legal/cross-references.ts +356 -0
- package/src/legal/defined-terms.ts +203 -0
- package/src/model/README.md +3 -0
- package/src/model/canonical-document.ts +1911 -0
- package/src/model/cds-1.0.0.ts +196 -0
- package/src/model/snapshot.ts +393 -0
- package/src/preservation/README.md +3 -0
- package/src/preservation/markup-compatibility.ts +48 -0
- package/src/preservation/opaque-fragment-store.ts +89 -0
- package/src/preservation/opaque-region.ts +233 -0
- package/src/preservation/package-preservation.ts +120 -0
- package/src/preservation/preserved-part-manifest.ts +56 -0
- package/src/preservation/relationship-retention.ts +57 -0
- package/src/preservation/store.ts +185 -0
- package/src/review/README.md +16 -0
- package/src/review/store/README.md +3 -0
- package/src/review/store/comment-anchors.ts +70 -0
- package/src/review/store/comment-remapping.ts +154 -0
- package/src/review/store/comment-store.ts +331 -0
- package/src/review/store/comment-thread.ts +109 -0
- package/src/review/store/revision-actions.ts +394 -0
- package/src/review/store/revision-store.ts +303 -0
- package/src/review/store/revision-types.ts +168 -0
- package/src/review/store/runtime-comment-store.ts +43 -0
- package/src/runtime/README.md +3 -0
- package/src/runtime/ai-action-policy.ts +764 -0
- package/src/runtime/document-runtime.ts +967 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
- package/src/runtime/review-runtime.ts +44 -0
- package/src/runtime/revision-runtime.ts +107 -0
- package/src/runtime/session-capabilities.ts +138 -0
- package/src/runtime/surface-projection.ts +570 -0
- package/src/runtime/table-commands.ts +87 -0
- package/src/runtime/table-schema.ts +140 -0
- package/src/runtime/virtualized-rendering.ts +258 -0
- package/src/ui/README.md +30 -0
- package/src/ui/WordReviewEditor.tsx +1506 -0
- package/src/ui/comments/README.md +3 -0
- package/src/ui/compatibility/README.md +3 -0
- package/src/ui/editor-surface/README.md +3 -0
- package/src/ui/headless/comment-decoration-model.ts +124 -0
- package/src/ui/headless/revision-decoration-model.ts +128 -0
- package/src/ui/headless/selection-helpers.ts +34 -0
- package/src/ui/headless/use-editor-keyboard.ts +98 -0
- package/src/ui/review/README.md +3 -0
- package/src/ui/shared/revision-filters.ts +31 -0
- package/src/ui/status/README.md +3 -0
- package/src/ui/theme/README.md +3 -0
- package/src/ui/toolbar/README.md +3 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
- package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
- package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
- package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
- package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
- package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
- package/src/ui-tailwind/index.ts +61 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
- package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
- package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
- package/src/ui-tailwind/theme/editor-theme.css +190 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
- package/src/validation/README.md +3 -0
- package/src/validation/compatibility-engine.ts +317 -0
- package/src/validation/compatibility-report.ts +160 -0
- package/src/validation/diagnostics.ts +203 -0
- package/src/validation/import-diagnostics.ts +128 -0
- package/src/validation/low-priority-word-surfaces.ts +373 -0
- package/dist/chunk-32W6IVQE.js +0 -7725
- package/dist/chunk-32W6IVQE.js.map +0 -1
- package/dist/index.cjs +0 -23722
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -7
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -16011
- package/dist/index.js.map +0 -1
- package/dist/public-types-DqCURAz8.d.cts +0 -1152
- package/dist/public-types-DqCURAz8.d.ts +0 -1152
- package/dist/tailwind.cjs +0 -8295
- package/dist/tailwind.cjs.map +0 -1
- package/dist/tailwind.d.cts +0 -323
- package/dist/tailwind.d.ts +0 -323
- package/dist/tailwind.js +0 -553
- 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, "&")
|
|
207
|
+
.replace(/</g, "<")
|
|
208
|
+
.replace(/>/g, ">");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function escapeAttribute(value: string): string {
|
|
212
|
+
return value
|
|
213
|
+
.replace(/&/g, "&")
|
|
214
|
+
.replace(/</g, "<")
|
|
215
|
+
.replace(/>/g, ">")
|
|
216
|
+
.replace(/"/g, """);
|
|
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, "&")
|
|
190
|
+
.replace(/</g, "<")
|
|
191
|
+
.replace(/>/g, ">");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function escapeAttribute(value: string): string {
|
|
195
|
+
return value
|
|
196
|
+
.replace(/&/g, "&")
|
|
197
|
+
.replace(/</g, "<")
|
|
198
|
+
.replace(/>/g, ">")
|
|
199
|
+
.replace(/"/g, """);
|
|
200
|
+
}
|