@beyondwork/docx-react-component 1.0.56 → 1.0.58
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/README.md +1 -1
- package/package.json +1 -1
- package/src/api/public-types.ts +330 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +158 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +421 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +760 -41
- package/src/runtime/document-search.ts +61 -0
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/query-scopes.ts +186 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +192 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +239 -11
- package/src/ui/editor-runtime-boundary.ts +97 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +24 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +157 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +214 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +23 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
type EditorState,
|
|
13
13
|
type EditorWarning as InternalEditorWarning,
|
|
14
14
|
} from "../core/state/editor-state.ts";
|
|
15
|
+
import {
|
|
16
|
+
logicalPositionToUnitIndex,
|
|
17
|
+
parseTextStory,
|
|
18
|
+
serializeTextStory,
|
|
19
|
+
} from "../core/schema/text-schema.ts";
|
|
15
20
|
import type {
|
|
16
21
|
AddCommentParams,
|
|
17
22
|
AddCommentReplyResult,
|
|
@@ -61,6 +66,7 @@ import type {
|
|
|
61
66
|
SurfaceInlineSegment,
|
|
62
67
|
StoryTextStreamSnapshot,
|
|
63
68
|
TextCommandAck,
|
|
69
|
+
TocRefreshTrigger,
|
|
64
70
|
TocSnapshot,
|
|
65
71
|
StyleCatalogSnapshot,
|
|
66
72
|
TocRefreshOptions,
|
|
@@ -80,6 +86,8 @@ import type {
|
|
|
80
86
|
WorkflowOverlay,
|
|
81
87
|
WorkflowScope,
|
|
82
88
|
WorkflowScopeSnapshot,
|
|
89
|
+
ScopeQueryFilter,
|
|
90
|
+
ScopeQueryResult,
|
|
83
91
|
WorkspaceMode,
|
|
84
92
|
WordReviewEditorEvent,
|
|
85
93
|
ZoomLevel,
|
|
@@ -115,6 +123,8 @@ import {
|
|
|
115
123
|
snapCommentAnchorAwayFromTable,
|
|
116
124
|
} from "../core/selection/review-anchors.ts";
|
|
117
125
|
import { buildBookmarkNameMap } from "../legal/bookmarks.ts";
|
|
126
|
+
import { createFieldResolver, type FieldResolver } from "./field-resolver.ts";
|
|
127
|
+
import { createFootnoteResolver, type FootnoteResolver } from "./footnote-resolver.ts";
|
|
118
128
|
import {
|
|
119
129
|
describeOpaqueFragment,
|
|
120
130
|
findOpaqueFragmentsIntersectingRange,
|
|
@@ -128,7 +138,16 @@ import {
|
|
|
128
138
|
} from "../review/store/revision-store.ts";
|
|
129
139
|
import { createSuggestionsSnapshot } from "./suggestions-snapshot.ts";
|
|
130
140
|
import { validateSelectionAgainstDocument } from "./selection/post-edit-validator.ts";
|
|
131
|
-
import {
|
|
141
|
+
import {
|
|
142
|
+
collectScopeLocations,
|
|
143
|
+
findAllScopesAt,
|
|
144
|
+
findScopesIntersecting,
|
|
145
|
+
resolveScope,
|
|
146
|
+
} from "./scope-resolver.ts";
|
|
147
|
+
import {
|
|
148
|
+
projectScopeQueryResults,
|
|
149
|
+
queryScopes as runQueryScopes,
|
|
150
|
+
} from "./query-scopes.ts";
|
|
132
151
|
import {
|
|
133
152
|
insertScopeMarkers,
|
|
134
153
|
removeScopeMarkers,
|
|
@@ -205,13 +224,16 @@ import {
|
|
|
205
224
|
createEditorViewStateSnapshot,
|
|
206
225
|
type ViewState,
|
|
207
226
|
} from "./view-state.ts";
|
|
227
|
+
import { ThemeColorResolver } from "./theme-color-resolver.ts";
|
|
208
228
|
import type {
|
|
209
229
|
BlockNode,
|
|
230
|
+
CanonicalDocument,
|
|
210
231
|
FieldNode,
|
|
211
232
|
FieldRefreshStatus,
|
|
212
233
|
InlineNode,
|
|
213
234
|
PageMargins,
|
|
214
235
|
ParagraphNode,
|
|
236
|
+
SectionProperties,
|
|
215
237
|
SubPartsCatalog,
|
|
216
238
|
} from "../model/canonical-document.ts";
|
|
217
239
|
import {
|
|
@@ -240,6 +262,14 @@ import type {
|
|
|
240
262
|
EditorStatePersister,
|
|
241
263
|
} from "../api/editor-state-types.ts";
|
|
242
264
|
import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
|
|
265
|
+
import { serializeFragmentToWordML } from "../io/paste/word-clipboard.ts";
|
|
266
|
+
import {
|
|
267
|
+
createObjectGrabState,
|
|
268
|
+
deselectObject as grabDeselectObject,
|
|
269
|
+
getGrabbedObject as grabGetGrabbedObject,
|
|
270
|
+
selectObject as grabSelectObject,
|
|
271
|
+
type ObjectGrabState,
|
|
272
|
+
} from "./object-grab/index.ts";
|
|
243
273
|
import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
|
|
244
274
|
import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
|
|
245
275
|
import { formatPageNumber } from "./page-number-format.ts";
|
|
@@ -277,8 +307,77 @@ export interface DocumentRuntime {
|
|
|
277
307
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
278
308
|
getCanonicalDocument(): CanonicalDocumentEnvelope;
|
|
279
309
|
getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
|
|
310
|
+
/** Return the parsed fontTable, if present in the loaded package. */
|
|
311
|
+
getFontTable(): CanonicalDocumentEnvelope["fontTable"];
|
|
312
|
+
/**
|
|
313
|
+
* Convenience accessor — return the `CanonicalFontEntry` for `name`, or
|
|
314
|
+
* undefined when the loaded package has no fontTable or no matching entry.
|
|
315
|
+
*/
|
|
316
|
+
getFontEntry(
|
|
317
|
+
name: string,
|
|
318
|
+
): NonNullable<CanonicalDocumentEnvelope["fontTable"]>["fonts"][string] | undefined;
|
|
280
319
|
replaceText(text: string, target?: EditorAnchorProjection, formatting?: TextFormattingDirective): void;
|
|
281
320
|
insertFragment(fragment: CanonicalDocumentFragment, target?: EditorAnchorProjection): void;
|
|
321
|
+
/**
|
|
322
|
+
* I2 Tier B Slice 4b — serialize the selection range to a
|
|
323
|
+
* `CanonicalDocumentFragment` and store it in an internal clipboard buffer.
|
|
324
|
+
* No document mutation. `target` defaults to the current selection.
|
|
325
|
+
*/
|
|
326
|
+
copy(target?: EditorAnchorProjection): void;
|
|
327
|
+
/**
|
|
328
|
+
* I2 Tier B Slice 4b — `copy(target)` + delete the range. Safe on empty /
|
|
329
|
+
* collapsed ranges (no-op).
|
|
330
|
+
*/
|
|
331
|
+
cut(target?: EditorAnchorProjection): void;
|
|
332
|
+
/**
|
|
333
|
+
* I2 Tier B Slice 4b — return the last fragment stored via `cut` / `copy`,
|
|
334
|
+
* or `null` when no clipboard operation has been performed yet. Mirrors
|
|
335
|
+
* what a browser `clipboardData.getData("web application/x-clip")` would
|
|
336
|
+
* return in a system-clipboard-aware build; for now hosts can use this to
|
|
337
|
+
* feed `insertFragment` directly.
|
|
338
|
+
*/
|
|
339
|
+
getClipboardBuffer(): CanonicalDocumentFragment | null;
|
|
340
|
+
/**
|
|
341
|
+
* v5 close-out — return the current clipboard buffer serialized to the
|
|
342
|
+
* wire formats browsers/Word accept, or `null` when no clipboard op has
|
|
343
|
+
* been performed. Hosts pair this with `navigator.clipboard.write` inside
|
|
344
|
+
* their own DOM `copy`/`cut` event handler; the editor does not install
|
|
345
|
+
* the DOM handler itself because hosts often route cut/copy through their
|
|
346
|
+
* own protocol. Three formats: WordML (`application/x-docx-fragment`),
|
|
347
|
+
* HTML (`text/html`), and plain text (`text/plain`).
|
|
348
|
+
*/
|
|
349
|
+
getClipboardWireFormats(): { wordml: string; html: string; plainText: string } | null;
|
|
350
|
+
/**
|
|
351
|
+
* R.3 ObjectGrabLayer — grab an inline / floating object (image, shape)
|
|
352
|
+
* by its stable id. Single-select model. Local-only state; not broadcast
|
|
353
|
+
* through collab. Lane 6 P11 paints the chrome handles.
|
|
354
|
+
*/
|
|
355
|
+
selectObject(objectId: string): void;
|
|
356
|
+
/**
|
|
357
|
+
* R.3 — release any grabbed object. Safe to call when nothing is grabbed.
|
|
358
|
+
*/
|
|
359
|
+
deselectObject(): void;
|
|
360
|
+
/**
|
|
361
|
+
* R.3 — return the currently grabbed object id, or `null` when no
|
|
362
|
+
* object is grabbed.
|
|
363
|
+
*/
|
|
364
|
+
getGrabbedObject(): string | null;
|
|
365
|
+
/**
|
|
366
|
+
* R.5.a — open an action bracket. Hosts use this to group compound edits
|
|
367
|
+
* (paste → insertFragment, cut → copy+delete, agent suggestion-apply) so
|
|
368
|
+
* snapshot emission + collab broadcast + undo grouping see them as one
|
|
369
|
+
* action. Nested brackets are tracked by depth; only the outermost
|
|
370
|
+
* `endAction` completes the bracket.
|
|
371
|
+
*
|
|
372
|
+
* Phase 1 (Item E): this API ships opt-in. Commands don't auto-bracket
|
|
373
|
+
* themselves yet — hosts that want single-undo paste must call
|
|
374
|
+
* `startAction` / `endAction` around their `insertFragment` call.
|
|
375
|
+
*/
|
|
376
|
+
startAction(name: string): void;
|
|
377
|
+
/** R.5.a — close one level of action bracketing. Unbalanced calls are no-ops. */
|
|
378
|
+
endAction(): void;
|
|
379
|
+
/** R.5.a — `true` when the runtime is inside one or more action brackets. */
|
|
380
|
+
isInAction(): boolean;
|
|
282
381
|
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
|
|
283
382
|
dispatch(command: EditorCommand): void;
|
|
284
383
|
/**
|
|
@@ -382,6 +481,20 @@ export interface DocumentRuntime {
|
|
|
382
481
|
}): DocumentSectionSnapshot | null;
|
|
383
482
|
describeEventImpact(event: WordReviewEditorEvent): SnapshotRefreshHints;
|
|
384
483
|
getFieldSnapshot(): FieldSnapshot;
|
|
484
|
+
/**
|
|
485
|
+
* CO3.5 — Field resolver exposing `resolve(entry)` for PAGE / NUMPAGES /
|
|
486
|
+
* PAGEREF / REF / STYLEREF. TOC entries return `undefined`. Reads
|
|
487
|
+
* `layoutEngine.getPageGraph()` + the active page index + the bookmark
|
|
488
|
+
* name map + paragraph start-offsets, so it updates naturally with the
|
|
489
|
+
* current document state.
|
|
490
|
+
*/
|
|
491
|
+
getFieldResolver(): FieldResolver;
|
|
492
|
+
/**
|
|
493
|
+
* CO3.5 — Footnote / endnote resolver. Returns `undefined` when the
|
|
494
|
+
* document has no footnote collection attached (no `footnotes.xml`
|
|
495
|
+
* part).
|
|
496
|
+
*/
|
|
497
|
+
getFootnoteResolver(): FootnoteResolver | undefined;
|
|
385
498
|
updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult;
|
|
386
499
|
updateTableOfContents(options?: TocRefreshOptions): TocRefreshResult;
|
|
387
500
|
getSessionState(): EditorSessionState;
|
|
@@ -402,6 +515,27 @@ export interface DocumentRuntime {
|
|
|
402
515
|
setWorkflowMetadataEntries(entries: WorkflowMetadataEntry[]): void;
|
|
403
516
|
clearWorkflowMetadataEntries(): void;
|
|
404
517
|
getWorkflowMetadataSnapshot(): WorkflowMetadataSnapshot;
|
|
518
|
+
/**
|
|
519
|
+
* Phase C §C1 — snapshot-based filter + join projection. See
|
|
520
|
+
* `WordReviewEditorRef.queryScopes` for contract.
|
|
521
|
+
*/
|
|
522
|
+
queryScopes(filter?: ScopeQueryFilter): ScopeQueryResult[];
|
|
523
|
+
/**
|
|
524
|
+
* Phase C §C2 — geometric scope queries. See `WordReviewEditorRef`
|
|
525
|
+
* for contract. Non-range anchors yield `[]`.
|
|
526
|
+
*/
|
|
527
|
+
findScopesAt(
|
|
528
|
+
position: EditorAnchorProjection,
|
|
529
|
+
options?: { includeHidden?: boolean; includeInvisible?: boolean },
|
|
530
|
+
): ScopeQueryResult[];
|
|
531
|
+
findScopesIntersecting(
|
|
532
|
+
range: EditorAnchorProjection,
|
|
533
|
+
options?: {
|
|
534
|
+
includeHidden?: boolean;
|
|
535
|
+
includeInvisible?: boolean;
|
|
536
|
+
mode?: "overlap" | "contain";
|
|
537
|
+
},
|
|
538
|
+
): ScopeQueryResult[];
|
|
405
539
|
setHostAnnotationOverlay(overlay: HostAnnotationOverlay): void;
|
|
406
540
|
clearHostAnnotationOverlay(): void;
|
|
407
541
|
getHostAnnotationSnapshot(): HostAnnotationSnapshot;
|
|
@@ -638,6 +772,12 @@ export function createDocumentRuntime(
|
|
|
638
772
|
// checks this flag) runs during construction.
|
|
639
773
|
let analyticsEmitScheduled = false;
|
|
640
774
|
|
|
775
|
+
// V6c — heading fingerprint for TOC auto-invalidation. Compared in notify();
|
|
776
|
+
// a mismatch schedules a microtask refresh of TOC fields.
|
|
777
|
+
let lastHeadingFingerprint: string = "";
|
|
778
|
+
let tocAutoRefreshScheduled = false;
|
|
779
|
+
let pendingTocTrigger: TocRefreshTrigger | null = null;
|
|
780
|
+
|
|
641
781
|
// Schema 1.2 — editor-state channel (policy, resolver/persister, debounce queues).
|
|
642
782
|
// Instantiated once per runtime; forwarded to the public interface.
|
|
643
783
|
const editorStateChannel = createEditorStateChannel();
|
|
@@ -693,7 +833,21 @@ export function createDocumentRuntime(
|
|
|
693
833
|
canonicalDocument: options.initialCanonicalDocument,
|
|
694
834
|
fatalError: options.fatalError as never,
|
|
695
835
|
});
|
|
836
|
+
// I2 Tier B Slice 4b — internal clipboard buffer for cut/copy. System-
|
|
837
|
+
// clipboard write lands with Slice 5 drag; for now hosts read this via
|
|
838
|
+
// `getClipboardBuffer()` and feed it to `insertFragment`.
|
|
839
|
+
let clipboardBuffer: CanonicalDocumentFragment | null = null;
|
|
840
|
+
// R.3 ObjectGrabLayer — local-only grab state for inline / floating objects.
|
|
841
|
+
// Not broadcast through collab; each peer has their own (mirrors text selection).
|
|
842
|
+
let grabState: ObjectGrabState = createObjectGrabState();
|
|
843
|
+
// R.5.a action bracketing — depth counter. `startAction` increments,
|
|
844
|
+
// `endAction` decrements (clamped at 0). `isInAction` returns `depth > 0`.
|
|
845
|
+
// Phase 2 (follow-up) will use this to gate snapshot emission so nested
|
|
846
|
+
// commands observe a single boundary.
|
|
847
|
+
let actionDepth = 0;
|
|
848
|
+
const actionStack: string[] = [];
|
|
696
849
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
850
|
+
lastHeadingFingerprint = computeHeadingFingerprint(state.document);
|
|
697
851
|
|
|
698
852
|
// Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
|
|
699
853
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
@@ -736,17 +890,18 @@ export function createDocumentRuntime(
|
|
|
736
890
|
canonicalDocument: () => state.document,
|
|
737
891
|
renderKernel: () => renderKernelRef,
|
|
738
892
|
getWorkflowRailInput: () => {
|
|
739
|
-
|
|
740
|
-
|
|
893
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
894
|
+
if (!normalizedWorkflowOverlay) return null;
|
|
895
|
+
const activeWorkItemId = normalizedWorkflowOverlay.activeWorkItemId ?? null;
|
|
741
896
|
const activeWorkItem =
|
|
742
897
|
activeWorkItemId !== null
|
|
743
|
-
?
|
|
898
|
+
? normalizedWorkflowOverlay.workItems?.find(
|
|
744
899
|
(item) => item.workItemId === activeWorkItemId,
|
|
745
900
|
)
|
|
746
901
|
: undefined;
|
|
747
902
|
return {
|
|
748
|
-
scopes:
|
|
749
|
-
candidates:
|
|
903
|
+
scopes: normalizedWorkflowOverlay.scopes,
|
|
904
|
+
candidates: normalizedWorkflowOverlay.candidates,
|
|
750
905
|
activeWorkItemScopeIds: activeWorkItem?.scopeIds ?? [],
|
|
751
906
|
activeStory,
|
|
752
907
|
};
|
|
@@ -933,6 +1088,13 @@ export function createDocumentRuntime(
|
|
|
933
1088
|
snapshot: WorkflowScopeSnapshot;
|
|
934
1089
|
}
|
|
935
1090
|
| undefined;
|
|
1091
|
+
let cachedNormalizedWorkflowOverlay:
|
|
1092
|
+
| {
|
|
1093
|
+
document: CanonicalDocumentEnvelope;
|
|
1094
|
+
workflowOverlay: WorkflowOverlay;
|
|
1095
|
+
normalized: WorkflowOverlay;
|
|
1096
|
+
}
|
|
1097
|
+
| undefined;
|
|
936
1098
|
let cachedWorkflowMarkupSnapshot:
|
|
937
1099
|
| {
|
|
938
1100
|
revisionToken: string;
|
|
@@ -1258,10 +1420,12 @@ export function createDocumentRuntime(
|
|
|
1258
1420
|
}
|
|
1259
1421
|
}
|
|
1260
1422
|
|
|
1261
|
-
|
|
1423
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1424
|
+
if (normalizedWorkflowOverlay) {
|
|
1262
1425
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
1426
|
+
const activeScopes = getEffectiveWorkflowScopes(normalizedWorkflowOverlay);
|
|
1263
1427
|
|
|
1264
|
-
if (!matchingScope &&
|
|
1428
|
+
if (!matchingScope && activeScopes.length > 0) {
|
|
1265
1429
|
reasons.push({
|
|
1266
1430
|
code: "outside_workflow_scope",
|
|
1267
1431
|
message: "Selection is outside any active workflow scope.",
|
|
@@ -1557,20 +1721,114 @@ export function createDocumentRuntime(
|
|
|
1557
1721
|
return left.from < right.to && right.from < left.to;
|
|
1558
1722
|
}
|
|
1559
1723
|
|
|
1560
|
-
function
|
|
1724
|
+
function workflowAnchorsEqual(
|
|
1725
|
+
left: EditorAnchorProjection,
|
|
1726
|
+
right: EditorAnchorProjection,
|
|
1727
|
+
): boolean {
|
|
1728
|
+
if (left.kind !== right.kind) return false;
|
|
1729
|
+
switch (left.kind) {
|
|
1730
|
+
case "range":
|
|
1731
|
+
return (
|
|
1732
|
+
right.kind === "range" &&
|
|
1733
|
+
left.from === right.from &&
|
|
1734
|
+
left.to === right.to &&
|
|
1735
|
+
left.assoc.start === right.assoc.start &&
|
|
1736
|
+
left.assoc.end === right.assoc.end
|
|
1737
|
+
);
|
|
1738
|
+
case "node":
|
|
1739
|
+
return right.kind === "node" && left.at === right.at;
|
|
1740
|
+
case "detached":
|
|
1741
|
+
return (
|
|
1742
|
+
right.kind === "detached" &&
|
|
1743
|
+
left.reason === right.reason &&
|
|
1744
|
+
left.lastKnownRange.from === right.lastKnownRange.from &&
|
|
1745
|
+
left.lastKnownRange.to === right.lastKnownRange.to
|
|
1746
|
+
);
|
|
1747
|
+
default:
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function normalizeWorkflowOverlayForDocument(
|
|
1753
|
+
document: CanonicalDocumentEnvelope,
|
|
1754
|
+
overlay: WorkflowOverlay,
|
|
1755
|
+
): WorkflowOverlay {
|
|
1756
|
+
if (
|
|
1757
|
+
cachedNormalizedWorkflowOverlay &&
|
|
1758
|
+
cachedNormalizedWorkflowOverlay.document === document &&
|
|
1759
|
+
cachedNormalizedWorkflowOverlay.workflowOverlay === overlay
|
|
1760
|
+
) {
|
|
1761
|
+
return cachedNormalizedWorkflowOverlay.normalized;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const scopeIdCounts = new Map<string, number>();
|
|
1765
|
+
for (const scope of overlay.scopes) {
|
|
1766
|
+
scopeIdCounts.set(scope.scopeId, (scopeIdCounts.get(scope.scopeId) ?? 0) + 1);
|
|
1767
|
+
}
|
|
1768
|
+
const locations = collectScopeLocations(document);
|
|
1769
|
+
let changed = false;
|
|
1770
|
+
const normalizedScopes = overlay.scopes.map((scope) => {
|
|
1771
|
+
if ((scopeIdCounts.get(scope.scopeId) ?? 0) !== 1) {
|
|
1772
|
+
return scope;
|
|
1773
|
+
}
|
|
1774
|
+
const location = locations.get(scope.scopeId);
|
|
1775
|
+
if (
|
|
1776
|
+
!location ||
|
|
1777
|
+
location.startPos === undefined ||
|
|
1778
|
+
location.endPos === undefined
|
|
1779
|
+
) {
|
|
1780
|
+
return scope;
|
|
1781
|
+
}
|
|
1782
|
+
const nextAnchor: EditorAnchorProjection = {
|
|
1783
|
+
kind: "range",
|
|
1784
|
+
from: Math.min(location.startPos, location.endPos),
|
|
1785
|
+
to: Math.max(location.startPos, location.endPos),
|
|
1786
|
+
assoc: { start: -1, end: 1 },
|
|
1787
|
+
};
|
|
1788
|
+
if (workflowAnchorsEqual(scope.anchor, nextAnchor)) {
|
|
1789
|
+
return scope;
|
|
1790
|
+
}
|
|
1791
|
+
changed = true;
|
|
1792
|
+
return {
|
|
1793
|
+
...scope,
|
|
1794
|
+
anchor: nextAnchor,
|
|
1795
|
+
};
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
const normalized = changed
|
|
1799
|
+
? {
|
|
1800
|
+
...overlay,
|
|
1801
|
+
scopes: normalizedScopes,
|
|
1802
|
+
}
|
|
1803
|
+
: overlay;
|
|
1804
|
+
cachedNormalizedWorkflowOverlay = {
|
|
1805
|
+
document,
|
|
1806
|
+
workflowOverlay: overlay,
|
|
1807
|
+
normalized,
|
|
1808
|
+
};
|
|
1809
|
+
return normalized;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function getNormalizedWorkflowOverlay(): WorkflowOverlay | null {
|
|
1561
1813
|
if (!workflowOverlay) return null;
|
|
1814
|
+
return normalizeWorkflowOverlayForDocument(state.document, workflowOverlay);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function deriveWorkflowScopeSnapshot(): WorkflowScopeSnapshot | null {
|
|
1818
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1819
|
+
if (!normalizedWorkflowOverlay) return null;
|
|
1562
1820
|
const blockedReasons = getCachedInteractionGuardSnapshot().blockedReasons;
|
|
1563
|
-
const activeItem =
|
|
1564
|
-
?
|
|
1565
|
-
(item) => item.workItemId ===
|
|
1821
|
+
const activeItem = normalizedWorkflowOverlay.activeWorkItemId
|
|
1822
|
+
? normalizedWorkflowOverlay.workItems?.find(
|
|
1823
|
+
(item) => item.workItemId === normalizedWorkflowOverlay.activeWorkItemId,
|
|
1566
1824
|
)
|
|
1567
1825
|
: undefined;
|
|
1568
1826
|
return {
|
|
1569
1827
|
overlayPresent: true,
|
|
1570
|
-
activeWorkItemId:
|
|
1828
|
+
activeWorkItemId: normalizedWorkflowOverlay.activeWorkItemId ?? null,
|
|
1571
1829
|
activeWorkItem: activeItem,
|
|
1572
|
-
scopes:
|
|
1573
|
-
candidates:
|
|
1830
|
+
scopes: normalizedWorkflowOverlay.scopes,
|
|
1831
|
+
candidates: normalizedWorkflowOverlay.candidates ?? [],
|
|
1574
1832
|
blockedReasons,
|
|
1575
1833
|
};
|
|
1576
1834
|
}
|
|
@@ -1590,15 +1848,16 @@ export function createDocumentRuntime(
|
|
|
1590
1848
|
}
|
|
1591
1849
|
|
|
1592
1850
|
function getEffectiveWorkflowScopes(overlay: WorkflowOverlay): WorkflowOverlay["scopes"] {
|
|
1593
|
-
const
|
|
1851
|
+
const normalizedOverlay = normalizeWorkflowOverlayForDocument(state.document, overlay);
|
|
1852
|
+
const activeWorkItemId = normalizedOverlay.activeWorkItemId ?? null;
|
|
1594
1853
|
const activeWorkItemScopeIds =
|
|
1595
1854
|
activeWorkItemId === null
|
|
1596
1855
|
? null
|
|
1597
1856
|
: new Set(
|
|
1598
|
-
|
|
1857
|
+
normalizedOverlay.workItems?.find((item) => item.workItemId === activeWorkItemId)?.scopeIds ?? [],
|
|
1599
1858
|
);
|
|
1600
1859
|
|
|
1601
|
-
return
|
|
1860
|
+
return normalizedOverlay.scopes.filter((scope) => {
|
|
1602
1861
|
const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
1603
1862
|
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) {
|
|
1604
1863
|
return false;
|
|
@@ -1844,12 +2103,13 @@ export function createDocumentRuntime(
|
|
|
1844
2103
|
const tCompat = performance.now();
|
|
1845
2104
|
const compat = toPublicCompatibilityReport(createDerivedCompatibility(state));
|
|
1846
2105
|
perfCounters.increment("ctxa.compat.us", Math.round((performance.now() - tCompat) * 1000));
|
|
2106
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
1847
2107
|
|
|
1848
2108
|
const tBuild = performance.now();
|
|
1849
2109
|
const snapshot = createRuntimeContextAnalyticsSnapshot({
|
|
1850
2110
|
query,
|
|
1851
2111
|
renderSnapshot: cachedRenderSnapshot,
|
|
1852
|
-
workflowOverlay,
|
|
2112
|
+
workflowOverlay: normalizedWorkflowOverlay,
|
|
1853
2113
|
workflowScopeSnapshot: wfScope,
|
|
1854
2114
|
interactionGuardSnapshot: wfGuard,
|
|
1855
2115
|
workflowMarkupSnapshot: wfMarkup,
|
|
@@ -1987,6 +2247,7 @@ export function createDocumentRuntime(
|
|
|
1987
2247
|
},
|
|
1988
2248
|
surface,
|
|
1989
2249
|
protectionSnapshot,
|
|
2250
|
+
grabbedObjectId: grabState.objectId,
|
|
1990
2251
|
};
|
|
1991
2252
|
}
|
|
1992
2253
|
|
|
@@ -2022,6 +2283,47 @@ export function createDocumentRuntime(
|
|
|
2022
2283
|
}
|
|
2023
2284
|
}
|
|
2024
2285
|
|
|
2286
|
+
function invalidateDerivedRuntimeCaches(): void {
|
|
2287
|
+
cachedSurface = undefined;
|
|
2288
|
+
cachedCompatibility = undefined;
|
|
2289
|
+
cachedComments = undefined;
|
|
2290
|
+
cachedTrackedChanges = undefined;
|
|
2291
|
+
cachedSuggestions = undefined;
|
|
2292
|
+
cachedReviewWork = undefined;
|
|
2293
|
+
cachedPageLayout = undefined;
|
|
2294
|
+
cachedNavigation = undefined;
|
|
2295
|
+
cachedViewStateSnapshot = undefined;
|
|
2296
|
+
cachedInteractionGuardSnapshot = undefined;
|
|
2297
|
+
cachedWorkflowScopeSnapshot = undefined;
|
|
2298
|
+
cachedNormalizedWorkflowOverlay = undefined;
|
|
2299
|
+
cachedWorkflowMarkupSnapshot = undefined;
|
|
2300
|
+
cachedContextAnalyticsSnapshots.clear();
|
|
2301
|
+
lastEmittedContextAnalyticsSnapshots = undefined;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function hydrateCanonicalDocumentInternally(
|
|
2305
|
+
document: CanonicalDocumentEnvelope,
|
|
2306
|
+
): boolean {
|
|
2307
|
+
if (document === state.document) {
|
|
2308
|
+
return false;
|
|
2309
|
+
}
|
|
2310
|
+
const previousDocument = state.document;
|
|
2311
|
+
state = {
|
|
2312
|
+
...state,
|
|
2313
|
+
document,
|
|
2314
|
+
};
|
|
2315
|
+
if (previousDocument.subParts !== document.subParts) {
|
|
2316
|
+
fontLoader.refresh(collectFontLoaderInput(document));
|
|
2317
|
+
layoutEngine.invalidateMeasurementCache();
|
|
2318
|
+
}
|
|
2319
|
+
invalidateDerivedRuntimeCaches();
|
|
2320
|
+
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2321
|
+
for (const listener of listeners) {
|
|
2322
|
+
listener();
|
|
2323
|
+
}
|
|
2324
|
+
return true;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2025
2327
|
function getCachedViewStateSnapshot(): EditorViewStateSnapshot {
|
|
2026
2328
|
const activeStoryKey = storyTargetKey(activeStory);
|
|
2027
2329
|
const pageLayout = cachedRenderSnapshot.pageLayout;
|
|
@@ -2131,7 +2433,11 @@ export function createDocumentRuntime(
|
|
|
2131
2433
|
const r5ScratchReplayState: typeof state = { ...state };
|
|
2132
2434
|
const r5ScratchReplaySnapshot: typeof cachedRenderSnapshot = { ...cachedRenderSnapshot };
|
|
2133
2435
|
|
|
2134
|
-
|
|
2436
|
+
const runtime: DocumentRuntime & {
|
|
2437
|
+
hydrateCanonicalDocumentInternally(
|
|
2438
|
+
document: CanonicalDocumentEnvelope,
|
|
2439
|
+
): boolean;
|
|
2440
|
+
} = {
|
|
2135
2441
|
subscribe(listener) {
|
|
2136
2442
|
listeners.add(listener);
|
|
2137
2443
|
return () => {
|
|
@@ -2153,6 +2459,12 @@ export function createDocumentRuntime(
|
|
|
2153
2459
|
getSourcePackage() {
|
|
2154
2460
|
return state.sourcePackage;
|
|
2155
2461
|
},
|
|
2462
|
+
getFontTable() {
|
|
2463
|
+
return state.document.fontTable;
|
|
2464
|
+
},
|
|
2465
|
+
getFontEntry(name: string) {
|
|
2466
|
+
return state.document.fontTable?.fonts[name];
|
|
2467
|
+
},
|
|
2156
2468
|
emitBlockedCommand(command, reasons) {
|
|
2157
2469
|
emit({
|
|
2158
2470
|
type: "command_blocked",
|
|
@@ -2480,6 +2792,11 @@ export function createDocumentRuntime(
|
|
|
2480
2792
|
insertFragment(fragment, target) {
|
|
2481
2793
|
// I2 Tier B Slice 1 — dispatch `fragment.insert` against the active story. The
|
|
2482
2794
|
// runtime command handler routes into `applyFragmentInsert` (structure-ops).
|
|
2795
|
+
// v5 B1: auto-bracketed via R.5.a so a host that wants single-undo paste
|
|
2796
|
+
// gets one action. Idempotent on nested brackets: if a caller already
|
|
2797
|
+
// opened `startAction`, this bracket just increments / decrements depth.
|
|
2798
|
+
actionDepth += 1;
|
|
2799
|
+
actionStack.push("insertFragment");
|
|
2483
2800
|
try {
|
|
2484
2801
|
const timestamp = clock();
|
|
2485
2802
|
applyTextCommandInActiveStory(
|
|
@@ -2495,8 +2812,144 @@ export function createDocumentRuntime(
|
|
|
2495
2812
|
);
|
|
2496
2813
|
} catch (error) {
|
|
2497
2814
|
emitError(toRuntimeError(error));
|
|
2815
|
+
} finally {
|
|
2816
|
+
actionDepth -= 1;
|
|
2817
|
+
actionStack.pop();
|
|
2498
2818
|
}
|
|
2499
2819
|
},
|
|
2820
|
+
copy(target) {
|
|
2821
|
+
// I2 Tier B Slice 4b — serialize the selection range to a fragment, store
|
|
2822
|
+
// it in the internal buffer. Does NOT mutate the document.
|
|
2823
|
+
// v5 A1 fix: story-aware extraction — footnote / header / endnote
|
|
2824
|
+
// selections read the right content tree, not main body.
|
|
2825
|
+
try {
|
|
2826
|
+
const selection = target ? createSelectionFromPublicAnchor(target) : state.selection;
|
|
2827
|
+
const fragment = extractSelectionFragment(state.document, selection, activeStory);
|
|
2828
|
+
clipboardBuffer = fragment;
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
emitError(toRuntimeError(error));
|
|
2831
|
+
}
|
|
2832
|
+
},
|
|
2833
|
+
cut(target) {
|
|
2834
|
+
// I2 Tier B Slice 4b — copy into buffer, then delete the range by
|
|
2835
|
+
// replacing it with empty text. (Empty fragment.insert is a deliberate
|
|
2836
|
+
// no-op in the splicer per Slice 1, so we use the text.insert path with
|
|
2837
|
+
// an empty string to delete the selected content.)
|
|
2838
|
+
// v5 B1: auto-bracketed so a host that wants single-undo paste-at-drop
|
|
2839
|
+
// gets one action even if its own bracket isn't open.
|
|
2840
|
+
try {
|
|
2841
|
+
const selection = target ? createSelectionFromPublicAnchor(target) : state.selection;
|
|
2842
|
+
const fragment = extractSelectionFragment(state.document, selection, activeStory);
|
|
2843
|
+
clipboardBuffer = fragment;
|
|
2844
|
+
if (selection.anchor !== selection.head) {
|
|
2845
|
+
actionDepth += 1;
|
|
2846
|
+
actionStack.push("cut");
|
|
2847
|
+
try {
|
|
2848
|
+
const timestamp = clock();
|
|
2849
|
+
applyTextCommandInActiveStory(
|
|
2850
|
+
{
|
|
2851
|
+
type: "text.insert",
|
|
2852
|
+
text: "",
|
|
2853
|
+
origin: createOrigin("api", timestamp),
|
|
2854
|
+
},
|
|
2855
|
+
{
|
|
2856
|
+
selection,
|
|
2857
|
+
blockedCommandName: "cut",
|
|
2858
|
+
},
|
|
2859
|
+
);
|
|
2860
|
+
} finally {
|
|
2861
|
+
actionDepth -= 1;
|
|
2862
|
+
actionStack.pop();
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
emitError(toRuntimeError(error));
|
|
2867
|
+
}
|
|
2868
|
+
},
|
|
2869
|
+
getClipboardBuffer() {
|
|
2870
|
+
return clipboardBuffer;
|
|
2871
|
+
},
|
|
2872
|
+
getClipboardWireFormats() {
|
|
2873
|
+
// v5 close-out — serialize the buffer to WordML + HTML + plain text for
|
|
2874
|
+
// host-owned `navigator.clipboard.write`. Returns null if nothing has
|
|
2875
|
+
// been cut/copied yet. Zero allocation on the null path.
|
|
2876
|
+
if (!clipboardBuffer || clipboardBuffer.blocks.length === 0) return null;
|
|
2877
|
+
const wordml = serializeFragmentToWordML(clipboardBuffer);
|
|
2878
|
+
// Minimal HTML: walk paragraph children emitting <p> wrappers with
|
|
2879
|
+
// text content. Keeps parity with our own parser (round-trips
|
|
2880
|
+
// cleanly) without implementing full CSS export.
|
|
2881
|
+
const htmlParts: string[] = [];
|
|
2882
|
+
const plainParts: string[] = [];
|
|
2883
|
+
for (const block of clipboardBuffer.blocks) {
|
|
2884
|
+
if (block.type !== "paragraph") continue;
|
|
2885
|
+
const runs: string[] = [];
|
|
2886
|
+
const plainRuns: string[] = [];
|
|
2887
|
+
for (const child of block.children) {
|
|
2888
|
+
if (child.type === "text") {
|
|
2889
|
+
const text = (child.text ?? "")
|
|
2890
|
+
.replace(/&/g, "&")
|
|
2891
|
+
.replace(/</g, "<")
|
|
2892
|
+
.replace(/>/g, ">");
|
|
2893
|
+
let wrapped = text;
|
|
2894
|
+
const marks = child.marks ?? [];
|
|
2895
|
+
if (marks.some((m) => m.type === "bold")) wrapped = `<b>${wrapped}</b>`;
|
|
2896
|
+
if (marks.some((m) => m.type === "italic")) wrapped = `<i>${wrapped}</i>`;
|
|
2897
|
+
if (marks.some((m) => m.type === "underline")) wrapped = `<u>${wrapped}</u>`;
|
|
2898
|
+
if (marks.some((m) => m.type === "strikethrough")) wrapped = `<s>${wrapped}</s>`;
|
|
2899
|
+
runs.push(wrapped);
|
|
2900
|
+
plainRuns.push(child.text ?? "");
|
|
2901
|
+
} else if (child.type === "hard_break") {
|
|
2902
|
+
runs.push("<br/>");
|
|
2903
|
+
plainRuns.push("\n");
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
htmlParts.push(`<p>${runs.join("")}</p>`);
|
|
2907
|
+
plainParts.push(plainRuns.join(""));
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
wordml,
|
|
2911
|
+
html: htmlParts.join(""),
|
|
2912
|
+
plainText: plainParts.join("\n"),
|
|
2913
|
+
};
|
|
2914
|
+
},
|
|
2915
|
+
selectObject(objectId) {
|
|
2916
|
+
// R.3 — local grab state mutation. No command dispatch; this is pure UI
|
|
2917
|
+
// state that chrome (Lane 6 P11) reads to paint handles.
|
|
2918
|
+
//
|
|
2919
|
+
// v5 A3: when the grab state actually changes, notify subscribers so
|
|
2920
|
+
// chrome can re-render. We do NOT bump `revisionToken` — grab state is
|
|
2921
|
+
// local UI state, not a document mutation, so collab/autosave/undo
|
|
2922
|
+
// should not observe a change.
|
|
2923
|
+
const next = grabSelectObject(grabState, objectId);
|
|
2924
|
+
if (next === grabState) return;
|
|
2925
|
+
grabState = next;
|
|
2926
|
+
for (const listener of listeners) {
|
|
2927
|
+
listener();
|
|
2928
|
+
}
|
|
2929
|
+
},
|
|
2930
|
+
deselectObject() {
|
|
2931
|
+
const next = grabDeselectObject(grabState);
|
|
2932
|
+
if (next === grabState) return;
|
|
2933
|
+
grabState = next;
|
|
2934
|
+
for (const listener of listeners) {
|
|
2935
|
+
listener();
|
|
2936
|
+
}
|
|
2937
|
+
},
|
|
2938
|
+
getGrabbedObject() {
|
|
2939
|
+
return grabGetGrabbedObject(grabState);
|
|
2940
|
+
},
|
|
2941
|
+
startAction(name) {
|
|
2942
|
+
actionDepth += 1;
|
|
2943
|
+
actionStack.push(name);
|
|
2944
|
+
},
|
|
2945
|
+
endAction() {
|
|
2946
|
+
if (actionDepth === 0) return; // unbalanced — ignore
|
|
2947
|
+
actionDepth -= 1;
|
|
2948
|
+
actionStack.pop();
|
|
2949
|
+
},
|
|
2950
|
+
isInAction() {
|
|
2951
|
+
return actionDepth > 0;
|
|
2952
|
+
},
|
|
2500
2953
|
applyActiveStoryTextCommand(command) {
|
|
2501
2954
|
try {
|
|
2502
2955
|
return applyTextCommandInActiveStory(command);
|
|
@@ -2553,15 +3006,14 @@ export function createDocumentRuntime(
|
|
|
2553
3006
|
anchor,
|
|
2554
3007
|
);
|
|
2555
3008
|
if (rejectionReason !== null) {
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
? "DOCX comments cannot currently anchor mid-run within a paragraph adjacent to a table boundary — snap the range to a paragraph or word boundary and retry."
|
|
2559
|
-
: "DOCX comments must use a non-empty range that stays within a single story and does not cross table or opaque-fragment boundaries.";
|
|
3009
|
+
// Post-O8: only `invalid_comment_anchor` remains as a rejection
|
|
3010
|
+
// reason (empty range / crosses opaque block / out-of-story).
|
|
2560
3011
|
const error: InternalEditorError = {
|
|
2561
3012
|
errorId: createSessionId("comment-anchor", clock()),
|
|
2562
3013
|
code: "validation_failed",
|
|
2563
3014
|
isFatal: false,
|
|
2564
|
-
message
|
|
3015
|
+
message:
|
|
3016
|
+
"DOCX comments must use a non-empty range that stays within a single story and does not cross table or opaque-fragment boundaries.",
|
|
2565
3017
|
source: "runtime",
|
|
2566
3018
|
details: {
|
|
2567
3019
|
reason: rejectionReason,
|
|
@@ -2681,7 +3133,7 @@ export function createDocumentRuntime(
|
|
|
2681
3133
|
});
|
|
2682
3134
|
}
|
|
2683
3135
|
|
|
2684
|
-
const resolved = resolveScope(
|
|
3136
|
+
const resolved = resolveScope(nextDocument, scopeId);
|
|
2685
3137
|
const publicAnchor: EditorAnchorProjection =
|
|
2686
3138
|
resolved && resolved.kind === "range"
|
|
2687
3139
|
? resolved
|
|
@@ -2741,21 +3193,19 @@ export function createDocumentRuntime(
|
|
|
2741
3193
|
};
|
|
2742
3194
|
},
|
|
2743
3195
|
getScope(scopeId) {
|
|
3196
|
+
const normalizedScope =
|
|
3197
|
+
getNormalizedWorkflowOverlay()?.scopes.find((scope) => scope.scopeId === scopeId) ??
|
|
3198
|
+
null;
|
|
3199
|
+
if (normalizedScope) {
|
|
3200
|
+
return normalizedScope;
|
|
3201
|
+
}
|
|
2744
3202
|
const resolved = resolveScope(state.document, scopeId);
|
|
2745
3203
|
if (!resolved) {
|
|
2746
|
-
|
|
2747
|
-
return stored ?? null;
|
|
2748
|
-
}
|
|
2749
|
-
const stored = workflowOverlay?.scopes.find((s) => s.scopeId === scopeId);
|
|
2750
|
-
if (!stored) {
|
|
2751
|
-
return {
|
|
2752
|
-
scopeId,
|
|
2753
|
-
mode: "comment",
|
|
2754
|
-
anchor: resolved,
|
|
2755
|
-
};
|
|
3204
|
+
return null;
|
|
2756
3205
|
}
|
|
2757
3206
|
return {
|
|
2758
|
-
|
|
3207
|
+
scopeId,
|
|
3208
|
+
mode: "comment",
|
|
2759
3209
|
anchor: resolved,
|
|
2760
3210
|
};
|
|
2761
3211
|
},
|
|
@@ -2907,6 +3357,33 @@ export function createDocumentRuntime(
|
|
|
2907
3357
|
getDocumentNavigationSnapshot() {
|
|
2908
3358
|
return getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
2909
3359
|
},
|
|
3360
|
+
getFieldResolver(): FieldResolver {
|
|
3361
|
+
const pageGraph = layoutEngine.getPageGraph({
|
|
3362
|
+
document: state.document,
|
|
3363
|
+
viewState: {
|
|
3364
|
+
activeStory,
|
|
3365
|
+
workspaceMode: viewState.workspaceMode,
|
|
3366
|
+
zoomLevel: viewState.zoomLevel,
|
|
3367
|
+
},
|
|
3368
|
+
});
|
|
3369
|
+
const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
3370
|
+
const bookmarkMap = buildBookmarkNameMap(state.document);
|
|
3371
|
+
const paragraphContexts = collectParagraphContexts(state.document.content.children);
|
|
3372
|
+
const paragraphOffsets = paragraphContexts.map((p) => p.startOffset);
|
|
3373
|
+
return createFieldResolver({
|
|
3374
|
+
pageGraph,
|
|
3375
|
+
activePageIndex: navigation.activePageIndex,
|
|
3376
|
+
bookmarkMap,
|
|
3377
|
+
paragraphOffsets,
|
|
3378
|
+
styles: state.document.styles,
|
|
3379
|
+
contentRoot: state.document.content as unknown as import("./field-resolver.ts").DocumentContainerNode,
|
|
3380
|
+
});
|
|
3381
|
+
},
|
|
3382
|
+
getFootnoteResolver(): FootnoteResolver | undefined {
|
|
3383
|
+
const collection = state.document.subParts?.footnoteCollection;
|
|
3384
|
+
if (!collection) return undefined;
|
|
3385
|
+
return createFootnoteResolver(collection, collectSectionPropertiesInOrder(state.document));
|
|
3386
|
+
},
|
|
2910
3387
|
layout: layoutFacet,
|
|
2911
3388
|
getCurrentLocation() {
|
|
2912
3389
|
const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
@@ -3090,6 +3567,7 @@ export function createDocumentRuntime(
|
|
|
3090
3567
|
},
|
|
3091
3568
|
getSessionState() {
|
|
3092
3569
|
const compatibility = createDerivedCompatibility(state);
|
|
3570
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
3093
3571
|
return editorSessionStateFromPersistedSnapshot(
|
|
3094
3572
|
{
|
|
3095
3573
|
...(createPersistedEditorSnapshot(state, {
|
|
@@ -3098,7 +3576,7 @@ export function createDocumentRuntime(
|
|
|
3098
3576
|
compatibility,
|
|
3099
3577
|
protectionSnapshot,
|
|
3100
3578
|
}) as unknown as PersistedEditorSnapshot),
|
|
3101
|
-
workflowOverlay:
|
|
3579
|
+
workflowOverlay: normalizedWorkflowOverlay ?? undefined,
|
|
3102
3580
|
workflowMetadata: deriveWorkflowMetadataSnapshot(),
|
|
3103
3581
|
},
|
|
3104
3582
|
);
|
|
@@ -3144,7 +3622,7 @@ export function createDocumentRuntime(
|
|
|
3144
3622
|
return { schemaVersion: "host-annotation-overlay/1", data: snap };
|
|
3145
3623
|
}
|
|
3146
3624
|
case "workflowOverlay": {
|
|
3147
|
-
const ov =
|
|
3625
|
+
const ov = getNormalizedWorkflowOverlay();
|
|
3148
3626
|
if (!ov) return null;
|
|
3149
3627
|
return { schemaVersion: "workflow-overlay/1", data: ov };
|
|
3150
3628
|
}
|
|
@@ -3179,10 +3657,11 @@ export function createDocumentRuntime(
|
|
|
3179
3657
|
overlay,
|
|
3180
3658
|
origin: createOrigin("api", clock()),
|
|
3181
3659
|
});
|
|
3660
|
+
const normalizedWorkflowOverlay = getNormalizedWorkflowOverlay();
|
|
3182
3661
|
editorStateChannel.recordMutation("workflowOverlay", {
|
|
3183
3662
|
namespace: "workflowOverlay",
|
|
3184
3663
|
schemaVersion: "workflow-overlay/1",
|
|
3185
|
-
data: overlay,
|
|
3664
|
+
data: normalizedWorkflowOverlay ?? overlay,
|
|
3186
3665
|
});
|
|
3187
3666
|
},
|
|
3188
3667
|
clearWorkflowOverlay() {
|
|
@@ -3192,7 +3671,7 @@ export function createDocumentRuntime(
|
|
|
3192
3671
|
});
|
|
3193
3672
|
},
|
|
3194
3673
|
getWorkflowOverlay() {
|
|
3195
|
-
return
|
|
3674
|
+
return getNormalizedWorkflowOverlay();
|
|
3196
3675
|
},
|
|
3197
3676
|
setSharedWorkflowState(state) {
|
|
3198
3677
|
if (state === sharedWorkflowState) return;
|
|
@@ -3244,6 +3723,40 @@ export function createDocumentRuntime(
|
|
|
3244
3723
|
getWorkflowMetadataSnapshot() {
|
|
3245
3724
|
return deriveWorkflowMetadataSnapshot();
|
|
3246
3725
|
},
|
|
3726
|
+
queryScopes(filter) {
|
|
3727
|
+
return runQueryScopes(
|
|
3728
|
+
{
|
|
3729
|
+
overlay: workflowOverlay,
|
|
3730
|
+
entries: workflowMetadataEntries,
|
|
3731
|
+
document: state.document,
|
|
3732
|
+
},
|
|
3733
|
+
filter,
|
|
3734
|
+
);
|
|
3735
|
+
},
|
|
3736
|
+
findScopesAt(position, options) {
|
|
3737
|
+
const pos =
|
|
3738
|
+
position.kind === "range"
|
|
3739
|
+
? position.from
|
|
3740
|
+
: position.kind === "node"
|
|
3741
|
+
? position.at
|
|
3742
|
+
: null;
|
|
3743
|
+
if (pos === null) return [];
|
|
3744
|
+
const hits = findAllScopesAt(state.document, pos);
|
|
3745
|
+
return projectScopeQueryResults(
|
|
3746
|
+
{ overlay: workflowOverlay, entries: workflowMetadataEntries, document: state.document },
|
|
3747
|
+
hits.map((h) => h.scopeId),
|
|
3748
|
+
options,
|
|
3749
|
+
);
|
|
3750
|
+
},
|
|
3751
|
+
findScopesIntersecting(range, options) {
|
|
3752
|
+
if (range.kind !== "range") return [];
|
|
3753
|
+
const hits = findScopesIntersecting(state.document, range.from, range.to, options?.mode);
|
|
3754
|
+
return projectScopeQueryResults(
|
|
3755
|
+
{ overlay: workflowOverlay, entries: workflowMetadataEntries, document: state.document },
|
|
3756
|
+
hits.map((h) => h.scopeId),
|
|
3757
|
+
options,
|
|
3758
|
+
);
|
|
3759
|
+
},
|
|
3247
3760
|
setHostAnnotationOverlay(overlay) {
|
|
3248
3761
|
this.dispatch({
|
|
3249
3762
|
type: "host-annotation.set-overlay",
|
|
@@ -3341,6 +3854,9 @@ export function createDocumentRuntime(
|
|
|
3341
3854
|
get editorStateChannel() {
|
|
3342
3855
|
return editorStateChannel;
|
|
3343
3856
|
},
|
|
3857
|
+
hydrateCanonicalDocumentInternally(document: CanonicalDocumentEnvelope) {
|
|
3858
|
+
return hydrateCanonicalDocumentInternally(document);
|
|
3859
|
+
},
|
|
3344
3860
|
getPerfCountersSnapshot() {
|
|
3345
3861
|
return perfCounters.snapshot();
|
|
3346
3862
|
},
|
|
@@ -3364,6 +3880,8 @@ export function createDocumentRuntime(
|
|
|
3364
3880
|
},
|
|
3365
3881
|
};
|
|
3366
3882
|
|
|
3883
|
+
return runtime;
|
|
3884
|
+
|
|
3367
3885
|
function applyHistory(direction: "undo" | "redo"): void {
|
|
3368
3886
|
const source = direction === "undo" ? history.past : history.future;
|
|
3369
3887
|
const target = source.pop();
|
|
@@ -3486,6 +4004,18 @@ export function createDocumentRuntime(
|
|
|
3486
4004
|
next: EditorState,
|
|
3487
4005
|
transaction: EditorTransaction,
|
|
3488
4006
|
): void {
|
|
4007
|
+
// V6c — heading-fingerprint comparison schedules an automatic TOC rebuild
|
|
4008
|
+
// when paragraph styleIds or heading text drift. Short-circuit on
|
|
4009
|
+
// document-identity equality (selection-only commits) to skip the walk.
|
|
4010
|
+
if (previous.document !== next.document) {
|
|
4011
|
+
const nextFingerprint = computeHeadingFingerprint(next.document);
|
|
4012
|
+
if (nextFingerprint !== lastHeadingFingerprint) {
|
|
4013
|
+
const trigger = deriveTocTrigger(lastHeadingFingerprint, nextFingerprint);
|
|
4014
|
+
lastHeadingFingerprint = nextFingerprint;
|
|
4015
|
+
scheduleTocAutoRefresh(trigger);
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3489
4019
|
const emittedSuggestionIds = new Set<string>();
|
|
3490
4020
|
if (previous.isDirty !== next.isDirty) {
|
|
3491
4021
|
emit({
|
|
@@ -3982,6 +4512,80 @@ export function createDocumentRuntime(
|
|
|
3982
4512
|
});
|
|
3983
4513
|
}
|
|
3984
4514
|
|
|
4515
|
+
// V6c — TOC auto-refresh scheduler. Mirrors scheduleContextAnalyticsEmit's
|
|
4516
|
+
// microtask-coalesce shape. Bursts of heading edits within one synchronous
|
|
4517
|
+
// call stack collapse to a single rebuild + a single toc_auto_refreshed
|
|
4518
|
+
// event. Trigger flags accumulate across coalesced edits.
|
|
4519
|
+
function scheduleTocAutoRefresh(trigger: TocRefreshTrigger): void {
|
|
4520
|
+
if (pendingTocTrigger) {
|
|
4521
|
+
pendingTocTrigger = {
|
|
4522
|
+
headingContentChanged:
|
|
4523
|
+
pendingTocTrigger.headingContentChanged || trigger.headingContentChanged,
|
|
4524
|
+
headingStructureChanged:
|
|
4525
|
+
pendingTocTrigger.headingStructureChanged || trigger.headingStructureChanged,
|
|
4526
|
+
};
|
|
4527
|
+
} else {
|
|
4528
|
+
pendingTocTrigger = trigger;
|
|
4529
|
+
}
|
|
4530
|
+
if (tocAutoRefreshScheduled) {
|
|
4531
|
+
perfCounters.increment("toc.autoRefresh.coalesced");
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
tocAutoRefreshScheduled = true;
|
|
4535
|
+
queueMicrotask(() => {
|
|
4536
|
+
tocAutoRefreshScheduled = false;
|
|
4537
|
+
const flushedTrigger = pendingTocTrigger;
|
|
4538
|
+
pendingTocTrigger = null;
|
|
4539
|
+
if (!flushedTrigger) return;
|
|
4540
|
+
const t = performance.now();
|
|
4541
|
+
const refreshed = refreshDocumentTableOfContents(
|
|
4542
|
+
state.document,
|
|
4543
|
+
state.selection.head,
|
|
4544
|
+
activeStory,
|
|
4545
|
+
undefined,
|
|
4546
|
+
(pageIndex: number) => layoutFacet.getDisplayPageNumber(pageIndex),
|
|
4547
|
+
);
|
|
4548
|
+
perfCounters.increment("toc.autoRefresh.us", Math.round((performance.now() - t) * 1000));
|
|
4549
|
+
if (!refreshed.changed) {
|
|
4550
|
+
perfCounters.increment("toc.autoRefresh.noopRebuild");
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
// Replay through executeEditorCommand so history, mapping, and the
|
|
4554
|
+
// downstream notify() stay consistent. The replay's notify() will
|
|
4555
|
+
// recompute the heading fingerprint and find no change (TOC field
|
|
4556
|
+
// text is not heading text), so this does not loop.
|
|
4557
|
+
const ctx = {
|
|
4558
|
+
timestamp: clock(),
|
|
4559
|
+
documentMode: getEffectiveDocumentMode(state.selection),
|
|
4560
|
+
defaultAuthorId: defaultAuthorId ?? undefined,
|
|
4561
|
+
renderSnapshot: cachedRenderSnapshot,
|
|
4562
|
+
} as const;
|
|
4563
|
+
try {
|
|
4564
|
+
const transaction = executeEditorCommand(
|
|
4565
|
+
state,
|
|
4566
|
+
{
|
|
4567
|
+
type: "document.replace",
|
|
4568
|
+
document: refreshed.document,
|
|
4569
|
+
mapping: createEmptyMapping(),
|
|
4570
|
+
protectionSelection: refreshed.protectionSelection,
|
|
4571
|
+
origin: createOrigin("api", clock()),
|
|
4572
|
+
},
|
|
4573
|
+
ctx,
|
|
4574
|
+
);
|
|
4575
|
+
commit(transaction);
|
|
4576
|
+
} catch (error) {
|
|
4577
|
+
emitError(toRuntimeError(error));
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
emit({
|
|
4581
|
+
type: "toc_auto_refreshed",
|
|
4582
|
+
documentId: state.documentId,
|
|
4583
|
+
entryCount: refreshed.result.entryCount,
|
|
4584
|
+
trigger: flushedTrigger,
|
|
4585
|
+
});
|
|
4586
|
+
});
|
|
4587
|
+
}
|
|
4588
|
+
|
|
3985
4589
|
function emit(event: DocumentRuntimeEvent): void {
|
|
3986
4590
|
perfCounters.increment(`emit.${event.type}.calls`);
|
|
3987
4591
|
const t0 = performance.now();
|
|
@@ -3998,6 +4602,7 @@ export function createDocumentRuntime(
|
|
|
3998
4602
|
switch (command.type) {
|
|
3999
4603
|
case "workflow.set-overlay": {
|
|
4000
4604
|
workflowOverlay = structuredClone(command.overlay);
|
|
4605
|
+
cachedNormalizedWorkflowOverlay = undefined;
|
|
4001
4606
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4002
4607
|
const snapshot = deriveWorkflowScopeSnapshot()!;
|
|
4003
4608
|
emit({
|
|
@@ -4016,6 +4621,7 @@ export function createDocumentRuntime(
|
|
|
4016
4621
|
}
|
|
4017
4622
|
case "workflow.clear-overlay": {
|
|
4018
4623
|
workflowOverlay = null;
|
|
4624
|
+
cachedNormalizedWorkflowOverlay = undefined;
|
|
4019
4625
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
4020
4626
|
emit({
|
|
4021
4627
|
type: "workflow_active_work_item_changed",
|
|
@@ -4415,6 +5021,61 @@ function createSelectionFromPublicAnchor(
|
|
|
4415
5021
|
}
|
|
4416
5022
|
}
|
|
4417
5023
|
|
|
5024
|
+
/**
|
|
5025
|
+
* I2 Tier B Slice 4b — extract the selection range from a document as a
|
|
5026
|
+
* `CanonicalDocumentFragment`. The fragment preserves text + marks +
|
|
5027
|
+
* hard-breaks + paragraph-breaks + tabs that fall inside the range.
|
|
5028
|
+
*
|
|
5029
|
+
* Uses the linear story layer (`parseTextStory` + `logicalPositionToUnitIndex`
|
|
5030
|
+
* + `serializeTextStory`) so the result is a properly-structured block list
|
|
5031
|
+
* that `insertFragment` can splice back in. Collapsed ranges return an empty
|
|
5032
|
+
* fragment.
|
|
5033
|
+
*
|
|
5034
|
+
* Story-aware (v5 A1 fix): `activeStory` selects which content tree the
|
|
5035
|
+
* selection offsets apply to. `getStoryBlocks` is the same helper
|
|
5036
|
+
* `applyTextCommandInActiveStory` uses, so cut/copy in footnote / header /
|
|
5037
|
+
* endnote stories extract from the right content rather than from main body.
|
|
5038
|
+
*
|
|
5039
|
+
* Complex content (tables, opaque blocks) inside the range serializes per
|
|
5040
|
+
* the underlying story layer's semantics — for a richer table-aware
|
|
5041
|
+
* clipboard, callers should use `serializeFragmentToWordML` (Slice 4a) on
|
|
5042
|
+
* the result of this extraction.
|
|
5043
|
+
*/
|
|
5044
|
+
function extractSelectionFragment(
|
|
5045
|
+
document: CanonicalDocumentEnvelope,
|
|
5046
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
5047
|
+
activeStory: EditorStoryTarget,
|
|
5048
|
+
): CanonicalDocumentFragment {
|
|
5049
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
5050
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
5051
|
+
if (from === to) {
|
|
5052
|
+
return { blocks: [] };
|
|
5053
|
+
}
|
|
5054
|
+
// Resolve the content node for the active story. For main body this is the
|
|
5055
|
+
// document root; for secondary stories (header / footer / footnote / endnote)
|
|
5056
|
+
// we wrap the story's blocks in a doc root so parseTextStory produces
|
|
5057
|
+
// offsets in the same frame as the selection.
|
|
5058
|
+
const storyBlocks = getStoryBlocks(document, activeStory);
|
|
5059
|
+
const storyContent =
|
|
5060
|
+
activeStory.kind === "main"
|
|
5061
|
+
? document.content
|
|
5062
|
+
: { type: "doc" as const, children: [...storyBlocks] };
|
|
5063
|
+
const story = parseTextStory(storyContent);
|
|
5064
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, from, "before");
|
|
5065
|
+
const unitTo = logicalPositionToUnitIndex(story.units, to, "after");
|
|
5066
|
+
const slicedUnits = story.units.slice(unitFrom, unitTo);
|
|
5067
|
+
if (slicedUnits.length === 0) {
|
|
5068
|
+
return { blocks: [] };
|
|
5069
|
+
}
|
|
5070
|
+
const slicedStory = {
|
|
5071
|
+
firstParagraph: story.firstParagraph,
|
|
5072
|
+
units: slicedUnits,
|
|
5073
|
+
size: 0,
|
|
5074
|
+
};
|
|
5075
|
+
const root = serializeTextStory(slicedStory);
|
|
5076
|
+
return { blocks: root.children };
|
|
5077
|
+
}
|
|
5078
|
+
|
|
4418
5079
|
/**
|
|
4419
5080
|
* Collect the stable ids of comment threads whose entry differs
|
|
4420
5081
|
* (present in one side but not the other, OR present in both but
|
|
@@ -4960,6 +5621,31 @@ function extractFieldDisplayText(field: FieldNode): string {
|
|
|
4960
5621
|
return flattenInlineDisplayText(field.children);
|
|
4961
5622
|
}
|
|
4962
5623
|
|
|
5624
|
+
// V6c — heading fingerprint over (styleId, visible text) pairs in document
|
|
5625
|
+
// order. Walks top-level paragraphs only; matches what
|
|
5626
|
+
// buildHeadingOutline()/createDocumentNavigationSnapshot read.
|
|
5627
|
+
function computeHeadingFingerprint(
|
|
5628
|
+
document: CanonicalDocumentEnvelope,
|
|
5629
|
+
): string {
|
|
5630
|
+
const parts: string[] = [];
|
|
5631
|
+
for (const block of document.content.children) {
|
|
5632
|
+
if (block.type !== "paragraph") continue;
|
|
5633
|
+
const styleId = block.styleId ?? "";
|
|
5634
|
+
if (!styleId.toLowerCase().startsWith("heading")) continue;
|
|
5635
|
+
parts.push(`${styleId}\u0001${flattenInlineDisplayText(block.children)}`);
|
|
5636
|
+
}
|
|
5637
|
+
return parts.join("\u0002");
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
function deriveTocTrigger(prev: string, next: string): TocRefreshTrigger {
|
|
5641
|
+
const prevCount = prev === "" ? 0 : prev.split("\u0002").length;
|
|
5642
|
+
const nextCount = next === "" ? 0 : next.split("\u0002").length;
|
|
5643
|
+
return {
|
|
5644
|
+
headingStructureChanged: prevCount !== nextCount,
|
|
5645
|
+
headingContentChanged: prevCount === nextCount,
|
|
5646
|
+
};
|
|
5647
|
+
}
|
|
5648
|
+
|
|
4963
5649
|
function flattenInlineDisplayText(children: readonly InlineNode[]): string {
|
|
4964
5650
|
return children
|
|
4965
5651
|
.map((child) => {
|
|
@@ -5368,6 +6054,17 @@ function refreshBlocksWithCursor(
|
|
|
5368
6054
|
return { blocks: nextBlocks, cursor, previousParagraph };
|
|
5369
6055
|
}
|
|
5370
6056
|
|
|
6057
|
+
/**
|
|
6058
|
+
* Get a ThemeColorResolver for the given document, or undefined if the
|
|
6059
|
+
* document has no theme part. Use this rather than reaching into subParts.canonicalTheme.
|
|
6060
|
+
*/
|
|
6061
|
+
export function getThemeColorResolver(
|
|
6062
|
+
doc: CanonicalDocument,
|
|
6063
|
+
): ThemeColorResolver | undefined {
|
|
6064
|
+
const ct = doc.subParts?.canonicalTheme;
|
|
6065
|
+
return ct ? new ThemeColorResolver(ct) : undefined;
|
|
6066
|
+
}
|
|
6067
|
+
|
|
5371
6068
|
function refreshInlineNodesWithCursor(
|
|
5372
6069
|
nodes: readonly InlineNode[],
|
|
5373
6070
|
visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
|
|
@@ -5701,6 +6398,28 @@ function collectParagraphContexts(blocks: readonly BlockNode[]): ParagraphContex
|
|
|
5701
6398
|
return paragraphs;
|
|
5702
6399
|
}
|
|
5703
6400
|
|
|
6401
|
+
/**
|
|
6402
|
+
* Collect every section's `SectionProperties` in document order. Each
|
|
6403
|
+
* `section_break` block contributes its own section; the final section
|
|
6404
|
+
* (the implicit one after the last break) is read from
|
|
6405
|
+
* `subParts.finalSectionProperties`. Used by `getFootnoteResolver()` so
|
|
6406
|
+
* per-section `footnotePr`/`endnotePr` can be resolved by section index.
|
|
6407
|
+
*/
|
|
6408
|
+
function collectSectionPropertiesInOrder(
|
|
6409
|
+
document: CanonicalDocumentEnvelope,
|
|
6410
|
+
): SectionProperties[] {
|
|
6411
|
+
const sections: SectionProperties[] = [];
|
|
6412
|
+
for (const block of document.content.children) {
|
|
6413
|
+
if (block.type === "section_break" && block.sectionProperties) {
|
|
6414
|
+
sections.push(block.sectionProperties);
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
if (document.subParts?.finalSectionProperties) {
|
|
6418
|
+
sections.push(document.subParts.finalSectionProperties);
|
|
6419
|
+
}
|
|
6420
|
+
return sections;
|
|
6421
|
+
}
|
|
6422
|
+
|
|
5704
6423
|
function collectParagraphContextsFromBlocks(
|
|
5705
6424
|
blocks: readonly BlockNode[],
|
|
5706
6425
|
paragraphs: ParagraphContext[],
|