@beyondwork/docx-react-component 1.0.86 → 1.0.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/public-types.ts +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +580 -40
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/preserve-position.ts +31 -6
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +92 -50
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +32 -3
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -0
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
MAIN_STORY_TARGET,
|
|
125
125
|
storyTargetsEqual,
|
|
126
126
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
127
|
+
type TransactionMapping,
|
|
127
128
|
} from "../core/selection/mapping.ts";
|
|
128
129
|
import {
|
|
129
130
|
toInternalAnchorProjection,
|
|
@@ -291,6 +292,7 @@ import type {
|
|
|
291
292
|
ParagraphNode,
|
|
292
293
|
SectionProperties,
|
|
293
294
|
SubPartsCatalog,
|
|
295
|
+
TocRegion,
|
|
294
296
|
} from "../model/canonical-document.ts";
|
|
295
297
|
import {
|
|
296
298
|
isSupportedFieldFamily,
|
|
@@ -1173,6 +1175,8 @@ export function createDocumentRuntime(
|
|
|
1173
1175
|
// to a single microtask-deferred emit. Declared early because emit() (which
|
|
1174
1176
|
// checks this flag) runs during construction.
|
|
1175
1177
|
let analyticsEmitScheduled = false;
|
|
1178
|
+
let analyticsEmitScheduleMode: "none" | "microtask" | "idle" = "none";
|
|
1179
|
+
let deferNextContextAnalyticsEmit = false;
|
|
1176
1180
|
|
|
1177
1181
|
// V6c — heading fingerprint for TOC auto-invalidation. Compared in notify();
|
|
1178
1182
|
// a mismatch schedules a microtask refresh of TOC fields.
|
|
@@ -1379,6 +1383,8 @@ export function createDocumentRuntime(
|
|
|
1379
1383
|
let viewportBlockRanges: readonly { start: number; end: number }[] | null = null;
|
|
1380
1384
|
/** Serialized fingerprint of the active ranges — used as the idempotency key for `refreshSurfaceOnly`. */
|
|
1381
1385
|
let viewportRangesKey: string = serializeViewportRanges(null);
|
|
1386
|
+
const EDITING_CORRIDOR_BLOCK_RADIUS = 8;
|
|
1387
|
+
const EDITING_CORRIDOR_MIN_BLOCKS = 24;
|
|
1382
1388
|
|
|
1383
1389
|
function applyViewportRanges(
|
|
1384
1390
|
incoming: readonly { start: number; end: number }[] | null,
|
|
@@ -1566,19 +1572,35 @@ export function createDocumentRuntime(
|
|
|
1566
1572
|
function getCachedSurface(
|
|
1567
1573
|
document: CanonicalDocumentEnvelope,
|
|
1568
1574
|
nextActiveStory: EditorStoryTarget,
|
|
1575
|
+
options: {
|
|
1576
|
+
viewportBlockRangesOverride?: readonly { start: number; end: number }[] | null;
|
|
1577
|
+
enrichCulledPlaceholders?: boolean;
|
|
1578
|
+
} = {},
|
|
1569
1579
|
): RuntimeRenderSnapshot["surface"] {
|
|
1570
1580
|
const activeStoryKey = storyTargetKey(nextActiveStory);
|
|
1581
|
+
const surfaceViewportRanges =
|
|
1582
|
+
"viewportBlockRangesOverride" in options
|
|
1583
|
+
? options.viewportBlockRangesOverride ?? null
|
|
1584
|
+
: viewportBlockRanges;
|
|
1585
|
+
const surfaceViewportRangesKey =
|
|
1586
|
+
"viewportBlockRangesOverride" in options
|
|
1587
|
+
? serializeViewportRanges(surfaceViewportRanges)
|
|
1588
|
+
: viewportRangesKey;
|
|
1589
|
+
const surfaceCacheKey =
|
|
1590
|
+
options.enrichCulledPlaceholders === false
|
|
1591
|
+
? `${surfaceViewportRangesKey}|raw-placeholders`
|
|
1592
|
+
: surfaceViewportRangesKey;
|
|
1571
1593
|
if (
|
|
1572
1594
|
cachedSurface &&
|
|
1573
1595
|
cachedSurface.revisionToken === state.revisionToken &&
|
|
1574
1596
|
cachedSurface.activeStoryKey === activeStoryKey &&
|
|
1575
|
-
cachedSurface.viewportRangesKey ===
|
|
1597
|
+
cachedSurface.viewportRangesKey === surfaceCacheKey
|
|
1576
1598
|
) {
|
|
1577
1599
|
return cachedSurface.snapshot;
|
|
1578
1600
|
}
|
|
1579
1601
|
|
|
1580
1602
|
const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory, {
|
|
1581
|
-
viewportBlockRanges,
|
|
1603
|
+
viewportBlockRanges: surfaceViewportRanges,
|
|
1582
1604
|
...(effectiveMarkupModeProvider
|
|
1583
1605
|
? { getEffectiveMarkupMode: effectiveMarkupModeProvider }
|
|
1584
1606
|
: {}),
|
|
@@ -1604,20 +1626,57 @@ export function createDocumentRuntime(
|
|
|
1604
1626
|
//
|
|
1605
1627
|
// No-op on pre-pagination calls (engine not yet ready → empty map)
|
|
1606
1628
|
// and on the inert facet.
|
|
1607
|
-
const enrichedSnapshot =
|
|
1629
|
+
const enrichedSnapshot =
|
|
1630
|
+
options.enrichCulledPlaceholders === false
|
|
1631
|
+
? snapshot
|
|
1632
|
+
: enrichCulledPlaceholdersWithHeights(snapshot);
|
|
1608
1633
|
cachedSurface = {
|
|
1609
1634
|
revisionToken: state.revisionToken,
|
|
1610
1635
|
activeStoryKey,
|
|
1611
|
-
viewportRangesKey,
|
|
1636
|
+
viewportRangesKey: surfaceCacheKey,
|
|
1612
1637
|
snapshot: enrichedSnapshot,
|
|
1613
1638
|
};
|
|
1614
1639
|
// Keep the scroll-path fingerprint in lockstep so a subsequent
|
|
1615
1640
|
// `maybeRefreshSurfaceForViewport` sees the freshly-built snapshot
|
|
1616
1641
|
// and short-circuits instead of paying a redundant projection.
|
|
1617
|
-
cachedSurfaceFingerprint = `${state.revisionToken}|${activeStoryKey}|${
|
|
1642
|
+
cachedSurfaceFingerprint = `${state.revisionToken}|${activeStoryKey}|${surfaceCacheKey}|${String(state.selection.anchor)}:${String(state.selection.head)}`;
|
|
1618
1643
|
return enrichedSnapshot;
|
|
1619
1644
|
}
|
|
1620
1645
|
|
|
1646
|
+
function getLocalTextCommitViewportRanges(
|
|
1647
|
+
previousSurface: RuntimeRenderSnapshot["surface"] | undefined,
|
|
1648
|
+
): readonly { start: number; end: number }[] | null {
|
|
1649
|
+
if (!previousSurface || previousSurface.blocks.length < EDITING_CORRIDOR_MIN_BLOCKS) {
|
|
1650
|
+
return viewportBlockRanges;
|
|
1651
|
+
}
|
|
1652
|
+
const caretBlockIndex = findSurfaceBlockIndexForSelection(previousSurface, state.selection);
|
|
1653
|
+
if (caretBlockIndex === -1) {
|
|
1654
|
+
return viewportBlockRanges;
|
|
1655
|
+
}
|
|
1656
|
+
const corridor = {
|
|
1657
|
+
start: Math.max(0, caretBlockIndex - EDITING_CORRIDOR_BLOCK_RADIUS),
|
|
1658
|
+
end: Math.min(previousSurface.blocks.length, caretBlockIndex + EDITING_CORRIDOR_BLOCK_RADIUS + 1),
|
|
1659
|
+
};
|
|
1660
|
+
return normalizeViewportRanges([...(viewportBlockRanges ?? []), corridor]);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function findSurfaceBlockIndexForSelection(
|
|
1664
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
1665
|
+
selection: EditorState["selection"],
|
|
1666
|
+
): number {
|
|
1667
|
+
if (!surface) return -1;
|
|
1668
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1669
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1670
|
+
for (let index = 0; index < surface.blocks.length; index += 1) {
|
|
1671
|
+
const block = surface.blocks[index];
|
|
1672
|
+
if (!block) continue;
|
|
1673
|
+
if (to >= block.from && from <= block.to) {
|
|
1674
|
+
return index;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return -1;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1621
1680
|
function enrichCulledPlaceholdersWithHeights(
|
|
1622
1681
|
snapshot: EditorSurfaceSnapshot,
|
|
1623
1682
|
): EditorSurfaceSnapshot {
|
|
@@ -2468,6 +2527,57 @@ export function createDocumentRuntime(
|
|
|
2468
2527
|
return snapshot;
|
|
2469
2528
|
}
|
|
2470
2529
|
|
|
2530
|
+
function refreshLocalTextCommitSnapshot(
|
|
2531
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
2532
|
+
mapping: TransactionMapping,
|
|
2533
|
+
): RuntimeRenderSnapshot {
|
|
2534
|
+
perfCounters.increment("refresh.localTextEquivalent");
|
|
2535
|
+
return {
|
|
2536
|
+
...cachedRenderSnapshot,
|
|
2537
|
+
documentId: state.documentId,
|
|
2538
|
+
sessionId: state.sessionId,
|
|
2539
|
+
sourceLabel: state.sourceLabel,
|
|
2540
|
+
revisionToken: state.revisionToken,
|
|
2541
|
+
isReady: state.phase === "ready",
|
|
2542
|
+
isDirty: state.isDirty,
|
|
2543
|
+
readOnly: state.readOnly,
|
|
2544
|
+
documentMode: viewState.documentMode,
|
|
2545
|
+
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
2546
|
+
activeStory,
|
|
2547
|
+
documentStats: updateDocumentStatsForLocalTextCommit(
|
|
2548
|
+
cachedRenderSnapshot.documentStats,
|
|
2549
|
+
mapping,
|
|
2550
|
+
),
|
|
2551
|
+
warnings: cachedRenderSnapshot.warnings,
|
|
2552
|
+
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
2553
|
+
commandState: {
|
|
2554
|
+
canUndo: history.past.length > 0,
|
|
2555
|
+
canRedo: history.future.length > 0,
|
|
2556
|
+
readOnly: state.readOnly,
|
|
2557
|
+
},
|
|
2558
|
+
surface,
|
|
2559
|
+
protectionSnapshot,
|
|
2560
|
+
grabbedObjectId: grabState.objectId,
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function updateDocumentStatsForLocalTextCommit(
|
|
2565
|
+
previousStats: RuntimeRenderSnapshot["documentStats"],
|
|
2566
|
+
mapping: TransactionMapping,
|
|
2567
|
+
): RuntimeRenderSnapshot["documentStats"] {
|
|
2568
|
+
if (!previousStats) {
|
|
2569
|
+
return toPublicDocumentStats(state);
|
|
2570
|
+
}
|
|
2571
|
+
let delta = 0;
|
|
2572
|
+
for (const step of mapping.steps) {
|
|
2573
|
+
delta += step.insertSize - Math.max(0, step.to - step.from);
|
|
2574
|
+
}
|
|
2575
|
+
return {
|
|
2576
|
+
...previousStats,
|
|
2577
|
+
storyLength: Math.max(0, previousStats.storyLength + delta),
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2471
2581
|
/**
|
|
2472
2582
|
* Fingerprint over the inputs that the viewport-refresh fast-path knows
|
|
2473
2583
|
* about. Used to short-circuit {@link maybeRefreshSurfaceForViewport}
|
|
@@ -3565,6 +3675,7 @@ export function createDocumentRuntime(
|
|
|
3565
3675
|
emitError(runtimeError);
|
|
3566
3676
|
return {
|
|
3567
3677
|
kind: "rejected",
|
|
3678
|
+
refreshClass: "blocked",
|
|
3568
3679
|
opId: (command.origin as { opId?: string } | undefined)?.opId,
|
|
3569
3680
|
newRevisionToken: "",
|
|
3570
3681
|
blockedReasons: [
|
|
@@ -4890,14 +5001,52 @@ export function createDocumentRuntime(
|
|
|
4890
5001
|
history.future = [];
|
|
4891
5002
|
}
|
|
4892
5003
|
|
|
4893
|
-
applyTransactionToState(transaction);
|
|
5004
|
+
applyTransactionToState(transaction, { allowLocalTextFastPath: true });
|
|
4894
5005
|
}
|
|
4895
5006
|
|
|
4896
5007
|
function commitRemote(transaction: EditorTransaction): void {
|
|
4897
5008
|
applyTransactionToState(transaction);
|
|
4898
5009
|
}
|
|
4899
5010
|
|
|
4900
|
-
function
|
|
5011
|
+
function shouldUseLocalTextCommitSnapshot(
|
|
5012
|
+
previous: EditorState,
|
|
5013
|
+
next: EditorState,
|
|
5014
|
+
transaction: EditorTransaction,
|
|
5015
|
+
effects: EditorTransaction["effects"],
|
|
5016
|
+
): boolean {
|
|
5017
|
+
if (activeStory.kind !== "main") return false;
|
|
5018
|
+
if (!transaction.markDirty || transaction.mapping.steps.length === 0) {
|
|
5019
|
+
return false;
|
|
5020
|
+
}
|
|
5021
|
+
if (transaction.mapping.metadata?.invalidatesStructures) {
|
|
5022
|
+
return false;
|
|
5023
|
+
}
|
|
5024
|
+
if (previous.document.subParts !== next.document.subParts) {
|
|
5025
|
+
return false;
|
|
5026
|
+
}
|
|
5027
|
+
if (effects.warningsAdded.length > 0 || effects.warningsCleared.length > 0) {
|
|
5028
|
+
return false;
|
|
5029
|
+
}
|
|
5030
|
+
if ((effects.transientWarnings?.length ?? 0) > 0) {
|
|
5031
|
+
return false;
|
|
5032
|
+
}
|
|
5033
|
+
return (
|
|
5034
|
+
!effects.commentAdded &&
|
|
5035
|
+
!effects.commentResolved &&
|
|
5036
|
+
!effects.commentReopened &&
|
|
5037
|
+
!effects.commentReplyAdded &&
|
|
5038
|
+
!effects.commentBodyEdited &&
|
|
5039
|
+
!effects.changeAccepted &&
|
|
5040
|
+
!effects.changeRejected &&
|
|
5041
|
+
!effects.revisionAuthored &&
|
|
5042
|
+
!effects.commandBlocked
|
|
5043
|
+
);
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
function applyTransactionToState(
|
|
5047
|
+
transaction: EditorTransaction,
|
|
5048
|
+
options: { allowLocalTextFastPath?: boolean } = {},
|
|
5049
|
+
): void {
|
|
4901
5050
|
// Pure-no-op short-circuit: when a reducer skipped without emitting any
|
|
4902
5051
|
// observable effect AND the selection is identical, skip finalizeState
|
|
4903
5052
|
// / invalidate / refresh / notify entirely. This keeps hot paths (e.g.
|
|
@@ -4939,7 +5088,9 @@ export function createDocumentRuntime(
|
|
|
4939
5088
|
const previous = state;
|
|
4940
5089
|
|
|
4941
5090
|
const tApply0 = performance.now();
|
|
5091
|
+
const tProtection0 = performance.now();
|
|
4942
5092
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
5093
|
+
perfCounters.increment("commit.protectionRemap.us", Math.round((performance.now() - tProtection0) * 1000));
|
|
4943
5094
|
const tFinalize0 = performance.now();
|
|
4944
5095
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
4945
5096
|
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
@@ -4947,8 +5098,10 @@ export function createDocumentRuntime(
|
|
|
4947
5098
|
// Re-sync marker-backed scope IDs against the new document; the
|
|
4948
5099
|
// store handles set computation + telemetry. `replaceOverlay(current, doc)`
|
|
4949
5100
|
// is idempotent on state and re-derives the marker-backed set.
|
|
5101
|
+
const tOverlay0 = performance.now();
|
|
4950
5102
|
overlayStore.replaceOverlay(overlayStore.getOverlay(), state.document);
|
|
4951
5103
|
const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
5104
|
+
perfCounters.increment("commit.overlaySync.us", Math.round((performance.now() - tOverlay0) * 1000));
|
|
4952
5105
|
|
|
4953
5106
|
const tInvalidate0 = performance.now();
|
|
4954
5107
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
@@ -4972,12 +5125,37 @@ export function createDocumentRuntime(
|
|
|
4972
5125
|
}
|
|
4973
5126
|
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
4974
5127
|
|
|
5128
|
+
const notifyEffects: EditorTransaction["effects"] = {
|
|
5129
|
+
...transaction.effects,
|
|
5130
|
+
warningsAdded: [
|
|
5131
|
+
...transaction.effects.warningsAdded,
|
|
5132
|
+
...detachedWorkflowScopeWarnings.added,
|
|
5133
|
+
],
|
|
5134
|
+
warningsCleared: [
|
|
5135
|
+
...transaction.effects.warningsCleared,
|
|
5136
|
+
...detachedWorkflowScopeWarnings.cleared,
|
|
5137
|
+
],
|
|
5138
|
+
};
|
|
5139
|
+
|
|
5140
|
+
const tClassify0 = performance.now();
|
|
5141
|
+
const useLocalTextCommitSnapshot =
|
|
5142
|
+
options.allowLocalTextFastPath === true &&
|
|
5143
|
+
shouldUseLocalTextCommitSnapshot(
|
|
5144
|
+
previous,
|
|
5145
|
+
state,
|
|
5146
|
+
transaction,
|
|
5147
|
+
notifyEffects,
|
|
5148
|
+
);
|
|
5149
|
+
perfCounters.increment("commit.refreshClassify.us", Math.round((performance.now() - tClassify0) * 1000));
|
|
5150
|
+
|
|
4975
5151
|
// I5: validate post-mutation selection against the new document bound.
|
|
4976
5152
|
// First call to getCachedSurface() here computes the post-mutation surface
|
|
4977
5153
|
// snapshot (cache miss — revisionToken just changed in finalizeState).
|
|
4978
|
-
//
|
|
4979
|
-
//
|
|
4980
|
-
//
|
|
5154
|
+
// For local text commits, this is an editing-corridor surface (current
|
|
5155
|
+
// block plus a small halo) so the immediate typing path keeps visible
|
|
5156
|
+
// truth hot without rebuilding offscreen table/comment markup. Structural,
|
|
5157
|
+
// review, and warning commits still use the full current viewport surface
|
|
5158
|
+
// and fall back to refreshRenderSnapshot().
|
|
4981
5159
|
//
|
|
4982
5160
|
// Wired ONLY at this chokepoint. The other 12 `cachedRenderSnapshot =
|
|
4983
5161
|
// refreshRenderSnapshot()` sites in this file are intentionally skipped:
|
|
@@ -4997,7 +5175,13 @@ export function createDocumentRuntime(
|
|
|
4997
5175
|
// The return type of getCachedSurface is `RuntimeRenderSnapshot["surface"]`
|
|
4998
5176
|
// which is optional in the public API for shape-only reasons — the helper
|
|
4999
5177
|
// itself always returns a defined snapshot.
|
|
5000
|
-
const
|
|
5178
|
+
const tValidation0 = performance.now();
|
|
5179
|
+
const surfaceForValidation = useLocalTextCommitSnapshot
|
|
5180
|
+
? getCachedSurface(state.document, activeStory, {
|
|
5181
|
+
viewportBlockRangesOverride: getLocalTextCommitViewportRanges(cachedRenderSnapshot.surface),
|
|
5182
|
+
enrichCulledPlaceholders: false,
|
|
5183
|
+
})!
|
|
5184
|
+
: getCachedSurface(state.document, activeStory)!;
|
|
5001
5185
|
const validationOptions = state.selection.activeRange.kind === "node"
|
|
5002
5186
|
? {
|
|
5003
5187
|
isValidNodeTarget: createSurfaceNodeSelectionProbe(surfaceForValidation),
|
|
@@ -5013,26 +5197,24 @@ export function createDocumentRuntime(
|
|
|
5013
5197
|
state = { ...state, selection: validatedSelection };
|
|
5014
5198
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
5015
5199
|
}
|
|
5200
|
+
perfCounters.increment("commit.selectionValidation.us", Math.round((performance.now() - tValidation0) * 1000));
|
|
5016
5201
|
|
|
5017
5202
|
const tRefresh0 = performance.now();
|
|
5018
|
-
cachedRenderSnapshot =
|
|
5203
|
+
cachedRenderSnapshot = useLocalTextCommitSnapshot
|
|
5204
|
+
? refreshLocalTextCommitSnapshot(surfaceForValidation, transaction.mapping)
|
|
5205
|
+
: refreshRenderSnapshot();
|
|
5019
5206
|
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
5020
5207
|
|
|
5021
5208
|
const tNotify0 = performance.now();
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
...transaction
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
...transaction.effects.warningsCleared,
|
|
5032
|
-
...detachedWorkflowScopeWarnings.cleared,
|
|
5033
|
-
],
|
|
5034
|
-
},
|
|
5035
|
-
});
|
|
5209
|
+
deferNextContextAnalyticsEmit = useLocalTextCommitSnapshot;
|
|
5210
|
+
try {
|
|
5211
|
+
notify(previous, state, {
|
|
5212
|
+
...transaction,
|
|
5213
|
+
effects: notifyEffects,
|
|
5214
|
+
});
|
|
5215
|
+
} finally {
|
|
5216
|
+
deferNextContextAnalyticsEmit = false;
|
|
5217
|
+
}
|
|
5036
5218
|
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
5037
5219
|
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
5038
5220
|
emitStageToken(telemetryBus, "commit", "commit.apply.complete", {
|
|
@@ -5319,6 +5501,7 @@ export function createDocumentRuntime(
|
|
|
5319
5501
|
});
|
|
5320
5502
|
return completeDispatch({
|
|
5321
5503
|
kind: "rejected",
|
|
5504
|
+
refreshClass: "blocked",
|
|
5322
5505
|
opId,
|
|
5323
5506
|
newRevisionToken: "",
|
|
5324
5507
|
blockedReasons: [{ code: "suggesting_unsupported", message }],
|
|
@@ -5335,6 +5518,7 @@ export function createDocumentRuntime(
|
|
|
5335
5518
|
});
|
|
5336
5519
|
return completeDispatch({
|
|
5337
5520
|
kind: "rejected",
|
|
5521
|
+
refreshClass: "blocked",
|
|
5338
5522
|
opId,
|
|
5339
5523
|
newRevisionToken: "",
|
|
5340
5524
|
blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
|
|
@@ -5408,6 +5592,7 @@ export function createDocumentRuntime(
|
|
|
5408
5592
|
});
|
|
5409
5593
|
return completeDispatch({
|
|
5410
5594
|
kind: "equivalent",
|
|
5595
|
+
refreshClass: "local-text-equivalent",
|
|
5411
5596
|
opId,
|
|
5412
5597
|
newRevisionToken: state.revisionToken,
|
|
5413
5598
|
});
|
|
@@ -5475,6 +5660,7 @@ export function createDocumentRuntime(
|
|
|
5475
5660
|
if (meta.invalidatesStructures) {
|
|
5476
5661
|
return {
|
|
5477
5662
|
kind: "structural-divergence",
|
|
5663
|
+
refreshClass: "full-projection",
|
|
5478
5664
|
opId,
|
|
5479
5665
|
newRevisionToken,
|
|
5480
5666
|
scopeTagTouches: touches,
|
|
@@ -5499,6 +5685,7 @@ export function createDocumentRuntime(
|
|
|
5499
5685
|
const adjustedRange = computeAdjustedRange(priorState, transaction);
|
|
5500
5686
|
return {
|
|
5501
5687
|
kind: "adjusted",
|
|
5688
|
+
refreshClass: "surface-only",
|
|
5502
5689
|
opId,
|
|
5503
5690
|
newRevisionToken,
|
|
5504
5691
|
adjustedRange,
|
|
@@ -5508,6 +5695,7 @@ export function createDocumentRuntime(
|
|
|
5508
5695
|
|
|
5509
5696
|
return {
|
|
5510
5697
|
kind: "equivalent",
|
|
5698
|
+
refreshClass: "local-text-equivalent",
|
|
5511
5699
|
opId,
|
|
5512
5700
|
newRevisionToken,
|
|
5513
5701
|
scopeTagTouches: touches,
|
|
@@ -5600,23 +5788,58 @@ export function createDocumentRuntime(
|
|
|
5600
5788
|
// synchronous call stack — one per commit. (Flag declared above near
|
|
5601
5789
|
// perfCounters so it is initialized before construction-time emits run.)
|
|
5602
5790
|
function scheduleContextAnalyticsEmit(): void {
|
|
5791
|
+
const shouldDefer = deferNextContextAnalyticsEmit;
|
|
5792
|
+
deferNextContextAnalyticsEmit = false;
|
|
5603
5793
|
if (analyticsEmitScheduled) {
|
|
5794
|
+
if (shouldDefer && analyticsEmitScheduleMode === "microtask") {
|
|
5795
|
+
analyticsEmitScheduleMode = "idle";
|
|
5796
|
+
perfCounters.increment("emit.contextAnalytics.deferred");
|
|
5797
|
+
return;
|
|
5798
|
+
}
|
|
5604
5799
|
perfCounters.increment("emit.contextAnalytics.coalesced");
|
|
5605
5800
|
return;
|
|
5606
5801
|
}
|
|
5607
5802
|
analyticsEmitScheduled = true;
|
|
5608
|
-
|
|
5803
|
+
analyticsEmitScheduleMode = shouldDefer ? "idle" : "microtask";
|
|
5804
|
+
const run = () => {
|
|
5609
5805
|
// Reset BEFORE the emit so any synchronous re-entrant emits triggered
|
|
5610
5806
|
// by listener callbacks schedule a fresh second microtask (no lost
|
|
5611
5807
|
// updates). Reversing this order would drop bursts originating in
|
|
5612
5808
|
// listeners.
|
|
5613
5809
|
analyticsEmitScheduled = false;
|
|
5810
|
+
analyticsEmitScheduleMode = "none";
|
|
5614
5811
|
const t = performance.now();
|
|
5615
5812
|
emitContextAnalyticsChanged();
|
|
5616
5813
|
perfCounters.increment("emit.contextAnalytics.us", Math.round((performance.now() - t) * 1000));
|
|
5814
|
+
};
|
|
5815
|
+
if (shouldDefer) {
|
|
5816
|
+
perfCounters.increment("emit.contextAnalytics.deferred");
|
|
5817
|
+
scheduleIdleContextAnalytics(run);
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5820
|
+
queueMicrotask(() => {
|
|
5821
|
+
if (analyticsEmitScheduleMode === "idle") {
|
|
5822
|
+
scheduleIdleContextAnalytics(run);
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
run();
|
|
5617
5826
|
});
|
|
5618
5827
|
}
|
|
5619
5828
|
|
|
5829
|
+
function scheduleIdleContextAnalytics(callback: () => void): void {
|
|
5830
|
+
const requestIdle = (globalThis as {
|
|
5831
|
+
requestIdleCallback?: (
|
|
5832
|
+
cb: () => void,
|
|
5833
|
+
options?: { timeout?: number },
|
|
5834
|
+
) => number;
|
|
5835
|
+
}).requestIdleCallback;
|
|
5836
|
+
if (typeof requestIdle === "function") {
|
|
5837
|
+
requestIdle(callback, { timeout: 250 });
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
setTimeout(callback, 0);
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5620
5843
|
// V6c — TOC auto-refresh scheduler. Mirrors scheduleContextAnalyticsEmit's
|
|
5621
5844
|
// microtask-coalesce shape. Bursts of heading edits within one synchronous
|
|
5622
5845
|
// call stack collapse to a single rebuild + a single toc_auto_refreshed
|
|
@@ -5685,6 +5908,10 @@ export function createDocumentRuntime(
|
|
|
5685
5908
|
emit({
|
|
5686
5909
|
type: "toc_auto_refreshed",
|
|
5687
5910
|
documentId: state.documentId,
|
|
5911
|
+
...(refreshed.result.tocId ? { tocId: refreshed.result.tocId } : {}),
|
|
5912
|
+
...(refreshed.document.fieldRegistry?.tocRegions
|
|
5913
|
+
? { regionCount: refreshed.document.fieldRegistry.tocRegions.length }
|
|
5914
|
+
: {}),
|
|
5688
5915
|
entryCount: refreshed.result.entryCount,
|
|
5689
5916
|
trigger: flushedTrigger,
|
|
5690
5917
|
});
|
|
@@ -7161,6 +7388,29 @@ function refreshDocumentTableOfContents(
|
|
|
7161
7388
|
changed: boolean;
|
|
7162
7389
|
protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
7163
7390
|
} {
|
|
7391
|
+
const selectedRegion = selectTocRegion(document.fieldRegistry?.tocRegions, options?.tocId);
|
|
7392
|
+
const refreshMode = options?.mode ?? "regenerate";
|
|
7393
|
+
if (refreshMode === "preserveCached") {
|
|
7394
|
+
const cachedEntries = selectedRegion?.cachedEntries ?? [];
|
|
7395
|
+
return {
|
|
7396
|
+
document,
|
|
7397
|
+
result: {
|
|
7398
|
+
...(selectedRegion ? { tocId: selectedRegion.tocId } : {}),
|
|
7399
|
+
mode: "preserveCached",
|
|
7400
|
+
status: selectedRegion?.status ?? (cachedEntries.length > 0 ? "stale" : "missing"),
|
|
7401
|
+
entryCount: cachedEntries.length,
|
|
7402
|
+
entries: cachedEntries.map((entry) => ({
|
|
7403
|
+
level: entry.level,
|
|
7404
|
+
text: entry.text,
|
|
7405
|
+
pageIndex: tocPageTextToPageIndex(entry.pageText),
|
|
7406
|
+
...(entry.pageText ? { pageText: entry.pageText } : {}),
|
|
7407
|
+
source: "cached",
|
|
7408
|
+
})),
|
|
7409
|
+
},
|
|
7410
|
+
changed: false,
|
|
7411
|
+
};
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7164
7414
|
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
7165
7415
|
// Build a single O(N) map from paragraph offset → bookmark name so the
|
|
7166
7416
|
// per-heading lookup below is O(1) instead of O(N) per heading.
|
|
@@ -7189,16 +7439,16 @@ function refreshDocumentTableOfContents(
|
|
|
7189
7439
|
}
|
|
7190
7440
|
}
|
|
7191
7441
|
let changed = false;
|
|
7192
|
-
let resultEntries:
|
|
7442
|
+
let resultEntries: RuntimeTocEntry[] = [];
|
|
7193
7443
|
let changedFrom: number | undefined;
|
|
7194
7444
|
let changedTo: number | undefined;
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
return field;
|
|
7198
|
-
}
|
|
7445
|
+
|
|
7446
|
+
const buildEntriesForField = (field: FieldNode): RuntimeTocEntry[] => {
|
|
7199
7447
|
const levelRange = options?.maxLevel
|
|
7200
7448
|
? { from: 1, to: options.maxLevel }
|
|
7201
|
-
:
|
|
7449
|
+
: selectedRegion && normalizeTocInstruction(selectedRegion.instruction.raw) === normalizeTocInstruction(field.instruction)
|
|
7450
|
+
? selectedRegion.instruction.outlineRange
|
|
7451
|
+
: parseTocLevelRange(field.instruction);
|
|
7202
7452
|
const entries = navigation.headings
|
|
7203
7453
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
7204
7454
|
.map((heading) => {
|
|
@@ -7213,6 +7463,30 @@ function refreshDocumentTableOfContents(
|
|
|
7213
7463
|
if (resultEntries.length === 0) {
|
|
7214
7464
|
resultEntries = entries;
|
|
7215
7465
|
}
|
|
7466
|
+
return entries;
|
|
7467
|
+
};
|
|
7468
|
+
|
|
7469
|
+
const tocRangeRefresh = refreshTocBlockRanges(
|
|
7470
|
+
document.content.children,
|
|
7471
|
+
buildEntriesForField,
|
|
7472
|
+
resolveDisplayPageNumber,
|
|
7473
|
+
selectedRegion,
|
|
7474
|
+
);
|
|
7475
|
+
if (tocRangeRefresh.changed) {
|
|
7476
|
+
changed = true;
|
|
7477
|
+
}
|
|
7478
|
+
|
|
7479
|
+
const nextChildren = refreshBlocksWithCursor(tocRangeRefresh.blocks, (field, range) => {
|
|
7480
|
+
if (field.fieldFamily !== "TOC") {
|
|
7481
|
+
return field;
|
|
7482
|
+
}
|
|
7483
|
+
if (selectedRegion && normalizeTocInstruction(field.instruction) !== normalizeTocInstruction(selectedRegion.instruction.raw)) {
|
|
7484
|
+
return field;
|
|
7485
|
+
}
|
|
7486
|
+
if (field.refreshStatus === "current" && field.children.length === 0) {
|
|
7487
|
+
return field;
|
|
7488
|
+
}
|
|
7489
|
+
const entries = buildEntriesForField(field);
|
|
7216
7490
|
const nextField: FieldNode = {
|
|
7217
7491
|
...field,
|
|
7218
7492
|
children: buildTocInlineNodes(entries, resolveDisplayPageNumber),
|
|
@@ -7228,7 +7502,7 @@ function refreshDocumentTableOfContents(
|
|
|
7228
7502
|
if (!changed) {
|
|
7229
7503
|
return {
|
|
7230
7504
|
document,
|
|
7231
|
-
result:
|
|
7505
|
+
result: buildTocRefreshResult(selectedRegion, "regenerate", resultEntries, "current"),
|
|
7232
7506
|
changed: false,
|
|
7233
7507
|
};
|
|
7234
7508
|
}
|
|
@@ -7245,13 +7519,24 @@ function refreshDocumentTableOfContents(
|
|
|
7245
7519
|
styles: nextDocument.styles,
|
|
7246
7520
|
subParts: nextDocument.subParts,
|
|
7247
7521
|
});
|
|
7248
|
-
|
|
7522
|
+
const nextTocRegions = nextRegistry.tocRegions?.map((region) => {
|
|
7523
|
+
if (!selectedRegion || normalizeTocInstruction(region.instruction.raw) === normalizeTocInstruction(selectedRegion.instruction.raw)) {
|
|
7524
|
+
return { ...region, status: "current" as const };
|
|
7525
|
+
}
|
|
7526
|
+
return region;
|
|
7527
|
+
});
|
|
7528
|
+
nextDocument.fieldRegistry = nextRegistry.tocStructure || nextTocRegions
|
|
7249
7529
|
? {
|
|
7250
7530
|
...nextRegistry,
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7531
|
+
...(nextTocRegions ? { tocRegions: nextTocRegions } : {}),
|
|
7532
|
+
...(nextRegistry.tocStructure
|
|
7533
|
+
? {
|
|
7534
|
+
tocStructure: {
|
|
7535
|
+
...nextRegistry.tocStructure,
|
|
7536
|
+
status: "current",
|
|
7537
|
+
},
|
|
7538
|
+
}
|
|
7539
|
+
: {}),
|
|
7255
7540
|
}
|
|
7256
7541
|
: nextRegistry;
|
|
7257
7542
|
let protectionSelection:
|
|
@@ -7263,12 +7548,267 @@ function refreshDocumentTableOfContents(
|
|
|
7263
7548
|
|
|
7264
7549
|
return {
|
|
7265
7550
|
document: nextDocument,
|
|
7266
|
-
result:
|
|
7551
|
+
result: buildTocRefreshResult(selectedRegion, "regenerate", resultEntries, "current"),
|
|
7267
7552
|
changed: true,
|
|
7268
7553
|
...(protectionSelection ? { protectionSelection } : {}),
|
|
7269
7554
|
};
|
|
7270
7555
|
}
|
|
7271
7556
|
|
|
7557
|
+
type RuntimeTocEntry = {
|
|
7558
|
+
level: number;
|
|
7559
|
+
text: string;
|
|
7560
|
+
pageIndex: number;
|
|
7561
|
+
bookmarkName?: string;
|
|
7562
|
+
};
|
|
7563
|
+
|
|
7564
|
+
function selectTocRegion(
|
|
7565
|
+
regions: readonly TocRegion[] | undefined,
|
|
7566
|
+
tocId: string | undefined,
|
|
7567
|
+
): TocRegion | undefined {
|
|
7568
|
+
if (!regions || regions.length === 0) {
|
|
7569
|
+
return undefined;
|
|
7570
|
+
}
|
|
7571
|
+
if (!tocId) {
|
|
7572
|
+
return regions[0];
|
|
7573
|
+
}
|
|
7574
|
+
return regions.find((region) => region.tocId === tocId);
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
function buildTocRefreshResult(
|
|
7578
|
+
region: TocRegion | undefined,
|
|
7579
|
+
mode: "preserveCached" | "regenerate",
|
|
7580
|
+
entries: readonly RuntimeTocEntry[],
|
|
7581
|
+
status: "current" | "stale" | "missing",
|
|
7582
|
+
): TocRefreshResult {
|
|
7583
|
+
return {
|
|
7584
|
+
...(region ? { tocId: region.tocId } : {}),
|
|
7585
|
+
mode,
|
|
7586
|
+
status,
|
|
7587
|
+
entryCount: entries.length,
|
|
7588
|
+
entries: entries.map((entry) => ({
|
|
7589
|
+
level: entry.level,
|
|
7590
|
+
text: entry.text,
|
|
7591
|
+
pageIndex: entry.pageIndex,
|
|
7592
|
+
source: "generated",
|
|
7593
|
+
})),
|
|
7594
|
+
};
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
function tocPageTextToPageIndex(pageText: string | undefined): number {
|
|
7598
|
+
const value = Number.parseInt(pageText ?? "", 10);
|
|
7599
|
+
return Number.isFinite(value) && value > 0 ? value - 1 : 0;
|
|
7600
|
+
}
|
|
7601
|
+
|
|
7602
|
+
function normalizeTocInstruction(instruction: string): string {
|
|
7603
|
+
return instruction.replace(/\s+/gu, " ").trim();
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
function refreshTocBlockRanges(
|
|
7607
|
+
blocks: readonly BlockNode[],
|
|
7608
|
+
buildEntriesForField: (field: FieldNode) => RuntimeTocEntry[],
|
|
7609
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7610
|
+
selectedRegion?: TocRegion,
|
|
7611
|
+
): { blocks: BlockNode[]; changed: boolean } {
|
|
7612
|
+
const nextBlocks: BlockNode[] = [];
|
|
7613
|
+
let changed = false;
|
|
7614
|
+
|
|
7615
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
7616
|
+
const block = blocks[index];
|
|
7617
|
+
|
|
7618
|
+
if (block.type === "paragraph") {
|
|
7619
|
+
const tocField = findTocFieldNode(block);
|
|
7620
|
+
if (
|
|
7621
|
+
tocField &&
|
|
7622
|
+
(!selectedRegion || normalizeTocInstruction(tocField.instruction) === normalizeTocInstruction(selectedRegion.instruction.raw)) &&
|
|
7623
|
+
startsTocParagraphRegion(blocks, index)
|
|
7624
|
+
) {
|
|
7625
|
+
const endIndex = findTocParagraphRegionEnd(blocks, index);
|
|
7626
|
+
const region = blocks.slice(index, endIndex + 1).filter(
|
|
7627
|
+
(candidate): candidate is ParagraphNode => candidate.type === "paragraph",
|
|
7628
|
+
);
|
|
7629
|
+
const replacement = buildTocRegionParagraphs(
|
|
7630
|
+
region,
|
|
7631
|
+
tocField,
|
|
7632
|
+
buildEntriesForField(tocField),
|
|
7633
|
+
resolveDisplayPageNumber,
|
|
7634
|
+
);
|
|
7635
|
+
if (tocRegionSignature(region) !== tocRegionSignature(replacement)) {
|
|
7636
|
+
changed = true;
|
|
7637
|
+
}
|
|
7638
|
+
nextBlocks.push(...replacement);
|
|
7639
|
+
index = endIndex;
|
|
7640
|
+
continue;
|
|
7641
|
+
}
|
|
7642
|
+
nextBlocks.push(block);
|
|
7643
|
+
continue;
|
|
7644
|
+
}
|
|
7645
|
+
|
|
7646
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
7647
|
+
const refreshed = refreshTocBlockRanges(
|
|
7648
|
+
block.children,
|
|
7649
|
+
buildEntriesForField,
|
|
7650
|
+
resolveDisplayPageNumber,
|
|
7651
|
+
selectedRegion,
|
|
7652
|
+
);
|
|
7653
|
+
if (refreshed.changed) {
|
|
7654
|
+
changed = true;
|
|
7655
|
+
nextBlocks.push({ ...block, children: refreshed.blocks });
|
|
7656
|
+
} else {
|
|
7657
|
+
nextBlocks.push(block);
|
|
7658
|
+
}
|
|
7659
|
+
continue;
|
|
7660
|
+
}
|
|
7661
|
+
|
|
7662
|
+
if (block.type === "table") {
|
|
7663
|
+
let tableChanged = false;
|
|
7664
|
+
const rows = block.rows.map((row) => {
|
|
7665
|
+
let rowChanged = false;
|
|
7666
|
+
const cells = row.cells.map((cell) => {
|
|
7667
|
+
const refreshed = refreshTocBlockRanges(
|
|
7668
|
+
cell.children,
|
|
7669
|
+
buildEntriesForField,
|
|
7670
|
+
resolveDisplayPageNumber,
|
|
7671
|
+
selectedRegion,
|
|
7672
|
+
);
|
|
7673
|
+
if (!refreshed.changed) {
|
|
7674
|
+
return cell;
|
|
7675
|
+
}
|
|
7676
|
+
tableChanged = true;
|
|
7677
|
+
rowChanged = true;
|
|
7678
|
+
return { ...cell, children: refreshed.blocks };
|
|
7679
|
+
});
|
|
7680
|
+
return rowChanged ? { ...row, cells } : row;
|
|
7681
|
+
});
|
|
7682
|
+
nextBlocks.push(tableChanged ? { ...block, rows } : block);
|
|
7683
|
+
changed = changed || tableChanged;
|
|
7684
|
+
continue;
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
nextBlocks.push(block);
|
|
7688
|
+
}
|
|
7689
|
+
|
|
7690
|
+
return { blocks: nextBlocks, changed };
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
function startsTocParagraphRegion(blocks: readonly BlockNode[], index: number): boolean {
|
|
7694
|
+
const current = blocks[index];
|
|
7695
|
+
if (current?.type === "paragraph" && isTocParagraphStyle(current.styleId)) {
|
|
7696
|
+
return true;
|
|
7697
|
+
}
|
|
7698
|
+
const next = blocks[index + 1];
|
|
7699
|
+
return next?.type === "paragraph" && isTocParagraphStyle(next.styleId);
|
|
7700
|
+
}
|
|
7701
|
+
|
|
7702
|
+
function findTocParagraphRegionEnd(blocks: readonly BlockNode[], startIndex: number): number {
|
|
7703
|
+
let endIndex = startIndex;
|
|
7704
|
+
while (endIndex + 1 < blocks.length) {
|
|
7705
|
+
const next = blocks[endIndex + 1];
|
|
7706
|
+
if (next?.type !== "paragraph" || !isTocParagraphStyle(next.styleId)) {
|
|
7707
|
+
break;
|
|
7708
|
+
}
|
|
7709
|
+
endIndex += 1;
|
|
7710
|
+
}
|
|
7711
|
+
return endIndex;
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
function isTocParagraphStyle(styleId: string | undefined): boolean {
|
|
7715
|
+
return /^TOC\d+$/u.test(styleId ?? "");
|
|
7716
|
+
}
|
|
7717
|
+
|
|
7718
|
+
function findTocFieldNode(paragraph: ParagraphNode): FieldNode | undefined {
|
|
7719
|
+
return paragraph.children.find(
|
|
7720
|
+
(child): child is FieldNode => child.type === "field" && child.fieldFamily === "TOC",
|
|
7721
|
+
);
|
|
7722
|
+
}
|
|
7723
|
+
|
|
7724
|
+
function buildTocRegionParagraphs(
|
|
7725
|
+
existingRegion: readonly ParagraphNode[],
|
|
7726
|
+
tocField: FieldNode,
|
|
7727
|
+
entries: readonly RuntimeTocEntry[],
|
|
7728
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7729
|
+
): ParagraphNode[] {
|
|
7730
|
+
const firstTemplate = existingRegion[0];
|
|
7731
|
+
if (!firstTemplate) {
|
|
7732
|
+
return [];
|
|
7733
|
+
}
|
|
7734
|
+
const templateByStyle = new Map<string, ParagraphNode>();
|
|
7735
|
+
for (const paragraph of existingRegion) {
|
|
7736
|
+
if (paragraph.styleId && !templateByStyle.has(paragraph.styleId)) {
|
|
7737
|
+
templateByStyle.set(paragraph.styleId, paragraph);
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
const refreshedField: FieldNode = {
|
|
7741
|
+
...tocField,
|
|
7742
|
+
children: [],
|
|
7743
|
+
refreshStatus: "current",
|
|
7744
|
+
};
|
|
7745
|
+
|
|
7746
|
+
if (entries.length === 0) {
|
|
7747
|
+
return [{ ...firstTemplate, children: [refreshedField] }];
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
return entries.map((entry, index) => {
|
|
7751
|
+
const styleId = `TOC${Math.max(1, Math.min(9, entry.level))}`;
|
|
7752
|
+
const template = templateByStyle.get(styleId) ?? firstTemplate;
|
|
7753
|
+
const children = buildTocEntryInlineNodes(entry, resolveDisplayPageNumber);
|
|
7754
|
+
return {
|
|
7755
|
+
...template,
|
|
7756
|
+
styleId,
|
|
7757
|
+
children: index === 0 ? [refreshedField, ...children] : children,
|
|
7758
|
+
};
|
|
7759
|
+
});
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
function buildTocEntryInlineNodes(
|
|
7763
|
+
entry: RuntimeTocEntry,
|
|
7764
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7765
|
+
): InlineNode[] {
|
|
7766
|
+
const children: InlineNode[] = [];
|
|
7767
|
+
if (entry.bookmarkName) {
|
|
7768
|
+
children.push({
|
|
7769
|
+
type: "hyperlink",
|
|
7770
|
+
href: `#${entry.bookmarkName}`,
|
|
7771
|
+
children: [{ type: "text", text: entry.text }],
|
|
7772
|
+
});
|
|
7773
|
+
} else {
|
|
7774
|
+
children.push({ type: "text", text: entry.text });
|
|
7775
|
+
}
|
|
7776
|
+
children.push({ type: "tab" });
|
|
7777
|
+
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
7778
|
+
children.push({
|
|
7779
|
+
type: "text",
|
|
7780
|
+
text: String(displayed ?? entry.pageIndex + 1),
|
|
7781
|
+
});
|
|
7782
|
+
return children;
|
|
7783
|
+
}
|
|
7784
|
+
|
|
7785
|
+
function tocRegionSignature(paragraphs: readonly ParagraphNode[]): string {
|
|
7786
|
+
return paragraphs
|
|
7787
|
+
.map((paragraph) => `${paragraph.styleId ?? ""}\u0001${tocInlineSignature(paragraph.children)}`)
|
|
7788
|
+
.join("\u0002");
|
|
7789
|
+
}
|
|
7790
|
+
|
|
7791
|
+
function tocInlineSignature(children: readonly InlineNode[]): string {
|
|
7792
|
+
return children
|
|
7793
|
+
.map((child) => {
|
|
7794
|
+
switch (child.type) {
|
|
7795
|
+
case "text":
|
|
7796
|
+
return `t:${child.text}`;
|
|
7797
|
+
case "tab":
|
|
7798
|
+
return "tab";
|
|
7799
|
+
case "hard_break":
|
|
7800
|
+
return "br";
|
|
7801
|
+
case "hyperlink":
|
|
7802
|
+
return `link:${child.href}:${tocInlineSignature(child.children)}`;
|
|
7803
|
+
case "field":
|
|
7804
|
+
return `field:${child.fieldFamily ?? ""}:${child.refreshStatus ?? ""}:${child.instruction}:${tocInlineSignature(child.children)}`;
|
|
7805
|
+
default:
|
|
7806
|
+
return child.type;
|
|
7807
|
+
}
|
|
7808
|
+
})
|
|
7809
|
+
.join("\u0001");
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7272
7812
|
function refreshBlocksWithCursor(
|
|
7273
7813
|
blocks: readonly BlockNode[],
|
|
7274
7814
|
visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
|