@beyondwork/docx-react-component 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- 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 +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -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 +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -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-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
|
@@ -577,56 +577,126 @@ function parseOoxmlNumericId(value: string): number | undefined {
|
|
|
577
577
|
return Number.isFinite(numericId) ? numericId : undefined;
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
|
|
580
|
+
export function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
|
|
581
581
|
const root = parseXml(documentXml);
|
|
582
582
|
const documentElement = findChildElement(root, "document");
|
|
583
583
|
const bodyElement = findChildElement(documentElement, "body");
|
|
584
584
|
const paragraphs: ParagraphBoundaryMap[] = [];
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
walkBlockNodesForParagraphBoundaries(
|
|
586
|
+
bodyElement.children,
|
|
587
|
+
documentXml,
|
|
588
|
+
paragraphs,
|
|
589
|
+
0,
|
|
590
|
+
-1,
|
|
591
|
+
true,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
return paragraphs;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function walkBlockNodesForParagraphBoundaries(
|
|
598
|
+
nodes: readonly XmlNode[],
|
|
599
|
+
documentXml: string,
|
|
600
|
+
paragraphs: ParagraphBoundaryMap[],
|
|
601
|
+
globalCursor: number,
|
|
602
|
+
paragraphIndex: number,
|
|
603
|
+
useSurfaceParagraphSeparators: boolean,
|
|
604
|
+
): {
|
|
605
|
+
globalCursor: number;
|
|
606
|
+
paragraphIndex: number;
|
|
607
|
+
} {
|
|
608
|
+
let nextCursor = globalCursor;
|
|
609
|
+
let nextParagraphIndex = paragraphIndex;
|
|
610
|
+
let elementIndex = -1;
|
|
588
611
|
|
|
589
|
-
for (const
|
|
590
|
-
if (
|
|
612
|
+
for (const node of nodes) {
|
|
613
|
+
if (node.type !== "element") {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
elementIndex += 1;
|
|
617
|
+
|
|
618
|
+
const name = localName(node.name);
|
|
619
|
+
if (name === "p") {
|
|
620
|
+
if (useSurfaceParagraphSeparators && elementIndex > 0) {
|
|
621
|
+
nextCursor += 1;
|
|
622
|
+
}
|
|
623
|
+
nextParagraphIndex += 1;
|
|
624
|
+
const boundaries = new Map<number, number>();
|
|
625
|
+
boundaries.set(nextCursor, node.start + openingTagLength(documentXml, node.start));
|
|
626
|
+
|
|
627
|
+
walkParagraphForBoundaries(
|
|
628
|
+
node,
|
|
629
|
+
documentXml,
|
|
630
|
+
boundaries,
|
|
631
|
+
() => nextCursor,
|
|
632
|
+
(next) => {
|
|
633
|
+
nextCursor = next;
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (!boundaries.has(nextCursor)) {
|
|
638
|
+
boundaries.set(nextCursor, node.end - 4);
|
|
639
|
+
}
|
|
640
|
+
paragraphs.push({
|
|
641
|
+
paragraphIndex: nextParagraphIndex,
|
|
642
|
+
start: Math.min(...boundaries.keys()),
|
|
643
|
+
end: Math.max(...boundaries.keys()),
|
|
644
|
+
boundaries,
|
|
645
|
+
});
|
|
591
646
|
continue;
|
|
592
647
|
}
|
|
593
648
|
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
649
|
+
if (name === "tbl") {
|
|
650
|
+
for (const child of node.children) {
|
|
651
|
+
if (child.type !== "element" || localName(child.name) !== "tr") {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
for (const rowChild of child.children) {
|
|
655
|
+
if (rowChild.type !== "element" || localName(rowChild.name) !== "tc") {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const result = walkBlockNodesForParagraphBoundaries(
|
|
659
|
+
rowChild.children,
|
|
660
|
+
documentXml,
|
|
661
|
+
paragraphs,
|
|
662
|
+
nextCursor,
|
|
663
|
+
nextParagraphIndex,
|
|
664
|
+
false,
|
|
665
|
+
);
|
|
666
|
+
nextCursor = result.globalCursor;
|
|
667
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
597
670
|
continue;
|
|
598
671
|
}
|
|
599
672
|
|
|
600
|
-
if (
|
|
601
|
-
|
|
673
|
+
if (name === "sdt") {
|
|
674
|
+
const sdtContent = findChildElement(node, "sdtContent");
|
|
675
|
+
const result = walkBlockNodesForParagraphBoundaries(
|
|
676
|
+
sdtContent.children,
|
|
677
|
+
documentXml,
|
|
678
|
+
paragraphs,
|
|
679
|
+
nextCursor,
|
|
680
|
+
nextParagraphIndex,
|
|
681
|
+
false,
|
|
682
|
+
);
|
|
683
|
+
nextCursor = result.globalCursor;
|
|
684
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
685
|
+
continue;
|
|
602
686
|
}
|
|
603
|
-
paragraphIndex += 1;
|
|
604
|
-
const boundaries = new Map<number, number>();
|
|
605
|
-
boundaries.set(globalCursor, child.start + openingTagLength(documentXml, child.start));
|
|
606
|
-
|
|
607
|
-
walkParagraphForBoundaries(
|
|
608
|
-
child,
|
|
609
|
-
documentXml,
|
|
610
|
-
boundaries,
|
|
611
|
-
() => globalCursor,
|
|
612
|
-
(next) => {
|
|
613
|
-
globalCursor = next;
|
|
614
|
-
},
|
|
615
|
-
);
|
|
616
687
|
|
|
617
|
-
if (
|
|
618
|
-
|
|
688
|
+
if (name === "customXml") {
|
|
689
|
+
nextCursor += 1;
|
|
690
|
+
continue;
|
|
619
691
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
start: Math.min(...boundaries.keys()),
|
|
623
|
-
end: Math.max(...boundaries.keys()),
|
|
624
|
-
boundaries,
|
|
625
|
-
});
|
|
626
|
-
previousWasParagraph = true;
|
|
692
|
+
|
|
693
|
+
nextCursor += 0;
|
|
627
694
|
}
|
|
628
695
|
|
|
629
|
-
return
|
|
696
|
+
return {
|
|
697
|
+
globalCursor: nextCursor,
|
|
698
|
+
paragraphIndex: nextParagraphIndex,
|
|
699
|
+
};
|
|
630
700
|
}
|
|
631
701
|
|
|
632
702
|
function walkParagraphForBoundaries(
|
|
@@ -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
|
+
}
|