@v7-cms/adapters 0.0.0-alpha.9

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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Local editing through the browser's File System Access API.
3
+ *
4
+ * The author picks their repository folder once, and the editor reads and writes it directly: no
5
+ * server, no token, no commit until they choose to push. This is the nicest local mode and also
6
+ * the least portable — only Chromium browsers implement the API — which is why the proxy exists
7
+ * as well.
8
+ *
9
+ * The handle is kept in IndexedDB because it cannot be serialised anywhere else, and permission is
10
+ * re-requested on each visit rather than assumed.
11
+ */
12
+ import { type DirEntry, type FileContents, type MediaStore, type StorageAdapter, type WriteOptions, type WriteResult } from '@v7-cms/core/storage';
13
+ export interface DirectoryPickerWindow {
14
+ showDirectoryPicker(options?: {
15
+ mode?: 'read' | 'readwrite';
16
+ }): Promise<FileSystemDirectoryHandle>;
17
+ }
18
+ export declare function supportsFileSystemAccess(): boolean;
19
+ export declare class FileSystemAdapter implements StorageAdapter {
20
+ readonly kind: "fs-access";
21
+ private root;
22
+ /** Media folder inside the picked directory. */
23
+ private mediaFolder;
24
+ /** Content of the last read, so a write can detect an outside change. */
25
+ private seen;
26
+ constructor(root: FileSystemDirectoryHandle, options?: {
27
+ mediaFolder?: string;
28
+ });
29
+ init(): Promise<void>;
30
+ private parts;
31
+ private directoryFor;
32
+ private fileHandle;
33
+ listDir(path: string): Promise<DirEntry[]>;
34
+ exists(path: string): Promise<boolean>;
35
+ readFile(path: string): Promise<FileContents>;
36
+ readBinary(path: string): Promise<Uint8Array>;
37
+ writeFile(path: string, contents: string | Uint8Array, options: WriteOptions): Promise<WriteResult>;
38
+ deleteFile(path: string, _options: WriteOptions): Promise<void>;
39
+ /**
40
+ * There is no branch concept on a plain folder. Editorial workflow keeps its state in a file
41
+ * inside the repository instead, so this reports a single implicit branch.
42
+ */
43
+ listBranches(): Promise<string[]>;
44
+ createBranch(): Promise<void>;
45
+ media(): MediaStore;
46
+ }
47
+ /** Ask the author for their repository folder. Must be called from a user gesture. */
48
+ export declare function pickDirectory(): Promise<FileSystemDirectoryHandle>;
49
+ /**
50
+ * Remember the picked folder between visits. Only the handle is stored, and the browser still
51
+ * asks for permission again — the API requires a gesture, and it keeps the grant honest.
52
+ */
53
+ export declare const handleStore: {
54
+ save(handle: FileSystemDirectoryHandle): Promise<void>;
55
+ load(): Promise<FileSystemDirectoryHandle | undefined>;
56
+ clear(): Promise<void>;
57
+ };
58
+ /** Whether the stored handle is still usable without prompting. */
59
+ export declare function hasPermission(handle: FileSystemDirectoryHandle, mode?: 'read' | 'readwrite'): Promise<boolean>;
60
+ /** Ask for permission again, which needs a user gesture. */
61
+ export declare function requestPermission(handle: FileSystemDirectoryHandle, mode?: 'read' | 'readwrite'): Promise<boolean>;
62
+ //# sourceMappingURL=fs-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-access.d.ts","sourceRoot":"","sources":["../src/fs-access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,qBAAqB;IACpC,mBAAmB,CAAC,OAAO,CAAC,EAAE;QAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;KAC7B,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACxC;AAED,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAED,qBAAa,iBAAkB,YAAW,cAAc;IACtD,QAAQ,CAAC,IAAI,EAAG,WAAW,CAAU;IACrC,OAAO,CAAC,IAAI,CAA4B;IACxC,gDAAgD;IAChD,OAAO,CAAC,WAAW,CAAS;IAC5B,yEAAyE;IACzE,OAAO,CAAC,IAAI,CAA6B;gBAE7B,IAAI,EAAE,yBAAyB,EAAE,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO;IAK7E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,OAAO,CAAC,KAAK;YAIC,YAAY;YAQZ,UAAU;IAQlB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAW1C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAStC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAW7C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM7C,SAAS,CACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,UAAU,EAC7B,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,WAAW,CAAC;IA2BjB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrE;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAIjC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC,KAAK,IAAI,UAAU;CA6BpB;AAED,sFAAsF;AACtF,wBAAsB,aAAa,IAAI,OAAO,CAAC,yBAAyB,CAAC,CAIxE;AAKD;;;GAGG;AACH,eAAO,MAAM,WAAW;iBACH,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAU9C,OAAO,CAAC,yBAAyB,GAAG,SAAS,CAAC;aAe7C,OAAO,CAAC,IAAI,CAAC;CAc7B,CAAC;AAEF,mEAAmE;AACnE,wBAAsB,aAAa,CACjC,MAAM,EAAE,yBAAyB,EACjC,IAAI,GAAE,MAAM,GAAG,WAAyB,GACvC,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED,4DAA4D;AAC5D,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,yBAAyB,EACjC,IAAI,GAAE,MAAM,GAAG,WAAyB,GACvC,OAAO,CAAC,OAAO,CAAC,CAQlB"}
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Local editing through the browser's File System Access API.
3
+ *
4
+ * The author picks their repository folder once, and the editor reads and writes it directly: no
5
+ * server, no token, no commit until they choose to push. This is the nicest local mode and also
6
+ * the least portable — only Chromium browsers implement the API — which is why the proxy exists
7
+ * as well.
8
+ *
9
+ * The handle is kept in IndexedDB because it cannot be serialised anywhere else, and permission is
10
+ * re-requested on each visit rather than assumed.
11
+ */
12
+ import { ConflictError, NotFoundError, } from '@v7-cms/core/storage';
13
+ export function supportsFileSystemAccess() {
14
+ return typeof window !== 'undefined' && 'showDirectoryPicker' in window;
15
+ }
16
+ export class FileSystemAdapter {
17
+ kind = 'fs-access';
18
+ root;
19
+ /** Media folder inside the picked directory. */
20
+ mediaFolder;
21
+ /** Content of the last read, so a write can detect an outside change. */
22
+ seen = new Map();
23
+ constructor(root, options = {}) {
24
+ this.root = root;
25
+ this.mediaFolder = options.mediaFolder ?? 'public/images/uploads';
26
+ }
27
+ async init() {
28
+ // Reading the root is the cheapest way to confirm permission is still granted.
29
+ await this.root.keys().next();
30
+ }
31
+ parts(path) {
32
+ return path.split('/').filter(Boolean);
33
+ }
34
+ async directoryFor(parts, create = false) {
35
+ let current = this.root;
36
+ for (const part of parts) {
37
+ current = await current.getDirectoryHandle(part, { create });
38
+ }
39
+ return current;
40
+ }
41
+ async fileHandle(path, create = false) {
42
+ const parts = this.parts(path);
43
+ const name = parts.pop();
44
+ if (!name)
45
+ throw new NotFoundError(path);
46
+ const directory = await this.directoryFor(parts, create);
47
+ return directory.getFileHandle(name, { create });
48
+ }
49
+ async listDir(path) {
50
+ const directory = await this.directoryFor(this.parts(path)).catch(() => null);
51
+ if (!directory)
52
+ return [];
53
+ const entries = [];
54
+ for await (const [name, handle] of directory.entries()) {
55
+ if (name.startsWith('.'))
56
+ continue;
57
+ entries.push({ name, type: handle.kind === 'directory' ? 'dir' : 'file' });
58
+ }
59
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
60
+ }
61
+ async exists(path) {
62
+ try {
63
+ await this.fileHandle(path);
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ async readFile(path) {
71
+ const handle = await this.fileHandle(path).catch(() => null);
72
+ if (!handle)
73
+ throw new NotFoundError(path);
74
+ const file = await handle.getFile();
75
+ const text = await file.text();
76
+ // The mtime doubles as the revision: it is what a later write checks against.
77
+ const sha = String(file.lastModified);
78
+ this.seen.set(path, sha);
79
+ return { text, sha };
80
+ }
81
+ async readBinary(path) {
82
+ const handle = await this.fileHandle(path).catch(() => null);
83
+ if (!handle)
84
+ throw new NotFoundError(path);
85
+ return new Uint8Array(await (await handle.getFile()).arrayBuffer());
86
+ }
87
+ async writeFile(path, contents, options) {
88
+ // If the file changed on disk since it was read, refuse rather than clobber. This is the only
89
+ // conflict signal the filesystem offers, and it is worth honouring: an editor that silently
90
+ // overwrites what another process wrote is worse than one that asks.
91
+ if (options.sha !== undefined) {
92
+ const handle = await this.fileHandle(path).catch(() => null);
93
+ if (handle) {
94
+ const current = String((await handle.getFile()).lastModified);
95
+ const original = this.seen.get(path);
96
+ if (original !== undefined && current !== original) {
97
+ throw new ConflictError(`"${path}" changed since it was read.`);
98
+ }
99
+ }
100
+ }
101
+ const handle = await this.fileHandle(path, true);
102
+ const writable = await handle.createWritable();
103
+ await writable.write(typeof contents === 'string' ? contents : new Blob([contents]));
104
+ await writable.close();
105
+ const written = String((await handle.getFile()).lastModified);
106
+ this.seen.set(path, written);
107
+ return { sha: written };
108
+ }
109
+ async deleteFile(path, _options) {
110
+ void _options;
111
+ const parts = this.parts(path);
112
+ const name = parts.pop();
113
+ if (!name)
114
+ throw new NotFoundError(path);
115
+ const directory = await this.directoryFor(parts).catch(() => null);
116
+ if (!directory)
117
+ throw new NotFoundError(path);
118
+ await directory.removeEntry(name);
119
+ this.seen.delete(path);
120
+ }
121
+ /**
122
+ * There is no branch concept on a plain folder. Editorial workflow keeps its state in a file
123
+ * inside the repository instead, so this reports a single implicit branch.
124
+ */
125
+ async listBranches() {
126
+ return ['local'];
127
+ }
128
+ async createBranch() {
129
+ /* Nothing to create: the working copy is the branch. */
130
+ }
131
+ media() {
132
+ return {
133
+ upload: async (file, options) => {
134
+ const name = options.filename ?? file.name;
135
+ const path = `${options.folder.replace(/\/+$/, '')}/${name}`;
136
+ const result = await this.writeFile(path, file.data, { message: `Upload ${name}` });
137
+ return {
138
+ path,
139
+ url: `/${path}`,
140
+ size: file.data.byteLength,
141
+ name,
142
+ ...(result.sha ? { sha: result.sha } : {}),
143
+ };
144
+ },
145
+ list: async (folder) => {
146
+ const entries = await this.listDir(folder);
147
+ return entries
148
+ .filter((entry) => entry.type === 'file')
149
+ .map((entry) => ({
150
+ path: `${folder}/${entry.name}`,
151
+ url: `/${folder}/${entry.name}`,
152
+ name: entry.name,
153
+ }));
154
+ },
155
+ remove: async (path) => {
156
+ await this.deleteFile(path, { message: `Delete ${path}` });
157
+ },
158
+ };
159
+ }
160
+ }
161
+ /** Ask the author for their repository folder. Must be called from a user gesture. */
162
+ export async function pickDirectory() {
163
+ const picker = window.showDirectoryPicker;
164
+ if (!picker)
165
+ throw new Error('This browser cannot open a local folder. Use the proxy instead.');
166
+ return picker({ mode: 'readwrite' });
167
+ }
168
+ const DB_NAME = 'v7-cms';
169
+ const STORE = 'handles';
170
+ /**
171
+ * Remember the picked folder between visits. Only the handle is stored, and the browser still
172
+ * asks for permission again — the API requires a gesture, and it keeps the grant honest.
173
+ */
174
+ export const handleStore = {
175
+ async save(handle) {
176
+ const db = await open();
177
+ await new Promise((resolve, reject) => {
178
+ const tx = db.transaction(STORE, 'readwrite');
179
+ tx.objectStore(STORE).put(handle, 'root');
180
+ tx.oncomplete = () => resolve();
181
+ tx.onerror = () => reject(tx.error ?? new Error('Could not remember the folder.'));
182
+ });
183
+ db.close();
184
+ },
185
+ async load() {
186
+ try {
187
+ const db = await open();
188
+ const handle = await new Promise((resolve, reject) => {
189
+ const tx = db.transaction(STORE, 'readonly');
190
+ const request = tx.objectStore(STORE).get('root');
191
+ request.onsuccess = () => resolve(request.result);
192
+ request.onerror = () => reject(request.error ?? new Error('unavailable'));
193
+ });
194
+ db.close();
195
+ return handle;
196
+ }
197
+ catch {
198
+ return undefined;
199
+ }
200
+ },
201
+ async clear() {
202
+ try {
203
+ const db = await open();
204
+ await new Promise((resolve) => {
205
+ const tx = db.transaction(STORE, 'readwrite');
206
+ tx.objectStore(STORE).delete('root');
207
+ tx.oncomplete = () => resolve();
208
+ tx.onerror = () => resolve();
209
+ });
210
+ db.close();
211
+ }
212
+ catch {
213
+ /* Nothing to clear. */
214
+ }
215
+ },
216
+ };
217
+ /** Whether the stored handle is still usable without prompting. */
218
+ export async function hasPermission(handle, mode = 'readwrite') {
219
+ const query = handle.queryPermission;
220
+ if (!query)
221
+ return true;
222
+ return (await query.call(handle, { mode })) === 'granted';
223
+ }
224
+ /** Ask for permission again, which needs a user gesture. */
225
+ export async function requestPermission(handle, mode = 'readwrite') {
226
+ const request = handle.requestPermission;
227
+ if (!request)
228
+ return true;
229
+ return (await request.call(handle, { mode })) === 'granted';
230
+ }
231
+ function open() {
232
+ return new Promise((resolve, reject) => {
233
+ const request = indexedDB.open(DB_NAME, 1);
234
+ request.onupgradeneeded = () => request.result.createObjectStore(STORE);
235
+ request.onsuccess = () => resolve(request.result);
236
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB unavailable'));
237
+ });
238
+ }
239
+ //# sourceMappingURL=fs-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-access.js","sourceRoot":"","sources":["../src/fs-access.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The GitHub backend.
3
+ *
4
+ * Reads and writes repository contents through the REST API. Two things shape the design:
5
+ *
6
+ * - Every write carries the sha it was based on, so a change made elsewhere in the meantime is
7
+ * refused rather than silently overwritten. `ConflictError` is what the editor reacts to.
8
+ * - Listing uses the trees API once per directory tree and caches it, instead of one request per
9
+ * file, because a content folder with a few hundred entries would otherwise cost a few hundred
10
+ * requests against a rate limit.
11
+ */
12
+ import { type AccountInfo, type DirEntry, type FileContents, type MediaStore, type StorageAdapter, type WriteOptions, type WriteResult } from '@v7-cms/core/storage';
13
+ export interface GitHubAdapterOptions {
14
+ owner: string;
15
+ repo: string;
16
+ branch?: string;
17
+ token: string;
18
+ /** Override for GitHub Enterprise. */
19
+ apiRoot?: string;
20
+ /** Injected in tests so the network can be faked. */
21
+ fetch?: typeof globalThis.fetch;
22
+ /** How the token was obtained; reported by `account()` so the UI can explain sign-out. */
23
+ via?: 'oauth' | 'token';
24
+ /** Commit author. GitHub attributes the commit to the token's user without this. */
25
+ author?: {
26
+ name: string;
27
+ email: string;
28
+ };
29
+ }
30
+ export declare class GitHubAdapter implements StorageAdapter {
31
+ readonly kind: "github";
32
+ private readonly owner;
33
+ private readonly repo;
34
+ private readonly apiRoot;
35
+ private readonly request;
36
+ private token;
37
+ private branch;
38
+ private via;
39
+ /** Cached after the first lookup, so the shell does not re-request on every render. */
40
+ private accountInfo;
41
+ private accountFetched;
42
+ private author?;
43
+ /** Path -> blob sha, filled from one tree request per branch. */
44
+ private tree;
45
+ private dirCache;
46
+ constructor(options: GitHubAdapterOptions);
47
+ init(): Promise<void>;
48
+ /**
49
+ * The authenticated user, fetched once.
50
+ *
51
+ * The token alone says nothing about who holds it, and an author who signed in on a shared
52
+ * machine needs to see that. `init()` already proved the token works, so a failure here is not
53
+ * worth failing the editor over — it returns undefined and the panel falls back to the repo.
54
+ */
55
+ account(): Promise<AccountInfo | undefined>;
56
+ /** Swap the token after a sign-in, so the same adapter can outlive a session change. */
57
+ setToken(token: string): void;
58
+ setBranch(branch: string): void;
59
+ private call;
60
+ /**
61
+ * One request for the whole branch, then everything is served from it. Cleared on every write,
62
+ * so a stale listing is never shown after a change.
63
+ */
64
+ private loadTree;
65
+ private forget;
66
+ listDir(path: string): Promise<DirEntry[]>;
67
+ exists(path: string): Promise<boolean>;
68
+ readFile(path: string): Promise<FileContents>;
69
+ readBinary(path: string): Promise<Uint8Array>;
70
+ writeFile(path: string, contents: string | Uint8Array, options: WriteOptions): Promise<WriteResult>;
71
+ private currentSha;
72
+ deleteFile(path: string, options: WriteOptions): Promise<void>;
73
+ listBranches(): Promise<string[]>;
74
+ createBranch(name: string, from: string): Promise<void>;
75
+ /** Media lives in the repository for this backend, so it reuses the same writes. */
76
+ media(): MediaStore;
77
+ }
78
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/github/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,YAAY,EAEjB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,0FAA0F;IAC1F,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,oFAAoF;IACpF,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAQD,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,GAAG,CAAoB;IAC/B,uFAAuF;IACvF,OAAO,CAAC,WAAW,CAA0B;IAC7C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,iEAAiE;IACjE,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,QAAQ,CAAiC;gBAErC,OAAO,EAAE,oBAAoB;IAenC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B;;;;;;OAMG;IACG,OAAO,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAwBjD,wFAAwF;IACxF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQ7B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;YAMjB,IAAI;IAalB;;;OAGG;YACW,QAAQ;IActB,OAAO,CAAC,MAAM;IAKR,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAqB1C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAa7C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAW7C,SAAS,CACb,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,UAAU,EAC7B,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,WAAW,CAAC;YAyCT,UAAU;IAalB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9D,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAMjC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB7D,oFAAoF;IACpF,KAAK,IAAI,UAAU;CA0BpB"}