@ecsplico/editor 2.0.0 → 2.1.1
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/CHANGELOG.md +37 -0
- package/README.md +4 -3
- package/dist/adapters/anchors.d.ts +80 -0
- package/dist/adapters/anchors.js +270 -0
- package/dist/adapters/anchors.js.map +1 -0
- package/dist/adapters/comments/client.d.ts +64 -0
- package/dist/adapters/comments/client.js +536 -0
- package/dist/adapters/comments/client.js.map +1 -0
- package/dist/adapters/comments/drizzle.d.ts +430 -0
- package/dist/adapters/comments/drizzle.js +236 -0
- package/dist/adapters/comments/drizzle.js.map +1 -0
- package/dist/adapters/comments/errors.d.ts +9 -0
- package/dist/adapters/comments/errors.js +19 -0
- package/dist/adapters/comments/errors.js.map +1 -0
- package/dist/adapters/comments/json.d.ts +19 -0
- package/dist/adapters/comments/json.js +94 -0
- package/dist/adapters/comments/json.js.map +1 -0
- package/dist/adapters/comments/record.js +78 -0
- package/dist/adapters/comments/record.js.map +1 -0
- package/dist/adapters/comments/server.d.ts +23 -0
- package/dist/adapters/comments/server.js +102 -0
- package/dist/adapters/comments/server.js.map +1 -0
- package/dist/adapters/comments/types.d.ts +109 -0
- package/dist/adapters/documents.js +108 -0
- package/dist/adapters/documents.js.map +1 -0
- package/dist/adapters/drizzle.d.ts +293 -0
- package/dist/adapters/drizzle.js +157 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/files.js +30 -0
- package/dist/adapters/files.js.map +1 -0
- package/dist/adapters/forge.d.ts +58 -0
- package/dist/adapters/forge.js +91 -0
- package/dist/adapters/forge.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +0 -0
- package/dist/adapters/json.d.ts +24 -0
- package/dist/adapters/json.js +128 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/nitro.d.ts +61 -0
- package/dist/adapters/nitro.js +212 -0
- package/dist/adapters/nitro.js.map +1 -0
- package/dist/adapters/routes.js +16 -0
- package/dist/adapters/routes.js.map +1 -0
- package/dist/adapters/shared.d.ts +37 -0
- package/dist/adapters/shared.js +32 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/vue.d.ts +67 -0
- package/dist/adapters/vue.js +187 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/doc-anchors.d.ts +5 -0
- package/dist/core/doc-anchors.js +4 -3
- package/dist/core/doc-anchors.js.map +1 -1
- package/dist/core/editor-ui.js +57 -1
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/surface-session.js +139 -0
- package/dist/core/surface-session.js.map +1 -0
- package/dist/document/DocumentSurface.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-op-review.d.ts +2 -2
- package/dist/features/ai/agent-op-review.js.map +1 -1
- package/dist/features/ai/agent-ops.d.ts +1 -1
- package/dist/features/ai/agent-ops.js +9 -6
- package/dist/features/ai/agent-ops.js.map +1 -1
- package/dist/features/ai/ai-apply.d.ts +1 -1
- package/dist/features/ai/ai-review.js +10 -1
- package/dist/features/ai/ai-review.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +78 -252
- package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
- package/dist/features/ai/components/AiSidebar.js +2 -2
- package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/components/DocumentChatOps.js +1 -0
- package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/components/SettingsAiTab.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/document-chat.d.ts +3 -2
- package/dist/features/ai/document-chat.js +16 -19
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/index.d.ts +5 -3
- package/dist/features/ai/index.js +4 -2
- package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
- package/dist/features/ai/protocol/agent-ops.js +65 -39
- package/dist/features/ai/protocol/agent-ops.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +89 -0
- package/dist/features/ai/protocol/block-lines.js +177 -0
- package/dist/features/ai/protocol/block-lines.js.map +1 -0
- package/dist/features/ai/protocol/chat.js +10 -10
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -2
- package/dist/features/ai/protocol/index.js +3 -2
- package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
- package/dist/features/ai/react/AiSelectionMenu.js +64 -255
- package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/react/AiSidebar.js +2 -2
- package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/react/DocumentChatOps.js +1 -0
- package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/react/SettingsAiTab.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
- package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
- package/dist/features/ai/selection-flow.d.ts +133 -0
- package/dist/features/ai/selection-flow.js +283 -0
- package/dist/features/ai/selection-flow.js.map +1 -0
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-document-chat.d.ts +1 -1
- package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
- package/dist/features/authors/feature.js +1 -1
- package/dist/features/authors/index.js +1 -1
- package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
- package/dist/features/comments/components/CommentBubble.js +1 -1
- package/dist/features/comments/components/CommentComposer.js +1 -1
- package/dist/features/comments/components/CommentThreadView.js +5 -2
- package/dist/features/comments/components/CommentThreadView.js.map +1 -1
- package/dist/features/comments/components/CommentsMargin.js +108 -230
- package/dist/features/comments/components/CommentsMargin.js.map +1 -1
- package/dist/features/comments/components/CommentsOverlay.js +1 -1
- package/dist/features/comments/feature.js +1 -1
- package/dist/features/comments/index.d.ts +4 -2
- package/dist/features/comments/index.js +4 -2
- package/dist/features/comments/margin-controller.d.ts +44 -0
- package/dist/features/comments/margin-controller.js +132 -0
- package/dist/features/comments/margin-controller.js.map +1 -0
- package/dist/features/comments/margin-layout.d.ts +55 -0
- package/dist/features/comments/margin-layout.js +79 -0
- package/dist/features/comments/margin-layout.js.map +1 -0
- package/dist/features/comments/model.d.ts +8 -0
- package/dist/features/comments/model.js +60 -34
- package/dist/features/comments/model.js.map +1 -1
- package/dist/features/comments/react/CommentBubble.js +1 -1
- package/dist/features/comments/react/CommentComposer.js +1 -1
- package/dist/features/comments/react/CommentThreadView.js +2 -2
- package/dist/features/comments/react/CommentThreadView.js.map +1 -1
- package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
- package/dist/features/comments/react/CommentsMargin.js +85 -251
- package/dist/features/comments/react/CommentsMargin.js.map +1 -1
- package/dist/features/comments/react/CommentsOverlay.js +1 -1
- package/dist/features/comments/react/use-comments.js +1 -1
- package/dist/features/comments/react/use-margin-layout.js +45 -0
- package/dist/features/comments/react/use-margin-layout.js.map +1 -0
- package/dist/features/comments/react.d.ts +1 -1
- package/dist/features/comments/schema.d.ts +23 -0
- package/dist/features/comments/schema.js.map +1 -1
- package/dist/features/comments/snapshot-equality.js +40 -0
- package/dist/features/comments/snapshot-equality.js.map +1 -0
- package/dist/features/comments/thread-anchor.js +33 -0
- package/dist/features/comments/thread-anchor.js.map +1 -0
- package/dist/features/comments/use-comments.d.ts +3 -1
- package/dist/features/comments/use-comments.js +1 -0
- package/dist/features/comments/use-comments.js.map +1 -1
- package/dist/features/comments/use-margin-layout.js +29 -0
- package/dist/features/comments/use-margin-layout.js.map +1 -0
- package/dist/features/comments/vue.d.ts +1 -1
- package/dist/features/composites/accepted-children.d.ts +21 -0
- package/dist/features/composites/accepted-children.js +126 -0
- package/dist/features/composites/accepted-children.js.map +1 -0
- package/dist/features/composites/components/CompositeDetails.js +1 -1
- package/dist/features/composites/components/CompositeInspector.js +38 -164
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/feature.d.ts +2 -1
- package/dist/features/composites/feature.js +8 -2
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/index.d.ts +5 -3
- package/dist/features/composites/index.js +8 -6
- package/dist/features/composites/inspector-model.d.ts +97 -0
- package/dist/features/composites/inspector-model.js +255 -0
- package/dist/features/composites/inspector-model.js.map +1 -0
- package/dist/features/composites/kinds.d.ts +11 -0
- package/dist/features/composites/kinds.js +17 -2
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/outline.d.ts +18 -5
- package/dist/features/composites/outline.js +44 -12
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.js +1 -1
- package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
- package/dist/features/composites/react/CompositeInspector.js +21 -180
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
- package/dist/features/composites/react/CompositeView.js +1 -1
- package/dist/features/composites/react/use-composite-inspector.js +29 -0
- package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/schema.d.ts +2 -2
- package/dist/features/composites/schema.js +2 -2
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/use-composite-anchors.d.ts +1 -1
- package/dist/features/composites/use-composite-inspector.js +41 -0
- package/dist/features/composites/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/use-composite-rows.d.ts +1 -1
- package/dist/features/media/index.d.ts +1 -1
- package/dist/features/media/vue.d.ts +1 -1
- package/dist/features/mentions/index.d.ts +3 -3
- package/dist/features/mentions/index.js +3 -3
- package/dist/features/mentions/schema.d.ts +1 -1
- package/dist/features/mentions/schema.js +1 -1
- package/dist/features/themes/index.d.ts +1 -1
- package/dist/features/themes/index.js +1 -1
- package/dist/features/transcript/feature.js +1 -1
- package/dist/features/transcript/index.js +2 -2
- package/dist/i18n/de/ai.js +2 -0
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/composites.js +8 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/tree.js +14 -1
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +22 -0
- package/dist/i18n/en/ai.js +2 -0
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/composites.js +8 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/tree.js +14 -1
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.js +2 -2
- package/dist/integrations/composite-ai/composite-summary.js +2 -2
- package/dist/integrations/composite-ai/index.js +2 -2
- package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
- package/dist/markdown/MarkdownSurface.d.ts +1 -1
- package/dist/markdown/MarkdownSurface.js +2 -2
- package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
- package/dist/markdown/index.d.ts +2 -2
- package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
- package/dist/react/EditorSurface.d.ts +1 -1
- package/dist/react/EditorSurface.js +43 -149
- package/dist/react/EditorSurface.js.map +1 -1
- package/dist/react/EditorToolbar.js +3 -40
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/schema/advanced.js +2 -2
- package/dist/server/adapter.d.ts +2 -2
- package/dist/server/collab.js +1 -1
- package/dist/server/handlers/ai.d.ts +4 -2
- package/dist/server/handlers/ai.js +11 -9
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +1 -1
- package/dist/server/handlers/editor.d.ts +1 -1
- package/dist/server/handlers/editor.js +2 -2
- package/dist/server/host.d.ts +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/settings/index.d.ts +1 -1
- package/dist/styles/tokens.css +1 -1
- package/dist/tree/DocumentTree.js +159 -387
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +37 -0
- package/dist/tree/drop-rules.js.map +1 -0
- package/dist/tree/react/DocumentTree.d.ts +2 -2
- package/dist/tree/react/DocumentTree.js +135 -397
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/react/use-tree-model.js +129 -0
- package/dist/tree/react/use-tree-model.js.map +1 -0
- package/dist/tree/react.js +1 -1
- package/dist/tree/tree-host.js +100 -0
- package/dist/tree/tree-host.js.map +1 -0
- package/dist/tree/tree-items.js +22 -43
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +580 -0
- package/dist/tree/tree-model.js.map +1 -0
- package/dist/tree/tree-rows.js +77 -0
- package/dist/tree/tree-rows.js.map +1 -0
- package/dist/tree/use-tree-model.js +94 -0
- package/dist/tree/use-tree-model.js.map +1 -0
- package/dist/tree/vue.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
- package/dist/vue/SimpleEditor.vue.d.ts +2 -2
- package/dist/vue/components/EditorSurface.js +27 -87
- package/dist/vue/components/EditorSurface.js.map +1 -1
- package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
- package/dist/vue/components/EditorToolbar.js +7 -44
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/context.d.ts +1 -1
- package/dist/vue/context.js +1 -1
- package/dist/vue/context.js.map +1 -1
- package/dist/vue/use-editor-ui.d.ts +1 -1
- package/dist/vue/use-notifier.d.ts +1 -1
- package/docs/integration.md +97 -4
- package/docs/llms.md +9 -1
- package/docs/usage.md +10 -5
- package/package.json +67 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EditorAdapter } from "../server/adapter.js";
|
|
2
|
+
import "../server/index.js";
|
|
3
|
+
import { AdapterHooks } from "./shared.js";
|
|
4
|
+
import { MarkdownDiagnostic, MarkdownPolicy } from "../markdown/policy.js";
|
|
5
|
+
import "../markdown/index.js";
|
|
6
|
+
import { AnyExtension } from "@tiptap/core";
|
|
7
|
+
//#region src/adapters/forge.d.ts
|
|
8
|
+
/** A Markdown file of the host, as the host stores it: the whole text with its frontmatter, and an opaque version of that file. */
|
|
9
|
+
export interface ForgeFile {
|
|
10
|
+
markdown: string;
|
|
11
|
+
/** Changes whenever the file's content changes (mtime and size, a hash, a revision); no NUL character. Becomes `contentVersion`. */
|
|
12
|
+
version: string;
|
|
13
|
+
}
|
|
14
|
+
/** Where the host keeps its Markdown documents. The adapter never touches a file system itself. */
|
|
15
|
+
export interface ForgeContent {
|
|
16
|
+
/** The file behind a document id, `null` = it does not exist. */
|
|
17
|
+
read(id: string): Promise<ForgeFile | null>;
|
|
18
|
+
/** Writes the file (frontmatter included). Only called for documents that exist. */
|
|
19
|
+
write(id: string, markdown: string): Promise<void>;
|
|
20
|
+
/** Called after every `write` the adapter made (change notifications, indexes). */
|
|
21
|
+
onWritten?(id: string): void | Promise<void>;
|
|
22
|
+
/** Ids of all documents, for `listContents` (the tag index). Omitted: `listContents` is empty. */
|
|
23
|
+
list?(): Promise<string[]>;
|
|
24
|
+
}
|
|
25
|
+
/** What the adapter delegates to a storage adapter that has no use for Markdown: Yjs state, assets, settings, themes (`createJsonAdapter` or `createDrizzleAdapter` fit). */
|
|
26
|
+
export type ForgeState = Pick<EditorAdapter, 'loadState' | 'storeState' | 'removeState' | 'storeAsset' | 'loadAsset'> & Partial<Pick<EditorAdapter, 'loadSettings' | 'storeSettings' | 'listThemes' | 'loadTheme' | 'storeTheme' | 'removeTheme'>>;
|
|
27
|
+
export interface ForgeAdapterOptions extends Omit<AdapterHooks, 'authenticate'> {
|
|
28
|
+
content: ForgeContent;
|
|
29
|
+
state: ForgeState;
|
|
30
|
+
/**
|
|
31
|
+
* The complete TipTap extension list of the Markdown conversion, e.g. `[...advancedSchemaExtensions(), MermaidBlock]`:
|
|
32
|
+
* nodes and marks with their Markdown handlers. Host nodes also go to the collaboration schema through `schemaExtensions`
|
|
33
|
+
* (which lists only the additions to `advancedSchemaExtensions()`).
|
|
34
|
+
*/
|
|
35
|
+
schema: AnyExtension[];
|
|
36
|
+
/** The host's session check; there is no default for a host that owns its Markdown files. */
|
|
37
|
+
authenticate: EditorAdapter['authenticate'];
|
|
38
|
+
/** Markdown policy of the conversion (raw HTML, data URLs); the default drops both. */
|
|
39
|
+
policy?: MarkdownPolicy;
|
|
40
|
+
/** Called with the diagnostics of every load that produced some (dropped HTML or URLs, unknown fences kept verbatim). */
|
|
41
|
+
onDiagnostics?(id: string, diagnostics: MarkdownDiagnostic[]): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The editor's adapter over the host's Markdown files (concept documents, vault pages). The host keeps owning the
|
|
45
|
+
* files, their locations, change notifications and access rules and passes them in as functions; this adapter does the
|
|
46
|
+
* Markdown ↔ TipTap JSON conversion and the contract:
|
|
47
|
+
*
|
|
48
|
+
* - `loadContent` splits off the frontmatter and parses the body (`markdownToContent`); a body the conversion could not
|
|
49
|
+
* keep intact (unclosed fence, malformed fence metadata) is refused with an error instead of loaded lossy.
|
|
50
|
+
* - `storeContent` re-reads the file, keeps its current frontmatter verbatim, writes the serialized body and calls
|
|
51
|
+
* `onWritten`. A file that is gone is not recreated, and an unchanged result is not rewritten.
|
|
52
|
+
* - `contentVersion` is the file's own version, read after the write so it reflects what the editor stored.
|
|
53
|
+
*
|
|
54
|
+
* Yjs state, assets and settings are `state`'s. The converter is one headless editor per adapter, built on first use.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createForgeAdapter(options: ForgeAdapterOptions): EditorAdapter;
|
|
57
|
+
//#endregion
|
|
58
|
+
//# sourceMappingURL=forge.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DocumentMarkdown } from "../core/document-markdown.js";
|
|
2
|
+
import { hasFatalDiagnostics } from "../markdown/policy.js";
|
|
3
|
+
import { contentToMarkdown, markdownToContent } from "../markdown/document.js";
|
|
4
|
+
import { joinFrontmatter, splitFrontmatter } from "../markdown/frontmatter.js";
|
|
5
|
+
import { Editor } from "@tiptap/core";
|
|
6
|
+
//#region src/adapters/forge.ts
|
|
7
|
+
const OPTIONAL_STATE = [
|
|
8
|
+
"loadSettings",
|
|
9
|
+
"storeSettings",
|
|
10
|
+
"listThemes",
|
|
11
|
+
"loadTheme",
|
|
12
|
+
"storeTheme",
|
|
13
|
+
"removeTheme"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* The editor's adapter over the host's Markdown files (concept documents, vault pages). The host keeps owning the
|
|
17
|
+
* files, their locations, change notifications and access rules and passes them in as functions; this adapter does the
|
|
18
|
+
* Markdown ↔ TipTap JSON conversion and the contract:
|
|
19
|
+
*
|
|
20
|
+
* - `loadContent` splits off the frontmatter and parses the body (`markdownToContent`); a body the conversion could not
|
|
21
|
+
* keep intact (unclosed fence, malformed fence metadata) is refused with an error instead of loaded lossy.
|
|
22
|
+
* - `storeContent` re-reads the file, keeps its current frontmatter verbatim, writes the serialized body and calls
|
|
23
|
+
* `onWritten`. A file that is gone is not recreated, and an unchanged result is not rewritten.
|
|
24
|
+
* - `contentVersion` is the file's own version, read after the write so it reflects what the editor stored.
|
|
25
|
+
*
|
|
26
|
+
* Yjs state, assets and settings are `state`'s. The converter is one headless editor per adapter, built on first use.
|
|
27
|
+
*/
|
|
28
|
+
function createForgeAdapter(options) {
|
|
29
|
+
const { content, state, schema, policy, onDiagnostics, authenticate, authorize, schemaExtensions, users } = options;
|
|
30
|
+
let converter;
|
|
31
|
+
const editor = () => converter ??= new Editor({
|
|
32
|
+
content: {
|
|
33
|
+
type: "doc",
|
|
34
|
+
content: [{ type: "paragraph" }]
|
|
35
|
+
},
|
|
36
|
+
extensions: [DocumentMarkdown, ...schema]
|
|
37
|
+
});
|
|
38
|
+
const optionalState = Object.fromEntries(OPTIONAL_STATE.filter((method) => state[method]).map((method) => [method, (...args) => state[method].apply(state, args)]));
|
|
39
|
+
function parse(id, file) {
|
|
40
|
+
const { content: json, diagnostics } = markdownToContent(editor(), splitFrontmatter(file.markdown).body, policy ? { policy } : {});
|
|
41
|
+
if (diagnostics.length) onDiagnostics?.(id, diagnostics);
|
|
42
|
+
if (hasFatalDiagnostics(diagnostics)) {
|
|
43
|
+
const problems = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
|
|
44
|
+
throw new Error(`@ecsplico/editor: document "${id}" cannot be converted without losing content: ${problems.join(" ")}`);
|
|
45
|
+
}
|
|
46
|
+
return json;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
authenticate: (request) => authenticate(request),
|
|
50
|
+
listUsers: async () => await users?.() ?? [],
|
|
51
|
+
...authorize && { authorize: (user, id) => authorize(user, id) },
|
|
52
|
+
...schemaExtensions && { schemaExtensions: () => schemaExtensions() },
|
|
53
|
+
async loadContent(id) {
|
|
54
|
+
const file = await content.read(id);
|
|
55
|
+
return file && parse(id, file);
|
|
56
|
+
},
|
|
57
|
+
async storeContent(id, json) {
|
|
58
|
+
const current = await content.read(id);
|
|
59
|
+
if (!current) return;
|
|
60
|
+
const instance = editor();
|
|
61
|
+
instance.commands.setContent(json, { emitUpdate: false });
|
|
62
|
+
const body = contentToMarkdown(instance);
|
|
63
|
+
const markdown = joinFrontmatter(splitFrontmatter(current.markdown).frontmatter, body && `${body}\n`);
|
|
64
|
+
if (markdown === current.markdown) return;
|
|
65
|
+
await content.write(id, markdown);
|
|
66
|
+
await content.onWritten?.(id);
|
|
67
|
+
},
|
|
68
|
+
async contentVersion(id) {
|
|
69
|
+
return (await content.read(id))?.version ?? null;
|
|
70
|
+
},
|
|
71
|
+
async listContents() {
|
|
72
|
+
return (await Promise.all((await content.list?.() ?? []).map(async (id) => {
|
|
73
|
+
const file = await content.read(id);
|
|
74
|
+
return file && {
|
|
75
|
+
id,
|
|
76
|
+
content: parse(id, file)
|
|
77
|
+
};
|
|
78
|
+
}))).filter((entry) => entry !== null);
|
|
79
|
+
},
|
|
80
|
+
loadState: (id) => state.loadState(id),
|
|
81
|
+
storeState: (id, bytes) => state.storeState(id, bytes),
|
|
82
|
+
removeState: (id) => state.removeState(id),
|
|
83
|
+
storeAsset: (id, asset) => state.storeAsset(id, asset),
|
|
84
|
+
loadAsset: (id, name) => state.loadAsset(id, name),
|
|
85
|
+
...optionalState
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { createForgeAdapter };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=forge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forge.js","names":[],"sources":["../../src/adapters/forge.ts"],"sourcesContent":["import { type AnyExtension, Editor } from '@tiptap/core'\nimport { DocumentMarkdown } from '../core'\nimport { contentToMarkdown, hasFatalDiagnostics, joinFrontmatter, type MarkdownDiagnostic, type MarkdownPolicy, markdownToContent, splitFrontmatter } from '../markdown'\nimport type { EditorAdapter } from '../server'\nimport type { AdapterHooks } from './shared'\n\n/** A Markdown file of the host, as the host stores it: the whole text with its frontmatter, and an opaque version of that file. */\nexport interface ForgeFile {\n markdown: string\n /** Changes whenever the file's content changes (mtime and size, a hash, a revision); no NUL character. Becomes `contentVersion`. */\n version: string\n}\n\n/** Where the host keeps its Markdown documents. The adapter never touches a file system itself. */\nexport interface ForgeContent {\n /** The file behind a document id, `null` = it does not exist. */\n read(id: string): Promise<ForgeFile | null>\n /** Writes the file (frontmatter included). Only called for documents that exist. */\n write(id: string, markdown: string): Promise<void>\n /** Called after every `write` the adapter made (change notifications, indexes). */\n onWritten?(id: string): void | Promise<void>\n /** Ids of all documents, for `listContents` (the tag index). Omitted: `listContents` is empty. */\n list?(): Promise<string[]>\n}\n\n/** What the adapter delegates to a storage adapter that has no use for Markdown: Yjs state, assets, settings, themes (`createJsonAdapter` or `createDrizzleAdapter` fit). */\nexport type ForgeState = Pick<EditorAdapter, 'loadState' | 'storeState' | 'removeState' | 'storeAsset' | 'loadAsset'>\n & Partial<Pick<EditorAdapter, 'loadSettings' | 'storeSettings' | 'listThemes' | 'loadTheme' | 'storeTheme' | 'removeTheme'>>\n\nexport interface ForgeAdapterOptions extends Omit<AdapterHooks, 'authenticate'> {\n content: ForgeContent\n state: ForgeState\n /**\n * The complete TipTap extension list of the Markdown conversion, e.g. `[...advancedSchemaExtensions(), MermaidBlock]`:\n * nodes and marks with their Markdown handlers. Host nodes also go to the collaboration schema through `schemaExtensions`\n * (which lists only the additions to `advancedSchemaExtensions()`).\n */\n schema: AnyExtension[]\n /** The host's session check; there is no default for a host that owns its Markdown files. */\n authenticate: EditorAdapter['authenticate']\n /** Markdown policy of the conversion (raw HTML, data URLs); the default drops both. */\n policy?: MarkdownPolicy\n /** Called with the diagnostics of every load that produced some (dropped HTML or URLs, unknown fences kept verbatim). */\n onDiagnostics?(id: string, diagnostics: MarkdownDiagnostic[]): void\n}\n\nconst OPTIONAL_STATE = ['loadSettings', 'storeSettings', 'listThemes', 'loadTheme', 'storeTheme', 'removeTheme'] as const\n\n/**\n * The editor's adapter over the host's Markdown files (concept documents, vault pages). The host keeps owning the\n * files, their locations, change notifications and access rules and passes them in as functions; this adapter does the\n * Markdown ↔ TipTap JSON conversion and the contract:\n *\n * - `loadContent` splits off the frontmatter and parses the body (`markdownToContent`); a body the conversion could not\n * keep intact (unclosed fence, malformed fence metadata) is refused with an error instead of loaded lossy.\n * - `storeContent` re-reads the file, keeps its current frontmatter verbatim, writes the serialized body and calls\n * `onWritten`. A file that is gone is not recreated, and an unchanged result is not rewritten.\n * - `contentVersion` is the file's own version, read after the write so it reflects what the editor stored.\n *\n * Yjs state, assets and settings are `state`'s. The converter is one headless editor per adapter, built on first use.\n */\nexport function createForgeAdapter(options: ForgeAdapterOptions): EditorAdapter {\n const { content, state, schema, policy, onDiagnostics, authenticate, authorize, schemaExtensions, users } = options\n\n let converter: Editor | undefined\n // No `window` is needed as long as the editor starts from JSON content, not an HTML string.\n const editor = () => converter ??= new Editor({ content: { type: 'doc', content: [{ type: 'paragraph' }] }, extensions: [DocumentMarkdown, ...schema] })\n\n // Present only when `state` has them: the routes and the settings client test for the methods.\n const optionalState = Object.fromEntries(OPTIONAL_STATE\n .filter(method => state[method])\n .map(method => [method, (...args: unknown[]) => (state[method] as (...args: unknown[]) => unknown).apply(state, args)])) as Partial<EditorAdapter>\n\n function parse(id: string, file: ForgeFile) {\n const { content: json, diagnostics } = markdownToContent(editor(), splitFrontmatter(file.markdown).body, policy ? { policy } : {})\n if (diagnostics.length) onDiagnostics?.(id, diagnostics)\n if (hasFatalDiagnostics(diagnostics)) {\n const problems = diagnostics.filter(diagnostic => diagnostic.severity === 'error').map(diagnostic => diagnostic.message)\n throw new Error(`@ecsplico/editor: document \"${id}\" cannot be converted without losing content: ${problems.join(' ')}`)\n }\n return json\n }\n\n return {\n authenticate: request => authenticate(request),\n listUsers: async () => (await users?.()) ?? [],\n ...(authorize && { authorize: (user: object, id: string) => authorize(user, id) }),\n ...(schemaExtensions && { schemaExtensions: () => schemaExtensions() }),\n\n async loadContent(id) {\n const file = await content.read(id)\n return file && parse(id, file)\n },\n async storeContent(id, json) {\n const current = await content.read(id)\n if (!current) return // deleted while a store was pending\n const instance = editor()\n instance.commands.setContent(json, { emitUpdate: false })\n const body = contentToMarkdown(instance)\n const markdown = joinFrontmatter(splitFrontmatter(current.markdown).frontmatter, body && `${body}\\n`)\n if (markdown === current.markdown) return\n await content.write(id, markdown)\n await content.onWritten?.(id)\n },\n async contentVersion(id) {\n return (await content.read(id))?.version ?? null\n },\n async listContents() {\n const entries = await Promise.all((await content.list?.() ?? []).map(async (id) => {\n const file = await content.read(id)\n return file && { id, content: parse(id, file) }\n }))\n return entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null)\n },\n\n loadState: id => state.loadState(id),\n storeState: (id, bytes) => state.storeState(id, bytes),\n removeState: id => state.removeState(id),\n storeAsset: (id, asset) => state.storeAsset(id, asset),\n loadAsset: (id, name) => state.loadAsset(id, name),\n ...optionalState,\n }\n}\n"],"mappings":";;;;;;AA8CA,MAAM,iBAAiB;CAAC;CAAgB;CAAiB;CAAc;CAAa;CAAc;AAAa;;;;;;;;;;;;;;AAe/G,SAAgB,mBAAmB,SAA6C;CAC9E,MAAM,EAAE,SAAS,OAAO,QAAQ,QAAQ,eAAe,cAAc,WAAW,kBAAkB,UAAU;CAE5G,IAAI;CAEJ,MAAM,eAAe,cAAc,IAAI,OAAO;EAAE,SAAS;GAAE,MAAM;GAAO,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;EAAE;EAAG,YAAY,CAAC,kBAAkB,GAAG,MAAM;CAAE,CAAC;CAGvJ,MAAM,gBAAgB,OAAO,YAAY,eACtC,QAAO,WAAU,MAAM,OAAO,CAAC,CAC/B,KAAI,WAAU,CAAC,SAAS,GAAG,SAAqB,MAAM,OAAO,CAAqC,MAAM,OAAO,IAAI,CAAC,CAAC,CAAC;CAEzH,SAAS,MAAM,IAAY,MAAiB;EAC1C,MAAM,EAAE,SAAS,MAAM,gBAAgB,kBAAkB,OAAO,GAAG,iBAAiB,KAAK,QAAQ,CAAC,CAAC,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC,CAAC;EACjI,IAAI,YAAY,QAAQ,gBAAgB,IAAI,WAAW;EACvD,IAAI,oBAAoB,WAAW,GAAG;GACpC,MAAM,WAAW,YAAY,QAAO,eAAc,WAAW,aAAa,OAAO,CAAC,CAAC,KAAI,eAAc,WAAW,OAAO;GACvH,MAAM,IAAI,MAAM,+BAA+B,GAAG,gDAAgD,SAAS,KAAK,GAAG,GAAG;EACxH;EACA,OAAO;CACT;CAEA,OAAO;EACL,eAAc,YAAW,aAAa,OAAO;EAC7C,WAAW,YAAa,MAAM,QAAQ,KAAM,CAAC;EAC7C,GAAI,aAAa,EAAE,YAAY,MAAc,OAAe,UAAU,MAAM,EAAE,EAAE;EAChF,GAAI,oBAAoB,EAAE,wBAAwB,iBAAiB,EAAE;EAErE,MAAM,YAAY,IAAI;GACpB,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;GAClC,OAAO,QAAQ,MAAM,IAAI,IAAI;EAC/B;EACA,MAAM,aAAa,IAAI,MAAM;GAC3B,MAAM,UAAU,MAAM,QAAQ,KAAK,EAAE;GACrC,IAAI,CAAC,SAAS;GACd,MAAM,WAAW,OAAO;GACxB,SAAS,SAAS,WAAW,MAAM,EAAE,YAAY,MAAM,CAAC;GACxD,MAAM,OAAO,kBAAkB,QAAQ;GACvC,MAAM,WAAW,gBAAgB,iBAAiB,QAAQ,QAAQ,CAAC,CAAC,aAAa,QAAQ,GAAG,KAAK,GAAG;GACpG,IAAI,aAAa,QAAQ,UAAU;GACnC,MAAM,QAAQ,MAAM,IAAI,QAAQ;GAChC,MAAM,QAAQ,YAAY,EAAE;EAC9B;EACA,MAAM,eAAe,IAAI;GACvB,QAAQ,MAAM,QAAQ,KAAK,EAAE,EAAA,EAAI,WAAW;EAC9C;EACA,MAAM,eAAe;GAKnB,QAAO,MAJe,QAAQ,KAAK,MAAM,QAAQ,OAAO,KAAK,CAAC,EAAA,CAAG,IAAI,OAAO,OAAO;IACjF,MAAM,OAAO,MAAM,QAAQ,KAAK,EAAE;IAClC,OAAO,QAAQ;KAAE;KAAI,SAAS,MAAM,IAAI,IAAI;IAAE;GAChD,CAAC,CAAC,EAAA,CACa,QAAQ,UAA8C,UAAU,IAAI;EACrF;EAEA,YAAW,OAAM,MAAM,UAAU,EAAE;EACnC,aAAa,IAAI,UAAU,MAAM,WAAW,IAAI,KAAK;EACrD,cAAa,OAAM,MAAM,YAAY,EAAE;EACvC,aAAa,IAAI,UAAU,MAAM,WAAW,IAAI,KAAK;EACrD,YAAY,IAAI,SAAS,MAAM,UAAU,IAAI,IAAI;EACjD,GAAG;CACL;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AnchorSource, AnchorTransaction, DocRange, OrphanedAnchor, PointAnchor, PortableAnchor, RangeAnchor } from "./anchors.js";
|
|
2
|
+
import { CommentsFetch, CommentsFetchResponse, RestCommentsModel, RestCommentsModelOptions } from "./comments/client.js";
|
|
3
|
+
import { AuthenticateRequest, EditorAdapter } from "../server/adapter.js";
|
|
4
|
+
import "../server/index.js";
|
|
5
|
+
import { AdapterHooks, DocumentAdmin, StoredEditorAdapter } from "./shared.js";
|
|
6
|
+
import { DrizzleAdapterOptions, EditorDb, EditorTables } from "./drizzle.js";
|
|
7
|
+
import { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from "./comments/types.js";
|
|
8
|
+
import { CommentTables, DrizzleCommentStoreOptions } from "./comments/drizzle.js";
|
|
9
|
+
import { JsonCommentStoreOptions } from "./comments/json.js";
|
|
10
|
+
import { CommentsRequest, CommentsResponse } from "./comments/server.js";
|
|
11
|
+
import { ForgeAdapterOptions, ForgeContent, ForgeFile, ForgeState } from "./forge.js";
|
|
12
|
+
import { JsonAdapterOptions } from "./json.js";
|
|
13
|
+
export type { AdapterHooks, AnchorSource, AnchorTransaction, Annotation, AuthenticateRequest, Comment, CommentAction, CommentStore, CommentTables, CommentThread, CommentsFetch, CommentsFetchResponse, CommentsRequest, CommentsResponse, CreateThreadInput, Discussion, DocRange, DocumentAdmin, DrizzleAdapterOptions, DrizzleCommentStoreOptions, EditorAdapter, EditorDb, EditorTables, ForgeAdapterOptions, ForgeContent, ForgeFile, ForgeState, JsonAdapterOptions, JsonCommentStoreOptions, NewThread, OrphanedAnchor, PointAnchor, PortableAnchor, RangeAnchor, RestCommentsModel, RestCommentsModelOptions, StoredEditorAdapter, ThreadStatus };
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AdapterHooks, StoredEditorAdapter } from "./shared.js";
|
|
2
|
+
//#region src/adapters/json.d.ts
|
|
3
|
+
export interface JsonAdapterOptions extends AdapterHooks {
|
|
4
|
+
/** Directory the adapter owns; created on demand. Single node only: the files are not coordinated across processes. */
|
|
5
|
+
dir: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The editor's adapter on plain files under `dir`:
|
|
9
|
+
*
|
|
10
|
+
* <dir>/<id>.json TipTap JSON of a document
|
|
11
|
+
* <dir>/<id>.ystate the collab server's state bytes
|
|
12
|
+
* <dir>/settings/<user>.json per-user editor settings
|
|
13
|
+
* <dir>/assets/<id>/<name> an uploaded file
|
|
14
|
+
* <dir>/assets/<id>/.records/<name>.json its `AssetRecord`
|
|
15
|
+
*
|
|
16
|
+
* Ids, user keys and asset names are percent-encoded into file names (`a/b` → `a%2Fb.json`), so no input leaves `dir`.
|
|
17
|
+
* A missing file reads as `null`; every write goes through a temp file and a rename. `contentVersion` is the content
|
|
18
|
+
* file's modification time (nanoseconds) and size, read after every write the editor makes, so a document edited by
|
|
19
|
+
* hand or by another tool invalidates the stored Yjs state. `storeContent` only updates existing documents: create
|
|
20
|
+
* them with `putDocument`. The adapter has no theme library (the themes routes answer 501).
|
|
21
|
+
*/
|
|
22
|
+
export declare function createJsonAdapter(options: JsonAdapterOptions): StoredEditorAdapter;
|
|
23
|
+
//#endregion
|
|
24
|
+
//# sourceMappingURL=json.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { bytesOf, identityPart, settingsKey } from "./shared.js";
|
|
2
|
+
import { isMissing, readOrNull, writeAtomic } from "./files.js";
|
|
3
|
+
import { promises } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region src/adapters/json.ts
|
|
6
|
+
const CONTENT_EXT = ".json";
|
|
7
|
+
const STATE_EXT = ".ystate";
|
|
8
|
+
const SETTINGS_DIR = "settings";
|
|
9
|
+
const ASSETS_DIR = "assets";
|
|
10
|
+
const RECORDS_DIR = ".records";
|
|
11
|
+
/**
|
|
12
|
+
* A file name for `value`: percent-encoded (reversible, no separators, no `.`/`..`), and a leading dot escaped so no
|
|
13
|
+
* key ever collides with the dot-named temp files and the `.records` directory.
|
|
14
|
+
*/
|
|
15
|
+
function fileKey(value) {
|
|
16
|
+
if (value === "") throw new TypeError("@ecsplico/editor: empty id");
|
|
17
|
+
const encoded = encodeURIComponent(value);
|
|
18
|
+
return encoded.startsWith(".") ? `%2E${encoded.slice(1)}` : encoded;
|
|
19
|
+
}
|
|
20
|
+
async function readJson(file) {
|
|
21
|
+
const data = await readOrNull(file);
|
|
22
|
+
return data === null ? null : JSON.parse(data.toString("utf8"));
|
|
23
|
+
}
|
|
24
|
+
/** Runs the actions of one key one after another, so a delete cannot interleave with an update of the same document. */
|
|
25
|
+
function keyedLock() {
|
|
26
|
+
const tails = /* @__PURE__ */ new Map();
|
|
27
|
+
return (key, action) => {
|
|
28
|
+
const run = (tails.get(key) ?? Promise.resolve()).then(action, action);
|
|
29
|
+
const tail = run.catch(() => {});
|
|
30
|
+
tails.set(key, tail);
|
|
31
|
+
tail.then(() => {
|
|
32
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
33
|
+
});
|
|
34
|
+
return run;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The editor's adapter on plain files under `dir`:
|
|
39
|
+
*
|
|
40
|
+
* <dir>/<id>.json TipTap JSON of a document
|
|
41
|
+
* <dir>/<id>.ystate the collab server's state bytes
|
|
42
|
+
* <dir>/settings/<user>.json per-user editor settings
|
|
43
|
+
* <dir>/assets/<id>/<name> an uploaded file
|
|
44
|
+
* <dir>/assets/<id>/.records/<name>.json its `AssetRecord`
|
|
45
|
+
*
|
|
46
|
+
* Ids, user keys and asset names are percent-encoded into file names (`a/b` → `a%2Fb.json`), so no input leaves `dir`.
|
|
47
|
+
* A missing file reads as `null`; every write goes through a temp file and a rename. `contentVersion` is the content
|
|
48
|
+
* file's modification time (nanoseconds) and size, read after every write the editor makes, so a document edited by
|
|
49
|
+
* hand or by another tool invalidates the stored Yjs state. `storeContent` only updates existing documents: create
|
|
50
|
+
* them with `putDocument`. The adapter has no theme library (the themes routes answer 501).
|
|
51
|
+
*/
|
|
52
|
+
function createJsonAdapter(options) {
|
|
53
|
+
const root = path.resolve(options.dir);
|
|
54
|
+
const contentFile = (id) => path.join(root, `${fileKey(id)}${CONTENT_EXT}`);
|
|
55
|
+
const stateFile = (id) => path.join(root, `${fileKey(id)}${STATE_EXT}`);
|
|
56
|
+
const settingsFile = (user) => path.join(root, SETTINGS_DIR, `${fileKey(settingsKey(user))}${CONTENT_EXT}`);
|
|
57
|
+
const assetDir = (id) => path.join(root, ASSETS_DIR, fileKey(id));
|
|
58
|
+
const assetFile = (id, name) => path.join(assetDir(id), fileKey(name));
|
|
59
|
+
const recordFile = (id, name) => path.join(assetDir(id), RECORDS_DIR, `${fileKey(name)}${CONTENT_EXT}`);
|
|
60
|
+
const locked = keyedLock();
|
|
61
|
+
async function listDocumentIds() {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = await promises.readdir(root, { withFileTypes: true });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (isMissing(error)) return [];
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return entries.filter((entry) => entry.isFile() && !entry.name.startsWith(".") && entry.name.endsWith(CONTENT_EXT)).map((entry) => decodeURIComponent(entry.name.slice(0, -5))).sort();
|
|
70
|
+
}
|
|
71
|
+
const loadContent = async (id) => readJson(contentFile(id));
|
|
72
|
+
return {
|
|
73
|
+
...identityPart(options),
|
|
74
|
+
loadContent,
|
|
75
|
+
storeContent: (id, content) => locked(id, async () => {
|
|
76
|
+
if (!await promises.stat(contentFile(id)).then(() => true, () => false)) return;
|
|
77
|
+
await writeAtomic(contentFile(id), JSON.stringify(content));
|
|
78
|
+
}),
|
|
79
|
+
async contentVersion(id) {
|
|
80
|
+
try {
|
|
81
|
+
const { mtimeNs, size } = await promises.stat(contentFile(id), { bigint: true });
|
|
82
|
+
return `${mtimeNs}:${size}`;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (isMissing(error)) return null;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async listContents() {
|
|
89
|
+
return (await Promise.all((await listDocumentIds()).map(async (id) => ({
|
|
90
|
+
id,
|
|
91
|
+
content: await loadContent(id)
|
|
92
|
+
})))).filter((entry) => entry.content !== null);
|
|
93
|
+
},
|
|
94
|
+
async loadState(id) {
|
|
95
|
+
const data = await readOrNull(stateFile(id));
|
|
96
|
+
return data && bytesOf(data);
|
|
97
|
+
},
|
|
98
|
+
storeState: async (id, state) => writeAtomic(stateFile(id), state),
|
|
99
|
+
removeState: async (id) => promises.rm(stateFile(id), { force: true }),
|
|
100
|
+
async storeAsset(id, { data, ...record }) {
|
|
101
|
+
await writeAtomic(assetFile(id, record.name), data);
|
|
102
|
+
await writeAtomic(recordFile(id, record.name), JSON.stringify(record));
|
|
103
|
+
},
|
|
104
|
+
async loadAsset(id, name) {
|
|
105
|
+
const [record, data] = await Promise.all([readJson(recordFile(id, name)), readOrNull(assetFile(id, name))]);
|
|
106
|
+
return record && data ? {
|
|
107
|
+
record,
|
|
108
|
+
data
|
|
109
|
+
} : null;
|
|
110
|
+
},
|
|
111
|
+
loadSettings: async (user) => readJson(settingsFile(user)),
|
|
112
|
+
storeSettings: async (user, settings) => writeAtomic(settingsFile(user), JSON.stringify(settings)),
|
|
113
|
+
putDocument: (id, content) => locked(id, () => writeAtomic(contentFile(id), JSON.stringify(content))),
|
|
114
|
+
deleteDocument: (id) => locked(id, async () => {
|
|
115
|
+
await promises.rm(contentFile(id), { force: true });
|
|
116
|
+
await promises.rm(stateFile(id), { force: true });
|
|
117
|
+
await promises.rm(assetDir(id), {
|
|
118
|
+
recursive: true,
|
|
119
|
+
force: true
|
|
120
|
+
});
|
|
121
|
+
}),
|
|
122
|
+
listDocumentIds
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { createJsonAdapter };
|
|
127
|
+
|
|
128
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","names":["fs"],"sources":["../../src/adapters/json.ts"],"sourcesContent":["import { type Dirent, promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport type { JSONContent } from '@tiptap/core'\nimport type { AssetRecord } from '../features/media/schema'\nimport { isMissing, readOrNull, writeAtomic } from './files'\nimport { type AdapterHooks, bytesOf, identityPart, settingsKey, type StoredEditorAdapter } from './shared'\n\nexport interface JsonAdapterOptions extends AdapterHooks {\n /** Directory the adapter owns; created on demand. Single node only: the files are not coordinated across processes. */\n dir: string\n}\n\nconst CONTENT_EXT = '.json'\nconst STATE_EXT = '.ystate'\nconst SETTINGS_DIR = 'settings'\nconst ASSETS_DIR = 'assets'\nconst RECORDS_DIR = '.records'\n\n/**\n * A file name for `value`: percent-encoded (reversible, no separators, no `.`/`..`), and a leading dot escaped so no\n * key ever collides with the dot-named temp files and the `.records` directory.\n */\nfunction fileKey(value: string): string {\n if (value === '') throw new TypeError('@ecsplico/editor: empty id')\n const encoded = encodeURIComponent(value)\n return encoded.startsWith('.') ? `%2E${encoded.slice(1)}` : encoded\n}\n\nasync function readJson<T>(file: string): Promise<T | null> {\n const data = await readOrNull(file)\n return data === null ? null : JSON.parse(data.toString('utf8')) as T\n}\n\n/** Runs the actions of one key one after another, so a delete cannot interleave with an update of the same document. */\nfunction keyedLock() {\n const tails = new Map<string, Promise<unknown>>()\n return <T>(key: string, action: () => Promise<T>): Promise<T> => {\n const run = (tails.get(key) ?? Promise.resolve()).then(action, action)\n const tail = run.catch(() => {})\n tails.set(key, tail)\n void tail.then(() => {\n if (tails.get(key) === tail) tails.delete(key)\n })\n return run\n }\n}\n\n/**\n * The editor's adapter on plain files under `dir`:\n *\n * <dir>/<id>.json TipTap JSON of a document\n * <dir>/<id>.ystate the collab server's state bytes\n * <dir>/settings/<user>.json per-user editor settings\n * <dir>/assets/<id>/<name> an uploaded file\n * <dir>/assets/<id>/.records/<name>.json its `AssetRecord`\n *\n * Ids, user keys and asset names are percent-encoded into file names (`a/b` → `a%2Fb.json`), so no input leaves `dir`.\n * A missing file reads as `null`; every write goes through a temp file and a rename. `contentVersion` is the content\n * file's modification time (nanoseconds) and size, read after every write the editor makes, so a document edited by\n * hand or by another tool invalidates the stored Yjs state. `storeContent` only updates existing documents: create\n * them with `putDocument`. The adapter has no theme library (the themes routes answer 501).\n */\nexport function createJsonAdapter(options: JsonAdapterOptions): StoredEditorAdapter {\n const root = path.resolve(options.dir)\n const contentFile = (id: string) => path.join(root, `${fileKey(id)}${CONTENT_EXT}`)\n const stateFile = (id: string) => path.join(root, `${fileKey(id)}${STATE_EXT}`)\n const settingsFile = (user: object) => path.join(root, SETTINGS_DIR, `${fileKey(settingsKey(user))}${CONTENT_EXT}`)\n const assetDir = (id: string) => path.join(root, ASSETS_DIR, fileKey(id))\n const assetFile = (id: string, name: string) => path.join(assetDir(id), fileKey(name))\n const recordFile = (id: string, name: string) => path.join(assetDir(id), RECORDS_DIR, `${fileKey(name)}${CONTENT_EXT}`)\n const locked = keyedLock()\n\n async function listDocumentIds(): Promise<string[]> {\n let entries: Dirent[]\n try {\n entries = await fs.readdir(root, { withFileTypes: true })\n }\n catch (error) {\n if (isMissing(error)) return []\n throw error\n }\n return entries\n .filter(entry => entry.isFile() && !entry.name.startsWith('.') && entry.name.endsWith(CONTENT_EXT))\n .map(entry => decodeURIComponent(entry.name.slice(0, -CONTENT_EXT.length)))\n .sort()\n }\n\n const loadContent = async (id: string) => readJson<JSONContent>(contentFile(id))\n\n return {\n ...identityPart(options),\n\n loadContent,\n storeContent: (id, content) => locked(id, async () => {\n // Deleted while a store was pending: writing would resurrect the document.\n if (!await fs.stat(contentFile(id)).then(() => true, () => false)) return\n await writeAtomic(contentFile(id), JSON.stringify(content))\n }),\n async contentVersion(id) {\n try {\n const { mtimeNs, size } = await fs.stat(contentFile(id), { bigint: true })\n return `${mtimeNs}:${size}`\n }\n catch (error) {\n if (isMissing(error)) return null\n throw error\n }\n },\n async listContents() {\n const contents = await Promise.all((await listDocumentIds()).map(async id => ({ id, content: await loadContent(id) })))\n return contents.filter((entry): entry is { id: string, content: JSONContent } => entry.content !== null)\n },\n\n async loadState(id) {\n const data = await readOrNull(stateFile(id))\n return data && bytesOf(data)\n },\n storeState: async (id, state) => writeAtomic(stateFile(id), state),\n removeState: async id => fs.rm(stateFile(id), { force: true }),\n\n async storeAsset(id, { data, ...record }) {\n await writeAtomic(assetFile(id, record.name), data)\n await writeAtomic(recordFile(id, record.name), JSON.stringify(record))\n },\n async loadAsset(id, name) {\n const [record, data] = await Promise.all([readJson<AssetRecord>(recordFile(id, name)), readOrNull(assetFile(id, name))])\n return record && data ? { record, data } : null\n },\n\n loadSettings: async user => readJson(settingsFile(user)),\n storeSettings: async (user, settings) => writeAtomic(settingsFile(user), JSON.stringify(settings)),\n\n putDocument: (id, content) => locked(id, () => writeAtomic(contentFile(id), JSON.stringify(content))),\n deleteDocument: id => locked(id, async () => {\n await fs.rm(contentFile(id), { force: true })\n await fs.rm(stateFile(id), { force: true })\n await fs.rm(assetDir(id), { recursive: true, force: true })\n }),\n listDocumentIds,\n }\n}\n"],"mappings":";;;;;AAYA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,eAAe;AACrB,MAAM,aAAa;AACnB,MAAM,cAAc;;;;;AAMpB,SAAS,QAAQ,OAAuB;CACtC,IAAI,UAAU,IAAI,MAAM,IAAI,UAAU,4BAA4B;CAClE,MAAM,UAAU,mBAAmB,KAAK;CACxC,OAAO,QAAQ,WAAW,GAAG,IAAI,MAAM,QAAQ,MAAM,CAAC,MAAM;AAC9D;AAEA,eAAe,SAAY,MAAiC;CAC1D,MAAM,OAAO,MAAM,WAAW,IAAI;CAClC,OAAO,SAAS,OAAO,OAAO,KAAK,MAAM,KAAK,SAAS,MAAM,CAAC;AAChE;;AAGA,SAAS,YAAY;CACnB,MAAM,wBAAQ,IAAI,IAA8B;CAChD,QAAW,KAAa,WAAyC;EAC/D,MAAM,OAAO,MAAM,IAAI,GAAG,KAAK,QAAQ,QAAQ,EAAA,CAAG,KAAK,QAAQ,MAAM;EACrE,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC;EAC/B,MAAM,IAAI,KAAK,IAAI;EACnB,KAAU,WAAW;GACnB,IAAI,MAAM,IAAI,GAAG,MAAM,MAAM,MAAM,OAAO,GAAG;EAC/C,CAAC;EACD,OAAO;CACT;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBAAkB,SAAkD;CAClF,MAAM,OAAO,KAAK,QAAQ,QAAQ,GAAG;CACrC,MAAM,eAAe,OAAe,KAAK,KAAK,MAAM,GAAG,QAAQ,EAAE,IAAI,aAAa;CAClF,MAAM,aAAa,OAAe,KAAK,KAAK,MAAM,GAAG,QAAQ,EAAE,IAAI,WAAW;CAC9E,MAAM,gBAAgB,SAAiB,KAAK,KAAK,MAAM,cAAc,GAAG,QAAQ,YAAY,IAAI,CAAC,IAAI,aAAa;CAClH,MAAM,YAAY,OAAe,KAAK,KAAK,MAAM,YAAY,QAAQ,EAAE,CAAC;CACxE,MAAM,aAAa,IAAY,SAAiB,KAAK,KAAK,SAAS,EAAE,GAAG,QAAQ,IAAI,CAAC;CACrF,MAAM,cAAc,IAAY,SAAiB,KAAK,KAAK,SAAS,EAAE,GAAG,aAAa,GAAG,QAAQ,IAAI,IAAI,aAAa;CACtH,MAAM,SAAS,UAAU;CAEzB,eAAe,kBAAqC;EAClD,IAAI;EACJ,IAAI;GACF,UAAU,MAAMA,SAAG,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;EAC1D,SACO,OAAO;GACZ,IAAI,UAAU,KAAK,GAAG,OAAO,CAAC;GAC9B,MAAM;EACR;EACA,OAAO,QACJ,QAAO,UAAS,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,GAAG,KAAK,MAAM,KAAK,SAAS,WAAW,CAAC,CAAC,CAClG,KAAI,UAAS,mBAAmB,MAAM,KAAK,MAAM,GAAG,EAAmB,CAAC,CAAC,CAAC,CAC1E,KAAK;CACV;CAEA,MAAM,cAAc,OAAO,OAAe,SAAsB,YAAY,EAAE,CAAC;CAE/E,OAAO;EACL,GAAG,aAAa,OAAO;EAEvB;EACA,eAAe,IAAI,YAAY,OAAO,IAAI,YAAY;GAEpD,IAAI,CAAC,MAAMA,SAAG,KAAK,YAAY,EAAE,CAAC,CAAC,CAAC,WAAW,YAAY,KAAK,GAAG;GACnE,MAAM,YAAY,YAAY,EAAE,GAAG,KAAK,UAAU,OAAO,CAAC;EAC5D,CAAC;EACD,MAAM,eAAe,IAAI;GACvB,IAAI;IACF,MAAM,EAAE,SAAS,SAAS,MAAMA,SAAG,KAAK,YAAY,EAAE,GAAG,EAAE,QAAQ,KAAK,CAAC;IACzE,OAAO,GAAG,QAAQ,GAAG;GACvB,SACO,OAAO;IACZ,IAAI,UAAU,KAAK,GAAG,OAAO;IAC7B,MAAM;GACR;EACF;EACA,MAAM,eAAe;GAEnB,QAAO,MADgB,QAAQ,KAAK,MAAM,gBAAgB,EAAA,CAAG,IAAI,OAAM,QAAO;IAAE;IAAI,SAAS,MAAM,YAAY,EAAE;GAAE,EAAE,CAAC,EAAA,CACtG,QAAQ,UAAyD,MAAM,YAAY,IAAI;EACzG;EAEA,MAAM,UAAU,IAAI;GAClB,MAAM,OAAO,MAAM,WAAW,UAAU,EAAE,CAAC;GAC3C,OAAO,QAAQ,QAAQ,IAAI;EAC7B;EACA,YAAY,OAAO,IAAI,UAAU,YAAY,UAAU,EAAE,GAAG,KAAK;EACjE,aAAa,OAAM,OAAMA,SAAG,GAAG,UAAU,EAAE,GAAG,EAAE,OAAO,KAAK,CAAC;EAE7D,MAAM,WAAW,IAAI,EAAE,MAAM,GAAG,UAAU;GACxC,MAAM,YAAY,UAAU,IAAI,OAAO,IAAI,GAAG,IAAI;GAClD,MAAM,YAAY,WAAW,IAAI,OAAO,IAAI,GAAG,KAAK,UAAU,MAAM,CAAC;EACvE;EACA,MAAM,UAAU,IAAI,MAAM;GACxB,MAAM,CAAC,QAAQ,QAAQ,MAAM,QAAQ,IAAI,CAAC,SAAsB,WAAW,IAAI,IAAI,CAAC,GAAG,WAAW,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC;GACvH,OAAO,UAAU,OAAO;IAAE;IAAQ;GAAK,IAAI;EAC7C;EAEA,cAAc,OAAM,SAAQ,SAAS,aAAa,IAAI,CAAC;EACvD,eAAe,OAAO,MAAM,aAAa,YAAY,aAAa,IAAI,GAAG,KAAK,UAAU,QAAQ,CAAC;EAEjG,cAAc,IAAI,YAAY,OAAO,UAAU,YAAY,YAAY,EAAE,GAAG,KAAK,UAAU,OAAO,CAAC,CAAC;EACpG,iBAAgB,OAAM,OAAO,IAAI,YAAY;GAC3C,MAAMA,SAAG,GAAG,YAAY,EAAE,GAAG,EAAE,OAAO,KAAK,CAAC;GAC5C,MAAMA,SAAG,GAAG,UAAU,EAAE,GAAG,EAAE,OAAO,KAAK,CAAC;GAC1C,MAAMA,SAAG,GAAG,SAAS,EAAE,GAAG;IAAE,WAAW;IAAM,OAAO;GAAK,CAAC;EAC5D,CAAC;EACD;CACF;AACF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EditorAdapter } from "../server/adapter.js";
|
|
2
|
+
import { AiModels, AiModelsConfig } from "../server/ai.js";
|
|
3
|
+
import { CollabServer, CollabServerOptions } from "../server/collab.js";
|
|
4
|
+
import { RateLimiterOptions } from "../server/rate-limit.js";
|
|
5
|
+
import "../server/index.js";
|
|
6
|
+
import { DocumentAdmin } from "./shared.js";
|
|
7
|
+
import { CommentStore } from "./comments/types.js";
|
|
8
|
+
import { Router } from "h3";
|
|
9
|
+
//#region src/adapters/nitro.d.ts
|
|
10
|
+
/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */
|
|
11
|
+
export interface NitroHost {
|
|
12
|
+
router: Pick<Router, 'add' | 'use'>;
|
|
13
|
+
hooks: {
|
|
14
|
+
hook(name: 'close', listener: () => void | Promise<void>): unknown;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface EditorServerOptions {
|
|
18
|
+
/** Storage and identity. An adapter that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them. */
|
|
19
|
+
adapter: EditorAdapter & Partial<DocumentAdmin>;
|
|
20
|
+
/**
|
|
21
|
+
* The collaboration websocket (`/collab/:id`). Extensions, hooks and Hocuspocus configuration of `createCollabServer`;
|
|
22
|
+
* `{}` takes everything from the adapter. `false` mounts no websocket (documents then only load and save over HTTP).
|
|
23
|
+
* Needs `experimental: { websocket: true }` in the Nitro config.
|
|
24
|
+
*/
|
|
25
|
+
collab: Omit<CollabServerOptions, 'adapter'> | false;
|
|
26
|
+
/**
|
|
27
|
+
* Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`
|
|
28
|
+
* and `listModels` default to the adapter's. Omitted: no AI routes.
|
|
29
|
+
*/
|
|
30
|
+
ai?: Partial<AiModelsConfig>;
|
|
31
|
+
/** Per-user limit of the AI endpoints; default 30 requests per minute. */
|
|
32
|
+
rateLimit?: RateLimiterOptions;
|
|
33
|
+
/** Mounts the comments endpoint (`/api/editor/comments/:id`) over this store; omitted: no comments routes. */
|
|
34
|
+
comments?: {
|
|
35
|
+
store: CommentStore;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface EditorServer {
|
|
39
|
+
adapter: EditorServerOptions['adapter'];
|
|
40
|
+
/** The collaboration server, `undefined` with `collab: false`. */
|
|
41
|
+
collab: CollabServer | undefined;
|
|
42
|
+
/** The AI models, `undefined` without `ai`. */
|
|
43
|
+
models: AiModels | undefined;
|
|
44
|
+
/** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wires the editor backend into `nitroApp` (call it from a Nitro plugin):
|
|
49
|
+
*
|
|
50
|
+
* /api/editor/{settings,users,tags,assets/…,themes/…} the editor's own endpoints
|
|
51
|
+
* /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)
|
|
52
|
+
* /api/editor/comments/:id GET/POST comments, with `comments`
|
|
53
|
+
* /api/ai/{models,chat,complete,generate,transcribe,transcript-cleanup} with `ai`
|
|
54
|
+
* /collab/:id the collaboration websocket, unless `collab: false`
|
|
55
|
+
*
|
|
56
|
+
* Every route authenticates through `adapter.authenticate`. The collaboration server is closed with Nitro's `close`
|
|
57
|
+
* hook. The paths are the ones `@ecsplico/editor/nuxt` serves, so the editor's client works unchanged.
|
|
58
|
+
*/
|
|
59
|
+
export declare function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer;
|
|
60
|
+
//#endregion
|
|
61
|
+
//# sourceMappingURL=nitro.d.ts.map
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { HttpError } from "../server/errors.js";
|
|
2
|
+
import { createAiModels } from "../server/ai.js";
|
|
3
|
+
import { createCollabServer } from "../server/collab.js";
|
|
4
|
+
import { createRateLimiter } from "../server/rate-limit.js";
|
|
5
|
+
import { chatPost, completePost, generatePost, modelsGet, transcribePost, transcriptCleanupPost } from "../server/handlers/ai.js";
|
|
6
|
+
import { assetGet, assetPost, settingsGet, settingsPut, tagsGet, themeDelete, themeGet, themePost, themesGet, usersGet } from "../server/handlers/editor.js";
|
|
7
|
+
import { AI_API, COLLAB_PATH, COMMENTS_ROUTE, DOCUMENT_ROUTE, EDITOR_API } from "./routes.js";
|
|
8
|
+
import { commentsGet, commentsPost, documentGet, documentPut } from "./documents.js";
|
|
9
|
+
import { createError, defineEventHandler, defineWebSocketHandler, getQuery, getRouterParams, readBody, readFormData, setResponseHeaders, setResponseStatus } from "h3";
|
|
10
|
+
//#region src/adapters/nitro.ts
|
|
11
|
+
const API = EDITOR_API;
|
|
12
|
+
/** The `HandlerRequest` of a Nitro event. `signal` aborts when the client goes away. */
|
|
13
|
+
function requestOf(event) {
|
|
14
|
+
const abort = new AbortController();
|
|
15
|
+
event.node.res.once("close", () => abort.abort());
|
|
16
|
+
return {
|
|
17
|
+
headers: event.headers,
|
|
18
|
+
params: getRouterParams(event, { decode: true }),
|
|
19
|
+
query: Object.fromEntries(Object.entries(getQuery(event)).map(([key, value]) => [key, value === void 0 ? void 0 : String(value)])),
|
|
20
|
+
readJson: () => readBody(event),
|
|
21
|
+
readForm: () => readFormData(event),
|
|
22
|
+
signal: abort.signal
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** An event handler that runs `handler` with `ctx` and sends its `HandlerResponse`; an `HttpError` becomes Nitro's error response. */
|
|
26
|
+
function routeOf(ctx, handler) {
|
|
27
|
+
return defineEventHandler(async (event) => {
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await handler(ctx, requestOf(event));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!(error instanceof HttpError)) throw error;
|
|
33
|
+
setResponseHeaders(event, error.headers);
|
|
34
|
+
throw createError({
|
|
35
|
+
statusCode: error.statusCode,
|
|
36
|
+
statusMessage: error.statusMessage,
|
|
37
|
+
message: error.statusMessage,
|
|
38
|
+
data: error.data
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
setResponseStatus(event, response.status);
|
|
42
|
+
setResponseHeaders(event, response.headers);
|
|
43
|
+
return response.body;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const editorRoutes = [
|
|
47
|
+
[
|
|
48
|
+
"get",
|
|
49
|
+
`${API}/settings`,
|
|
50
|
+
settingsGet
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
"put",
|
|
54
|
+
`${API}/settings`,
|
|
55
|
+
settingsPut
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"get",
|
|
59
|
+
`${API}/users`,
|
|
60
|
+
usersGet
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"get",
|
|
64
|
+
`${API}/tags`,
|
|
65
|
+
tagsGet
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"post",
|
|
69
|
+
`${API}/assets/:id`,
|
|
70
|
+
assetPost
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"get",
|
|
74
|
+
`${API}/assets/:id/:name`,
|
|
75
|
+
assetGet
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"get",
|
|
79
|
+
`${API}/themes`,
|
|
80
|
+
themesGet
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"post",
|
|
84
|
+
`${API}/themes`,
|
|
85
|
+
themePost
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"get",
|
|
89
|
+
`${API}/themes/:id`,
|
|
90
|
+
themeGet
|
|
91
|
+
],
|
|
92
|
+
[
|
|
93
|
+
"delete",
|
|
94
|
+
`${API}/themes/:id`,
|
|
95
|
+
themeDelete
|
|
96
|
+
]
|
|
97
|
+
];
|
|
98
|
+
const aiRoutes = [
|
|
99
|
+
[
|
|
100
|
+
"get",
|
|
101
|
+
`${AI_API}/models`,
|
|
102
|
+
modelsGet
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"post",
|
|
106
|
+
`${AI_API}/chat`,
|
|
107
|
+
chatPost
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"post",
|
|
111
|
+
`${AI_API}/complete`,
|
|
112
|
+
completePost
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"post",
|
|
116
|
+
`${AI_API}/generate`,
|
|
117
|
+
generatePost
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"post",
|
|
121
|
+
`${AI_API}/transcribe`,
|
|
122
|
+
transcribePost
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"post",
|
|
126
|
+
`${AI_API}/transcript-cleanup`,
|
|
127
|
+
transcriptCleanupPost
|
|
128
|
+
]
|
|
129
|
+
];
|
|
130
|
+
const collabRoute = (collab) => defineWebSocketHandler({
|
|
131
|
+
open(peer) {
|
|
132
|
+
peer._hocuspocus = collab.handleConnection(peer.websocket, peer.request);
|
|
133
|
+
},
|
|
134
|
+
message(peer, message) {
|
|
135
|
+
peer._hocuspocus?.handleMessage(message.uint8Array());
|
|
136
|
+
},
|
|
137
|
+
close(peer, details) {
|
|
138
|
+
peer._hocuspocus?.handleClose({
|
|
139
|
+
code: details.code ?? 1e3,
|
|
140
|
+
reason: details.reason ?? ""
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
error(peer, error) {
|
|
144
|
+
console.error("[collab]", peer.id, error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* Wires the editor backend into `nitroApp` (call it from a Nitro plugin):
|
|
149
|
+
*
|
|
150
|
+
* /api/editor/{settings,users,tags,assets/…,themes/…} the editor's own endpoints
|
|
151
|
+
* /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)
|
|
152
|
+
* /api/editor/comments/:id GET/POST comments, with `comments`
|
|
153
|
+
* /api/ai/{models,chat,complete,generate,transcribe,transcript-cleanup} with `ai`
|
|
154
|
+
* /collab/:id the collaboration websocket, unless `collab: false`
|
|
155
|
+
*
|
|
156
|
+
* Every route authenticates through `adapter.authenticate`. The collaboration server is closed with Nitro's `close`
|
|
157
|
+
* hook. The paths are the ones `@ecsplico/editor/nuxt` serves, so the editor's client works unchanged.
|
|
158
|
+
*/
|
|
159
|
+
function installEditorServer(nitroApp, options) {
|
|
160
|
+
const { adapter } = options;
|
|
161
|
+
const collab = options.collab === false ? void 0 : createCollabServer({
|
|
162
|
+
adapter,
|
|
163
|
+
...options.collab
|
|
164
|
+
});
|
|
165
|
+
const models = options.ai && createAiModels({
|
|
166
|
+
presets: {},
|
|
167
|
+
languageModel: adapter.languageModel && ((ref, user) => adapter.languageModel(ref, user)),
|
|
168
|
+
listModels: adapter.listModels && ((user) => adapter.listModels(user)),
|
|
169
|
+
...options.ai
|
|
170
|
+
});
|
|
171
|
+
const context = {
|
|
172
|
+
adapter,
|
|
173
|
+
...collab && { collab }
|
|
174
|
+
};
|
|
175
|
+
for (const [method, path, handler] of editorRoutes) nitroApp.router.add(path, routeOf(context, handler), method);
|
|
176
|
+
nitroApp.router.add(DOCUMENT_ROUTE, routeOf(context, documentGet), "get");
|
|
177
|
+
nitroApp.router.add(DOCUMENT_ROUTE, routeOf(context, documentPut), "put");
|
|
178
|
+
if (options.comments) {
|
|
179
|
+
const comments = {
|
|
180
|
+
...context,
|
|
181
|
+
store: options.comments.store
|
|
182
|
+
};
|
|
183
|
+
nitroApp.router.add(COMMENTS_ROUTE, routeOf(comments, commentsGet), "get");
|
|
184
|
+
nitroApp.router.add(COMMENTS_ROUTE, routeOf(comments, commentsPost), "post");
|
|
185
|
+
}
|
|
186
|
+
if (models) {
|
|
187
|
+
const ai = {
|
|
188
|
+
...context,
|
|
189
|
+
ai: models,
|
|
190
|
+
limiter: createRateLimiter(options.rateLimit ?? {
|
|
191
|
+
max: 30,
|
|
192
|
+
windowMs: 6e4
|
|
193
|
+
})
|
|
194
|
+
};
|
|
195
|
+
for (const [method, path, handler] of aiRoutes) nitroApp.router.add(path, routeOf(ai, handler), method);
|
|
196
|
+
}
|
|
197
|
+
if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab));
|
|
198
|
+
const server = {
|
|
199
|
+
adapter,
|
|
200
|
+
collab,
|
|
201
|
+
models,
|
|
202
|
+
close: async () => {
|
|
203
|
+
await collab?.close();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
nitroApp.hooks.hook("close", () => server.close());
|
|
207
|
+
return server;
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
export { installEditorServer };
|
|
211
|
+
|
|
212
|
+
//# sourceMappingURL=nitro.js.map
|