@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,483 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export * from "./document-layout.ts";
|
|
4
|
+
|
|
5
|
+
// Shared Page Yjs owns only section/column/block structure. Page settings are
|
|
6
|
+
// changed through their owning DB mutations and must not be persisted here.
|
|
7
|
+
export const pageCollabFieldsSchema = z.object({}).strict();
|
|
8
|
+
|
|
9
|
+
export const PAGE_SOURCE_OWNED_FIELD_KEYS = ["title", "summary"] as const;
|
|
10
|
+
export const PAGE_SHARED_FIELD_KEYS = [] as const;
|
|
11
|
+
export const PAGE_LOCALE_SECTION_PROP_KEYS = [
|
|
12
|
+
"title",
|
|
13
|
+
"label",
|
|
14
|
+
"description",
|
|
15
|
+
"caption",
|
|
16
|
+
"copyJson",
|
|
17
|
+
] as const;
|
|
18
|
+
export const PAGE_RICH_TEXT_BLOCK_LOCALE_PROP_KEYS = {
|
|
19
|
+
codeBlock: ["title"],
|
|
20
|
+
file: ["alt", "caption"],
|
|
21
|
+
map: ["caption"],
|
|
22
|
+
p5Sketch: ["title"],
|
|
23
|
+
threeScene: ["title"],
|
|
24
|
+
shader: ["title"],
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export const PAGE_RICH_TEXT_BLOCK_SOURCE_OWNED_CONTENT_TYPES = [
|
|
28
|
+
"p5Sketch",
|
|
29
|
+
"threeScene",
|
|
30
|
+
"shader",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export function isPageRichTextBlockContentSourceOwned(
|
|
34
|
+
blockType: string,
|
|
35
|
+
): boolean {
|
|
36
|
+
return PAGE_RICH_TEXT_BLOCK_SOURCE_OWNED_CONTENT_TYPES.includes(
|
|
37
|
+
blockType as (typeof PAGE_RICH_TEXT_BLOCK_SOURCE_OWNED_CONTENT_TYPES)[number],
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PAGE_VISUAL_UNIT_DURABLE_PROP_KEYS = new Set([
|
|
42
|
+
"id",
|
|
43
|
+
"name",
|
|
44
|
+
"attribution",
|
|
45
|
+
"mesh",
|
|
46
|
+
"meshSource",
|
|
47
|
+
"meshFileId",
|
|
48
|
+
"meshObjectName",
|
|
49
|
+
"meshOptimizationCandidateId",
|
|
50
|
+
"meshOptimizationSourceFileId",
|
|
51
|
+
"meshOptimizationFileId",
|
|
52
|
+
"scale",
|
|
53
|
+
"meshOffsetY",
|
|
54
|
+
"particleSize",
|
|
55
|
+
"holdSeconds",
|
|
56
|
+
"rotationX",
|
|
57
|
+
"rotationY",
|
|
58
|
+
"rotationZ",
|
|
59
|
+
"rotationSpeedX",
|
|
60
|
+
"rotationSpeedY",
|
|
61
|
+
"rotationSpeedZ",
|
|
62
|
+
"scrollRotationTurnsX",
|
|
63
|
+
"scrollRotationTurnsY",
|
|
64
|
+
"scrollRotationTurnsZ",
|
|
65
|
+
"color",
|
|
66
|
+
"textureSource",
|
|
67
|
+
"textureFileId",
|
|
68
|
+
"darkColor",
|
|
69
|
+
"darkTextureSource",
|
|
70
|
+
"darkTextureFileId",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const PAGE_VISUAL_COPY_DURABLE_PROP_KEYS = new Set(["id", "title", "text"]);
|
|
74
|
+
|
|
75
|
+
export function sanitizePageVisualUnitsJson(value: string): string {
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(value);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error("Failed to parse page visual units JSON");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(parsed)) {
|
|
84
|
+
throw new Error("Page visual units JSON must be an array");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return JSON.stringify(
|
|
88
|
+
parsed.map((unit) => {
|
|
89
|
+
if (!unit || typeof unit !== "object" || Array.isArray(unit)) {
|
|
90
|
+
return unit;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const durableUnit: Record<string, unknown> = {};
|
|
94
|
+
for (const [key, fieldValue] of Object.entries(unit)) {
|
|
95
|
+
if (PAGE_VISUAL_UNIT_DURABLE_PROP_KEYS.has(key)) {
|
|
96
|
+
durableUnit[key] = fieldValue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return durableUnit;
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function sanitizePageVisualCopyJson(value: string): string {
|
|
105
|
+
if (!value.trim()) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(value);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error("Failed to parse page visual copy JSON");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!Array.isArray(parsed)) {
|
|
117
|
+
throw new Error("Page visual copy JSON must be an array");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return JSON.stringify(
|
|
121
|
+
parsed.map((unit) => {
|
|
122
|
+
if (!unit || typeof unit !== "object" || Array.isArray(unit)) {
|
|
123
|
+
return unit;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const durableCopy: Record<string, string> = {};
|
|
127
|
+
for (const [key, fieldValue] of Object.entries(unit)) {
|
|
128
|
+
if (
|
|
129
|
+
PAGE_VISUAL_COPY_DURABLE_PROP_KEYS.has(key) &&
|
|
130
|
+
typeof fieldValue === "string"
|
|
131
|
+
) {
|
|
132
|
+
durableCopy[key] = fieldValue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return durableCopy;
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PAGE_RICH_TEXT_BLOCK_SHARED_PROP_KEYS = {
|
|
141
|
+
file: ["fileId", "name", "width", "height", "previewWidth", "textAlignment"],
|
|
142
|
+
} as const;
|
|
143
|
+
|
|
144
|
+
function sanitizeSharedRichTextBlockPropValue(value: unknown): unknown {
|
|
145
|
+
if (typeof value !== "string") {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function pickSharedRichTextBlockProps(
|
|
153
|
+
sharedProps: Record<string, unknown>,
|
|
154
|
+
blockType: string,
|
|
155
|
+
): Record<string, unknown> {
|
|
156
|
+
const sharedKeys =
|
|
157
|
+
PAGE_RICH_TEXT_BLOCK_SHARED_PROP_KEYS[
|
|
158
|
+
blockType as keyof typeof PAGE_RICH_TEXT_BLOCK_SHARED_PROP_KEYS
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
if (!sharedKeys) {
|
|
162
|
+
const localizedKeys = new Set<string>(
|
|
163
|
+
PAGE_RICH_TEXT_BLOCK_LOCALE_PROP_KEYS[
|
|
164
|
+
blockType as keyof typeof PAGE_RICH_TEXT_BLOCK_LOCALE_PROP_KEYS
|
|
165
|
+
] ?? [],
|
|
166
|
+
);
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
Object.entries(sharedProps).filter(([key]) => !localizedKeys.has(key)),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const nextProps: Record<string, unknown> = {};
|
|
173
|
+
for (const key of sharedKeys) {
|
|
174
|
+
const value = sanitizeSharedRichTextBlockPropValue(sharedProps[key]);
|
|
175
|
+
if (value !== undefined) {
|
|
176
|
+
nextProps[key] = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return nextProps;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type PageCollabFields = z.infer<typeof pageCollabFieldsSchema>;
|
|
184
|
+
export type PageFieldValue = string | boolean | null;
|
|
185
|
+
export type PageSourceOwnedFieldKey =
|
|
186
|
+
(typeof PAGE_SOURCE_OWNED_FIELD_KEYS)[number];
|
|
187
|
+
export type PageSharedFieldKey = (typeof PAGE_SHARED_FIELD_KEYS)[number];
|
|
188
|
+
export type PageLocaleSectionPropKey =
|
|
189
|
+
(typeof PAGE_LOCALE_SECTION_PROP_KEYS)[number];
|
|
190
|
+
|
|
191
|
+
export function getRichTextBlockLocalePropKeys(
|
|
192
|
+
blockType: string,
|
|
193
|
+
): readonly string[] {
|
|
194
|
+
return (
|
|
195
|
+
PAGE_RICH_TEXT_BLOCK_LOCALE_PROP_KEYS[
|
|
196
|
+
blockType as keyof typeof PAGE_RICH_TEXT_BLOCK_LOCALE_PROP_KEYS
|
|
197
|
+
] ?? []
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function hasRichTextBlockLocaleProps(blockType: string): boolean {
|
|
202
|
+
return getRichTextBlockLocalePropKeys(blockType).length > 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pageTranslationRecord(value: unknown): Record<string, unknown> | null {
|
|
206
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
207
|
+
? (value as Record<string, unknown>)
|
|
208
|
+
: null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function pageTranslationArray(value: unknown): unknown[] {
|
|
212
|
+
return Array.isArray(value) ? value : [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pageTranslationString(value: unknown): string {
|
|
216
|
+
return typeof value === "string" ? value : "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function appendPageTranslationText(parts: string[], value: unknown): void {
|
|
220
|
+
const text = pageTranslationString(value).trim();
|
|
221
|
+
if (text) {
|
|
222
|
+
parts.push(text);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function pageInlineText(content: unknown): string {
|
|
227
|
+
const parts: string[] = [];
|
|
228
|
+
for (const rawNode of pageTranslationArray(content)) {
|
|
229
|
+
const node = pageTranslationRecord(rawNode);
|
|
230
|
+
if (!node) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (node.type === "text") {
|
|
234
|
+
const text = pageTranslationString(node.text);
|
|
235
|
+
if (text.trim()) {
|
|
236
|
+
parts.push(text);
|
|
237
|
+
}
|
|
238
|
+
} else if (node.type === "link") {
|
|
239
|
+
const text = pageInlineText(node.content);
|
|
240
|
+
if (text) {
|
|
241
|
+
parts.push(text);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return parts.join("");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function pageTableText(content: unknown): string {
|
|
249
|
+
const table = pageTranslationRecord(content);
|
|
250
|
+
if (table?.type !== "tableContent") {
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parts: string[] = [];
|
|
255
|
+
for (const rawRow of pageTranslationArray(table.rows)) {
|
|
256
|
+
const row = pageTranslationRecord(rawRow);
|
|
257
|
+
for (const rawCell of pageTranslationArray(row?.cells)) {
|
|
258
|
+
const cell = pageTranslationRecord(rawCell);
|
|
259
|
+
const text = pageInlineText(cell?.content);
|
|
260
|
+
if (text) {
|
|
261
|
+
parts.push(text);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return parts.join("");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function pageBlockText(block: Record<string, unknown>): string {
|
|
269
|
+
return block.type === "table"
|
|
270
|
+
? pageTableText(block.content)
|
|
271
|
+
: pageInlineText(block.content);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function extractPageBlockTranslationText(blocks: unknown): string {
|
|
275
|
+
const parts: string[] = [];
|
|
276
|
+
for (const rawBlock of pageTranslationArray(blocks)) {
|
|
277
|
+
const block = pageTranslationRecord(rawBlock);
|
|
278
|
+
if (!block) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const text = pageBlockText(block);
|
|
283
|
+
if (text) {
|
|
284
|
+
parts.push(text);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const props = pageTranslationRecord(block.props) ?? {};
|
|
288
|
+
for (const key of getRichTextBlockLocalePropKeys(
|
|
289
|
+
pageTranslationString(block.type),
|
|
290
|
+
)) {
|
|
291
|
+
appendPageTranslationText(parts, props[key]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const childText = extractPageBlockTranslationText(block.children);
|
|
295
|
+
if (childText) {
|
|
296
|
+
parts.push(childText);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return parts.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function appendImmersiveSceneCopyText(parts: string[], value: unknown): void {
|
|
303
|
+
const copyJson = pageTranslationString(value).trim();
|
|
304
|
+
if (!copyJson) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let units: unknown;
|
|
309
|
+
try {
|
|
310
|
+
units = JSON.parse(copyJson) as unknown;
|
|
311
|
+
} catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const rawUnit of pageTranslationArray(units)) {
|
|
316
|
+
const unit = pageTranslationRecord(rawUnit);
|
|
317
|
+
if (!unit) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
appendPageTranslationText(parts, unit.title);
|
|
321
|
+
appendPageTranslationText(parts, unit.text);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function appendPageSectionTranslationText(
|
|
326
|
+
parts: string[],
|
|
327
|
+
sections: unknown,
|
|
328
|
+
): void {
|
|
329
|
+
for (const rawSection of pageTranslationArray(sections)) {
|
|
330
|
+
const section = pageTranslationRecord(rawSection);
|
|
331
|
+
if (!section) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const type = pageTranslationString(section.type);
|
|
336
|
+
if (type === "rich-text") {
|
|
337
|
+
const text = extractPageBlockTranslationText(section.content);
|
|
338
|
+
if (text) {
|
|
339
|
+
parts.push(text);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const props = pageTranslationRecord(section.props) ?? {};
|
|
344
|
+
for (const key of PAGE_LOCALE_SECTION_PROP_KEYS) {
|
|
345
|
+
if (key === "copyJson") {
|
|
346
|
+
if (type === "immersive-scene") {
|
|
347
|
+
appendImmersiveSceneCopyText(parts, props[key]);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
appendPageTranslationText(parts, props[key]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (type === "columns") {
|
|
355
|
+
for (const rawColumn of pageTranslationArray(section.columns)) {
|
|
356
|
+
const column = pageTranslationRecord(rawColumn);
|
|
357
|
+
appendPageSectionTranslationText(parts, column?.sections);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function extractPageTranslationContentText(sections: unknown): string {
|
|
364
|
+
const parts: string[] = [];
|
|
365
|
+
appendPageSectionTranslationText(parts, sections);
|
|
366
|
+
return parts.join("\n").trim();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function pickPageLocaleSectionProps(
|
|
370
|
+
props: Record<string, unknown> | undefined,
|
|
371
|
+
): Record<string, unknown> | undefined {
|
|
372
|
+
if (!props) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const localizedProps: Record<string, unknown> = {};
|
|
377
|
+
for (const key of PAGE_LOCALE_SECTION_PROP_KEYS) {
|
|
378
|
+
const value = props[key];
|
|
379
|
+
if (typeof value === "string") {
|
|
380
|
+
localizedProps[key] =
|
|
381
|
+
key === "copyJson" ? sanitizePageVisualCopyJson(value) : value;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Object.keys(localizedProps).length > 0 ? localizedProps : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function stripPageLocaleSectionProps(
|
|
389
|
+
props: Record<string, unknown> | undefined,
|
|
390
|
+
): Record<string, unknown> {
|
|
391
|
+
if (!props) {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const next: Record<string, unknown> = {};
|
|
396
|
+
for (const [key, value] of Object.entries(props)) {
|
|
397
|
+
if (
|
|
398
|
+
!PAGE_LOCALE_SECTION_PROP_KEYS.includes(
|
|
399
|
+
key as (typeof PAGE_LOCALE_SECTION_PROP_KEYS)[number],
|
|
400
|
+
)
|
|
401
|
+
) {
|
|
402
|
+
next[key] =
|
|
403
|
+
key === "unitsJson" && typeof value === "string"
|
|
404
|
+
? sanitizePageVisualUnitsJson(value)
|
|
405
|
+
: value;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return next;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function mergePageLocaleSectionProps(
|
|
412
|
+
sharedPropsValue: unknown,
|
|
413
|
+
localizedPropsValue: unknown,
|
|
414
|
+
): Record<string, unknown> | undefined {
|
|
415
|
+
const sharedProps =
|
|
416
|
+
sharedPropsValue &&
|
|
417
|
+
typeof sharedPropsValue === "object" &&
|
|
418
|
+
!Array.isArray(sharedPropsValue)
|
|
419
|
+
? (sharedPropsValue as Record<string, unknown>)
|
|
420
|
+
: {};
|
|
421
|
+
const nextProps = stripPageLocaleSectionProps(sharedProps);
|
|
422
|
+
const localizedProps =
|
|
423
|
+
localizedPropsValue &&
|
|
424
|
+
typeof localizedPropsValue === "object" &&
|
|
425
|
+
!Array.isArray(localizedPropsValue)
|
|
426
|
+
? (localizedPropsValue as Record<string, unknown>)
|
|
427
|
+
: {};
|
|
428
|
+
|
|
429
|
+
for (const key of PAGE_LOCALE_SECTION_PROP_KEYS) {
|
|
430
|
+
const value = localizedProps[key];
|
|
431
|
+
if (typeof value === "string") {
|
|
432
|
+
nextProps[key] =
|
|
433
|
+
key === "copyJson" ? sanitizePageVisualCopyJson(value) : value;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
delete nextProps[key];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return Object.keys(nextProps).length > 0 ? nextProps : undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function mergeLocalizedBlockProps(
|
|
443
|
+
sharedPropsValue: unknown,
|
|
444
|
+
localizedPropsValue: unknown,
|
|
445
|
+
blockType: string,
|
|
446
|
+
): Record<string, unknown> | undefined {
|
|
447
|
+
if (
|
|
448
|
+
!sharedPropsValue ||
|
|
449
|
+
typeof sharedPropsValue !== "object" ||
|
|
450
|
+
Array.isArray(sharedPropsValue)
|
|
451
|
+
) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const nextProps = pickSharedRichTextBlockProps(
|
|
456
|
+
sharedPropsValue as Record<string, unknown>,
|
|
457
|
+
blockType,
|
|
458
|
+
);
|
|
459
|
+
const localizedProps =
|
|
460
|
+
localizedPropsValue &&
|
|
461
|
+
typeof localizedPropsValue === "object" &&
|
|
462
|
+
!Array.isArray(localizedPropsValue)
|
|
463
|
+
? (localizedPropsValue as Record<string, unknown>)
|
|
464
|
+
: {};
|
|
465
|
+
|
|
466
|
+
for (const key of getRichTextBlockLocalePropKeys(blockType)) {
|
|
467
|
+
const value = localizedProps[key];
|
|
468
|
+
if (typeof value === "string") {
|
|
469
|
+
nextProps[key] = value.trim();
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
delete nextProps[key];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return Object.keys(nextProps).length > 0 ? nextProps : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function extractPageFields(fieldsMap: {
|
|
479
|
+
get(key: string): PageFieldValue | undefined;
|
|
480
|
+
}): PageCollabFields {
|
|
481
|
+
void fieldsMap;
|
|
482
|
+
return pageCollabFieldsSchema.parse({});
|
|
483
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
|
|
3
|
+
export const POST_SERIES_CONTEXT_MAP_NAME = "post-series-context";
|
|
4
|
+
export const POST_SERIES_SOURCE_FIELDS_MAP_NAME = "post-series-source-fields";
|
|
5
|
+
export const POST_SERIES_LOCALE_FIELDS_MAP_NAME = "post-series-locale-fields";
|
|
6
|
+
|
|
7
|
+
export interface PostSeriesLocaleFields {
|
|
8
|
+
title: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PostSeriesStoredLocaleFields {
|
|
13
|
+
title?: string;
|
|
14
|
+
summary?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PostSeriesCanonicalRoomInput {
|
|
18
|
+
sourceLocale: string;
|
|
19
|
+
locale: string;
|
|
20
|
+
localeExists: boolean;
|
|
21
|
+
source: PostSeriesStoredLocaleFields;
|
|
22
|
+
requested: PostSeriesStoredLocaleFields;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function postSeriesLocaleFieldsMap(document: Y.Doc): Y.Map<string> {
|
|
26
|
+
return document.getMap<string>(POST_SERIES_LOCALE_FIELDS_MAP_NAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hydratePostSeriesCanonicalRoom(
|
|
30
|
+
input: PostSeriesCanonicalRoomInput,
|
|
31
|
+
): Y.Doc {
|
|
32
|
+
const document = new Y.Doc();
|
|
33
|
+
document.transact(() => {
|
|
34
|
+
const context = document.getMap<string | boolean>(
|
|
35
|
+
POST_SERIES_CONTEXT_MAP_NAME,
|
|
36
|
+
);
|
|
37
|
+
context.set("sourceLocale", input.sourceLocale);
|
|
38
|
+
context.set("locale", input.locale);
|
|
39
|
+
context.set("localeExists", input.localeExists);
|
|
40
|
+
const source = document.getMap<string>(POST_SERIES_SOURCE_FIELDS_MAP_NAME);
|
|
41
|
+
const requested = postSeriesLocaleFieldsMap(document);
|
|
42
|
+
assignPresentFields(source, input.source);
|
|
43
|
+
assignPresentFields(requested, input.requested);
|
|
44
|
+
});
|
|
45
|
+
return document;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function materializePostSeriesLocaleFields(
|
|
49
|
+
document: Y.Doc,
|
|
50
|
+
): PostSeriesLocaleFields {
|
|
51
|
+
const source = document.getMap<string>(POST_SERIES_SOURCE_FIELDS_MAP_NAME);
|
|
52
|
+
const requested = postSeriesLocaleFieldsMap(document);
|
|
53
|
+
return {
|
|
54
|
+
title: requested.has("title")
|
|
55
|
+
? (requested.get("title") ?? "")
|
|
56
|
+
: (source.get("title") ?? ""),
|
|
57
|
+
summary: requested.has("summary")
|
|
58
|
+
? (requested.get("summary") ?? "")
|
|
59
|
+
: (source.get("summary") ?? ""),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function extractPostSeriesStoredLocaleFields(
|
|
64
|
+
document: Y.Doc,
|
|
65
|
+
): PostSeriesStoredLocaleFields {
|
|
66
|
+
const requested = postSeriesLocaleFieldsMap(document);
|
|
67
|
+
return {
|
|
68
|
+
...(requested.has("title") ? { title: requested.get("title") ?? "" } : {}),
|
|
69
|
+
...(requested.has("summary")
|
|
70
|
+
? { summary: requested.get("summary") ?? "" }
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function setPostSeriesLocaleField(
|
|
76
|
+
document: Y.Doc,
|
|
77
|
+
field: keyof PostSeriesLocaleFields,
|
|
78
|
+
value: string,
|
|
79
|
+
): void {
|
|
80
|
+
postSeriesLocaleFieldsMap(document).set(field, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function unsetPostSeriesLocaleField(
|
|
84
|
+
document: Y.Doc,
|
|
85
|
+
field: keyof PostSeriesLocaleFields,
|
|
86
|
+
): void {
|
|
87
|
+
postSeriesLocaleFieldsMap(document).delete(field);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assignPresentFields(
|
|
91
|
+
target: Y.Map<string>,
|
|
92
|
+
fields: PostSeriesStoredLocaleFields,
|
|
93
|
+
): void {
|
|
94
|
+
if (fields.title !== undefined) target.set("title", fields.title);
|
|
95
|
+
if (fields.summary !== undefined) target.set("summary", fields.summary);
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const postCollabFieldsSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
title: z.string().optional(),
|
|
6
|
+
summary: z.string().optional(),
|
|
7
|
+
categoryIds: z.array(z.string()).optional(),
|
|
8
|
+
tagIds: z.array(z.string()).optional(),
|
|
9
|
+
})
|
|
10
|
+
.strict();
|
|
11
|
+
|
|
12
|
+
export const POST_JSON_KEYS: ReadonlySet<
|
|
13
|
+
keyof z.infer<typeof postCollabFieldsSchema>
|
|
14
|
+
> = new Set(["categoryIds", "tagIds"]);
|
|
15
|
+
|
|
16
|
+
export const POST_SOURCE_OWNED_FIELD_KEYS = ["title", "summary"] as const;
|
|
17
|
+
export const POST_SHARED_FIELD_KEYS = ["categoryIds", "tagIds"] as const;
|
|
18
|
+
|
|
19
|
+
export type PostCollabFields = z.infer<typeof postCollabFieldsSchema>;
|
|
20
|
+
export type PostFieldValue = string | string[];
|
|
21
|
+
export type PostSourceOwnedFieldKey =
|
|
22
|
+
(typeof POST_SOURCE_OWNED_FIELD_KEYS)[number];
|
|
23
|
+
export type PostSharedFieldKey = (typeof POST_SHARED_FIELD_KEYS)[number];
|
|
24
|
+
|
|
25
|
+
export function extractPostFields(fieldsMap: {
|
|
26
|
+
get(key: string): PostFieldValue | undefined;
|
|
27
|
+
}): PostCollabFields {
|
|
28
|
+
const raw: Record<string, unknown> = {};
|
|
29
|
+
|
|
30
|
+
for (const key of Object.keys(postCollabFieldsSchema.shape)) {
|
|
31
|
+
let value = fieldsMap.get(key);
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
POST_JSON_KEYS.has(key as keyof PostCollabFields) &&
|
|
35
|
+
typeof value === "string"
|
|
36
|
+
) {
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(value) as PostFieldValue;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`Failed to parse JSON for post field "${key}"`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value !== undefined) {
|
|
45
|
+
raw[key] = value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return postCollabFieldsSchema.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pickPostSharedFields(
|
|
53
|
+
fields: PostCollabFields,
|
|
54
|
+
): Pick<PostCollabFields, PostSharedFieldKey> {
|
|
55
|
+
return {
|
|
56
|
+
categoryIds: fields.categoryIds,
|
|
57
|
+
tagIds: fields.tagIds,
|
|
58
|
+
};
|
|
59
|
+
}
|