@beyondwork/docx-react-component 1.0.103 → 1.0.105
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 +66 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/_pe2-evidence.ts +153 -0
- package/src/api/v3/ai/bundle.ts +13 -5
- package/src/api/v3/ai/inspect.ts +7 -1
- package/src/api/v3/ai/outline.ts +2 -7
- package/src/api/v3/ai/replacement.ts +113 -0
- package/src/api/v3/runtime/geometry.ts +79 -0
- package/src/api/v3/ui/_types.ts +86 -0
- package/src/api/v3/ui/index.ts +5 -0
- package/src/api/v3/ui/overlays.ts +104 -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 +9 -0
- package/src/model/layout/page-graph-types.ts +150 -0
- package/src/model/layout/runtime-page-graph-types.ts +23 -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 +35 -5
- 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 +44 -0
- package/src/runtime/geometry/geometry-index.ts +1268 -0
- package/src/runtime/geometry/geometry-types.ts +227 -1
- package/src/runtime/geometry/index.ts +26 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
- package/src/runtime/geometry/object-handles.ts +7 -4
- package/src/runtime/geometry/replacement-envelope.ts +41 -2
- package/src/runtime/layout/layout-engine-instance.ts +2 -0
- package/src/runtime/layout/layout-engine-version.ts +44 -1
- package/src/runtime/layout/page-graph.ts +877 -2
- package/src/runtime/layout/project-block-fragments.ts +101 -1
- package/src/runtime/layout/public-facet.ts +152 -0
- package/src/runtime/prerender/graph-canonicalize.ts +44 -0
- package/src/runtime/surface-projection.ts +43 -3
- package/src/runtime/workflow/coordinator.ts +57 -11
- package/src/ui/ui-controller-factory.ts +11 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
package/src/api/v3/ui/_types.ts
CHANGED
|
@@ -127,6 +127,21 @@ export interface UiController {
|
|
|
127
127
|
readonly subscribeOverlays?: (
|
|
128
128
|
listener: UiListener<OverlayAnchorQuery>,
|
|
129
129
|
) => UiUnsubscribe;
|
|
130
|
+
/**
|
|
131
|
+
* PE2 overlay-lane snapshot resolver. This is the mounted-surface lane
|
|
132
|
+
* contract for selection/caret/redline/scope/comment/table/object/search
|
|
133
|
+
* and presence overlays. The bind-side supplies already-projected lane
|
|
134
|
+
* entries; L10 owns the observable shape, not layout or geometry caches.
|
|
135
|
+
*/
|
|
136
|
+
readonly getOverlayLane?: (kind: UiOverlayLaneKind) => UiOverlayLaneSnapshot;
|
|
137
|
+
/**
|
|
138
|
+
* PE2 overlay-lane subscription. Must fan out lane changes without
|
|
139
|
+
* dispatching PM document transactions or invalidating L04 layout.
|
|
140
|
+
*/
|
|
141
|
+
readonly subscribeOverlayLane?: (
|
|
142
|
+
kind: UiOverlayLaneKind,
|
|
143
|
+
listener: UiListener<UiOverlayLaneSnapshot>,
|
|
144
|
+
) => UiUnsubscribe;
|
|
130
145
|
/**
|
|
131
146
|
* Overlay anchor resolver. Expected to be a direct projection of the
|
|
132
147
|
* geometry facet (U4 — overlays derive from geometry, not DOM). Returns
|
|
@@ -267,6 +282,60 @@ export type OverlayAnchorQuery =
|
|
|
267
282
|
| { kind: "page"; value: number }
|
|
268
283
|
| { kind: "selection" };
|
|
269
284
|
|
|
285
|
+
/* ================================================================== */
|
|
286
|
+
/* PE2 overlay lanes */
|
|
287
|
+
/* ================================================================== */
|
|
288
|
+
|
|
289
|
+
export type UiOverlayLaneKind =
|
|
290
|
+
| "selection"
|
|
291
|
+
| "caret"
|
|
292
|
+
| "redlines"
|
|
293
|
+
| "field-scopes"
|
|
294
|
+
| "broad-scopes"
|
|
295
|
+
| "comments"
|
|
296
|
+
| "issues"
|
|
297
|
+
| "tables"
|
|
298
|
+
| "objects"
|
|
299
|
+
| "search"
|
|
300
|
+
| "presence";
|
|
301
|
+
|
|
302
|
+
export type UiOverlayLaneStatus =
|
|
303
|
+
| "resolved"
|
|
304
|
+
| "requires-rehydration"
|
|
305
|
+
| "unavailable";
|
|
306
|
+
|
|
307
|
+
export type UiOverlayLaneSource =
|
|
308
|
+
| "geometry"
|
|
309
|
+
| "workflow"
|
|
310
|
+
| "search"
|
|
311
|
+
| "awareness"
|
|
312
|
+
| "controller"
|
|
313
|
+
| "unavailable";
|
|
314
|
+
|
|
315
|
+
export interface UiOverlayLaneEntry {
|
|
316
|
+
readonly id: string;
|
|
317
|
+
readonly status: UiOverlayLaneStatus;
|
|
318
|
+
readonly anchor?: OverlayAnchorQuery;
|
|
319
|
+
readonly rects?: readonly GeometryRect[];
|
|
320
|
+
readonly reason?: string;
|
|
321
|
+
/**
|
|
322
|
+
* Lane-specific plain payload. Examples: peer identity for presence,
|
|
323
|
+
* issue severity, search rank, table/object classification. Kept plain
|
|
324
|
+
* so non-React consumers and debug runners can serialize snapshots.
|
|
325
|
+
*/
|
|
326
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface UiOverlayLaneSnapshot {
|
|
330
|
+
readonly __mock?: true;
|
|
331
|
+
readonly kind: UiOverlayLaneKind;
|
|
332
|
+
readonly status: UiOverlayLaneStatus;
|
|
333
|
+
readonly entries: readonly UiOverlayLaneEntry[];
|
|
334
|
+
readonly revision: number;
|
|
335
|
+
readonly source: UiOverlayLaneSource;
|
|
336
|
+
readonly reason?: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
270
339
|
/* ================================================================== */
|
|
271
340
|
/* Overlay visibility — U9 (state-classes X3) */
|
|
272
341
|
/* ================================================================== */
|
|
@@ -453,6 +522,23 @@ export interface ApiV3UiOverlays {
|
|
|
453
522
|
query: OverlayAnchorQuery,
|
|
454
523
|
listener: UiListener<GeometryRect | null>,
|
|
455
524
|
): UiUnsubscribe;
|
|
525
|
+
/**
|
|
526
|
+
* PE2 overlay-lane read. Returns a plain snapshot for a mounted lane
|
|
527
|
+
* such as selection, caret, redlines, comments, tables, objects,
|
|
528
|
+
* search, or presence. When the host has not wired a lane yet, returns
|
|
529
|
+
* an explicit `unavailable` snapshot instead of measuring DOM or
|
|
530
|
+
* forcing geometry rehydration.
|
|
531
|
+
*/
|
|
532
|
+
getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot;
|
|
533
|
+
/**
|
|
534
|
+
* PE2 overlay-lane subscription. The listener receives lane snapshots,
|
|
535
|
+
* never PM transactions. Presence/awareness updates must travel through
|
|
536
|
+
* this channel without invalidating L04 layout.
|
|
537
|
+
*/
|
|
538
|
+
subscribeLane(
|
|
539
|
+
kind: UiOverlayLaneKind,
|
|
540
|
+
listener: UiListener<UiOverlayLaneSnapshot>,
|
|
541
|
+
): UiUnsubscribe;
|
|
456
542
|
|
|
457
543
|
/**
|
|
458
544
|
* U9 · Composed overlay visibility. Merges the class-A policy from
|
package/src/api/v3/ui/index.ts
CHANGED
|
@@ -43,6 +43,11 @@ export type {
|
|
|
43
43
|
DebugSession,
|
|
44
44
|
DebugAttachment,
|
|
45
45
|
GeometryRect,
|
|
46
|
+
UiOverlayLaneEntry,
|
|
47
|
+
UiOverlayLaneKind,
|
|
48
|
+
UiOverlayLaneSnapshot,
|
|
49
|
+
UiOverlayLaneSource,
|
|
50
|
+
UiOverlayLaneStatus,
|
|
46
51
|
} from "./_types.ts";
|
|
47
52
|
|
|
48
53
|
// Chrome composition types (U5.b) — relocated to layer 10 in Slice 8.
|
|
@@ -42,6 +42,8 @@ import type {
|
|
|
42
42
|
GeometryRect,
|
|
43
43
|
OverlayAnchorQuery,
|
|
44
44
|
OverlayVisibility,
|
|
45
|
+
UiOverlayLaneKind,
|
|
46
|
+
UiOverlayLaneSnapshot,
|
|
45
47
|
UiListener,
|
|
46
48
|
UiUnsubscribe,
|
|
47
49
|
} from "./_types.ts";
|
|
@@ -195,6 +197,57 @@ export const subscribeQueryMetadata: ApiV3FnMetadata = {
|
|
|
195
197
|
rwdReference: "§UI API § ui.overlays.subscribeQuery (KI-006 close). Per-query coalesced variant of `subscribe`. Registers a shared coarse `controller.subscribeOverlays` tick on the first query subscription; re-resolves each attached query on every tick via `getAnchor(query)` (memoized); fires per-query listeners only when the rect actually changes. Torn down on last unsubscribe. Listener receives the new `GeometryRect | null` value. Callers that need the full invalidation stream keep using `subscribe(listener)`.",
|
|
196
198
|
};
|
|
197
199
|
|
|
200
|
+
// ----- PE2 overlay-lane contract -------------------------------------------
|
|
201
|
+
|
|
202
|
+
export const getLaneMetadata: ApiV3FnMetadata = {
|
|
203
|
+
name: "ui.overlays.getLane",
|
|
204
|
+
status: "live-with-adapter",
|
|
205
|
+
sourceLayer: "presentation",
|
|
206
|
+
liveEvidence: {
|
|
207
|
+
runnerTest: "test/api/v3/ui/overlays-lanes.test.ts",
|
|
208
|
+
commit: "refactor-10-pe2-overlay-lanes",
|
|
209
|
+
},
|
|
210
|
+
mockShape: {
|
|
211
|
+
deterministic: true,
|
|
212
|
+
seededFrom: "fixed",
|
|
213
|
+
shapeDescription:
|
|
214
|
+
"Unavailable UiOverlayLaneSnapshot with entries:[] and revision:0 when the mounted controller has not wired the requested PE2 overlay lane yet.",
|
|
215
|
+
carriesMockFlag: true,
|
|
216
|
+
},
|
|
217
|
+
uxIntent: { uiVisible: false },
|
|
218
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-overlays-lane-read" },
|
|
219
|
+
stateClass: "C-local",
|
|
220
|
+
persistsTo: "none",
|
|
221
|
+
rwdReference:
|
|
222
|
+
"§UI API § PE2 overlay lanes. Reads the mounted controller's lane snapshot for selection/caret/redlines/field-scopes/broad-scopes/comments/issues/tables/objects/search/presence. Returns explicit status:'unavailable' when the lane is not wired; never measures DOM or wakes layout.",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const subscribeLaneMetadata: ApiV3FnMetadata = {
|
|
226
|
+
name: "ui.overlays.subscribeLane",
|
|
227
|
+
status: "live-with-adapter",
|
|
228
|
+
sourceLayer: "presentation",
|
|
229
|
+
liveEvidence: {
|
|
230
|
+
runnerTest: "test/api/v3/ui/overlays-lanes.test.ts",
|
|
231
|
+
commit: "refactor-10-pe2-overlay-lanes",
|
|
232
|
+
},
|
|
233
|
+
uxIntent: {
|
|
234
|
+
uiVisible: true,
|
|
235
|
+
expectsUxResponse: "surface-refresh",
|
|
236
|
+
expectedDelta: "overlay-lane subscriber attached; future lane snapshots propagate through the listener without PM document transactions",
|
|
237
|
+
},
|
|
238
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-overlays-lane-subscribe" },
|
|
239
|
+
stateClass: "C-local",
|
|
240
|
+
persistsTo: "none",
|
|
241
|
+
bidirectional: true,
|
|
242
|
+
subscriptionShape: {
|
|
243
|
+
eventType: "ui.overlays.lane_changed",
|
|
244
|
+
payloadType: "UiOverlayLaneSnapshot",
|
|
245
|
+
coalescing: "raf",
|
|
246
|
+
},
|
|
247
|
+
rwdReference:
|
|
248
|
+
"§UI API § PE2 overlay lanes. Adapter delegates to UiController.subscribeOverlayLane(kind, listener). Presence/awareness, selection, search, and review lane updates are observable UI state only; this method does not dispatch PM document transactions or invalidate L04 layout.",
|
|
249
|
+
};
|
|
250
|
+
|
|
198
251
|
// ----- U9 overlay-visibility metadata (state-classes X3) -----
|
|
199
252
|
|
|
200
253
|
export const getVisibilityMetadata: ApiV3FnMetadata = {
|
|
@@ -354,6 +407,21 @@ export function createOverlaysFamily(ctx: UiApiContext) {
|
|
|
354
407
|
const queryChannels = new Map<string, QueryChannel>();
|
|
355
408
|
let queryCoarseUnsubscribe: (() => void) | null = null;
|
|
356
409
|
|
|
410
|
+
function unavailableLane(
|
|
411
|
+
kind: UiOverlayLaneKind,
|
|
412
|
+
reason = "overlay lane is not wired on the active controller",
|
|
413
|
+
): UiOverlayLaneSnapshot {
|
|
414
|
+
return {
|
|
415
|
+
__mock: true,
|
|
416
|
+
kind,
|
|
417
|
+
status: "unavailable",
|
|
418
|
+
entries: Object.freeze([]),
|
|
419
|
+
revision: 0,
|
|
420
|
+
source: "unavailable",
|
|
421
|
+
reason,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
357
425
|
function queryKey(q: OverlayAnchorQuery): string {
|
|
358
426
|
// `selection` is a singleton kind with no `value` — key by kind alone.
|
|
359
427
|
return q.kind === "selection" ? "selection" : `${q.kind}:${q.value}`;
|
|
@@ -560,6 +628,42 @@ export function createOverlaysFamily(ctx: UiApiContext) {
|
|
|
560
628
|
};
|
|
561
629
|
},
|
|
562
630
|
|
|
631
|
+
// ----- PE2 overlay lanes -----
|
|
632
|
+
|
|
633
|
+
getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot {
|
|
634
|
+
const hook = ctx.binding?.controller.getOverlayLane;
|
|
635
|
+
return hook
|
|
636
|
+
? hook(kind)
|
|
637
|
+
: unavailableLane(kind);
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
subscribeLane(
|
|
641
|
+
kind: UiOverlayLaneKind,
|
|
642
|
+
listener: UiListener<UiOverlayLaneSnapshot>,
|
|
643
|
+
): UiUnsubscribe {
|
|
644
|
+
const controller = ctx.binding?.controller;
|
|
645
|
+
if (!controller) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
"ui.overlays.subscribeLane: no controller bound — call ui.session.bind(controller) first",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!controller.subscribeOverlayLane) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`ui.overlays.subscribeLane: controller of kind "${controller.kind}" did not provide a subscribeOverlayLane hook`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const unsubscribe = controller.subscribeOverlayLane(kind, listener);
|
|
656
|
+
emitUxResponse(ctx.handle, {
|
|
657
|
+
apiFn: subscribeLaneMetadata.name,
|
|
658
|
+
intent: subscribeLaneMetadata.uxIntent.expectedDelta ?? "",
|
|
659
|
+
mockOrLive: "live-with-adapter",
|
|
660
|
+
uiVisible: true,
|
|
661
|
+
expectedDelta: subscribeLaneMetadata.uxIntent.expectedDelta,
|
|
662
|
+
actualDelta: { kind: "surface-refresh", payload: { subscribed: "ui.overlays.lane", lane: kind } },
|
|
663
|
+
});
|
|
664
|
+
return unsubscribe;
|
|
665
|
+
},
|
|
666
|
+
|
|
563
667
|
// ----- U9 overlay-visibility (state-classes X3) -----
|
|
564
668
|
|
|
565
669
|
getVisibility(kind: OverlayKind): OverlayVisibility {
|
|
@@ -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
|
+
}
|