@beyondwork/docx-react-component 1.0.56 → 1.0.57
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 +1 -1
- package/src/api/public-types.ts +157 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +107 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +415 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +693 -41
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +186 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +168 -10
- package/src/ui/editor-runtime-boundary.ts +94 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +188 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
package/src/io/docx-session.ts
CHANGED
|
@@ -148,10 +148,10 @@ import {
|
|
|
148
148
|
parseFooterXml,
|
|
149
149
|
} from "./ooxml/parse-headers-footers.ts";
|
|
150
150
|
import { parseFootnotesXml, parseEndnotesXml } from "./ooxml/parse-footnotes.ts";
|
|
151
|
-
import { parseThemeXml } from "./ooxml/parse-theme.ts";
|
|
152
|
-
import { resolveTheme } from "./ooxml/parse-theme.ts";
|
|
151
|
+
import { materializeCanonicalTheme, parseThemeXml, resolveTheme } from "./ooxml/parse-theme.ts";
|
|
153
152
|
import { parseSettingsXml } from "./ooxml/parse-settings.ts";
|
|
154
153
|
import { parseStylesXml, type ParseStylesResult } from "./ooxml/parse-styles.ts";
|
|
154
|
+
import { parseFontTable } from "./ooxml/parse-font-table.ts";
|
|
155
155
|
import {
|
|
156
156
|
serializeHeaderXml,
|
|
157
157
|
serializeHeaderXmlWithRevisions,
|
|
@@ -224,6 +224,9 @@ const SETTINGS_RELATIONSHIP_TYPE =
|
|
|
224
224
|
const STYLES_RELATIONSHIP_TYPE =
|
|
225
225
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
|
|
226
226
|
const STYLES_PART_PATH = "/word/styles.xml";
|
|
227
|
+
const FONT_TABLE_RELATIONSHIP_TYPE =
|
|
228
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
|
|
229
|
+
const FONT_TABLE_PART_PATH = "/word/fontTable.xml";
|
|
227
230
|
const FOOTNOTES_PART_PATH = "/word/footnotes.xml";
|
|
228
231
|
const ENDNOTES_PART_PATH = "/word/endnotes.xml";
|
|
229
232
|
const SETTINGS_PART_PATH = "/word/settings.xml";
|
|
@@ -717,6 +720,13 @@ export function loadDocxEditorSession(
|
|
|
717
720
|
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
718
721
|
)
|
|
719
722
|
: undefined;
|
|
723
|
+
const canonicalTheme =
|
|
724
|
+
parsedTheme !== undefined
|
|
725
|
+
? materializeCanonicalTheme(
|
|
726
|
+
parsedTheme,
|
|
727
|
+
parsedSettings?.clrSchemeMapping ?? {},
|
|
728
|
+
)
|
|
729
|
+
: undefined;
|
|
720
730
|
const settingsXmlForProtection =
|
|
721
731
|
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
722
732
|
? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
|
|
@@ -739,6 +749,21 @@ export function loadDocxEditorSession(
|
|
|
739
749
|
)
|
|
740
750
|
: parseStylesXml("");
|
|
741
751
|
|
|
752
|
+
// ---- Parse fontTable.xml for canonical font catalog ----
|
|
753
|
+
const fontTablePartPath = resolveDocumentRelatedPartPath(
|
|
754
|
+
sourcePackage,
|
|
755
|
+
mainDocumentPath,
|
|
756
|
+
documentPart.relationships,
|
|
757
|
+
FONT_TABLE_RELATIONSHIP_TYPE,
|
|
758
|
+
FONT_TABLE_PART_PATH,
|
|
759
|
+
);
|
|
760
|
+
const parsedFontTable =
|
|
761
|
+
fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
|
|
762
|
+
? parseFontTable(
|
|
763
|
+
decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
|
|
764
|
+
)
|
|
765
|
+
: undefined;
|
|
766
|
+
|
|
742
767
|
const subParts: SubPartsCatalog | undefined =
|
|
743
768
|
parsedHeaders.length > 0 ||
|
|
744
769
|
parsedFooters.length > 0 ||
|
|
@@ -746,6 +771,7 @@ export function loadDocxEditorSession(
|
|
|
746
771
|
parsedTheme !== undefined ||
|
|
747
772
|
normalizedDocument.finalSectionProperties !== undefined ||
|
|
748
773
|
resolvedTheme !== undefined ||
|
|
774
|
+
canonicalTheme !== undefined ||
|
|
749
775
|
parsedSettings !== undefined
|
|
750
776
|
? {
|
|
751
777
|
headers: parsedHeaders,
|
|
@@ -756,6 +782,7 @@ export function loadDocxEditorSession(
|
|
|
756
782
|
? { finalSectionProperties: normalizedDocument.finalSectionProperties }
|
|
757
783
|
: {}),
|
|
758
784
|
...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
|
|
785
|
+
...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
|
|
759
786
|
...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
|
|
760
787
|
}
|
|
761
788
|
: undefined;
|
|
@@ -775,6 +802,7 @@ export function loadDocxEditorSession(
|
|
|
775
802
|
content: normalizedDocument.content,
|
|
776
803
|
subParts,
|
|
777
804
|
parsedStyles,
|
|
805
|
+
fontTable: parsedFontTable,
|
|
778
806
|
preservation: {
|
|
779
807
|
...normalizedDocument.preservation,
|
|
780
808
|
packageParts: {
|
|
@@ -1627,6 +1655,13 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1627
1655
|
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
1628
1656
|
)
|
|
1629
1657
|
: undefined;
|
|
1658
|
+
const canonicalTheme =
|
|
1659
|
+
parsedTheme !== undefined
|
|
1660
|
+
? materializeCanonicalTheme(
|
|
1661
|
+
parsedTheme,
|
|
1662
|
+
parsedSettings?.clrSchemeMapping ?? {},
|
|
1663
|
+
)
|
|
1664
|
+
: undefined;
|
|
1630
1665
|
const settingsXmlForProtection =
|
|
1631
1666
|
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
1632
1667
|
? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
|
|
@@ -1650,6 +1685,21 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1650
1685
|
: parseStylesXml("");
|
|
1651
1686
|
await scheduler.yield();
|
|
1652
1687
|
|
|
1688
|
+
// ---- Parse fontTable.xml for canonical font catalog ----
|
|
1689
|
+
const fontTablePartPath = resolveDocumentRelatedPartPath(
|
|
1690
|
+
sourcePackage,
|
|
1691
|
+
mainDocumentPath,
|
|
1692
|
+
documentPart.relationships,
|
|
1693
|
+
FONT_TABLE_RELATIONSHIP_TYPE,
|
|
1694
|
+
FONT_TABLE_PART_PATH,
|
|
1695
|
+
);
|
|
1696
|
+
const parsedFontTable =
|
|
1697
|
+
fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
|
|
1698
|
+
? parseFontTable(
|
|
1699
|
+
decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
|
|
1700
|
+
)
|
|
1701
|
+
: undefined;
|
|
1702
|
+
|
|
1653
1703
|
const subParts: SubPartsCatalog | undefined =
|
|
1654
1704
|
parsedHeaders.length > 0 ||
|
|
1655
1705
|
parsedFooters.length > 0 ||
|
|
@@ -1657,6 +1707,7 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1657
1707
|
parsedTheme !== undefined ||
|
|
1658
1708
|
normalizedDocument.finalSectionProperties !== undefined ||
|
|
1659
1709
|
resolvedTheme !== undefined ||
|
|
1710
|
+
canonicalTheme !== undefined ||
|
|
1660
1711
|
parsedSettings !== undefined
|
|
1661
1712
|
? {
|
|
1662
1713
|
headers: parsedHeaders,
|
|
@@ -1667,6 +1718,7 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1667
1718
|
? { finalSectionProperties: normalizedDocument.finalSectionProperties }
|
|
1668
1719
|
: {}),
|
|
1669
1720
|
...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
|
|
1721
|
+
...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
|
|
1670
1722
|
...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
|
|
1671
1723
|
}
|
|
1672
1724
|
: undefined;
|
|
@@ -1686,6 +1738,7 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1686
1738
|
content: normalizedDocument.content,
|
|
1687
1739
|
subParts,
|
|
1688
1740
|
parsedStyles,
|
|
1741
|
+
fontTable: parsedFontTable,
|
|
1689
1742
|
preservation: {
|
|
1690
1743
|
...normalizedDocument.preservation,
|
|
1691
1744
|
packageParts: {
|
|
@@ -2401,6 +2454,7 @@ function createImportedCanonicalDocument(input: {
|
|
|
2401
2454
|
content: CanonicalDocumentEnvelope["content"];
|
|
2402
2455
|
subParts?: SubPartsCatalog;
|
|
2403
2456
|
parsedStyles?: ParseStylesResult;
|
|
2457
|
+
fontTable?: CanonicalDocumentEnvelope["fontTable"];
|
|
2404
2458
|
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
2405
2459
|
diagnostics: CanonicalDocumentEnvelope["diagnostics"];
|
|
2406
2460
|
review: CanonicalDocumentEnvelope["review"];
|
|
@@ -2431,6 +2485,7 @@ function createImportedCanonicalDocument(input: {
|
|
|
2431
2485
|
preservation: input.preservation,
|
|
2432
2486
|
diagnostics: input.diagnostics,
|
|
2433
2487
|
...(input.subParts !== undefined ? { subParts: input.subParts } : {}),
|
|
2488
|
+
...(input.fontTable !== undefined ? { fontTable: input.fontTable } : {}),
|
|
2434
2489
|
};
|
|
2435
2490
|
}
|
|
2436
2491
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BorderSpec,
|
|
4
4
|
CustomXmlNode,
|
|
5
5
|
DocumentRootNode,
|
|
6
|
+
FootnoteProperties,
|
|
6
7
|
InlineNode,
|
|
7
8
|
MediaCatalog,
|
|
8
9
|
ParagraphNode,
|
|
@@ -530,6 +531,8 @@ function serializeTableInlineNode(
|
|
|
530
531
|
case "wordart":
|
|
531
532
|
case "vml_shape":
|
|
532
533
|
return wrapInlineRawXml(node.rawXml);
|
|
534
|
+
case "drawing_frame":
|
|
535
|
+
return serializeDrawingFrameNode(node);
|
|
533
536
|
case "hyperlink": {
|
|
534
537
|
const hyperlinkOpen = node.href.startsWith("#")
|
|
535
538
|
? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
|
|
@@ -962,6 +965,21 @@ function serializeInlineNode(
|
|
|
962
965
|
boundaries,
|
|
963
966
|
};
|
|
964
967
|
}
|
|
968
|
+
case "drawing_frame": {
|
|
969
|
+
// CO4 F4.1 — emit preserved rawXml from content (every DrawingFrame
|
|
970
|
+
// variant retains the original w:drawing slice on its content or is
|
|
971
|
+
// a picture that also keeps the slice on its parent). Matches the
|
|
972
|
+
// same "rawXml preservation" contract as shape/chart/smartart above.
|
|
973
|
+
const xml = serializeDrawingFrameNode(node);
|
|
974
|
+
const boundaries = new Map<number, number>();
|
|
975
|
+
boundaries.set(cursor, xmlOffset);
|
|
976
|
+
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
977
|
+
return {
|
|
978
|
+
xml,
|
|
979
|
+
cursor: cursor + 1,
|
|
980
|
+
boundaries,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
965
983
|
case "hyperlink": {
|
|
966
984
|
const hyperlinkOpen = node.href.startsWith("#")
|
|
967
985
|
? `<w:hyperlink w:anchor="${escapeXmlAttribute(node.href.slice(1))}">`
|
|
@@ -1125,6 +1143,42 @@ function serializeInlineNode(
|
|
|
1125
1143
|
}
|
|
1126
1144
|
}
|
|
1127
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* CO4 F4.1 — serialize a DrawingFrameNode by emitting the preserved rawXml.
|
|
1148
|
+
*
|
|
1149
|
+
* Every DrawingFrame variant carries a lossless original-XML reference:
|
|
1150
|
+
* - shape, chart_preview, smartart_preview, opaque → `content.rawXml`
|
|
1151
|
+
* - picture → the parse layer stores the containing drawing XML on the outer
|
|
1152
|
+
* DrawingFrame's context; for the MVP we reconstruct from the anchor +
|
|
1153
|
+
* blipRef if needed. In practice picture content also originates from a
|
|
1154
|
+
* larger drawingXml slice — the parent `w:drawing` substring — which we
|
|
1155
|
+
* don't retain explicitly yet. When `content.rawXml` is absent we emit a
|
|
1156
|
+
* minimal wp:inline/wp:anchor envelope preserving extent + relationship id.
|
|
1157
|
+
*
|
|
1158
|
+
* For the round-trip v1 contract: if any `rawXml` is present, round-trip is
|
|
1159
|
+
* byte-stable modulo whitespace. If the picture-content path has no rawXml,
|
|
1160
|
+
* a reconstructed minimal drawing is emitted.
|
|
1161
|
+
*/
|
|
1162
|
+
function serializeDrawingFrameNode(
|
|
1163
|
+
node: Extract<InlineNode, { type: "drawing_frame" }>,
|
|
1164
|
+
): string {
|
|
1165
|
+
const content = node.content;
|
|
1166
|
+
if ("rawXml" in content && typeof content.rawXml === "string" && content.rawXml.length > 0) {
|
|
1167
|
+
return wrapInlineRawXml(content.rawXml);
|
|
1168
|
+
}
|
|
1169
|
+
// Picture content with no rawXml — reconstruct a minimal inline image so the
|
|
1170
|
+
// round-trip doesn't silently drop the image. Use the blipRef + anchor extent.
|
|
1171
|
+
if (content.type === "picture") {
|
|
1172
|
+
const { widthEmu, heightEmu } = node.anchor.extent;
|
|
1173
|
+
const embed = escapeXmlAttribute(content.blipRef);
|
|
1174
|
+
const envelope = node.anchor.display === "floating" ? "wp:anchor" : "wp:inline";
|
|
1175
|
+
return `<w:r><w:drawing><${envelope} xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${widthEmu}" cy="${heightEmu}"/><wp:docPr id="1" name=""/><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name=""/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${embed}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${widthEmu}" cy="${heightEmu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></${envelope}></w:drawing></w:r>`;
|
|
1176
|
+
}
|
|
1177
|
+
// Shape/chart/smartart/opaque without rawXml — nothing to emit. Should not
|
|
1178
|
+
// happen in practice since the parser always sets rawXml.
|
|
1179
|
+
return "";
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1128
1182
|
function serializeImageNode(
|
|
1129
1183
|
node: Extract<InlineNode, { type: "image" }>,
|
|
1130
1184
|
state: SerializationState,
|
|
@@ -1482,6 +1536,15 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
|
|
|
1482
1536
|
}
|
|
1483
1537
|
}
|
|
1484
1538
|
|
|
1539
|
+
// Per-section footnote + endnote configuration. ECMA-376 §17.6.18 places
|
|
1540
|
+
// these after the header/footer references and before <w:type>.
|
|
1541
|
+
if (props.footnotePr) {
|
|
1542
|
+
children.push(serializeFootnoteLikeProperties("w:footnotePr", props.footnotePr));
|
|
1543
|
+
}
|
|
1544
|
+
if (props.endnotePr) {
|
|
1545
|
+
children.push(serializeFootnoteLikeProperties("w:endnotePr", props.endnotePr));
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1485
1548
|
// Section type
|
|
1486
1549
|
if (props.sectionType) {
|
|
1487
1550
|
children.push(`<w:type w:val="${escapeXmlAttribute(props.sectionType)}"/>`);
|
|
@@ -1623,6 +1686,25 @@ export function serializeSectionPropertiesXml(props: SectionProperties): string
|
|
|
1623
1686
|
return `<w:sectPr>${children.join("")}</w:sectPr>`;
|
|
1624
1687
|
}
|
|
1625
1688
|
|
|
1689
|
+
/**
|
|
1690
|
+
* Emit `<w:footnotePr>` or `<w:endnotePr>` from the typed
|
|
1691
|
+
* `FootnoteProperties` / `EndnoteProperties` shape. Child order follows
|
|
1692
|
+
* ECMA-376 §17.11.11–.18: pos → numFmt → numStart → numRestart.
|
|
1693
|
+
* Each child is emitted only when its typed field is present.
|
|
1694
|
+
*/
|
|
1695
|
+
function serializeFootnoteLikeProperties(
|
|
1696
|
+
elementName: "w:footnotePr" | "w:endnotePr",
|
|
1697
|
+
props: FootnoteProperties,
|
|
1698
|
+
): string {
|
|
1699
|
+
const parts: string[] = [];
|
|
1700
|
+
if (props.pos) parts.push(`<w:pos w:val="${escapeXmlAttribute(props.pos)}"/>`);
|
|
1701
|
+
if (props.numFmt) parts.push(`<w:numFmt w:val="${escapeXmlAttribute(props.numFmt)}"/>`);
|
|
1702
|
+
if (props.numStart !== undefined) parts.push(`<w:numStart w:val="${twip(props.numStart)}"/>`);
|
|
1703
|
+
if (props.numRestart) parts.push(`<w:numRestart w:val="${escapeXmlAttribute(props.numRestart)}"/>`);
|
|
1704
|
+
if (parts.length === 0) return `<${elementName}/>`;
|
|
1705
|
+
return `<${elementName}>${parts.join("")}</${elementName}>`;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1626
1708
|
function wrapInlineRawXml(rawXml: string): string {
|
|
1627
1709
|
const trimmed = rawXml.trimStart();
|
|
1628
1710
|
return trimmed.startsWith("<w:r") ? rawXml : `<w:r>${rawXml}</w:r>`;
|
|
@@ -67,16 +67,18 @@ function buildParagraphStyleXml(
|
|
|
67
67
|
): string {
|
|
68
68
|
const defaultAttr = style.isDefault ? ` w:default="1"` : "";
|
|
69
69
|
const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
|
|
70
|
+
const aliasesEl = buildAliasesXml(style.aliases);
|
|
70
71
|
const basedOnEl = style.basedOn
|
|
71
72
|
? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
|
|
72
73
|
: "";
|
|
73
74
|
const nextEl = style.nextStyle
|
|
74
75
|
? `<w:next w:val="${escXml(style.nextStyle)}"/>`
|
|
75
76
|
: "";
|
|
76
|
-
// ECMA-376 §17.7 emit order: name → basedOn → next → link → ...
|
|
77
|
+
// ECMA-376 §17.7 emit order: name → aliases → basedOn → next → link → autoRedefine → ...
|
|
77
78
|
const linkEl = style.linkedStyleId
|
|
78
79
|
? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
|
|
79
80
|
: "";
|
|
81
|
+
const autoRedefineEl = buildOnOffEl("autoRedefine", style.autoRedefine);
|
|
80
82
|
|
|
81
83
|
// Build pPr: may contain numPr (from numbering) and any canonical formatting.
|
|
82
84
|
// We reconstruct the pPr children in canonical order:
|
|
@@ -97,15 +99,37 @@ function buildParagraphStyleXml(
|
|
|
97
99
|
return (
|
|
98
100
|
`<w:style w:type="paragraph" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
|
|
99
101
|
nameEl +
|
|
102
|
+
aliasesEl +
|
|
100
103
|
basedOnEl +
|
|
101
104
|
nextEl +
|
|
102
105
|
linkEl +
|
|
106
|
+
autoRedefineEl +
|
|
103
107
|
pPrBodyXml +
|
|
104
108
|
rPrXml +
|
|
105
109
|
`</w:style>`
|
|
106
110
|
);
|
|
107
111
|
}
|
|
108
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Emit an ST_OnOff element following A.3 discipline: undefined → "",
|
|
115
|
+
* true → `<w:tag/>`, false → `<w:tag w:val="false"/>`.
|
|
116
|
+
*/
|
|
117
|
+
function buildOnOffEl(tag: string, value: boolean | undefined): string {
|
|
118
|
+
if (value === undefined) return "";
|
|
119
|
+
return value ? `<w:${tag}/>` : `<w:${tag} w:val="false"/>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Emit `<w:aliases w:val="A,B"/>` — ECMA-376 §17.7.4.2. Returns empty when
|
|
124
|
+
* `aliases` is undefined or the array is empty. Commas are preserved verbatim;
|
|
125
|
+
* callers are responsible for not embedding commas inside alias names (Word
|
|
126
|
+
* does not escape them either).
|
|
127
|
+
*/
|
|
128
|
+
function buildAliasesXml(aliases: string[] | undefined): string {
|
|
129
|
+
if (!aliases || aliases.length === 0) return "";
|
|
130
|
+
return `<w:aliases w:val="${escXml(aliases.join(","))}"/>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
function buildStyleNumPrXml(
|
|
110
134
|
numbering: NonNullable<StylesCatalog["paragraphs"][string]["numbering"]>,
|
|
111
135
|
): string {
|
|
@@ -172,15 +196,44 @@ function buildParagraphPropertiesXmlWithNumPr(
|
|
|
172
196
|
return body.length > 0 ? `<w:pPr>${body}</w:pPr>` : "";
|
|
173
197
|
}
|
|
174
198
|
|
|
199
|
+
function buildNumberingStyleXml(
|
|
200
|
+
style: NonNullable<StylesCatalog["numberingStyles"]>[string],
|
|
201
|
+
): string {
|
|
202
|
+
const defaultAttr = style.isDefault ? ` w:default="1"` : "";
|
|
203
|
+
const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
|
|
204
|
+
const aliasesEl = buildAliasesXml(style.aliases);
|
|
205
|
+
const basedOnEl = style.basedOn
|
|
206
|
+
? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
|
|
207
|
+
: "";
|
|
208
|
+
// Emit `<w:pPr><w:numPr><w:numId/></w:numPr></w:pPr>` when an instance ref
|
|
209
|
+
// was captured. Strip canonical "num:" prefix per serialize-numbering pattern.
|
|
210
|
+
let numPrXml = "";
|
|
211
|
+
if (style.numberingInstanceId) {
|
|
212
|
+
const rawId = style.numberingInstanceId.startsWith("num:")
|
|
213
|
+
? style.numberingInstanceId.slice(4)
|
|
214
|
+
: style.numberingInstanceId;
|
|
215
|
+
numPrXml = `<w:pPr><w:numPr><w:numId w:val="${escXml(rawId)}"/></w:numPr></w:pPr>`;
|
|
216
|
+
}
|
|
217
|
+
return (
|
|
218
|
+
`<w:style w:type="numbering" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
|
|
219
|
+
nameEl +
|
|
220
|
+
aliasesEl +
|
|
221
|
+
basedOnEl +
|
|
222
|
+
numPrXml +
|
|
223
|
+
`</w:style>`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
175
227
|
function buildCharacterStyleXml(
|
|
176
228
|
style: StylesCatalog["characters"][string],
|
|
177
229
|
): string {
|
|
178
230
|
const defaultAttr = style.isDefault ? ` w:default="1"` : "";
|
|
179
231
|
const nameEl = `<w:name w:val="${escXml(style.displayName)}"/>`;
|
|
232
|
+
const aliasesEl = buildAliasesXml(style.aliases);
|
|
180
233
|
const basedOnEl = style.basedOn
|
|
181
234
|
? `<w:basedOn w:val="${escXml(style.basedOn)}"/>`
|
|
182
235
|
: "";
|
|
183
|
-
// ECMA-376 §17.7 emit order: name → basedOn → link → ... → rPr
|
|
236
|
+
// ECMA-376 §17.7 emit order: name → aliases → basedOn → link → ... → rPr
|
|
184
237
|
const linkEl = style.linkedStyleId
|
|
185
238
|
? `<w:link w:val="${escXml(style.linkedStyleId)}"/>`
|
|
186
239
|
: "";
|
|
@@ -189,6 +242,7 @@ function buildCharacterStyleXml(
|
|
|
189
242
|
return (
|
|
190
243
|
`<w:style w:type="character" w:styleId="${escXml(style.styleId)}"${defaultAttr}>` +
|
|
191
244
|
nameEl +
|
|
245
|
+
aliasesEl +
|
|
192
246
|
basedOnEl +
|
|
193
247
|
linkEl +
|
|
194
248
|
rPrXml +
|
|
@@ -213,7 +267,11 @@ export function serializeStylesXml(catalog: StylesCatalog): string {
|
|
|
213
267
|
.map((style) => buildCharacterStyleXml(style))
|
|
214
268
|
.join("");
|
|
215
269
|
|
|
216
|
-
const
|
|
270
|
+
const numberingStyles = Object.values(catalog.numberingStyles ?? {})
|
|
271
|
+
.map((style) => buildNumberingStyleXml(style))
|
|
272
|
+
.join("");
|
|
273
|
+
|
|
274
|
+
const body = docDefaultsXml + paragraphStyles + characterStyles + numberingStyles;
|
|
217
275
|
|
|
218
276
|
return [
|
|
219
277
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { twip } from "./twip.ts";
|
|
2
2
|
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
3
|
+
import { emitPropertyGrabBag } from "../ooxml/property-grab-bag.ts";
|
|
4
|
+
import type { UnknownPropertyChild } from "../../model/canonical-document.ts";
|
|
3
5
|
|
|
4
6
|
interface TableWidthLike {
|
|
5
7
|
value: number;
|
|
@@ -66,6 +68,7 @@ interface TableFloatingPropertiesLike {
|
|
|
66
68
|
|
|
67
69
|
interface TablePropertiesLike {
|
|
68
70
|
propertiesXml?: string;
|
|
71
|
+
unknownPropertyChildren?: UnknownPropertyChild[];
|
|
69
72
|
styleId?: string;
|
|
70
73
|
width?: TableWidthLike;
|
|
71
74
|
alignment?: string;
|
|
@@ -83,6 +86,7 @@ interface TablePropertiesLike {
|
|
|
83
86
|
|
|
84
87
|
interface TableRowPropertiesLike {
|
|
85
88
|
propertiesXml?: string;
|
|
89
|
+
unknownPropertyChildren?: UnknownPropertyChild[];
|
|
86
90
|
gridBefore?: number;
|
|
87
91
|
widthBefore?: TableWidthLike;
|
|
88
92
|
gridAfter?: number;
|
|
@@ -97,6 +101,7 @@ interface TableRowPropertiesLike {
|
|
|
97
101
|
|
|
98
102
|
interface TableCellPropertiesLike {
|
|
99
103
|
propertiesXml?: string;
|
|
104
|
+
unknownPropertyChildren?: UnknownPropertyChild[];
|
|
100
105
|
width?: TableWidthLike;
|
|
101
106
|
gridSpan?: number;
|
|
102
107
|
verticalMerge?: "restart" | "continue";
|
|
@@ -166,6 +171,7 @@ export function serializeTablePropertiesXml(table: TablePropertiesLike): string
|
|
|
166
171
|
return mergePropertiesXml(
|
|
167
172
|
"w:tblPr",
|
|
168
173
|
table.propertiesXml,
|
|
174
|
+
table.unknownPropertyChildren,
|
|
169
175
|
buildTablePropertiesInnerXml(table),
|
|
170
176
|
TABLE_PROPERTY_STRIP_SPEC,
|
|
171
177
|
);
|
|
@@ -175,6 +181,7 @@ export function serializeTableRowPropertiesXml(row: TableRowPropertiesLike): str
|
|
|
175
181
|
return mergePropertiesXml(
|
|
176
182
|
"w:trPr",
|
|
177
183
|
row.propertiesXml,
|
|
184
|
+
row.unknownPropertyChildren,
|
|
178
185
|
buildTableRowPropertiesInnerXml(row),
|
|
179
186
|
ROW_PROPERTY_STRIP_SPEC,
|
|
180
187
|
);
|
|
@@ -184,21 +191,29 @@ export function serializeTableCellPropertiesXml(cell: TableCellPropertiesLike):
|
|
|
184
191
|
return mergePropertiesXml(
|
|
185
192
|
"w:tcPr",
|
|
186
193
|
cell.propertiesXml,
|
|
194
|
+
cell.unknownPropertyChildren,
|
|
187
195
|
buildTableCellPropertiesInnerXml(cell),
|
|
188
196
|
CELL_PROPERTY_STRIP_SPEC,
|
|
189
197
|
);
|
|
190
198
|
}
|
|
191
199
|
|
|
200
|
+
// Phase 7 Slice A — `unknownPropertyChildren` (typed grab-bag) is the
|
|
201
|
+
// preferred path. The legacy `existingXml` regex-strip fallback only fires
|
|
202
|
+
// for snapshots that pre-date the typed retrofit (no `unknownPropertyChildren`
|
|
203
|
+
// captured). Once persisted snapshots are migrated and tests no longer
|
|
204
|
+
// reference `propertiesXml`, the fallback branch and `stripKnownProperties`
|
|
205
|
+
// helper can be retired entirely.
|
|
192
206
|
function mergePropertiesXml(
|
|
193
207
|
tagName: "w:tblPr" | "w:trPr" | "w:tcPr",
|
|
194
208
|
existingXml: string | undefined,
|
|
209
|
+
unknownPropertyChildren: readonly UnknownPropertyChild[] | undefined,
|
|
195
210
|
supportedInnerXml: string,
|
|
196
211
|
stripSpec: PropertyStripSpec,
|
|
197
212
|
): string {
|
|
198
|
-
const preservedInnerXml =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
213
|
+
const preservedInnerXml =
|
|
214
|
+
unknownPropertyChildren !== undefined
|
|
215
|
+
? emitPropertyGrabBag(unknownPropertyChildren)
|
|
216
|
+
: stripKnownProperties(extractWrappedChildren(tagName, existingXml), stripSpec);
|
|
202
217
|
const mergedInnerXml = [supportedInnerXml, preservedInnerXml]
|
|
203
218
|
.filter((part) => part.length > 0)
|
|
204
219
|
.join("");
|
|
@@ -287,6 +287,7 @@ function normalizeTable(
|
|
|
287
287
|
type: "table",
|
|
288
288
|
...(table.styleId ? { styleId: table.styleId } : {}),
|
|
289
289
|
...(table.propertiesXml ? { propertiesXml: table.propertiesXml } : {}),
|
|
290
|
+
...(table.unknownPropertyChildren ? { unknownPropertyChildren: table.unknownPropertyChildren } : {}),
|
|
290
291
|
gridColumns: table.gridColumns,
|
|
291
292
|
rows,
|
|
292
293
|
...(table.width ? { width: table.width } : {}),
|
|
@@ -313,6 +314,7 @@ function normalizeTableRow(
|
|
|
313
314
|
return {
|
|
314
315
|
type: "table_row",
|
|
315
316
|
...(row.propertiesXml ? { propertiesXml: row.propertiesXml } : {}),
|
|
317
|
+
...(row.unknownPropertyChildren ? { unknownPropertyChildren: row.unknownPropertyChildren } : {}),
|
|
316
318
|
...(row.gridBefore !== undefined ? { gridBefore: row.gridBefore } : {}),
|
|
317
319
|
...(row.widthBefore ? { widthBefore: row.widthBefore } : {}),
|
|
318
320
|
...(row.gridAfter !== undefined ? { gridAfter: row.gridAfter } : {}),
|
|
@@ -343,6 +345,7 @@ function normalizeTableCell(
|
|
|
343
345
|
return {
|
|
344
346
|
type: "table_cell",
|
|
345
347
|
...(cell.propertiesXml ? { propertiesXml: cell.propertiesXml } : {}),
|
|
348
|
+
...(cell.unknownPropertyChildren ? { unknownPropertyChildren: cell.unknownPropertyChildren } : {}),
|
|
346
349
|
...(cell.gridSpan ? { gridSpan: cell.gridSpan } : {}),
|
|
347
350
|
...(cell.verticalMerge ? { verticalMerge: cell.verticalMerge } : {}),
|
|
348
351
|
...(cell.width ? { width: cell.width } : {}),
|
|
@@ -454,6 +457,10 @@ function normalizeInlineChildren(
|
|
|
454
457
|
normalized.push(normalizeImageNode(node, state));
|
|
455
458
|
state.cursor += 1;
|
|
456
459
|
break;
|
|
460
|
+
case "drawing_frame":
|
|
461
|
+
normalized.push(normalizeDrawingFrameNode(node, state));
|
|
462
|
+
state.cursor += 1;
|
|
463
|
+
break;
|
|
457
464
|
case "hyperlink":
|
|
458
465
|
normalized.push(normalizeHyperlink(node));
|
|
459
466
|
state.cursor += measureHyperlink(node);
|
|
@@ -617,6 +624,32 @@ function normalizeImageNode(
|
|
|
617
624
|
};
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
function normalizeDrawingFrameNode(
|
|
628
|
+
node: Extract<ParsedInlineNode, { type: "drawing_frame" }>,
|
|
629
|
+
state: NormalizationState,
|
|
630
|
+
): InlineNode {
|
|
631
|
+
if (node.content.type === "picture" && node.content.mediaId) {
|
|
632
|
+
const existingMediaItem = state.media.items[node.content.mediaId];
|
|
633
|
+
const packagePartName =
|
|
634
|
+
typeof node.content.packagePartName === "string" && node.content.packagePartName.length > 0
|
|
635
|
+
? node.content.packagePartName
|
|
636
|
+
: `/${node.content.mediaId.slice("media:".length)}`;
|
|
637
|
+
const filename = packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin";
|
|
638
|
+
state.media.items[node.content.mediaId] = {
|
|
639
|
+
mediaId: node.content.mediaId,
|
|
640
|
+
contentType: existingMediaItem?.contentType ?? "application/octet-stream",
|
|
641
|
+
filename,
|
|
642
|
+
packagePartName,
|
|
643
|
+
relationshipId: node.content.blipRef,
|
|
644
|
+
...(node.anchor.docPr?.descr ? { altText: node.anchor.docPr.descr } : {}),
|
|
645
|
+
widthEmu: node.anchor.extent.widthEmu,
|
|
646
|
+
heightEmu: node.anchor.extent.heightEmu,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return node;
|
|
651
|
+
}
|
|
652
|
+
|
|
620
653
|
/**
|
|
621
654
|
* Register a chart/SmartArt preview bitmap in the media catalog so the
|
|
622
655
|
* surface renderer can resolve `previewMediaId` → `previewSrc` the same
|