@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
package/LICENSE.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Geul common
|
|
2
|
+
|
|
3
|
+
Shared TypeScript contracts and helpers for Geul editors, collaboration,
|
|
4
|
+
localized content, media blocks, pages, posts, and runtime events.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @echovisionlab/geul-common
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package exports TypeScript source. Consumers must support `.ts` package
|
|
11
|
+
exports. `yjs` is a peer dependency.
|
|
12
|
+
|
|
13
|
+
## Development
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
corepack enable
|
|
17
|
+
pnpm install --frozen-lockfile
|
|
18
|
+
pnpm format:check
|
|
19
|
+
pnpm lint
|
|
20
|
+
pnpm typecheck
|
|
21
|
+
pnpm test:coverage
|
|
22
|
+
pnpm package:smoke
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@echovisionlab/geul-proto` is the contract source of truth. For coordinated
|
|
26
|
+
local changes, `typecheck:local-contracts` and `test:local-contracts` resolve it
|
|
27
|
+
from the sibling `geul-event-contracts` checkout.
|
|
28
|
+
|
|
29
|
+
## Release
|
|
30
|
+
|
|
31
|
+
Release Please creates releases from `main`. npm publication uses GitHub
|
|
32
|
+
Actions trusted publishing without a repository npm token.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
PolyForm Noncommercial 1.0.0. Commercial use requires a separate license from
|
|
37
|
+
Echo Vision Lab. See [LICENSE.md](LICENSE.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@echovisionlab/geul-common",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/echovisionlab/geul-common.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": "24.19.0"
|
|
12
|
+
},
|
|
13
|
+
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
|
|
14
|
+
"files": [
|
|
15
|
+
"README.md",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"format": "prettier --write .",
|
|
21
|
+
"format:check": "prettier --check .",
|
|
22
|
+
"lint": "eslint --cache .",
|
|
23
|
+
"lint-fix": "eslint --cache . --fix",
|
|
24
|
+
"deadcode": "knip --include files,dependencies,unlisted,exports,types",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"typecheck:local-contracts": "tsc --noEmit -p tsconfig.local-contracts.json",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:local-contracts": "vitest run --config vitest.local-contracts.config.mjs",
|
|
29
|
+
"test:coverage": "vitest run --coverage",
|
|
30
|
+
"package:smoke": "npm pack --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./src/index.ts",
|
|
34
|
+
"./collaboration/artist": "./src/collaboration/artist.ts",
|
|
35
|
+
"./collaboration/block-room-codec": "./src/collaboration/block-room-codec.ts",
|
|
36
|
+
"./collaboration/campaign": "./src/collaboration/campaign.ts",
|
|
37
|
+
"./collaboration/document-layout": "./src/collaboration/document-layout.ts",
|
|
38
|
+
"./collaboration/document": "./src/collaboration/document.ts",
|
|
39
|
+
"./collaboration/email-layout": "./src/collaboration/email-layout.ts",
|
|
40
|
+
"./collaboration/form": "./src/collaboration/form.ts",
|
|
41
|
+
"./collaboration/label": "./src/collaboration/label.ts",
|
|
42
|
+
"./collaboration/map-theme": "./src/collaboration/map-theme.ts",
|
|
43
|
+
"./collaboration/menu": "./src/collaboration/menu.ts",
|
|
44
|
+
"./collaboration/metadata-ai": "./src/collaboration/metadata-ai.ts",
|
|
45
|
+
"./collaboration/page": "./src/collaboration/page.ts",
|
|
46
|
+
"./collaboration/post": "./src/collaboration/post.ts",
|
|
47
|
+
"./collaboration/post-series": "./src/collaboration/post-series.ts",
|
|
48
|
+
"./collaboration/release": "./src/collaboration/release.ts",
|
|
49
|
+
"./collaboration/runtime-events": "./src/collaboration/runtime-events.ts",
|
|
50
|
+
"./collaboration/work": "./src/collaboration/work.ts",
|
|
51
|
+
"./editor/materialized-blocks": "./src/editor/materialized-blocks.ts",
|
|
52
|
+
"./editor/link-normalization": "./src/editor/link-normalization.ts",
|
|
53
|
+
"./page": "./src/page/index.ts",
|
|
54
|
+
"./media/block-schemas": "./src/media/block-schemas.ts",
|
|
55
|
+
"./media/hydration": "./src/media/hydration.ts",
|
|
56
|
+
"./post": "./src/post/index.ts",
|
|
57
|
+
"./test/random-id": "./src/test/random-id.ts",
|
|
58
|
+
"./translation/release": "./src/translation/release.ts",
|
|
59
|
+
"./types": "./src/types.ts"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@bufbuild/protobuf": "^2.14.0",
|
|
63
|
+
"@echovisionlab/geul-proto": "^0.1.0",
|
|
64
|
+
"zod": "^4.4.3"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"yjs": "^13.6.32"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "^24.13.3",
|
|
71
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
72
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
73
|
+
"eslint": "^10.8.1",
|
|
74
|
+
"knip": "6.32.2",
|
|
75
|
+
"prettier": "3.9.6",
|
|
76
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
77
|
+
"typescript-eslint": "^8.67.0",
|
|
78
|
+
"vitest": "^4.1.10",
|
|
79
|
+
"yjs": "^13.6.32"
|
|
80
|
+
},
|
|
81
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
82
|
+
"homepage": "https://github.com/echovisionlab/geul-common#readme",
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/echovisionlab/geul-common/issues"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const artistCollabFieldsSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
status: z.enum(["draft", "published"]).optional(),
|
|
6
|
+
realName: z.string().optional(),
|
|
7
|
+
countryCode: z.string().optional(),
|
|
8
|
+
website: z.string().optional(),
|
|
9
|
+
socialLinks: z.record(z.string(), z.string()).optional(),
|
|
10
|
+
slug: z.string().optional(),
|
|
11
|
+
labelIds: z.array(z.string()).optional(),
|
|
12
|
+
parentArtistId: z.string().nullable().optional(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
|
|
16
|
+
export const ARTIST_JSON_KEYS: ReadonlySet<
|
|
17
|
+
keyof z.infer<typeof artistCollabFieldsSchema>
|
|
18
|
+
> = new Set(["socialLinks", "labelIds"]);
|
|
19
|
+
|
|
20
|
+
export const ARTIST_SOURCE_OWNED_FIELD_KEYS = ["name"] as const;
|
|
21
|
+
export const ARTIST_SHARED_FIELD_KEYS = [
|
|
22
|
+
"status",
|
|
23
|
+
"realName",
|
|
24
|
+
"countryCode",
|
|
25
|
+
"website",
|
|
26
|
+
"socialLinks",
|
|
27
|
+
"slug",
|
|
28
|
+
"labelIds",
|
|
29
|
+
"parentArtistId",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type ArtistCollabFields = z.infer<typeof artistCollabFieldsSchema>;
|
|
33
|
+
export type ArtistSourceOwnedFieldKey =
|
|
34
|
+
(typeof ARTIST_SOURCE_OWNED_FIELD_KEYS)[number];
|
|
35
|
+
export type ArtistFieldValue =
|
|
36
|
+
string | string[] | Record<string, string> | null;
|
|
37
|
+
|
|
38
|
+
export function extractArtistFields(fieldsMap: {
|
|
39
|
+
get(key: string): ArtistFieldValue | undefined;
|
|
40
|
+
}): ArtistCollabFields {
|
|
41
|
+
const raw: Record<string, unknown> = {};
|
|
42
|
+
|
|
43
|
+
for (const key of Object.keys(artistCollabFieldsSchema.shape)) {
|
|
44
|
+
let value = fieldsMap.get(key);
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
ARTIST_JSON_KEYS.has(key as keyof ArtistCollabFields) &&
|
|
48
|
+
typeof value === "string"
|
|
49
|
+
) {
|
|
50
|
+
try {
|
|
51
|
+
value = JSON.parse(value) as ArtistFieldValue;
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`Failed to parse JSON for artist field "${key}"`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (value !== undefined) {
|
|
58
|
+
raw[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return artistCollabFieldsSchema.parse(raw);
|
|
63
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { create, fromJson } from "@bufbuild/protobuf";
|
|
2
|
+
import {
|
|
3
|
+
richTextBlockCatalog,
|
|
4
|
+
richTextBlockKindByProtoCase,
|
|
5
|
+
type RichTextBlockKind,
|
|
6
|
+
} from "@echovisionlab/geul-proto/content/block_catalog.ts";
|
|
7
|
+
import {
|
|
8
|
+
RichTextBlockDataSchema,
|
|
9
|
+
RichTextBlockLocaleDataSchema,
|
|
10
|
+
RichTextBlockLocaleSchema,
|
|
11
|
+
RichTextBlockNodeSchema,
|
|
12
|
+
type RichTextBlockData,
|
|
13
|
+
type RichTextBlockLocale,
|
|
14
|
+
type RichTextBlockLocaleData,
|
|
15
|
+
type RichTextBlockNode,
|
|
16
|
+
} from "@echovisionlab/geul-proto/content/block_content_pb.ts";
|
|
17
|
+
import {
|
|
18
|
+
AIDocumentFieldTargetSchema,
|
|
19
|
+
type AIDocumentOperation,
|
|
20
|
+
} from "@echovisionlab/geul-proto/secure/ai_pb.ts";
|
|
21
|
+
import * as Y from "yjs";
|
|
22
|
+
import {
|
|
23
|
+
attachAIDocumentFile,
|
|
24
|
+
detachAIDocumentFile,
|
|
25
|
+
setAIDocumentField,
|
|
26
|
+
unsetAIDocumentField,
|
|
27
|
+
} from "./ai-document-field-mutations.ts";
|
|
28
|
+
import { applyPageStructureAIDocumentOperation } from "./ai-document-page-structure-mutations.ts";
|
|
29
|
+
import { operationFail, requiredHandle } from "./ai-document-values.ts";
|
|
30
|
+
import { yMap, type BlockRoomDocumentType } from "./internal.ts";
|
|
31
|
+
import { assertBlockRoomLocaleChangeAllowed } from "./locale-change-validation.ts";
|
|
32
|
+
import {
|
|
33
|
+
canonicalBlockRoomLocaleValueTarget,
|
|
34
|
+
markBlockRoomLocaleValuePresent,
|
|
35
|
+
} from "./locale-presence.ts";
|
|
36
|
+
import {
|
|
37
|
+
decodeCanonicalBlockRoom,
|
|
38
|
+
materializeCanonicalBlockRoom,
|
|
39
|
+
} from "./materialization.ts";
|
|
40
|
+
import { replaceRichTextBlockData } from "./payload-mutations.ts";
|
|
41
|
+
import {
|
|
42
|
+
roomDocumentType,
|
|
43
|
+
roomLocale,
|
|
44
|
+
roomLocaleRole,
|
|
45
|
+
roomNode,
|
|
46
|
+
} from "./room-access.ts";
|
|
47
|
+
import {
|
|
48
|
+
deleteBlockRoomBaseNode,
|
|
49
|
+
insertRichTextBlockNode,
|
|
50
|
+
insertRichTextBlockLocale,
|
|
51
|
+
moveRichTextBlockNode,
|
|
52
|
+
} from "./structure-mutations.ts";
|
|
53
|
+
|
|
54
|
+
export interface ApplyAIDocumentOperationsOptions {
|
|
55
|
+
expectedRoomLocale: string;
|
|
56
|
+
origin: object;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const richTextProtoCaseByKind = new Map<string, string>(
|
|
60
|
+
Object.entries(richTextBlockKindByProtoCase).map(([protoCase, kind]) => [
|
|
61
|
+
kind,
|
|
62
|
+
protoCase,
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function siblingPosition(
|
|
67
|
+
yDocument: Y.Doc,
|
|
68
|
+
parentId: string | null,
|
|
69
|
+
afterId: string | undefined,
|
|
70
|
+
movingId?: string,
|
|
71
|
+
): number {
|
|
72
|
+
const snapshot = decodeCanonicalBlockRoom(
|
|
73
|
+
yDocument,
|
|
74
|
+
roomDocumentType(yDocument),
|
|
75
|
+
);
|
|
76
|
+
const siblings = snapshot.baseNodes
|
|
77
|
+
.filter((node) => node.parentId === parentId && node.id !== movingId)
|
|
78
|
+
.sort((left, right) => left.position - right.position);
|
|
79
|
+
if (afterId === undefined) return 0;
|
|
80
|
+
const after = siblings.findIndex(({ id }) => id === afterId);
|
|
81
|
+
return after < 0
|
|
82
|
+
? operationFail(`after_block:${afterId}:not_sibling`)
|
|
83
|
+
: after + 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function richTextNode(
|
|
87
|
+
blockId: string,
|
|
88
|
+
kind: string,
|
|
89
|
+
parentId: string | undefined,
|
|
90
|
+
index: number,
|
|
91
|
+
): { node: RichTextBlockNode; locale: RichTextBlockLocale } {
|
|
92
|
+
const protoCase = richTextProtoCaseByKind.get(kind);
|
|
93
|
+
if (!protoCase) return operationFail(`block_kind:${kind}`);
|
|
94
|
+
return {
|
|
95
|
+
node: fromJson(RichTextBlockNodeSchema, {
|
|
96
|
+
block: { id: blockId, [protoCase]: {} },
|
|
97
|
+
placement: {
|
|
98
|
+
index,
|
|
99
|
+
...(parentId ? { parentBlockId: parentId } : {}),
|
|
100
|
+
},
|
|
101
|
+
}) as RichTextBlockNode,
|
|
102
|
+
locale: fromJson(RichTextBlockLocaleSchema, {
|
|
103
|
+
blockId,
|
|
104
|
+
[protoCase]: {},
|
|
105
|
+
}) as RichTextBlockLocale,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function insertBlock(
|
|
110
|
+
yDocument: Y.Doc,
|
|
111
|
+
value: Extract<
|
|
112
|
+
AIDocumentOperation["operation"],
|
|
113
|
+
{ case: "insertBlock" }
|
|
114
|
+
>["value"],
|
|
115
|
+
): void {
|
|
116
|
+
const id = requiredHandle(value.blockHandle, "insert:block");
|
|
117
|
+
const kind = requiredHandle(value.kind, "insert:kind");
|
|
118
|
+
const parentId = value.parentBlockHandle ?? null;
|
|
119
|
+
const index = siblingPosition(yDocument, parentId, value.afterBlockHandle);
|
|
120
|
+
const created = richTextNode(id, kind, value.parentBlockHandle, index);
|
|
121
|
+
insertRichTextBlockNode(yDocument, created.node);
|
|
122
|
+
insertRichTextBlockLocale(yDocument, created.locale);
|
|
123
|
+
const content = (
|
|
124
|
+
richTextBlockCatalog as Readonly<
|
|
125
|
+
Record<string, { readonly content: string }>
|
|
126
|
+
>
|
|
127
|
+
)[kind]?.content;
|
|
128
|
+
if (
|
|
129
|
+
roomLocaleRole(yDocument) === "source" &&
|
|
130
|
+
roomDocumentType(yDocument) === "page" &&
|
|
131
|
+
(content === "inline" || content === "locale_text")
|
|
132
|
+
) {
|
|
133
|
+
const localePayload = yMap(
|
|
134
|
+
roomNode(yDocument, {
|
|
135
|
+
id,
|
|
136
|
+
family: "rich_text",
|
|
137
|
+
locale: true,
|
|
138
|
+
}).get("payload"),
|
|
139
|
+
"insert:locale",
|
|
140
|
+
);
|
|
141
|
+
localePayload.set(
|
|
142
|
+
"content",
|
|
143
|
+
content === "inline" ? new Y.Array<unknown>() : new Y.Text(),
|
|
144
|
+
);
|
|
145
|
+
markBlockRoomLocaleValuePresent(
|
|
146
|
+
yDocument,
|
|
147
|
+
create(AIDocumentFieldTargetSchema, {
|
|
148
|
+
owner: { case: "blockHandle", value: id },
|
|
149
|
+
fieldHandle: "content",
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function moveBlock(
|
|
156
|
+
yDocument: Y.Doc,
|
|
157
|
+
blockId: string,
|
|
158
|
+
parentId: string | undefined,
|
|
159
|
+
afterId: string | undefined,
|
|
160
|
+
): void {
|
|
161
|
+
const snapshot = decodeCanonicalBlockRoom(
|
|
162
|
+
yDocument,
|
|
163
|
+
roomDocumentType(yDocument),
|
|
164
|
+
);
|
|
165
|
+
const node = snapshot.baseNodes.find(({ id }) => id === blockId);
|
|
166
|
+
if (!node) return operationFail(`move:block:${blockId}`);
|
|
167
|
+
const index = siblingPosition(yDocument, parentId ?? null, afterId, blockId);
|
|
168
|
+
moveRichTextBlockNode(yDocument, blockId, {
|
|
169
|
+
...(parentId ? { parentBlockId: parentId } : {}),
|
|
170
|
+
index,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function replaceBlockKind(
|
|
175
|
+
yDocument: Y.Doc,
|
|
176
|
+
blockId: string,
|
|
177
|
+
kind: string,
|
|
178
|
+
): void {
|
|
179
|
+
const snapshot = decodeCanonicalBlockRoom(
|
|
180
|
+
yDocument,
|
|
181
|
+
roomDocumentType(yDocument),
|
|
182
|
+
);
|
|
183
|
+
const current = snapshot.baseNodes.find(({ id }) => id === blockId);
|
|
184
|
+
if (!current) return operationFail(`replace:block:${blockId}`);
|
|
185
|
+
const protoCase = richTextProtoCaseByKind.get(kind);
|
|
186
|
+
const currentCatalogKind =
|
|
187
|
+
richTextBlockKindByProtoCase[
|
|
188
|
+
current.kind as keyof typeof richTextBlockKindByProtoCase
|
|
189
|
+
];
|
|
190
|
+
if (!protoCase) return operationFail(`replace:kind:${kind}`);
|
|
191
|
+
const data = fromJson(RichTextBlockDataSchema, {
|
|
192
|
+
[protoCase]: {},
|
|
193
|
+
}) as RichTextBlockData;
|
|
194
|
+
const locale = fromJson(RichTextBlockLocaleDataSchema, {
|
|
195
|
+
[protoCase]: {},
|
|
196
|
+
}) as RichTextBlockLocaleData;
|
|
197
|
+
replaceRichTextBlockData(yDocument, blockId, data, {
|
|
198
|
+
expectedKind: currentCatalogKind as RichTextBlockKind,
|
|
199
|
+
localeData: locale,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function applyOperation(
|
|
204
|
+
yDocument: Y.Doc,
|
|
205
|
+
documentType: BlockRoomDocumentType,
|
|
206
|
+
operation: AIDocumentOperation,
|
|
207
|
+
): void {
|
|
208
|
+
if (
|
|
209
|
+
documentType === "page" &&
|
|
210
|
+
applyPageStructureAIDocumentOperation(yDocument, operation)
|
|
211
|
+
) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
switch (operation.operation.case) {
|
|
215
|
+
case "setField":
|
|
216
|
+
setAIDocumentField(
|
|
217
|
+
yDocument,
|
|
218
|
+
operation.operation.value.target,
|
|
219
|
+
operation.operation.value.value,
|
|
220
|
+
);
|
|
221
|
+
return;
|
|
222
|
+
case "unsetField":
|
|
223
|
+
unsetAIDocumentField(yDocument, operation.operation.value.target);
|
|
224
|
+
return;
|
|
225
|
+
case "insertBlock":
|
|
226
|
+
insertBlock(yDocument, operation.operation.value);
|
|
227
|
+
return;
|
|
228
|
+
case "deleteBlock":
|
|
229
|
+
deleteBlockRoomBaseNode(
|
|
230
|
+
yDocument,
|
|
231
|
+
requiredHandle(operation.operation.value.blockHandle, "delete:block"),
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
case "moveBlock":
|
|
235
|
+
moveBlock(
|
|
236
|
+
yDocument,
|
|
237
|
+
requiredHandle(operation.operation.value.blockHandle, "move:block"),
|
|
238
|
+
operation.operation.value.parentBlockHandle,
|
|
239
|
+
operation.operation.value.afterBlockHandle,
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
case "replaceBlockKind":
|
|
243
|
+
replaceBlockKind(
|
|
244
|
+
yDocument,
|
|
245
|
+
requiredHandle(operation.operation.value.blockHandle, "replace:block"),
|
|
246
|
+
requiredHandle(operation.operation.value.kind, "replace:kind"),
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
case "attachFile":
|
|
250
|
+
attachAIDocumentFile(
|
|
251
|
+
yDocument,
|
|
252
|
+
operation.operation.value.target,
|
|
253
|
+
operation.operation.value.fileHandle,
|
|
254
|
+
);
|
|
255
|
+
return;
|
|
256
|
+
case "detachFile":
|
|
257
|
+
detachAIDocumentFile(yDocument, operation.operation.value.target);
|
|
258
|
+
return;
|
|
259
|
+
case "insertRelationItem":
|
|
260
|
+
case "deleteRelationItem":
|
|
261
|
+
case "moveRelationItem":
|
|
262
|
+
case "createTranslation":
|
|
263
|
+
case "deleteTranslation":
|
|
264
|
+
operationFail(`unsupported:${operation.operation.case}`);
|
|
265
|
+
case undefined:
|
|
266
|
+
operationFail("missing_kind");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Applies one already-authorized and normalized DCDP batch to a Block Room.
|
|
272
|
+
* The resident document stays untouched unless the complete result validates.
|
|
273
|
+
*/
|
|
274
|
+
export function applyAIDocumentOperationsToBlockRoom(
|
|
275
|
+
yDocument: Y.Doc,
|
|
276
|
+
documentType: BlockRoomDocumentType,
|
|
277
|
+
operations: readonly AIDocumentOperation[],
|
|
278
|
+
options: ApplyAIDocumentOperationsOptions,
|
|
279
|
+
): boolean {
|
|
280
|
+
if (roomDocumentType(yDocument) !== documentType)
|
|
281
|
+
operationFail("document_type");
|
|
282
|
+
if (
|
|
283
|
+
!options.expectedRoomLocale ||
|
|
284
|
+
roomLocale(yDocument) !== options.expectedRoomLocale
|
|
285
|
+
)
|
|
286
|
+
operationFail("room_locale");
|
|
287
|
+
if (!options.origin || typeof options.origin !== "object")
|
|
288
|
+
operationFail("origin");
|
|
289
|
+
if (operations.length === 0) return false;
|
|
290
|
+
if (
|
|
291
|
+
operations.some(({ operation }) => {
|
|
292
|
+
if (operation.case !== "unsetField") return false;
|
|
293
|
+
if (roomLocaleRole(yDocument) === "target") return true;
|
|
294
|
+
try {
|
|
295
|
+
canonicalBlockRoomLocaleValueTarget(yDocument, operation.value.target);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return operation.value.target?.fieldHandle === "tableContent";
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
) {
|
|
302
|
+
operationFail("locale_clear");
|
|
303
|
+
}
|
|
304
|
+
const before = Y.encodeStateVector(yDocument);
|
|
305
|
+
const working = new Y.Doc();
|
|
306
|
+
let update: Uint8Array;
|
|
307
|
+
try {
|
|
308
|
+
Y.applyUpdate(working, Y.encodeStateAsUpdate(yDocument));
|
|
309
|
+
for (const operation of operations)
|
|
310
|
+
applyOperation(working, documentType, operation);
|
|
311
|
+
assertBlockRoomLocaleChangeAllowed(yDocument, working, documentType);
|
|
312
|
+
materializeCanonicalBlockRoom(working, documentType);
|
|
313
|
+
update = Y.encodeStateAsUpdate(working, before);
|
|
314
|
+
} finally {
|
|
315
|
+
working.destroy();
|
|
316
|
+
}
|
|
317
|
+
Y.applyUpdate(yDocument, update, options.origin);
|
|
318
|
+
return true;
|
|
319
|
+
}
|