@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { EditorCommand } from "../../core/commands/index.ts";
|
|
2
|
+
import type { SelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
3
|
+
import type { EditorStoryTarget } from "../../api/public-types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single delta event recorded in the shared collaboration event log.
|
|
7
|
+
*
|
|
8
|
+
* The collaboration model for the runtime layer is:
|
|
9
|
+
* `currentState = replay(baseDocxBytes, eventLog)`
|
|
10
|
+
*
|
|
11
|
+
* Every client loads the same base `.docx`, then the event log — stored as
|
|
12
|
+
* a `Y.Array<CommandEvent>` on the shared `Y.Doc` — is replayed through
|
|
13
|
+
* the runtime's `executeEditorCommand` pipeline to arrive at the current
|
|
14
|
+
* canonical document state. Because all mutations flow through the
|
|
15
|
+
* runtime, export/download and every other downstream read see the full,
|
|
16
|
+
* up-to-date state automatically.
|
|
17
|
+
*
|
|
18
|
+
* Events are meant to be small deltas (text insertions, comment adds,
|
|
19
|
+
* change accepts, etc.). They are NOT full-document snapshots.
|
|
20
|
+
*
|
|
21
|
+
* `document.replace` is a special case: some formatting/table/list
|
|
22
|
+
* operations currently produce `document.replace` commands that carry
|
|
23
|
+
* a full `CanonicalDocumentEnvelope`. These events are bandwidth-heavy
|
|
24
|
+
* but functionally correct. A follow-up will promote them to first-class
|
|
25
|
+
* delta commands.
|
|
26
|
+
*/
|
|
27
|
+
export interface CommandEvent {
|
|
28
|
+
/** UUID — dedup key for defensive idempotency. */
|
|
29
|
+
eventId: string;
|
|
30
|
+
/** `Y.Doc.clientID` of the originating client. */
|
|
31
|
+
originClientId: number;
|
|
32
|
+
/** User identity for attribution (e.g. `currentUser.userId`). */
|
|
33
|
+
authorId: string;
|
|
34
|
+
/** ISO-8601 UTC timestamp at origin. */
|
|
35
|
+
timestamp: string;
|
|
36
|
+
/** The exact `EditorCommand` that produced this delta. */
|
|
37
|
+
command: EditorCommand;
|
|
38
|
+
/** Context required for deterministic replay on remote clients. */
|
|
39
|
+
context: {
|
|
40
|
+
documentMode?: "editing" | "suggesting" | "viewing" | "commenting";
|
|
41
|
+
defaultAuthorId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Selection at the moment the command was dispatched on the origin
|
|
44
|
+
* client. Replay uses this to restore the caret before applying the
|
|
45
|
+
* command so text insertions, backspaces, etc. land at the position
|
|
46
|
+
* the author intended rather than the replaying client's current
|
|
47
|
+
* caret (which is arbitrary).
|
|
48
|
+
*/
|
|
49
|
+
preSelection?: SelectionSnapshot;
|
|
50
|
+
/**
|
|
51
|
+
* Active story at the moment the command was dispatched. Replay
|
|
52
|
+
* must target the same story (main, header, footer, note, ...) so
|
|
53
|
+
* mutations apply to the intended region.
|
|
54
|
+
*/
|
|
55
|
+
activeStory?: EditorStoryTarget;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Command types that are broadcast to all collaborators.
|
|
61
|
+
*
|
|
62
|
+
* Any command whose type is in this set represents a mutation to the
|
|
63
|
+
* shared document state (content, comments, tracked changes, review
|
|
64
|
+
* decisions). Local-only commands are intentionally excluded — see
|
|
65
|
+
* `LOCAL_ONLY_COMMAND_TYPES`.
|
|
66
|
+
*/
|
|
67
|
+
export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new Set<
|
|
68
|
+
EditorCommand["type"]
|
|
69
|
+
>([
|
|
70
|
+
"text.insert",
|
|
71
|
+
"text.delete-backward",
|
|
72
|
+
"text.delete-forward",
|
|
73
|
+
"text.insert-tab",
|
|
74
|
+
"text.insert-hard-break",
|
|
75
|
+
"paragraph.split",
|
|
76
|
+
"document.replace",
|
|
77
|
+
"comment.add",
|
|
78
|
+
"comment.resolve",
|
|
79
|
+
"comment.reopen",
|
|
80
|
+
"comment.add-reply",
|
|
81
|
+
"comment.edit-body",
|
|
82
|
+
"change.accept",
|
|
83
|
+
"change.reject",
|
|
84
|
+
"change.accept-all",
|
|
85
|
+
"change.reject-all",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Command types that are NEVER broadcast. These are intentionally
|
|
90
|
+
* per-client: cursor/selection state (handled by Awareness instead),
|
|
91
|
+
* UI focus, local warnings, and the local history stack.
|
|
92
|
+
*/
|
|
93
|
+
export const LOCAL_ONLY_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new Set<
|
|
94
|
+
EditorCommand["type"]
|
|
95
|
+
>([
|
|
96
|
+
"selection.set",
|
|
97
|
+
"comment.open",
|
|
98
|
+
"warning.add",
|
|
99
|
+
"warning.clear",
|
|
100
|
+
"history.undo",
|
|
101
|
+
"history.redo",
|
|
102
|
+
"runtime.focus",
|
|
103
|
+
"runtime.set-read-only",
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns `true` when the command is local-only and must NOT be broadcast
|
|
108
|
+
* on the shared collaboration event log.
|
|
109
|
+
*/
|
|
110
|
+
export function isLocalOnlyCommand(command: EditorCommand): boolean {
|
|
111
|
+
return LOCAL_ONLY_COMMAND_TYPES.has(command.type);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns `true` when the command must be broadcast on the shared event
|
|
116
|
+
* log. Defensive helper — equivalent to `!isLocalOnlyCommand(command)`
|
|
117
|
+
* for every known command type, but raises visibility if a new command
|
|
118
|
+
* type is added without being classified (both predicates would be
|
|
119
|
+
* `false`, which is easier to spot in tests).
|
|
120
|
+
*/
|
|
121
|
+
export function isBroadcastCommand(command: EditorCommand): boolean {
|
|
122
|
+
return BROADCAST_COMMAND_TYPES.has(command.type);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CreateCommandEventInput {
|
|
126
|
+
command: EditorCommand;
|
|
127
|
+
originClientId: number;
|
|
128
|
+
authorId: string;
|
|
129
|
+
timestamp: string;
|
|
130
|
+
context?: CommandEvent["context"];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build a `CommandEvent` to append to the shared event log. Callers are
|
|
135
|
+
* responsible for ensuring `command` is a broadcast-eligible command.
|
|
136
|
+
*/
|
|
137
|
+
export function createCommandEvent(input: CreateCommandEventInput): CommandEvent {
|
|
138
|
+
return {
|
|
139
|
+
eventId: generateEventId(),
|
|
140
|
+
originClientId: input.originClientId,
|
|
141
|
+
authorId: input.authorId,
|
|
142
|
+
timestamp: input.timestamp,
|
|
143
|
+
command: input.command,
|
|
144
|
+
context: {
|
|
145
|
+
documentMode: input.context?.documentMode,
|
|
146
|
+
defaultAuthorId: input.context?.defaultAuthorId,
|
|
147
|
+
preSelection: input.context?.preSelection,
|
|
148
|
+
activeStory: input.context?.activeStory,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let eventIdCounter = 0;
|
|
154
|
+
|
|
155
|
+
function generateEventId(): string {
|
|
156
|
+
// A short, collision-resistant-enough id for local dedup. Clients
|
|
157
|
+
// distinguish their own events by `originClientId` first; this id is
|
|
158
|
+
// a defensive fallback against replays of identical client-scoped
|
|
159
|
+
// sequences. No crypto requirements here.
|
|
160
|
+
eventIdCounter = (eventIdCounter + 1) >>> 0;
|
|
161
|
+
const random = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, "0");
|
|
162
|
+
const counter = eventIdCounter.toString(16).padStart(8, "0");
|
|
163
|
+
const time = Date.now().toString(16);
|
|
164
|
+
return `evt-${time}-${counter}-${random}`;
|
|
165
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createRuntimeCollabSync,
|
|
3
|
+
createRuntimeCommandAppliedBridge,
|
|
4
|
+
type RuntimeCollabSyncHandle,
|
|
5
|
+
type RuntimeCollabSyncOptions,
|
|
6
|
+
type RuntimeCommandAppliedBridge,
|
|
7
|
+
type RuntimeCommandAppliedListener,
|
|
8
|
+
} from "./runtime-collab-sync.ts";
|
|
9
|
+
export {
|
|
10
|
+
createCommandEvent,
|
|
11
|
+
isBroadcastCommand,
|
|
12
|
+
isLocalOnlyCommand,
|
|
13
|
+
type CommandEvent,
|
|
14
|
+
type CreateCommandEventInput,
|
|
15
|
+
} from "./event-types.ts";
|
|
16
|
+
export {
|
|
17
|
+
clearLocalCursorState,
|
|
18
|
+
getCursorColorForUser,
|
|
19
|
+
getRemoteCursorStates,
|
|
20
|
+
setLocalCursorState,
|
|
21
|
+
type RemoteCursorState,
|
|
22
|
+
} from "./remote-cursor-awareness.ts";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Awareness } from "y-protocols/awareness";
|
|
2
|
+
import type { EditorStoryTarget } from "../../api/public-types";
|
|
3
|
+
|
|
4
|
+
export interface RemoteCursorState {
|
|
5
|
+
userId: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
color: string;
|
|
8
|
+
anchor: number;
|
|
9
|
+
head: number;
|
|
10
|
+
storyTarget: EditorStoryTarget;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CURSOR_STATE_KEY = "cursor";
|
|
14
|
+
|
|
15
|
+
const CURSOR_COLORS = [
|
|
16
|
+
"#e11d48",
|
|
17
|
+
"#ea580c",
|
|
18
|
+
"#ca8a04",
|
|
19
|
+
"#16a34a",
|
|
20
|
+
"#0891b2",
|
|
21
|
+
"#2563eb",
|
|
22
|
+
"#7c3aed",
|
|
23
|
+
"#c026d3",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function hashUserId(userId: string): number {
|
|
27
|
+
let hash = 0;
|
|
28
|
+
for (let i = 0; i < userId.length; i++) {
|
|
29
|
+
const char = userId.charCodeAt(i);
|
|
30
|
+
hash = (hash << 5) - hash + char;
|
|
31
|
+
hash = hash & hash;
|
|
32
|
+
}
|
|
33
|
+
return Math.abs(hash);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getCursorColorForUser(userId: string): string {
|
|
37
|
+
const index = hashUserId(userId) % CURSOR_COLORS.length;
|
|
38
|
+
return CURSOR_COLORS[index];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setLocalCursorState(
|
|
42
|
+
awareness: Awareness,
|
|
43
|
+
state: RemoteCursorState,
|
|
44
|
+
): void {
|
|
45
|
+
awareness.setLocalStateField(CURSOR_STATE_KEY, state);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function clearLocalCursorState(awareness: Awareness): void {
|
|
49
|
+
const localState = awareness.getLocalState();
|
|
50
|
+
if (localState && CURSOR_STATE_KEY in localState) {
|
|
51
|
+
awareness.setLocalStateField(CURSOR_STATE_KEY, null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SAFE_HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
56
|
+
|
|
57
|
+
export function isSafeCursorColor(value: unknown): value is string {
|
|
58
|
+
return typeof value === "string" && SAFE_HEX_COLOR_PATTERN.test(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getRemoteCursorStates(
|
|
62
|
+
awareness: Awareness,
|
|
63
|
+
localClientId: number,
|
|
64
|
+
): RemoteCursorState[] {
|
|
65
|
+
const states = awareness.getStates();
|
|
66
|
+
const result: RemoteCursorState[] = [];
|
|
67
|
+
|
|
68
|
+
for (const [clientId, clientState] of states) {
|
|
69
|
+
if (clientId === localClientId) continue;
|
|
70
|
+
|
|
71
|
+
const cursorState = clientState[CURSOR_STATE_KEY];
|
|
72
|
+
if (!cursorState) continue;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
typeof cursorState.userId === "string" &&
|
|
76
|
+
typeof cursorState.displayName === "string" &&
|
|
77
|
+
typeof cursorState.anchor === "number" &&
|
|
78
|
+
typeof cursorState.head === "number" &&
|
|
79
|
+
cursorState.storyTarget &&
|
|
80
|
+
typeof cursorState.storyTarget.kind === "string"
|
|
81
|
+
) {
|
|
82
|
+
const safeColor = isSafeCursorColor(cursorState.color)
|
|
83
|
+
? cursorState.color
|
|
84
|
+
: getCursorColorForUser(cursorState.userId);
|
|
85
|
+
result.push({
|
|
86
|
+
...(cursorState as RemoteCursorState),
|
|
87
|
+
color: safeColor,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
CommandExecutionContext,
|
|
5
|
+
EditorCommand,
|
|
6
|
+
EditorTransaction,
|
|
7
|
+
} from "../../core/commands/index.ts";
|
|
8
|
+
import type {
|
|
9
|
+
CommandAppliedMeta,
|
|
10
|
+
DocumentRuntime,
|
|
11
|
+
Unsubscribe,
|
|
12
|
+
} from "../document-runtime.ts";
|
|
13
|
+
import {
|
|
14
|
+
createCommandEvent,
|
|
15
|
+
isBroadcastCommand,
|
|
16
|
+
isLocalOnlyCommand,
|
|
17
|
+
type CommandEvent,
|
|
18
|
+
} from "./event-types.ts";
|
|
19
|
+
|
|
20
|
+
export type RuntimeCommandAppliedListener = (
|
|
21
|
+
command: EditorCommand,
|
|
22
|
+
transaction: EditorTransaction,
|
|
23
|
+
context: CommandExecutionContext,
|
|
24
|
+
meta: CommandAppliedMeta,
|
|
25
|
+
) => void;
|
|
26
|
+
|
|
27
|
+
export interface RuntimeCommandAppliedBridge {
|
|
28
|
+
onCommandApplied: RuntimeCommandAppliedListener;
|
|
29
|
+
subscribe(listener: RuntimeCommandAppliedListener): Unsubscribe;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RuntimeCollabSyncOptions {
|
|
33
|
+
ydoc: Y.Doc;
|
|
34
|
+
runtime: DocumentRuntime;
|
|
35
|
+
authorId: string;
|
|
36
|
+
commandAppliedBridge: RuntimeCommandAppliedBridge;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RuntimeCollabSyncHandle {
|
|
40
|
+
destroy(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createRuntimeCommandAppliedBridge(): RuntimeCommandAppliedBridge {
|
|
44
|
+
const listeners = new Set<RuntimeCommandAppliedListener>();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
onCommandApplied(command, transaction, context, meta) {
|
|
48
|
+
for (const listener of [...listeners]) {
|
|
49
|
+
listener(command, transaction, context, meta);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
listeners.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createRuntimeCollabSync(
|
|
62
|
+
options: RuntimeCollabSyncOptions,
|
|
63
|
+
): RuntimeCollabSyncHandle {
|
|
64
|
+
const { ydoc, runtime, authorId, commandAppliedBridge } = options;
|
|
65
|
+
const yEvents = ydoc.getArray<CommandEvent>("commandEvents");
|
|
66
|
+
const appliedEventIds = new Set<string>();
|
|
67
|
+
|
|
68
|
+
const unsubscribeCommandApplied = commandAppliedBridge.subscribe((command, _transaction, context, meta) => {
|
|
69
|
+
if (isLocalOnlyCommand(command)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!isBroadcastCommand(command)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const event = createCommandEvent({
|
|
77
|
+
command,
|
|
78
|
+
originClientId: ydoc.clientID,
|
|
79
|
+
authorId,
|
|
80
|
+
timestamp: context.timestamp,
|
|
81
|
+
context: {
|
|
82
|
+
documentMode: context.documentMode,
|
|
83
|
+
defaultAuthorId: context.defaultAuthorId,
|
|
84
|
+
preSelection: meta.preSelection,
|
|
85
|
+
activeStory: meta.activeStory,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
appliedEventIds.add(event.eventId);
|
|
90
|
+
yEvents.push([event]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
yEvents.observe(onYEventsChange);
|
|
94
|
+
|
|
95
|
+
for (const value of yEvents.toArray()) {
|
|
96
|
+
applyStartupEvent(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
destroy() {
|
|
101
|
+
unsubscribeCommandApplied();
|
|
102
|
+
yEvents.unobserve(onYEventsChange);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function onYEventsChange(event: Y.YArrayEvent<CommandEvent>): void {
|
|
107
|
+
for (const delta of event.changes.delta) {
|
|
108
|
+
if (!Array.isArray(delta.insert)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const value of delta.insert) {
|
|
113
|
+
applyObservedEvent(value);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function applyStartupEvent(value: unknown): void {
|
|
119
|
+
const event = asCommandEvent(value);
|
|
120
|
+
if (!event) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (appliedEventIds.has(event.eventId)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!isReplayableEvent(event)) {
|
|
127
|
+
appliedEventIds.add(event.eventId);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
appliedEventIds.add(event.eventId);
|
|
132
|
+
applyEventToRuntime(event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function applyObservedEvent(value: unknown): void {
|
|
136
|
+
const event = asCommandEvent(value);
|
|
137
|
+
if (!event) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (appliedEventIds.has(event.eventId)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!isReplayableEvent(event)) {
|
|
144
|
+
appliedEventIds.add(event.eventId);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
appliedEventIds.add(event.eventId);
|
|
149
|
+
if (event.originClientId === ydoc.clientID) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
applyEventToRuntime(event);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isReplayableEvent(event: CommandEvent): boolean {
|
|
156
|
+
if (isLocalOnlyCommand(event.command)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (!isBroadcastCommand(event.command)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyEventToRuntime(event: CommandEvent): void {
|
|
166
|
+
runtime.applyRemoteCommand(
|
|
167
|
+
event.command,
|
|
168
|
+
{
|
|
169
|
+
timestamp: event.timestamp,
|
|
170
|
+
documentMode: event.context.documentMode,
|
|
171
|
+
defaultAuthorId: event.context.defaultAuthorId ?? event.authorId,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
preSelection: event.context.preSelection,
|
|
175
|
+
activeStory: event.context.activeStory,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asCommandEvent(value: unknown): CommandEvent | null {
|
|
182
|
+
if (!isRecord(value)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value.eventId !== "string") {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value.originClientId !== "number" || !Number.isFinite(value.originClientId)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (typeof value.authorId !== "string") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const timestamp = normalizeTimestamp(value.timestamp);
|
|
195
|
+
if (timestamp === null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const command = asEditorCommand(value.command);
|
|
200
|
+
const context = asCommandEventContext(value.context);
|
|
201
|
+
if (!command || !context) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
eventId: value.eventId,
|
|
207
|
+
originClientId: value.originClientId,
|
|
208
|
+
authorId: value.authorId,
|
|
209
|
+
timestamp,
|
|
210
|
+
command,
|
|
211
|
+
context,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeTimestamp(value: unknown): string | null {
|
|
216
|
+
if (typeof value === "string" && value.length > 0) {
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
220
|
+
return new Date(value).toISOString();
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function asEditorCommand(value: unknown): EditorCommand | null {
|
|
226
|
+
if (!isRecord(value)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (typeof value.type !== "string") {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return value as EditorCommand;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function asCommandEventContext(value: unknown): CommandEvent["context"] | null {
|
|
236
|
+
if (!isRecord(value)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
documentMode: isCommandDocumentMode(value.documentMode) ? value.documentMode : undefined,
|
|
242
|
+
defaultAuthorId: typeof value.defaultAuthorId === "string" ? value.defaultAuthorId : undefined,
|
|
243
|
+
preSelection: asSelectionSnapshot(value.preSelection),
|
|
244
|
+
activeStory: asStoryTarget(value.activeStory),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function asSelectionSnapshot(value: unknown): CommandEvent["context"]["preSelection"] {
|
|
249
|
+
if (!isRecord(value)) return undefined;
|
|
250
|
+
if (typeof value.anchor !== "number" || typeof value.head !== "number") return undefined;
|
|
251
|
+
if (typeof value.isCollapsed !== "boolean") return undefined;
|
|
252
|
+
if (!isRecord(value.activeRange)) return undefined;
|
|
253
|
+
return value as unknown as CommandEvent["context"]["preSelection"];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function asStoryTarget(value: unknown): CommandEvent["context"]["activeStory"] {
|
|
257
|
+
if (!isRecord(value)) return undefined;
|
|
258
|
+
if (typeof value.kind !== "string") return undefined;
|
|
259
|
+
return value as unknown as CommandEvent["context"]["activeStory"];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isCommandDocumentMode(
|
|
263
|
+
value: unknown,
|
|
264
|
+
): value is NonNullable<CommandEvent["context"]["documentMode"]> {
|
|
265
|
+
return value === "editing"
|
|
266
|
+
|| value === "suggesting"
|
|
267
|
+
|| value === "viewing"
|
|
268
|
+
|| value === "commenting";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
272
|
+
return typeof value === "object" && value !== null;
|
|
273
|
+
}
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
SubPartsCatalog,
|
|
16
16
|
} from "../model/canonical-document.ts";
|
|
17
17
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
18
|
+
import { resolveDefaultPageSizeTwips } from "./layout/default-page-format.ts";
|
|
18
19
|
import {
|
|
19
20
|
resolveSectionVariants,
|
|
20
21
|
sectionSupportsStoryTarget,
|
|
@@ -88,9 +89,10 @@ export function buildPageLayoutSnapshot(
|
|
|
88
89
|
properties: SectionProperties | undefined,
|
|
89
90
|
subParts: SubPartsCatalog | undefined,
|
|
90
91
|
): PageLayoutSnapshot {
|
|
92
|
+
const defaultSize = resolveDefaultPageSizeTwips();
|
|
91
93
|
const pageSize = properties?.pageSize ?? {
|
|
92
|
-
width:
|
|
93
|
-
height:
|
|
94
|
+
width: defaultSize.widthTwips,
|
|
95
|
+
height: defaultSize.heightTwips,
|
|
94
96
|
orientation: "portrait" as const,
|
|
95
97
|
};
|
|
96
98
|
const margins = properties?.pageMargins ?? {
|
|
@@ -206,7 +206,7 @@ function headingLevelFromStyleId(styleId?: string): number | null {
|
|
|
206
206
|
return null;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
function buildHeadingOutline(
|
|
209
|
+
export function buildHeadingOutline(
|
|
210
210
|
document: CanonicalDocumentEnvelope,
|
|
211
211
|
mainSurface: EditorSurfaceSnapshot,
|
|
212
212
|
sections: ResolvedDocumentSection[],
|