@beyondwork/docx-react-component 1.0.83 → 1.0.85

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/api/internal/build-ref-projections.ts +3 -0
  3. package/src/api/public-types.ts +86 -4
  4. package/src/api/v3/_runtime-handle.ts +15 -0
  5. package/src/api/v3/runtime/content.ts +148 -1
  6. package/src/api/v3/runtime/formatting.ts +41 -0
  7. package/src/api/v3/runtime/review.ts +98 -0
  8. package/src/api/v3/runtime/workflow.ts +154 -6
  9. package/src/core/commands/index.ts +81 -25
  10. package/src/core/state/editor-state.ts +15 -0
  11. package/src/io/export/serialize-main-document.ts +72 -6
  12. package/src/io/ooxml/header-footer-reference.ts +38 -0
  13. package/src/io/ooxml/parse-headers-footers.ts +11 -23
  14. package/src/io/ooxml/parse-main-document.ts +7 -10
  15. package/src/io/ooxml/workflow-payload-validator.ts +24 -0
  16. package/src/io/ooxml/workflow-payload.ts +12 -0
  17. package/src/model/canonical-document.ts +9 -0
  18. package/src/model/review/comment-types.ts +2 -0
  19. package/src/runtime/document-runtime.ts +718 -68
  20. package/src/runtime/formatting/field/resolver.ts +73 -8
  21. package/src/runtime/layout/layout-engine-version.ts +31 -12
  22. package/src/runtime/layout/paginated-layout-engine.ts +18 -11
  23. package/src/runtime/layout/public-facet.ts +119 -16
  24. package/src/runtime/layout/resolve-page-fields.ts +68 -6
  25. package/src/runtime/layout/resolve-page-previews.ts +1 -1
  26. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  27. package/src/runtime/scopes/action-validation.ts +54 -45
  28. package/src/runtime/scopes/workflow-overlap.ts +41 -9
  29. package/src/runtime/suggestions-snapshot.ts +24 -0
  30. package/src/runtime/surface-projection.ts +59 -2
  31. package/src/runtime/workflow/coordinator.ts +66 -14
  32. package/src/runtime/workflow/scope-writer.ts +83 -5
  33. package/src/shell/ref-commands.ts +3 -354
  34. package/src/shell/session-bootstrap.ts +10 -0
  35. package/src/ui/WordReviewEditor.tsx +99 -9
  36. package/src/ui/editor-command-bag.ts +3 -1
  37. package/src/ui/headless/revision-decoration-model.ts +13 -0
  38. package/src/ui/headless/selection-tool-types.ts +2 -0
  39. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
  40. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
  41. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
  42. package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
  43. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
  44. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
  45. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
  46. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
  47. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
  48. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
  49. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
  50. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
  51. package/src/ui-tailwind/review-workspace/types.ts +3 -2
  52. package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
  53. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
  54. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
  55. package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
@@ -94,6 +94,7 @@ import type {
94
94
  ScopeQueryFilter,
95
95
  ScopeQueryResult,
96
96
  ScopeVisibility,
97
+ WorkflowScopeGuardPolicy,
97
98
  ScopeChromeVisibilityState,
98
99
  SearchOptions,
99
100
  TextStyleFilter,
@@ -187,7 +188,11 @@ import {
187
188
  insertScopeMarkers,
188
189
  removeScopeMarkers,
189
190
  } from "../core/commands/add-scope.ts";
190
- import { applyFormattingOperationToDocument } from "../core/commands/formatting-commands.ts";
191
+ import {
192
+ applyFormattingOperationToDocument,
193
+ type FormattingOperation,
194
+ } from "../core/commands/formatting-commands.ts";
195
+ import { resolveActiveParagraphIndex } from "../core/commands/paragraph-layout-commands.ts";
191
196
  import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
192
197
  import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
193
198
  import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
@@ -206,7 +211,6 @@ import {
206
211
  } from "./workflow/rail/compose.ts";
207
212
  import {
208
213
  createDocumentNavigationSnapshot,
209
- findPageForOffset,
210
214
  } from "./document-navigation.ts";
211
215
  import {
212
216
  createDocxFontLoader,
@@ -325,7 +329,8 @@ import {
325
329
  import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
326
330
  import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
327
331
  import { mapLocalSelectionOnRemoteReplay } from "./collab/map-local-selection-on-remote-replay.ts";
328
- import { formatPageNumber } from "./formatting/field/page-number-format.ts";
332
+ import { resolvePageFieldDisplayText } from "./layout/resolve-page-fields.ts";
333
+ import type { RuntimePageGraph, RuntimePageNode } from "./layout/page-graph.ts";
329
334
 
330
335
  /** Internal extension of ExportDocxOptions that threads the collected
331
336
  * editorState payload from the runtime to the docx serializer. */
@@ -370,6 +375,7 @@ export interface DocumentRuntime {
370
375
  name: string,
371
376
  ): NonNullable<CanonicalDocumentEnvelope["fontTable"]>["fonts"][string] | undefined;
372
377
  replaceText(text: string, target?: EditorAnchorProjection, formatting?: TextFormattingDirective): void;
378
+ applyFormattingOperation(operation: FormattingOperation): void;
373
379
  insertFragment(fragment: CanonicalDocumentFragment, target?: EditorAnchorProjection): void;
374
380
  /**
375
381
  * I2 Tier B Slice 4b — serialize the selection range to a
@@ -515,6 +521,16 @@ export interface DocumentRuntime {
515
521
  body: string,
516
522
  authorId?: string,
517
523
  ): AddCommentReplyResult | null;
524
+ getCommentThreadForChange(changeId: string): CommentSidebarThreadSnapshot | null;
525
+ ensureCommentThreadForChange(
526
+ changeId: string,
527
+ authorId?: string,
528
+ ): AddCommentResult | null;
529
+ addReplyToChange(
530
+ changeId: string,
531
+ body: string,
532
+ authorId?: string,
533
+ ): AddCommentReplyResult | null;
518
534
  editCommentBody(commentId: string, body: string): void;
519
535
  addScope(params: AddScopeParams): AddScopeResult;
520
536
  getScope(scopeId: string): WorkflowScope | null;
@@ -588,6 +604,13 @@ export interface DocumentRuntime {
588
604
  setScopeVisibility(scopeId: string, visibility: ScopeVisibility): void;
589
605
  /** §C8 — Get a scope's current visibility (absent = "visible"). */
590
606
  getScopeVisibility(scopeId: string): ScopeVisibility;
607
+ /** Scope edit-enforcement posture (collab-replicated). */
608
+ setScopeGuardPolicy(
609
+ scopeId: string,
610
+ guardPolicy: WorkflowScopeGuardPolicy,
611
+ ): void;
612
+ /** Effective scope edit-enforcement posture (unknown = "none"). */
613
+ getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy;
591
614
  /** §C7 — Set local chrome visibility state (never collab-replicated). */
592
615
  setScopeChromeVisibility(state: ScopeChromeVisibilityState): void;
593
616
  /** §C7 — Get local chrome visibility state (default: { mode: "all" }). */
@@ -3071,6 +3094,167 @@ export function createDocumentRuntime(
3071
3094
  emitError(toRuntimeError(error));
3072
3095
  }
3073
3096
  },
3097
+ applyFormattingOperation(operation) {
3098
+ try {
3099
+ const commandName = getFormattingOperationCommandName(operation);
3100
+ const snapshot = cachedRenderSnapshot;
3101
+ if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
3102
+ return;
3103
+ }
3104
+
3105
+ const blockedReasons = workflowCoordinator.evaluateBlockedReasons(
3106
+ state.selection,
3107
+ commandName,
3108
+ );
3109
+ if (blockedReasons.length > 0) {
3110
+ this.emitBlockedCommand(commandName, blockedReasons);
3111
+ return;
3112
+ }
3113
+
3114
+ const persistedDocument = state.document;
3115
+ const localDocument =
3116
+ activeStory.kind === "main"
3117
+ ? persistedDocument
3118
+ : {
3119
+ ...persistedDocument,
3120
+ content: {
3121
+ type: "doc" as const,
3122
+ children: [...getStoryBlocks(persistedDocument, activeStory)],
3123
+ },
3124
+ };
3125
+ const localSnapshot: RuntimeRenderSnapshot =
3126
+ activeStory.kind === "main"
3127
+ ? snapshot
3128
+ : {
3129
+ ...snapshot,
3130
+ activeStory: MAIN_STORY_TARGET,
3131
+ selection: stripStoryTarget(snapshot.selection),
3132
+ };
3133
+
3134
+ const suggesting =
3135
+ workflowCoordinator.getEffectiveDocumentMode(state.selection) === "suggesting";
3136
+ const timestamp = clock();
3137
+
3138
+ if (suggesting) {
3139
+ if (activeStory.kind !== "main") {
3140
+ this.emitBlockedCommand(commandName, [{
3141
+ code: "suggesting_unsupported",
3142
+ message: `"${commandName}" is not supported in suggesting mode for this story.`,
3143
+ }]);
3144
+ return;
3145
+ }
3146
+
3147
+ if (
3148
+ operation.type === "set-alignment" ||
3149
+ operation.type === "indent" ||
3150
+ operation.type === "outdent"
3151
+ ) {
3152
+ const paragraphContext = resolveActiveParagraphContext(localSnapshot);
3153
+ if (!paragraphContext) {
3154
+ return;
3155
+ }
3156
+ const beforeXml = buildParagraphPropertyBeforeXml(paragraphContext.paragraph);
3157
+ const result = applyFormattingOperationToDocument(
3158
+ localDocument,
3159
+ localSnapshot,
3160
+ operation,
3161
+ );
3162
+ if (!result.changed) {
3163
+ return;
3164
+ }
3165
+ const nextDocument = appendPropertyChangeSuggestion(
3166
+ result.document,
3167
+ {
3168
+ from: paragraphContext.paragraph.from,
3169
+ to: paragraphContext.paragraph.to,
3170
+ },
3171
+ {
3172
+ originalRevisionType: "pPrChange",
3173
+ xmlTag: "pPrChange",
3174
+ beforeXml,
3175
+ semanticKind: "paragraph-property-change",
3176
+ storyTarget: activeStory,
3177
+ authorId: defaultAuthorId ?? undefined,
3178
+ },
3179
+ timestamp,
3180
+ );
3181
+ this.dispatch({
3182
+ type: "document.replace",
3183
+ document: { ...nextDocument, updatedAt: timestamp },
3184
+ mapping: createEmptyMapping(),
3185
+ selection: toInternalSelectionSnapshot(result.selection),
3186
+ origin: createOrigin("api", timestamp),
3187
+ });
3188
+ return;
3189
+ }
3190
+
3191
+ const segment = findSingleSelectedTextSegment(localSnapshot);
3192
+ if (!segment) {
3193
+ this.emitBlockedCommand(commandName, [{
3194
+ code: "suggesting_unsupported",
3195
+ message: `"${commandName}" requires one bounded text segment in suggesting mode.`,
3196
+ }]);
3197
+ return;
3198
+ }
3199
+ const beforeXml = buildRunPropertyBeforeXml(segment);
3200
+ const result = applyFormattingOperationToDocument(
3201
+ localDocument,
3202
+ localSnapshot,
3203
+ operation,
3204
+ );
3205
+ if (!result.changed) {
3206
+ return;
3207
+ }
3208
+ const nextDocument = appendPropertyChangeSuggestion(
3209
+ result.document,
3210
+ { from: segment.from, to: segment.to },
3211
+ {
3212
+ originalRevisionType: "rPrChange",
3213
+ xmlTag: "rPrChange",
3214
+ beforeXml,
3215
+ semanticKind: "formatting-change",
3216
+ storyTarget: activeStory,
3217
+ authorId: defaultAuthorId ?? undefined,
3218
+ },
3219
+ timestamp,
3220
+ );
3221
+ this.dispatch({
3222
+ type: "document.replace",
3223
+ document: { ...nextDocument, updatedAt: timestamp },
3224
+ mapping: createEmptyMapping(),
3225
+ selection: toInternalSelectionSnapshot(result.selection),
3226
+ origin: createOrigin("api", timestamp),
3227
+ });
3228
+ return;
3229
+ }
3230
+
3231
+ const result = applyFormattingOperationToDocument(
3232
+ localDocument,
3233
+ localSnapshot,
3234
+ operation,
3235
+ );
3236
+ if (!result.changed) {
3237
+ return;
3238
+ }
3239
+ const nextDocument =
3240
+ activeStory.kind === "main"
3241
+ ? result.document
3242
+ : replaceStoryBlocks(
3243
+ persistedDocument,
3244
+ activeStory,
3245
+ result.document.content.children,
3246
+ );
3247
+ this.dispatch({
3248
+ type: "document.replace",
3249
+ document: { ...nextDocument, updatedAt: timestamp },
3250
+ mapping: createEmptyMapping(),
3251
+ selection: toInternalSelectionSnapshot(result.selection),
3252
+ origin: createOrigin("api", timestamp),
3253
+ });
3254
+ } catch (error) {
3255
+ emitError(toRuntimeError(error));
3256
+ }
3257
+ },
3074
3258
  applyScopeReplacement(plan: RuntimeOperationPlan) {
3075
3259
  // Layer-08 Slice 5. Each step lowers to an existing command; the
3076
3260
  // apply pipeline (`src/runtime/scopes/replacement/apply.ts`) owns
@@ -3095,6 +3279,7 @@ export function createDocumentRuntime(
3095
3279
  {
3096
3280
  selection: createSelectionFromPublicAnchor(anchor),
3097
3281
  blockedCommandName: "applyScopeReplacement",
3282
+ skipWorkflowGuard: true,
3098
3283
  },
3099
3284
  );
3100
3285
  } catch (error) {
@@ -3129,6 +3314,7 @@ export function createDocumentRuntime(
3129
3314
  {
3130
3315
  selection: createSelectionFromPublicAnchor(anchor),
3131
3316
  blockedCommandName: "applyScopeReplacement",
3317
+ skipWorkflowGuard: true,
3132
3318
  },
3133
3319
  );
3134
3320
  } catch (error) {
@@ -3168,6 +3354,7 @@ export function createDocumentRuntime(
3168
3354
  selection: createSelectionFromPublicAnchor(anchor),
3169
3355
  blockedCommandName: "applyScopeReplacement",
3170
3356
  documentModeOverride: "suggesting",
3357
+ skipWorkflowGuard: true,
3171
3358
  },
3172
3359
  );
3173
3360
  } catch (error) {
@@ -3200,6 +3387,7 @@ export function createDocumentRuntime(
3200
3387
  selection: createSelectionFromPublicAnchor(anchor),
3201
3388
  blockedCommandName: "applyScopeReplacement",
3202
3389
  documentModeOverride: "suggesting",
3390
+ skipWorkflowGuard: true,
3203
3391
  },
3204
3392
  );
3205
3393
  } catch (error) {
@@ -3522,6 +3710,137 @@ export function createDocumentRuntime(
3522
3710
  const last = entries[entries.length - 1]!;
3523
3711
  return { commentId, entryId: last.entryId };
3524
3712
  },
3713
+ getCommentThreadForChange(changeId) {
3714
+ return (
3715
+ this.getRenderSnapshot().comments.threads.find(
3716
+ (thread) => thread.linkedRevisionId === changeId,
3717
+ ) ?? null
3718
+ );
3719
+ },
3720
+ ensureCommentThreadForChange(changeId, authorId) {
3721
+ const existing = this.getCommentThreadForChange(changeId);
3722
+ if (existing) {
3723
+ this.openComment(existing.commentId);
3724
+ return { commentId: existing.commentId, anchor: existing.anchor };
3725
+ }
3726
+
3727
+ if (viewState.documentMode === "viewing") {
3728
+ return null;
3729
+ }
3730
+ const revision = state.document.review.revisions[changeId];
3731
+ if (!revision || revision.anchor.kind !== "range") {
3732
+ return null;
3733
+ }
3734
+ const rejectionReason = commentAnchorRejectionReason(
3735
+ cachedRenderSnapshot.surface,
3736
+ revision.anchor,
3737
+ );
3738
+ if (rejectionReason !== null) {
3739
+ return null;
3740
+ }
3741
+
3742
+ const commentId = createEntityId("comment", state.document.review.comments, clock());
3743
+ const createdBy = authorId ?? defaultAuthorId ?? "unknown";
3744
+ const createdAt = clock();
3745
+ const entryId = `${commentId}-entry-1`;
3746
+ const comment: CommentThreadRecord = {
3747
+ commentId,
3748
+ anchor: revision.anchor,
3749
+ createdAt,
3750
+ createdBy,
3751
+ authorId: createdBy,
3752
+ body: "",
3753
+ entries: [
3754
+ {
3755
+ entryId,
3756
+ authorId: createdBy,
3757
+ body: "",
3758
+ createdAt,
3759
+ },
3760
+ ],
3761
+ status: "open",
3762
+ warningIds: [],
3763
+ isResolved: false,
3764
+ metadata: {
3765
+ source: "runtime",
3766
+ linkedRevisionId: changeId,
3767
+ },
3768
+ };
3769
+
3770
+ this.dispatch({
3771
+ type: "comment.add",
3772
+ comment,
3773
+ selection: createSelectionFromPublicAnchor(
3774
+ toPublicAnchorProjection(revision.anchor),
3775
+ ),
3776
+ origin: createOrigin("api", clock()),
3777
+ });
3778
+
3779
+ return {
3780
+ commentId,
3781
+ anchor: toPublicAnchorProjection(revision.anchor),
3782
+ };
3783
+ },
3784
+ addReplyToChange(changeId, body, authorId) {
3785
+ const existing = this.getCommentThreadForChange(changeId);
3786
+ if (existing) {
3787
+ return this.addCommentReply(existing.commentId, body, authorId);
3788
+ }
3789
+
3790
+ if (viewState.documentMode === "viewing") {
3791
+ return null;
3792
+ }
3793
+ const revision = state.document.review.revisions[changeId];
3794
+ if (!revision || revision.anchor.kind !== "range") {
3795
+ return null;
3796
+ }
3797
+ const rejectionReason = commentAnchorRejectionReason(
3798
+ cachedRenderSnapshot.surface,
3799
+ revision.anchor,
3800
+ );
3801
+ if (rejectionReason !== null) {
3802
+ return null;
3803
+ }
3804
+
3805
+ const commentId = createEntityId("comment", state.document.review.comments, clock());
3806
+ const createdBy = authorId ?? defaultAuthorId ?? "unknown";
3807
+ const createdAt = clock();
3808
+ const entryId = `${commentId}-entry-1`;
3809
+ const comment: CommentThreadRecord = {
3810
+ commentId,
3811
+ anchor: revision.anchor,
3812
+ createdAt,
3813
+ createdBy,
3814
+ authorId: createdBy,
3815
+ body,
3816
+ entries: [
3817
+ {
3818
+ entryId,
3819
+ authorId: createdBy,
3820
+ body,
3821
+ createdAt,
3822
+ },
3823
+ ],
3824
+ status: "open",
3825
+ warningIds: [],
3826
+ isResolved: false,
3827
+ metadata: {
3828
+ source: "runtime",
3829
+ linkedRevisionId: changeId,
3830
+ },
3831
+ };
3832
+
3833
+ this.dispatch({
3834
+ type: "comment.add",
3835
+ comment,
3836
+ selection: createSelectionFromPublicAnchor(
3837
+ toPublicAnchorProjection(revision.anchor),
3838
+ ),
3839
+ origin: createOrigin("api", clock()),
3840
+ });
3841
+
3842
+ return { commentId, entryId };
3843
+ },
3525
3844
  editCommentBody(commentId, body) {
3526
3845
  this.dispatch({
3527
3846
  type: "comment.edit-body",
@@ -3580,6 +3899,12 @@ export function createDocumentRuntime(
3580
3899
  getScopeVisibility(scopeId) {
3581
3900
  return workflowCoordinator.getScopeVisibility(scopeId);
3582
3901
  },
3902
+ setScopeGuardPolicy(scopeId, guardPolicy) {
3903
+ workflowCoordinator.setScopeGuardPolicy(scopeId, guardPolicy);
3904
+ },
3905
+ getScopeGuardPolicy(scopeId) {
3906
+ return workflowCoordinator.getScopeGuardPolicy(scopeId);
3907
+ },
3583
3908
  setScopeChromeVisibility(chromeVisibility) {
3584
3909
  workflowCoordinator.setScopeChromeVisibility(chromeVisibility);
3585
3910
  },
@@ -3839,13 +4164,16 @@ export function createDocumentRuntime(
3839
4164
  zoomLevel: viewState.zoomLevel,
3840
4165
  },
3841
4166
  });
3842
- const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
3843
4167
  const bookmarkMap = buildBookmarkNameMap(state.document);
3844
4168
  const paragraphContexts = collectParagraphContexts(state.document.content.children);
3845
4169
  const paragraphOffsets = paragraphContexts.map((p) => p.startOffset);
3846
4170
  return createFieldResolver({
3847
4171
  pageGraph,
3848
- activePageIndex: navigation.activePageIndex,
4172
+ activePageIndex: resolveActivePageIndexFromPageGraph(
4173
+ pageGraph,
4174
+ state.selection.head,
4175
+ activeStory,
4176
+ ),
3849
4177
  bookmarkMap,
3850
4178
  paragraphOffsets,
3851
4179
  styles: state.document.styles,
@@ -4015,10 +4343,19 @@ export function createDocumentRuntime(
4015
4343
  return buildFieldSnapshot(state.document);
4016
4344
  },
4017
4345
  updateFields(options?: UpdateFieldsOptions): UpdateFieldsResult {
4346
+ const pageGraph = layoutEngine.getPageGraph({
4347
+ document: state.document,
4348
+ viewState: {
4349
+ activeStory,
4350
+ workspaceMode: viewState.workspaceMode,
4351
+ zoomLevel: viewState.zoomLevel,
4352
+ },
4353
+ });
4018
4354
  const refreshed = refreshDocumentFields(
4019
4355
  state.document,
4020
4356
  state.selection.head,
4021
4357
  activeStory,
4358
+ pageGraph,
4022
4359
  options,
4023
4360
  );
4024
4361
  if (refreshed.changed) {
@@ -4939,6 +5276,13 @@ export function createDocumentRuntime(
4939
5276
  * override to the context that `executeEditorCommand` reads.
4940
5277
  */
4941
5278
  documentModeOverride?: DocumentMode;
5279
+ /**
5280
+ * Scope replacements are validated against their target scope by the
5281
+ * Layer-08 compiler before reaching this mechanical dispatch step.
5282
+ * Re-running the selection-scoped workflow guard here would make the
5283
+ * live cursor, not the target scope, decide whether the edit lands.
5284
+ */
5285
+ skipWorkflowGuard?: boolean;
4942
5286
  } = {},
4943
5287
  ): TextCommandAck {
4944
5288
  emitStageToken(telemetryBus, "command", "command.dispatch.start", {
@@ -4980,20 +5324,22 @@ export function createDocumentRuntime(
4980
5324
  blockedReasons: [{ code: "suggesting_unsupported", message }],
4981
5325
  });
4982
5326
  }
4983
- const blockedReasons = workflowCoordinator.evaluateBlockedReasons(selection, command.type);
4984
- if (blockedReasons.length > 0) {
4985
- emit({
4986
- type: "command_blocked",
4987
- documentId: state.documentId,
4988
- command: textOptions.blockedCommandName ?? command.type,
4989
- reasons: blockedReasons,
4990
- });
4991
- return completeDispatch({
4992
- kind: "rejected",
4993
- opId,
4994
- newRevisionToken: "",
4995
- blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
4996
- });
5327
+ if (!textOptions.skipWorkflowGuard) {
5328
+ const blockedReasons = workflowCoordinator.evaluateBlockedReasons(selection, command.type);
5329
+ if (blockedReasons.length > 0) {
5330
+ emit({
5331
+ type: "command_blocked",
5332
+ documentId: state.documentId,
5333
+ command: textOptions.blockedCommandName ?? command.type,
5334
+ reasons: blockedReasons,
5335
+ });
5336
+ return completeDispatch({
5337
+ kind: "rejected",
5338
+ opId,
5339
+ newRevisionToken: "",
5340
+ blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
5341
+ });
5342
+ }
4997
5343
  }
4998
5344
 
4999
5345
  const timestamp = normalizeCommandTimestamp(command.origin?.timestamp) ?? clock();
@@ -6084,6 +6430,7 @@ function toPublicCommentSidebarSnapshot(
6084
6430
  createdBy: thread.createdBy,
6085
6431
  warningCount: thread.warningCount,
6086
6432
  anchorLabel: thread.anchorLabel,
6433
+ linkedRevisionId: sourceThread?.metadata?.linkedRevisionId,
6087
6434
  detachedReason: sourceThread?.metadata?.detachedReason,
6088
6435
  actionabilityNote: sourceThread?.metadata?.actionabilityNote,
6089
6436
  isActive: thread.isActive,
@@ -6102,6 +6449,7 @@ function toPublicTrackedChangesSnapshot(
6102
6449
  createRevisionStoreFromDocument(state),
6103
6450
  );
6104
6451
  const storyPlainTextCache = new Map<string, string>();
6452
+ const commentLinks = collectRevisionCommentLinks(state);
6105
6453
 
6106
6454
  return {
6107
6455
  pendingChangeIds: projection.activeRevisionIds,
@@ -6120,6 +6468,7 @@ function toPublicTrackedChangesSnapshot(
6120
6468
  createDetachedAnchor({ from: 0, to: 0 }, "importAmbiguity"),
6121
6469
  getStoryPlainText(state.document, storyTarget, storyPlainTextCache),
6122
6470
  );
6471
+ const linkedComments = commentLinks.get(revision.revisionId);
6123
6472
 
6124
6473
  return {
6125
6474
  revisionId: revision.revisionId,
@@ -6143,6 +6492,12 @@ function toPublicTrackedChangesSnapshot(
6143
6492
  warningCount: revision.warningCount,
6144
6493
  canAccept: revision.canAccept,
6145
6494
  canReject: revision.canReject,
6495
+ ...(linkedComments
6496
+ ? {
6497
+ commentThreadIds: linkedComments.commentThreadIds,
6498
+ replyCount: linkedComments.replyCount,
6499
+ }
6500
+ : {}),
6146
6501
  importedRevisionForm: sourceRevision?.metadata?.importedRevisionForm,
6147
6502
  preserveOnlyReason: revision.preserveOnlyReason,
6148
6503
  excerpt: preview.excerpt,
@@ -6152,6 +6507,23 @@ function toPublicTrackedChangesSnapshot(
6152
6507
  };
6153
6508
  }
6154
6509
 
6510
+ function collectRevisionCommentLinks(
6511
+ state: Pick<EditorState, "document">,
6512
+ ): Map<string, { commentThreadIds: string[]; replyCount: number }> {
6513
+ const links = new Map<string, { commentThreadIds: string[]; replyCount: number }>();
6514
+ for (const thread of Object.values(state.document.review.comments)) {
6515
+ const revisionId = thread.metadata?.linkedRevisionId;
6516
+ if (!revisionId) {
6517
+ continue;
6518
+ }
6519
+ const current = links.get(revisionId) ?? { commentThreadIds: [], replyCount: 0 };
6520
+ current.commentThreadIds.push(thread.commentId);
6521
+ current.replyCount += Math.max(0, (thread.entries?.length ?? 0) - 1);
6522
+ links.set(revisionId, current);
6523
+ }
6524
+ return links;
6525
+ }
6526
+
6155
6527
  function createRevisionStoreFromDocument(
6156
6528
  state: Pick<EditorState, "document">,
6157
6529
  ): RevisionStore {
@@ -6574,6 +6946,7 @@ function refreshDocumentFields(
6574
6946
  document: CanonicalDocumentEnvelope,
6575
6947
  selectionHead: number,
6576
6948
  activeStory: EditorStoryTarget,
6949
+ pageGraph: RuntimePageGraph,
6577
6950
  options?: UpdateFieldsOptions,
6578
6951
  ): {
6579
6952
  document: CanonicalDocumentEnvelope;
@@ -6584,7 +6957,11 @@ function refreshDocumentFields(
6584
6957
  const supportedOnly = options?.supportedOnly ?? true;
6585
6958
  const bookmarkMap = buildBookmarkNameMap(document);
6586
6959
  const paragraphs = collectParagraphContexts(document.content.children);
6587
- const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
6960
+ const activePageIndex = resolveActivePageIndexFromPageGraph(
6961
+ pageGraph,
6962
+ selectionHead,
6963
+ activeStory,
6964
+ );
6588
6965
  let updatedCount = 0;
6589
6966
  let changed = false;
6590
6967
  let changedFrom: number | undefined;
@@ -6609,7 +6986,8 @@ function refreshDocumentFields(
6609
6986
  document,
6610
6987
  bookmarkMap,
6611
6988
  paragraphs,
6612
- navigation,
6989
+ pageGraph,
6990
+ activePageIndex,
6613
6991
  storyTarget,
6614
6992
  );
6615
6993
  if (!display) {
@@ -7145,7 +7523,8 @@ function resolveSupportedFieldDisplay(
7145
7523
  document: CanonicalDocumentEnvelope,
7146
7524
  bookmarkMap: Map<string, { bookmarkId: string; paragraphIndex: number }>,
7147
7525
  paragraphs: readonly ParagraphContext[],
7148
- navigation: DocumentNavigationSnapshot,
7526
+ pageGraph: RuntimePageGraph,
7527
+ activePageIndex: number,
7149
7528
  storyTarget: EditorStoryTarget,
7150
7529
  ): { displayText: string; refreshStatus: FieldRefreshStatus } | undefined {
7151
7530
  if (!field.fieldFamily || !isSupportedFieldFamily(field.fieldFamily)) {
@@ -7160,33 +7539,41 @@ function resolveSupportedFieldDisplay(
7160
7539
  return { displayText: result.text, refreshStatus: result.refreshStatus };
7161
7540
  }
7162
7541
  if (field.fieldFamily === "SECTIONPAGES") {
7163
- const sectionIndex = "sectionIndex" in storyTarget && typeof storyTarget.sectionIndex === "number"
7164
- ? storyTarget.sectionIndex
7165
- : navigation.activeSectionIndex;
7166
- const sectionPages = navigation.pages.filter((p) => p.sectionIndex === sectionIndex);
7167
- if (sectionPages.length === 0) return { displayText: "", refreshStatus: "unresolvable" };
7168
- const fmt = sectionPages[0]?.layout.pageNumbering?.format;
7542
+ const page = resolveRepresentativePageForStory(pageGraph, activePageIndex, storyTarget);
7543
+ if (!page) {
7544
+ return { displayText: "", refreshStatus: "unresolvable" };
7545
+ }
7169
7546
  return {
7170
- displayText: formatPageNumber(sectionPages.length, fmt),
7547
+ displayText: resolvePageFieldDisplayText("SECTIONPAGES", "", {
7548
+ page,
7549
+ graph: pageGraph,
7550
+ }),
7171
7551
  refreshStatus: "current",
7172
7552
  };
7173
7553
  }
7174
7554
  if (field.fieldFamily === "PAGE") {
7175
- const page = resolveRepresentativePageForStory(navigation, storyTarget);
7555
+ const page = resolveRepresentativePageForStory(pageGraph, activePageIndex, storyTarget);
7176
7556
  if (!page) {
7177
7557
  return { displayText: "", refreshStatus: "unresolvable" };
7178
7558
  }
7179
7559
  return {
7180
- displayText: resolveDisplayedPageNumber(page),
7560
+ displayText: resolvePageFieldDisplayText("PAGE", "", {
7561
+ page,
7562
+ graph: pageGraph,
7563
+ }),
7181
7564
  refreshStatus: "current",
7182
7565
  };
7183
7566
  }
7184
7567
  if (field.fieldFamily === "NUMPAGES") {
7185
- if (navigation.pageCount === 0) {
7568
+ const page = pageGraph.pages[activePageIndex] ?? firstContentPage(pageGraph);
7569
+ if (!page || pageGraph.contentPageCount === 0) {
7186
7570
  return { displayText: "", refreshStatus: "unresolvable" };
7187
7571
  }
7188
7572
  return {
7189
- displayText: String(navigation.pageCount),
7573
+ displayText: resolvePageFieldDisplayText("NUMPAGES", "", {
7574
+ page,
7575
+ graph: pageGraph,
7576
+ }),
7190
7577
  refreshStatus: "current",
7191
7578
  };
7192
7579
  }
@@ -7211,20 +7598,28 @@ function resolveSupportedFieldDisplay(
7211
7598
 
7212
7599
  // \p switch: emit relative position text ("above" / "below" / "on this page")
7213
7600
  if (field.switches?.relativePosition === true) {
7214
- const fieldPage = resolveRepresentativePageForStory(navigation, storyTarget);
7215
- const targetPageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
7601
+ const fieldPage = resolveRepresentativePageForStory(pageGraph, activePageIndex, storyTarget);
7602
+ const targetPageIndex = findPageIndexForRuntimeOffset(
7603
+ pageGraph.pages,
7604
+ paragraph.startOffset,
7605
+ );
7216
7606
  const fieldPageIndex = fieldPage
7217
- ? navigation.pages.indexOf(fieldPage)
7218
- : navigation.activePageIndex;
7607
+ ? pageGraph.pages.findIndex((page) => page.pageId === fieldPage.pageId)
7608
+ : activePageIndex;
7219
7609
  if (targetPageIndex < fieldPageIndex) return { displayText: "above", refreshStatus: "current" };
7220
7610
  if (targetPageIndex > fieldPageIndex) return { displayText: "below", refreshStatus: "current" };
7221
7611
  return { displayText: "on this page", refreshStatus: "current" };
7222
7612
  }
7223
7613
 
7224
- const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
7225
- const page = navigation.pages[pageIndex] ?? navigation.pages[0];
7614
+ const page = pageForRuntimeOffset(pageGraph, paragraph.startOffset);
7226
7615
  return page
7227
- ? { displayText: resolveDisplayedPageNumber(page), refreshStatus: "current" }
7616
+ ? {
7617
+ displayText: resolvePageFieldDisplayText("PAGE", "", {
7618
+ page,
7619
+ graph: pageGraph,
7620
+ }),
7621
+ refreshStatus: "current",
7622
+ }
7228
7623
  : { displayText: "", refreshStatus: "unresolvable" };
7229
7624
  }
7230
7625
  if (field.fieldFamily === "NOTEREF") {
@@ -7240,52 +7635,133 @@ function resolveSupportedFieldDisplay(
7240
7635
  return undefined;
7241
7636
  }
7242
7637
 
7243
- function resolveRepresentativePageForStory(
7244
- navigation: DocumentNavigationSnapshot,
7638
+ function resolveActivePageIndexFromPageGraph(
7639
+ graph: RuntimePageGraph,
7640
+ selectionHead: number,
7245
7641
  storyTarget: EditorStoryTarget,
7246
- ): DocumentNavigationSnapshot["pages"][number] | undefined {
7642
+ ): number {
7643
+ if (graph.pages.length === 0) {
7644
+ return 0;
7645
+ }
7646
+
7247
7647
  if (storyTarget.kind === "main") {
7248
- return navigation.pages[navigation.activePageIndex] ?? navigation.pages[0];
7648
+ return findPageForRuntimeOffset(graph.pages, selectionHead);
7249
7649
  }
7250
7650
 
7251
7651
  if (storyTarget.kind === "header" || storyTarget.kind === "footer") {
7252
- const sectionIndex = storyTarget.sectionIndex ?? navigation.activeSectionIndex;
7253
- const sectionPages = navigation.pages.filter((page) => page.sectionIndex === sectionIndex);
7254
- if (sectionPages.length === 0) {
7255
- return navigation.pages[0];
7652
+ const matchingPageIndex = graph.pages.findIndex((page) => {
7653
+ if (page.isBlankFiller) return false;
7654
+ const activeStory =
7655
+ storyTarget.kind === "header" ? page.stories.header : page.stories.footer;
7656
+ return activeStory ? storyTargetsMatchForPageInstance(activeStory, storyTarget) : false;
7657
+ });
7658
+ if (matchingPageIndex >= 0) {
7659
+ return matchingPageIndex;
7256
7660
  }
7257
- if (storyTarget.variant === "first") {
7258
- return sectionPages[0];
7661
+
7662
+ const sectionIndex = storyTarget.sectionIndex ?? graph.pages[0]!.sectionIndex;
7663
+ const sectionPageIndex = graph.pages.findIndex(
7664
+ (page) => page.sectionIndex === sectionIndex && !page.isBlankFiller,
7665
+ );
7666
+ return sectionPageIndex >= 0 ? sectionPageIndex : 0;
7667
+ }
7668
+
7669
+ return findPageForRuntimeOffset(graph.pages, selectionHead);
7670
+ }
7671
+
7672
+ function findPageForRuntimeOffset(
7673
+ pages: RuntimePageGraph["pages"],
7674
+ offset: number,
7675
+ ): number {
7676
+ return findPageIndexForRuntimeOffset(pages, offset);
7677
+ }
7678
+
7679
+ function findPageIndexForRuntimeOffset(
7680
+ pages: RuntimePageGraph["pages"],
7681
+ offset: number,
7682
+ ): number {
7683
+ let lastContentPageIndex = -1;
7684
+ for (let i = 0; i < pages.length; i += 1) {
7685
+ const page = pages[i]!;
7686
+ if (page.isBlankFiller) {
7687
+ continue;
7259
7688
  }
7260
- if (storyTarget.variant === "even") {
7261
- return sectionPages.find((page) => (page.pageInSection + 1) % 2 === 0) ?? sectionPages[0];
7689
+ lastContentPageIndex = i;
7690
+ if (offset < page.endOffset) {
7691
+ return i;
7262
7692
  }
7263
- return (
7264
- sectionPages.find((page) => isDefaultHeaderFooterPage(page)) ??
7265
- sectionPages[0]
7266
- );
7267
7693
  }
7694
+ if (lastContentPageIndex >= 0) {
7695
+ return lastContentPageIndex;
7696
+ }
7697
+ return Math.max(0, pages.length - 1);
7698
+ }
7268
7699
 
7269
- return navigation.pages[navigation.activePageIndex] ?? navigation.pages[0];
7700
+ function pageForRuntimeOffset(
7701
+ graph: RuntimePageGraph,
7702
+ offset: number,
7703
+ ): RuntimePageNode | undefined {
7704
+ return graph.pages[findPageIndexForRuntimeOffset(graph.pages, offset)];
7270
7705
  }
7271
7706
 
7272
- function isDefaultHeaderFooterPage(
7273
- page: DocumentNavigationSnapshot["pages"][number],
7707
+ function resolveRepresentativePageForStory(
7708
+ graph: RuntimePageGraph,
7709
+ activePageIndex: number,
7710
+ storyTarget: EditorStoryTarget,
7711
+ ): RuntimePageNode | undefined {
7712
+ if (storyTarget.kind === "main") {
7713
+ return graph.pages[activePageIndex] ?? firstContentPage(graph);
7714
+ }
7715
+
7716
+ if (storyTarget.kind === "header" || storyTarget.kind === "footer") {
7717
+ const matchedPage = graph.pages.find((page) => {
7718
+ if (page.isBlankFiller) return false;
7719
+ const activeStory =
7720
+ storyTarget.kind === "header" ? page.stories.header : page.stories.footer;
7721
+ return activeStory ? storyTargetsMatchForPageInstance(activeStory, storyTarget) : false;
7722
+ });
7723
+ if (matchedPage) {
7724
+ return matchedPage;
7725
+ }
7726
+ if (typeof storyTarget.sectionIndex === "number") {
7727
+ return graph.pages.find(
7728
+ (page) => page.sectionIndex === storyTarget.sectionIndex && !page.isBlankFiller,
7729
+ ) ?? firstContentPage(graph);
7730
+ }
7731
+ return firstContentPage(graph);
7732
+ }
7733
+
7734
+ return graph.pages[activePageIndex] ?? firstContentPage(graph);
7735
+ }
7736
+
7737
+ function storyTargetsMatchForPageInstance(
7738
+ activeStory: EditorStoryTarget,
7739
+ requestedStory: EditorStoryTarget,
7274
7740
  ): boolean {
7275
- if (page.layout.differentFirstPage && page.pageInSection === 0) {
7741
+ if (activeStory.kind !== requestedStory.kind) {
7276
7742
  return false;
7277
7743
  }
7278
- if (page.layout.differentOddEvenPages) {
7279
- return (page.pageInSection + 1) % 2 === 1;
7744
+ if (activeStory.kind === "header" && requestedStory.kind === "header") {
7745
+ return (
7746
+ activeStory.relationshipId === requestedStory.relationshipId &&
7747
+ activeStory.variant === requestedStory.variant &&
7748
+ (requestedStory.sectionIndex === undefined ||
7749
+ activeStory.sectionIndex === requestedStory.sectionIndex)
7750
+ );
7280
7751
  }
7281
- return true;
7752
+ if (activeStory.kind === "footer" && requestedStory.kind === "footer") {
7753
+ return (
7754
+ activeStory.relationshipId === requestedStory.relationshipId &&
7755
+ activeStory.variant === requestedStory.variant &&
7756
+ (requestedStory.sectionIndex === undefined ||
7757
+ activeStory.sectionIndex === requestedStory.sectionIndex)
7758
+ );
7759
+ }
7760
+ return storyTargetsEqual(activeStory, requestedStory);
7282
7761
  }
7283
7762
 
7284
- function resolveDisplayedPageNumber(
7285
- page: DocumentNavigationSnapshot["pages"][number],
7286
- ): string {
7287
- const n = (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
7288
- return formatPageNumber(n, page.layout.pageNumbering?.format);
7763
+ function firstContentPage(graph: RuntimePageGraph): RuntimePageNode | undefined {
7764
+ return graph.pages.find((page) => !page.isBlankFiller) ?? graph.pages[0];
7289
7765
  }
7290
7766
 
7291
7767
  interface ParagraphContext {
@@ -7703,6 +8179,93 @@ function isHighlightedSegment(
7703
8179
  return typeof bg === "string" && bg.length > 0;
7704
8180
  }
7705
8181
 
8182
+ function resolveActiveParagraphContext(
8183
+ snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
8184
+ ): {
8185
+ paragraphIndex: number;
8186
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
8187
+ atParagraphStart: boolean;
8188
+ isEmpty: boolean;
8189
+ } | null {
8190
+ if (!snapshot.surface) {
8191
+ return null;
8192
+ }
8193
+
8194
+ const paragraphIndex = resolveActiveParagraphIndex(
8195
+ snapshot.surface.blocks,
8196
+ snapshot.selection,
8197
+ );
8198
+ if (paragraphIndex === null) {
8199
+ return null;
8200
+ }
8201
+
8202
+ const selectionPosition =
8203
+ snapshot.selection.activeRange.kind === "node"
8204
+ ? snapshot.selection.activeRange.at
8205
+ : snapshot.selection.head;
8206
+ const paragraph = findSurfaceParagraphAtPosition(
8207
+ snapshot.surface.blocks,
8208
+ selectionPosition,
8209
+ );
8210
+ if (!paragraph) {
8211
+ return null;
8212
+ }
8213
+
8214
+ return {
8215
+ paragraphIndex,
8216
+ paragraph,
8217
+ atParagraphStart:
8218
+ snapshot.selection.isCollapsed &&
8219
+ snapshot.selection.activeRange.kind !== "node" &&
8220
+ snapshot.selection.anchor === snapshot.selection.head &&
8221
+ snapshot.selection.head === paragraph.from,
8222
+ isEmpty: isSurfaceParagraphEmpty(paragraph),
8223
+ };
8224
+ }
8225
+
8226
+ function findSurfaceParagraphAtPosition(
8227
+ blocks: readonly SurfaceBlockSnapshot[],
8228
+ position: number,
8229
+ ): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
8230
+ for (const block of blocks) {
8231
+ if (position < block.from || position > block.to) {
8232
+ continue;
8233
+ }
8234
+ if (block.kind === "paragraph") {
8235
+ return block;
8236
+ }
8237
+ if (block.kind === "table") {
8238
+ for (const row of block.rows) {
8239
+ for (const cell of row.cells) {
8240
+ const paragraph = findSurfaceParagraphAtPosition(cell.content, position);
8241
+ if (paragraph) {
8242
+ return paragraph;
8243
+ }
8244
+ }
8245
+ }
8246
+ continue;
8247
+ }
8248
+ if (block.kind === "sdt_block") {
8249
+ const paragraph = findSurfaceParagraphAtPosition(block.children, position);
8250
+ if (paragraph) {
8251
+ return paragraph;
8252
+ }
8253
+ }
8254
+ }
8255
+ return null;
8256
+ }
8257
+
8258
+ function isSurfaceParagraphEmpty(
8259
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
8260
+ ): boolean {
8261
+ if (paragraph.segments.length === 0) {
8262
+ return true;
8263
+ }
8264
+ return paragraph.segments.every(
8265
+ (segment) => segment.kind === "text" && segment.text.length === 0,
8266
+ );
8267
+ }
8268
+
7706
8269
  function forEachParagraphBlock(
7707
8270
  blocks: readonly SurfaceBlockSnapshot[],
7708
8271
  visit: (paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
@@ -7816,6 +8379,44 @@ function buildRunPropertyBeforeXml(
7816
8379
  return `<w:rPr>${parts.join("")}</w:rPr>`;
7817
8380
  }
7818
8381
 
8382
+ function buildParagraphPropertyBeforeXml(
8383
+ paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
8384
+ ): string {
8385
+ const parts: string[] = [];
8386
+ if (paragraph.styleId) {
8387
+ parts.push(`<w:pStyle w:val="${escapeAttributeXml(paragraph.styleId)}"/>`);
8388
+ }
8389
+ if (paragraph.numbering) {
8390
+ parts.push(
8391
+ `<w:numPr><w:ilvl w:val="${paragraph.numbering.level}"/><w:numId w:val="${escapeAttributeXml(
8392
+ paragraph.numbering.numberingInstanceId.replace(/^num:/u, ""),
8393
+ )}"/></w:numPr>`,
8394
+ );
8395
+ }
8396
+ if (paragraph.alignment) {
8397
+ parts.push(`<w:jc w:val="${escapeAttributeXml(paragraph.alignment)}"/>`);
8398
+ }
8399
+ if (paragraph.indentation) {
8400
+ const attrs: string[] = [];
8401
+ if (paragraph.indentation.left !== undefined) {
8402
+ attrs.push(`w:left="${paragraph.indentation.left}"`);
8403
+ }
8404
+ if (paragraph.indentation.right !== undefined) {
8405
+ attrs.push(`w:right="${paragraph.indentation.right}"`);
8406
+ }
8407
+ if (paragraph.indentation.firstLine !== undefined) {
8408
+ attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
8409
+ }
8410
+ if (paragraph.indentation.hanging !== undefined) {
8411
+ attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
8412
+ }
8413
+ if (attrs.length > 0) {
8414
+ parts.push(`<w:ind ${attrs.join(" ")}/>`);
8415
+ }
8416
+ }
8417
+ return `<w:pPr>${parts.join("")}</w:pPr>`;
8418
+ }
8419
+
7819
8420
  function escapeAttributeXml(value: string): string {
7820
8421
  return value
7821
8422
  .replace(/&/g, "&amp;")
@@ -7824,6 +8425,55 @@ function escapeAttributeXml(value: string): string {
7824
8425
  .replace(/"/g, "&quot;");
7825
8426
  }
7826
8427
 
8428
+ function stripStoryTarget(selection: SelectionSnapshot): SelectionSnapshot {
8429
+ const { storyTarget: _storyTarget, ...rest } = selection;
8430
+ return rest;
8431
+ }
8432
+
8433
+ function toInternalSelectionSnapshot(
8434
+ selection: SelectionSnapshot,
8435
+ ): EditorState["selection"] {
8436
+ return {
8437
+ anchor: selection.anchor,
8438
+ head: selection.head,
8439
+ isCollapsed: selection.isCollapsed,
8440
+ activeRange:
8441
+ selection.activeRange.kind === "range"
8442
+ ? createRangeAnchor(
8443
+ selection.activeRange.from,
8444
+ selection.activeRange.to,
8445
+ selection.activeRange.assoc,
8446
+ )
8447
+ : selection.activeRange.kind === "node"
8448
+ ? createNodeAnchor(selection.activeRange.at, selection.activeRange.assoc)
8449
+ : createDetachedAnchor(
8450
+ selection.activeRange.lastKnownRange,
8451
+ selection.activeRange.reason,
8452
+ ),
8453
+ };
8454
+ }
8455
+
8456
+ function getFormattingOperationCommandName(operation: FormattingOperation): string {
8457
+ switch (operation.type) {
8458
+ case "toggle":
8459
+ return `toggle${operation.mark.charAt(0).toUpperCase()}${operation.mark.slice(1)}`;
8460
+ case "set-font-family":
8461
+ return "setFontFamily";
8462
+ case "set-font-size":
8463
+ return "setFontSize";
8464
+ case "set-text-color":
8465
+ return "setTextColor";
8466
+ case "set-highlight-color":
8467
+ return "setHighlightColor";
8468
+ case "set-alignment":
8469
+ return "setAlignment";
8470
+ case "indent":
8471
+ return "indent";
8472
+ case "outdent":
8473
+ return "outdent";
8474
+ }
8475
+ }
8476
+
7827
8477
  function appendPropertyChangeSuggestion(
7828
8478
  document: CanonicalDocumentEnvelope,
7829
8479
  anchor: { from: number; to: number },