@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,450 @@
|
|
|
1
|
+
import { fromJson, toJson, type JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import {
|
|
3
|
+
LocalizedPageDocumentSchema,
|
|
4
|
+
LocalizedRichTextDocumentSchema,
|
|
5
|
+
PageSectionNodeSchema,
|
|
6
|
+
RichTextBlockNodeSchema,
|
|
7
|
+
type LocalizedPageDocument,
|
|
8
|
+
type LocalizedRichTextDocument,
|
|
9
|
+
type PageSectionNode,
|
|
10
|
+
type RichTextBlockNode,
|
|
11
|
+
} from "@echovisionlab/geul-proto/content/block_content_pb.ts";
|
|
12
|
+
import * as Y from "yjs";
|
|
13
|
+
import {
|
|
14
|
+
BASE_NODE_KEYS,
|
|
15
|
+
BLOCK_ROOM_BASE_NODES,
|
|
16
|
+
BLOCK_ROOM_BASE_ORDER,
|
|
17
|
+
BLOCK_ROOM_LOCALE_OVERLAY,
|
|
18
|
+
BLOCK_ROOM_ROOT,
|
|
19
|
+
LOCALE_NODE_KEYS,
|
|
20
|
+
ROOT_KEYS,
|
|
21
|
+
assertBlockRoomLocaleProjectionParity,
|
|
22
|
+
assertExactKeys,
|
|
23
|
+
canonicalBlockRoomDocumentBytes,
|
|
24
|
+
fail,
|
|
25
|
+
integerValue,
|
|
26
|
+
jsonObject,
|
|
27
|
+
materializePageSectionPayload,
|
|
28
|
+
normalizeBlockDocument,
|
|
29
|
+
orderContainerKey,
|
|
30
|
+
pageSectionSlot,
|
|
31
|
+
richTextSlot,
|
|
32
|
+
stringValue,
|
|
33
|
+
yMap,
|
|
34
|
+
fromYValue,
|
|
35
|
+
type BlockRoomDocumentType,
|
|
36
|
+
type BlockRoomNodeFamily,
|
|
37
|
+
type BlockRoomTypedDocument,
|
|
38
|
+
} from "./internal.ts";
|
|
39
|
+
|
|
40
|
+
export function nodePayload(
|
|
41
|
+
node: Y.Map<unknown>,
|
|
42
|
+
allowed: ReadonlySet<string>,
|
|
43
|
+
reason: string,
|
|
44
|
+
): { family: BlockRoomNodeFamily; kind: string; payload: JsonValue } {
|
|
45
|
+
assertExactKeys(node, allowed, reason);
|
|
46
|
+
const family = stringValue(node.get("family"), `${reason}:family`);
|
|
47
|
+
if (family !== "page_section" && family !== "rich_text")
|
|
48
|
+
fail(`${reason}:family`);
|
|
49
|
+
return {
|
|
50
|
+
family,
|
|
51
|
+
kind: stringValue(node.get("kind"), `${reason}:kind`),
|
|
52
|
+
payload: fromYValue(node.get("payload")),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BlockRoomBaseNodeSnapshot {
|
|
57
|
+
id: string;
|
|
58
|
+
family: BlockRoomNodeFamily;
|
|
59
|
+
kind: string;
|
|
60
|
+
payload: JsonValue;
|
|
61
|
+
parentId: string | null;
|
|
62
|
+
containerSlot: string;
|
|
63
|
+
position: number;
|
|
64
|
+
columnId?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BlockRoomLocaleNodeSnapshot {
|
|
68
|
+
id: string;
|
|
69
|
+
family: BlockRoomNodeFamily;
|
|
70
|
+
kind: string;
|
|
71
|
+
payload: JsonValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function blockRoomBaseNodes(yDocument: Y.Doc): Y.Map<unknown> {
|
|
75
|
+
return yMap(
|
|
76
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get(BLOCK_ROOM_BASE_NODES),
|
|
77
|
+
"base_nodes",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function blockRoomBaseOrder(yDocument: Y.Doc): Y.Map<unknown> {
|
|
82
|
+
return yMap(
|
|
83
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get(BLOCK_ROOM_BASE_ORDER),
|
|
84
|
+
"base_order",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function blockRoomLocaleOverlay(yDocument: Y.Doc): Y.Map<unknown> {
|
|
89
|
+
return yMap(
|
|
90
|
+
yDocument.getMap<unknown>(BLOCK_ROOM_ROOT).get(BLOCK_ROOM_LOCALE_OVERLAY),
|
|
91
|
+
"locale_overlay",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function decodeBaseNodes(
|
|
96
|
+
nodes: Y.Map<unknown>,
|
|
97
|
+
orders: Y.Map<unknown>,
|
|
98
|
+
): Map<string, BlockRoomBaseNodeSnapshot> {
|
|
99
|
+
const decoded = new Map(
|
|
100
|
+
[...nodes.entries()]
|
|
101
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
102
|
+
.map(([id, rawNode]) => {
|
|
103
|
+
const reason = `base_node:${id}`;
|
|
104
|
+
const node = yMap(rawNode, reason);
|
|
105
|
+
const { family, kind, payload } = nodePayload(
|
|
106
|
+
node,
|
|
107
|
+
BASE_NODE_KEYS,
|
|
108
|
+
reason,
|
|
109
|
+
);
|
|
110
|
+
const rawParentId = node.get("parentId");
|
|
111
|
+
const rawColumnId = node.get("columnId");
|
|
112
|
+
const parentId =
|
|
113
|
+
rawParentId === null
|
|
114
|
+
? null
|
|
115
|
+
: stringValue(rawParentId, `${reason}:parent`);
|
|
116
|
+
const columnId =
|
|
117
|
+
rawColumnId === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: stringValue(rawColumnId, `${reason}:column`);
|
|
120
|
+
return [
|
|
121
|
+
id,
|
|
122
|
+
{
|
|
123
|
+
id,
|
|
124
|
+
family,
|
|
125
|
+
kind,
|
|
126
|
+
payload,
|
|
127
|
+
parentId,
|
|
128
|
+
containerSlot: stringValue(
|
|
129
|
+
node.get("containerSlot"),
|
|
130
|
+
`${reason}:slot`,
|
|
131
|
+
),
|
|
132
|
+
position: -1,
|
|
133
|
+
...(columnId ? { columnId } : {}),
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
const orderedIds = new Set<string>();
|
|
139
|
+
for (const [key, rawOrder] of [...orders.entries()].sort(([left], [right]) =>
|
|
140
|
+
left.localeCompare(right),
|
|
141
|
+
)) {
|
|
142
|
+
const order =
|
|
143
|
+
rawOrder instanceof Y.Array
|
|
144
|
+
? rawOrder
|
|
145
|
+
: fail(`base_order:${key}:not_array`);
|
|
146
|
+
const ids = order.toArray();
|
|
147
|
+
ids.forEach((rawId, position) => {
|
|
148
|
+
const id = stringValue(rawId, `base_order:${key}:id`);
|
|
149
|
+
if (orderedIds.has(id)) fail(`base_order:${key}:duplicate:${id}`);
|
|
150
|
+
const node = decoded.get(id);
|
|
151
|
+
if (!node) fail(`base_order:${key}:missing_node:${id}`);
|
|
152
|
+
if (key !== orderContainerKey(node.parentId, node.containerSlot)) {
|
|
153
|
+
fail(`base_order:${key}:wrong_container:${id}`);
|
|
154
|
+
}
|
|
155
|
+
node.position = position;
|
|
156
|
+
orderedIds.add(id);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const id of decoded.keys()) {
|
|
160
|
+
if (!orderedIds.has(id)) fail(`base_order:missing:${id}`);
|
|
161
|
+
}
|
|
162
|
+
return decoded;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function materializeRichTextNode(
|
|
166
|
+
node: BlockRoomBaseNodeSnapshot,
|
|
167
|
+
topLevelParentId?: string,
|
|
168
|
+
): RichTextBlockNode {
|
|
169
|
+
if (node.containerSlot !== richTextSlot() || node.columnId !== undefined) {
|
|
170
|
+
fail(`base_node:${node.id}:placement`);
|
|
171
|
+
}
|
|
172
|
+
const placement: { index: number; parentBlockId?: string } = {
|
|
173
|
+
index: node.position,
|
|
174
|
+
};
|
|
175
|
+
if (node.parentId !== null && node.parentId !== topLevelParentId)
|
|
176
|
+
placement.parentBlockId = node.parentId;
|
|
177
|
+
return fromJson(RichTextBlockNodeSchema, {
|
|
178
|
+
block: { id: node.id, [node.kind]: node.payload },
|
|
179
|
+
placement,
|
|
180
|
+
}) as RichTextBlockNode;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function richTextSectionId(
|
|
184
|
+
node: BlockRoomBaseNodeSnapshot,
|
|
185
|
+
nodes: ReadonlyMap<string, BlockRoomBaseNodeSnapshot>,
|
|
186
|
+
): string {
|
|
187
|
+
const visited = new Set([node.id]);
|
|
188
|
+
let parentId = node.parentId;
|
|
189
|
+
while (parentId !== null) {
|
|
190
|
+
if (visited.has(parentId)) fail(`base_node:${node.id}:parent_cycle`);
|
|
191
|
+
visited.add(parentId);
|
|
192
|
+
const parent = nodes.get(parentId);
|
|
193
|
+
if (!parent) fail(`base_node:${node.id}:missing_parent`);
|
|
194
|
+
if (parent.family === "page_section") {
|
|
195
|
+
if (parent.kind !== "richText") fail(`base_node:${node.id}:section_kind`);
|
|
196
|
+
return parent.id;
|
|
197
|
+
}
|
|
198
|
+
parentId = parent.parentId;
|
|
199
|
+
}
|
|
200
|
+
return fail(`base_node:${node.id}:missing_section_parent`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function materializeStandaloneRichTextBase(
|
|
204
|
+
nodes: ReadonlyMap<string, BlockRoomBaseNodeSnapshot>,
|
|
205
|
+
): RichTextBlockNode[] {
|
|
206
|
+
return [...nodes.values()].map((node) => {
|
|
207
|
+
if (node.family !== "rich_text") fail(`base_node:${node.id}:family`);
|
|
208
|
+
return materializeRichTextNode(node);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function materializePageBase(
|
|
213
|
+
nodes: ReadonlyMap<string, BlockRoomBaseNodeSnapshot>,
|
|
214
|
+
): PageSectionNode[] {
|
|
215
|
+
const richTextBySection = new Map<string, BlockRoomBaseNodeSnapshot[]>();
|
|
216
|
+
for (const node of nodes.values()) {
|
|
217
|
+
if (node.family !== "rich_text") continue;
|
|
218
|
+
const sectionId = richTextSectionId(node, nodes);
|
|
219
|
+
const entries = richTextBySection.get(sectionId) ?? [];
|
|
220
|
+
entries.push(node);
|
|
221
|
+
richTextBySection.set(sectionId, entries);
|
|
222
|
+
}
|
|
223
|
+
return [...nodes.values()]
|
|
224
|
+
.filter((node) => node.family === "page_section")
|
|
225
|
+
.map((node) => {
|
|
226
|
+
const expectedSlot = pageSectionSlot(
|
|
227
|
+
node.parentId ?? undefined,
|
|
228
|
+
node.columnId,
|
|
229
|
+
);
|
|
230
|
+
if (node.containerSlot !== expectedSlot)
|
|
231
|
+
fail(`base_node:${node.id}:slot`);
|
|
232
|
+
const placement: {
|
|
233
|
+
index: number;
|
|
234
|
+
parentSectionId?: string;
|
|
235
|
+
columnId?: string;
|
|
236
|
+
} = { index: node.position };
|
|
237
|
+
if (node.parentId !== null) placement.parentSectionId = node.parentId;
|
|
238
|
+
if (node.columnId) placement.columnId = node.columnId;
|
|
239
|
+
const materialized = materializePageSectionPayload(
|
|
240
|
+
node.payload,
|
|
241
|
+
`base_node:${node.id}:payload`,
|
|
242
|
+
);
|
|
243
|
+
const payload =
|
|
244
|
+
node.kind === "richText"
|
|
245
|
+
? {
|
|
246
|
+
...materialized.value,
|
|
247
|
+
blocks: {
|
|
248
|
+
nodes: (richTextBySection.get(node.id) ?? []).map((block) =>
|
|
249
|
+
toJson(
|
|
250
|
+
RichTextBlockNodeSchema,
|
|
251
|
+
materializeRichTextNode(block, node.id),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
: materialized.value;
|
|
257
|
+
return fromJson(PageSectionNodeSchema, {
|
|
258
|
+
section: {
|
|
259
|
+
id: node.id,
|
|
260
|
+
...(materialized.settings === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { settings: materialized.settings }),
|
|
263
|
+
[node.kind]: payload,
|
|
264
|
+
},
|
|
265
|
+
placement,
|
|
266
|
+
}) as PageSectionNode;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function decodeLocaleNodes(
|
|
271
|
+
rawNodes: unknown,
|
|
272
|
+
): Map<string, BlockRoomLocaleNodeSnapshot> {
|
|
273
|
+
const nodes = yMap(rawNodes, "locale");
|
|
274
|
+
return new Map(
|
|
275
|
+
[...nodes.entries()]
|
|
276
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
277
|
+
.map(([id, rawNode]) => {
|
|
278
|
+
const reason = `locale:${id}`;
|
|
279
|
+
const { family, kind, payload } = nodePayload(
|
|
280
|
+
yMap(rawNode, reason),
|
|
281
|
+
LOCALE_NODE_KEYS,
|
|
282
|
+
reason,
|
|
283
|
+
);
|
|
284
|
+
return [id, { id, family, kind, payload }];
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function richTextLocaleJson(node: BlockRoomLocaleNodeSnapshot): {
|
|
290
|
+
blockId: string;
|
|
291
|
+
[key: string]: JsonValue;
|
|
292
|
+
} {
|
|
293
|
+
return { blockId: node.id, [node.kind]: node.payload };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function materializeRichTextLocaleOverlay(
|
|
297
|
+
roomLocale: string,
|
|
298
|
+
localeNodes: Y.Map<unknown>,
|
|
299
|
+
): JsonValue[] {
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
locale: roomLocale,
|
|
303
|
+
blocks: [...decodeLocaleNodes(localeNodes).values()].map(
|
|
304
|
+
richTextLocaleJson,
|
|
305
|
+
),
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function materializePageLocaleOverlay(
|
|
311
|
+
roomLocale: string,
|
|
312
|
+
localeNodes: Y.Map<unknown>,
|
|
313
|
+
baseNodes: ReadonlyMap<string, BlockRoomBaseNodeSnapshot>,
|
|
314
|
+
): JsonValue[] {
|
|
315
|
+
const nodes = decodeLocaleNodes(localeNodes);
|
|
316
|
+
const richTextBySection = new Map<string, BlockRoomLocaleNodeSnapshot[]>();
|
|
317
|
+
for (const node of nodes.values()) {
|
|
318
|
+
if (node.family !== "rich_text") continue;
|
|
319
|
+
const base = baseNodes.get(node.id)!;
|
|
320
|
+
const sectionId = richTextSectionId(base, baseNodes);
|
|
321
|
+
const entries = richTextBySection.get(sectionId) ?? [];
|
|
322
|
+
entries.push(node);
|
|
323
|
+
richTextBySection.set(sectionId, entries);
|
|
324
|
+
}
|
|
325
|
+
const sectionIds = new Set([
|
|
326
|
+
...[...nodes.values()]
|
|
327
|
+
.filter((node) => node.family === "page_section")
|
|
328
|
+
.map((node) => node.id),
|
|
329
|
+
...richTextBySection.keys(),
|
|
330
|
+
]);
|
|
331
|
+
const sections = [...sectionIds].sort().map((sectionId) => {
|
|
332
|
+
const localeSection = nodes.get(sectionId)!;
|
|
333
|
+
const kind = localeSection.kind;
|
|
334
|
+
const payload = localeSection.payload;
|
|
335
|
+
const value =
|
|
336
|
+
kind === "richText"
|
|
337
|
+
? {
|
|
338
|
+
...jsonObject(payload, `locale:${sectionId}:payload`),
|
|
339
|
+
blocks: {
|
|
340
|
+
locale: roomLocale,
|
|
341
|
+
blocks: (richTextBySection.get(sectionId) ?? []).map(
|
|
342
|
+
richTextLocaleJson,
|
|
343
|
+
),
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
: payload;
|
|
347
|
+
return { sectionId, [kind]: value };
|
|
348
|
+
});
|
|
349
|
+
return [{ locale: roomLocale, sections }];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function materializeCanonicalBlockRoom(
|
|
353
|
+
yDocument: Y.Doc,
|
|
354
|
+
expectedDocumentType: BlockRoomDocumentType,
|
|
355
|
+
): BlockRoomTypedDocument {
|
|
356
|
+
const root = yDocument.getMap<unknown>(BLOCK_ROOM_ROOT);
|
|
357
|
+
assertExactKeys(root, ROOT_KEYS, "root");
|
|
358
|
+
const documentType = stringValue(root.get("documentType"), "document_type");
|
|
359
|
+
if (documentType !== expectedDocumentType) fail("document_type");
|
|
360
|
+
const fingerprint = stringValue(
|
|
361
|
+
root.get("blockCatalogFingerprint"),
|
|
362
|
+
"catalog_fingerprint",
|
|
363
|
+
);
|
|
364
|
+
stringValue(root.get("sourceLocale"), "source_locale");
|
|
365
|
+
const roomLocale = stringValue(root.get("roomLocale"), "room_locale");
|
|
366
|
+
const nodes = decodeBaseNodes(
|
|
367
|
+
blockRoomBaseNodes(yDocument),
|
|
368
|
+
blockRoomBaseOrder(yDocument),
|
|
369
|
+
);
|
|
370
|
+
assertBlockRoomLocaleProjectionParity(yDocument);
|
|
371
|
+
const localeNodes = blockRoomLocaleOverlay(yDocument);
|
|
372
|
+
if (expectedDocumentType === "page") {
|
|
373
|
+
if (root.has("profile")) fail("page_profile");
|
|
374
|
+
const document = fromJson(LocalizedPageDocumentSchema, {
|
|
375
|
+
blockCatalogFingerprint: fingerprint,
|
|
376
|
+
locale: roomLocale,
|
|
377
|
+
base: {
|
|
378
|
+
nodes: materializePageBase(nodes).map((node) =>
|
|
379
|
+
toJson(PageSectionNodeSchema, node),
|
|
380
|
+
),
|
|
381
|
+
},
|
|
382
|
+
localeOverlay: materializePageLocaleOverlay(
|
|
383
|
+
roomLocale,
|
|
384
|
+
localeNodes,
|
|
385
|
+
nodes,
|
|
386
|
+
)[0],
|
|
387
|
+
}) as LocalizedPageDocument;
|
|
388
|
+
return normalizeBlockDocument(
|
|
389
|
+
expectedDocumentType,
|
|
390
|
+
document,
|
|
391
|
+
) as LocalizedPageDocument;
|
|
392
|
+
}
|
|
393
|
+
const profile = integerValue(root.get("profile"), "profile");
|
|
394
|
+
const document = fromJson(LocalizedRichTextDocumentSchema, {
|
|
395
|
+
blockCatalogFingerprint: fingerprint,
|
|
396
|
+
profile,
|
|
397
|
+
locale: roomLocale,
|
|
398
|
+
base: {
|
|
399
|
+
nodes: materializeStandaloneRichTextBase(nodes).map((node) =>
|
|
400
|
+
toJson(RichTextBlockNodeSchema, node),
|
|
401
|
+
),
|
|
402
|
+
},
|
|
403
|
+
localeOverlay: materializeRichTextLocaleOverlay(roomLocale, localeNodes)[0],
|
|
404
|
+
}) as LocalizedRichTextDocument;
|
|
405
|
+
return normalizeBlockDocument(
|
|
406
|
+
expectedDocumentType,
|
|
407
|
+
document,
|
|
408
|
+
) as LocalizedRichTextDocument;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function assertCanonicalBlockRoomParity(
|
|
412
|
+
yDocument: Y.Doc,
|
|
413
|
+
documentType: BlockRoomDocumentType,
|
|
414
|
+
expected: BlockRoomTypedDocument,
|
|
415
|
+
): void {
|
|
416
|
+
const actualBytes = canonicalBlockRoomDocumentBytes(
|
|
417
|
+
documentType,
|
|
418
|
+
materializeCanonicalBlockRoom(yDocument, documentType),
|
|
419
|
+
);
|
|
420
|
+
const expectedBytes = canonicalBlockRoomDocumentBytes(documentType, expected);
|
|
421
|
+
if (
|
|
422
|
+
actualBytes.length !== expectedBytes.length ||
|
|
423
|
+
!actualBytes.every((value, index) => value === expectedBytes[index])
|
|
424
|
+
)
|
|
425
|
+
fail("canonical_parity");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface CanonicalBlockRoomSnapshot {
|
|
429
|
+
document: BlockRoomTypedDocument;
|
|
430
|
+
baseNodes: BlockRoomBaseNodeSnapshot[];
|
|
431
|
+
localeOverlay: BlockRoomLocaleNodeSnapshot[];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function decodeCanonicalBlockRoom(
|
|
435
|
+
yDocument: Y.Doc,
|
|
436
|
+
expectedDocumentType: BlockRoomDocumentType,
|
|
437
|
+
): CanonicalBlockRoomSnapshot {
|
|
438
|
+
const document = materializeCanonicalBlockRoom(
|
|
439
|
+
yDocument,
|
|
440
|
+
expectedDocumentType,
|
|
441
|
+
);
|
|
442
|
+
const baseNodes = decodeBaseNodes(
|
|
443
|
+
blockRoomBaseNodes(yDocument),
|
|
444
|
+
blockRoomBaseOrder(yDocument),
|
|
445
|
+
);
|
|
446
|
+
const localeOverlay = [
|
|
447
|
+
...decodeLocaleNodes(blockRoomLocaleOverlay(yDocument)).values(),
|
|
448
|
+
];
|
|
449
|
+
return { document, baseNodes: [...baseNodes.values()], localeOverlay };
|
|
450
|
+
}
|