@beyondwork/docx-react-component 1.0.58 → 1.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +978 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +476 -34
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +5 -8
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 7c Slice 7c.1 — `<w:object>` parser (OLE preserve-only).
|
|
3
|
+
*
|
|
4
|
+
* Extracts OLE embedded objects from a `<w:object>` subtree and returns
|
|
5
|
+
* a typed `OleEmbedNode`. Non-OLE `<w:object>` elements (legacy VML
|
|
6
|
+
* drawings wrapped in `<w:object>` without `<o:OLEObject>`) return
|
|
7
|
+
* `undefined` so the caller can fall through to the generic
|
|
8
|
+
* opaque-fragment path in `src/preservation/store.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Preservation contract (see `docs/wiki/images-and-media.md`):
|
|
11
|
+
* - `rawXml` is the authoritative round-trip source and is re-emitted
|
|
12
|
+
* verbatim by the serializer.
|
|
13
|
+
* - The binary embedding part (e.g. `/word/embeddings/oleObject1.bin`)
|
|
14
|
+
* is preserved automatically via `collectPreservedPackageParts` in
|
|
15
|
+
* `src/io/docx-session.ts` — the relationship id stored on the node
|
|
16
|
+
* is the handle.
|
|
17
|
+
* - Placeholder chrome (badge with ProgID + filename) is Lane 6b's
|
|
18
|
+
* responsibility post-v2.0; this module emits only data.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { OleEmbedNode } from "../../model/canonical-document.ts";
|
|
22
|
+
import type { OpcRelationship } from "./part-manifest.ts";
|
|
23
|
+
import type { XmlElementNode } from "./xml-element.ts";
|
|
24
|
+
import { resolveOleRelationship } from "./parse-ole-relationship.ts";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `<w:object>` element into an `OleEmbedNode` if it contains an
|
|
28
|
+
* `<o:OLEObject>` child with a resolvable relationship. Returns
|
|
29
|
+
* `undefined` when the element is not an OLE embedding — the caller
|
|
30
|
+
* should then fall back to opaque-fragment preservation.
|
|
31
|
+
*
|
|
32
|
+
* `rawXml` must be the full verbatim source of the `<w:object>` element
|
|
33
|
+
* (the caller typically slices `sourceXml.slice(node.start, node.end)`).
|
|
34
|
+
* It is not recomputed from the parsed tree because the grab-bag
|
|
35
|
+
* reconstruction is not byte-identical and this slice preserves rawXml
|
|
36
|
+
* bytes as the authoritative round-trip source.
|
|
37
|
+
*
|
|
38
|
+
* The canonical id is derived from the relationship id + an optional
|
|
39
|
+
* occurrence counter so callers that encounter multiple `<w:object>`
|
|
40
|
+
* elements sharing a relationship id still get stable, unique node
|
|
41
|
+
* identifiers. When `occurrenceIndex` is omitted the id is simply
|
|
42
|
+
* `ole-embed-${relationshipId}`.
|
|
43
|
+
*/
|
|
44
|
+
export function parseObject(
|
|
45
|
+
node: XmlElementNode,
|
|
46
|
+
rawXml: string,
|
|
47
|
+
relationships: readonly OpcRelationship[],
|
|
48
|
+
occurrenceIndex?: number,
|
|
49
|
+
): OleEmbedNode | undefined {
|
|
50
|
+
const oleChild = findDirectChild(node, "OLEObject");
|
|
51
|
+
if (!oleChild) return undefined;
|
|
52
|
+
|
|
53
|
+
const progId = readOleAttribute(oleChild, "ProgID");
|
|
54
|
+
const classId = readOleAttribute(oleChild, "ClassID");
|
|
55
|
+
const shapeId = readOleAttribute(oleChild, "ShapeID");
|
|
56
|
+
const relationshipIdRaw = readRelationshipIdAttribute(oleChild);
|
|
57
|
+
|
|
58
|
+
const resolved = resolveOleRelationship(relationshipIdRaw, relationships);
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
// No resolvable internal relationship — treat as non-OLE. The
|
|
61
|
+
// generic opaque-fragment path preserves the subtree via rawXml;
|
|
62
|
+
// promoting to OleEmbedNode would lose the handoff semantics the
|
|
63
|
+
// post-v2.0 chrome slice depends on.
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const metadata: OleEmbedNode["metadata"] = {};
|
|
68
|
+
if (resolved.originalFilename) {
|
|
69
|
+
metadata.originalFilename = resolved.originalFilename;
|
|
70
|
+
}
|
|
71
|
+
if (classId) {
|
|
72
|
+
metadata.classId = classId;
|
|
73
|
+
}
|
|
74
|
+
if (shapeId) {
|
|
75
|
+
metadata.shapeId = shapeId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const id =
|
|
79
|
+
typeof occurrenceIndex === "number"
|
|
80
|
+
? `ole-embed-${resolved.relationshipId}-${occurrenceIndex}`
|
|
81
|
+
: `ole-embed-${resolved.relationshipId}`;
|
|
82
|
+
|
|
83
|
+
const result: OleEmbedNode = {
|
|
84
|
+
type: "ole_embed",
|
|
85
|
+
id,
|
|
86
|
+
embedType: "oleObject",
|
|
87
|
+
relationshipId: resolved.relationshipId,
|
|
88
|
+
metadata,
|
|
89
|
+
rawXml,
|
|
90
|
+
};
|
|
91
|
+
if (progId) {
|
|
92
|
+
result.progId = progId;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findDirectChild(
|
|
98
|
+
node: XmlElementNode,
|
|
99
|
+
localName: string,
|
|
100
|
+
): XmlElementNode | undefined {
|
|
101
|
+
for (const child of node.children) {
|
|
102
|
+
if (child.type !== "element") continue;
|
|
103
|
+
const colon = child.name.indexOf(":");
|
|
104
|
+
const local = colon < 0 ? child.name : child.name.slice(colon + 1);
|
|
105
|
+
if (local === localName) return child;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read a `<o:OLEObject>` attribute by local name. OLE attributes use
|
|
112
|
+
* the VML / Office-drawing namespaces which may appear bare, prefixed
|
|
113
|
+
* as `o:`, or with another prefix depending on the source document.
|
|
114
|
+
*/
|
|
115
|
+
function readOleAttribute(
|
|
116
|
+
node: XmlElementNode,
|
|
117
|
+
localName: string,
|
|
118
|
+
): string | undefined {
|
|
119
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
120
|
+
const colon = key.indexOf(":");
|
|
121
|
+
const local = colon < 0 ? key : key.slice(colon + 1);
|
|
122
|
+
if (local === localName) return value;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read the `r:id` attribute on `<o:OLEObject>` that references the
|
|
129
|
+
* embedding relationship. The attribute lives in the Office-
|
|
130
|
+
* relationships namespace and must have an explicit prefix on
|
|
131
|
+
* `<o:OLEObject>` — a bare `id` is `ObjectID`-style bookkeeping, not a
|
|
132
|
+
* relationship reference. We accept `r:id` or any other prefix pointing
|
|
133
|
+
* at the same namespace URI, but reject bare `id` so we don't
|
|
134
|
+
* misinterpret non-relationship attributes.
|
|
135
|
+
*/
|
|
136
|
+
function readRelationshipIdAttribute(
|
|
137
|
+
node: XmlElementNode,
|
|
138
|
+
): string | undefined {
|
|
139
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
140
|
+
const colon = key.indexOf(":");
|
|
141
|
+
if (colon < 0) continue; // skip bare `id` / `ObjectID` / `ShapeID`
|
|
142
|
+
if (key.slice(colon + 1) === "id") {
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane 7c Slice 7c.1 — OLE embedded-object relationship resolver.
|
|
3
|
+
*
|
|
4
|
+
* Resolves an `r:id` referenced from an `<o:OLEObject>` element against
|
|
5
|
+
* the parent part's relationship list. The binary package part (e.g.
|
|
6
|
+
* `/word/embeddings/oleObject1.bin`) is preserved automatically by the
|
|
7
|
+
* OPC reader → `collectPreservedPackageParts` pipeline in
|
|
8
|
+
* `src/io/docx-session.ts` — this helper only reports where the binary
|
|
9
|
+
* lives so parse-object can populate metadata on the canonical
|
|
10
|
+
* `OleEmbedNode`.
|
|
11
|
+
*
|
|
12
|
+
* Non-OLE `<w:object>` elements (legacy VML-wrapped objects without
|
|
13
|
+
* `<o:OLEObject>`) must NOT use this helper; they flow through the
|
|
14
|
+
* generic opaque-fragment path.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { OpcRelationship } from "./part-manifest.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* OLE-embedding relationship target classes defined by OOXML. Used only
|
|
21
|
+
* for logging / validator wording today; the preservation contract is
|
|
22
|
+
* insensitive to the exact relationship type as long as the binary part
|
|
23
|
+
* survives.
|
|
24
|
+
*/
|
|
25
|
+
export const OLE_RELATIONSHIP_TYPES: ReadonlySet<string> = new Set([
|
|
26
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject",
|
|
27
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/package",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export interface ResolvedOleRelationship {
|
|
31
|
+
relationshipId: string;
|
|
32
|
+
/**
|
|
33
|
+
* Relative target as declared in the relationship XML (e.g.
|
|
34
|
+
* `"embeddings/oleObject1.bin"` or `"embeddings/Microsoft_Excel_Worksheet.xlsx"`).
|
|
35
|
+
* The absolute package path is resolved downstream by
|
|
36
|
+
* `resolveRelationshipTarget` against the source part's folder.
|
|
37
|
+
*/
|
|
38
|
+
target: string;
|
|
39
|
+
relationshipType: string;
|
|
40
|
+
/**
|
|
41
|
+
* Extracted filename from the target path (without folder prefix). Used
|
|
42
|
+
* as a human-facing label in preserve-only diagnostics. Undefined when
|
|
43
|
+
* the target is unparseable.
|
|
44
|
+
*/
|
|
45
|
+
originalFilename?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Look up the relationship record for an r:id referenced by an
|
|
50
|
+
* `<o:OLEObject>` element. Returns undefined when the id is absent or
|
|
51
|
+
* the referenced record does not look like an OLE embedding — callers
|
|
52
|
+
* treat that as "fall through to generic opaque-fragment preservation".
|
|
53
|
+
*
|
|
54
|
+
* This helper is intentionally read-only: it does not mutate
|
|
55
|
+
* relationship records or allocate preservation store entries. Binary
|
|
56
|
+
* preservation is a separate pipeline stage.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveOleRelationship(
|
|
59
|
+
relationshipId: string | undefined,
|
|
60
|
+
relationships: readonly OpcRelationship[],
|
|
61
|
+
): ResolvedOleRelationship | undefined {
|
|
62
|
+
if (!relationshipId) return undefined;
|
|
63
|
+
const record = relationships.find((rel) => rel.id === relationshipId);
|
|
64
|
+
if (!record) return undefined;
|
|
65
|
+
if (record.targetMode !== "internal") return undefined;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
relationshipId: record.id,
|
|
69
|
+
target: record.target,
|
|
70
|
+
relationshipType: record.type,
|
|
71
|
+
originalFilename: extractFilename(record.target),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractFilename(target: string): string | undefined {
|
|
76
|
+
if (!target) return undefined;
|
|
77
|
+
const slashIdx = target.lastIndexOf("/");
|
|
78
|
+
const backslashIdx = target.lastIndexOf("\\");
|
|
79
|
+
const idx = Math.max(slashIdx, backslashIdx);
|
|
80
|
+
const name = idx >= 0 ? target.slice(idx + 1) : target;
|
|
81
|
+
return name.length > 0 ? name : undefined;
|
|
82
|
+
}
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
type PropertyGrabBagDescriptor,
|
|
24
24
|
} from "./property-grab-bag.ts";
|
|
25
25
|
|
|
26
|
+
const TAB_ALIGN_VOCAB = new Set<TabStop["align"]>(["left", "center", "right", "decimal", "num", "bar", "clear"]);
|
|
27
|
+
const TAB_LEADER_VOCAB = new Set<TabStop["leader"]>(["none", "dot", "hyphen", "underscore", "heavy", "middleDot"]);
|
|
28
|
+
|
|
26
29
|
/**
|
|
27
30
|
* Modelled direct children of `<w:pPr>` that `readParagraphProperties` below
|
|
28
31
|
* dispatches into typed fields on `CanonicalParagraphFormatting`. Anything
|
|
@@ -92,16 +95,10 @@ function readTabStops(node: XmlElementNode): TabStop[] | undefined {
|
|
|
92
95
|
if (!Number.isFinite(pos)) continue;
|
|
93
96
|
const val = (c.attributes["w:val"] ?? c.attributes.val ?? "left").toLowerCase();
|
|
94
97
|
const leader = (c.attributes["w:leader"] ?? c.attributes.leader ?? "none").toLowerCase();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const mappedLeader: TabStop["leader"] | undefined =
|
|
100
|
-
leader === "dot" || leader === "hyphen" || leader === "underscore" || leader === "heavy"
|
|
101
|
-
? (leader as TabStop["leader"])
|
|
102
|
-
: leader === "middledot"
|
|
103
|
-
? "middleDot"
|
|
104
|
-
: undefined;
|
|
98
|
+
const alignCandidate = val as TabStop["align"];
|
|
99
|
+
const align: TabStop["align"] = TAB_ALIGN_VOCAB.has(alignCandidate) ? alignCandidate : "left";
|
|
100
|
+
const leaderNorm = leader === "middledot" ? "middleDot" : (leader as TabStop["leader"]);
|
|
101
|
+
const mappedLeader: TabStop["leader"] | undefined = TAB_LEADER_VOCAB.has(leaderNorm) ? leaderNorm : undefined;
|
|
105
102
|
tabs.push({
|
|
106
103
|
position: pos,
|
|
107
104
|
align,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-picture-sdt.ts — CO4.5
|
|
3
|
+
*
|
|
4
|
+
* Public adapter for parsing a `<w:sdt>` element that wraps picture or shape
|
|
5
|
+
* drawing content. Thin wrapper over `parseSdtXml` from parse-main-document.ts,
|
|
6
|
+
* which threads drawing-frame opts (relationships, mediaParts) through the full
|
|
7
|
+
* block-parse chain so `<w:drawing>` inside SDT content becomes a
|
|
8
|
+
* `DrawingFrameNode` inside the paragraph's inline children.
|
|
9
|
+
*
|
|
10
|
+
* The typical OOXML structure is:
|
|
11
|
+
* <w:sdt> → <w:sdtContent> → <w:p> → <w:r> → <w:drawing>
|
|
12
|
+
* The SDT's canonical child is a ParagraphNode; the drawing lives inside the
|
|
13
|
+
* paragraph's inline content as a DrawingFrameNode.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { OpcRelationship } from "./part-manifest.ts";
|
|
17
|
+
import type { InlineMediaPart } from "./parse-inline-media.ts";
|
|
18
|
+
import type { ChartPartLookup } from "./parse-complex-content.ts";
|
|
19
|
+
import { type XmlElementNode, localName } from "./_mini-xml.ts";
|
|
20
|
+
import { parseSdtXml } from "./parse-main-document.ts";
|
|
21
|
+
import type { ParsedBlockNode } from "./parse-main-document.ts";
|
|
22
|
+
|
|
23
|
+
export type { ParsedBlockNode };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a raw `<w:sdt>` XML string with full drawing support.
|
|
27
|
+
*
|
|
28
|
+
* Returns the canonical block node — typically a `ParsedSdtNode` whose
|
|
29
|
+
* children are `ParsedParagraphNode` items carrying `DrawingFrameNode`
|
|
30
|
+
* inline content. Falls back to `opaque_block` if the fragment cannot be
|
|
31
|
+
* parsed (malformed XML, no `<w:sdt>` root, etc.).
|
|
32
|
+
*/
|
|
33
|
+
export function parsePictureSdt(
|
|
34
|
+
rawXml: string,
|
|
35
|
+
relationships: readonly OpcRelationship[] = [],
|
|
36
|
+
mediaParts: ReadonlyMap<string, InlineMediaPart> = new Map(),
|
|
37
|
+
sourcePartPath?: string,
|
|
38
|
+
chartPartLookup?: ChartPartLookup,
|
|
39
|
+
): ParsedBlockNode {
|
|
40
|
+
return parseSdtXml(
|
|
41
|
+
rawXml,
|
|
42
|
+
relationships,
|
|
43
|
+
mediaParts,
|
|
44
|
+
sourcePartPath ?? "/word/document.xml",
|
|
45
|
+
chartPartLookup,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Cheap predicate: returns true when any paragraph inside `<w:sdtContent>`
|
|
51
|
+
* contains a `<w:drawing>` or `<w:pict>` child (directly or inside a run).
|
|
52
|
+
*
|
|
53
|
+
* Accepts either the `<w:sdt>` element directly or the `__root__` wrapper
|
|
54
|
+
* returned by `parseXml()`.
|
|
55
|
+
*/
|
|
56
|
+
export function sdtContentHasDrawing(node: XmlElementNode): boolean {
|
|
57
|
+
const sdt =
|
|
58
|
+
node.name === "__root__"
|
|
59
|
+
? node.children.find((c): c is XmlElementNode => c.type === "element")
|
|
60
|
+
: node;
|
|
61
|
+
if (!sdt) return false;
|
|
62
|
+
|
|
63
|
+
const contentNode = sdt.children.find(
|
|
64
|
+
(c): c is XmlElementNode =>
|
|
65
|
+
c.type === "element" && localName(c.name) === "sdtContent",
|
|
66
|
+
);
|
|
67
|
+
if (!contentNode) return false;
|
|
68
|
+
for (const child of contentNode.children) {
|
|
69
|
+
if (child.type !== "element") continue;
|
|
70
|
+
if (localName(child.name) !== "p") continue;
|
|
71
|
+
for (const pChild of (child as XmlElementNode).children) {
|
|
72
|
+
if (pChild.type !== "element") continue;
|
|
73
|
+
const pChildName = localName((pChild as XmlElementNode).name);
|
|
74
|
+
if (pChildName === "drawing" || pChildName === "pict") return true;
|
|
75
|
+
if (pChildName === "r") {
|
|
76
|
+
for (const rChild of (pChild as XmlElementNode).children) {
|
|
77
|
+
if (rChild.type !== "element") continue;
|
|
78
|
+
const rChildName = localName((rChild as XmlElementNode).name);
|
|
79
|
+
if (rChildName === "drawing" || rChildName === "pict") return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import type { PictureContent } from "../../model/canonical-document.ts";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
import {
|
|
3
|
+
type XmlElementNode,
|
|
4
|
+
findFirstChild,
|
|
5
|
+
findFirstDescendant,
|
|
6
|
+
} from "./_mini-xml.ts";
|
|
7
|
+
|
|
8
|
+
const SAFE_HEX_COLOR_RE = /^(?:[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
9
|
+
const SAFE_SCHEME_COLOR_SLOTS = new Set([
|
|
10
|
+
"dk1",
|
|
11
|
+
"lt1",
|
|
12
|
+
"dk2",
|
|
13
|
+
"lt2",
|
|
14
|
+
"accent1",
|
|
15
|
+
"accent2",
|
|
16
|
+
"accent3",
|
|
17
|
+
"accent4",
|
|
18
|
+
"accent5",
|
|
19
|
+
"accent6",
|
|
20
|
+
"hlink",
|
|
21
|
+
"folhlink",
|
|
22
|
+
"tx1",
|
|
23
|
+
"tx2",
|
|
24
|
+
"bg1",
|
|
25
|
+
"bg2",
|
|
26
|
+
"phclr",
|
|
27
|
+
]);
|
|
16
28
|
|
|
17
29
|
/**
|
|
18
30
|
* Parse a pic:pic element (child of a:graphicData) into PictureContent.
|
|
@@ -25,13 +37,18 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
|
|
|
25
37
|
const pic = findFirstDescendant(graphicDataEl, "pic");
|
|
26
38
|
if (!pic) return null;
|
|
27
39
|
|
|
28
|
-
// blipFill/blip r:embed
|
|
40
|
+
// blipFill/blip — Phase 4.4 G4: accept r:embed (embedded, primary) OR r:link
|
|
41
|
+
// (externally-linked image). Previously only r:embed was honored, silently
|
|
42
|
+
// dropping valid linked images.
|
|
29
43
|
const blipFill = findFirstChild(pic, "blipFill");
|
|
30
44
|
if (!blipFill) return null;
|
|
31
45
|
const blip = findFirstChild(blipFill, "blip");
|
|
32
46
|
if (!blip) return null;
|
|
33
|
-
const
|
|
47
|
+
const blipEmbed = blip.attributes["r:embed"] ?? blip.attributes.embed;
|
|
48
|
+
const blipLink = blip.attributes["r:link"] ?? blip.attributes.link;
|
|
49
|
+
const blipRef = blipEmbed ?? blipLink ?? "";
|
|
34
50
|
if (!blipRef) return null;
|
|
51
|
+
const isLinked = !blipEmbed && !!blipLink;
|
|
35
52
|
|
|
36
53
|
// srcRect (percentage crop: 0..100000 = 0..100%)
|
|
37
54
|
const srcRectEl = findFirstChild(blipFill, "srcRect");
|
|
@@ -44,10 +61,15 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
|
|
|
44
61
|
}
|
|
45
62
|
: undefined;
|
|
46
63
|
|
|
47
|
-
// stretch
|
|
64
|
+
// stretch vs tile — mutually exclusive per OOXML spec. Prefer stretch
|
|
65
|
+
// when both appear (matches Word's observed serializer behavior).
|
|
48
66
|
const stretchEl = findFirstChild(blipFill, "stretch");
|
|
49
67
|
const stretch = stretchEl ? true : undefined;
|
|
50
68
|
|
|
69
|
+
// Phase 4.5 G7 — a:tile (blipFill tile repeat)
|
|
70
|
+
const tileEl = findFirstChild(blipFill, "tile");
|
|
71
|
+
const tile = tileEl ? readTileAttrs(tileEl) : undefined;
|
|
72
|
+
|
|
51
73
|
// spPr/xfrm — rotation and flip
|
|
52
74
|
const spPr = findFirstChild(pic, "spPr");
|
|
53
75
|
const xfrm = spPr ? findFirstChild(spPr, "xfrm") : undefined;
|
|
@@ -72,8 +94,10 @@ export function parsePicture(graphicDataEl: XmlElementNode): PictureContent | nu
|
|
|
72
94
|
const glow = glowEl ? parseGlow(glowEl) : undefined;
|
|
73
95
|
|
|
74
96
|
const result: PictureContent = { type: "picture", blipRef };
|
|
97
|
+
if (isLinked) result.isLinked = true;
|
|
75
98
|
if (srcRect) result.srcRect = srcRect;
|
|
76
99
|
if (stretch !== undefined) result.stretch = stretch;
|
|
100
|
+
if (tile !== undefined) result.tile = tile;
|
|
77
101
|
if (rotation !== undefined) result.rotation = rotation;
|
|
78
102
|
if (flipH !== undefined) result.flipH = flipH;
|
|
79
103
|
if (flipV !== undefined) result.flipV = flipV;
|
|
@@ -94,11 +118,15 @@ function readEmuAttr(el: XmlElementNode, name: string): number | undefined {
|
|
|
94
118
|
function parseColorFromEl(el: XmlElementNode): { color: string; colorType: "srgbClr" | "schemeClr" } | null {
|
|
95
119
|
const srgb = findFirstChild(el, "srgbClr");
|
|
96
120
|
if (srgb) {
|
|
97
|
-
|
|
121
|
+
const color = (srgb.attributes.val ?? "000000").trim();
|
|
122
|
+
if (!SAFE_HEX_COLOR_RE.test(color)) return null;
|
|
123
|
+
return { color: color.toUpperCase(), colorType: "srgbClr" };
|
|
98
124
|
}
|
|
99
125
|
const scheme = findFirstChild(el, "schemeClr");
|
|
100
126
|
if (scheme) {
|
|
101
|
-
|
|
127
|
+
const slot = (scheme.attributes.val ?? "dk1").trim().toLowerCase();
|
|
128
|
+
if (!SAFE_SCHEME_COLOR_SLOTS.has(slot)) return null;
|
|
129
|
+
return { color: slot, colorType: "schemeClr" };
|
|
102
130
|
}
|
|
103
131
|
return null;
|
|
104
132
|
}
|
|
@@ -121,6 +149,25 @@ function parseGlow(el: XmlElementNode): PictureContent["glow"] {
|
|
|
121
149
|
return { radius, color: colorInfo.color, colorType: colorInfo.colorType };
|
|
122
150
|
}
|
|
123
151
|
|
|
152
|
+
function readTileAttrs(
|
|
153
|
+
tileEl: XmlElementNode,
|
|
154
|
+
): NonNullable<PictureContent["tile"]> | undefined {
|
|
155
|
+
const result: NonNullable<PictureContent["tile"]> = {};
|
|
156
|
+
const intAttrs = ["tx", "ty", "sx", "sy"] as const;
|
|
157
|
+
for (const key of intAttrs) {
|
|
158
|
+
const v = tileEl.attributes[key];
|
|
159
|
+
if (v !== undefined) {
|
|
160
|
+
const n = parseInt(v, 10);
|
|
161
|
+
if (Number.isFinite(n)) result[key] = n;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const flip = tileEl.attributes.flip;
|
|
165
|
+
if (flip === "x" || flip === "y" || flip === "xy") result.flip = flip;
|
|
166
|
+
const algn = tileEl.attributes.algn;
|
|
167
|
+
if (algn) result.algn = algn;
|
|
168
|
+
return Object.keys(result).length > 0 ? result : {};
|
|
169
|
+
}
|
|
170
|
+
|
|
124
171
|
function readPercentAttr(el: XmlElementNode, name: string): number {
|
|
125
172
|
const v = el.attributes[name];
|
|
126
173
|
if (v === undefined) return 0;
|
|
@@ -128,31 +175,14 @@ function readPercentAttr(el: XmlElementNode, name: string): number {
|
|
|
128
175
|
}
|
|
129
176
|
|
|
130
177
|
function readBoolAttr(el: XmlElementNode, name: string): boolean | undefined {
|
|
131
|
-
|
|
178
|
+
// Phase 1.3 B5 — check unprefixed, wp:-prefixed, and a:-prefixed variants.
|
|
179
|
+
// Serialized OOXML occasionally keeps DrawingML attrs in the a: namespace
|
|
180
|
+
// (e.g. `<a:xfrm a:flipH="1"/>`) even though xfrm itself is in the a: NS.
|
|
181
|
+
// parse-anchor.ts::readBoolAttr uses the same pattern; staying consistent.
|
|
182
|
+
const v = el.attributes[name] ?? el.attributes[`wp:${name}`] ?? el.attributes[`a:${name}`];
|
|
132
183
|
if (v === undefined) return undefined;
|
|
133
184
|
return v !== "0" && v !== "false";
|
|
134
185
|
}
|
|
135
186
|
|
|
136
|
-
|
|
137
|
-
for (const child of node.children) {
|
|
138
|
-
if (child.type === "element" && localName(child.name) === local) return child;
|
|
139
|
-
}
|
|
140
|
-
return undefined;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function findFirstDescendant(node: XmlElementNode, local: string): XmlElementNode | undefined {
|
|
144
|
-
for (const child of node.children) {
|
|
145
|
-
if (child.type !== "element") continue;
|
|
146
|
-
if (localName(child.name) === local) return child;
|
|
147
|
-
const found = findFirstDescendant(child, local);
|
|
148
|
-
if (found) return found;
|
|
149
|
-
}
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function localName(name: string): string {
|
|
154
|
-
const i = name.indexOf(":");
|
|
155
|
-
return i >= 0 ? name.slice(i + 1) : name;
|
|
156
|
-
}
|
|
157
|
-
|
|
187
|
+
/** @deprecated Phase 6.2 — re-export from _mini-xml.ts; call sites should migrate to XmlElementNode directly. */
|
|
158
188
|
export { type XmlElementNode as PictureXmlElement };
|