@beyondwork/docx-react-component 1.0.46 → 1.0.47
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/core/commands/index.ts +120 -1
- package/src/io/docx-session.ts +37 -8
- package/src/io/ooxml/workflow-payload.ts +27 -0
- package/src/runtime/collab/event-types.ts +10 -5
- package/src/runtime/collab/runtime-collab-sync.ts +125 -1
- package/src/runtime/document-runtime.ts +7 -0
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/text-ack-range.ts +3 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.47",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"packageManager": "pnpm@10.30.3",
|
|
7
7
|
"type": "module",
|
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
outdentParagraphAtSelection,
|
|
35
35
|
splitParagraph,
|
|
36
36
|
} from "./text-commands.ts";
|
|
37
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
BlockNode,
|
|
39
|
+
RevisionRecord as CanonicalRevisionRecord,
|
|
40
|
+
} from "../../model/canonical-document.ts";
|
|
38
41
|
import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
|
|
39
42
|
import { collectScopeTagTouches } from "../../review/store/scope-tag-diff.ts";
|
|
40
43
|
import { applyRevisionRuntimeCommand } from "../../runtime/revision-runtime.ts";
|
|
@@ -86,6 +89,15 @@ import {
|
|
|
86
89
|
import { insertPageBreak, insertTable } from "./text-commands.ts";
|
|
87
90
|
import type { TableSelectionDescriptor } from "../../runtime/table-commands.ts";
|
|
88
91
|
|
|
92
|
+
export type ContentChildrenPatch =
|
|
93
|
+
| { kind: "keep-all" }
|
|
94
|
+
| { kind: "replace-all"; children: BlockNode[] }
|
|
95
|
+
| {
|
|
96
|
+
kind: "index-map";
|
|
97
|
+
baseLength: number;
|
|
98
|
+
entries: ReadonlyArray<{ index: number; block: BlockNode }>;
|
|
99
|
+
};
|
|
100
|
+
|
|
89
101
|
export interface CommandOrigin {
|
|
90
102
|
source:
|
|
91
103
|
| "keyboard"
|
|
@@ -112,6 +124,15 @@ export type EditorCommand =
|
|
|
112
124
|
protectionSelection?: SelectionSnapshot;
|
|
113
125
|
origin?: CommandOrigin;
|
|
114
126
|
}
|
|
127
|
+
| {
|
|
128
|
+
type: "document.patch";
|
|
129
|
+
patch: Partial<CanonicalDocumentEnvelope>;
|
|
130
|
+
contentChildren?: ContentChildrenPatch;
|
|
131
|
+
mapping?: TransactionMapping;
|
|
132
|
+
selection?: SelectionSnapshot;
|
|
133
|
+
protectionSelection?: SelectionSnapshot;
|
|
134
|
+
origin?: CommandOrigin;
|
|
135
|
+
}
|
|
115
136
|
| {
|
|
116
137
|
type: "text.insert";
|
|
117
138
|
text: string;
|
|
@@ -482,6 +503,53 @@ export function executeEditorCommand(
|
|
|
482
503
|
},
|
|
483
504
|
);
|
|
484
505
|
}
|
|
506
|
+
case "document.patch": {
|
|
507
|
+
const mapping = command.mapping ?? createEmptyMapping();
|
|
508
|
+
const nextDocument = applyDocumentPatch(
|
|
509
|
+
state.document,
|
|
510
|
+
command.patch,
|
|
511
|
+
command.contentChildren,
|
|
512
|
+
);
|
|
513
|
+
const selection =
|
|
514
|
+
command.selection ?? remapSelection(state.selection, mapping);
|
|
515
|
+
const reviewState = remapReviewStateAfterContentChange(
|
|
516
|
+
state,
|
|
517
|
+
nextDocument,
|
|
518
|
+
mapping,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
return createTransaction(
|
|
522
|
+
{
|
|
523
|
+
...state,
|
|
524
|
+
document: reviewState.document,
|
|
525
|
+
selection,
|
|
526
|
+
warnings: reviewState.warnings,
|
|
527
|
+
runtime: {
|
|
528
|
+
...state.runtime,
|
|
529
|
+
activeCommentId: reviewState.activeCommentId,
|
|
530
|
+
},
|
|
531
|
+
compatibility: {
|
|
532
|
+
...state.compatibility,
|
|
533
|
+
generatedAt: context.timestamp,
|
|
534
|
+
warnings: reviewState.warnings,
|
|
535
|
+
featureEntries: state.compatibility.featureEntries.map((entry) =>
|
|
536
|
+
entry.affectedAnchor
|
|
537
|
+
? {
|
|
538
|
+
...entry,
|
|
539
|
+
affectedAnchor: mapAnchor(entry.affectedAnchor, mapping),
|
|
540
|
+
}
|
|
541
|
+
: entry,
|
|
542
|
+
),
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
historyBoundary: "push",
|
|
547
|
+
markDirty: true,
|
|
548
|
+
mapping,
|
|
549
|
+
effects: reviewState.effects,
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
}
|
|
485
553
|
case "text.insert": {
|
|
486
554
|
const suggestingResult = context.documentMode === "suggesting"
|
|
487
555
|
? applySuggestingInsert(state, command.text, context)
|
|
@@ -1141,6 +1209,57 @@ export function executeEditorCommand(
|
|
|
1141
1209
|
}
|
|
1142
1210
|
}
|
|
1143
1211
|
|
|
1212
|
+
export function applyDocumentPatch(
|
|
1213
|
+
base: CanonicalDocumentEnvelope,
|
|
1214
|
+
patch: Partial<CanonicalDocumentEnvelope>,
|
|
1215
|
+
contentChildren?: ContentChildrenPatch,
|
|
1216
|
+
): CanonicalDocumentEnvelope {
|
|
1217
|
+
const nextContent = resolvePatchedContent(base, patch, contentChildren);
|
|
1218
|
+
const merged: CanonicalDocumentEnvelope = { ...base, ...patch };
|
|
1219
|
+
if (nextContent) {
|
|
1220
|
+
merged.content = nextContent;
|
|
1221
|
+
}
|
|
1222
|
+
return merged;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function resolvePatchedContent(
|
|
1226
|
+
base: CanonicalDocumentEnvelope,
|
|
1227
|
+
patch: Partial<CanonicalDocumentEnvelope>,
|
|
1228
|
+
contentChildren: ContentChildrenPatch | undefined,
|
|
1229
|
+
): CanonicalDocumentEnvelope["content"] | null {
|
|
1230
|
+
if (!contentChildren) {
|
|
1231
|
+
return patch.content ?? null;
|
|
1232
|
+
}
|
|
1233
|
+
if (contentChildren.kind === "keep-all") {
|
|
1234
|
+
return { ...base.content, children: base.content.children };
|
|
1235
|
+
}
|
|
1236
|
+
if (contentChildren.kind === "replace-all") {
|
|
1237
|
+
return {
|
|
1238
|
+
...(patch.content ?? base.content),
|
|
1239
|
+
children: contentChildren.children,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const baseChildren = base.content.children;
|
|
1243
|
+
if (contentChildren.baseLength !== baseChildren.length) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`applyDocumentPatch: index-map baseLength ${contentChildren.baseLength} does not match current children length ${baseChildren.length}`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const nextChildren: BlockNode[] = baseChildren.slice();
|
|
1249
|
+
for (const entry of contentChildren.entries) {
|
|
1250
|
+
if (entry.index < 0 || entry.index >= nextChildren.length) {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
`applyDocumentPatch: index-map entry index ${entry.index} out of range [0, ${nextChildren.length})`,
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
nextChildren[entry.index] = entry.block;
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
...(patch.content ?? base.content),
|
|
1259
|
+
children: nextChildren,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1144
1263
|
function buildDocumentReplaceTransaction(
|
|
1145
1264
|
state: EditorState,
|
|
1146
1265
|
context: CommandExecutionContext,
|
package/src/io/docx-session.ts
CHANGED
|
@@ -59,14 +59,12 @@ import {
|
|
|
59
59
|
getDocumentBackedWorkflowMetadata,
|
|
60
60
|
parseWorkflowPayloadEnvelopeFromPackage,
|
|
61
61
|
resolvePayloadPartPath,
|
|
62
|
+
resolveWorkflowPayloadPartPaths,
|
|
62
63
|
WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
63
64
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
|
|
64
65
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
65
66
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
|
|
66
67
|
WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
|
|
67
|
-
WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
|
|
68
|
-
WORKFLOW_PAYLOAD_PART_PATH,
|
|
69
|
-
WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
|
|
70
68
|
} from "./ooxml/workflow-payload.ts";
|
|
71
69
|
import {
|
|
72
70
|
classifyCorruptPackageError,
|
|
@@ -1736,12 +1734,17 @@ function exportDocxEditorSession(
|
|
|
1736
1734
|
}
|
|
1737
1735
|
}
|
|
1738
1736
|
|
|
1737
|
+
const workflowPayloadPartPaths = resolveWorkflowPayloadPartPaths(
|
|
1738
|
+
state.sourcePackage,
|
|
1739
|
+
sessionState.documentId,
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1739
1742
|
const exportSession = createExportSession(state.sourcePackage, [
|
|
1740
1743
|
state.sourceDocumentPartPath,
|
|
1741
1744
|
APP_PROPERTIES_PART_PATH,
|
|
1742
1745
|
CORE_PROPERTIES_PART_PATH,
|
|
1743
|
-
|
|
1744
|
-
|
|
1746
|
+
workflowPayloadPartPaths.payloadPartPath,
|
|
1747
|
+
workflowPayloadPartPaths.itemPropsPartPath,
|
|
1745
1748
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
1746
1749
|
numberingPartPath,
|
|
1747
1750
|
commentsPartPath,
|
|
@@ -1974,7 +1977,14 @@ function exportDocxEditorSession(
|
|
|
1974
1977
|
ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
|
|
1975
1978
|
// Schema 1.2: pass through editorState payload collected by the runtime channel.
|
|
1976
1979
|
const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
|
|
1977
|
-
ensureWorkflowPayloadParts(
|
|
1980
|
+
ensureWorkflowPayloadParts(
|
|
1981
|
+
exportSession,
|
|
1982
|
+
sessionState,
|
|
1983
|
+
currentDocument,
|
|
1984
|
+
state.sourcePackage,
|
|
1985
|
+
internalEditorState,
|
|
1986
|
+
workflowPayloadPartPaths,
|
|
1987
|
+
);
|
|
1978
1988
|
|
|
1979
1989
|
return {
|
|
1980
1990
|
bytes: exportSession.serialize(),
|
|
@@ -3701,6 +3711,7 @@ function ensureWorkflowPayloadParts(
|
|
|
3701
3711
|
document: CanonicalDocumentEnvelope,
|
|
3702
3712
|
sourcePackage: OpcPackage,
|
|
3703
3713
|
editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
|
|
3714
|
+
precomputedPartPaths?: { payloadPartPath: string; itemPropsPartPath: string },
|
|
3704
3715
|
): void {
|
|
3705
3716
|
const payloadParts = buildWorkflowPayloadParts({
|
|
3706
3717
|
sourcePackage,
|
|
@@ -3716,19 +3727,37 @@ function ensureWorkflowPayloadParts(
|
|
|
3716
3727
|
return;
|
|
3717
3728
|
}
|
|
3718
3729
|
|
|
3730
|
+
// Export ownership is resolved before session creation, so any precomputed
|
|
3731
|
+
// paths must stay in lockstep with buildWorkflowPayloadParts for the same
|
|
3732
|
+
// source package and documentId.
|
|
3733
|
+
if (precomputedPartPaths) {
|
|
3734
|
+
if (
|
|
3735
|
+
precomputedPartPaths.payloadPartPath !== payloadParts.payloadPartPath ||
|
|
3736
|
+
precomputedPartPaths.itemPropsPartPath !== payloadParts.itemPropsPartPath
|
|
3737
|
+
) {
|
|
3738
|
+
throw new Error(
|
|
3739
|
+
`ensureWorkflowPayloadParts: precomputed payload part paths `
|
|
3740
|
+
+ `(${precomputedPartPaths.payloadPartPath} / ${precomputedPartPaths.itemPropsPartPath}) `
|
|
3741
|
+
+ `do not match buildWorkflowPayloadParts output `
|
|
3742
|
+
+ `(${payloadParts.payloadPartPath} / ${payloadParts.itemPropsPartPath}). `
|
|
3743
|
+
+ `This is a bug in the export orchestration.`,
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3719
3748
|
const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
|
|
3720
3749
|
const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
|
|
3721
3750
|
const customPropsPart = sourcePackage.parts.get(WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH);
|
|
3722
3751
|
|
|
3723
3752
|
exportSession.replaceOwnedPart({
|
|
3724
|
-
path: payloadParts.payloadPartPath,
|
|
3753
|
+
path: precomputedPartPaths?.payloadPartPath ?? payloadParts.payloadPartPath,
|
|
3725
3754
|
bytes: new TextEncoder().encode(payloadParts.payloadPartXml),
|
|
3726
3755
|
contentType: payloadPart?.contentType ?? WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
3727
3756
|
relationships: payloadParts.payloadRelationships,
|
|
3728
3757
|
compression: payloadPart?.compression,
|
|
3729
3758
|
});
|
|
3730
3759
|
exportSession.replaceOwnedPart({
|
|
3731
|
-
path: payloadParts.itemPropsPartPath,
|
|
3760
|
+
path: precomputedPartPaths?.itemPropsPartPath ?? payloadParts.itemPropsPartPath,
|
|
3732
3761
|
bytes: new TextEncoder().encode(payloadParts.itemPropsXml),
|
|
3733
3762
|
contentType: itemPropsPart?.contentType ?? WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
|
|
3734
3763
|
compression: itemPropsPart?.compression,
|
|
@@ -1158,6 +1158,33 @@ function resolveDescriptor(sourcePackage: OpcPackage, documentId: string): Embed
|
|
|
1158
1158
|
};
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
+
/**
|
|
1162
|
+
* Resolve the OPC part paths the workflow-payload serializer will write
|
|
1163
|
+
* to on export, based on the source package's existing customXml slots.
|
|
1164
|
+
*
|
|
1165
|
+
* - If the source already contains a BW workflow payload, the existing
|
|
1166
|
+
* `/customXml/itemN.xml` path is reused (via `resolvePayloadPartPath`).
|
|
1167
|
+
* - Otherwise, `findNextAvailableItemNumber(sourcePackage)` picks the
|
|
1168
|
+
* next free slot (e.g. `item5.xml` when `item1..4.xml` already exist
|
|
1169
|
+
* in the source).
|
|
1170
|
+
*
|
|
1171
|
+
* The returned paths MUST be passed to `createExportSession` as owned
|
|
1172
|
+
* paths so `replaceOwnedPart(...)` does not throw when
|
|
1173
|
+
* `ensureWorkflowPayloadParts` later writes to them. The custom-props
|
|
1174
|
+
* part lives at the fixed path `WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH`
|
|
1175
|
+
* and is not part of this resolver.
|
|
1176
|
+
*/
|
|
1177
|
+
export function resolveWorkflowPayloadPartPaths(
|
|
1178
|
+
sourcePackage: OpcPackage,
|
|
1179
|
+
documentId: string,
|
|
1180
|
+
): { payloadPartPath: string; itemPropsPartPath: string } {
|
|
1181
|
+
const descriptor = resolveDescriptor(sourcePackage, documentId);
|
|
1182
|
+
return {
|
|
1183
|
+
payloadPartPath: descriptor.payloadPartPath,
|
|
1184
|
+
itemPropsPartPath: descriptor.itemPropsPartPath,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1161
1188
|
export function resolvePayloadPartPath(sourcePackage: OpcPackage): string | null {
|
|
1162
1189
|
const mirroredItemId = readCustomPropertyValue(
|
|
1163
1190
|
sourcePackage,
|
|
@@ -18,11 +18,15 @@ import type { EditorStoryTarget } from "../../api/public-types.ts";
|
|
|
18
18
|
* Events are meant to be small deltas (text insertions, comment adds,
|
|
19
19
|
* change accepts, etc.). They are NOT full-document snapshots.
|
|
20
20
|
*
|
|
21
|
-
* `document.replace`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* `document.replace` carries a full `CanonicalDocumentEnvelope` and is
|
|
22
|
+
* therefore bandwidth-heavy. The collab sync bridge transparently
|
|
23
|
+
* converts broadcast `document.replace` commands to `document.patch`
|
|
24
|
+
* deltas by diffing against the pre-commit document snapshot, so the
|
|
25
|
+
* shared event log only ever contains the changed top-level keys (and,
|
|
26
|
+
* for `content.children`, only the changed block indices). Remote
|
|
27
|
+
* replicas apply `document.patch` by merging the delta onto their
|
|
28
|
+
* current canonical state — see `applyDocumentPatch` in
|
|
29
|
+
* `core/commands/index.ts`.
|
|
26
30
|
*/
|
|
27
31
|
export interface CommandEvent {
|
|
28
32
|
/** UUID — dedup key for defensive idempotency. */
|
|
@@ -74,6 +78,7 @@ export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new S
|
|
|
74
78
|
"text.insert-hard-break",
|
|
75
79
|
"paragraph.split",
|
|
76
80
|
"document.replace",
|
|
81
|
+
"document.patch",
|
|
77
82
|
"comment.add",
|
|
78
83
|
"comment.resolve",
|
|
79
84
|
"comment.reopen",
|
|
@@ -2,9 +2,12 @@ import * as Y from "yjs";
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
CommandExecutionContext,
|
|
5
|
+
ContentChildrenPatch,
|
|
5
6
|
EditorCommand,
|
|
6
7
|
EditorTransaction,
|
|
7
8
|
} from "../../core/commands/index.ts";
|
|
9
|
+
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
10
|
+
import type { BlockNode } from "../../model/canonical-document.ts";
|
|
8
11
|
import type {
|
|
9
12
|
CommandAppliedMeta,
|
|
10
13
|
DocumentRuntime,
|
|
@@ -17,6 +20,122 @@ import {
|
|
|
17
20
|
type CommandEvent,
|
|
18
21
|
} from "./event-types.ts";
|
|
19
22
|
|
|
23
|
+
const PATCHABLE_TOP_LEVEL_KEYS = [
|
|
24
|
+
"updatedAt",
|
|
25
|
+
"metadata",
|
|
26
|
+
"styles",
|
|
27
|
+
"numbering",
|
|
28
|
+
"media",
|
|
29
|
+
"content",
|
|
30
|
+
"review",
|
|
31
|
+
"preservation",
|
|
32
|
+
"diagnostics",
|
|
33
|
+
"subParts",
|
|
34
|
+
"fieldRegistry",
|
|
35
|
+
] as const satisfies ReadonlyArray<keyof CanonicalDocumentEnvelope>;
|
|
36
|
+
|
|
37
|
+
export function compressDocumentReplaceForBroadcast(
|
|
38
|
+
command: EditorCommand,
|
|
39
|
+
priorDocument: CanonicalDocumentEnvelope,
|
|
40
|
+
): EditorCommand {
|
|
41
|
+
if (command.type !== "document.replace") {
|
|
42
|
+
return command;
|
|
43
|
+
}
|
|
44
|
+
const next = command.document;
|
|
45
|
+
if (next === priorDocument) {
|
|
46
|
+
return command;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const patch: Partial<CanonicalDocumentEnvelope> = {};
|
|
50
|
+
let anyTopLevelChange = false;
|
|
51
|
+
for (const key of PATCHABLE_TOP_LEVEL_KEYS) {
|
|
52
|
+
if (key === "content") continue;
|
|
53
|
+
if (next[key] !== priorDocument[key]) {
|
|
54
|
+
(patch as Record<string, unknown>)[key] = next[key];
|
|
55
|
+
anyTopLevelChange = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const contentChildrenPatch = diffContentChildren(
|
|
60
|
+
priorDocument.content.children,
|
|
61
|
+
next.content.children,
|
|
62
|
+
);
|
|
63
|
+
const contentMetaChanged =
|
|
64
|
+
next.content !== priorDocument.content &&
|
|
65
|
+
!contentReferenceEqualExceptChildren(priorDocument.content, next.content);
|
|
66
|
+
if (contentMetaChanged) {
|
|
67
|
+
const { children: _children, ...rest } = next.content;
|
|
68
|
+
patch.content = { ...rest, children: [] } as CanonicalDocumentEnvelope["content"];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const contentChanged =
|
|
72
|
+
contentChildrenPatch.kind !== "keep-all" || contentMetaChanged;
|
|
73
|
+
if (!contentChanged && !anyTopLevelChange) {
|
|
74
|
+
return {
|
|
75
|
+
type: "document.patch",
|
|
76
|
+
patch: {},
|
|
77
|
+
contentChildren: { kind: "keep-all" },
|
|
78
|
+
mapping: command.mapping,
|
|
79
|
+
selection: command.selection,
|
|
80
|
+
protectionSelection: command.protectionSelection,
|
|
81
|
+
origin: command.origin,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
type: "document.patch",
|
|
87
|
+
patch,
|
|
88
|
+
contentChildren: contentChildrenPatch,
|
|
89
|
+
mapping: command.mapping,
|
|
90
|
+
selection: command.selection,
|
|
91
|
+
protectionSelection: command.protectionSelection,
|
|
92
|
+
origin: command.origin,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function contentReferenceEqualExceptChildren(
|
|
97
|
+
a: CanonicalDocumentEnvelope["content"],
|
|
98
|
+
b: CanonicalDocumentEnvelope["content"],
|
|
99
|
+
): boolean {
|
|
100
|
+
const aKeys = Object.keys(a);
|
|
101
|
+
const bKeys = Object.keys(b);
|
|
102
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
103
|
+
for (const key of aKeys) {
|
|
104
|
+
if (key === "children") continue;
|
|
105
|
+
if ((a as unknown as Record<string, unknown>)[key] !== (b as unknown as Record<string, unknown>)[key]) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function diffContentChildren(
|
|
113
|
+
prior: readonly BlockNode[],
|
|
114
|
+
next: readonly BlockNode[],
|
|
115
|
+
): ContentChildrenPatch {
|
|
116
|
+
if (prior === next) {
|
|
117
|
+
return { kind: "keep-all" };
|
|
118
|
+
}
|
|
119
|
+
if (prior.length === next.length) {
|
|
120
|
+
const entries: Array<{ index: number; block: BlockNode }> = [];
|
|
121
|
+
for (let i = 0; i < next.length; i += 1) {
|
|
122
|
+
const priorBlock = prior[i];
|
|
123
|
+
const nextBlock = next[i];
|
|
124
|
+
if (priorBlock === undefined || nextBlock === undefined) {
|
|
125
|
+
return { kind: "replace-all", children: next.slice() };
|
|
126
|
+
}
|
|
127
|
+
if (priorBlock !== nextBlock) {
|
|
128
|
+
entries.push({ index: i, block: nextBlock });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
return { kind: "keep-all" };
|
|
133
|
+
}
|
|
134
|
+
return { kind: "index-map", baseLength: prior.length, entries };
|
|
135
|
+
}
|
|
136
|
+
return { kind: "replace-all", children: next.slice() };
|
|
137
|
+
}
|
|
138
|
+
|
|
20
139
|
export type RuntimeCommandAppliedListener = (
|
|
21
140
|
command: EditorCommand,
|
|
22
141
|
transaction: EditorTransaction,
|
|
@@ -73,8 +192,13 @@ export function createRuntimeCollabSync(
|
|
|
73
192
|
return;
|
|
74
193
|
}
|
|
75
194
|
|
|
76
|
-
const
|
|
195
|
+
const broadcastCommand = compressDocumentReplaceForBroadcast(
|
|
77
196
|
command,
|
|
197
|
+
meta.priorDocument,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const event = createCommandEvent({
|
|
201
|
+
command: broadcastCommand,
|
|
78
202
|
originClientId: ydoc.clientID,
|
|
79
203
|
authorId,
|
|
80
204
|
timestamp: context.timestamp,
|
|
@@ -406,6 +406,7 @@ export interface DocumentRuntime {
|
|
|
406
406
|
export interface CommandAppliedMeta {
|
|
407
407
|
preSelection: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
408
408
|
activeStory: EditorStoryTarget;
|
|
409
|
+
priorDocument: CanonicalDocumentEnvelope;
|
|
409
410
|
}
|
|
410
411
|
|
|
411
412
|
export interface CreateDocumentRuntimeOptions {
|
|
@@ -2111,6 +2112,7 @@ export function createDocumentRuntime(
|
|
|
2111
2112
|
options.onCommandApplied?.(command, noopTransaction, context, {
|
|
2112
2113
|
preSelection: state.selection,
|
|
2113
2114
|
activeStory,
|
|
2115
|
+
priorDocument: state.document,
|
|
2114
2116
|
});
|
|
2115
2117
|
return;
|
|
2116
2118
|
}
|
|
@@ -2124,11 +2126,13 @@ export function createDocumentRuntime(
|
|
|
2124
2126
|
} as const;
|
|
2125
2127
|
const preSelection = commandSelection;
|
|
2126
2128
|
const preActiveStory = activeStory;
|
|
2129
|
+
const priorDocument = state.document;
|
|
2127
2130
|
const transaction = executeEditorCommand(state, command, context);
|
|
2128
2131
|
commit(transaction);
|
|
2129
2132
|
options.onCommandApplied?.(command, transaction, context, {
|
|
2130
2133
|
preSelection,
|
|
2131
2134
|
activeStory: preActiveStory,
|
|
2135
|
+
priorDocument,
|
|
2132
2136
|
});
|
|
2133
2137
|
} catch (error) {
|
|
2134
2138
|
emitError(toRuntimeError(error));
|
|
@@ -3283,12 +3287,14 @@ export function createDocumentRuntime(
|
|
|
3283
3287
|
|
|
3284
3288
|
const preSelection = selection;
|
|
3285
3289
|
const preActiveStory = activeStory;
|
|
3290
|
+
const priorDocument = state.document;
|
|
3286
3291
|
if (activeStory.kind === "main") {
|
|
3287
3292
|
const mainTransaction = executeEditorCommand(baseState, command, context);
|
|
3288
3293
|
commit(mainTransaction);
|
|
3289
3294
|
options.onCommandApplied?.(command, mainTransaction, context, {
|
|
3290
3295
|
preSelection,
|
|
3291
3296
|
activeStory: preActiveStory,
|
|
3297
|
+
priorDocument,
|
|
3292
3298
|
});
|
|
3293
3299
|
return classifyAck({
|
|
3294
3300
|
command,
|
|
@@ -3370,6 +3376,7 @@ export function createDocumentRuntime(
|
|
|
3370
3376
|
options.onCommandApplied?.(broadcastCommand, mergedTransaction, context, {
|
|
3371
3377
|
preSelection,
|
|
3372
3378
|
activeStory: preActiveStory,
|
|
3379
|
+
priorDocument,
|
|
3373
3380
|
});
|
|
3374
3381
|
return classifyAck({
|
|
3375
3382
|
command,
|
|
@@ -24,8 +24,26 @@
|
|
|
24
24
|
* internal cachedGraph + cachedKey without triggering fullRebuild.
|
|
25
25
|
* Does not change geometry — but the public interface changed, so
|
|
26
26
|
* persisted envelopes MUST re-derive their cacheKey under 3.
|
|
27
|
+
* 4 — PR #188 `fix(export)` bumped the version to satisfy the
|
|
28
|
+
* `src/runtime/layout/**` gate, even though the font-loader
|
|
29
|
+
* type-def restoration intended for that PR did not survive the
|
|
30
|
+
* squash-merge. Runtime and cached geometry unchanged.
|
|
31
|
+
* 5 — PR #187 `joakim/commentevents` restores the local `Minimal*`
|
|
32
|
+
* font-loader type defs under `src/runtime/layout/docx-font-loader.ts`
|
|
33
|
+
* (re-application of previously-shipped PRs #162/#163 which a
|
|
34
|
+
* subsequent merge reverted) so downstream consumers whose
|
|
35
|
+
* TS `lib` does not expose `FontFaceSet.add` can type-check the
|
|
36
|
+
* package, and adds the `comments_changed` event plumbing in
|
|
37
|
+
* runtime domains outside `src/runtime/layout/**`. TypeScript-
|
|
38
|
+
* surface-only from the layout engine's perspective: no cached
|
|
39
|
+
* geometry or cache-key derivation changes. The bump exists
|
|
40
|
+
* solely to satisfy the
|
|
41
|
+
* `scripts/ci-check-layout-engine-version.mjs` gate because a
|
|
42
|
+
* file under `src/runtime/layout/**` changed. Safe to treat
|
|
43
|
+
* versions 3, 4, and 5 as cache-compatible if a migration ever
|
|
44
|
+
* needs to collapse them.
|
|
27
45
|
*/
|
|
28
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
46
|
+
export const LAYOUT_ENGINE_VERSION = 5 as const;
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
49
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* as `adjustedRange`. The semantics: the union of all step post-edit ranges
|
|
4
4
|
* (where each step covers `[step.from, step.from + step.insertSize]` in the
|
|
5
5
|
* new document). When the transaction has no steps (e.g., a non-main-story
|
|
6
|
-
* `document.replace` that uses `createEmptyMapping()`),
|
|
7
|
-
* normalized prior selection range so consumers still
|
|
8
|
-
* pointer.
|
|
6
|
+
* `document.replace` or `document.patch` that uses `createEmptyMapping()`),
|
|
7
|
+
* fall back to the normalized prior selection range so consumers still
|
|
8
|
+
* receive a meaningful pointer.
|
|
9
9
|
*
|
|
10
10
|
* Note: only `step.from` and `step.insertSize` are considered; `step.to`
|
|
11
11
|
* (the pre-edit end of the replaced range) is intentionally ignored. A pure
|