@moloodev/editor-sdk 0.0.0-stage → 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.
Files changed (110) hide show
  1. package/package.json +62 -3
  2. package/src/channel.ts +287 -0
  3. package/src/chrome-manifest.ts +111 -0
  4. package/src/commands/clipboard/router.ts +99 -0
  5. package/src/commands/clipboard/types.ts +20 -0
  6. package/src/commands/history/stack.ts +53 -0
  7. package/src/commands/icons/catalog.ts +172 -0
  8. package/src/commands/index.ts +100 -0
  9. package/src/commands/keys/display.ts +16 -0
  10. package/src/commands/keys/normalize.ts +55 -0
  11. package/src/commands/keys/reserved.ts +62 -0
  12. package/src/commands/keys/validate.ts +77 -0
  13. package/src/commands/manifest/configuration.ts +59 -0
  14. package/src/commands/manifest/contribution-manifest-schema.ts +56 -0
  15. package/src/commands/manifest/editor-document.ts +501 -0
  16. package/src/commands/manifest/ids.ts +2 -0
  17. package/src/commands/manifest/merge.ts +215 -0
  18. package/src/commands/manifest/types.ts +113 -0
  19. package/src/commands/manifest/validate-manifest.ts +45 -0
  20. package/src/commands/react/context.tsx +249 -0
  21. package/src/commands/react/hotkey-defaults.ts +11 -0
  22. package/src/commands/react/scope-context.tsx +31 -0
  23. package/src/commands/react/scope-provider.tsx +123 -0
  24. package/src/commands/react/store.ts +182 -0
  25. package/src/commands/registry/create.ts +55 -0
  26. package/src/commands/registry/types.ts +12 -0
  27. package/src/commands/when/evaluate.ts +80 -0
  28. package/src/commands/when/index.ts +19 -0
  29. package/src/commands/when/parse.ts +276 -0
  30. package/src/commands/when/types.ts +24 -0
  31. package/src/commands/when/validate.ts +20 -0
  32. package/src/crdt/awareness.ts +29 -0
  33. package/src/crdt/bytes.ts +26 -0
  34. package/src/crdt/connect.ts +285 -0
  35. package/src/crdt/diff.ts +86 -0
  36. package/src/crdt/doc.ts +67 -0
  37. package/src/crdt/front-matter.ts +45 -0
  38. package/src/crdt/index.ts +76 -0
  39. package/src/crdt/persist.ts +40 -0
  40. package/src/crdt/protocol.ts +119 -0
  41. package/src/crdt/shared.ts +104 -0
  42. package/src/crdt/text-body.ts +125 -0
  43. package/src/crdt/undo.ts +59 -0
  44. package/src/crdt/vite-plugin.ts +42 -0
  45. package/src/crdt/yjs.ts +16 -0
  46. package/src/editor-common.ts +160 -0
  47. package/src/editor-id.ts +56 -0
  48. package/src/frame-attributes.ts +9 -0
  49. package/src/index.ts +229 -0
  50. package/src/json-schema.ts +13 -0
  51. package/src/layout/bind-toolbar.ts +60 -0
  52. package/src/layout/catalog.ts +82 -0
  53. package/src/layout/chrome/command-affordances.ts +47 -0
  54. package/src/layout/chrome/command-layout-state.ts +42 -0
  55. package/src/layout/chrome/editor-menu.tsx +358 -0
  56. package/src/layout/chrome/editor-shortcut-keys.tsx +27 -0
  57. package/src/layout/chrome/menu-styles.ts +3 -0
  58. package/src/layout/chrome/mid-menu-nodes.ts +27 -0
  59. package/src/layout/chrome/use-editor-command-affordances.ts +33 -0
  60. package/src/layout/chrome/visible-menu-leaves.ts +15 -0
  61. package/src/layout/content-error-notice.tsx +31 -0
  62. package/src/layout/define-editor-layout.ts +60 -0
  63. package/src/layout/define-editor-layout.types.ts +57 -0
  64. package/src/layout/doc-undo-bridge.tsx +22 -0
  65. package/src/layout/dock-zones.tsx +61 -0
  66. package/src/layout/drop.ts +39 -0
  67. package/src/layout/editor-layout.tsx +287 -0
  68. package/src/layout/editor-loading.tsx +23 -0
  69. package/src/layout/frame-layout-binding.tsx +74 -0
  70. package/src/layout/frame.ts +23 -0
  71. package/src/layout/hide.ts +103 -0
  72. package/src/layout/index.ts +14 -0
  73. package/src/layout/layout-binding.tsx +42 -0
  74. package/src/layout/layout-storage-key.ts +6 -0
  75. package/src/layout/layout-values.ts +48 -0
  76. package/src/layout/mutate.ts +486 -0
  77. package/src/layout/nesting-context.tsx +20 -0
  78. package/src/layout/panel-header-action.tsx +29 -0
  79. package/src/layout/panel-header.tsx +168 -0
  80. package/src/layout/panel-hide-menu.tsx +6 -0
  81. package/src/layout/panel-menu.tsx +85 -0
  82. package/src/layout/panel-section.tsx +114 -0
  83. package/src/layout/panel-size.ts +94 -0
  84. package/src/layout/panel-title.tsx +5 -0
  85. package/src/layout/registry.tsx +333 -0
  86. package/src/layout/restore-panel.ts +26 -0
  87. package/src/layout/slots-context.tsx +189 -0
  88. package/src/layout/tab-button.tsx +60 -0
  89. package/src/layout/toolbar-scroll.ts +22 -0
  90. package/src/layout/toolbar.tsx +168 -0
  91. package/src/layout/types.ts +41 -0
  92. package/src/layout/use-collapsed-sections.ts +50 -0
  93. package/src/layout/validate-layout-spec.ts +143 -0
  94. package/src/message-samples.ts +377 -0
  95. package/src/origin-headers.ts +57 -0
  96. package/src/print/index.ts +10 -0
  97. package/src/print/inspect.ts +16 -0
  98. package/src/print/schema.ts +122 -0
  99. package/src/protocol-entry.ts +19 -0
  100. package/src/protocol.ts +471 -0
  101. package/src/render-result.ts +25 -0
  102. package/src/view/editor-chrome.ts +28 -0
  103. package/src/view/hooks.ts +260 -0
  104. package/src/view/mount-editor-view.ts +36 -0
  105. package/src/view/port-binding.ts +127 -0
  106. package/src/view/port-commands.ts +36 -0
  107. package/src/view/port.ts +35 -0
  108. package/src/view/theme-class.ts +11 -0
  109. package/src/view.ts +313 -0
  110. package/README.md +0 -3
package/src/view.ts ADDED
@@ -0,0 +1,313 @@
1
+ export { EDITOR_COMMON_CONTRIBUTES } from "./editor-common";
2
+ export * from "./view/hooks";
3
+ export { PortEditorView } from "./view/port-binding";
4
+ export { PortCommandsProvider, bindViewCommands, shortcutForwarder } from "./view/port-commands";
5
+ export { bindThemeClass } from "./view/theme-class";
6
+
7
+ import {
8
+ REMOTE_ORIGIN,
9
+ applyUpdate,
10
+ createDoc,
11
+ fromBase64,
12
+ toBase64,
13
+ type Doc,
14
+ } from "./crdt";
15
+ import {
16
+ createEditorChannel,
17
+ type EditorChannel,
18
+ type HandshakeResult,
19
+ } from "./channel";
20
+ import type {
21
+ EditorAccess,
22
+ EditorEvent,
23
+ EditorFailure,
24
+ EditorOperation,
25
+ EditorRequestType,
26
+ EditorResponse,
27
+ EditorSession,
28
+ } from "./index";
29
+ import { EDITOR_IMPORT_BYTES_MAX, parseEditorId, type ChromeManifest } from "./protocol";
30
+ import { editorPort } from "./view/port";
31
+
32
+ export type EditorViewSnapshot = {
33
+ session: EditorSession | null;
34
+ access: EditorAccess | null;
35
+ theme: "dark" | "light" | null;
36
+ doc: Doc;
37
+ /** How many `host.createRequested` events have arrived. */
38
+ createRequested: number;
39
+ };
40
+
41
+ type CommandStateBody = Extract<EditorEvent, { type: "command.state" }>["body"];
42
+
43
+ type ShortcutBody = Extract<EditorEvent, { type: "command.shortcut" }>["body"];
44
+
45
+ type SubmittedChrome = {
46
+ keybindings: readonly ShortcutBody[];
47
+ commandIds: ReadonlySet<string>;
48
+ };
49
+
50
+ export type EditorViewCall = {
51
+ [Type in Exclude<EditorRequestType, "doc.append">]: (
52
+ body: EditorOperation[Type]["body"],
53
+ ) => Promise<EditorResponse<Type>>;
54
+ };
55
+
56
+ export type EditorView = {
57
+ readonly handshake: Promise<HandshakeResult>;
58
+ getSnapshot(): EditorViewSnapshot;
59
+ subscribe(listener: () => void): () => void;
60
+ onImportFile(listener: (file: File) => void): () => void;
61
+ onCommand(listener: (commandId: string) => void): () => void;
62
+ sendCommandState(state: CommandStateBody): boolean;
63
+ sendShortcut(body: ShortcutBody): boolean;
64
+ close(): void;
65
+ readonly call: EditorViewCall;
66
+ };
67
+
68
+ /** `parent` is the `parent` query value. Null means the query is missing. */
69
+ export type EditorViewDeps = {
70
+ parent: string | null;
71
+ editorId: string;
72
+ postMessage(
73
+ message: unknown,
74
+ targetOrigin: string,
75
+ transfer: readonly MessagePort[],
76
+ ): void;
77
+ createChannel(): MessageChannel;
78
+ };
79
+
80
+ function absoluteHttpOrigin(parent: string | null): string | undefined {
81
+ if (parent === null || parent.length === 0) return undefined;
82
+ let url: URL;
83
+ try {
84
+ url = new URL(parent);
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
89
+ if (url.origin !== parent) return undefined;
90
+ return parent;
91
+ }
92
+
93
+ function submittedChrome(manifest: ChromeManifest): SubmittedChrome {
94
+ const commandIds = new Set<string>();
95
+ for (const command of manifest.contributes?.commands ?? []) {
96
+ commandIds.add(command.id);
97
+ }
98
+ return {
99
+ commandIds,
100
+ keybindings: (manifest.contributes?.keybindings ?? []).map((binding) => ({
101
+ ...binding,
102
+ })),
103
+ };
104
+ }
105
+
106
+ function closedResponse(): EditorFailure {
107
+ return { v: 1, id: "view:0", ok: false, code: "closed" };
108
+ }
109
+
110
+ function unexpected(event: never): never {
111
+ throw new Error(`unexpected editor event: ${JSON.stringify(event)}`);
112
+ }
113
+
114
+ export function connectEditorView(deps: EditorViewDeps): EditorView {
115
+ const doc = createDoc();
116
+ let snapshot: EditorViewSnapshot = {
117
+ session: null,
118
+ access: null,
119
+ theme: null,
120
+ doc,
121
+ createRequested: 0,
122
+ };
123
+ const listeners = new Set<() => void>();
124
+ const importFileListeners = new Set<(file: File) => void>();
125
+ const pendingImportFiles: File[] = [];
126
+ const commandListeners = new Set<(commandId: string) => void>();
127
+ let submitted: SubmittedChrome | null = null;
128
+ let stopped = false;
129
+ let channel: EditorChannel | undefined;
130
+
131
+ function publish(next: EditorViewSnapshot) {
132
+ if (stopped) return;
133
+ snapshot = next;
134
+ for (const listener of [...listeners]) listener();
135
+ }
136
+
137
+ function onDocUpdate(update: Uint8Array, origin: unknown) {
138
+ if (stopped || origin === REMOTE_ORIGIN) return;
139
+ const open = channel;
140
+ const asset = snapshot.session?.asset;
141
+ if (!open || !asset) return;
142
+ void open.request("doc.append", {
143
+ assetId: asset.id,
144
+ update: toBase64(update),
145
+ });
146
+ }
147
+ doc.on("update", onDocUpdate);
148
+
149
+ function applyRemote(update: string) {
150
+ try {
151
+ applyUpdate(doc, fromBase64(update), REMOTE_ORIGIN);
152
+ } catch {
153
+ return;
154
+ }
155
+ publish({ ...snapshot });
156
+ }
157
+
158
+ function onEvent(event: EditorEvent) {
159
+ if (stopped) return;
160
+ switch (event.type) {
161
+ case "file.import": {
162
+ const bytes = fromBase64(event.body.data);
163
+ if (bytes.byteLength > EDITOR_IMPORT_BYTES_MAX) return;
164
+ const file = new File([new Uint8Array(bytes)], event.body.name);
165
+ if (importFileListeners.size === 0) pendingImportFiles.push(file);
166
+ else for (const listener of [...importFileListeners]) listener(file);
167
+ return;
168
+ }
169
+ case "access":
170
+ publish({ ...snapshot, access: event.body });
171
+ return;
172
+ case "theme":
173
+ publish({ ...snapshot, theme: event.body.theme });
174
+ return;
175
+ case "session":
176
+ publish({ ...snapshot, session: event.body });
177
+ return;
178
+ case "doc.update":
179
+ if (event.body.assetId !== snapshot.session?.asset?.id) return;
180
+ applyRemote(event.body.update);
181
+ return;
182
+ case "host.createRequested":
183
+ publish({
184
+ ...snapshot,
185
+ createRequested: snapshot.createRequested + 1,
186
+ });
187
+ return;
188
+ case "command.invoke":
189
+ if (!submitted?.commandIds.has(event.body.commandId)) return;
190
+ for (const listener of [...commandListeners]) {
191
+ listener(event.body.commandId);
192
+ }
193
+ return;
194
+ case "command.shortcut":
195
+ case "command.state":
196
+ return;
197
+ default:
198
+ unexpected(event);
199
+ }
200
+ }
201
+
202
+ function request<Type extends EditorRequestType>(
203
+ type: Type,
204
+ body: EditorOperation[Type]["body"],
205
+ ): Promise<EditorResponse<Type>> {
206
+ if (!channel) return Promise.resolve(closedResponse());
207
+ return channel.request(type, body);
208
+ }
209
+
210
+ const call = {
211
+ "layout.load": (body) => request("layout.load", body),
212
+ "layout.save": (body) => request("layout.save", body),
213
+ "asset.rename": (body) => request("asset.rename", body),
214
+ "asset.read": (body) => request("asset.read", body),
215
+ "asset.list": (body) => request("asset.list", body),
216
+ "asset.listRevisions": (body) => request("asset.listRevisions", body),
217
+ "asset.getComposition": (body) => request("asset.getComposition", body),
218
+ "asset.getPublication": (body) => request("asset.getPublication", body),
219
+ "asset.confirmBundle": (body) => request("asset.confirmBundle", body),
220
+ "asset.putPublication": (body) => request("asset.putPublication", body),
221
+ "host.pick": (body) => request("host.pick", body),
222
+ "host.openAsset": (body) => request("host.openAsset", body),
223
+ "host.openInfo": (body) => request("host.openInfo", body),
224
+ "host.start": (body) => request("host.start", body),
225
+ "host.openBrowser": (body) => request("host.openBrowser", body),
226
+ "host.clipboardWrite": (body) => request("host.clipboardWrite", body),
227
+ "host.download": (body) => request("host.download", body),
228
+ "host.openPopup": (body) => request("host.openPopup", body),
229
+ "host.render": (body) => request("host.render", body),
230
+ "chrome.submit": async (body) => {
231
+ const response = await request("chrome.submit", body);
232
+ if (!response.ok) return response;
233
+ submitted = submittedChrome(body);
234
+ return response;
235
+ },
236
+ } satisfies EditorViewCall;
237
+
238
+ const origin = absoluteHttpOrigin(deps.parent);
239
+ const editor = parseEditorId(deps.editorId, true);
240
+ const handshake: Promise<HandshakeResult> =
241
+ origin === undefined || editor === undefined
242
+ ? Promise.resolve({ ok: false, code: "rejected" })
243
+ : open(origin);
244
+
245
+ function open(parentOrigin: string): Promise<HandshakeResult> {
246
+ const ports = deps.createChannel();
247
+ deps.postMessage(
248
+ { v: 1, type: "session.open", body: { editorId: deps.editorId } },
249
+ parentOrigin,
250
+ [ports.port1],
251
+ );
252
+ channel = createEditorChannel({
253
+ role: "view",
254
+ editorId: deps.editorId,
255
+ port: editorPort(ports.port2),
256
+ onEvent,
257
+ });
258
+ return channel.handshake;
259
+ }
260
+
261
+ return {
262
+ handshake,
263
+ getSnapshot() {
264
+ return snapshot;
265
+ },
266
+ subscribe(listener) {
267
+ listeners.add(listener);
268
+ return () => {
269
+ listeners.delete(listener);
270
+ };
271
+ },
272
+ onImportFile(listener) {
273
+ if (stopped) return () => {};
274
+ importFileListeners.add(listener);
275
+ for (const file of pendingImportFiles.splice(0)) listener(file);
276
+ return () => { importFileListeners.delete(listener); };
277
+ },
278
+ onCommand(listener) {
279
+ commandListeners.add(listener);
280
+ return () => {
281
+ commandListeners.delete(listener);
282
+ };
283
+ },
284
+ sendCommandState(state) {
285
+ if (!submitted?.commandIds.has(state.id)) return false;
286
+ return channel?.emit({ v: 1, type: "command.state", body: state }) ?? false;
287
+ },
288
+ sendShortcut(body) {
289
+ if (!submitted?.commandIds.has(body.commandId)) return false;
290
+ if (
291
+ !submitted.keybindings.some(
292
+ (binding) => binding.key === body.key && binding.commandId === body.commandId,
293
+ )
294
+ ) return false;
295
+ return channel?.emit({ v: 1, type: "command.shortcut", body }) ?? false;
296
+ },
297
+ close() {
298
+ if (stopped) return;
299
+ stopped = true;
300
+ doc.off("update", onDocUpdate);
301
+ listeners.clear();
302
+ commandListeners.clear();
303
+ importFileListeners.clear();
304
+ pendingImportFiles.length = 0;
305
+ channel?.close();
306
+ },
307
+ call,
308
+ };
309
+ }
310
+
311
+ export { defineEditorManifest, type EditorManifestInput, type EditorTemplate } from "./commands/manifest/editor-document";
312
+ export { createEditorChrome } from "./view/editor-chrome";
313
+ export { mountEditorView } from "./view/mount-editor-view";
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.