@beyondwork/docx-react-component 1.0.40 → 1.0.42
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 +13 -1
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +347 -4
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +568 -1
- package/src/index.ts +118 -1
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +271 -0
- package/src/io/ooxml/workflow-payload.ts +146 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +280 -93
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +122 -12
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +230 -34
- package/src/runtime/layout/public-facet.ts +185 -13
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +587 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +11 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +32 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
|
@@ -319,6 +319,7 @@ export interface DocumentRuntime {
|
|
|
319
319
|
getSuggestionsSnapshot(): SuggestionsSnapshot;
|
|
320
320
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
321
321
|
clearWorkflowOverlay(): void;
|
|
322
|
+
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
322
323
|
getWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null;
|
|
323
324
|
getInteractionGuardSnapshot(): InteractionGuardSnapshot;
|
|
324
325
|
getWorkflowMarkupSnapshot(): WorkflowMarkupSnapshot;
|
|
@@ -1662,11 +1663,34 @@ export function createDocumentRuntime(
|
|
|
1662
1663
|
applyHistory("redo");
|
|
1663
1664
|
return;
|
|
1664
1665
|
}
|
|
1666
|
+
|
|
1667
|
+
if (isRuntimeStateOverlayCommand(command)) {
|
|
1668
|
+
applyRuntimeStateOverlayCommand(command);
|
|
1669
|
+
const context = {
|
|
1670
|
+
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1671
|
+
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1672
|
+
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1673
|
+
} as const;
|
|
1674
|
+
const noopTransaction: EditorTransaction = {
|
|
1675
|
+
nextState: state,
|
|
1676
|
+
mapping: createEmptyMapping(),
|
|
1677
|
+
effects: { warningsAdded: [], warningsCleared: [] },
|
|
1678
|
+
historyBoundary: "skip",
|
|
1679
|
+
markDirty: false,
|
|
1680
|
+
};
|
|
1681
|
+
options.onCommandApplied?.(command, noopTransaction, context, {
|
|
1682
|
+
preSelection: state.selection,
|
|
1683
|
+
activeStory,
|
|
1684
|
+
});
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1665
1688
|
try {
|
|
1666
1689
|
const context = {
|
|
1667
1690
|
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1668
1691
|
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1669
1692
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1693
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
1670
1694
|
} as const;
|
|
1671
1695
|
const preSelection = commandSelection;
|
|
1672
1696
|
const preActiveStory = activeStory;
|
|
@@ -1685,6 +1709,10 @@ export function createDocumentRuntime(
|
|
|
1685
1709
|
if (command.type === "history.undo" || command.type === "history.redo") {
|
|
1686
1710
|
return;
|
|
1687
1711
|
}
|
|
1712
|
+
if (isRuntimeStateOverlayCommand(command)) {
|
|
1713
|
+
applyRuntimeStateOverlayCommand(command);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1688
1716
|
if (meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory)) {
|
|
1689
1717
|
activeStory = meta.activeStory;
|
|
1690
1718
|
storySelections.set(
|
|
@@ -1695,7 +1723,17 @@ export function createDocumentRuntime(
|
|
|
1695
1723
|
const replayState = meta?.preSelection
|
|
1696
1724
|
? { ...state, selection: meta.preSelection }
|
|
1697
1725
|
: state;
|
|
1698
|
-
const
|
|
1726
|
+
const replaySnapshot = meta?.preSelection
|
|
1727
|
+
? {
|
|
1728
|
+
...cachedRenderSnapshot,
|
|
1729
|
+
selection: toPublicSelectionSnapshot(meta.preSelection, activeStory),
|
|
1730
|
+
}
|
|
1731
|
+
: cachedRenderSnapshot;
|
|
1732
|
+
const replayContext = {
|
|
1733
|
+
...context,
|
|
1734
|
+
renderSnapshot: replaySnapshot,
|
|
1735
|
+
};
|
|
1736
|
+
const transaction = executeEditorCommand(replayState, command, replayContext);
|
|
1699
1737
|
commitRemote(transaction);
|
|
1700
1738
|
} catch (error) {
|
|
1701
1739
|
emitError(toRuntimeError(error));
|
|
@@ -2156,6 +2194,9 @@ export function createDocumentRuntime(
|
|
|
2156
2194
|
state.selection.head,
|
|
2157
2195
|
activeStory,
|
|
2158
2196
|
options,
|
|
2197
|
+
// P5 — TOC entries print Word's display number (honors page-
|
|
2198
|
+
// number restarts), not the raw 0-based pageIndex+1.
|
|
2199
|
+
(pageIndex: number) => layoutFacet.getDisplayPageNumber(pageIndex),
|
|
2159
2200
|
);
|
|
2160
2201
|
if (refreshed.changed) {
|
|
2161
2202
|
this.dispatch({
|
|
@@ -2224,47 +2265,20 @@ export function createDocumentRuntime(
|
|
|
2224
2265
|
return result;
|
|
2225
2266
|
},
|
|
2226
2267
|
setWorkflowOverlay(overlay) {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
type: "workflow_overlay_changed",
|
|
2232
|
-
documentId: state.documentId,
|
|
2233
|
-
snapshot,
|
|
2268
|
+
this.dispatch({
|
|
2269
|
+
type: "workflow.set-overlay",
|
|
2270
|
+
overlay,
|
|
2271
|
+
origin: createOrigin("api", clock()),
|
|
2234
2272
|
});
|
|
2235
|
-
if (workflowOverlay.activeWorkItemId !== undefined) {
|
|
2236
|
-
emit({
|
|
2237
|
-
type: "workflow_active_work_item_changed",
|
|
2238
|
-
documentId: state.documentId,
|
|
2239
|
-
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
2240
|
-
});
|
|
2241
|
-
}
|
|
2242
|
-
for (const listener of listeners) {
|
|
2243
|
-
listener();
|
|
2244
|
-
}
|
|
2245
2273
|
},
|
|
2246
2274
|
clearWorkflowOverlay() {
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
type: "workflow_active_work_item_changed",
|
|
2251
|
-
documentId: state.documentId,
|
|
2252
|
-
activeWorkItemId: null,
|
|
2253
|
-
});
|
|
2254
|
-
emit({
|
|
2255
|
-
type: "workflow_overlay_changed",
|
|
2256
|
-
documentId: state.documentId,
|
|
2257
|
-
snapshot: {
|
|
2258
|
-
overlayPresent: false,
|
|
2259
|
-
activeWorkItemId: null,
|
|
2260
|
-
scopes: [],
|
|
2261
|
-
candidates: [],
|
|
2262
|
-
blockedReasons: [],
|
|
2263
|
-
},
|
|
2275
|
+
this.dispatch({
|
|
2276
|
+
type: "workflow.clear-overlay",
|
|
2277
|
+
origin: createOrigin("api", clock()),
|
|
2264
2278
|
});
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2279
|
+
},
|
|
2280
|
+
getWorkflowOverlay() {
|
|
2281
|
+
return workflowOverlay;
|
|
2268
2282
|
},
|
|
2269
2283
|
getWorkflowScopeSnapshot() {
|
|
2270
2284
|
return getCachedWorkflowScopeSnapshot();
|
|
@@ -2276,77 +2290,46 @@ export function createDocumentRuntime(
|
|
|
2276
2290
|
return getCachedWorkflowMarkupSnapshot();
|
|
2277
2291
|
},
|
|
2278
2292
|
setWorkflowMetadataDefinitions(definitions) {
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
documentId: state.documentId,
|
|
2284
|
-
snapshot,
|
|
2293
|
+
this.dispatch({
|
|
2294
|
+
type: "workflow.set-metadata-definitions",
|
|
2295
|
+
definitions,
|
|
2296
|
+
origin: createOrigin("api", clock()),
|
|
2285
2297
|
});
|
|
2286
|
-
for (const listener of listeners) {
|
|
2287
|
-
listener();
|
|
2288
|
-
}
|
|
2289
2298
|
},
|
|
2290
2299
|
clearWorkflowMetadataDefinitions() {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
type: "workflow_metadata_changed",
|
|
2295
|
-
documentId: state.documentId,
|
|
2296
|
-
snapshot,
|
|
2300
|
+
this.dispatch({
|
|
2301
|
+
type: "workflow.clear-metadata-definitions",
|
|
2302
|
+
origin: createOrigin("api", clock()),
|
|
2297
2303
|
});
|
|
2298
|
-
for (const listener of listeners) {
|
|
2299
|
-
listener();
|
|
2300
|
-
}
|
|
2301
2304
|
},
|
|
2302
2305
|
setWorkflowMetadataEntries(entries) {
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
documentId: state.documentId,
|
|
2308
|
-
snapshot,
|
|
2306
|
+
this.dispatch({
|
|
2307
|
+
type: "workflow.set-metadata-entries",
|
|
2308
|
+
entries,
|
|
2309
|
+
origin: createOrigin("api", clock()),
|
|
2309
2310
|
});
|
|
2310
|
-
for (const listener of listeners) {
|
|
2311
|
-
listener();
|
|
2312
|
-
}
|
|
2313
2311
|
},
|
|
2314
2312
|
clearWorkflowMetadataEntries() {
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
type: "workflow_metadata_changed",
|
|
2319
|
-
documentId: state.documentId,
|
|
2320
|
-
snapshot,
|
|
2313
|
+
this.dispatch({
|
|
2314
|
+
type: "workflow.clear-metadata-entries",
|
|
2315
|
+
origin: createOrigin("api", clock()),
|
|
2321
2316
|
});
|
|
2322
|
-
for (const listener of listeners) {
|
|
2323
|
-
listener();
|
|
2324
|
-
}
|
|
2325
2317
|
},
|
|
2326
2318
|
getWorkflowMetadataSnapshot() {
|
|
2327
2319
|
return deriveWorkflowMetadataSnapshot();
|
|
2328
2320
|
},
|
|
2329
2321
|
setHostAnnotationOverlay(overlay) {
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
snapshot: deriveHostAnnotationSnapshot(),
|
|
2322
|
+
this.dispatch({
|
|
2323
|
+
type: "host-annotation.set-overlay",
|
|
2324
|
+
overlay,
|
|
2325
|
+
origin: createOrigin("api", clock()),
|
|
2335
2326
|
});
|
|
2336
|
-
for (const listener of listeners) {
|
|
2337
|
-
listener();
|
|
2338
|
-
}
|
|
2339
2327
|
},
|
|
2340
2328
|
clearHostAnnotationOverlay() {
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
documentId: state.documentId,
|
|
2345
|
-
snapshot: deriveHostAnnotationSnapshot(),
|
|
2329
|
+
this.dispatch({
|
|
2330
|
+
type: "host-annotation.clear-overlay",
|
|
2331
|
+
origin: createOrigin("api", clock()),
|
|
2346
2332
|
});
|
|
2347
|
-
for (const listener of listeners) {
|
|
2348
|
-
listener();
|
|
2349
|
-
}
|
|
2350
2333
|
},
|
|
2351
2334
|
getHostAnnotationSnapshot() {
|
|
2352
2335
|
return deriveHostAnnotationSnapshot();
|
|
@@ -2480,9 +2463,14 @@ export function createDocumentRuntime(
|
|
|
2480
2463
|
// Font-loader refresh on subParts identity change — this is the
|
|
2481
2464
|
// lightweight proxy for "a change that could affect which fonts the
|
|
2482
2465
|
// canvas backend measures against". Typing edits don't rebuild
|
|
2483
|
-
// subParts; style + font + numbering imports do.
|
|
2466
|
+
// subParts; style + font + numbering imports do. Hardening: also
|
|
2467
|
+
// invalidate the measurement provider's glyph cache AND the engine's
|
|
2468
|
+
// cached graph so the next pagination run re-measures with the
|
|
2469
|
+
// newly-registered FontFaces (pre-hardening the canvas backend kept
|
|
2470
|
+
// returning pre-refresh widths from its cache).
|
|
2484
2471
|
if (previous.document.subParts !== state.document.subParts) {
|
|
2485
2472
|
fontLoader.refresh(collectFontLoaderInput(state.document));
|
|
2473
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2486
2474
|
}
|
|
2487
2475
|
|
|
2488
2476
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
@@ -2715,6 +2703,7 @@ export function createDocumentRuntime(
|
|
|
2715
2703
|
timestamp,
|
|
2716
2704
|
documentMode: getEffectiveDocumentMode(selection),
|
|
2717
2705
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
2706
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
2718
2707
|
} as const;
|
|
2719
2708
|
const baseState = selection === state.selection
|
|
2720
2709
|
? state
|
|
@@ -2953,6 +2942,109 @@ export function createDocumentRuntime(
|
|
|
2953
2942
|
}
|
|
2954
2943
|
}
|
|
2955
2944
|
|
|
2945
|
+
function applyRuntimeStateOverlayCommand(
|
|
2946
|
+
command: RuntimeStateOverlayCommand,
|
|
2947
|
+
): void {
|
|
2948
|
+
switch (command.type) {
|
|
2949
|
+
case "workflow.set-overlay": {
|
|
2950
|
+
workflowOverlay = structuredClone(command.overlay);
|
|
2951
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2952
|
+
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
2953
|
+
emit({
|
|
2954
|
+
type: "workflow_overlay_changed",
|
|
2955
|
+
documentId: state.documentId,
|
|
2956
|
+
snapshot,
|
|
2957
|
+
});
|
|
2958
|
+
if (workflowOverlay.activeWorkItemId !== undefined) {
|
|
2959
|
+
emit({
|
|
2960
|
+
type: "workflow_active_work_item_changed",
|
|
2961
|
+
documentId: state.documentId,
|
|
2962
|
+
activeWorkItemId: workflowOverlay.activeWorkItemId ?? null,
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2967
|
+
case "workflow.clear-overlay": {
|
|
2968
|
+
workflowOverlay = null;
|
|
2969
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2970
|
+
emit({
|
|
2971
|
+
type: "workflow_active_work_item_changed",
|
|
2972
|
+
documentId: state.documentId,
|
|
2973
|
+
activeWorkItemId: null,
|
|
2974
|
+
});
|
|
2975
|
+
emit({
|
|
2976
|
+
type: "workflow_overlay_changed",
|
|
2977
|
+
documentId: state.documentId,
|
|
2978
|
+
snapshot: {
|
|
2979
|
+
overlayPresent: false,
|
|
2980
|
+
activeWorkItemId: null,
|
|
2981
|
+
scopes: [],
|
|
2982
|
+
candidates: [],
|
|
2983
|
+
blockedReasons: [],
|
|
2984
|
+
},
|
|
2985
|
+
});
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
case "workflow.set-metadata-definitions": {
|
|
2989
|
+
workflowMetadataDefinitions = structuredClone(command.definitions);
|
|
2990
|
+
emit({
|
|
2991
|
+
type: "workflow_metadata_changed",
|
|
2992
|
+
documentId: state.documentId,
|
|
2993
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
2994
|
+
});
|
|
2995
|
+
break;
|
|
2996
|
+
}
|
|
2997
|
+
case "workflow.clear-metadata-definitions": {
|
|
2998
|
+
workflowMetadataDefinitions = [];
|
|
2999
|
+
emit({
|
|
3000
|
+
type: "workflow_metadata_changed",
|
|
3001
|
+
documentId: state.documentId,
|
|
3002
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3003
|
+
});
|
|
3004
|
+
break;
|
|
3005
|
+
}
|
|
3006
|
+
case "workflow.set-metadata-entries": {
|
|
3007
|
+
workflowMetadataEntries = structuredClone(command.entries);
|
|
3008
|
+
emit({
|
|
3009
|
+
type: "workflow_metadata_changed",
|
|
3010
|
+
documentId: state.documentId,
|
|
3011
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3012
|
+
});
|
|
3013
|
+
break;
|
|
3014
|
+
}
|
|
3015
|
+
case "workflow.clear-metadata-entries": {
|
|
3016
|
+
workflowMetadataEntries = [];
|
|
3017
|
+
emit({
|
|
3018
|
+
type: "workflow_metadata_changed",
|
|
3019
|
+
documentId: state.documentId,
|
|
3020
|
+
snapshot: deriveWorkflowMetadataSnapshot(),
|
|
3021
|
+
});
|
|
3022
|
+
break;
|
|
3023
|
+
}
|
|
3024
|
+
case "host-annotation.set-overlay": {
|
|
3025
|
+
hostAnnotationOverlay = structuredClone(command.overlay);
|
|
3026
|
+
emit({
|
|
3027
|
+
type: "host_annotation_overlay_changed",
|
|
3028
|
+
documentId: state.documentId,
|
|
3029
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
3030
|
+
});
|
|
3031
|
+
break;
|
|
3032
|
+
}
|
|
3033
|
+
case "host-annotation.clear-overlay": {
|
|
3034
|
+
hostAnnotationOverlay = null;
|
|
3035
|
+
emit({
|
|
3036
|
+
type: "host_annotation_overlay_changed",
|
|
3037
|
+
documentId: state.documentId,
|
|
3038
|
+
snapshot: deriveHostAnnotationSnapshot(),
|
|
3039
|
+
});
|
|
3040
|
+
break;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
for (const listener of listeners) {
|
|
3044
|
+
listener();
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2956
3048
|
function emitInternal(event: DocumentRuntimeEvent): void {
|
|
2957
3049
|
options.onEvent?.(event);
|
|
2958
3050
|
for (const listener of eventListeners) {
|
|
@@ -3111,6 +3203,39 @@ function normalizeCommandTimestamp(value: unknown): string | undefined {
|
|
|
3111
3203
|
return undefined;
|
|
3112
3204
|
}
|
|
3113
3205
|
|
|
3206
|
+
type RuntimeStateOverlayCommand = Extract<
|
|
3207
|
+
EditorCommand,
|
|
3208
|
+
{
|
|
3209
|
+
type:
|
|
3210
|
+
| "workflow.set-overlay"
|
|
3211
|
+
| "workflow.clear-overlay"
|
|
3212
|
+
| "workflow.set-metadata-definitions"
|
|
3213
|
+
| "workflow.clear-metadata-definitions"
|
|
3214
|
+
| "workflow.set-metadata-entries"
|
|
3215
|
+
| "workflow.clear-metadata-entries"
|
|
3216
|
+
| "host-annotation.set-overlay"
|
|
3217
|
+
| "host-annotation.clear-overlay";
|
|
3218
|
+
}
|
|
3219
|
+
>;
|
|
3220
|
+
|
|
3221
|
+
function isRuntimeStateOverlayCommand(
|
|
3222
|
+
command: EditorCommand,
|
|
3223
|
+
): command is RuntimeStateOverlayCommand {
|
|
3224
|
+
switch (command.type) {
|
|
3225
|
+
case "workflow.set-overlay":
|
|
3226
|
+
case "workflow.clear-overlay":
|
|
3227
|
+
case "workflow.set-metadata-definitions":
|
|
3228
|
+
case "workflow.clear-metadata-definitions":
|
|
3229
|
+
case "workflow.set-metadata-entries":
|
|
3230
|
+
case "workflow.clear-metadata-entries":
|
|
3231
|
+
case "host-annotation.set-overlay":
|
|
3232
|
+
case "host-annotation.clear-overlay":
|
|
3233
|
+
return true;
|
|
3234
|
+
default:
|
|
3235
|
+
return false;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3114
3239
|
function finalizeState(
|
|
3115
3240
|
state: EditorState,
|
|
3116
3241
|
markDirty: boolean,
|
|
@@ -3684,6 +3809,14 @@ const NON_MUTATION_COMMANDS = new Set([
|
|
|
3684
3809
|
"warning.add",
|
|
3685
3810
|
"warning.clear",
|
|
3686
3811
|
"comment.open",
|
|
3812
|
+
"workflow.set-overlay",
|
|
3813
|
+
"workflow.clear-overlay",
|
|
3814
|
+
"workflow.set-metadata-definitions",
|
|
3815
|
+
"workflow.clear-metadata-definitions",
|
|
3816
|
+
"workflow.set-metadata-entries",
|
|
3817
|
+
"workflow.clear-metadata-entries",
|
|
3818
|
+
"host-annotation.set-overlay",
|
|
3819
|
+
"host-annotation.clear-overlay",
|
|
3687
3820
|
]);
|
|
3688
3821
|
|
|
3689
3822
|
/** Mutation commands that are not yet supported in suggesting mode. */
|
|
@@ -4008,6 +4141,7 @@ function refreshDocumentTableOfContents(
|
|
|
4008
4141
|
selectionHead: number,
|
|
4009
4142
|
activeStory: EditorStoryTarget,
|
|
4010
4143
|
options?: TocRefreshOptions,
|
|
4144
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4011
4145
|
): {
|
|
4012
4146
|
document: CanonicalDocumentEnvelope;
|
|
4013
4147
|
result: TocRefreshResult;
|
|
@@ -4038,7 +4172,7 @@ function refreshDocumentTableOfContents(
|
|
|
4038
4172
|
}
|
|
4039
4173
|
const nextField: FieldNode = {
|
|
4040
4174
|
...field,
|
|
4041
|
-
children: buildTocInlineNodes(entries),
|
|
4175
|
+
children: buildTocInlineNodes(entries, resolveDisplayPageNumber),
|
|
4042
4176
|
refreshStatus: "current",
|
|
4043
4177
|
};
|
|
4044
4178
|
if (flattenInlineDisplayText(nextField.children) !== flattenInlineDisplayText(field.children)) {
|
|
@@ -4221,14 +4355,26 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
4221
4355
|
return children;
|
|
4222
4356
|
}
|
|
4223
4357
|
|
|
4358
|
+
/**
|
|
4359
|
+
* P5 — TOC entry rendering with display-number resolution. When
|
|
4360
|
+
* `resolveDisplayPageNumber` is supplied, TOC entries print the number
|
|
4361
|
+
* Word would print on the page (honors `w:pgNumType/@w:start` restarts
|
|
4362
|
+
* for front-matter roman numerals → body arabic restart). Without the
|
|
4363
|
+
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
4364
|
+
*/
|
|
4224
4365
|
function buildTocInlineNodes(
|
|
4225
4366
|
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
4367
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4226
4368
|
): InlineNode[] {
|
|
4227
4369
|
const children: InlineNode[] = [];
|
|
4228
4370
|
entries.forEach((entry, index) => {
|
|
4229
4371
|
children.push({ type: "text", text: entry.text });
|
|
4230
4372
|
children.push({ type: "tab" });
|
|
4231
|
-
|
|
4373
|
+
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
4374
|
+
children.push({
|
|
4375
|
+
type: "text",
|
|
4376
|
+
text: String(displayed ?? entry.pageIndex + 1),
|
|
4377
|
+
});
|
|
4232
4378
|
if (index < entries.length - 1) {
|
|
4233
4379
|
children.push({ type: "hard_break" });
|
|
4234
4380
|
}
|
|
@@ -4236,6 +4382,14 @@ function buildTocInlineNodes(
|
|
|
4236
4382
|
return children;
|
|
4237
4383
|
}
|
|
4238
4384
|
|
|
4385
|
+
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
4386
|
+
export function __buildTocInlineNodes(
|
|
4387
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
4388
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
4389
|
+
): InlineNode[] {
|
|
4390
|
+
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4239
4393
|
function collectFieldsFromSubParts(
|
|
4240
4394
|
subParts: SubPartsCatalog | undefined,
|
|
4241
4395
|
entries: FieldEntrySnapshot[],
|
|
@@ -4575,8 +4729,38 @@ function remapProtectionSnapshot(
|
|
|
4575
4729
|
* let the loader register system fonts it finds via
|
|
4576
4730
|
* `document.fonts.check(...)`.
|
|
4577
4731
|
*/
|
|
4732
|
+
// P14.d — memoize the font-family walk by `(content, styles)` reference
|
|
4733
|
+
// identity. Both shapes change identity only on a real edit / import /
|
|
4734
|
+
// style mutation; per-keystroke edits keep the same references because
|
|
4735
|
+
// `finalizeState` shallow-spreads `state.document`. WeakMap two-level
|
|
4736
|
+
// cache so the entries free with the documents that own them.
|
|
4737
|
+
const fontLoaderInputCache = new WeakMap<
|
|
4738
|
+
object,
|
|
4739
|
+
WeakMap<object, { families: readonly string[] }>
|
|
4740
|
+
>();
|
|
4741
|
+
|
|
4578
4742
|
function collectFontLoaderInput(
|
|
4579
4743
|
document: CanonicalDocumentEnvelope,
|
|
4744
|
+
): { families: readonly string[] } {
|
|
4745
|
+
const contentKey = document.content as unknown as object;
|
|
4746
|
+
const stylesKey = (document.styles ?? FONT_LOADER_EMPTY_STYLES_KEY) as unknown as object;
|
|
4747
|
+
let stylesCache = fontLoaderInputCache.get(contentKey);
|
|
4748
|
+
if (stylesCache) {
|
|
4749
|
+
const cached = stylesCache.get(stylesKey);
|
|
4750
|
+
if (cached) return cached;
|
|
4751
|
+
} else {
|
|
4752
|
+
stylesCache = new WeakMap();
|
|
4753
|
+
fontLoaderInputCache.set(contentKey, stylesCache);
|
|
4754
|
+
}
|
|
4755
|
+
const result = collectFontLoaderInputUncached(document);
|
|
4756
|
+
stylesCache.set(stylesKey, result);
|
|
4757
|
+
return result;
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
const FONT_LOADER_EMPTY_STYLES_KEY = Object.freeze({});
|
|
4761
|
+
|
|
4762
|
+
function collectFontLoaderInputUncached(
|
|
4763
|
+
document: CanonicalDocumentEnvelope,
|
|
4580
4764
|
): { families: readonly string[] } {
|
|
4581
4765
|
try {
|
|
4582
4766
|
const families = new Set<string>();
|
|
@@ -4604,6 +4788,9 @@ function collectFontLoaderInput(
|
|
|
4604
4788
|
}
|
|
4605
4789
|
}
|
|
4606
4790
|
|
|
4791
|
+
/** Test-only export of the uncached walk so memoization tests can spy on it. */
|
|
4792
|
+
export const __collectFontLoaderInputUncached = collectFontLoaderInputUncached;
|
|
4793
|
+
|
|
4607
4794
|
/**
|
|
4608
4795
|
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
4609
4796
|
* the platform supports it and fonts have resolved. Errors are swallowed
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sendToExternal,
|
|
3
|
+
type SendToExternalBlock,
|
|
4
|
+
type SendToExternalResult,
|
|
5
|
+
} from "../io/export/external-send.ts";
|
|
6
|
+
import type {
|
|
7
|
+
ExternalCustodyResolver,
|
|
8
|
+
} from "../api/external-custody-types.ts";
|
|
9
|
+
import type { CollabSessionBridge } from "./collab-session-bridge.ts";
|
|
10
|
+
import { resignPayload } from "./resign-payload.ts";
|
|
11
|
+
import type { PayloadSigner } from "../io/ooxml/payload-signature.ts";
|
|
12
|
+
import type { TamperGate } from "./tamper-gate.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runtime-level composition of the P7 pure pipeline with:
|
|
16
|
+
* - the P8c `CollabSessionBridge` (snapshots of negotiation /
|
|
17
|
+
* presentation / participants)
|
|
18
|
+
* - the P8e `TamperGate` (blocks when `metadataIntegrity === "tampered"`)
|
|
19
|
+
* - the P8a `resignPayload()` hook (every write re-signs)
|
|
20
|
+
*
|
|
21
|
+
* Callers supply the raw workflow-payload XML alongside the collab
|
|
22
|
+
* state. On success the result carries:
|
|
23
|
+
* - the custody receipt (to be emitted inside bw:extensions)
|
|
24
|
+
* - the kept snapshots (to replace the pre-send facet state)
|
|
25
|
+
* - the re-signed payload XML (to persist in the shipped docx)
|
|
26
|
+
*
|
|
27
|
+
* This helper does NOT rewrite `word/document.xml` / `word/comments.xml`
|
|
28
|
+
* or the three companion parts; the caller owns the zip rewrite using
|
|
29
|
+
* `result.stripped.commentIds`. This keeps the runtime layer isolated
|
|
30
|
+
* from OPC packaging.
|
|
31
|
+
*/
|
|
32
|
+
export interface RuntimeSendToExternalArgs {
|
|
33
|
+
bridge: CollabSessionBridge;
|
|
34
|
+
tamperGate: TamperGate;
|
|
35
|
+
signer: PayloadSigner;
|
|
36
|
+
|
|
37
|
+
/** The raw `<bw:workflowPayload …>…</bw:workflowPayload>` XML. */
|
|
38
|
+
payloadXml: string;
|
|
39
|
+
|
|
40
|
+
role: "author" | "reviewer" | "observer";
|
|
41
|
+
|
|
42
|
+
originDocumentId: string;
|
|
43
|
+
originPayloadId: string;
|
|
44
|
+
/** sha256:{hex} of canonicalized word/document.xml at send time. */
|
|
45
|
+
originContentHash: string;
|
|
46
|
+
|
|
47
|
+
resolver: ExternalCustodyResolver;
|
|
48
|
+
recipient: string;
|
|
49
|
+
sentBy: string;
|
|
50
|
+
archiveRef: string;
|
|
51
|
+
|
|
52
|
+
/** Optional deterministic overrides for tests. */
|
|
53
|
+
custodyId?: string;
|
|
54
|
+
now?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type RuntimeSendToExternalResult =
|
|
58
|
+
| { ok: false; reason: "collab_role_restricted" }
|
|
59
|
+
| { ok: false; reason: "metadata_tampered" }
|
|
60
|
+
| {
|
|
61
|
+
ok: true;
|
|
62
|
+
custody: SendToExternalResult["custody"];
|
|
63
|
+
kept: SendToExternalResult["kept"];
|
|
64
|
+
stripped: SendToExternalResult["stripped"];
|
|
65
|
+
/** Re-signed `<bw:workflowPayload …>…</bw:workflowPayload>` ready to persist. */
|
|
66
|
+
payloadXml: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export async function runtimeSendToExternal(
|
|
70
|
+
args: RuntimeSendToExternalArgs,
|
|
71
|
+
): Promise<RuntimeSendToExternalResult> {
|
|
72
|
+
const guard = args.tamperGate.guard();
|
|
73
|
+
if (!guard.ok) {
|
|
74
|
+
return { ok: false, reason: guard.reason };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const presentation = args.bridge.getCommentPresentationSnapshot();
|
|
78
|
+
const negotiation = args.bridge.getCommentNegotiationSnapshot();
|
|
79
|
+
const participants = args.bridge.getParticipantRoster();
|
|
80
|
+
|
|
81
|
+
const pipelineArgs = {
|
|
82
|
+
presentation,
|
|
83
|
+
negotiation,
|
|
84
|
+
participants,
|
|
85
|
+
role: args.role,
|
|
86
|
+
metadataIntegrity:
|
|
87
|
+
args.tamperGate.state === "unsigned" ? "verified" : args.tamperGate.state,
|
|
88
|
+
originDocumentId: args.originDocumentId,
|
|
89
|
+
originPayloadId: args.originPayloadId,
|
|
90
|
+
originContentHash: args.originContentHash,
|
|
91
|
+
resolver: args.resolver,
|
|
92
|
+
recipient: args.recipient,
|
|
93
|
+
sentBy: args.sentBy,
|
|
94
|
+
archiveRef: args.archiveRef,
|
|
95
|
+
...(args.custodyId !== undefined ? { custodyId: args.custodyId } : {}),
|
|
96
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
const pipeline: SendToExternalBlock = await sendToExternal(pipelineArgs);
|
|
100
|
+
if (!pipeline.ok) {
|
|
101
|
+
return { ok: false, reason: pipeline.reason };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { payloadXml } = await resignPayload({
|
|
105
|
+
payloadXml: args.payloadXml,
|
|
106
|
+
signer: args.signer,
|
|
107
|
+
...(args.now !== undefined ? { now: args.now } : {}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
custody: pipeline.result.custody,
|
|
113
|
+
kept: pipeline.result.kept,
|
|
114
|
+
stripped: pipeline.result.stripped,
|
|
115
|
+
payloadXml,
|
|
116
|
+
};
|
|
117
|
+
}
|