@beyondwork/docx-react-component 1.0.19 → 1.0.21
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/package.json +44 -25
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +130 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -3,6 +3,9 @@ import type {
|
|
|
3
3
|
FootnoteDefinition,
|
|
4
4
|
InlineNode,
|
|
5
5
|
ParagraphNode,
|
|
6
|
+
TableCellNode,
|
|
7
|
+
TableNode,
|
|
8
|
+
TableRowNode,
|
|
6
9
|
TextMark,
|
|
7
10
|
} from "../../model/canonical-document.ts";
|
|
8
11
|
|
|
@@ -71,6 +74,12 @@ function serializeNoteDefinition(
|
|
|
71
74
|
if (block.type === "paragraph") {
|
|
72
75
|
return serializeParagraph(block);
|
|
73
76
|
}
|
|
77
|
+
if (block.type === "table") {
|
|
78
|
+
return serializeTable(block);
|
|
79
|
+
}
|
|
80
|
+
if (block.type === "opaque_block" && typeof block.rawXml === "string") {
|
|
81
|
+
return block.rawXml;
|
|
82
|
+
}
|
|
74
83
|
throw new Error(`Cannot safely serialize ${block.type} content in note sub-parts.`);
|
|
75
84
|
})
|
|
76
85
|
.join("");
|
|
@@ -96,6 +105,54 @@ function serializeParagraph(paragraph: ParagraphNode): string {
|
|
|
96
105
|
return xml;
|
|
97
106
|
}
|
|
98
107
|
|
|
108
|
+
function serializeTable(table: TableNode): string {
|
|
109
|
+
let xml = "<w:tbl>";
|
|
110
|
+
if (table.propertiesXml) {
|
|
111
|
+
xml += table.propertiesXml;
|
|
112
|
+
}
|
|
113
|
+
if (table.gridColumns.length > 0) {
|
|
114
|
+
xml += "<w:tblGrid>";
|
|
115
|
+
for (const width of table.gridColumns) {
|
|
116
|
+
xml += `<w:gridCol w:w="${width}"/>`;
|
|
117
|
+
}
|
|
118
|
+
xml += "</w:tblGrid>";
|
|
119
|
+
}
|
|
120
|
+
for (const row of table.rows) {
|
|
121
|
+
xml += serializeTableRow(row);
|
|
122
|
+
}
|
|
123
|
+
xml += "</w:tbl>";
|
|
124
|
+
return xml;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function serializeTableRow(row: TableRowNode): string {
|
|
128
|
+
let xml = "<w:tr>";
|
|
129
|
+
if (row.propertiesXml) {
|
|
130
|
+
xml += wrapPropertiesXml("w:trPr", row.propertiesXml);
|
|
131
|
+
}
|
|
132
|
+
for (const cell of row.cells) {
|
|
133
|
+
xml += serializeTableCell(cell);
|
|
134
|
+
}
|
|
135
|
+
xml += "</w:tr>";
|
|
136
|
+
return xml;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function serializeTableCell(cell: TableCellNode): string {
|
|
140
|
+
let xml = "<w:tc>";
|
|
141
|
+
const propertiesXml = buildTableCellPropertiesXml(cell);
|
|
142
|
+
if (propertiesXml) {
|
|
143
|
+
xml += propertiesXml;
|
|
144
|
+
}
|
|
145
|
+
for (const child of cell.children) {
|
|
146
|
+
if (child.type === "paragraph") {
|
|
147
|
+
xml += serializeParagraph(child);
|
|
148
|
+
} else {
|
|
149
|
+
throw new Error(`Cannot safely serialize ${child.type} content in note table cells.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
xml += "</w:tc>";
|
|
153
|
+
return xml;
|
|
154
|
+
}
|
|
155
|
+
|
|
99
156
|
function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
100
157
|
const parts: string[] = [];
|
|
101
158
|
|
|
@@ -105,6 +162,26 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
105
162
|
if (paragraph.alignment) {
|
|
106
163
|
parts.push(`<w:jc w:val="${escapeAttribute(paragraph.alignment)}"/>`);
|
|
107
164
|
}
|
|
165
|
+
if (paragraph.spacing) {
|
|
166
|
+
const attrs: string[] = [];
|
|
167
|
+
if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${paragraph.spacing.before}"`);
|
|
168
|
+
if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${paragraph.spacing.after}"`);
|
|
169
|
+
if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${paragraph.spacing.line}"`);
|
|
170
|
+
if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${escapeAttribute(paragraph.spacing.lineRule)}"`);
|
|
171
|
+
if (attrs.length > 0) {
|
|
172
|
+
parts.push(`<w:spacing ${attrs.join(" ")}/>`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (paragraph.indentation) {
|
|
176
|
+
const attrs: string[] = [];
|
|
177
|
+
if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
|
|
178
|
+
if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
|
|
179
|
+
if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
|
|
180
|
+
if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
|
|
181
|
+
if (attrs.length > 0) {
|
|
182
|
+
parts.push(`<w:ind ${attrs.join(" ")}/>`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
108
185
|
|
|
109
186
|
return parts.length > 0 ? `<w:pPr>${parts.join("")}</w:pPr>` : "";
|
|
110
187
|
}
|
|
@@ -133,9 +210,62 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
133
210
|
: "EndnoteReference";
|
|
134
211
|
return `<w:r><w:rPr><w:rStyle w:val="${styleVal}"/></w:rPr>${refElement}</w:r>`;
|
|
135
212
|
}
|
|
213
|
+
case "bookmark_start":
|
|
214
|
+
return `<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}" w:name="${escapeAttribute(node.name)}"/>`;
|
|
215
|
+
case "bookmark_end":
|
|
216
|
+
return `<w:bookmarkEnd w:id="${escapeAttribute(node.bookmarkId)}"/>`;
|
|
217
|
+
case "field":
|
|
218
|
+
if (node.children && node.children.length > 0) {
|
|
219
|
+
const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
|
|
220
|
+
if (node.fieldType === "complex") {
|
|
221
|
+
return (
|
|
222
|
+
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
223
|
+
`<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
|
|
224
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
225
|
+
childrenXml +
|
|
226
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
|
|
230
|
+
}
|
|
231
|
+
if (node.fieldType === "simple") {
|
|
232
|
+
return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
|
|
233
|
+
}
|
|
234
|
+
return (
|
|
235
|
+
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
236
|
+
`<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
|
|
237
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
238
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
239
|
+
);
|
|
240
|
+
case "hyperlink":
|
|
241
|
+
return serializeHyperlinkNode(node);
|
|
242
|
+
case "field":
|
|
243
|
+
if (node.children && node.children.length > 0) {
|
|
244
|
+
const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
|
|
245
|
+
if (node.fieldType === "complex") {
|
|
246
|
+
return (
|
|
247
|
+
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
248
|
+
`<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
|
|
249
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
250
|
+
childrenXml +
|
|
251
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}">${childrenXml}</w:fldSimple>`;
|
|
255
|
+
}
|
|
256
|
+
if (node.fieldType === "simple") {
|
|
257
|
+
return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
|
|
258
|
+
}
|
|
259
|
+
return (
|
|
260
|
+
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
261
|
+
`<w:r><w:instrText xml:space="preserve">${escapeXml(node.instruction)}</w:instrText></w:r>` +
|
|
262
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>` +
|
|
263
|
+
`<w:r><w:fldChar w:fldCharType="end"/></w:r>`
|
|
264
|
+
);
|
|
265
|
+
case "hyperlink":
|
|
266
|
+
return serializeHyperlinkNode(node);
|
|
136
267
|
case "opaque_inline":
|
|
137
268
|
throw new Error(`Cannot safely serialize ${node.type} content in note sub-parts.`);
|
|
138
|
-
case "hyperlink":
|
|
139
269
|
default:
|
|
140
270
|
throw new Error(`Cannot safely serialize ${node.type} content in note sub-parts.`);
|
|
141
271
|
}
|
|
@@ -164,6 +294,23 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
|
|
|
164
294
|
case "doubleStrikethrough":
|
|
165
295
|
parts.push("<w:dstrike/>");
|
|
166
296
|
break;
|
|
297
|
+
case "fontFamily":
|
|
298
|
+
parts.push(
|
|
299
|
+
`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`,
|
|
300
|
+
);
|
|
301
|
+
break;
|
|
302
|
+
case "fontSize":
|
|
303
|
+
parts.push(`<w:sz w:val="${mark.val}"/>`);
|
|
304
|
+
break;
|
|
305
|
+
case "textColor":
|
|
306
|
+
parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
|
|
307
|
+
break;
|
|
308
|
+
case "smallCaps":
|
|
309
|
+
parts.push("<w:smallCaps/>");
|
|
310
|
+
break;
|
|
311
|
+
case "allCaps":
|
|
312
|
+
parts.push("<w:caps/>");
|
|
313
|
+
break;
|
|
167
314
|
default:
|
|
168
315
|
throw new Error(`Cannot safely serialize ${mark.type} marks in note sub-parts.`);
|
|
169
316
|
}
|
|
@@ -200,3 +347,53 @@ function escapeAttribute(value: string): string {
|
|
|
200
347
|
.replace(/>/g, ">")
|
|
201
348
|
.replace(/"/g, """);
|
|
202
349
|
}
|
|
350
|
+
|
|
351
|
+
function serializeHyperlinkNode(node: Extract<InlineNode, { type: "hyperlink" }>): string {
|
|
352
|
+
const childrenXml = node.children.map((child) => serializeInlineNode(child)).join("");
|
|
353
|
+
if (node.href.startsWith("#")) {
|
|
354
|
+
return `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">${childrenXml}</w:hyperlink>`;
|
|
355
|
+
}
|
|
356
|
+
if (!/^rId[A-Za-z0-9._-]+$/u.test(node.href)) {
|
|
357
|
+
throw new Error("Cannot safely serialize URL-backed note hyperlinks without relationship context.");
|
|
358
|
+
}
|
|
359
|
+
return `<w:hyperlink r:id="${escapeAttribute(node.href)}">${childrenXml}</w:hyperlink>`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function wrapPropertiesXml(tagName: "w:trPr" | "w:tcPr", xml: string): string {
|
|
363
|
+
const trimmed = xml.trim();
|
|
364
|
+
if (trimmed.startsWith(`<${tagName}`)) {
|
|
365
|
+
return trimmed;
|
|
366
|
+
}
|
|
367
|
+
return `<${tagName}>${trimmed}</${tagName}>`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function extractWrappedChildren(tagName: "w:tcPr", xml: string | undefined): string {
|
|
371
|
+
if (!xml) {
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
const trimmed = xml.trim();
|
|
375
|
+
const wrapped = new RegExp(`^<${tagName}\\b[^>]*>([\\s\\S]*)</${tagName}>$`, "u").exec(trimmed);
|
|
376
|
+
return wrapped?.[1] ?? trimmed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function buildTableCellPropertiesXml(cell: TableCellNode): string {
|
|
380
|
+
const innerXml = extractWrappedChildren("w:tcPr", cell.propertiesXml)
|
|
381
|
+
.replace(/<w:gridSpan\b[^>]*\/>/gu, "")
|
|
382
|
+
.replace(/<w:vMerge\b[^>]*\/>/gu, "")
|
|
383
|
+
.trim();
|
|
384
|
+
const parts: string[] = [];
|
|
385
|
+
if (cell.gridSpan && cell.gridSpan > 1) {
|
|
386
|
+
parts.push(`<w:gridSpan w:val="${cell.gridSpan}"/>`);
|
|
387
|
+
}
|
|
388
|
+
if (cell.verticalMerge) {
|
|
389
|
+
parts.push(
|
|
390
|
+
cell.verticalMerge === "restart"
|
|
391
|
+
? `<w:vMerge w:val="restart"/>`
|
|
392
|
+
: `<w:vMerge/>`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (innerXml.length > 0) {
|
|
396
|
+
parts.push(innerXml);
|
|
397
|
+
}
|
|
398
|
+
return parts.length > 0 ? `<w:tcPr>${parts.join("")}</w:tcPr>` : "";
|
|
399
|
+
}
|
|
@@ -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
|
-
|
|
117
|
-
case "
|
|
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
|
-
|
|
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, ">")
|
|
178
321
|
.replace(/"/g, """);
|
|
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
|
+
}
|
|
@@ -407,7 +407,7 @@ function serializeTableInlineNode(
|
|
|
407
407
|
case "shape":
|
|
408
408
|
case "wordart":
|
|
409
409
|
case "vml_shape":
|
|
410
|
-
return node.rawXml;
|
|
410
|
+
return wrapInlineRawXml(node.rawXml);
|
|
411
411
|
case "hyperlink": {
|
|
412
412
|
const hyperlinkOpen = node.href.startsWith("#")
|
|
413
413
|
? `<w:hyperlink w:anchor="${escapeAttribute(node.href.slice(1))}">`
|
|
@@ -426,8 +426,24 @@ function serializeTableInlineNode(
|
|
|
426
426
|
const childrenXml = node.children.map((child) => serializeTableInlineNode(child, state)).join("");
|
|
427
427
|
return `${hyperlinkOpen}${childrenXml}</w:hyperlink>`;
|
|
428
428
|
}
|
|
429
|
-
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
|
+
}
|
|
430
445
|
return `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
|
|
446
|
+
}
|
|
431
447
|
case "bookmark_start":
|
|
432
448
|
return (
|
|
433
449
|
`<w:bookmarkStart w:id="${escapeAttribute(node.bookmarkId)}"` +
|
|
@@ -484,6 +500,13 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
484
500
|
if (s.lineRule !== undefined) attrs.push(`w:lineRule="${s.lineRule}"`);
|
|
485
501
|
if (attrs.length > 0) children.push(`<w:spacing ${attrs.join(" ")}/>`);
|
|
486
502
|
}
|
|
503
|
+
if (paragraph.contextualSpacing !== undefined) {
|
|
504
|
+
children.push(
|
|
505
|
+
paragraph.contextualSpacing
|
|
506
|
+
? "<w:contextualSpacing/>"
|
|
507
|
+
: `<w:contextualSpacing w:val="0"/>`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
487
510
|
if (paragraph.indentation) {
|
|
488
511
|
const ind = paragraph.indentation;
|
|
489
512
|
const attrs: string[] = [];
|
|
@@ -806,7 +829,7 @@ function serializeInlineNode(
|
|
|
806
829
|
case "wordart":
|
|
807
830
|
case "vml_shape": {
|
|
808
831
|
// Reattach original XML unchanged for lossless round-trip.
|
|
809
|
-
const xml = node.rawXml;
|
|
832
|
+
const xml = wrapInlineRawXml(node.rawXml);
|
|
810
833
|
const boundaries = new Map<number, number>();
|
|
811
834
|
boundaries.set(cursor, xmlOffset);
|
|
812
835
|
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
@@ -856,6 +879,55 @@ function serializeInlineNode(
|
|
|
856
879
|
};
|
|
857
880
|
}
|
|
858
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
|
+
|
|
859
931
|
const xml = `<w:fldSimple w:instr="${escapeAttribute(node.instruction)}"/>`;
|
|
860
932
|
const boundaries = new Map<number, number>();
|
|
861
933
|
boundaries.set(cursor, xmlOffset);
|
|
@@ -966,6 +1038,9 @@ function serializeRunProperties(marks: TextMark[] | undefined): string {
|
|
|
966
1038
|
case "lang":
|
|
967
1039
|
markParts.push(`<w:lang w:val="${escapeAttribute(mark.val)}"/>`);
|
|
968
1040
|
break;
|
|
1041
|
+
case "highlight":
|
|
1042
|
+
markParts.push(`<w:highlight w:val="${escapeAttribute(mark.val)}"/>`);
|
|
1043
|
+
break;
|
|
969
1044
|
case "backgroundColor":
|
|
970
1045
|
markParts.push(
|
|
971
1046
|
`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttribute(mark.color)}"/>`,
|
|
@@ -1246,6 +1321,11 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
|
|
|
1246
1321
|
return `<w:sectPr>${children.join("")}</w:sectPr>`;
|
|
1247
1322
|
}
|
|
1248
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
|
+
|
|
1249
1329
|
function documentNeedsW14Namespace(content: DocumentRootNode): boolean {
|
|
1250
1330
|
const blockQueue = [...content.children];
|
|
1251
1331
|
while (blockQueue.length > 0) {
|