@beyondwork/docx-react-component 1.0.60 → 1.0.61
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 +33 -44
- package/src/api/public-types.ts +41 -0
- package/src/io/docx-session.ts +167 -8
- package/src/io/export/serialize-footnotes.ts +36 -5
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +25 -18
- package/src/io/export/serialize-paragraph-formatting.ts +6 -0
- package/src/io/export/serialize-settings.ts +130 -3
- package/src/io/normalize/normalize-text.ts +8 -4
- package/src/io/ooxml/parse-footnotes.ts +11 -0
- package/src/io/ooxml/parse-headers-footers.ts +117 -42
- package/src/io/ooxml/parse-main-document.ts +20 -8
- package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
- package/src/io/ooxml/parse-settings.ts +91 -1
- package/src/model/canonical-document.ts +36 -2
- package/src/runtime/document-runtime.ts +424 -0
- package/src/runtime/footnote-resolver.ts +32 -8
- package/src/runtime/layout/layout-engine-version.ts +7 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
- package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
- package/src/runtime/layout/paginated-layout-engine.ts +41 -8
- package/src/runtime/layout/resolved-formatting-document.ts +11 -9
- package/src/runtime/layout/resolved-formatting-state.ts +4 -0
- package/src/runtime/numbering-prefix.ts +26 -2
- package/src/runtime/surface-projection.ts +75 -14
- package/src/runtime/table-schema.ts +26 -0
- package/src/ui/WordReviewEditor.tsx +25 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.61",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
-
"packageManager": "pnpm@10.30.3",
|
|
7
6
|
"type": "module",
|
|
8
7
|
"sideEffects": [
|
|
9
8
|
"**/*.css"
|
|
@@ -93,38 +92,6 @@
|
|
|
93
92
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
94
93
|
},
|
|
95
94
|
"types": "./src/index.ts",
|
|
96
|
-
"scripts": {
|
|
97
|
-
"build": "tsup",
|
|
98
|
-
"test": "bash scripts/run-workspace-tests.sh",
|
|
99
|
-
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
100
|
-
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
101
|
-
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
102
|
-
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
103
|
-
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
104
|
-
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
105
|
-
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
106
|
-
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
107
|
-
"test:visual:redline": "VISUAL_SMOKE_PROFILE=redline-cycle pnpm exec playwright test --project=chromium",
|
|
108
|
-
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
109
|
-
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
110
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
111
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
112
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
113
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
114
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
115
|
-
"generate:token-reference": "node scripts/generate-token-reference.mjs",
|
|
116
|
-
"check:token-reference": "node scripts/generate-token-reference.mjs --check",
|
|
117
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
118
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
119
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
120
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
121
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
122
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
123
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
124
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
125
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
126
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
127
|
-
},
|
|
128
95
|
"keywords": [
|
|
129
96
|
"docx",
|
|
130
97
|
"word",
|
|
@@ -208,14 +175,36 @@
|
|
|
208
175
|
"y-protocols": "^1.0.7",
|
|
209
176
|
"yjs": "^13.6.30"
|
|
210
177
|
},
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
178
|
+
"scripts": {
|
|
179
|
+
"build": "tsup",
|
|
180
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
181
|
+
"test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
|
|
182
|
+
"test:repo:all": "node scripts/run-repo-tests.mjs all",
|
|
183
|
+
"test:repo:optional": "node scripts/run-repo-tests.mjs optional",
|
|
184
|
+
"test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
|
|
185
|
+
"test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
|
|
186
|
+
"test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
|
|
187
|
+
"test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
|
|
188
|
+
"test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
|
|
189
|
+
"test:visual:redline": "VISUAL_SMOKE_PROFILE=redline-cycle pnpm exec playwright test --project=chromium",
|
|
190
|
+
"visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
|
|
191
|
+
"mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
|
|
192
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
193
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
194
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
195
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
196
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
197
|
+
"generate:token-reference": "node scripts/generate-token-reference.mjs",
|
|
198
|
+
"check:token-reference": "node scripts/generate-token-reference.mjs --check",
|
|
199
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
200
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
201
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
202
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
203
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
204
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
205
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
206
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
207
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
208
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
220
209
|
}
|
|
221
|
-
}
|
|
210
|
+
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -524,6 +524,26 @@ export interface SetSelectionOptions {
|
|
|
524
524
|
silent?: boolean;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
+
export interface ClearHighlightOptions {
|
|
528
|
+
/**
|
|
529
|
+
* Range to clear. Defaults to the current document selection when omitted.
|
|
530
|
+
* Only `{ kind: "range" }` anchors produce a clear; other anchor kinds
|
|
531
|
+
* (`"node"`, `"detached"`) and collapsed selections are no-ops unless
|
|
532
|
+
* `expand` widens them into a real range.
|
|
533
|
+
*/
|
|
534
|
+
range?: EditorAnchorProjection;
|
|
535
|
+
/**
|
|
536
|
+
* When `true`, the resolved range is grown outward to cover the full
|
|
537
|
+
* contiguous highlighted span that touches it before clearing. A text
|
|
538
|
+
* segment counts as highlighted when `markAttrs.backgroundColor` is a
|
|
539
|
+
* non-empty string. Expansion scans each paragraph that overlaps the
|
|
540
|
+
* resolved range independently and stops at paragraph and table-cell
|
|
541
|
+
* boundaries — it will never leap into a neighboring paragraph or a
|
|
542
|
+
* different table cell. Defaults to `false`.
|
|
543
|
+
*/
|
|
544
|
+
expandToFullHighlight?: boolean;
|
|
545
|
+
}
|
|
546
|
+
|
|
527
547
|
export interface SearchResultSnapshot {
|
|
528
548
|
resultId: string;
|
|
529
549
|
anchor: EditorAnchorProjection;
|
|
@@ -1043,6 +1063,14 @@ export interface SurfaceTableCellSnapshot {
|
|
|
1043
1063
|
borderRight?: string | null;
|
|
1044
1064
|
borderBottom?: string | null;
|
|
1045
1065
|
borderLeft?: string | null;
|
|
1066
|
+
/**
|
|
1067
|
+
* Effective cell padding in twips after applying the cell's direct `tcMar`
|
|
1068
|
+
* and then falling back per-side to table-level `tblCellMar`.
|
|
1069
|
+
*/
|
|
1070
|
+
paddingTop?: number | null;
|
|
1071
|
+
paddingRight?: number | null;
|
|
1072
|
+
paddingBottom?: number | null;
|
|
1073
|
+
paddingLeft?: number | null;
|
|
1046
1074
|
/**
|
|
1047
1075
|
* R3.a Phase 2: per-cell text-flow direction copied from
|
|
1048
1076
|
* `TableCellNode.textDirection`. The node-view maps these to CSS
|
|
@@ -4058,6 +4086,19 @@ export interface WordReviewEditorRef {
|
|
|
4058
4086
|
setFontSize(size: number | null): void;
|
|
4059
4087
|
setTextColor(color: string | null): void;
|
|
4060
4088
|
setHighlightColor(color: string | null): void;
|
|
4089
|
+
/**
|
|
4090
|
+
* Clears the highlight (background color) on a range in the currently
|
|
4091
|
+
* active story. The caller's selection is NOT moved — the cursor stays
|
|
4092
|
+
* where the user left it.
|
|
4093
|
+
*
|
|
4094
|
+
* When `options.range` is omitted, the current document selection is used.
|
|
4095
|
+
*
|
|
4096
|
+
* Tracked-changes / suggesting mode is honored: in suggesting mode the
|
|
4097
|
+
* clear is recorded as a property-change suggestion when the resolved
|
|
4098
|
+
* range is one bounded text segment, and is blocked (with a
|
|
4099
|
+
* `command_blocked` event) when the range spans multiple segments.
|
|
4100
|
+
*/
|
|
4101
|
+
clearHighlight(options?: ClearHighlightOptions): void;
|
|
4061
4102
|
setAlignment(alignment: FormattingAlignment): void;
|
|
4062
4103
|
setParagraphStyle(styleId: string | null): void;
|
|
4063
4104
|
setTableStyle(styleId: string | null): void;
|
package/src/io/docx-session.ts
CHANGED
|
@@ -130,6 +130,7 @@ import type {
|
|
|
130
130
|
FootnoteCollection,
|
|
131
131
|
HeaderDocument,
|
|
132
132
|
FooterDocument,
|
|
133
|
+
InlineNode,
|
|
133
134
|
MediaCatalog,
|
|
134
135
|
NumberingCatalog,
|
|
135
136
|
OpaqueFragmentRecord,
|
|
@@ -581,15 +582,27 @@ export function loadDocxEditorSession(
|
|
|
581
582
|
}
|
|
582
583
|
|
|
583
584
|
const partPath = resolveRelationshipTarget(mainDocumentPath, relationship);
|
|
584
|
-
const
|
|
585
|
+
const part = sourcePackage.parts.get(partPath);
|
|
586
|
+
const partBytes = part?.bytes;
|
|
585
587
|
if (!partBytes) {
|
|
586
588
|
continue;
|
|
587
589
|
}
|
|
588
590
|
|
|
589
591
|
const xml = decodeUtf8(partBytes);
|
|
592
|
+
const subPartRelationships = part?.relationships ?? [];
|
|
593
|
+
const subPartChartPartLookup = createChartPartLookup(
|
|
594
|
+
sourcePackage,
|
|
595
|
+
partPath,
|
|
596
|
+
subPartRelationships,
|
|
597
|
+
);
|
|
590
598
|
if (ref.kind === "header") {
|
|
591
599
|
const parsedHeaderRevisions = parseRevisionsFromStoryXml(xml);
|
|
592
|
-
const parsed = parseHeaderXml(xml
|
|
600
|
+
const parsed = parseHeaderXml(xml, {
|
|
601
|
+
relationships: subPartRelationships,
|
|
602
|
+
mediaParts,
|
|
603
|
+
sourcePartPath: partPath,
|
|
604
|
+
chartPartLookup: subPartChartPartLookup,
|
|
605
|
+
});
|
|
593
606
|
parsedHeaders.push({
|
|
594
607
|
variant: ref.variant,
|
|
595
608
|
partPath,
|
|
@@ -621,7 +634,12 @@ export function loadDocxEditorSession(
|
|
|
621
634
|
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
622
635
|
} else {
|
|
623
636
|
const parsedFooterRevisions = parseRevisionsFromStoryXml(xml);
|
|
624
|
-
const parsed = parseFooterXml(xml
|
|
637
|
+
const parsed = parseFooterXml(xml, {
|
|
638
|
+
relationships: subPartRelationships,
|
|
639
|
+
mediaParts,
|
|
640
|
+
sourcePartPath: partPath,
|
|
641
|
+
chartPartLookup: subPartChartPartLookup,
|
|
642
|
+
});
|
|
625
643
|
parsedFooters.push({
|
|
626
644
|
variant: ref.variant,
|
|
627
645
|
partPath,
|
|
@@ -775,6 +793,13 @@ export function loadDocxEditorSession(
|
|
|
775
793
|
)
|
|
776
794
|
: undefined;
|
|
777
795
|
|
|
796
|
+
const mergedMedia = mergeSecondaryStoryMediaCatalog(normalizedDocument.media, {
|
|
797
|
+
headers: parsedHeaders,
|
|
798
|
+
footers: parsedFooters,
|
|
799
|
+
footnoteCollection,
|
|
800
|
+
mediaParts,
|
|
801
|
+
});
|
|
802
|
+
|
|
778
803
|
const subParts: SubPartsCatalog | undefined =
|
|
779
804
|
parsedHeaders.length > 0 ||
|
|
780
805
|
parsedFooters.length > 0 ||
|
|
@@ -809,7 +834,7 @@ export function loadDocxEditorSession(
|
|
|
809
834
|
documentId: options.documentId,
|
|
810
835
|
timestamp,
|
|
811
836
|
numbering: parsedNumbering,
|
|
812
|
-
media:
|
|
837
|
+
media: mergedMedia,
|
|
813
838
|
content: normalizedDocument.content,
|
|
814
839
|
subParts,
|
|
815
840
|
parsedStyles,
|
|
@@ -1518,16 +1543,28 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1518
1543
|
}
|
|
1519
1544
|
|
|
1520
1545
|
const partPath = resolveRelationshipTarget(mainDocumentPath, relationship);
|
|
1521
|
-
const
|
|
1546
|
+
const part = sourcePackage.parts.get(partPath);
|
|
1547
|
+
const partBytes = part?.bytes;
|
|
1522
1548
|
if (!partBytes) {
|
|
1523
1549
|
continue;
|
|
1524
1550
|
}
|
|
1525
1551
|
|
|
1526
1552
|
await scheduler.yield();
|
|
1527
1553
|
const xml = decodeUtf8(partBytes);
|
|
1554
|
+
const subPartRelationships = part?.relationships ?? [];
|
|
1555
|
+
const subPartChartPartLookup = createChartPartLookup(
|
|
1556
|
+
sourcePackage,
|
|
1557
|
+
partPath,
|
|
1558
|
+
subPartRelationships,
|
|
1559
|
+
);
|
|
1528
1560
|
if (ref.kind === "header") {
|
|
1529
1561
|
const parsedHeaderRevisions = parseRevisionsFromStoryXml(xml);
|
|
1530
|
-
const parsed = parseHeaderXml(xml
|
|
1562
|
+
const parsed = parseHeaderXml(xml, {
|
|
1563
|
+
relationships: subPartRelationships,
|
|
1564
|
+
mediaParts,
|
|
1565
|
+
sourcePartPath: partPath,
|
|
1566
|
+
chartPartLookup: subPartChartPartLookup,
|
|
1567
|
+
});
|
|
1531
1568
|
parsedHeaders.push({
|
|
1532
1569
|
variant: ref.variant,
|
|
1533
1570
|
partPath,
|
|
@@ -1559,7 +1596,12 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1559
1596
|
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
1560
1597
|
} else {
|
|
1561
1598
|
const parsedFooterRevisions = parseRevisionsFromStoryXml(xml);
|
|
1562
|
-
const parsed = parseFooterXml(xml
|
|
1599
|
+
const parsed = parseFooterXml(xml, {
|
|
1600
|
+
relationships: subPartRelationships,
|
|
1601
|
+
mediaParts,
|
|
1602
|
+
sourcePartPath: partPath,
|
|
1603
|
+
chartPartLookup: subPartChartPartLookup,
|
|
1604
|
+
});
|
|
1563
1605
|
parsedFooters.push({
|
|
1564
1606
|
variant: ref.variant,
|
|
1565
1607
|
partPath,
|
|
@@ -1715,6 +1757,13 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1715
1757
|
)
|
|
1716
1758
|
: undefined;
|
|
1717
1759
|
|
|
1760
|
+
const mergedMedia = mergeSecondaryStoryMediaCatalog(normalizedDocument.media, {
|
|
1761
|
+
headers: parsedHeaders,
|
|
1762
|
+
footers: parsedFooters,
|
|
1763
|
+
footnoteCollection,
|
|
1764
|
+
mediaParts,
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1718
1767
|
const subParts: SubPartsCatalog | undefined =
|
|
1719
1768
|
parsedHeaders.length > 0 ||
|
|
1720
1769
|
parsedFooters.length > 0 ||
|
|
@@ -1749,7 +1798,7 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1749
1798
|
documentId: options.documentId,
|
|
1750
1799
|
timestamp,
|
|
1751
1800
|
numbering: parsedNumbering,
|
|
1752
|
-
media:
|
|
1801
|
+
media: mergedMedia,
|
|
1753
1802
|
content: normalizedDocument.content,
|
|
1754
1803
|
subParts,
|
|
1755
1804
|
parsedStyles,
|
|
@@ -2644,6 +2693,116 @@ function recordImportedOpaqueBlock(
|
|
|
2644
2693
|
};
|
|
2645
2694
|
}
|
|
2646
2695
|
|
|
2696
|
+
function mergeSecondaryStoryMediaCatalog(
|
|
2697
|
+
media: MediaCatalog,
|
|
2698
|
+
input: {
|
|
2699
|
+
headers: readonly HeaderDocument[];
|
|
2700
|
+
footers: readonly FooterDocument[];
|
|
2701
|
+
footnoteCollection?: FootnoteCollection;
|
|
2702
|
+
mediaParts: ReadonlyMap<string, { path: string; contentType: string }>;
|
|
2703
|
+
},
|
|
2704
|
+
): MediaCatalog {
|
|
2705
|
+
const items = { ...media.items };
|
|
2706
|
+
let changed = false;
|
|
2707
|
+
|
|
2708
|
+
const registerMediaItem = (
|
|
2709
|
+
mediaId: string,
|
|
2710
|
+
record: Omit<NonNullable<MediaCatalog["items"][string]>, "mediaId">,
|
|
2711
|
+
) => {
|
|
2712
|
+
const existing = items[mediaId];
|
|
2713
|
+
items[mediaId] = existing
|
|
2714
|
+
? {
|
|
2715
|
+
...existing,
|
|
2716
|
+
...record,
|
|
2717
|
+
mediaId,
|
|
2718
|
+
}
|
|
2719
|
+
: {
|
|
2720
|
+
mediaId,
|
|
2721
|
+
...record,
|
|
2722
|
+
};
|
|
2723
|
+
changed = true;
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
const visitInline = (node: InlineNode) => {
|
|
2727
|
+
if (node.type === "image") {
|
|
2728
|
+
const packagePartName = `/${node.mediaId.slice("media:".length)}`;
|
|
2729
|
+
registerMediaItem(node.mediaId, {
|
|
2730
|
+
contentType:
|
|
2731
|
+
items[node.mediaId]?.contentType ??
|
|
2732
|
+
input.mediaParts.get(packagePartName)?.contentType ??
|
|
2733
|
+
"application/octet-stream",
|
|
2734
|
+
filename: packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin",
|
|
2735
|
+
packagePartName,
|
|
2736
|
+
...(node.altText ? { altText: node.altText } : {}),
|
|
2737
|
+
});
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (node.type === "drawing_frame" && node.content.type === "picture" && node.content.mediaId) {
|
|
2741
|
+
const packagePartName =
|
|
2742
|
+
typeof node.content.packagePartName === "string" && node.content.packagePartName.length > 0
|
|
2743
|
+
? node.content.packagePartName
|
|
2744
|
+
: `/${node.content.mediaId.slice("media:".length)}`;
|
|
2745
|
+
registerMediaItem(node.content.mediaId, {
|
|
2746
|
+
contentType:
|
|
2747
|
+
items[node.content.mediaId]?.contentType ??
|
|
2748
|
+
input.mediaParts.get(packagePartName)?.contentType ??
|
|
2749
|
+
"application/octet-stream",
|
|
2750
|
+
filename: packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin",
|
|
2751
|
+
packagePartName,
|
|
2752
|
+
relationshipId: node.content.blipRef,
|
|
2753
|
+
...(node.anchor.docPr?.descr ? { altText: node.anchor.docPr.descr } : {}),
|
|
2754
|
+
widthEmu: node.anchor.extent.widthEmu,
|
|
2755
|
+
heightEmu: node.anchor.extent.heightEmu,
|
|
2756
|
+
});
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
if (node.type === "hyperlink" || node.type === "field") {
|
|
2760
|
+
for (const child of node.children) {
|
|
2761
|
+
visitInline(child);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
|
|
2767
|
+
for (const block of blocks) {
|
|
2768
|
+
if (block.type === "paragraph") {
|
|
2769
|
+
for (const child of block.children) {
|
|
2770
|
+
visitInline(child);
|
|
2771
|
+
}
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
if (block.type === "table") {
|
|
2775
|
+
for (const row of block.rows) {
|
|
2776
|
+
for (const cell of row.cells) {
|
|
2777
|
+
visitBlocks(cell.children);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2783
|
+
visitBlocks(block.children);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
for (const header of input.headers) {
|
|
2789
|
+
visitBlocks(header.blocks);
|
|
2790
|
+
}
|
|
2791
|
+
for (const footer of input.footers) {
|
|
2792
|
+
visitBlocks(footer.blocks);
|
|
2793
|
+
}
|
|
2794
|
+
if (input.footnoteCollection) {
|
|
2795
|
+
for (const note of Object.values(input.footnoteCollection.footnotes)) {
|
|
2796
|
+
visitBlocks(note.blocks);
|
|
2797
|
+
}
|
|
2798
|
+
for (const note of Object.values(input.footnoteCollection.endnotes)) {
|
|
2799
|
+
visitBlocks(note.blocks);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
return changed ? { ...media, items } : media;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2647
2806
|
// Canonical model styleId validation pattern — styleIds that don't match
|
|
2648
2807
|
// are excluded from the catalog to avoid snapshot validation failures.
|
|
2649
2808
|
const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -37,8 +37,20 @@ export function serializeFootnotesXml(
|
|
|
37
37
|
): string {
|
|
38
38
|
const entries = Object.values(collection.footnotes).sort(compareNoteIds);
|
|
39
39
|
const body = [
|
|
40
|
-
serializeSeparatorStub(
|
|
41
|
-
|
|
40
|
+
serializeSeparatorStub(
|
|
41
|
+
"footnote",
|
|
42
|
+
"-1",
|
|
43
|
+
"separator",
|
|
44
|
+
collection.footnoteSeparators?.separatorParagraphXml,
|
|
45
|
+
collection.footnoteSeparators?.separatorContent,
|
|
46
|
+
),
|
|
47
|
+
serializeSeparatorStub(
|
|
48
|
+
"footnote",
|
|
49
|
+
"0",
|
|
50
|
+
"continuationSeparator",
|
|
51
|
+
collection.footnoteSeparators?.continuationSeparatorParagraphXml,
|
|
52
|
+
collection.footnoteSeparators?.continuationSeparatorContent,
|
|
53
|
+
),
|
|
42
54
|
...entries.map((entry) =>
|
|
43
55
|
serializeNoteDefinition("footnote", entry, revisionsByNoteId[entry.noteId] ?? [])),
|
|
44
56
|
].join("");
|
|
@@ -59,8 +71,20 @@ export function serializeEndnotesXml(
|
|
|
59
71
|
): string {
|
|
60
72
|
const entries = Object.values(collection.endnotes).sort(compareNoteIds);
|
|
61
73
|
const body = [
|
|
62
|
-
serializeSeparatorStub(
|
|
63
|
-
|
|
74
|
+
serializeSeparatorStub(
|
|
75
|
+
"endnote",
|
|
76
|
+
"-1",
|
|
77
|
+
"separator",
|
|
78
|
+
collection.endnoteSeparators?.separatorParagraphXml,
|
|
79
|
+
collection.endnoteSeparators?.separatorContent,
|
|
80
|
+
),
|
|
81
|
+
serializeSeparatorStub(
|
|
82
|
+
"endnote",
|
|
83
|
+
"0",
|
|
84
|
+
"continuationSeparator",
|
|
85
|
+
collection.endnoteSeparators?.continuationSeparatorParagraphXml,
|
|
86
|
+
collection.endnoteSeparators?.continuationSeparatorContent,
|
|
87
|
+
),
|
|
64
88
|
...entries.map((entry) =>
|
|
65
89
|
serializeNoteDefinition("endnote", entry, revisionsByNoteId[entry.noteId] ?? [])),
|
|
66
90
|
].join("");
|
|
@@ -77,9 +101,16 @@ function serializeSeparatorStub(
|
|
|
77
101
|
kind: "footnote" | "endnote",
|
|
78
102
|
id: string,
|
|
79
103
|
type: "separator" | "continuationSeparator",
|
|
104
|
+
paragraphXml?: string,
|
|
105
|
+
content?: string,
|
|
80
106
|
): string {
|
|
81
107
|
const tag = kind === "footnote" ? "w:footnote" : "w:endnote";
|
|
82
|
-
|
|
108
|
+
const body = paragraphXml && paragraphXml.length > 0
|
|
109
|
+
? paragraphXml
|
|
110
|
+
: content && content.length > 0
|
|
111
|
+
? `<w:p>${content}</w:p>`
|
|
112
|
+
: "<w:p/>";
|
|
113
|
+
return `<${tag} w:type="${type}" w:id="${id}">${body}</${tag}>`;
|
|
83
114
|
}
|
|
84
115
|
|
|
85
116
|
function serializeNoteDefinition(
|
|
@@ -263,6 +263,13 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
263
263
|
case "wordart":
|
|
264
264
|
case "vml_shape":
|
|
265
265
|
return wrapInlineRawXml(node.rawXml);
|
|
266
|
+
case "drawing_frame": {
|
|
267
|
+
const { content } = node;
|
|
268
|
+
if (!("rawXml" in content) || typeof content.rawXml !== "string" || content.rawXml.length === 0) {
|
|
269
|
+
throw new Error("Cannot safely serialize drawing_frame content in header/footer sub-parts without rawXml.");
|
|
270
|
+
}
|
|
271
|
+
return wrapInlineRawXml(content.rawXml);
|
|
272
|
+
}
|
|
266
273
|
case "ole_embed":
|
|
267
274
|
// OLE in header/footer is rare but legal in OOXML. Relationship
|
|
268
275
|
// tracking for sub-parts is not wired; emit rawXml verbatim
|
|
@@ -675,18 +675,10 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
675
675
|
if (paragraph.styleId) {
|
|
676
676
|
children.push(`<w:pStyle w:val="${escapeXmlAttribute(paragraph.styleId)}"/>`);
|
|
677
677
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
children.push("<w:keepLines/>");
|
|
683
|
-
}
|
|
684
|
-
if (paragraph.pageBreakBefore) {
|
|
685
|
-
children.push("<w:pageBreakBefore/>");
|
|
686
|
-
}
|
|
687
|
-
if (paragraph.widowControl) {
|
|
688
|
-
children.push("<w:widowControl/>");
|
|
689
|
-
}
|
|
678
|
+
pushOnOffParagraphProperty(children, "keepNext", paragraph.keepNext);
|
|
679
|
+
pushOnOffParagraphProperty(children, "keepLines", paragraph.keepLines);
|
|
680
|
+
pushOnOffParagraphProperty(children, "pageBreakBefore", paragraph.pageBreakBefore);
|
|
681
|
+
pushOnOffParagraphProperty(children, "widowControl", paragraph.widowControl);
|
|
690
682
|
if (paragraph.outlineLevel !== undefined) {
|
|
691
683
|
children.push(`<w:outlineLvl w:val="${paragraph.outlineLevel}"/>`);
|
|
692
684
|
}
|
|
@@ -738,12 +730,8 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
|
|
|
738
730
|
children.push(shadingXml);
|
|
739
731
|
}
|
|
740
732
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
if (paragraph.suppressLineNumbers) {
|
|
745
|
-
children.push("<w:suppressLineNumbers/>");
|
|
746
|
-
}
|
|
733
|
+
pushOnOffParagraphProperty(children, "bidi", paragraph.bidi);
|
|
734
|
+
pushOnOffParagraphProperty(children, "suppressLineNumbers", paragraph.suppressLineNumbers);
|
|
747
735
|
if (paragraph.cnfStyle) {
|
|
748
736
|
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(paragraph.cnfStyle)}"/>`);
|
|
749
737
|
}
|
|
@@ -811,9 +799,28 @@ function serializeParagraphShading(shading: ParagraphNode["shading"]): string {
|
|
|
811
799
|
} else if (shading.val === "clear") {
|
|
812
800
|
attrs.push(`w:fill="auto"`);
|
|
813
801
|
}
|
|
802
|
+
if (shading.themeFill) attrs.push(`w:themeFill="${escapeXmlAttribute(shading.themeFill)}"`);
|
|
803
|
+
if (shading.themeFillTint) attrs.push(`w:themeFillTint="${escapeXmlAttribute(shading.themeFillTint)}"`);
|
|
804
|
+
if (shading.themeFillShade) attrs.push(`w:themeFillShade="${escapeXmlAttribute(shading.themeFillShade)}"`);
|
|
805
|
+
if (shading.themeColor) attrs.push(`w:themeColor="${escapeXmlAttribute(shading.themeColor)}"`);
|
|
806
|
+
if (shading.themeColorTint) attrs.push(`w:themeColorTint="${escapeXmlAttribute(shading.themeColorTint)}"`);
|
|
807
|
+
if (shading.themeColorShade) attrs.push(`w:themeColorShade="${escapeXmlAttribute(shading.themeColorShade)}"`);
|
|
814
808
|
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
815
809
|
}
|
|
816
810
|
|
|
811
|
+
function pushOnOffParagraphProperty(
|
|
812
|
+
children: string[],
|
|
813
|
+
localName: string,
|
|
814
|
+
value: boolean | undefined,
|
|
815
|
+
): void {
|
|
816
|
+
if (value === undefined) return;
|
|
817
|
+
children.push(
|
|
818
|
+
value
|
|
819
|
+
? `<w:${localName}/>`
|
|
820
|
+
: `<w:${localName} w:val="false"/>`,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
817
824
|
function serializeRunPropertiesFromMarks(marks: TextMark[] | undefined): string {
|
|
818
825
|
return serializeRunProperties(marks);
|
|
819
826
|
}
|
|
@@ -61,6 +61,12 @@ function buildShadingXml(s: ParagraphShading | undefined): string {
|
|
|
61
61
|
if (s.val) attrs.push(`w:val="${escXml(s.val)}"`);
|
|
62
62
|
if (s.color) attrs.push(`w:color="${escXml(s.color)}"`);
|
|
63
63
|
if (s.fill) attrs.push(`w:fill="${escXml(s.fill)}"`);
|
|
64
|
+
if (s.themeFill) attrs.push(`w:themeFill="${escXml(s.themeFill)}"`);
|
|
65
|
+
if (s.themeFillTint) attrs.push(`w:themeFillTint="${escXml(s.themeFillTint)}"`);
|
|
66
|
+
if (s.themeFillShade) attrs.push(`w:themeFillShade="${escXml(s.themeFillShade)}"`);
|
|
67
|
+
if (s.themeColor) attrs.push(`w:themeColor="${escXml(s.themeColor)}"`);
|
|
68
|
+
if (s.themeColorTint) attrs.push(`w:themeColorTint="${escXml(s.themeColorTint)}"`);
|
|
69
|
+
if (s.themeColorShade) attrs.push(`w:themeColorShade="${escXml(s.themeColorShade)}"`);
|
|
64
70
|
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
65
71
|
}
|
|
66
72
|
|