@beyondwork/docx-react-component 1.0.103 → 1.0.104
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 +63 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/outline.ts +2 -7
- package/src/api/v3/runtime/geometry.ts +79 -0
- package/src/io/ooxml/parse-drawing.ts +99 -1
- package/src/io/ooxml/parse-fields.ts +27 -6
- package/src/io/ooxml/parse-shapes.ts +130 -0
- package/src/model/canonical-document.ts +34 -3
- package/src/model/canonical-layout-inputs.ts +979 -0
- package/src/model/layout/index.ts +6 -0
- package/src/model/layout/page-graph-types.ts +61 -0
- package/src/model/layout/runtime-page-graph-types.ts +10 -0
- package/src/runtime/collab/runtime-collab-sync.ts +3 -3
- package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
- package/src/runtime/document-runtime.ts +30 -14
- package/src/runtime/event-refresh-hints.ts +3 -0
- package/src/runtime/formatting/formatting-context.ts +110 -9
- package/src/runtime/formatting/index.ts +2 -0
- package/src/runtime/formatting/layout-inputs.ts +67 -3
- package/src/runtime/geometry/caret-geometry.ts +82 -10
- package/src/runtime/geometry/geometry-facet.ts +36 -0
- package/src/runtime/geometry/geometry-index.ts +891 -0
- package/src/runtime/geometry/geometry-types.ts +221 -1
- package/src/runtime/geometry/index.ts +26 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
- package/src/runtime/geometry/replacement-envelope.ts +41 -2
- package/src/runtime/layout/layout-engine-version.ts +16 -1
- package/src/runtime/layout/page-graph.ts +191 -1
- package/src/runtime/prerender/graph-canonicalize.ts +30 -0
- package/src/runtime/surface-projection.ts +43 -3
- package/src/runtime/workflow/coordinator.ts +57 -11
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.104",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -1223,6 +1223,23 @@ export interface SurfacePictureEffects {
|
|
|
1223
1223
|
glow?: { radius: number; color: string; colorType: "srgbClr" | "schemeClr" };
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
+
export interface SurfacePreserveOnlyObjectSizing {
|
|
1227
|
+
sourceId?: string;
|
|
1228
|
+
display: "inline" | "floating" | "unknown";
|
|
1229
|
+
extentEmu?: { widthEmu: number; heightEmu: number };
|
|
1230
|
+
fallbackHint:
|
|
1231
|
+
| "drawing-inline"
|
|
1232
|
+
| "drawing-floating"
|
|
1233
|
+
| "chart"
|
|
1234
|
+
| "smartart"
|
|
1235
|
+
| "shape"
|
|
1236
|
+
| "wordart"
|
|
1237
|
+
| "vml-shape"
|
|
1238
|
+
| "ole-object"
|
|
1239
|
+
| "opaque-object";
|
|
1240
|
+
relationshipIds?: string[];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1226
1243
|
export type SurfaceInlineSegment =
|
|
1227
1244
|
| {
|
|
1228
1245
|
segmentId: string;
|
|
@@ -1338,6 +1355,20 @@ export type SurfaceInlineSegment =
|
|
|
1338
1355
|
* can render `ChartSurface` instead of the fallback bitmap or badge.
|
|
1339
1356
|
*/
|
|
1340
1357
|
parsedChartId?: string;
|
|
1358
|
+
/**
|
|
1359
|
+
* PE2 preserve-only object handoff. Present for unsupported drawing,
|
|
1360
|
+
* chart, SmartArt, WordArt, VML, and opaque-object previews when import
|
|
1361
|
+
* could recover source ids, relationship ids, fallback hints, or
|
|
1362
|
+
* extents. Layout consumers may size placeholders from this metadata;
|
|
1363
|
+
* it is not a layout computation.
|
|
1364
|
+
*/
|
|
1365
|
+
preserveOnlyObject?: SurfacePreserveOnlyObjectSizing;
|
|
1366
|
+
/**
|
|
1367
|
+
* Drawing anchor geometry for preserve-only objects that originated in a
|
|
1368
|
+
* `drawing_frame`. Mirrors image/shape anchor semantics so L04 can join
|
|
1369
|
+
* the object to source/canonical/layout rows without re-parsing OOXML.
|
|
1370
|
+
*/
|
|
1371
|
+
anchor?: SurfaceDrawingAnchor;
|
|
1341
1372
|
state: "locked-preserve-only";
|
|
1342
1373
|
}
|
|
1343
1374
|
| {
|
|
@@ -1403,6 +1434,7 @@ export type SurfaceInlineSegment =
|
|
|
1403
1434
|
isTextBox?: boolean;
|
|
1404
1435
|
/** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
|
|
1405
1436
|
textBoxBody?: SurfaceTextBoxBodyProperties;
|
|
1437
|
+
preserveOnlyObject?: SurfacePreserveOnlyObjectSizing;
|
|
1406
1438
|
/** First-paragraph plain-text preview when `isTextBox` is true. */
|
|
1407
1439
|
txbxText?: string;
|
|
1408
1440
|
/** Run-level marks from the first text-box text run, when available. */
|
|
@@ -3609,6 +3641,11 @@ export interface WordReviewEditorPasteEvent {
|
|
|
3609
3641
|
source: "paste" | "drop";
|
|
3610
3642
|
}
|
|
3611
3643
|
|
|
3644
|
+
export interface WorkflowEventOrigin {
|
|
3645
|
+
readonly source: "api" | "runtime" | "collab" | string;
|
|
3646
|
+
readonly at?: string;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3612
3649
|
export type WordReviewEditorEvent =
|
|
3613
3650
|
| {
|
|
3614
3651
|
type: "ready";
|
|
@@ -3724,16 +3761,38 @@ export type WordReviewEditorEvent =
|
|
|
3724
3761
|
type: "workflow_overlay_changed";
|
|
3725
3762
|
documentId: string;
|
|
3726
3763
|
snapshot: WorkflowScopeSnapshot;
|
|
3764
|
+
origin?: WorkflowEventOrigin;
|
|
3727
3765
|
}
|
|
3728
3766
|
| {
|
|
3729
3767
|
type: "workflow_active_work_item_changed";
|
|
3730
3768
|
documentId: string;
|
|
3731
3769
|
activeWorkItemId: string | null;
|
|
3770
|
+
origin?: WorkflowEventOrigin;
|
|
3732
3771
|
}
|
|
3733
3772
|
| {
|
|
3734
3773
|
type: "workflow_metadata_changed";
|
|
3735
3774
|
documentId: string;
|
|
3736
3775
|
snapshot: WorkflowMetadataSnapshot;
|
|
3776
|
+
origin?: WorkflowEventOrigin;
|
|
3777
|
+
}
|
|
3778
|
+
| {
|
|
3779
|
+
type: "workflow_shared_state_changed";
|
|
3780
|
+
documentId: string;
|
|
3781
|
+
state: SharedWorkflowState | null;
|
|
3782
|
+
origin: WorkflowEventOrigin;
|
|
3783
|
+
}
|
|
3784
|
+
| {
|
|
3785
|
+
type: "workflow_visibility_policy_changed";
|
|
3786
|
+
documentId: string;
|
|
3787
|
+
kind?: OverlayKind;
|
|
3788
|
+
policy?: OverlayVisibilityPolicy | null;
|
|
3789
|
+
origin: WorkflowEventOrigin;
|
|
3790
|
+
}
|
|
3791
|
+
| {
|
|
3792
|
+
type: "workflow_markup_mode_policy_changed";
|
|
3793
|
+
documentId: string;
|
|
3794
|
+
policy: WorkflowMarkupModePolicy | null;
|
|
3795
|
+
origin: WorkflowEventOrigin;
|
|
3737
3796
|
}
|
|
3738
3797
|
| {
|
|
3739
3798
|
type: "host_annotation_overlay_changed";
|
|
@@ -5010,7 +5069,10 @@ export interface WordReviewEditorRef {
|
|
|
5010
5069
|
* `replaceText` calls to block with `workflow_comment_only`.
|
|
5011
5070
|
*/
|
|
5012
5071
|
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
5013
|
-
setSharedWorkflowState(
|
|
5072
|
+
setSharedWorkflowState(
|
|
5073
|
+
state: SharedWorkflowState | null,
|
|
5074
|
+
origin?: WorkflowEventOrigin,
|
|
5075
|
+
): void;
|
|
5014
5076
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
5015
5077
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
5016
5078
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
@@ -48,6 +48,7 @@ export type RuntimeApiHandle = Pick<
|
|
|
48
48
|
| "getCompatibilityReport"
|
|
49
49
|
| "getWarnings"
|
|
50
50
|
| "getRenderSnapshot"
|
|
51
|
+
| "getDocumentNavigationSnapshot"
|
|
51
52
|
// Canonical document read (ai.inspect + ai.bundle + ai.resolve families,
|
|
52
53
|
// added in refactor/08 Slice 2/3 graduations)
|
|
53
54
|
| "getCanonicalDocument"
|
|
@@ -155,6 +156,7 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
|
|
|
155
156
|
getCompatibilityReport: true,
|
|
156
157
|
getWarnings: true,
|
|
157
158
|
getRenderSnapshot: true,
|
|
159
|
+
getDocumentNavigationSnapshot: true,
|
|
158
160
|
getCanonicalDocument: true,
|
|
159
161
|
findAllText: true,
|
|
160
162
|
replaceText: true,
|
package/src/api/v3/ai/outline.ts
CHANGED
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
DocumentOutlineHeadingSnapshot,
|
|
20
20
|
DocumentOutlineSnapshot,
|
|
21
21
|
} from "../../public-types.ts";
|
|
22
|
-
import { createDocumentNavigationSnapshot } from "../../../runtime/document-navigation.ts";
|
|
23
22
|
import { createDocumentOutlineSnapshot } from "../../../runtime/document-outline.ts";
|
|
24
23
|
import {
|
|
25
24
|
computeBlockPositions,
|
|
@@ -68,7 +67,7 @@ export const getDocumentOutlineMetadata: ApiV3FnMetadata = {
|
|
|
68
67
|
stateClass: "A-canonical",
|
|
69
68
|
persistsTo: "canonical",
|
|
70
69
|
rwdReference:
|
|
71
|
-
"§AI API § ai.getDocumentOutline. Composes
|
|
70
|
+
"§AI API § ai.getDocumentOutline. Composes runtime.getDocumentNavigationSnapshot() (L04 graph-derived navigation) with createDocumentOutlineSnapshot (L07) to surface the heading tree. Read-only; no audit emission.",
|
|
72
71
|
};
|
|
73
72
|
|
|
74
73
|
export function createOutlineFamily(runtime: RuntimeApiHandle) {
|
|
@@ -82,11 +81,7 @@ export function createOutlineFamily(runtime: RuntimeApiHandle) {
|
|
|
82
81
|
const snapshot = runtime.getRenderSnapshot();
|
|
83
82
|
const document = runtime.getCanonicalDocument();
|
|
84
83
|
const selectionHead = snapshot.selection.head;
|
|
85
|
-
const navigation =
|
|
86
|
-
document,
|
|
87
|
-
selectionHead,
|
|
88
|
-
snapshot.activeStory,
|
|
89
|
-
);
|
|
84
|
+
const navigation = runtime.getDocumentNavigationSnapshot();
|
|
90
85
|
const outline = createDocumentOutlineSnapshot({
|
|
91
86
|
navigation,
|
|
92
87
|
activeStory: snapshot.activeStory,
|
|
@@ -15,6 +15,26 @@ import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
|
15
15
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
16
16
|
import { mockPayload } from "../_mocks.ts";
|
|
17
17
|
import type { MockPayload } from "../_layer-metadata.ts";
|
|
18
|
+
import type {
|
|
19
|
+
GeometryIndex,
|
|
20
|
+
GeometryIndexCoverage,
|
|
21
|
+
} from "../../../runtime/geometry/index.ts";
|
|
22
|
+
|
|
23
|
+
function createUnavailableGeometryCoverage(): GeometryIndexCoverage {
|
|
24
|
+
return {
|
|
25
|
+
status: "unavailable",
|
|
26
|
+
pageCount: 0,
|
|
27
|
+
regionCount: 0,
|
|
28
|
+
sliceCount: 0,
|
|
29
|
+
lineCount: 0,
|
|
30
|
+
anchorCount: 0,
|
|
31
|
+
hitTargetCount: 0,
|
|
32
|
+
semanticEntryCount: 0,
|
|
33
|
+
replacementEnvelopeCount: 0,
|
|
34
|
+
objectHandleCount: 0,
|
|
35
|
+
precision: { exact: 0, "within-tolerance": 0, heuristic: 0 },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
|
|
19
39
|
export interface BlockRectEntry {
|
|
20
40
|
readonly blockId: string;
|
|
@@ -57,6 +77,50 @@ export interface HitTestResult {
|
|
|
57
77
|
readonly __mock?: true;
|
|
58
78
|
}
|
|
59
79
|
|
|
80
|
+
/* ================================================================== */
|
|
81
|
+
/* PE2 geometry index + coverage */
|
|
82
|
+
/* ================================================================== */
|
|
83
|
+
|
|
84
|
+
export const getGeometryIndexMetadata: ApiV3FnMetadata = {
|
|
85
|
+
name: "runtime.geometry.getGeometryIndex",
|
|
86
|
+
status: "live-with-adapter",
|
|
87
|
+
sourceLayer: "geometry-projection",
|
|
88
|
+
liveEvidence: {
|
|
89
|
+
runnerTest: "test/api/v3/geometry-uses-geometry-handle.test.ts",
|
|
90
|
+
commit: "refactor-07-pe2-geometry-index-read-surface",
|
|
91
|
+
},
|
|
92
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
93
|
+
agentMetadata: {
|
|
94
|
+
readOrMutate: "read",
|
|
95
|
+
boundedScope: "document",
|
|
96
|
+
auditCategory: "geometry-read",
|
|
97
|
+
},
|
|
98
|
+
stateClass: "A-canonical",
|
|
99
|
+
persistsTo: "canonical",
|
|
100
|
+
rwdReference:
|
|
101
|
+
"§Runtime API § runtime.geometry.getGeometryIndex. PE2 read surface over GeometryFacet.getGeometryIndex(): renderer-neutral pages, regions, slices, lines, anchors, hit targets, semantic entries, replacement envelopes, object handles, and coverage. Returns null when no geometry facet or render frame is warm. Promotes to live when L05 projects directly from L04 page slices instead of the render-frame adapter.",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const getGeometryCoverageMetadata: ApiV3FnMetadata = {
|
|
105
|
+
name: "runtime.geometry.getGeometryCoverage",
|
|
106
|
+
status: "live-with-adapter",
|
|
107
|
+
sourceLayer: "geometry-projection",
|
|
108
|
+
liveEvidence: {
|
|
109
|
+
runnerTest: "test/api/v3/geometry-uses-geometry-handle.test.ts",
|
|
110
|
+
commit: "refactor-07-pe2-geometry-index-read-surface",
|
|
111
|
+
},
|
|
112
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
113
|
+
agentMetadata: {
|
|
114
|
+
readOrMutate: "read",
|
|
115
|
+
boundedScope: "document",
|
|
116
|
+
auditCategory: "geometry-read",
|
|
117
|
+
},
|
|
118
|
+
stateClass: "A-canonical",
|
|
119
|
+
persistsTo: "canonical",
|
|
120
|
+
rwdReference:
|
|
121
|
+
"§Runtime API § runtime.geometry.getGeometryCoverage. PE2 lightweight status read over GeometryFacet.getGeometryCoverage(). Always returns a coverage object; unwired/pre-paint states report status:'unavailable' with zero counts so agents and evidence runners can distinguish no geometry from empty geometry.",
|
|
122
|
+
};
|
|
123
|
+
|
|
60
124
|
/* ================================================================== */
|
|
61
125
|
/* getCaret — Slice-4 caret geometry (Slice X3 of §4 of coord-05) */
|
|
62
126
|
/* ================================================================== */
|
|
@@ -224,6 +288,21 @@ export function createGeometryFamily(runtime: RuntimeApiHandle) {
|
|
|
224
288
|
const geometry = runtime.geometry;
|
|
225
289
|
|
|
226
290
|
return {
|
|
291
|
+
getGeometryIndex(): GeometryIndex | null {
|
|
292
|
+
// @endStateApi — live-with-adapter. Exposes the Layer-05 PE2
|
|
293
|
+
// geometry index through the v3 runtime seam. The current L05
|
|
294
|
+
// index is projected from the render frame; callers still get
|
|
295
|
+
// renderer-neutral value objects and no DOM/PM/runtime instances.
|
|
296
|
+
return geometry?.getGeometryIndex() ?? null;
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
getGeometryCoverage(): GeometryIndexCoverage {
|
|
300
|
+
// @endStateApi — live-with-adapter. Coverage is intentionally
|
|
301
|
+
// total: an unwired/pre-paint handle reports an unavailable zero
|
|
302
|
+
// summary instead of null so evidence code can branch on status.
|
|
303
|
+
return geometry?.getGeometryCoverage() ?? createUnavailableGeometryCoverage();
|
|
304
|
+
},
|
|
305
|
+
|
|
227
306
|
getBlockRects(blockIds: readonly string[]): ReadonlyArray<BlockRectEntry & Partial<MockPayload>> {
|
|
228
307
|
// @endStateApi — live-with-adapter. Routes directly through the
|
|
229
308
|
// Layer-05 GeometryFacet's `getBlockRects(blockId)`, which walks
|
|
@@ -2,7 +2,12 @@ import type { OpcRelationship } from "./part-manifest.ts";
|
|
|
2
2
|
import { normalizePartPath, resolveRelationshipTarget } from "./part-manifest.ts";
|
|
3
3
|
import type { InlineMediaPart } from "./parse-inline-media.ts";
|
|
4
4
|
import type { ChartPartLookup } from "./parse-complex-content.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
AnchorGeometry,
|
|
7
|
+
DrawingFrameNode,
|
|
8
|
+
Mutable,
|
|
9
|
+
PreserveOnlyObjectSizing,
|
|
10
|
+
} from "../../model/canonical-document.ts";
|
|
6
11
|
import { parseAnchorGeometry } from "./parse-anchor.ts";
|
|
7
12
|
import { parsePicture } from "./parse-picture.ts";
|
|
8
13
|
import { parseShapeContent, type TxbxBlockParser } from "./parse-shapes.ts";
|
|
@@ -144,6 +149,13 @@ function resolveFromBranch(
|
|
|
144
149
|
if (uri === WPS_SHAPE_GRAPHIC_URI && isWordArtGraphicData(graphicData)) return null;
|
|
145
150
|
|
|
146
151
|
const content = resolveContent(uri, graphicData, outerRawXml, opts);
|
|
152
|
+
attachPreserveOnlyObjectSizing(content, {
|
|
153
|
+
anchor: geometry,
|
|
154
|
+
container,
|
|
155
|
+
fallbackHint: fallbackHintForContent(content, geometry),
|
|
156
|
+
rawXml: outerRawXml,
|
|
157
|
+
sourcePartPath: opts.sourcePartPath,
|
|
158
|
+
});
|
|
147
159
|
return { type: "drawing_frame", anchor: geometry, content };
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -231,6 +243,92 @@ function resolveContent(
|
|
|
231
243
|
return { type: "opaque", rawXml };
|
|
232
244
|
}
|
|
233
245
|
|
|
246
|
+
function attachPreserveOnlyObjectSizing(
|
|
247
|
+
content: DrawingFrameNode["content"],
|
|
248
|
+
context: {
|
|
249
|
+
anchor: AnchorGeometry;
|
|
250
|
+
container: XmlElementNode;
|
|
251
|
+
fallbackHint: PreserveOnlyObjectSizing["fallbackHint"] | null;
|
|
252
|
+
rawXml: string;
|
|
253
|
+
sourcePartPath: string | undefined;
|
|
254
|
+
},
|
|
255
|
+
): void {
|
|
256
|
+
if (content.type === "picture" || context.fallbackHint === null) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
(content as Mutable<typeof content>).preserveOnlyObject = {
|
|
261
|
+
sourceId: createPreserveOnlyObjectSourceId(
|
|
262
|
+
context.sourcePartPath,
|
|
263
|
+
context.anchor,
|
|
264
|
+
context.rawXml,
|
|
265
|
+
),
|
|
266
|
+
display: context.anchor.display,
|
|
267
|
+
extentEmu: {
|
|
268
|
+
widthEmu: context.anchor.extent.widthEmu,
|
|
269
|
+
heightEmu: context.anchor.extent.heightEmu,
|
|
270
|
+
},
|
|
271
|
+
fallbackHint: context.fallbackHint,
|
|
272
|
+
relationshipIds: collectRelationshipIds(context.container),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function fallbackHintForContent(
|
|
277
|
+
content: DrawingFrameNode["content"],
|
|
278
|
+
anchor: AnchorGeometry,
|
|
279
|
+
): PreserveOnlyObjectSizing["fallbackHint"] | null {
|
|
280
|
+
switch (content.type) {
|
|
281
|
+
case "picture":
|
|
282
|
+
return null;
|
|
283
|
+
case "chart_preview":
|
|
284
|
+
return "chart";
|
|
285
|
+
case "smartart_preview":
|
|
286
|
+
return "smartart";
|
|
287
|
+
case "shape":
|
|
288
|
+
return "shape";
|
|
289
|
+
case "opaque":
|
|
290
|
+
return anchor.display === "floating" ? "drawing-floating" : "drawing-inline";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function createPreserveOnlyObjectSourceId(
|
|
295
|
+
sourcePartPath: string | undefined,
|
|
296
|
+
anchor: AnchorGeometry,
|
|
297
|
+
rawXml: string,
|
|
298
|
+
): string {
|
|
299
|
+
const part = sourcePartPath ?? "/word/document.xml";
|
|
300
|
+
const docPrId = anchor.docPr?.id;
|
|
301
|
+
if (docPrId) return `part:${part}#drawing-docPr:${docPrId}`;
|
|
302
|
+
return `part:${part}#drawing:${hashString(rawXml)}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function collectRelationshipIds(node: XmlElementNode): string[] {
|
|
306
|
+
const ids = new Set<string>();
|
|
307
|
+
const visit = (current: XmlElementNode): void => {
|
|
308
|
+
const id =
|
|
309
|
+
current.attributes["r:id"] ??
|
|
310
|
+
current.attributes["r:embed"] ??
|
|
311
|
+
current.attributes.embed ??
|
|
312
|
+
current.attributes["r:link"] ??
|
|
313
|
+
current.attributes.link;
|
|
314
|
+
if (id) ids.add(id);
|
|
315
|
+
for (const child of current.children) {
|
|
316
|
+
if (child.type === "element") visit(child);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
visit(node);
|
|
320
|
+
return [...ids].sort();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function hashString(value: string): string {
|
|
324
|
+
let hash = 2166136261;
|
|
325
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
326
|
+
hash ^= value.charCodeAt(index);
|
|
327
|
+
hash = Math.imul(hash, 16777619);
|
|
328
|
+
}
|
|
329
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
330
|
+
}
|
|
331
|
+
|
|
234
332
|
function extractChartRelId(graphicData: XmlElementNode | undefined): string | null {
|
|
235
333
|
if (!graphicData) return null;
|
|
236
334
|
const chart = findFirstDescendant(graphicData, "chart");
|
|
@@ -330,6 +330,11 @@ import type {
|
|
|
330
330
|
TocStructure,
|
|
331
331
|
Mutable,
|
|
332
332
|
} from "../../model/canonical-document.ts";
|
|
333
|
+
import {
|
|
334
|
+
MAIN_STORY_KEY,
|
|
335
|
+
createHeaderFooterStoryKey,
|
|
336
|
+
createNoteStoryKey,
|
|
337
|
+
} from "../../model/canonical-layout-inputs.ts";
|
|
333
338
|
import { parseFieldSwitches } from "./parse-field-switches.ts";
|
|
334
339
|
|
|
335
340
|
const FIELD_FAMILY_PATTERN =
|
|
@@ -439,6 +444,7 @@ export function buildFieldRegistry(
|
|
|
439
444
|
...(classification.target ? { fieldTarget: classification.target } : {}),
|
|
440
445
|
displayText,
|
|
441
446
|
paragraphIndex,
|
|
447
|
+
storyKey: MAIN_STORY_KEY,
|
|
442
448
|
refreshStatus: node.refreshStatus ?? (classification.supported ? "stale" : "preserve-only"),
|
|
443
449
|
...(classification.switches ? { switches: classification.switches } : {}),
|
|
444
450
|
};
|
|
@@ -454,7 +460,7 @@ export function buildFieldRegistry(
|
|
|
454
460
|
}
|
|
455
461
|
});
|
|
456
462
|
if (document.subParts) {
|
|
457
|
-
walkSubPartFields(document.subParts, (node, pIdx) => {
|
|
463
|
+
walkSubPartFields(document.subParts, (node, pIdx, storyKey) => {
|
|
458
464
|
paragraphIndex = pIdx;
|
|
459
465
|
if (node.type === "field") {
|
|
460
466
|
const classification = node.fieldFamily
|
|
@@ -469,6 +475,7 @@ export function buildFieldRegistry(
|
|
|
469
475
|
...(classification.target ? { fieldTarget: classification.target } : {}),
|
|
470
476
|
displayText,
|
|
471
477
|
paragraphIndex,
|
|
478
|
+
storyKey,
|
|
472
479
|
refreshStatus: node.refreshStatus ?? (classification.supported ? "stale" : "preserve-only"),
|
|
473
480
|
...(classification.switches ? { switches: classification.switches } : {}),
|
|
474
481
|
};
|
|
@@ -1062,27 +1069,41 @@ function walkFieldDocument(
|
|
|
1062
1069
|
|
|
1063
1070
|
function walkSubPartFields(
|
|
1064
1071
|
subParts: Mutable<SubPartsCatalog>,
|
|
1065
|
-
visit: (node: DocumentNode, paragraphIndex: number) => void,
|
|
1072
|
+
visit: (node: DocumentNode, paragraphIndex: number, storyKey: string) => void,
|
|
1066
1073
|
): void {
|
|
1067
1074
|
for (const header of subParts.headers) {
|
|
1075
|
+
const storyKey = createHeaderFooterStoryKey({
|
|
1076
|
+
kind: "header",
|
|
1077
|
+
relationshipId: header.relationshipId,
|
|
1078
|
+
variant: header.variant,
|
|
1079
|
+
...(header.sectionIndex !== undefined ? { sectionIndex: header.sectionIndex } : {}),
|
|
1080
|
+
});
|
|
1068
1081
|
for (const block of header.blocks) {
|
|
1069
|
-
walkFieldDocument(block, visit);
|
|
1082
|
+
walkFieldDocument(block, (node, paragraphIndex) => visit(node, paragraphIndex, storyKey));
|
|
1070
1083
|
}
|
|
1071
1084
|
}
|
|
1072
1085
|
for (const footer of subParts.footers) {
|
|
1086
|
+
const storyKey = createHeaderFooterStoryKey({
|
|
1087
|
+
kind: "footer",
|
|
1088
|
+
relationshipId: footer.relationshipId,
|
|
1089
|
+
variant: footer.variant,
|
|
1090
|
+
...(footer.sectionIndex !== undefined ? { sectionIndex: footer.sectionIndex } : {}),
|
|
1091
|
+
});
|
|
1073
1092
|
for (const block of footer.blocks) {
|
|
1074
|
-
walkFieldDocument(block, visit);
|
|
1093
|
+
walkFieldDocument(block, (node, paragraphIndex) => visit(node, paragraphIndex, storyKey));
|
|
1075
1094
|
}
|
|
1076
1095
|
}
|
|
1077
1096
|
if (subParts.footnoteCollection) {
|
|
1078
1097
|
for (const note of Object.values(subParts.footnoteCollection.footnotes)) {
|
|
1098
|
+
const storyKey = createNoteStoryKey("footnote", note.noteId);
|
|
1079
1099
|
for (const block of note.blocks) {
|
|
1080
|
-
walkFieldDocument(block, visit);
|
|
1100
|
+
walkFieldDocument(block, (node, paragraphIndex) => visit(node, paragraphIndex, storyKey));
|
|
1081
1101
|
}
|
|
1082
1102
|
}
|
|
1083
1103
|
for (const note of Object.values(subParts.footnoteCollection.endnotes)) {
|
|
1104
|
+
const storyKey = createNoteStoryKey("endnote", note.noteId);
|
|
1084
1105
|
for (const block of note.blocks) {
|
|
1085
|
-
walkFieldDocument(block, visit);
|
|
1106
|
+
walkFieldDocument(block, (node, paragraphIndex) => visit(node, paragraphIndex, storyKey));
|
|
1086
1107
|
}
|
|
1087
1108
|
}
|
|
1088
1109
|
}
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ShapeContent,
|
|
16
16
|
TextBoxBodyProperties,
|
|
17
17
|
Mutable,
|
|
18
|
+
PreserveOnlyObjectSizing,
|
|
18
19
|
} from "../../model/canonical-document.ts";
|
|
19
20
|
import { parseFill } from "./parse-fill.ts";
|
|
20
21
|
import {
|
|
@@ -52,6 +53,7 @@ export interface ParsedWpsShape {
|
|
|
52
53
|
geometry?: string;
|
|
53
54
|
/** Original drawing XML for lossless round-trip export. */
|
|
54
55
|
rawXml: string;
|
|
56
|
+
preserveOnlyObject?: PreserveOnlyObjectSizing;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export interface ParsedWordArt {
|
|
@@ -62,6 +64,7 @@ export interface ParsedWordArt {
|
|
|
62
64
|
geometry?: string;
|
|
63
65
|
/** Original drawing XML for lossless round-trip export. */
|
|
64
66
|
rawXml: string;
|
|
67
|
+
preserveOnlyObject?: PreserveOnlyObjectSizing;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface ParsedVmlShape {
|
|
@@ -72,6 +75,7 @@ export interface ParsedVmlShape {
|
|
|
72
75
|
text?: string;
|
|
73
76
|
/** Original w:pict XML for lossless round-trip export. */
|
|
74
77
|
rawXml: string;
|
|
78
|
+
preserveOnlyObject?: PreserveOnlyObjectSizing;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
export type ParsedShape = ParsedWpsShape | ParsedWordArt | ParsedVmlShape;
|
|
@@ -113,6 +117,11 @@ export function parseShapeXml(
|
|
|
113
117
|
text: text ?? "",
|
|
114
118
|
...(prst ? { geometry: prst } : {}),
|
|
115
119
|
rawXml: drawingXml,
|
|
120
|
+
preserveOnlyObject: createDrawingPreserveOnlyObject(
|
|
121
|
+
drawingXml,
|
|
122
|
+
"wordart",
|
|
123
|
+
collectRelationshipIds(root),
|
|
124
|
+
),
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
|
|
@@ -158,6 +167,11 @@ export function parseShapeXml(
|
|
|
158
167
|
...(txbxBlocks && txbxBlocks.length > 0 ? { txbxBlocks } : {}),
|
|
159
168
|
...(prst ? { geometry: prst } : {}),
|
|
160
169
|
rawXml: drawingXml,
|
|
170
|
+
preserveOnlyObject: createDrawingPreserveOnlyObject(
|
|
171
|
+
drawingXml,
|
|
172
|
+
"shape",
|
|
173
|
+
collectRelationshipIds(root),
|
|
174
|
+
),
|
|
161
175
|
};
|
|
162
176
|
}
|
|
163
177
|
|
|
@@ -200,6 +214,11 @@ export function parseVmlXml(pictXml: string): ParsedVmlShape | null {
|
|
|
200
214
|
...(shapeType ? { shapeType } : {}),
|
|
201
215
|
...(text ? { text } : {}),
|
|
202
216
|
rawXml: pictXml,
|
|
217
|
+
preserveOnlyObject: createVmlPreserveOnlyObject(
|
|
218
|
+
pictXml,
|
|
219
|
+
vmlNode,
|
|
220
|
+
collectRelationshipIds(root),
|
|
221
|
+
),
|
|
203
222
|
};
|
|
204
223
|
}
|
|
205
224
|
|
|
@@ -361,3 +380,114 @@ function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
|
|
|
361
380
|
const parsed = parseInt(raw, 10);
|
|
362
381
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
363
382
|
}
|
|
383
|
+
|
|
384
|
+
function createDrawingPreserveOnlyObject(
|
|
385
|
+
rawXml: string,
|
|
386
|
+
fallbackHint: PreserveOnlyObjectSizing["fallbackHint"],
|
|
387
|
+
relationshipIds: string[],
|
|
388
|
+
): PreserveOnlyObjectSizing {
|
|
389
|
+
const extentEmu = readDrawingExtentEmu(rawXml);
|
|
390
|
+
return {
|
|
391
|
+
sourceId: `drawing:${hashString(rawXml)}`,
|
|
392
|
+
display: rawXml.includes("<wp:anchor") ? "floating" : "inline",
|
|
393
|
+
...(extentEmu ? { extentEmu } : {}),
|
|
394
|
+
fallbackHint,
|
|
395
|
+
...(relationshipIds.length > 0 ? { relationshipIds } : {}),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function createVmlPreserveOnlyObject(
|
|
400
|
+
rawXml: string,
|
|
401
|
+
node: XmlElementNode,
|
|
402
|
+
relationshipIds: string[],
|
|
403
|
+
): PreserveOnlyObjectSizing {
|
|
404
|
+
const extentEmu = readVmlExtentEmu(node.attributes.style);
|
|
405
|
+
return {
|
|
406
|
+
sourceId: `vml:${node.attributes.id ?? hashString(rawXml)}`,
|
|
407
|
+
display: "unknown",
|
|
408
|
+
...(extentEmu ? { extentEmu } : {}),
|
|
409
|
+
fallbackHint: "vml-shape",
|
|
410
|
+
...(relationshipIds.length > 0 ? { relationshipIds } : {}),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function readDrawingExtentEmu(
|
|
415
|
+
rawXml: string,
|
|
416
|
+
): { widthEmu: number; heightEmu: number } | undefined {
|
|
417
|
+
const extentMatch = /<wp:extent\b[^>]*\bcx=["']([0-9]+)["'][^>]*\bcy=["']([0-9]+)["'][^>]*\/?>/u.exec(rawXml);
|
|
418
|
+
if (extentMatch) {
|
|
419
|
+
return {
|
|
420
|
+
widthEmu: Number.parseInt(extentMatch[1] ?? "0", 10),
|
|
421
|
+
heightEmu: Number.parseInt(extentMatch[2] ?? "0", 10),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const xfrmMatch = /<a:ext\b[^>]*\bcx=["']([0-9]+)["'][^>]*\bcy=["']([0-9]+)["'][^>]*\/?>/u.exec(rawXml);
|
|
425
|
+
if (!xfrmMatch) return undefined;
|
|
426
|
+
return {
|
|
427
|
+
widthEmu: Number.parseInt(xfrmMatch[1] ?? "0", 10),
|
|
428
|
+
heightEmu: Number.parseInt(xfrmMatch[2] ?? "0", 10),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function readVmlExtentEmu(
|
|
433
|
+
style: string | undefined,
|
|
434
|
+
): { widthEmu: number; heightEmu: number } | undefined {
|
|
435
|
+
if (!style) return undefined;
|
|
436
|
+
const widthTwips = readCssLengthTwips(style, "width");
|
|
437
|
+
const heightTwips = readCssLengthTwips(style, "height");
|
|
438
|
+
if (widthTwips === undefined || heightTwips === undefined) return undefined;
|
|
439
|
+
return {
|
|
440
|
+
widthEmu: Math.round(widthTwips * 635),
|
|
441
|
+
heightEmu: Math.round(heightTwips * 635),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function readCssLengthTwips(style: string, property: "width" | "height"): number | undefined {
|
|
446
|
+
const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)\\s*(pt|in|cm|mm|px)?`, "iu")
|
|
447
|
+
.exec(style);
|
|
448
|
+
if (!match) return undefined;
|
|
449
|
+
const value = Number.parseFloat(match[1] ?? "");
|
|
450
|
+
if (!Number.isFinite(value)) return undefined;
|
|
451
|
+
const unit = (match[2] ?? "pt").toLowerCase();
|
|
452
|
+
switch (unit) {
|
|
453
|
+
case "pt":
|
|
454
|
+
return Math.round(value * 20);
|
|
455
|
+
case "in":
|
|
456
|
+
return Math.round(value * 1440);
|
|
457
|
+
case "cm":
|
|
458
|
+
return Math.round(value * 567);
|
|
459
|
+
case "mm":
|
|
460
|
+
return Math.round(value * 56.7);
|
|
461
|
+
case "px":
|
|
462
|
+
return Math.round(value * 15);
|
|
463
|
+
default:
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function collectRelationshipIds(node: XmlElementNode): string[] {
|
|
469
|
+
const ids = new Set<string>();
|
|
470
|
+
const visit = (current: XmlElementNode): void => {
|
|
471
|
+
const id =
|
|
472
|
+
current.attributes["r:id"] ??
|
|
473
|
+
current.attributes["r:embed"] ??
|
|
474
|
+
current.attributes.embed ??
|
|
475
|
+
current.attributes["r:link"] ??
|
|
476
|
+
current.attributes.link;
|
|
477
|
+
if (id) ids.add(id);
|
|
478
|
+
for (const child of current.children) {
|
|
479
|
+
if (child.type === "element") visit(child);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
visit(node);
|
|
483
|
+
return [...ids].sort();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function hashString(value: string): string {
|
|
487
|
+
let hash = 2166136261;
|
|
488
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
489
|
+
hash ^= value.charCodeAt(index);
|
|
490
|
+
hash = Math.imul(hash, 16777619);
|
|
491
|
+
}
|
|
492
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
493
|
+
}
|