@beyondwork/docx-react-component 1.0.60 → 1.0.62
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/classify-embedding.ts +193 -0
- 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-object.ts +23 -0
- 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
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from "../../model/canonical-document.ts";
|
|
15
15
|
import type { LegacyFormFieldNode } from "../../model/canonical-document.ts";
|
|
16
16
|
import { resolveHighlightColor } from "./highlight-colors.ts";
|
|
17
|
+
import type { ParseDrawingOpts } from "./parse-drawing.ts";
|
|
17
18
|
import { parseFFDataFromFldChar } from "./parse-ffdata.ts";
|
|
18
19
|
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
19
20
|
import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
readTableStyleId,
|
|
50
51
|
readTableWidth,
|
|
51
52
|
} from "./parse-tables.ts";
|
|
53
|
+
import { parseDrawingFrame } from "./parse-drawing.ts";
|
|
52
54
|
import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
|
|
53
55
|
|
|
54
56
|
const TAB_ALIGN_VOCAB = new Set<TabStop["align"]>(["left", "center", "right", "decimal", "num", "bar", "clear"]);
|
|
@@ -67,6 +69,8 @@ export interface ParsedHeaderFooterDocument {
|
|
|
67
69
|
blocks: BlockNode[];
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
export type ParseHeaderFooterOpts = Omit<ParseDrawingOpts, "blockParser">;
|
|
73
|
+
|
|
70
74
|
// ---- XML node types (inline, no external dep) ----
|
|
71
75
|
|
|
72
76
|
interface XmlElementNode {
|
|
@@ -121,15 +125,21 @@ export function parseHeaderFooterReferences(
|
|
|
121
125
|
/**
|
|
122
126
|
* Parse a headerN.xml part (<w:hdr> root) into block nodes.
|
|
123
127
|
*/
|
|
124
|
-
export function parseHeaderXml(
|
|
125
|
-
|
|
128
|
+
export function parseHeaderXml(
|
|
129
|
+
xml: string,
|
|
130
|
+
opts: ParseHeaderFooterOpts = { relationships: [] },
|
|
131
|
+
): ParsedHeaderFooterDocument {
|
|
132
|
+
return parseHdrFtrXml(xml, "hdr", opts);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
136
|
* Parse a footerN.xml part (<w:ftr> root) into block nodes.
|
|
130
137
|
*/
|
|
131
|
-
export function parseFooterXml(
|
|
132
|
-
|
|
138
|
+
export function parseFooterXml(
|
|
139
|
+
xml: string,
|
|
140
|
+
opts: ParseHeaderFooterOpts = { relationships: [] },
|
|
141
|
+
): ParsedHeaderFooterDocument {
|
|
142
|
+
return parseHdrFtrXml(xml, "ftr", opts);
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
// ---- Internal helpers ----
|
|
@@ -213,6 +223,7 @@ function toHeaderFooterVariant(raw: string): HeaderFooterVariant {
|
|
|
213
223
|
function parseHdrFtrXml(
|
|
214
224
|
xml: string,
|
|
215
225
|
rootLocalName: "hdr" | "ftr",
|
|
226
|
+
opts: ParseHeaderFooterOpts = { relationships: [] },
|
|
216
227
|
): ParsedHeaderFooterDocument {
|
|
217
228
|
currentSourceXml = xml;
|
|
218
229
|
let root: XmlElementNode;
|
|
@@ -239,12 +250,12 @@ function parseHdrFtrXml(
|
|
|
239
250
|
const name = localName(child.name);
|
|
240
251
|
|
|
241
252
|
if (name === "p") {
|
|
242
|
-
blocks.push(parseParagraphElement(child, xml));
|
|
253
|
+
blocks.push(parseParagraphElement(child, xml, opts));
|
|
243
254
|
} else if (name === "tbl") {
|
|
244
255
|
// Simple tables (no revisions, fields, or nested tables) are promoted
|
|
245
256
|
// to supported-roundtrip; structurally risky tables stay opaque.
|
|
246
257
|
if (isSimpleSecondaryStoryTable(child)) {
|
|
247
|
-
blocks.push(parseSimpleTableElement(child, xml));
|
|
258
|
+
blocks.push(parseSimpleTableElement(child, xml, opts));
|
|
248
259
|
} else {
|
|
249
260
|
blocks.push({
|
|
250
261
|
type: "opaque_block",
|
|
@@ -267,7 +278,11 @@ function parseHdrFtrXml(
|
|
|
267
278
|
return { blocks };
|
|
268
279
|
}
|
|
269
280
|
|
|
270
|
-
function parseParagraphElement(
|
|
281
|
+
function parseParagraphElement(
|
|
282
|
+
pElement: XmlElementNode,
|
|
283
|
+
sourceXml: string,
|
|
284
|
+
opts: ParseHeaderFooterOpts,
|
|
285
|
+
): ParagraphNode {
|
|
271
286
|
let styleId: string | undefined;
|
|
272
287
|
let alignment: ParagraphNode["alignment"];
|
|
273
288
|
let spacing: ParagraphNode["spacing"];
|
|
@@ -295,9 +310,9 @@ function parseParagraphElement(pElement: XmlElementNode, sourceXml: string): Par
|
|
|
295
310
|
indentation = readParagraphIndentation(child);
|
|
296
311
|
tabStops = readParagraphTabStops(child);
|
|
297
312
|
} else if (name === "r") {
|
|
298
|
-
activeComplexField = appendRunNodes(child, children, activeComplexField, sourceXml);
|
|
313
|
+
activeComplexField = appendRunNodes(child, children, activeComplexField, sourceXml, opts);
|
|
299
314
|
} else if (name === "hyperlink") {
|
|
300
|
-
children.push(parseHyperlinkElement(child));
|
|
315
|
+
children.push(parseHyperlinkElement(child, opts));
|
|
301
316
|
} else if (name === "bookmarkStart" || name === "bookmarkEnd") {
|
|
302
317
|
children.push(parseBookmarkElement(child));
|
|
303
318
|
} else if (name === "fldSimple") {
|
|
@@ -350,6 +365,7 @@ function appendRunNodes(
|
|
|
350
365
|
nodes: InlineNode[],
|
|
351
366
|
activeComplexField: ActiveComplexField | null,
|
|
352
367
|
sourceXml: string,
|
|
368
|
+
opts: ParseHeaderFooterOpts,
|
|
353
369
|
): ActiveComplexField | null {
|
|
354
370
|
const marks: TextMark[] = parseRunProperties(rElement);
|
|
355
371
|
|
|
@@ -398,7 +414,7 @@ function appendRunNodes(
|
|
|
398
414
|
continue;
|
|
399
415
|
}
|
|
400
416
|
|
|
401
|
-
const inlineNode = parseRunChildNode(child, marks);
|
|
417
|
+
const inlineNode = parseRunChildNode(child, marks, opts);
|
|
402
418
|
if (!inlineNode) {
|
|
403
419
|
continue;
|
|
404
420
|
}
|
|
@@ -420,7 +436,10 @@ function appendRunNodes(
|
|
|
420
436
|
return activeComplexField;
|
|
421
437
|
}
|
|
422
438
|
|
|
423
|
-
function parseRunElement(
|
|
439
|
+
function parseRunElement(
|
|
440
|
+
rElement: XmlElementNode,
|
|
441
|
+
opts: ParseHeaderFooterOpts,
|
|
442
|
+
): InlineNode[] {
|
|
424
443
|
const nodes: InlineNode[] = [];
|
|
425
444
|
const marks: TextMark[] = parseRunProperties(rElement);
|
|
426
445
|
|
|
@@ -472,9 +491,9 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
|
472
491
|
pushFieldNode(nodes, child, "complex");
|
|
473
492
|
} else if (name === "drawing") {
|
|
474
493
|
const drawingXml = currentSourceXml.slice(child.start, child.end);
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
nodes.push(
|
|
494
|
+
const drawingResult = parseDrawingInlineNode(drawingXml, opts);
|
|
495
|
+
if (drawingResult) {
|
|
496
|
+
nodes.push(drawingResult);
|
|
478
497
|
}
|
|
479
498
|
} else if (name === "pict") {
|
|
480
499
|
const pictXml = currentSourceXml.slice(child.start, child.end);
|
|
@@ -483,17 +502,19 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
|
483
502
|
nodes.push(vmlResult);
|
|
484
503
|
}
|
|
485
504
|
} else if (name === "AlternateContent") {
|
|
505
|
+
const alternateXml = currentSourceXml.slice(child.start, child.end);
|
|
486
506
|
const drawingNode = findFirstDescendant(child, "drawing");
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
507
|
+
const legacyDrawingXml = drawingNode
|
|
508
|
+
? currentSourceXml.slice(drawingNode.start, drawingNode.end)
|
|
509
|
+
: undefined;
|
|
510
|
+
const alternateDrawingResult = parseDrawingInlineNode(
|
|
511
|
+
alternateXml,
|
|
512
|
+
opts,
|
|
513
|
+
legacyDrawingXml,
|
|
514
|
+
);
|
|
515
|
+
if (alternateDrawingResult) {
|
|
516
|
+
nodes.push(alternateDrawingResult);
|
|
517
|
+
continue;
|
|
497
518
|
}
|
|
498
519
|
const pictNode = findFirstDescendant(child, "pict");
|
|
499
520
|
if (pictNode) {
|
|
@@ -515,6 +536,7 @@ function parseRunElement(rElement: XmlElementNode): InlineNode[] {
|
|
|
515
536
|
function parseRunChildNode(
|
|
516
537
|
child: XmlElementNode,
|
|
517
538
|
marks: TextMark[],
|
|
539
|
+
opts: ParseHeaderFooterOpts,
|
|
518
540
|
): InlineNode | null {
|
|
519
541
|
const name = localName(child.name);
|
|
520
542
|
|
|
@@ -566,23 +588,21 @@ function parseRunChildNode(
|
|
|
566
588
|
}
|
|
567
589
|
if (name === "drawing") {
|
|
568
590
|
const drawingXml = currentSourceXml.slice(child.start, child.end);
|
|
569
|
-
return
|
|
591
|
+
return parseDrawingInlineNode(drawingXml, opts);
|
|
570
592
|
}
|
|
571
593
|
if (name === "pict") {
|
|
572
594
|
const pictXml = currentSourceXml.slice(child.start, child.end);
|
|
573
595
|
return parseVmlXml(pictXml);
|
|
574
596
|
}
|
|
575
597
|
if (name === "AlternateContent") {
|
|
598
|
+
const alternateXml = currentSourceXml.slice(child.start, child.end);
|
|
576
599
|
const drawingNode = findFirstDescendant(child, "drawing");
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
rawXml: currentSourceXml.slice(child.start, child.end),
|
|
584
|
-
};
|
|
585
|
-
}
|
|
600
|
+
const drawingXml = drawingNode
|
|
601
|
+
? currentSourceXml.slice(drawingNode.start, drawingNode.end)
|
|
602
|
+
: undefined;
|
|
603
|
+
const drawingResult = parseDrawingInlineNode(alternateXml, opts, drawingXml);
|
|
604
|
+
if (drawingResult) {
|
|
605
|
+
return drawingResult;
|
|
586
606
|
}
|
|
587
607
|
const pictNode = findFirstDescendant(child, "pict");
|
|
588
608
|
if (pictNode) {
|
|
@@ -600,7 +620,10 @@ function parseRunChildNode(
|
|
|
600
620
|
return null;
|
|
601
621
|
}
|
|
602
622
|
|
|
603
|
-
function parseHyperlinkElement(
|
|
623
|
+
function parseHyperlinkElement(
|
|
624
|
+
element: XmlElementNode,
|
|
625
|
+
opts: ParseHeaderFooterOpts,
|
|
626
|
+
): Extract<InlineNode, { type: "hyperlink" }> {
|
|
604
627
|
const href = element.attributes["w:anchor"]
|
|
605
628
|
? `#${element.attributes["w:anchor"]}`
|
|
606
629
|
: element.attributes["r:id"] ?? "relationship:unknown";
|
|
@@ -608,7 +631,7 @@ function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { t
|
|
|
608
631
|
|
|
609
632
|
for (const child of element.children) {
|
|
610
633
|
if (child.type === "element" && localName(child.name) === "r") {
|
|
611
|
-
for (const runChild of parseRunElement(child)) {
|
|
634
|
+
for (const runChild of parseRunElement(child, opts)) {
|
|
612
635
|
if (runChild.type === "text" || runChild.type === "hard_break" || runChild.type === "tab") {
|
|
613
636
|
children.push(runChild);
|
|
614
637
|
}
|
|
@@ -623,6 +646,46 @@ function parseHyperlinkElement(element: XmlElementNode): Extract<InlineNode, { t
|
|
|
623
646
|
};
|
|
624
647
|
}
|
|
625
648
|
|
|
649
|
+
function parseDrawingInlineNode(
|
|
650
|
+
rawXml: string,
|
|
651
|
+
opts: ParseHeaderFooterOpts,
|
|
652
|
+
legacyDrawingXml?: string,
|
|
653
|
+
): InlineNode | null {
|
|
654
|
+
try {
|
|
655
|
+
const frame = parseDrawingFrame(rawXml, {
|
|
656
|
+
...opts,
|
|
657
|
+
relationships: opts.relationships ?? [],
|
|
658
|
+
});
|
|
659
|
+
if (
|
|
660
|
+
frame &&
|
|
661
|
+
!(
|
|
662
|
+
frame.content.type === "shape" &&
|
|
663
|
+
frame.content.isTextBox
|
|
664
|
+
)
|
|
665
|
+
) {
|
|
666
|
+
return frame;
|
|
667
|
+
}
|
|
668
|
+
if (frame?.content.type !== "shape" || !frame.content.isTextBox) {
|
|
669
|
+
return frame;
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
// Fall through to the legacy shape path.
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const shapeXml = legacyDrawingXml ?? rawXml;
|
|
676
|
+
const legacyShape = parseShapeXml(shapeXml);
|
|
677
|
+
if (!legacyShape) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
if (legacyDrawingXml) {
|
|
681
|
+
return {
|
|
682
|
+
...legacyShape,
|
|
683
|
+
rawXml,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
return legacyShape;
|
|
687
|
+
}
|
|
688
|
+
|
|
626
689
|
function parseBookmarkElement(
|
|
627
690
|
element: XmlElementNode,
|
|
628
691
|
): Extract<InlineNode, { type: "bookmark_start" | "bookmark_end" }> {
|
|
@@ -958,7 +1021,11 @@ function isSafeSecondaryStoryFieldFamily(family: string): boolean {
|
|
|
958
1021
|
);
|
|
959
1022
|
}
|
|
960
1023
|
|
|
961
|
-
function parseSimpleTableElement(
|
|
1024
|
+
function parseSimpleTableElement(
|
|
1025
|
+
tblElement: XmlElementNode,
|
|
1026
|
+
sourceXml: string,
|
|
1027
|
+
opts: ParseHeaderFooterOpts,
|
|
1028
|
+
): TableNode {
|
|
962
1029
|
let gridColumns: number[] = [];
|
|
963
1030
|
const rows: TableRowNode[] = [];
|
|
964
1031
|
let propertiesXml: string | undefined;
|
|
@@ -998,7 +1065,7 @@ function parseSimpleTableElement(tblElement: XmlElementNode, sourceXml: string):
|
|
|
998
1065
|
} else if (name === "tblGrid") {
|
|
999
1066
|
gridColumns = readGridColumns(child);
|
|
1000
1067
|
} else if (name === "tr") {
|
|
1001
|
-
rows.push(parseSimpleTableRow(child, sourceXml));
|
|
1068
|
+
rows.push(parseSimpleTableRow(child, sourceXml, opts));
|
|
1002
1069
|
}
|
|
1003
1070
|
}
|
|
1004
1071
|
|
|
@@ -1027,7 +1094,11 @@ function readGridColumns(tblGrid: XmlElementNode): number[] {
|
|
|
1027
1094
|
return readSharedGridColumns(tblGrid);
|
|
1028
1095
|
}
|
|
1029
1096
|
|
|
1030
|
-
function parseSimpleTableRow(
|
|
1097
|
+
function parseSimpleTableRow(
|
|
1098
|
+
trElement: XmlElementNode,
|
|
1099
|
+
sourceXml: string,
|
|
1100
|
+
opts: ParseHeaderFooterOpts,
|
|
1101
|
+
): TableRowNode {
|
|
1031
1102
|
const cells: TableCellNode[] = [];
|
|
1032
1103
|
let propertiesXml: string | undefined;
|
|
1033
1104
|
let height: TableRowNode["height"];
|
|
@@ -1050,7 +1121,7 @@ function parseSimpleTableRow(trElement: XmlElementNode, sourceXml: string): Tabl
|
|
|
1050
1121
|
horizontalAlignment = readRowHorizontalAlignment(child);
|
|
1051
1122
|
cnfStyle = readRowCnfStyle(child);
|
|
1052
1123
|
} else if (name === "tc") {
|
|
1053
|
-
cells.push(parseSimpleTableCell(child, sourceXml));
|
|
1124
|
+
cells.push(parseSimpleTableCell(child, sourceXml, opts));
|
|
1054
1125
|
}
|
|
1055
1126
|
}
|
|
1056
1127
|
|
|
@@ -1067,7 +1138,11 @@ function parseSimpleTableRow(trElement: XmlElementNode, sourceXml: string): Tabl
|
|
|
1067
1138
|
};
|
|
1068
1139
|
}
|
|
1069
1140
|
|
|
1070
|
-
function parseSimpleTableCell(
|
|
1141
|
+
function parseSimpleTableCell(
|
|
1142
|
+
tcElement: XmlElementNode,
|
|
1143
|
+
sourceXml: string,
|
|
1144
|
+
opts: ParseHeaderFooterOpts,
|
|
1145
|
+
): TableCellNode {
|
|
1071
1146
|
const children: BlockNode[] = [];
|
|
1072
1147
|
let propertiesXml: string | undefined;
|
|
1073
1148
|
let gridSpan: number | undefined;
|
|
@@ -1107,7 +1182,7 @@ function parseSimpleTableCell(tcElement: XmlElementNode, sourceXml: string): Tab
|
|
|
1107
1182
|
margins = readCellMargins(child);
|
|
1108
1183
|
cnfStyle = readCellCnfStyle(child);
|
|
1109
1184
|
} else if (name === "p") {
|
|
1110
|
-
children.push(parseParagraphElement(child, sourceXml));
|
|
1185
|
+
children.push(parseParagraphElement(child, sourceXml, opts));
|
|
1111
1186
|
}
|
|
1112
1187
|
}
|
|
1113
1188
|
|
|
@@ -1150,15 +1150,15 @@ function parseBodyChild(
|
|
|
1150
1150
|
...(contextualSpacing !== undefined ? { contextualSpacing } : {}),
|
|
1151
1151
|
...(indentation ? { indentation } : {}),
|
|
1152
1152
|
...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
|
|
1153
|
-
...(keepNext ? { keepNext } : {}),
|
|
1154
|
-
...(keepLines ? { keepLines } : {}),
|
|
1153
|
+
...(keepNext !== undefined ? { keepNext } : {}),
|
|
1154
|
+
...(keepLines !== undefined ? { keepLines } : {}),
|
|
1155
1155
|
...(outlineLevel !== undefined ? { outlineLevel } : {}),
|
|
1156
|
-
...(pageBreakBefore ? { pageBreakBefore } : {}),
|
|
1157
|
-
...(widowControl ? { widowControl } : {}),
|
|
1156
|
+
...(pageBreakBefore !== undefined ? { pageBreakBefore } : {}),
|
|
1157
|
+
...(widowControl !== undefined ? { widowControl } : {}),
|
|
1158
1158
|
...(borders ? { borders } : {}),
|
|
1159
1159
|
...(shading ? { shading } : {}),
|
|
1160
|
-
...(bidi ? { bidi } : {}),
|
|
1161
|
-
...(suppressLineNumbers ? { suppressLineNumbers } : {}),
|
|
1160
|
+
...(bidi !== undefined ? { bidi } : {}),
|
|
1161
|
+
...(suppressLineNumbers !== undefined ? { suppressLineNumbers } : {}),
|
|
1162
1162
|
...(cnfStyle ? { cnfStyle } : {}),
|
|
1163
1163
|
...(wordExtensionIds ? { wordExtensionIds } : {}),
|
|
1164
1164
|
...(sectionProperties ? { sectionProperties } : {}),
|
|
@@ -1848,7 +1848,7 @@ function tableRequiresOpaquePreservation(rawXml: string): boolean {
|
|
|
1848
1848
|
// nested tables, floating images, VML preview atoms, and bounded field
|
|
1849
1849
|
// families already owned by the current field slice. Risky table-local
|
|
1850
1850
|
// semantics still fail closed to preserve-only.
|
|
1851
|
-
if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag
|
|
1851
|
+
if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag)\b/.test(rawXml)) {
|
|
1852
1852
|
return true;
|
|
1853
1853
|
}
|
|
1854
1854
|
|
|
@@ -2103,7 +2103,7 @@ function readOnOffParagraphProperty(node: XmlElementNode, name: string): boolean
|
|
|
2103
2103
|
);
|
|
2104
2104
|
if (!propNode) return undefined;
|
|
2105
2105
|
const val = (propNode.attributes["w:val"] ?? propNode.attributes.val ?? "true").toLowerCase();
|
|
2106
|
-
return val !== "false" && val !== "0" && val !== "off"
|
|
2106
|
+
return val !== "false" && val !== "0" && val !== "off";
|
|
2107
2107
|
}
|
|
2108
2108
|
|
|
2109
2109
|
function readOptionalOnOffParagraphProperty(
|
|
@@ -2172,9 +2172,21 @@ function readParagraphShading(node: XmlElementNode): ParagraphShading | undefine
|
|
|
2172
2172
|
const fill = shadingNode.attributes["w:fill"] ?? shadingNode.attributes.fill;
|
|
2173
2173
|
const color = shadingNode.attributes["w:color"] ?? shadingNode.attributes.color;
|
|
2174
2174
|
const val = shadingNode.attributes["w:val"] ?? shadingNode.attributes.val;
|
|
2175
|
+
const themeFill = shadingNode.attributes["w:themeFill"] ?? shadingNode.attributes.themeFill;
|
|
2176
|
+
const themeFillTint = shadingNode.attributes["w:themeFillTint"] ?? shadingNode.attributes.themeFillTint;
|
|
2177
|
+
const themeFillShade = shadingNode.attributes["w:themeFillShade"] ?? shadingNode.attributes.themeFillShade;
|
|
2178
|
+
const themeColor = shadingNode.attributes["w:themeColor"] ?? shadingNode.attributes.themeColor;
|
|
2179
|
+
const themeColorTint = shadingNode.attributes["w:themeColorTint"] ?? shadingNode.attributes.themeColorTint;
|
|
2180
|
+
const themeColorShade = shadingNode.attributes["w:themeColorShade"] ?? shadingNode.attributes.themeColorShade;
|
|
2175
2181
|
if (fill) shading.fill = fill;
|
|
2176
2182
|
if (color) shading.color = color;
|
|
2177
2183
|
if (val) shading.val = val;
|
|
2184
|
+
if (themeFill) shading.themeFill = themeFill;
|
|
2185
|
+
if (themeFillTint) shading.themeFillTint = themeFillTint;
|
|
2186
|
+
if (themeFillShade) shading.themeFillShade = themeFillShade;
|
|
2187
|
+
if (themeColor) shading.themeColor = themeColor;
|
|
2188
|
+
if (themeColorTint) shading.themeColorTint = themeColorTint;
|
|
2189
|
+
if (themeColorShade) shading.themeColorShade = themeColorShade;
|
|
2178
2190
|
return Object.keys(shading).length > 0 ? shading : undefined;
|
|
2179
2191
|
}
|
|
2180
2192
|
|
|
@@ -22,6 +22,7 @@ import type { OleEmbedNode } from "../../model/canonical-document.ts";
|
|
|
22
22
|
import type { OpcRelationship } from "./part-manifest.ts";
|
|
23
23
|
import type { XmlElementNode } from "./xml-element.ts";
|
|
24
24
|
import { resolveOleRelationship } from "./parse-ole-relationship.ts";
|
|
25
|
+
import { classifyEmbedding } from "./classify-embedding.ts";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Parse a `<w:object>` element into an `OleEmbedNode` if it contains an
|
|
@@ -64,6 +65,28 @@ export function parseObject(
|
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// hotfix/ole-digestibility-guard — classify the embedding before
|
|
69
|
+
// constructing a canonical node. When the classifier returns
|
|
70
|
+
// "store-only" (nested Word docs, PDF OLE, Excel/PowerPoint
|
|
71
|
+
// embeddings, unknown ProgIDs), return undefined so the caller's
|
|
72
|
+
// existing opaque-fragment fallback preserves both <w:object> XML
|
|
73
|
+
// and its r:id verbatim. Binary preservation is unaffected —
|
|
74
|
+
// collectPreservedPackageParts indexes embedding parts by path, not
|
|
75
|
+
// by canonical-tree reference.
|
|
76
|
+
//
|
|
77
|
+
// TODO(refactor/01 Step 6-7): replace this skip-construction with
|
|
78
|
+
// extraction + offload via hostAdapter.storeEmbeddedDocument?. See
|
|
79
|
+
// docs/architecture/01-package-session.md §P8 + docs/plans/refactor/
|
|
80
|
+
// 01-package-session.md Steps 6-7.
|
|
81
|
+
const kind = classifyEmbedding({
|
|
82
|
+
progId,
|
|
83
|
+
relationshipType: resolved.relationshipType,
|
|
84
|
+
targetPath: resolved.target,
|
|
85
|
+
});
|
|
86
|
+
if (kind !== "digestible") {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
const metadata: OleEmbedNode["metadata"] = {};
|
|
68
91
|
if (resolved.originalFilename) {
|
|
69
92
|
metadata.originalFilename = resolved.originalFilename;
|
|
@@ -132,11 +132,35 @@ function readShading(node: XmlElementNode): ParagraphShading | undefined {
|
|
|
132
132
|
const val = node.attributes["w:val"] ?? node.attributes.val;
|
|
133
133
|
const fill = node.attributes["w:fill"] ?? node.attributes.fill;
|
|
134
134
|
const color = node.attributes["w:color"] ?? node.attributes.color;
|
|
135
|
-
|
|
135
|
+
const themeFill = node.attributes["w:themeFill"] ?? node.attributes.themeFill;
|
|
136
|
+
const themeFillTint = node.attributes["w:themeFillTint"] ?? node.attributes.themeFillTint;
|
|
137
|
+
const themeFillShade = node.attributes["w:themeFillShade"] ?? node.attributes.themeFillShade;
|
|
138
|
+
const themeColor = node.attributes["w:themeColor"] ?? node.attributes.themeColor;
|
|
139
|
+
const themeColorTint = node.attributes["w:themeColorTint"] ?? node.attributes.themeColorTint;
|
|
140
|
+
const themeColorShade = node.attributes["w:themeColorShade"] ?? node.attributes.themeColorShade;
|
|
141
|
+
if (
|
|
142
|
+
!val &&
|
|
143
|
+
!fill &&
|
|
144
|
+
!color &&
|
|
145
|
+
!themeFill &&
|
|
146
|
+
!themeFillTint &&
|
|
147
|
+
!themeFillShade &&
|
|
148
|
+
!themeColor &&
|
|
149
|
+
!themeColorTint &&
|
|
150
|
+
!themeColorShade
|
|
151
|
+
) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
136
154
|
return {
|
|
137
155
|
...(val ? { val } : {}),
|
|
138
156
|
...(fill ? { fill } : {}),
|
|
139
157
|
...(color ? { color } : {}),
|
|
158
|
+
...(themeFill ? { themeFill } : {}),
|
|
159
|
+
...(themeFillTint ? { themeFillTint } : {}),
|
|
160
|
+
...(themeFillShade ? { themeFillShade } : {}),
|
|
161
|
+
...(themeColor ? { themeColor } : {}),
|
|
162
|
+
...(themeColorTint ? { themeColorTint } : {}),
|
|
163
|
+
...(themeColorShade ? { themeColorShade } : {}),
|
|
140
164
|
};
|
|
141
165
|
}
|
|
142
166
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ClrSchemeMapping,
|
|
4
4
|
ClrSchemeMappingSlot,
|
|
5
5
|
DocumentSettings,
|
|
6
|
+
FootnoteProperties,
|
|
6
7
|
ThemeColorSlot,
|
|
7
8
|
} from "../../model/canonical-document.ts";
|
|
8
9
|
import { parseXml } from "./xml-parser.ts";
|
|
@@ -51,6 +52,13 @@ export function parseSettingsXml(xml: string): DocumentSettings {
|
|
|
51
52
|
const compatPartition = compat ? partitionCompat(compat) : undefined;
|
|
52
53
|
const rootCompatFlags = readRootCompatFlags(settingsElement);
|
|
53
54
|
const themeFontLangElement = findChildElementOptional(settingsElement, "themeFontLang");
|
|
55
|
+
const defaultTabStop = readDefaultTabStop(settingsElement);
|
|
56
|
+
const footnotePr = readFootnoteLikeProperties(
|
|
57
|
+
findChildElementOptional(settingsElement, "footnotePr"),
|
|
58
|
+
);
|
|
59
|
+
const endnotePr = readFootnoteLikeProperties(
|
|
60
|
+
findChildElementOptional(settingsElement, "endnotePr"),
|
|
61
|
+
);
|
|
54
62
|
const clrSchemeMapping = parseClrSchemeMapping(settingsElement);
|
|
55
63
|
const unmodelled = readUnmodelledSettingsChildren(settingsElement);
|
|
56
64
|
|
|
@@ -71,6 +79,9 @@ export function parseSettingsXml(xml: string): DocumentSettings {
|
|
|
71
79
|
...(themeFontLangElement
|
|
72
80
|
? { themeFontLang: { ...themeFontLangElement.attributes } }
|
|
73
81
|
: {}),
|
|
82
|
+
...(defaultTabStop !== undefined ? { defaultTabStop } : {}),
|
|
83
|
+
...(footnotePr ? { footnotePr } : {}),
|
|
84
|
+
...(endnotePr ? { endnotePr } : {}),
|
|
74
85
|
...(clrSchemeMapping !== undefined ? { clrSchemeMapping } : {}),
|
|
75
86
|
...(unmodelled.length > 0 ? { unmodelledSettingsChildren: unmodelled } : {}),
|
|
76
87
|
};
|
|
@@ -86,6 +97,9 @@ const MODELLED_SETTINGS_CHILD_NAMES = new Set<string>([
|
|
|
86
97
|
"zoom",
|
|
87
98
|
"compat",
|
|
88
99
|
"themeFontLang",
|
|
100
|
+
"defaultTabStop",
|
|
101
|
+
"footnotePr",
|
|
102
|
+
"endnotePr",
|
|
89
103
|
"clrSchemeMapping",
|
|
90
104
|
]);
|
|
91
105
|
|
|
@@ -130,6 +144,83 @@ function readRootCompatFlags(
|
|
|
130
144
|
return flags;
|
|
131
145
|
}
|
|
132
146
|
|
|
147
|
+
function readDefaultTabStop(
|
|
148
|
+
settingsElement: XmlElementNode,
|
|
149
|
+
): number | undefined {
|
|
150
|
+
const element = findChildElementOptional(settingsElement, "defaultTabStop");
|
|
151
|
+
if (!element) return undefined;
|
|
152
|
+
const rawValue = element.attributes["w:val"] ?? element.attributes.val;
|
|
153
|
+
if (!rawValue) return undefined;
|
|
154
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
155
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readFootnoteLikeProperties(
|
|
159
|
+
element: XmlElementNode | undefined,
|
|
160
|
+
): FootnoteProperties | undefined {
|
|
161
|
+
if (!element) return undefined;
|
|
162
|
+
|
|
163
|
+
const result: FootnoteProperties = {};
|
|
164
|
+
for (const child of element.children) {
|
|
165
|
+
if (child.type !== "element") continue;
|
|
166
|
+
const name = localName(child.name);
|
|
167
|
+
const val = child.attributes["w:val"] ?? child.attributes.val;
|
|
168
|
+
|
|
169
|
+
if (name === "pos") {
|
|
170
|
+
if (
|
|
171
|
+
val === "pageBottom" ||
|
|
172
|
+
val === "beneathText" ||
|
|
173
|
+
val === "sectEnd" ||
|
|
174
|
+
val === "docEnd"
|
|
175
|
+
) {
|
|
176
|
+
result.pos = val;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (name === "numFmt") {
|
|
182
|
+
if (
|
|
183
|
+
val === "decimal" ||
|
|
184
|
+
val === "upperRoman" ||
|
|
185
|
+
val === "lowerRoman" ||
|
|
186
|
+
val === "upperLetter" ||
|
|
187
|
+
val === "lowerLetter" ||
|
|
188
|
+
val === "ordinal" ||
|
|
189
|
+
val === "cardinalText" ||
|
|
190
|
+
val === "ordinalText" ||
|
|
191
|
+
val === "hex" ||
|
|
192
|
+
val === "chicago" ||
|
|
193
|
+
val === "bullet" ||
|
|
194
|
+
val === "ideographDigital" ||
|
|
195
|
+
val === "japaneseCounting" ||
|
|
196
|
+
val === "arabicAbjad" ||
|
|
197
|
+
val === "arabicAlpha" ||
|
|
198
|
+
val === "none"
|
|
199
|
+
) {
|
|
200
|
+
result.numFmt = val;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (name === "numStart") {
|
|
206
|
+
const parsed = Number.parseInt(val ?? "", 10);
|
|
207
|
+
if (Number.isFinite(parsed)) {
|
|
208
|
+
result.numStart = Math.max(1, parsed);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
name === "numRestart" &&
|
|
215
|
+
(val === "continuous" || val === "eachSect" || val === "eachPage")
|
|
216
|
+
) {
|
|
217
|
+
result.numRestart = val;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
133
224
|
interface CompatPartition {
|
|
134
225
|
compatSettings: CompatSetting[];
|
|
135
226
|
compatFlags: Record<string, boolean>;
|
|
@@ -218,4 +309,3 @@ function readZoomLevel(
|
|
|
218
309
|
|
|
219
310
|
return { zoomLevel: parsed };
|
|
220
311
|
}
|
|
221
|
-
|
|
@@ -346,14 +346,20 @@ export interface FootnoteDefinition {
|
|
|
346
346
|
* footnote/endnote entries with `w:type="separator"` and
|
|
347
347
|
* `w:type="continuationSeparator"`.
|
|
348
348
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
349
|
+
* The canonical model keeps both the legacy run-only payload and the full
|
|
350
|
+
* first-paragraph XML. The paragraph form closes round-trip fidelity for
|
|
351
|
+
* imported separator paragraph properties, while the run form remains for
|
|
352
|
+
* back-compat with the current note-separator runtime helpers.
|
|
351
353
|
*/
|
|
352
354
|
export interface FootnoteSeparators {
|
|
353
355
|
/** Raw XML of the <w:r> children inside the separator paragraph. */
|
|
354
356
|
separatorContent?: string;
|
|
357
|
+
/** Full XML of the first separator paragraph. */
|
|
358
|
+
separatorParagraphXml?: string;
|
|
355
359
|
/** Raw XML of the <w:r> children inside the continuationSeparator paragraph. */
|
|
356
360
|
continuationSeparatorContent?: string;
|
|
361
|
+
/** Full XML of the first continuation-separator paragraph. */
|
|
362
|
+
continuationSeparatorParagraphXml?: string;
|
|
357
363
|
}
|
|
358
364
|
|
|
359
365
|
export interface FootnoteCollection {
|
|
@@ -399,6 +405,22 @@ export interface CompatSetting {
|
|
|
399
405
|
export interface DocumentSettings {
|
|
400
406
|
evenAndOddHeaders?: boolean;
|
|
401
407
|
zoomLevel?: "pageWidth" | "onePage" | number;
|
|
408
|
+
/**
|
|
409
|
+
* Document-wide default tab stop interval from `<w:defaultTabStop w:val>`.
|
|
410
|
+
* Value is stored in twips and feeds layout measurement whenever a paragraph
|
|
411
|
+
* does not declare an explicit next tab stop.
|
|
412
|
+
*/
|
|
413
|
+
defaultTabStop?: number;
|
|
414
|
+
/**
|
|
415
|
+
* Settings-level default footnote configuration from `<w:footnotePr>`.
|
|
416
|
+
* Section-level `SectionProperties.footnotePr` overrides this when present.
|
|
417
|
+
*/
|
|
418
|
+
footnotePr?: FootnoteProperties;
|
|
419
|
+
/**
|
|
420
|
+
* Settings-level default endnote configuration from `<w:endnotePr>`.
|
|
421
|
+
* Section-level `SectionProperties.endnotePr` overrides this when present.
|
|
422
|
+
*/
|
|
423
|
+
endnotePr?: EndnoteProperties;
|
|
402
424
|
/**
|
|
403
425
|
* Ordered list of <w:compatSetting> entries inside <w:compat>. Insertion
|
|
404
426
|
* order is preserved for serializer diff stability.
|
|
@@ -606,6 +628,18 @@ export interface ParagraphShading {
|
|
|
606
628
|
fill?: string;
|
|
607
629
|
color?: string;
|
|
608
630
|
val?: string;
|
|
631
|
+
/**
|
|
632
|
+
* Theme shading references (§17.3.5). When `themeFill` is set and `fill`
|
|
633
|
+
* is absent or `"auto"`, render-time shading resolves through the theme
|
|
634
|
+
* color resolver. The raw theme attrs remain on the canonical model so
|
|
635
|
+
* export can round-trip the original `<w:shd>` byte-for-byte.
|
|
636
|
+
*/
|
|
637
|
+
themeFill?: string;
|
|
638
|
+
themeFillTint?: string;
|
|
639
|
+
themeFillShade?: string;
|
|
640
|
+
themeColor?: string;
|
|
641
|
+
themeColorTint?: string;
|
|
642
|
+
themeColorShade?: string;
|
|
609
643
|
}
|
|
610
644
|
|
|
611
645
|
/** Body of an OOXML `<w:rPr>` (run properties). All fields optional; absence = "not specified at this level". */
|