@beyondwork/docx-react-component 1.0.52 → 1.0.54
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 +31 -40
- package/src/api/public-types.ts +67 -7
- package/src/io/chart-preview-resolver.ts +41 -0
- package/src/io/docx-session.ts +217 -23
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +88 -8
- package/src/runtime/document-runtime.ts +182 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +97 -2
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +70 -1
- package/src/runtime/prerender/cache-envelope.ts +30 -0
- package/src/runtime/prerender/customxml-cache.ts +17 -3
- package/src/runtime/prerender/prerender-document.ts +17 -1
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/render/render-kernel.ts +67 -19
- package/src/runtime/surface-projection.ts +28 -0
- package/src/runtime/table-schema.ts +27 -0
- package/src/runtime/table-style-resolver.ts +51 -0
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/editor-runtime-boundary.ts +39 -2
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
- package/src/ui-tailwind/theme/editor-theme.css +275 -46
- package/src/ui-tailwind/theme/tokens.css +345 -0
- package/src/ui-tailwind/theme/tokens.ts +313 -0
- package/src/ui-tailwind/theme/use-density.ts +60 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -11,6 +11,7 @@ import type { BlockNode } from "../../model/canonical-document.ts";
|
|
|
11
11
|
import type {
|
|
12
12
|
CommandAppliedMeta,
|
|
13
13
|
DocumentRuntime,
|
|
14
|
+
RemoteCommandEnvelope,
|
|
14
15
|
Unsubscribe,
|
|
15
16
|
} from "../document-runtime.ts";
|
|
16
17
|
import {
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
type CommandEvent,
|
|
22
23
|
} from "./event-types.ts";
|
|
23
24
|
import { computeBaseDocFingerprint } from "./base-doc-fingerprint.ts";
|
|
24
|
-
import type
|
|
25
|
+
import { CHECKPOINTS_KEY, type Checkpoint } from "./checkpoint-store.ts";
|
|
25
26
|
import { createWorkflowShared, type WorkflowSharedHandle } from "./workflow-shared.ts";
|
|
26
27
|
|
|
27
28
|
/** Shared Y.Map key — {@link SHARED_META_MAP_KEY}. */
|
|
@@ -29,7 +30,6 @@ const SHARED_META_MAP_KEY = "meta";
|
|
|
29
30
|
const META_BASE_DOC_HASH_KEY = "baseDocHash";
|
|
30
31
|
const META_SCHEMA_VERSION_KEY = "schemaVersion";
|
|
31
32
|
const META_CREATED_AT_KEY = "createdAt";
|
|
32
|
-
const CHECKPOINTS_KEY = "checkpoints";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Lifecycle + correctness events surfaced by a
|
|
@@ -200,6 +200,30 @@ export interface RuntimeCollabSyncOptions {
|
|
|
200
200
|
* - `"observer"`: all writes refused with `collab_observer_readonly`.
|
|
201
201
|
*/
|
|
202
202
|
role?: "author" | "reviewer" | "observer";
|
|
203
|
+
/**
|
|
204
|
+
* R1 — coalesce inbound remote-replay events into one commit per
|
|
205
|
+
* animation frame. When `true` (default), N events arriving within a
|
|
206
|
+
* single rAF tick (or `setTimeout(0)` in non-DOM contexts) drain via
|
|
207
|
+
* one `runtime.applyRemoteCommandBatch(...)` call → one
|
|
208
|
+
* `refreshRenderSnapshot` / one `notify`. Observer semantics change:
|
|
209
|
+
* subscribers see one `DocumentRuntimeEvent` per burst rather than
|
|
210
|
+
* one per event. Hosts that depend on per-event semantics (rare —
|
|
211
|
+
* counted-event tests, granular per-comment-add observers) opt out
|
|
212
|
+
* with `false`. The whole batch is atomic: any per-command effect
|
|
213
|
+
* collision falls back to per-event replay automatically.
|
|
214
|
+
*
|
|
215
|
+
* Defaults to `true` in both DOM and Node contexts so tests exercise
|
|
216
|
+
* the coalesced path without ceremony.
|
|
217
|
+
*/
|
|
218
|
+
coalesceRemoteReplay?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* R1 hook point for R3 idle-priority plumbing. Invoked once per
|
|
221
|
+
* scheduled drain (NOT per queued event). R3's future implementation
|
|
222
|
+
* will use this to abort any in-flight idle prerender task on
|
|
223
|
+
* `src/io/load-scheduler.ts` so the upcoming commit doesn't compete
|
|
224
|
+
* with idle work. Safe to omit; R1 functions identically without it.
|
|
225
|
+
*/
|
|
226
|
+
onRemoteReplayScheduled?: () => void;
|
|
203
227
|
}
|
|
204
228
|
|
|
205
229
|
export interface RuntimeCollabSyncHandle {
|
|
@@ -268,6 +292,48 @@ export function createRuntimeCollabSync(
|
|
|
268
292
|
let readOnly = false;
|
|
269
293
|
let baseDocFingerprint: string | null = null;
|
|
270
294
|
|
|
295
|
+
// R1 — coalesced remote-replay scheduler. Default: ON in both DOM and
|
|
296
|
+
// Node contexts. Falls back to setTimeout(0) when requestAnimationFrame
|
|
297
|
+
// is missing so tests and non-DOM hosts exercise the same drain path.
|
|
298
|
+
const useCoalescing = options.coalesceRemoteReplay !== false;
|
|
299
|
+
const replayQueue: CommandEvent[] = [];
|
|
300
|
+
let rafHandle: number | null = null;
|
|
301
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
302
|
+
|
|
303
|
+
function scheduleDrain(): void {
|
|
304
|
+
if (rafHandle !== null || timeoutHandle !== null) return;
|
|
305
|
+
options.onRemoteReplayScheduled?.();
|
|
306
|
+
if (typeof requestAnimationFrame === "function") {
|
|
307
|
+
rafHandle = requestAnimationFrame(() => {
|
|
308
|
+
rafHandle = null;
|
|
309
|
+
drainReplayQueue();
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
timeoutHandle = setTimeout(() => {
|
|
313
|
+
timeoutHandle = null;
|
|
314
|
+
drainReplayQueue();
|
|
315
|
+
}, 0);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function drainReplayQueue(): void {
|
|
320
|
+
if (replayQueue.length === 0) return;
|
|
321
|
+
const batch = replayQueue.splice(0, replayQueue.length);
|
|
322
|
+
const envelopes = batch.map(eventToEnvelope);
|
|
323
|
+
runtime.applyRemoteCommandBatch(envelopes);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function cancelPendingDrain(): void {
|
|
327
|
+
if (rafHandle !== null) {
|
|
328
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(rafHandle);
|
|
329
|
+
rafHandle = null;
|
|
330
|
+
}
|
|
331
|
+
if (timeoutHandle !== null) {
|
|
332
|
+
clearTimeout(timeoutHandle);
|
|
333
|
+
timeoutHandle = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
271
337
|
// Events emitted before any subscriber exists are buffered and flushed
|
|
272
338
|
// to the first subscriber. This lets hosts react to the attach-path
|
|
273
339
|
// base-doc mismatch and schema-version mismatch for pre-existing
|
|
@@ -494,6 +560,10 @@ export function createRuntimeCollabSync(
|
|
|
494
560
|
|
|
495
561
|
return {
|
|
496
562
|
destroy() {
|
|
563
|
+
// R1 — drain any queued replay events synchronously before tearing
|
|
564
|
+
// down so callers don't lose remote events on an early unmount.
|
|
565
|
+
cancelPendingDrain();
|
|
566
|
+
drainReplayQueue();
|
|
497
567
|
unsubscribeCommandApplied();
|
|
498
568
|
yEvents.unobserve(onYEventsChange);
|
|
499
569
|
yMeta.unobserve(checkFingerprintAgainstMeta);
|
|
@@ -607,19 +677,29 @@ export function createRuntimeCollabSync(
|
|
|
607
677
|
return true;
|
|
608
678
|
}
|
|
609
679
|
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
event.command,
|
|
613
|
-
{
|
|
680
|
+
function eventToEnvelope(event: CommandEvent): RemoteCommandEnvelope {
|
|
681
|
+
return {
|
|
682
|
+
command: event.command,
|
|
683
|
+
context: {
|
|
614
684
|
timestamp: event.timestamp,
|
|
615
685
|
documentMode: event.context.documentMode,
|
|
616
686
|
defaultAuthorId: event.context.defaultAuthorId ?? event.authorId,
|
|
617
687
|
},
|
|
618
|
-
{
|
|
688
|
+
meta: {
|
|
619
689
|
preSelection: event.context.preSelection,
|
|
620
690
|
activeStory: event.context.activeStory,
|
|
621
691
|
},
|
|
622
|
-
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function applyEventToRuntime(event: CommandEvent): void {
|
|
696
|
+
if (useCoalescing) {
|
|
697
|
+
replayQueue.push(event);
|
|
698
|
+
scheduleDrain();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const env = eventToEnvelope(event);
|
|
702
|
+
runtime.applyRemoteCommand(env.command, env.context, env.meta);
|
|
623
703
|
}
|
|
624
704
|
}
|
|
625
705
|
|
|
@@ -160,6 +160,7 @@ import {
|
|
|
160
160
|
createDocumentSectionSnapshots,
|
|
161
161
|
createSectionLocations,
|
|
162
162
|
createTocSnapshot,
|
|
163
|
+
findBookmarkNameForOffset,
|
|
163
164
|
findDocumentSectionSnapshot,
|
|
164
165
|
} from "./document-outline.ts";
|
|
165
166
|
import {
|
|
@@ -209,6 +210,7 @@ import type {
|
|
|
209
210
|
BlockNode,
|
|
210
211
|
FieldNode,
|
|
211
212
|
FieldRefreshStatus,
|
|
213
|
+
HyperlinkNode,
|
|
212
214
|
InlineNode,
|
|
213
215
|
PageMargins,
|
|
214
216
|
ParagraphNode,
|
|
@@ -298,6 +300,27 @@ export interface DocumentRuntime {
|
|
|
298
300
|
context: CommandExecutionContext,
|
|
299
301
|
meta?: Partial<CommandAppliedMeta>,
|
|
300
302
|
): void;
|
|
303
|
+
/**
|
|
304
|
+
* R1 — coalesced remote-replay entry point. Applies N envelopes as ONE
|
|
305
|
+
* commit: per-command transactions are chained (`replayState = txN.nextState`),
|
|
306
|
+
* mapping steps concatenated, `markDirty` ORed, and a single `commitRemote`
|
|
307
|
+
* call drives `finalizeState` / `invalidate` / `refreshRenderSnapshot` /
|
|
308
|
+
* `notify` exactly once. Per-command `meta.activeStory` isolation is
|
|
309
|
+
* preserved (the remote story scopes its own replay; the local cursor
|
|
310
|
+
* stays put). Behavior matches `applyRemoteCommand` invoked N times for
|
|
311
|
+
* a single envelope; for empty arrays it's a no-op.
|
|
312
|
+
*
|
|
313
|
+
* Singular effect collisions (≥2 envelopes both emitting `commentAdded` /
|
|
314
|
+
* `changeAccepted` / etc.) fall back to per-envelope replay so observers
|
|
315
|
+
* still see every effect — typical text-burst replay (all warnings-only
|
|
316
|
+
* effects) takes the fast path.
|
|
317
|
+
*
|
|
318
|
+
* History commands (`history.undo` / `history.redo`) and runtime overlay
|
|
319
|
+
* commands (`workflow.set-overlay`, etc.) inside a batch are processed
|
|
320
|
+
* individually as in the per-event path; they don't participate in the
|
|
321
|
+
* chained commit.
|
|
322
|
+
*/
|
|
323
|
+
applyRemoteCommandBatch(envelopes: ReadonlyArray<RemoteCommandEnvelope>): void;
|
|
301
324
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
302
325
|
undo(): void;
|
|
303
326
|
redo(): void;
|
|
@@ -433,6 +456,17 @@ export interface CommandAppliedMeta {
|
|
|
433
456
|
priorDocument: CanonicalDocumentEnvelope;
|
|
434
457
|
}
|
|
435
458
|
|
|
459
|
+
/**
|
|
460
|
+
* Single envelope shape consumed by {@link DocumentRuntime.applyRemoteCommandBatch}.
|
|
461
|
+
* Mirrors the `applyRemoteCommand(command, context, meta)` signature so
|
|
462
|
+
* batch and per-event paths stay schema-compatible.
|
|
463
|
+
*/
|
|
464
|
+
export interface RemoteCommandEnvelope {
|
|
465
|
+
command: EditorCommand;
|
|
466
|
+
context: CommandExecutionContext;
|
|
467
|
+
meta?: Partial<CommandAppliedMeta>;
|
|
468
|
+
}
|
|
469
|
+
|
|
436
470
|
export interface CreateDocumentRuntimeOptions {
|
|
437
471
|
documentId: string;
|
|
438
472
|
initialSessionState?: EditorSessionState;
|
|
@@ -2264,6 +2298,133 @@ export function createDocumentRuntime(
|
|
|
2264
2298
|
emitError(toRuntimeError(error));
|
|
2265
2299
|
}
|
|
2266
2300
|
},
|
|
2301
|
+
applyRemoteCommandBatch(envelopes) {
|
|
2302
|
+
if (envelopes.length === 0) return;
|
|
2303
|
+
if (envelopes.length === 1) {
|
|
2304
|
+
const e = envelopes[0]!;
|
|
2305
|
+
this.applyRemoteCommand(e.command, e.context, e.meta);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
try {
|
|
2310
|
+
// Chain per-command transactions before ONE commitRemote at the end.
|
|
2311
|
+
// Each command runs through executeEditorCommand on the replayed
|
|
2312
|
+
// state; resulting nextState becomes the input to the next command.
|
|
2313
|
+
// History/overlay commands are processed individually as in the
|
|
2314
|
+
// per-event path (they don't participate in the chained commit).
|
|
2315
|
+
let replayState: typeof state = state;
|
|
2316
|
+
const stepsAccumulator: import("../core/selection/mapping.ts").MappingStep[] = [];
|
|
2317
|
+
let anyDirty = false;
|
|
2318
|
+
let lastNextState: typeof state | null = null;
|
|
2319
|
+
let lastCrossStoryReplay = false;
|
|
2320
|
+
const warningsAdded: import("../core/state/editor-state.ts").EditorWarning[] = [];
|
|
2321
|
+
const warningsCleared: Array<{ warningId: string; code: import("../core/state/editor-state.ts").EditorWarning["code"] }> = [];
|
|
2322
|
+
const SINGULAR_EFFECT_KEYS = [
|
|
2323
|
+
"commentAdded",
|
|
2324
|
+
"commentResolved",
|
|
2325
|
+
"commentReopened",
|
|
2326
|
+
"commentReplyAdded",
|
|
2327
|
+
"commentBodyEdited",
|
|
2328
|
+
"changeAccepted",
|
|
2329
|
+
"changeRejected",
|
|
2330
|
+
"revisionAuthored",
|
|
2331
|
+
"commandBlocked",
|
|
2332
|
+
] as const;
|
|
2333
|
+
const singularEffectsCounts = new Map<string, number>();
|
|
2334
|
+
const aggregatedSingular: Record<string, unknown> = {};
|
|
2335
|
+
|
|
2336
|
+
for (const env of envelopes) {
|
|
2337
|
+
const { command, context, meta } = env;
|
|
2338
|
+
if (command.type === "history.undo" || command.type === "history.redo") {
|
|
2339
|
+
// Match per-event path: silently skipped on remote replay.
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (isRuntimeStateOverlayCommand(command)) {
|
|
2343
|
+
// Overlays bypass commitRemote in the per-event path; mirror that
|
|
2344
|
+
// here without disturbing the chained replayState.
|
|
2345
|
+
applyRuntimeStateOverlayCommand(command);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const replayStory = meta?.activeStory ?? activeStory;
|
|
2349
|
+
const crossStoryReplay = Boolean(
|
|
2350
|
+
meta?.activeStory && !storyTargetsEqual(meta.activeStory, activeStory),
|
|
2351
|
+
);
|
|
2352
|
+
let stateForCommand: typeof state;
|
|
2353
|
+
let snapshotForCommand: typeof cachedRenderSnapshot;
|
|
2354
|
+
if (meta?.preSelection) {
|
|
2355
|
+
// Reuse the R5 scratch (avoids per-command allocation across the
|
|
2356
|
+
// burst). Seed from the CURRENT chain state so each command sees
|
|
2357
|
+
// the prior command's nextState as its starting point.
|
|
2358
|
+
Object.assign(r5ScratchReplayState, replayState);
|
|
2359
|
+
r5ScratchReplayState.selection = meta.preSelection;
|
|
2360
|
+
Object.assign(r5ScratchReplaySnapshot, cachedRenderSnapshot);
|
|
2361
|
+
r5ScratchReplaySnapshot.selection = toPublicSelectionSnapshot(meta.preSelection, replayStory);
|
|
2362
|
+
stateForCommand = r5ScratchReplayState;
|
|
2363
|
+
snapshotForCommand = r5ScratchReplaySnapshot;
|
|
2364
|
+
} else {
|
|
2365
|
+
stateForCommand = replayState;
|
|
2366
|
+
snapshotForCommand = cachedRenderSnapshot;
|
|
2367
|
+
}
|
|
2368
|
+
const replayContext = {
|
|
2369
|
+
...context,
|
|
2370
|
+
renderSnapshot: snapshotForCommand,
|
|
2371
|
+
};
|
|
2372
|
+
const transaction = executeEditorCommand(stateForCommand, command, replayContext);
|
|
2373
|
+
replayState = transaction.nextState;
|
|
2374
|
+
stepsAccumulator.push(...transaction.mapping.steps);
|
|
2375
|
+
anyDirty = anyDirty || transaction.markDirty;
|
|
2376
|
+
warningsAdded.push(...transaction.effects.warningsAdded);
|
|
2377
|
+
warningsCleared.push(...transaction.effects.warningsCleared);
|
|
2378
|
+
for (const key of SINGULAR_EFFECT_KEYS) {
|
|
2379
|
+
const v = (transaction.effects as unknown as Record<string, unknown>)[key];
|
|
2380
|
+
if (v !== undefined) {
|
|
2381
|
+
singularEffectsCounts.set(key, (singularEffectsCounts.get(key) ?? 0) + 1);
|
|
2382
|
+
aggregatedSingular[key] = v;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
lastNextState = transaction.nextState;
|
|
2386
|
+
lastCrossStoryReplay = crossStoryReplay;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// Singular-effect collision (≥2 envelopes both setting the same
|
|
2390
|
+
// singular field): fall back to per-envelope replay so observers
|
|
2391
|
+
// see every effect. Common burst (all text.insert with empty
|
|
2392
|
+
// effects) takes the fast path above.
|
|
2393
|
+
let singularCollision = false;
|
|
2394
|
+
for (const count of singularEffectsCounts.values()) {
|
|
2395
|
+
if (count > 1) {
|
|
2396
|
+
singularCollision = true;
|
|
2397
|
+
break;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (singularCollision) {
|
|
2401
|
+
for (const env of envelopes) {
|
|
2402
|
+
this.applyRemoteCommand(env.command, env.context, env.meta);
|
|
2403
|
+
}
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (lastNextState === null) {
|
|
2407
|
+
// All envelopes were skip/overlay — nothing to commit.
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
const consolidated = {
|
|
2411
|
+
nextState: lastCrossStoryReplay
|
|
2412
|
+
? { ...lastNextState, selection: state.selection }
|
|
2413
|
+
: lastNextState,
|
|
2414
|
+
mapping: { steps: stepsAccumulator },
|
|
2415
|
+
effects: {
|
|
2416
|
+
warningsAdded,
|
|
2417
|
+
warningsCleared,
|
|
2418
|
+
...aggregatedSingular,
|
|
2419
|
+
} as import("../core/commands/index.ts").TransactionEffects,
|
|
2420
|
+
historyBoundary: "skip" as const,
|
|
2421
|
+
markDirty: anyDirty,
|
|
2422
|
+
};
|
|
2423
|
+
commitRemote(consolidated);
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
emitError(toRuntimeError(error));
|
|
2426
|
+
}
|
|
2427
|
+
},
|
|
2267
2428
|
undo() {
|
|
2268
2429
|
this.dispatch({
|
|
2269
2430
|
type: "history.undo",
|
|
@@ -5038,7 +5199,7 @@ function refreshDocumentTableOfContents(
|
|
|
5038
5199
|
} {
|
|
5039
5200
|
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
5040
5201
|
let changed = false;
|
|
5041
|
-
let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
|
|
5202
|
+
let resultEntries: Array<{ level: number; text: string; pageIndex: number; bookmarkName?: string }> = [];
|
|
5042
5203
|
let changedFrom: number | undefined;
|
|
5043
5204
|
let changedTo: number | undefined;
|
|
5044
5205
|
const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
|
|
@@ -5050,11 +5211,15 @@ function refreshDocumentTableOfContents(
|
|
|
5050
5211
|
: parseTocLevelRange(field.instruction);
|
|
5051
5212
|
const entries = navigation.headings
|
|
5052
5213
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
5053
|
-
.map((heading) =>
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5214
|
+
.map((heading) => {
|
|
5215
|
+
const bookmarkName = findBookmarkNameForOffset(document, heading.offset);
|
|
5216
|
+
return {
|
|
5217
|
+
level: heading.level,
|
|
5218
|
+
text: heading.text,
|
|
5219
|
+
pageIndex: heading.pageIndex,
|
|
5220
|
+
...(bookmarkName ? { bookmarkName } : {}),
|
|
5221
|
+
};
|
|
5222
|
+
});
|
|
5058
5223
|
if (resultEntries.length === 0) {
|
|
5059
5224
|
resultEntries = entries;
|
|
5060
5225
|
}
|
|
@@ -5251,12 +5416,20 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
5251
5416
|
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
5252
5417
|
*/
|
|
5253
5418
|
function buildTocInlineNodes(
|
|
5254
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5419
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5255
5420
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5256
5421
|
): InlineNode[] {
|
|
5257
5422
|
const children: InlineNode[] = [];
|
|
5258
5423
|
entries.forEach((entry, index) => {
|
|
5259
|
-
|
|
5424
|
+
if (entry.bookmarkName) {
|
|
5425
|
+
children.push({
|
|
5426
|
+
type: "hyperlink",
|
|
5427
|
+
href: `#${entry.bookmarkName}`,
|
|
5428
|
+
children: [{ type: "text", text: entry.text }],
|
|
5429
|
+
} as HyperlinkNode);
|
|
5430
|
+
} else {
|
|
5431
|
+
children.push({ type: "text", text: entry.text });
|
|
5432
|
+
}
|
|
5260
5433
|
children.push({ type: "tab" });
|
|
5261
5434
|
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
5262
5435
|
children.push({
|
|
@@ -5272,7 +5445,7 @@ function buildTocInlineNodes(
|
|
|
5272
5445
|
|
|
5273
5446
|
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
5274
5447
|
export function __buildTocInlineNodes(
|
|
5275
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5448
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5276
5449
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5277
5450
|
): InlineNode[] {
|
|
5278
5451
|
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
@@ -59,6 +59,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
59
59
|
swapMeasurementProvider: () => undefined,
|
|
60
60
|
invalidateMeasurementCache: () => undefined,
|
|
61
61
|
getTableRenderPlan: () => null,
|
|
62
|
+
getTableBodyYOffsetOnPage: () => null,
|
|
62
63
|
getDirtyFieldFamilies: () => [],
|
|
63
64
|
getFieldDirtinessReport: () => emptyReport,
|
|
64
65
|
setVisibleBlockRange: () => undefined,
|
|
@@ -93,8 +93,94 @@
|
|
|
93
93
|
* pixel-geometry change; cache envelopes from v9 invalidate
|
|
94
94
|
* because page-node identity semantics on bounded splices
|
|
95
95
|
* changed.
|
|
96
|
+
* 11 — Lane 3a P10 Phase B2. `toPublicPageNode` memoizes its
|
|
97
|
+
* `RuntimePageNode → PublicPageNode` projection via WeakMap so
|
|
98
|
+
* Phase D1's stable runtime-node references cascade into stable
|
|
99
|
+
* public-facet return values. `facet.getPage(pageIndex)` now
|
|
100
|
+
* returns reference-equal `PublicPageNode`s across bounded
|
|
101
|
+
* relayouts where the underlying runtime node was reused.
|
|
102
|
+
* Cascades into downstream consumers (React.memo-gated page
|
|
103
|
+
* subtrees, per-page test hooks) and closes the Phase B → D1
|
|
104
|
+
* stable-reference chain. No pixel-geometry change; cache
|
|
105
|
+
* envelopes from v10 invalidate because the public-facet
|
|
106
|
+
* contract changed from "fresh object per call" to "stable ref
|
|
107
|
+
* per underlying runtime node".
|
|
108
|
+
* 12 — Lane 3a R3 table row-split: `getTableRenderPlan` now derives
|
|
109
|
+
* `isContinuationPage` from the page graph's fragment slice
|
|
110
|
+
* metadata (`kind === "table-slice" && tableRowRange.from > 0`)
|
|
111
|
+
* and passes it to `buildTableRenderPlan`. Tables split across
|
|
112
|
+
* pages now carry non-empty `repeatedHeaderRows` on continuation
|
|
113
|
+
* pages so chrome can prepend header rows visually. No
|
|
114
|
+
* pixel-geometry change; cache envelopes from v11 invalidate
|
|
115
|
+
* because the table-render-plan contract changed.
|
|
116
|
+
* 13 — Lane 6d.U2 canvas-seam pill polish: the canvas-posture page-break
|
|
117
|
+
* widget's "N / M" badge is promoted from transparent text over the
|
|
118
|
+
* dotted seam to a true pill with `--radius-pill` geometry, hairline
|
|
119
|
+
* `--color-border-default` border, and `--shadow-soft`. Widget DOM
|
|
120
|
+
* shape changed (new `data-variant="pill"` attribute; additional
|
|
121
|
+
* inline style declarations on the badge). Cache envelopes from v12
|
|
122
|
+
* invalidate because the decoration's cacheable DOM shape changed.
|
|
123
|
+
* 13 — Lane 3a P14.c: render-kernel gains a single-slot `DecorationIndex`
|
|
124
|
+
* cache keyed on (revision, activeStory.kind, zoom.pxPerTwip, and
|
|
125
|
+
* reference equality on each decoration source). When layout
|
|
126
|
+
* changes but decoration sources are unchanged, `resolveDecorationIndex`
|
|
127
|
+
* is skipped and the prior result is reused. The frame-level cache
|
|
128
|
+
* already handles repeat reads; this slot targets the post-invalidate
|
|
129
|
+
* rebuild path (on every keystroke that triggers a layout event).
|
|
130
|
+
* No pixel-geometry change; cache envelopes from v12 invalidate
|
|
131
|
+
* because the render-kernel source changed.
|
|
132
|
+
* 14 — Lane 3a Slice 5: `RuntimeBlockFragment` gains `resolvedStyleChainRef`
|
|
133
|
+
* (block's styleId) and `numberingInstanceId` (block's list-instance id).
|
|
134
|
+
* `analyzeInvalidation` for `styles-change` (when `dirtyStyleIds` is
|
|
135
|
+
* supplied) and `numbering-change` (when `numberingInstanceId` is
|
|
136
|
+
* supplied) now return `scope: "bounded"` starting from the first page
|
|
137
|
+
* whose fragments reference the dirty style / instance. Fallback to
|
|
138
|
+
* `scope: "full"` when payload is absent or no match found. No
|
|
139
|
+
* pixel-geometry change; cache envelopes from v13 invalidate because
|
|
140
|
+
* the fragment shape and invalidation-scope contract changed.
|
|
141
|
+
* 15 — Bug fixes: `pageNodesStructurallyEqual` now compares
|
|
142
|
+
* `lineBoxes.length` and `noteAllocations.length` as structural
|
|
143
|
+
* proxies to prevent stale-node reuse when line geometry changes
|
|
144
|
+
* with stable fragment IDs (L1). `analyzeSectionChange` normalizes
|
|
145
|
+
* `dirtySectionRange` to guarantee from ≤ to for all graph states
|
|
146
|
+
* including empty-sections fallback (L2).
|
|
147
|
+
* 16 — Bug fixes: `diffRenderFrames` now flags pages whose physical frame
|
|
148
|
+
* changed (but block regions are stable) with `pageFrameChanged: true`
|
|
149
|
+
* in `changedPages` so consumers can re-project without a block-region
|
|
150
|
+
* signal (R1). Chrome reservation changes (`railLaneTwips`,
|
|
151
|
+
* `balloonLaneTwips`, `footnoteAreaTwips`, `pageFrameWidthPx`,
|
|
152
|
+
* `pageFrameHeightPx`) now trigger `changedPages` so overlay
|
|
153
|
+
* re-projection is not silently skipped (R2).
|
|
154
|
+
* 17 — Lane 3a Slice 2 + R4: `WordReviewEditorLayoutFacet` gains
|
|
155
|
+
* `getTableBodyYOffsetOnPage(blockId, pageIndex)` which returns the
|
|
156
|
+
* Y offset (in twips from body top) of the table's first fragment on
|
|
157
|
+
* a given page by summing prior body-fragment heights. Used by the
|
|
158
|
+
* new `TwTableContinuationHeader` chrome overlay to position repeated
|
|
159
|
+
* header rows on continuation pages of multi-page tables — no DOM
|
|
160
|
+
* measurement, layout-engine fragment heights only. No cached-geometry
|
|
161
|
+
* change; cache envelopes from v16 invalidate because the facet
|
|
162
|
+
* interface changed.
|
|
163
|
+
* 18 — Lane 3a Slice 6: `buildPageStackFromWithSplits` no longer discards
|
|
164
|
+
* `resumeAt.startOffset`. When `startOffset > 0` and no block
|
|
165
|
+
* straddles the dirty section boundary, only sections at and after
|
|
166
|
+
* the first dirty section are paginated; the resulting page indices
|
|
167
|
+
* are shifted by `startPageIndex` so they align with the global graph.
|
|
168
|
+
* Full-paginate + tail-slice fallback used when a block straddles the
|
|
169
|
+
* section boundary (safety guard). This eliminates re-paginating
|
|
170
|
+
* settled head sections on every bounded-invalidation relayout.
|
|
171
|
+
* No pixel-geometry change; cache envelopes from v17 invalidate
|
|
172
|
+
* because `buildPageStackFromWithSplits` output contract changed.
|
|
173
|
+
* 19 — Slice 5 bug-fix: `analyzeNumberingChange` now honors its own
|
|
174
|
+
* "Fallback to full rebuild when absent or no match" contract. When
|
|
175
|
+
* `numberingInstanceId` is supplied but no materialized fragment
|
|
176
|
+
* matches it, the analyzer returns `scope: "full"` +
|
|
177
|
+
* `requiresFullRecompute: true` instead of the prior "bounded over
|
|
178
|
+
* full range" shortcut, which bypassed the safety guard and could
|
|
179
|
+
* leak stale field-family projections. No pixel-geometry change;
|
|
180
|
+
* cache envelopes from v18 invalidate because the invalidation
|
|
181
|
+
* classifier's contract corrected.
|
|
96
182
|
*/
|
|
97
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
183
|
+
export const LAYOUT_ENGINE_VERSION = 19 as const;
|
|
98
184
|
|
|
99
185
|
/**
|
|
100
186
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -113,5 +199,14 @@ export const LAYOUT_ENGINE_VERSION = 10 as const;
|
|
|
113
199
|
* correctly invalidates. v1 envelopes are rejected on load under v2 —
|
|
114
200
|
* no corruption path exists because schemaVersion is the top-level
|
|
115
201
|
* discriminator.
|
|
202
|
+
* 3 — L7 Phase 2 Finale C3: adds optional `compatibilityReport` field so
|
|
203
|
+
* the warm-Plan-B short-circuit can skip `buildCompatibilityReport`
|
|
204
|
+
* (~60–100 ms on extra-large). `generatedAt` inside the cached report
|
|
205
|
+
* is pinned to a fixed sentinel (`"__cache_normalized__"`) at
|
|
206
|
+
* prerender write time so two sequential prerenders produce
|
|
207
|
+
* byte-identical envelopes. The field is optional — v3 envelopes
|
|
208
|
+
* written without the report are still valid; readers fall through
|
|
209
|
+
* to the live `buildCompatibilityReport` call. v2 envelopes are
|
|
210
|
+
* rejected cleanly on v3 readers (schemaVersion mismatch).
|
|
116
211
|
*/
|
|
117
|
-
export const LAYCACHE_SCHEMA_VERSION =
|
|
212
|
+
export const LAYCACHE_SCHEMA_VERSION = 3 as const;
|