@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
persistedSnapshotFromEditorSessionState,
|
|
83
83
|
} from "../api/session-state.ts";
|
|
84
84
|
import {
|
|
85
|
+
type CommandExecutionContext,
|
|
85
86
|
executeEditorCommand,
|
|
86
87
|
selectionChanged,
|
|
87
88
|
type CommandOrigin,
|
|
@@ -124,11 +125,16 @@ import {
|
|
|
124
125
|
findPageForOffset,
|
|
125
126
|
} from "./document-navigation.ts";
|
|
126
127
|
import {
|
|
128
|
+
createDocxFontLoader,
|
|
127
129
|
createLayoutEngine,
|
|
128
130
|
createLayoutFacet,
|
|
131
|
+
createMeasurementProvider,
|
|
132
|
+
type DocxFontLoader,
|
|
129
133
|
type LayoutEngineInstance,
|
|
134
|
+
type LayoutMeasurementProvider,
|
|
130
135
|
type WordReviewEditorLayoutFacet,
|
|
131
136
|
} from "./layout/index.ts";
|
|
137
|
+
import { createRenderKernel, type RenderKernel } from "./render/index.ts";
|
|
132
138
|
import {
|
|
133
139
|
createDocumentOutlineSnapshot,
|
|
134
140
|
createDocumentSectionSnapshots,
|
|
@@ -174,6 +180,8 @@ import {
|
|
|
174
180
|
setCaretAffinity as applyCaretAffinity,
|
|
175
181
|
setActivePageRegion as applyActivePageRegion,
|
|
176
182
|
setActiveObjectFrame as applyActiveObjectFrame,
|
|
183
|
+
setEditorRole as applyEditorRole,
|
|
184
|
+
setChromePin as applyChromePins,
|
|
177
185
|
createEditorViewStateSnapshot,
|
|
178
186
|
type ViewState,
|
|
179
187
|
} from "./view-state.ts";
|
|
@@ -224,6 +232,24 @@ export interface DocumentRuntime {
|
|
|
224
232
|
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
225
233
|
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
|
|
226
234
|
dispatch(command: EditorCommand): void;
|
|
235
|
+
/**
|
|
236
|
+
* Apply a command received from a remote collaborator. The command
|
|
237
|
+
* executes through `executeEditorCommand` exactly like a local dispatch,
|
|
238
|
+
* but:
|
|
239
|
+
* - `onCommandApplied` is NOT fired (no echo back to the network)
|
|
240
|
+
* - the local undo/redo history is NOT mutated (remote edits are
|
|
241
|
+
* not undoable by the local user)
|
|
242
|
+
* - local workflow/blocked-command checks are bypassed (the remote
|
|
243
|
+
* already performed them)
|
|
244
|
+
*
|
|
245
|
+
* Used by runtime-level collaboration sync to replay `CommandEvent`s
|
|
246
|
+
* from the shared event log.
|
|
247
|
+
*/
|
|
248
|
+
applyRemoteCommand(
|
|
249
|
+
command: EditorCommand,
|
|
250
|
+
context: CommandExecutionContext,
|
|
251
|
+
meta?: Partial<CommandAppliedMeta>,
|
|
252
|
+
): void;
|
|
227
253
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
228
254
|
undo(): void;
|
|
229
255
|
redo(): void;
|
|
@@ -250,6 +276,8 @@ export interface DocumentRuntime {
|
|
|
250
276
|
getProtectionSnapshot(): ProtectionSnapshot;
|
|
251
277
|
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
252
278
|
setZoom(level: ZoomLevel): void;
|
|
279
|
+
setEditorRole(role: import("./view-state.ts").ViewState["editorRole"]): void;
|
|
280
|
+
setChromePin(surface: import("../api/public-types.ts").ChromePinSurface, pin: import("../api/public-types.ts").PinState | null): void;
|
|
253
281
|
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
254
282
|
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
255
283
|
/**
|
|
@@ -314,6 +342,11 @@ export interface DocumentRuntime {
|
|
|
314
342
|
): RuntimeContextAnalyticsSnapshot | null;
|
|
315
343
|
}
|
|
316
344
|
|
|
345
|
+
export interface CommandAppliedMeta {
|
|
346
|
+
preSelection: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
347
|
+
activeStory: EditorStoryTarget;
|
|
348
|
+
}
|
|
349
|
+
|
|
317
350
|
export interface CreateDocumentRuntimeOptions {
|
|
318
351
|
documentId: string;
|
|
319
352
|
initialSessionState?: EditorSessionState;
|
|
@@ -333,6 +366,23 @@ export interface CreateDocumentRuntimeOptions {
|
|
|
333
366
|
onEvent?: (event: DocumentRuntimeEvent) => void;
|
|
334
367
|
onWarning?: (warning: EditorWarning) => void;
|
|
335
368
|
onError?: (error: EditorError) => void;
|
|
369
|
+
/**
|
|
370
|
+
* Fired AFTER a command has been applied locally via `dispatch()` or
|
|
371
|
+
* `applyActiveStoryTextCommand()`. Used by collaboration sync to
|
|
372
|
+
* broadcast the command to remote clients. NOT fired for remote commands
|
|
373
|
+
* applied via `applyRemoteCommand()` — this prevents echo loops.
|
|
374
|
+
*
|
|
375
|
+
* Not fired for:
|
|
376
|
+
* - `history.undo` / `history.redo` (applied via `applyHistory()` which
|
|
377
|
+
* does not pass through `commit()`'s new hook call site)
|
|
378
|
+
* - Remote replays applied via `applyRemoteCommand()`
|
|
379
|
+
*/
|
|
380
|
+
onCommandApplied?: (
|
|
381
|
+
command: EditorCommand,
|
|
382
|
+
transaction: EditorTransaction,
|
|
383
|
+
context: CommandExecutionContext,
|
|
384
|
+
meta: CommandAppliedMeta,
|
|
385
|
+
) => void;
|
|
336
386
|
initialViewState?: Partial<ViewState>;
|
|
337
387
|
protectionSnapshot?: ProtectionSnapshot;
|
|
338
388
|
}
|
|
@@ -405,7 +455,20 @@ export function createDocumentRuntime(
|
|
|
405
455
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
406
456
|
// (content, styles, subParts). It is the single internal source of truth
|
|
407
457
|
// for page composition, story resolution, and layout invalidation.
|
|
458
|
+
//
|
|
459
|
+
// R0 measurement wiring: the engine starts with the sync empirical backend
|
|
460
|
+
// so the runtime is available immediately, then we kick off
|
|
461
|
+
// `createMeasurementProvider({ preference: "auto", fontLoader })` which
|
|
462
|
+
// upgrades to the canvas backend once fonts resolve. `swapMeasurementProvider`
|
|
463
|
+
// emits `measurement_backend_ready` so chrome consumers can re-read metrics.
|
|
408
464
|
const layoutEngine: LayoutEngineInstance = createLayoutEngine();
|
|
465
|
+
const fontLoader: DocxFontLoader = createDocxFontLoader(
|
|
466
|
+
collectFontLoaderInput(state.document),
|
|
467
|
+
);
|
|
468
|
+
void upgradeMeasurementProvider(layoutEngine, fontLoader);
|
|
469
|
+
// `renderKernelRef` is a forward reference so the facet can reach the
|
|
470
|
+
// kernel after it is created below (kernel creation needs the facet).
|
|
471
|
+
let renderKernelRef: RenderKernel | null = null;
|
|
409
472
|
const layoutFacet: WordReviewEditorLayoutFacet = createLayoutFacet({
|
|
410
473
|
engine: layoutEngine,
|
|
411
474
|
getQueryInput: () => ({
|
|
@@ -416,6 +479,27 @@ export function createDocumentRuntime(
|
|
|
416
479
|
zoomLevel: viewState.zoomLevel,
|
|
417
480
|
},
|
|
418
481
|
}),
|
|
482
|
+
renderKernel: () => renderKernelRef,
|
|
483
|
+
getWorkflowRailInput: () => {
|
|
484
|
+
if (!workflowOverlay) return null;
|
|
485
|
+
const activeWorkItemId = workflowOverlay.activeWorkItemId ?? null;
|
|
486
|
+
const activeWorkItem =
|
|
487
|
+
activeWorkItemId !== null
|
|
488
|
+
? workflowOverlay.workItems?.find(
|
|
489
|
+
(item) => item.workItemId === activeWorkItemId,
|
|
490
|
+
)
|
|
491
|
+
: undefined;
|
|
492
|
+
return {
|
|
493
|
+
scopes: workflowOverlay.scopes,
|
|
494
|
+
candidates: workflowOverlay.candidates,
|
|
495
|
+
activeWorkItemScopeIds: activeWorkItem?.scopeIds ?? [],
|
|
496
|
+
activeStory,
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
renderKernelRef = createRenderKernel({
|
|
501
|
+
facet: layoutFacet,
|
|
502
|
+
getActiveStory: () => activeStory,
|
|
419
503
|
});
|
|
420
504
|
let cachedSurface:
|
|
421
505
|
| {
|
|
@@ -1572,12 +1656,40 @@ export function createDocumentRuntime(
|
|
|
1572
1656
|
return;
|
|
1573
1657
|
}
|
|
1574
1658
|
try {
|
|
1575
|
-
const
|
|
1576
|
-
timestamp: command.origin?.timestamp ?? clock(),
|
|
1659
|
+
const context = {
|
|
1660
|
+
timestamp: normalizeCommandTimestamp(command.origin?.timestamp) ?? clock(),
|
|
1577
1661
|
documentMode: getEffectiveDocumentMode(commandSelection),
|
|
1578
1662
|
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
1579
|
-
}
|
|
1663
|
+
} as const;
|
|
1664
|
+
const preSelection = commandSelection;
|
|
1665
|
+
const preActiveStory = activeStory;
|
|
1666
|
+
const transaction = executeEditorCommand(state, command, context);
|
|
1580
1667
|
commit(transaction);
|
|
1668
|
+
options.onCommandApplied?.(command, transaction, context, {
|
|
1669
|
+
preSelection,
|
|
1670
|
+
activeStory: preActiveStory,
|
|
1671
|
+
});
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
emitError(toRuntimeError(error));
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
applyRemoteCommand(command, context, meta) {
|
|
1677
|
+
try {
|
|
1678
|
+
if (command.type === "history.undo" || command.type === "history.redo") {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory)) {
|
|
1682
|
+
activeStory = meta.activeStory;
|
|
1683
|
+
storySelections.set(
|
|
1684
|
+
storyTargetKey(activeStory),
|
|
1685
|
+
meta.preSelection ?? state.selection,
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
const replayState = meta?.preSelection
|
|
1689
|
+
? { ...state, selection: meta.preSelection }
|
|
1690
|
+
: state;
|
|
1691
|
+
const transaction = executeEditorCommand(replayState, command, context);
|
|
1692
|
+
commitRemote(transaction);
|
|
1581
1693
|
} catch (error) {
|
|
1582
1694
|
emitError(toRuntimeError(error));
|
|
1583
1695
|
}
|
|
@@ -1851,6 +1963,20 @@ export function createDocumentRuntime(
|
|
|
1851
1963
|
listener();
|
|
1852
1964
|
}
|
|
1853
1965
|
},
|
|
1966
|
+
setEditorRole(role) {
|
|
1967
|
+
viewState = applyEditorRole(viewState, role);
|
|
1968
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1969
|
+
for (const listener of listeners) {
|
|
1970
|
+
listener();
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
setChromePin(surface, pin) {
|
|
1974
|
+
viewState = applyChromePins(viewState, surface, pin);
|
|
1975
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
1976
|
+
for (const listener of listeners) {
|
|
1977
|
+
listener();
|
|
1978
|
+
}
|
|
1979
|
+
},
|
|
1854
1980
|
getPageLayoutSnapshot() {
|
|
1855
1981
|
return getCachedPageLayoutSnapshot(state, activeStory);
|
|
1856
1982
|
},
|
|
@@ -2307,13 +2433,21 @@ export function createDocumentRuntime(
|
|
|
2307
2433
|
}
|
|
2308
2434
|
|
|
2309
2435
|
function commit(transaction: EditorTransaction): void {
|
|
2310
|
-
const previous = state;
|
|
2311
|
-
|
|
2312
2436
|
if (transaction.historyBoundary === "push") {
|
|
2313
2437
|
history.past.push(state);
|
|
2314
2438
|
history.future = [];
|
|
2315
2439
|
}
|
|
2316
2440
|
|
|
2441
|
+
applyTransactionToState(transaction);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
function commitRemote(transaction: EditorTransaction): void {
|
|
2445
|
+
applyTransactionToState(transaction);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function applyTransactionToState(transaction: EditorTransaction): void {
|
|
2449
|
+
const previous = state;
|
|
2450
|
+
|
|
2317
2451
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
2318
2452
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
2319
2453
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
@@ -2336,6 +2470,14 @@ export function createDocumentRuntime(
|
|
|
2336
2470
|
}
|
|
2337
2471
|
}
|
|
2338
2472
|
|
|
2473
|
+
// Font-loader refresh on subParts identity change — this is the
|
|
2474
|
+
// lightweight proxy for "a change that could affect which fonts the
|
|
2475
|
+
// canvas backend measures against". Typing edits don't rebuild
|
|
2476
|
+
// subParts; style + font + numbering imports do.
|
|
2477
|
+
if (previous.document.subParts !== state.document.subParts) {
|
|
2478
|
+
fontLoader.refresh(collectFontLoaderInput(state.document));
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2339
2481
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2340
2482
|
notify(previous, state, transaction);
|
|
2341
2483
|
}
|
|
@@ -2515,13 +2657,13 @@ export function createDocumentRuntime(
|
|
|
2515
2657
|
|
|
2516
2658
|
function applyTextCommandInActiveStory(
|
|
2517
2659
|
command: ActiveStoryTextCommand,
|
|
2518
|
-
|
|
2660
|
+
textOptions: {
|
|
2519
2661
|
selection?: EditorState["selection"];
|
|
2520
2662
|
blockedCommandName?: string;
|
|
2521
2663
|
} = {},
|
|
2522
2664
|
): TextCommandAck {
|
|
2523
2665
|
const opId = (command.origin as { opId?: string } | undefined)?.opId;
|
|
2524
|
-
const selection =
|
|
2666
|
+
const selection = textOptions.selection ?? state.selection;
|
|
2525
2667
|
if (
|
|
2526
2668
|
activeStory.kind !== "main" &&
|
|
2527
2669
|
getEffectiveDocumentMode(selection) === "suggesting" &&
|
|
@@ -2531,7 +2673,7 @@ export function createDocumentRuntime(
|
|
|
2531
2673
|
emit({
|
|
2532
2674
|
type: "command_blocked",
|
|
2533
2675
|
documentId: state.documentId,
|
|
2534
|
-
command:
|
|
2676
|
+
command: textOptions.blockedCommandName ?? command.type,
|
|
2535
2677
|
reasons: [{
|
|
2536
2678
|
code: "suggesting_unsupported",
|
|
2537
2679
|
message,
|
|
@@ -2550,7 +2692,7 @@ export function createDocumentRuntime(
|
|
|
2550
2692
|
emit({
|
|
2551
2693
|
type: "command_blocked",
|
|
2552
2694
|
documentId: state.documentId,
|
|
2553
|
-
command:
|
|
2695
|
+
command: textOptions.blockedCommandName ?? command.type,
|
|
2554
2696
|
reasons: blockedReasons,
|
|
2555
2697
|
});
|
|
2556
2698
|
return {
|
|
@@ -2561,7 +2703,7 @@ export function createDocumentRuntime(
|
|
|
2561
2703
|
};
|
|
2562
2704
|
}
|
|
2563
2705
|
|
|
2564
|
-
const timestamp = command.origin?.timestamp ?? clock();
|
|
2706
|
+
const timestamp = normalizeCommandTimestamp(command.origin?.timestamp) ?? clock();
|
|
2565
2707
|
const context = {
|
|
2566
2708
|
timestamp,
|
|
2567
2709
|
documentMode: getEffectiveDocumentMode(selection),
|
|
@@ -2574,9 +2716,15 @@ export function createDocumentRuntime(
|
|
|
2574
2716
|
selection,
|
|
2575
2717
|
};
|
|
2576
2718
|
|
|
2719
|
+
const preSelection = selection;
|
|
2720
|
+
const preActiveStory = activeStory;
|
|
2577
2721
|
if (activeStory.kind === "main") {
|
|
2578
2722
|
const mainTransaction = executeEditorCommand(baseState, command, context);
|
|
2579
2723
|
commit(mainTransaction);
|
|
2724
|
+
options.onCommandApplied?.(command, mainTransaction, context, {
|
|
2725
|
+
preSelection,
|
|
2726
|
+
activeStory: preActiveStory,
|
|
2727
|
+
});
|
|
2580
2728
|
return classifyAck({
|
|
2581
2729
|
command,
|
|
2582
2730
|
opId,
|
|
@@ -2635,16 +2783,17 @@ export function createDocumentRuntime(
|
|
|
2635
2783
|
activeStory,
|
|
2636
2784
|
),
|
|
2637
2785
|
};
|
|
2786
|
+
const broadcastCommand: EditorCommand = {
|
|
2787
|
+
type: "document.replace",
|
|
2788
|
+
document: nextDocumentWithReview,
|
|
2789
|
+
selection: localTransaction.nextState.selection,
|
|
2790
|
+
mapping: createEmptyMapping(),
|
|
2791
|
+
protectionSelection: selection,
|
|
2792
|
+
origin: command.origin,
|
|
2793
|
+
};
|
|
2638
2794
|
const fullTransaction = executeEditorCommand(
|
|
2639
2795
|
baseState,
|
|
2640
|
-
|
|
2641
|
-
type: "document.replace",
|
|
2642
|
-
document: nextDocumentWithReview,
|
|
2643
|
-
selection: localTransaction.nextState.selection,
|
|
2644
|
-
mapping: createEmptyMapping(),
|
|
2645
|
-
protectionSelection: selection,
|
|
2646
|
-
origin: command.origin,
|
|
2647
|
-
},
|
|
2796
|
+
broadcastCommand,
|
|
2648
2797
|
context,
|
|
2649
2798
|
);
|
|
2650
2799
|
|
|
@@ -2653,6 +2802,10 @@ export function createDocumentRuntime(
|
|
|
2653
2802
|
effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
|
|
2654
2803
|
};
|
|
2655
2804
|
commit(mergedTransaction);
|
|
2805
|
+
options.onCommandApplied?.(broadcastCommand, mergedTransaction, context, {
|
|
2806
|
+
preSelection,
|
|
2807
|
+
activeStory: preActiveStory,
|
|
2808
|
+
});
|
|
2656
2809
|
return classifyAck({
|
|
2657
2810
|
command,
|
|
2658
2811
|
opId,
|
|
@@ -2941,6 +3094,16 @@ function createEntityId(
|
|
|
2941
3094
|
return nextId;
|
|
2942
3095
|
}
|
|
2943
3096
|
|
|
3097
|
+
function normalizeCommandTimestamp(value: unknown): string | undefined {
|
|
3098
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3099
|
+
return value;
|
|
3100
|
+
}
|
|
3101
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3102
|
+
return new Date(value).toISOString();
|
|
3103
|
+
}
|
|
3104
|
+
return undefined;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2944
3107
|
function finalizeState(
|
|
2945
3108
|
state: EditorState,
|
|
2946
3109
|
markDirty: boolean,
|
|
@@ -4390,3 +4553,70 @@ function remapProtectionSnapshot(
|
|
|
4390
4553
|
preservedRangeCount: nextRanges.filter((range) => !range.enforced).length,
|
|
4391
4554
|
};
|
|
4392
4555
|
}
|
|
4556
|
+
|
|
4557
|
+
// ---------------------------------------------------------------------------
|
|
4558
|
+
// Measurement provider wiring (R0)
|
|
4559
|
+
// ---------------------------------------------------------------------------
|
|
4560
|
+
|
|
4561
|
+
/**
|
|
4562
|
+
* Build the initial input the `DocxFontLoader` needs: a list of font
|
|
4563
|
+
* families the document actively uses, plus any embedded font payloads the
|
|
4564
|
+
* import pipeline may have extracted.
|
|
4565
|
+
*
|
|
4566
|
+
* Walks the document content tree once per call. Embedded font extraction
|
|
4567
|
+
* is not yet wired into the canonical model; we pass an empty map today and
|
|
4568
|
+
* let the loader register system fonts it finds via
|
|
4569
|
+
* `document.fonts.check(...)`.
|
|
4570
|
+
*/
|
|
4571
|
+
function collectFontLoaderInput(
|
|
4572
|
+
document: CanonicalDocumentEnvelope,
|
|
4573
|
+
): { families: readonly string[] } {
|
|
4574
|
+
try {
|
|
4575
|
+
const families = new Set<string>();
|
|
4576
|
+
const visit = (node: unknown): void => {
|
|
4577
|
+
if (!node || typeof node !== "object") return;
|
|
4578
|
+
const record = node as Record<string, unknown>;
|
|
4579
|
+
const rpr = record["runProperties"] as
|
|
4580
|
+
| Record<string, unknown>
|
|
4581
|
+
| undefined;
|
|
4582
|
+
if (rpr && typeof rpr["fontFamily"] === "string") {
|
|
4583
|
+
families.add(rpr["fontFamily"] as string);
|
|
4584
|
+
}
|
|
4585
|
+
for (const value of Object.values(record)) {
|
|
4586
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
4587
|
+
else if (value && typeof value === "object") visit(value);
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
visit(document.content);
|
|
4591
|
+
if (document.styles) {
|
|
4592
|
+
visit(document.styles);
|
|
4593
|
+
}
|
|
4594
|
+
return { families: Array.from(families) };
|
|
4595
|
+
} catch {
|
|
4596
|
+
return { families: [] };
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
/**
|
|
4601
|
+
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
4602
|
+
* the platform supports it and fonts have resolved. Errors are swallowed
|
|
4603
|
+
* so a failure in the upgrade path can never break the empirical baseline.
|
|
4604
|
+
*/
|
|
4605
|
+
async function upgradeMeasurementProvider(
|
|
4606
|
+
engine: LayoutEngineInstance,
|
|
4607
|
+
fontLoader: DocxFontLoader,
|
|
4608
|
+
): Promise<void> {
|
|
4609
|
+
try {
|
|
4610
|
+
const provider: LayoutMeasurementProvider = await createMeasurementProvider({
|
|
4611
|
+
preference: "auto",
|
|
4612
|
+
fontLoader,
|
|
4613
|
+
});
|
|
4614
|
+
// If the host is running in SSR or a jsdom test shell, the factory will
|
|
4615
|
+
// fall back to the empirical backend. In that case swapping is a no-op
|
|
4616
|
+
// but still emits `measurement_backend_ready` with `empirical` which is
|
|
4617
|
+
// informational; chrome consumers use the event to refresh metrics.
|
|
4618
|
+
engine.swapMeasurementProvider(provider);
|
|
4619
|
+
} catch {
|
|
4620
|
+
// fall through — the empirical backend remains in place
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware default page format.
|
|
3
|
+
*
|
|
4
|
+
* Word historically defaults to US Letter for `en-US` hosts and A4 everywhere
|
|
5
|
+
* else. This module is the single place that decides which format a
|
|
6
|
+
* newly-created document uses when no section carries an explicit `w:pgSz`.
|
|
7
|
+
*
|
|
8
|
+
* It never overrides an existing section's page size on import — importers
|
|
9
|
+
* always preserve what the source document specified. The default is
|
|
10
|
+
* consulted in two places:
|
|
11
|
+
*
|
|
12
|
+
* 1. `serialize-main-document.ts` when the canonical model carries a
|
|
13
|
+
* section with no `pageSize` (e.g. programmatic document construction).
|
|
14
|
+
* 2. `DocumentRuntime` when rendering a brand-new blank document.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getPageFormatById,
|
|
19
|
+
type PageFormatDefinition,
|
|
20
|
+
} from "./page-format-catalog.ts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Locale resolution
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Which BCP-47 language tags should fall back to Letter.
|
|
28
|
+
*
|
|
29
|
+
* Anything else defaults to A4. This intentionally uses a small whitelist
|
|
30
|
+
* rather than a `startsWith("en")` check — `en-GB`, `en-AU`, `en-IN` all
|
|
31
|
+
* expect ISO paper sizes, not US Letter.
|
|
32
|
+
*/
|
|
33
|
+
const LETTER_LOCALES = new Set<string>([
|
|
34
|
+
"en-us",
|
|
35
|
+
"en-ca",
|
|
36
|
+
"fr-ca",
|
|
37
|
+
"es-mx",
|
|
38
|
+
"es-cl",
|
|
39
|
+
"fil-ph",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export interface ResolveDefaultPageFormatOptions {
|
|
43
|
+
/** Explicit BCP-47 locale (e.g. "en-US", "de-DE"). */
|
|
44
|
+
locale?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return the default `PageFormatDefinition` for a given locale.
|
|
49
|
+
*
|
|
50
|
+
* When `locale` is omitted the function consults `Intl.DateTimeFormat` to
|
|
51
|
+
* infer the current locale. When `Intl` is unavailable (unusual in modern
|
|
52
|
+
* runtimes) it falls back to A4 as the safer international default.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveDefaultPageFormat(
|
|
55
|
+
options: ResolveDefaultPageFormatOptions = {},
|
|
56
|
+
): PageFormatDefinition {
|
|
57
|
+
const raw = options.locale ?? tryResolveHostLocale();
|
|
58
|
+
if (!raw) {
|
|
59
|
+
return getPageFormatById("a4");
|
|
60
|
+
}
|
|
61
|
+
const normalized = raw.toLowerCase();
|
|
62
|
+
if (LETTER_LOCALES.has(normalized)) {
|
|
63
|
+
return getPageFormatById("letter");
|
|
64
|
+
}
|
|
65
|
+
// Match just the language-region prefix (e.g. "en-us-1234" → "en-us")
|
|
66
|
+
const region = normalized.split("-").slice(0, 2).join("-");
|
|
67
|
+
if (LETTER_LOCALES.has(region)) {
|
|
68
|
+
return getPageFormatById("letter");
|
|
69
|
+
}
|
|
70
|
+
return getPageFormatById("a4");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tryResolveHostLocale(): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return the default `w:pgSz` payload (width/height in twips) for a given
|
|
86
|
+
* locale. Convenience wrapper used by the export pipeline.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveDefaultPageSizeTwips(
|
|
89
|
+
options: ResolveDefaultPageFormatOptions = {},
|
|
90
|
+
): { widthTwips: number; heightTwips: number } {
|
|
91
|
+
const format = resolveDefaultPageFormat(options);
|
|
92
|
+
return {
|
|
93
|
+
widthTwips: format.portraitWidthTwips,
|
|
94
|
+
heightTwips: format.portraitHeightTwips,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -158,6 +158,8 @@ export {
|
|
|
158
158
|
type EmbeddedFontBytes,
|
|
159
159
|
} from "./docx-font-loader.ts";
|
|
160
160
|
|
|
161
|
+
export { createCanvasBackend } from "./measurement-backend-canvas.ts";
|
|
162
|
+
|
|
161
163
|
// ---------------------------------------------------------------------------
|
|
162
164
|
// Public facet (Phase 7)
|
|
163
165
|
// ---------------------------------------------------------------------------
|
|
@@ -184,5 +186,50 @@ export {
|
|
|
184
186
|
type PublicFieldDirtinessReport,
|
|
185
187
|
type LayoutFacetEvent,
|
|
186
188
|
type LayoutFacetInvalidationReason,
|
|
189
|
+
type RenderZoomSummary,
|
|
187
190
|
type CreateLayoutFacetInput,
|
|
191
|
+
type PageFormatDefinition,
|
|
192
|
+
type ActivePageFormat,
|
|
193
|
+
type MarginPresetDefinition,
|
|
194
|
+
type ActiveMarginPreset,
|
|
188
195
|
} from "./public-facet.ts";
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Page-format catalog + margin preset catalog + locale defaults (R0.5)
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
export {
|
|
202
|
+
PAGE_FORMAT_CATALOG,
|
|
203
|
+
matchPageFormat,
|
|
204
|
+
getPageFormatById,
|
|
205
|
+
type PageFormatId,
|
|
206
|
+
type PageFormatRegion,
|
|
207
|
+
type PageFormatLocaleDefault,
|
|
208
|
+
type PageFormatDisplay,
|
|
209
|
+
type MatchPageFormatInput,
|
|
210
|
+
} from "./page-format-catalog.ts";
|
|
211
|
+
|
|
212
|
+
export {
|
|
213
|
+
MARGIN_PRESET_CATALOG,
|
|
214
|
+
matchMarginPreset,
|
|
215
|
+
getMarginPresetById,
|
|
216
|
+
type MarginPresetId,
|
|
217
|
+
type MatchMarginPresetInput,
|
|
218
|
+
} from "./margin-preset-catalog.ts";
|
|
219
|
+
|
|
220
|
+
export {
|
|
221
|
+
resolveDefaultPageFormat,
|
|
222
|
+
resolveDefaultPageSizeTwips,
|
|
223
|
+
type ResolveDefaultPageFormatOptions,
|
|
224
|
+
} from "./default-page-format.ts";
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Workflow rail segments (R3a)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
collectScopeRailSegments,
|
|
232
|
+
type CollectScopeRailSegmentsInput,
|
|
233
|
+
type ScopeRailPosture,
|
|
234
|
+
type ScopeRailSegment,
|
|
235
|
+
} from "../workflow-rail-segments.ts";
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
PublicMeasurementFidelity,
|
|
13
13
|
WordReviewEditorLayoutFacet,
|
|
14
14
|
} from "./public-facet.ts";
|
|
15
|
+
import { MARGIN_PRESET_CATALOG } from "./margin-preset-catalog.ts";
|
|
16
|
+
import { PAGE_FORMAT_CATALOG } from "./page-format-catalog.ts";
|
|
15
17
|
|
|
16
18
|
export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
17
19
|
const emptyReport: PublicFieldDirtinessReport = {
|
|
@@ -32,12 +34,26 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
32
34
|
getActiveStoriesOnPage: () => null,
|
|
33
35
|
getDisplayPageNumber: () => null,
|
|
34
36
|
getLineBoxes: () => [],
|
|
37
|
+
getLineBoxesForRegion: () => [],
|
|
35
38
|
getFragmentsForPage: () => [],
|
|
39
|
+
getPageFormatCatalog: () => PAGE_FORMAT_CATALOG,
|
|
40
|
+
getActivePageFormat: () => null,
|
|
41
|
+
getMarginPresetCatalog: () => MARGIN_PRESET_CATALOG,
|
|
42
|
+
getActiveMarginPreset: () => null,
|
|
43
|
+
getRenderFrame: () => null,
|
|
44
|
+
getRenderZoom: () => null,
|
|
45
|
+
hitTest: () => null,
|
|
46
|
+
getAnchorRects: () => [],
|
|
47
|
+
getScopeRailSegments: () => [],
|
|
48
|
+
getAllScopeRailSegments: () => [],
|
|
36
49
|
getResolvedFormatting: () => null,
|
|
37
50
|
getResolvedRunFormatting: () => null,
|
|
38
51
|
getMeasurement: () => null,
|
|
39
52
|
getMeasurementFidelity: () => fidelity,
|
|
40
53
|
whenMeasurementReady: () => Promise.resolve(),
|
|
54
|
+
getFirstPageIndexForBlock: () => null,
|
|
55
|
+
swapMeasurementProvider: () => undefined,
|
|
56
|
+
getTableRenderPlan: () => null,
|
|
41
57
|
getDirtyFieldFamilies: () => [],
|
|
42
58
|
getFieldDirtinessReport: () => emptyReport,
|
|
43
59
|
subscribe: (_listener: (event: LayoutFacetEvent) => void) => () => undefined,
|