@echovisionlab/geul-common 0.1.0
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/LICENSE.md +6 -0
- package/README.md +37 -0
- package/package.json +89 -0
- package/src/collaboration/artist.ts +63 -0
- package/src/collaboration/block-room-codec/ai-document-applicator.ts +319 -0
- package/src/collaboration/block-room-codec/ai-document-field-mutations.ts +557 -0
- package/src/collaboration/block-room-codec/ai-document-page-structure-mutations.ts +449 -0
- package/src/collaboration/block-room-codec/ai-document-values.ts +368 -0
- package/src/collaboration/block-room-codec/hydration.ts +257 -0
- package/src/collaboration/block-room-codec/internal.ts +432 -0
- package/src/collaboration/block-room-codec/locale-change-validation.ts +237 -0
- package/src/collaboration/block-room-codec/locale-presence.ts +827 -0
- package/src/collaboration/block-room-codec/materialization.ts +450 -0
- package/src/collaboration/block-room-codec/observation.ts +518 -0
- package/src/collaboration/block-room-codec/payload-mutations.ts +347 -0
- package/src/collaboration/block-room-codec/room-access.ts +154 -0
- package/src/collaboration/block-room-codec/structure-mutations.ts +456 -0
- package/src/collaboration/block-room-codec.ts +86 -0
- package/src/collaboration/campaign.ts +37 -0
- package/src/collaboration/document-layout.ts +75 -0
- package/src/collaboration/document.ts +185 -0
- package/src/collaboration/email-layout.ts +150 -0
- package/src/collaboration/form.ts +513 -0
- package/src/collaboration/label.ts +49 -0
- package/src/collaboration/map-theme.ts +157 -0
- package/src/collaboration/member-id.ts +9 -0
- package/src/collaboration/menu.ts +258 -0
- package/src/collaboration/metadata-ai.ts +99 -0
- package/src/collaboration/page.ts +483 -0
- package/src/collaboration/post-series.ts +96 -0
- package/src/collaboration/post.ts +59 -0
- package/src/collaboration/release.ts +221 -0
- package/src/collaboration/runtime-events.ts +547 -0
- package/src/collaboration/work.ts +107 -0
- package/src/editor/link-normalization.ts +212 -0
- package/src/editor/materialized-blocks.ts +58 -0
- package/src/index.ts +22 -0
- package/src/media/block-schemas.ts +78 -0
- package/src/media/hydration.ts +226 -0
- package/src/page/block-fixtures.ts +586 -0
- package/src/page/index.ts +3 -0
- package/src/page/types.ts +57 -0
- package/src/post/index.ts +1 -0
- package/src/post/types.ts +13 -0
- package/src/test/random-id.ts +9 -0
- package/src/translation/release.ts +88 -0
- package/src/types.ts +14 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { toJson, type JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import {
|
|
3
|
+
richTextBlockKindByProtoCase,
|
|
4
|
+
type RichTextBlockKind,
|
|
5
|
+
} from "@echovisionlab/geul-proto/content/block_catalog.ts";
|
|
6
|
+
import {
|
|
7
|
+
RichTextBlockDataSchema,
|
|
8
|
+
RichTextBlockLocaleDataSchema,
|
|
9
|
+
type RichTextBlockData,
|
|
10
|
+
type RichTextBlockLocaleData,
|
|
11
|
+
} from "@echovisionlab/geul-proto/content/block_content_pb.ts";
|
|
12
|
+
import * as Y from "yjs";
|
|
13
|
+
import {
|
|
14
|
+
arrayPath,
|
|
15
|
+
fail,
|
|
16
|
+
fromYValue,
|
|
17
|
+
isCollaborativeTextPath,
|
|
18
|
+
jsonObject,
|
|
19
|
+
setNodePayload,
|
|
20
|
+
toYValue,
|
|
21
|
+
} from "./internal.ts";
|
|
22
|
+
import { blockRoomLocaleOverlay } from "./materialization.ts";
|
|
23
|
+
import {
|
|
24
|
+
nodeKind,
|
|
25
|
+
nodeTextPredicate,
|
|
26
|
+
payloadArray,
|
|
27
|
+
payloadParent,
|
|
28
|
+
payloadValue,
|
|
29
|
+
roomNode,
|
|
30
|
+
type BlockRoomMutationOptions,
|
|
31
|
+
type BlockRoomNodeRef,
|
|
32
|
+
type BlockRoomPayloadRef,
|
|
33
|
+
roomLocaleRole,
|
|
34
|
+
} from "./room-access.ts";
|
|
35
|
+
import {
|
|
36
|
+
deleteBlockRoomLocalePresenceForBlocks,
|
|
37
|
+
markAllBlockRoomLocaleValuesPresentForBlock,
|
|
38
|
+
} from "./locale-presence.ts";
|
|
39
|
+
|
|
40
|
+
export type BlockRoomAtomicValue = string | number | boolean | null;
|
|
41
|
+
|
|
42
|
+
export function getBlockRoomCollaborativeText(
|
|
43
|
+
yDocument: Y.Doc,
|
|
44
|
+
ref: BlockRoomPayloadRef,
|
|
45
|
+
): Y.Text {
|
|
46
|
+
const node = roomNode(yDocument, ref);
|
|
47
|
+
if (!isCollaborativeTextPath(ref.family, nodeKind(node, ref), ref.path)) {
|
|
48
|
+
return fail(`node:${ref.id}:not_collaborative_text:${ref.path}`);
|
|
49
|
+
}
|
|
50
|
+
const value = payloadValue(node, ref.path, `node:${ref.id}:path:${ref.path}`);
|
|
51
|
+
return value instanceof Y.Text
|
|
52
|
+
? value
|
|
53
|
+
: fail(`node:${ref.id}:text_shape:${ref.path}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function replaceBlockRoomCollaborativeText(
|
|
57
|
+
yDocument: Y.Doc,
|
|
58
|
+
ref: BlockRoomPayloadRef,
|
|
59
|
+
value: string,
|
|
60
|
+
options: BlockRoomMutationOptions = {},
|
|
61
|
+
): void {
|
|
62
|
+
yDocument.transact(() => {
|
|
63
|
+
const node = roomNode(yDocument, ref);
|
|
64
|
+
if (!isCollaborativeTextPath(ref.family, nodeKind(node, ref), ref.path)) {
|
|
65
|
+
fail(`node:${ref.id}:not_collaborative_text:${ref.path}`);
|
|
66
|
+
}
|
|
67
|
+
const { parent, key } = payloadParent(
|
|
68
|
+
node,
|
|
69
|
+
ref.path,
|
|
70
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
71
|
+
);
|
|
72
|
+
const textParent = parent as Y.Map<unknown>;
|
|
73
|
+
const textKey = key as string;
|
|
74
|
+
const existing = textParent.get(textKey);
|
|
75
|
+
const text = existing === undefined ? new Y.Text() : existing;
|
|
76
|
+
if (!(text instanceof Y.Text))
|
|
77
|
+
fail(`node:${ref.id}:text_shape:${ref.path}`);
|
|
78
|
+
if (existing === undefined) textParent.set(textKey, text);
|
|
79
|
+
if (text.length > 0) text.delete(0, text.length);
|
|
80
|
+
if (value) text.insert(0, value);
|
|
81
|
+
}, options.origin);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getBlockRoomAtomicValue(
|
|
85
|
+
yDocument: Y.Doc,
|
|
86
|
+
ref: BlockRoomPayloadRef,
|
|
87
|
+
): BlockRoomAtomicValue {
|
|
88
|
+
const node = roomNode(yDocument, ref);
|
|
89
|
+
if (isCollaborativeTextPath(ref.family, nodeKind(node, ref), ref.path)) {
|
|
90
|
+
return fail(`node:${ref.id}:collaborative_text:${ref.path}`);
|
|
91
|
+
}
|
|
92
|
+
const value = payloadValue(node, ref.path, `node:${ref.id}:path:${ref.path}`);
|
|
93
|
+
if (
|
|
94
|
+
value === null ||
|
|
95
|
+
typeof value === "string" ||
|
|
96
|
+
typeof value === "boolean" ||
|
|
97
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
98
|
+
)
|
|
99
|
+
return value;
|
|
100
|
+
return fail(`node:${ref.id}:not_atomic:${ref.path}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function setBlockRoomAtomicValue(
|
|
104
|
+
yDocument: Y.Doc,
|
|
105
|
+
ref: BlockRoomPayloadRef,
|
|
106
|
+
value: BlockRoomAtomicValue,
|
|
107
|
+
options: BlockRoomMutationOptions = {},
|
|
108
|
+
): void {
|
|
109
|
+
yDocument.transact(() => {
|
|
110
|
+
const node = roomNode(yDocument, ref);
|
|
111
|
+
if (isCollaborativeTextPath(ref.family, nodeKind(node, ref), ref.path)) {
|
|
112
|
+
fail(`node:${ref.id}:collaborative_text:${ref.path}`);
|
|
113
|
+
}
|
|
114
|
+
const { parent, key } = payloadParent(
|
|
115
|
+
node,
|
|
116
|
+
ref.path,
|
|
117
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
118
|
+
);
|
|
119
|
+
if (parent instanceof Y.Map && typeof key === "string") {
|
|
120
|
+
parent.set(key, value);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const array = parent as Y.Array<unknown>;
|
|
124
|
+
const index = key as number;
|
|
125
|
+
if (index < 0 || index >= array.length)
|
|
126
|
+
fail(`node:${ref.id}:path:${ref.path}`);
|
|
127
|
+
array.delete(index, 1);
|
|
128
|
+
array.insert(index, [value]);
|
|
129
|
+
}, options.origin);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function deleteBlockRoomAtomicValue(
|
|
133
|
+
yDocument: Y.Doc,
|
|
134
|
+
ref: BlockRoomPayloadRef,
|
|
135
|
+
options: BlockRoomMutationOptions = {},
|
|
136
|
+
): void {
|
|
137
|
+
yDocument.transact(() => {
|
|
138
|
+
const node = roomNode(yDocument, ref);
|
|
139
|
+
if (isCollaborativeTextPath(ref.family, nodeKind(node, ref), ref.path)) {
|
|
140
|
+
fail(`node:${ref.id}:collaborative_text:${ref.path}`);
|
|
141
|
+
}
|
|
142
|
+
const { parent, key } = payloadParent(
|
|
143
|
+
node,
|
|
144
|
+
ref.path,
|
|
145
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
146
|
+
);
|
|
147
|
+
if (parent instanceof Y.Map && typeof key === "string") {
|
|
148
|
+
if (!parent.has(key)) fail(`node:${ref.id}:path:${ref.path}`);
|
|
149
|
+
parent.delete(key);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const array = parent as Y.Array<unknown>;
|
|
153
|
+
const index = key as number;
|
|
154
|
+
if (index < 0 || index >= array.length)
|
|
155
|
+
fail(`node:${ref.id}:path:${ref.path}`);
|
|
156
|
+
array.delete(index, 1);
|
|
157
|
+
}, options.origin);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function payloadArrayForReplacement(
|
|
161
|
+
node: Y.Map<unknown>,
|
|
162
|
+
path: string,
|
|
163
|
+
reason: string,
|
|
164
|
+
): Y.Array<unknown> {
|
|
165
|
+
const { parent, key } = payloadParent(node, path, reason);
|
|
166
|
+
if (!(parent instanceof Y.Map) || typeof key !== "string") {
|
|
167
|
+
return payloadArray(node, path, reason);
|
|
168
|
+
}
|
|
169
|
+
const existing = parent.get(key);
|
|
170
|
+
if (existing instanceof Y.Array) return existing;
|
|
171
|
+
if (existing !== undefined) return fail(reason);
|
|
172
|
+
const array = new Y.Array<unknown>();
|
|
173
|
+
parent.set(key, array);
|
|
174
|
+
return array;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function insertBlockRoomPayloadArrayItem(
|
|
178
|
+
yDocument: Y.Doc,
|
|
179
|
+
ref: BlockRoomPayloadRef,
|
|
180
|
+
index: number,
|
|
181
|
+
value: JsonValue,
|
|
182
|
+
options: BlockRoomMutationOptions = {},
|
|
183
|
+
): void {
|
|
184
|
+
yDocument.transact(() => {
|
|
185
|
+
const node = roomNode(yDocument, ref);
|
|
186
|
+
const array = payloadArray(
|
|
187
|
+
node,
|
|
188
|
+
ref.path,
|
|
189
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
190
|
+
);
|
|
191
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > array.length) {
|
|
192
|
+
fail(`node:${ref.id}:array_index:${ref.path}`);
|
|
193
|
+
}
|
|
194
|
+
array.insert(index, [
|
|
195
|
+
toYValue(value, arrayPath(ref.path, index), nodeTextPredicate(node, ref)),
|
|
196
|
+
]);
|
|
197
|
+
}, options.origin);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function replaceBlockRoomPayloadArray(
|
|
201
|
+
yDocument: Y.Doc,
|
|
202
|
+
ref: BlockRoomPayloadRef,
|
|
203
|
+
values: readonly JsonValue[],
|
|
204
|
+
options: BlockRoomMutationOptions = {},
|
|
205
|
+
): void {
|
|
206
|
+
yDocument.transact(() => {
|
|
207
|
+
const node = roomNode(yDocument, ref);
|
|
208
|
+
const array = payloadArrayForReplacement(
|
|
209
|
+
node,
|
|
210
|
+
ref.path,
|
|
211
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
212
|
+
);
|
|
213
|
+
if (array.length > 0) array.delete(0, array.length);
|
|
214
|
+
if (values.length > 0) {
|
|
215
|
+
array.insert(
|
|
216
|
+
0,
|
|
217
|
+
values.map((value, index) =>
|
|
218
|
+
toYValue(
|
|
219
|
+
value,
|
|
220
|
+
arrayPath(ref.path, index),
|
|
221
|
+
nodeTextPredicate(node, ref),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}, options.origin);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function deleteBlockRoomPayloadArrayItem(
|
|
230
|
+
yDocument: Y.Doc,
|
|
231
|
+
ref: BlockRoomPayloadRef,
|
|
232
|
+
index: number,
|
|
233
|
+
options: BlockRoomMutationOptions = {},
|
|
234
|
+
): void {
|
|
235
|
+
yDocument.transact(() => {
|
|
236
|
+
const node = roomNode(yDocument, ref);
|
|
237
|
+
const array = payloadArray(
|
|
238
|
+
node,
|
|
239
|
+
ref.path,
|
|
240
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
241
|
+
);
|
|
242
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= array.length) {
|
|
243
|
+
fail(`node:${ref.id}:array_index:${ref.path}`);
|
|
244
|
+
}
|
|
245
|
+
array.delete(index, 1);
|
|
246
|
+
}, options.origin);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function moveBlockRoomPayloadArrayItem(
|
|
250
|
+
yDocument: Y.Doc,
|
|
251
|
+
ref: BlockRoomPayloadRef,
|
|
252
|
+
fromIndex: number,
|
|
253
|
+
toIndex: number,
|
|
254
|
+
options: BlockRoomMutationOptions = {},
|
|
255
|
+
): void {
|
|
256
|
+
yDocument.transact(() => {
|
|
257
|
+
const node = roomNode(yDocument, ref);
|
|
258
|
+
const array = payloadArray(
|
|
259
|
+
node,
|
|
260
|
+
ref.path,
|
|
261
|
+
`node:${ref.id}:path:${ref.path}`,
|
|
262
|
+
);
|
|
263
|
+
if (
|
|
264
|
+
!Number.isSafeInteger(fromIndex) ||
|
|
265
|
+
!Number.isSafeInteger(toIndex) ||
|
|
266
|
+
fromIndex < 0 ||
|
|
267
|
+
fromIndex >= array.length ||
|
|
268
|
+
toIndex < 0 ||
|
|
269
|
+
toIndex >= array.length
|
|
270
|
+
)
|
|
271
|
+
fail(`node:${ref.id}:array_index:${ref.path}`);
|
|
272
|
+
if (fromIndex === toIndex) return;
|
|
273
|
+
const value = fromYValue(array.get(fromIndex));
|
|
274
|
+
array.delete(fromIndex, 1);
|
|
275
|
+
array.insert(toIndex, [
|
|
276
|
+
toYValue(
|
|
277
|
+
value,
|
|
278
|
+
arrayPath(ref.path, toIndex),
|
|
279
|
+
nodeTextPredicate(node, ref),
|
|
280
|
+
),
|
|
281
|
+
]);
|
|
282
|
+
}, options.origin);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function oneofData(
|
|
286
|
+
value: JsonValue,
|
|
287
|
+
reason: string,
|
|
288
|
+
): { kind: string; payload: JsonValue } {
|
|
289
|
+
const object = jsonObject(value, reason);
|
|
290
|
+
const cases = Object.keys(object);
|
|
291
|
+
if (cases.length !== 1) fail(`${reason}:invalid_oneof`);
|
|
292
|
+
const kind = cases[0]!;
|
|
293
|
+
return { kind, payload: object[kind]! };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ReplaceRichTextBlockDataOptions extends BlockRoomMutationOptions {
|
|
297
|
+
expectedKind: RichTextBlockKind;
|
|
298
|
+
localeData: RichTextBlockLocaleData | null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function replaceRichTextBlockData(
|
|
302
|
+
yDocument: Y.Doc,
|
|
303
|
+
id: string,
|
|
304
|
+
data: RichTextBlockData,
|
|
305
|
+
options: ReplaceRichTextBlockDataOptions,
|
|
306
|
+
): void {
|
|
307
|
+
yDocument.transact(() => {
|
|
308
|
+
const ref: BlockRoomNodeRef = { id, family: "rich_text" };
|
|
309
|
+
const node = roomNode(yDocument, ref);
|
|
310
|
+
const currentKind = nodeKind(node, ref);
|
|
311
|
+
const currentCatalogKind =
|
|
312
|
+
richTextBlockKindByProtoCase[
|
|
313
|
+
currentKind as keyof typeof richTextBlockKindByProtoCase
|
|
314
|
+
];
|
|
315
|
+
if (currentCatalogKind !== options.expectedKind)
|
|
316
|
+
fail(`node:${id}:kind_changed`);
|
|
317
|
+
const base = oneofData(
|
|
318
|
+
toJson(RichTextBlockDataSchema, data),
|
|
319
|
+
`node:${id}:base_data`,
|
|
320
|
+
);
|
|
321
|
+
const parsedLocale =
|
|
322
|
+
options.localeData === null
|
|
323
|
+
? null
|
|
324
|
+
: oneofData(
|
|
325
|
+
toJson(RichTextBlockLocaleDataSchema, options.localeData),
|
|
326
|
+
`node:${id}:locale_data`,
|
|
327
|
+
);
|
|
328
|
+
if (parsedLocale && parsedLocale.kind !== base.kind)
|
|
329
|
+
fail(`node:${id}:locale_kind`);
|
|
330
|
+
const sourceRoom = roomLocaleRole(yDocument) === "source";
|
|
331
|
+
if (sourceRoom)
|
|
332
|
+
deleteBlockRoomLocalePresenceForBlocks(yDocument, new Set([id]));
|
|
333
|
+
setNodePayload(node, "rich_text", base.kind, base.payload);
|
|
334
|
+
const localeNodes = blockRoomLocaleOverlay(yDocument);
|
|
335
|
+
localeNodes.delete(id);
|
|
336
|
+
if (!parsedLocale) return;
|
|
337
|
+
const localeNode = new Y.Map<unknown>();
|
|
338
|
+
setNodePayload(
|
|
339
|
+
localeNode,
|
|
340
|
+
"rich_text",
|
|
341
|
+
parsedLocale.kind,
|
|
342
|
+
parsedLocale.payload,
|
|
343
|
+
);
|
|
344
|
+
localeNodes.set(id, localeNode);
|
|
345
|
+
if (sourceRoom) markAllBlockRoomLocaleValuesPresentForBlock(yDocument, id);
|
|
346
|
+
}, options.origin);
|
|
347
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import {
|
|
3
|
+
BLOCK_ROOM_ROOT,
|
|
4
|
+
blockRoomDocumentTypes,
|
|
5
|
+
fail,
|
|
6
|
+
isCollaborativeTextPath,
|
|
7
|
+
stringValue,
|
|
8
|
+
yMap,
|
|
9
|
+
type BlockRoomDocumentType,
|
|
10
|
+
type BlockRoomLocaleRole,
|
|
11
|
+
type BlockRoomNodeFamily,
|
|
12
|
+
type CollaborativeTextPredicate,
|
|
13
|
+
} from "./internal.ts";
|
|
14
|
+
import {
|
|
15
|
+
blockRoomBaseNodes,
|
|
16
|
+
blockRoomLocaleOverlay,
|
|
17
|
+
} from "./materialization.ts";
|
|
18
|
+
|
|
19
|
+
export interface BlockRoomNodeRef {
|
|
20
|
+
id: string;
|
|
21
|
+
family: BlockRoomNodeFamily;
|
|
22
|
+
locale?: true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BlockRoomPayloadRef extends BlockRoomNodeRef {
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BlockRoomMutationOptions {
|
|
30
|
+
origin?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BlockRoomAnchoredMutationOptions extends BlockRoomMutationOptions {
|
|
34
|
+
anchor?: Y.RelativePosition;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function roomDocumentType(yDocument: Y.Doc): BlockRoomDocumentType {
|
|
38
|
+
const value = stringValue(
|
|
39
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get("documentType"),
|
|
40
|
+
"document_type",
|
|
41
|
+
);
|
|
42
|
+
if (!blockRoomDocumentTypes.has(value as BlockRoomDocumentType)) {
|
|
43
|
+
return fail("document_type");
|
|
44
|
+
}
|
|
45
|
+
return value as BlockRoomDocumentType;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function roomSourceLocale(yDocument: Y.Doc): string {
|
|
49
|
+
return stringValue(
|
|
50
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get("sourceLocale"),
|
|
51
|
+
"source_locale",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function roomLocale(yDocument: Y.Doc): string {
|
|
56
|
+
return stringValue(
|
|
57
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get("roomLocale"),
|
|
58
|
+
"room_locale",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function roomLocaleRole(yDocument: Y.Doc): BlockRoomLocaleRole {
|
|
63
|
+
return roomLocale(yDocument) === roomSourceLocale(yDocument)
|
|
64
|
+
? "source"
|
|
65
|
+
: "target";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function roomNode(
|
|
69
|
+
yDocument: Y.Doc,
|
|
70
|
+
ref: BlockRoomNodeRef,
|
|
71
|
+
): Y.Map<unknown> {
|
|
72
|
+
const nodes = ref.locale
|
|
73
|
+
? blockRoomLocaleOverlay(yDocument)
|
|
74
|
+
: blockRoomBaseNodes(yDocument);
|
|
75
|
+
const node = yMap(nodes.get(ref.id), `node:${ref.id}:missing`);
|
|
76
|
+
const actualFamily = stringValue(node.get("family"), `node:${ref.id}:family`);
|
|
77
|
+
if (actualFamily !== ref.family) fail(`node:${ref.id}:family`);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type PayloadPathPart = string | number;
|
|
82
|
+
|
|
83
|
+
function parsePayloadPath(path: string): PayloadPathPart[] {
|
|
84
|
+
if (!path) return fail("payload_path:empty");
|
|
85
|
+
const result: PayloadPathPart[] = [];
|
|
86
|
+
for (const segment of path.split(".")) {
|
|
87
|
+
const match = /^([A-Za-z][A-Za-z0-9]*)(?:\[(\d+)\])?$/.exec(segment);
|
|
88
|
+
if (!match) fail(`payload_path:invalid:${path}`);
|
|
89
|
+
result.push(match[1]!);
|
|
90
|
+
if (match[2] !== undefined) result.push(Number(match[2]));
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function childAtPath(
|
|
96
|
+
value: unknown,
|
|
97
|
+
part: PayloadPathPart,
|
|
98
|
+
reason: string,
|
|
99
|
+
): unknown {
|
|
100
|
+
if (typeof part === "string") return yMap(value, reason).get(part);
|
|
101
|
+
const array = value instanceof Y.Array ? value : fail(reason);
|
|
102
|
+
if (part < 0 || part >= array.length) return fail(reason);
|
|
103
|
+
return array.get(part);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function payloadValue(
|
|
107
|
+
node: Y.Map<unknown>,
|
|
108
|
+
path: string,
|
|
109
|
+
reason: string,
|
|
110
|
+
): unknown {
|
|
111
|
+
let value: unknown = node.get("payload");
|
|
112
|
+
for (const part of parsePayloadPath(path)) {
|
|
113
|
+
value = childAtPath(value, part, reason);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function payloadParent(
|
|
119
|
+
node: Y.Map<unknown>,
|
|
120
|
+
path: string,
|
|
121
|
+
reason: string,
|
|
122
|
+
): { parent: Y.Map<unknown> | Y.Array<unknown>; key: PayloadPathPart } {
|
|
123
|
+
const parts = parsePayloadPath(path);
|
|
124
|
+
const key = parts.pop()!;
|
|
125
|
+
let value: unknown = node.get("payload");
|
|
126
|
+
for (const part of parts) value = childAtPath(value, part, reason);
|
|
127
|
+
if (!(value instanceof Y.Map) && !(value instanceof Y.Array))
|
|
128
|
+
return fail(reason);
|
|
129
|
+
if (typeof key === "string" && !(value instanceof Y.Map)) return fail(reason);
|
|
130
|
+
if (typeof key === "number" && !(value instanceof Y.Array))
|
|
131
|
+
return fail(reason);
|
|
132
|
+
return { parent: value, key };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function payloadArray(
|
|
136
|
+
node: Y.Map<unknown>,
|
|
137
|
+
path: string,
|
|
138
|
+
reason: string,
|
|
139
|
+
): Y.Array<unknown> {
|
|
140
|
+
const value = payloadValue(node, path, reason);
|
|
141
|
+
return value instanceof Y.Array ? value : fail(reason);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function nodeKind(node: Y.Map<unknown>, ref: BlockRoomNodeRef): string {
|
|
145
|
+
return stringValue(node.get("kind"), `node:${ref.id}:kind`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function nodeTextPredicate(
|
|
149
|
+
node: Y.Map<unknown>,
|
|
150
|
+
ref: BlockRoomNodeRef,
|
|
151
|
+
): CollaborativeTextPredicate {
|
|
152
|
+
const kind = nodeKind(node, ref);
|
|
153
|
+
return (path) => isCollaborativeTextPath(ref.family, kind, path);
|
|
154
|
+
}
|