@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.
- package/package.json +1 -1
- package/src/api/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +86 -4
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +718 -68
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +10 -0
- package/src/ui/WordReviewEditor.tsx +99 -9
- package/src/ui/editor-command-bag.ts +3 -1
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/types.ts +3 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
- 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 {
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
7164
|
-
|
|
7165
|
-
:
|
|
7166
|
-
|
|
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:
|
|
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(
|
|
7555
|
+
const page = resolveRepresentativePageForStory(pageGraph, activePageIndex, storyTarget);
|
|
7176
7556
|
if (!page) {
|
|
7177
7557
|
return { displayText: "", refreshStatus: "unresolvable" };
|
|
7178
7558
|
}
|
|
7179
7559
|
return {
|
|
7180
|
-
displayText:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
7215
|
-
const targetPageIndex =
|
|
7601
|
+
const fieldPage = resolveRepresentativePageForStory(pageGraph, activePageIndex, storyTarget);
|
|
7602
|
+
const targetPageIndex = findPageIndexForRuntimeOffset(
|
|
7603
|
+
pageGraph.pages,
|
|
7604
|
+
paragraph.startOffset,
|
|
7605
|
+
);
|
|
7216
7606
|
const fieldPageIndex = fieldPage
|
|
7217
|
-
?
|
|
7218
|
-
:
|
|
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
|
|
7225
|
-
const page = navigation.pages[pageIndex] ?? navigation.pages[0];
|
|
7614
|
+
const page = pageForRuntimeOffset(pageGraph, paragraph.startOffset);
|
|
7226
7615
|
return page
|
|
7227
|
-
? {
|
|
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
|
|
7244
|
-
|
|
7638
|
+
function resolveActivePageIndexFromPageGraph(
|
|
7639
|
+
graph: RuntimePageGraph,
|
|
7640
|
+
selectionHead: number,
|
|
7245
7641
|
storyTarget: EditorStoryTarget,
|
|
7246
|
-
):
|
|
7642
|
+
): number {
|
|
7643
|
+
if (graph.pages.length === 0) {
|
|
7644
|
+
return 0;
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7247
7647
|
if (storyTarget.kind === "main") {
|
|
7248
|
-
return
|
|
7648
|
+
return findPageForRuntimeOffset(graph.pages, selectionHead);
|
|
7249
7649
|
}
|
|
7250
7650
|
|
|
7251
7651
|
if (storyTarget.kind === "header" || storyTarget.kind === "footer") {
|
|
7252
|
-
const
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
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
|
-
|
|
7258
|
-
|
|
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
|
-
|
|
7261
|
-
|
|
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
|
-
|
|
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
|
|
7273
|
-
|
|
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 (
|
|
7741
|
+
if (activeStory.kind !== requestedStory.kind) {
|
|
7276
7742
|
return false;
|
|
7277
7743
|
}
|
|
7278
|
-
if (
|
|
7279
|
-
return (
|
|
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
|
-
|
|
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
|
|
7285
|
-
page
|
|
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, "&")
|
|
@@ -7824,6 +8425,55 @@ function escapeAttributeXml(value: string): string {
|
|
|
7824
8425
|
.replace(/"/g, """);
|
|
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 },
|