@ecsplico/editor 3.4.0 → 3.5.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/code/CodeSurface.vue.d.ts +3 -3
  3. package/dist/core/abilities.js +1 -0
  4. package/dist/core/abilities.js.map +1 -1
  5. package/dist/core/backend-capability.d.ts +1 -1
  6. package/dist/core/backend-capability.js +2 -1
  7. package/dist/core/backend-capability.js.map +1 -1
  8. package/dist/core/backend-types.d.ts +25 -0
  9. package/dist/core/backend-types.js.map +1 -1
  10. package/dist/core/backend.d.ts +9 -1
  11. package/dist/core/backend.js.map +1 -1
  12. package/dist/core/http-backend.js +17 -0
  13. package/dist/core/http-backend.js.map +1 -1
  14. package/dist/core/schema/abilities.d.ts +1 -0
  15. package/dist/core/schema/abilities.js +4 -0
  16. package/dist/core/schema/abilities.js.map +1 -1
  17. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  18. package/dist/features/composites/nodes.js +1 -0
  19. package/dist/features/composites/nodes.js.map +1 -1
  20. package/dist/features/image-studio/components/ImageStudioChat.js +121 -0
  21. package/dist/features/image-studio/components/ImageStudioChat.js.map +1 -0
  22. package/dist/features/image-studio/components/ImageStudioDialog.js +371 -0
  23. package/dist/features/image-studio/components/ImageStudioDialog.js.map +1 -0
  24. package/dist/features/image-studio/components/ImageStudioDialog.vue.d.ts +25 -0
  25. package/dist/features/image-studio/components/ImageStudioTile.js +74 -0
  26. package/dist/features/image-studio/components/ImageStudioTile.js.map +1 -0
  27. package/dist/features/image-studio/components/ImageStudioView.js +117 -0
  28. package/dist/features/image-studio/components/ImageStudioView.js.map +1 -0
  29. package/dist/features/image-studio/feature.d.ts +24 -0
  30. package/dist/features/image-studio/feature.js +53 -0
  31. package/dist/features/image-studio/feature.js.map +1 -0
  32. package/dist/features/image-studio/index.d.ts +5 -0
  33. package/dist/features/image-studio/index.js +4 -0
  34. package/dist/features/image-studio/node-core.d.ts +46 -0
  35. package/dist/features/image-studio/node-core.js +79 -0
  36. package/dist/features/image-studio/node-core.js.map +1 -0
  37. package/dist/features/image-studio/node-store.d.ts +19 -0
  38. package/dist/features/image-studio/node-store.js +35 -0
  39. package/dist/features/image-studio/node-store.js.map +1 -0
  40. package/dist/features/image-studio/react/ImageStudioChat.js +101 -0
  41. package/dist/features/image-studio/react/ImageStudioChat.js.map +1 -0
  42. package/dist/features/image-studio/react/ImageStudioDialog.d.ts +19 -0
  43. package/dist/features/image-studio/react/ImageStudioDialog.js +357 -0
  44. package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -0
  45. package/dist/features/image-studio/react/ImageStudioTile.js +51 -0
  46. package/dist/features/image-studio/react/ImageStudioTile.js.map +1 -0
  47. package/dist/features/image-studio/react/ImageStudioView.js +126 -0
  48. package/dist/features/image-studio/react/ImageStudioView.js.map +1 -0
  49. package/dist/features/image-studio/react/store.d.ts +37 -0
  50. package/dist/features/image-studio/react/store.js +46 -0
  51. package/dist/features/image-studio/react/store.js.map +1 -0
  52. package/dist/features/image-studio/react.d.ts +18 -0
  53. package/dist/features/image-studio/react.js +21 -0
  54. package/dist/features/image-studio/react.js.map +1 -0
  55. package/dist/features/image-studio/schema.d.ts +29 -0
  56. package/dist/features/image-studio/schema.js +86 -0
  57. package/dist/features/image-studio/schema.js.map +1 -0
  58. package/dist/features/image-studio/state.d.ts +52 -0
  59. package/dist/features/image-studio/state.js +147 -0
  60. package/dist/features/image-studio/state.js.map +1 -0
  61. package/dist/features/image-studio/store.d.ts +29 -0
  62. package/dist/features/image-studio/vue.d.ts +17 -0
  63. package/dist/features/image-studio/vue.js +20 -0
  64. package/dist/features/image-studio/vue.js.map +1 -0
  65. package/dist/features/media/schema.d.ts +5 -1
  66. package/dist/features/media/schema.js +3 -3
  67. package/dist/features/media/schema.js.map +1 -1
  68. package/dist/features/transcript/segment-node.js +1 -0
  69. package/dist/features/transcript/segment-node.js.map +1 -1
  70. package/dist/i18n/de/image-studio.js +39 -0
  71. package/dist/i18n/de/image-studio.js.map +1 -0
  72. package/dist/i18n/de.d.ts +32 -0
  73. package/dist/i18n/de.js +13 -11
  74. package/dist/i18n/de.js.map +1 -1
  75. package/dist/i18n/en/image-studio.js +39 -0
  76. package/dist/i18n/en/image-studio.js.map +1 -0
  77. package/dist/i18n/en.js +13 -11
  78. package/dist/i18n/en.js.map +1 -1
  79. package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
  80. package/dist/nuxt/module.d.ts +64 -0
  81. package/dist/server/handlers/ai.d.ts +2 -2
  82. package/dist/server/handlers/images.d.ts +22 -0
  83. package/dist/server/handlers/images.js +46 -0
  84. package/dist/server/handlers/images.js.map +1 -0
  85. package/dist/server/handlers/index.d.ts +2 -1
  86. package/dist/server/handlers/index.js +2 -1
  87. package/dist/server/index.d.ts +2 -2
  88. package/dist/server/ports.d.ts +11 -1
  89. package/dist/server/ports.js +2 -1
  90. package/dist/server/ports.js.map +1 -1
  91. package/dist/server/routes.js +19 -0
  92. package/dist/server/routes.js.map +1 -1
  93. package/dist/tree/tree-host.js +1 -1
  94. package/dist/tree/tree-items.js +1 -1
  95. package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
  96. package/dist/vue/SimpleEditor.vue.d.ts +6 -6
  97. package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
  98. package/package.json +21 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.1 - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - React entry of the Bild-Studio, `@ecsplico/editor/image-studio/react`: `imageStudioFeature()` (the `imageStudio` block with its shadcn node view), `ImageStudioDialog` (props `open`, `onOpenChange`, `store`, `title?`; same behaviour and texts as the Vue dialog) and `ImageStudioReactStore`, the dialog's contract with plain values instead of refs (`state`, `promptBlocks`, `problem`, `update`, `generate`, `upload`; the host re-renders with the new `state` after `update`). `useImageStudioNodeStore({ editor, getPos, backend, documentId })` is the store of one node.
8
+ - `createImageStudioNodeCore(options)` (`ImageStudioNodeCore`, `ImageStudioNodeSnapshot`, `ImageStudioNodeCoreOptions`, exported from `/image-studio/react`): the framework-free store of one `imageStudio` node (`get()`, `subscribe(listener)`, `update`, `generate`, `upload`, `dispose()`). `createImageStudioNodeStore` (Vue) is now a wrapper over it; its public API is unchanged.
9
+
10
+ ## 3.5.0 - 2026-10-21
11
+
12
+ ### Added
13
+
14
+ - Bild-Studio (`@ecsplico/editor/image-studio`, `/image-studio/vue`, `/image-studio/schema`): an atom block `imageStudio` (attrs `src`, `alt`, `studio`; ability `ABILITY.imageStudio` = `insert.imageStudio`, revokes text abilities) that holds the text describing an image, the active Prompt-Bausteine, references, previous generations, the selected image and a chat log (`ImageStudioState`, `ImageStudioImage`, `ImageStudioTurn`, `ImageStudioPromptBlock`). `src` always mirrors the selected image. Markdown form `[Bild-Studio: <alt>](<src>) <!-- {"studio": …} -->`, round-trip stable; HTML `<img data-image-studio data-studio>`. Commands `insertImageStudio(attrs?)` and `setImageStudioState(pos, state)` (one transaction, `src` follows), `/` entry „Bild-Studio“, block action `image-studio.open` „Im Bild-Studio öffnen“ (placement `both`, event `IMAGE_STUDIO_OPEN`). Pure reducers `EMPTY_IMAGE_STUDIO_STATE`, `setDescription`, `togglePromptBlock`, `toggleImage`, `select`, `removeImage`, `addReference`, `addGeneration` (appends and selects), `selectedImage`, `normalizeImageStudioState`, `isEmptyImageStudioState`.
15
+ - `ImageStudioDialog` (Vue): three columns (description and Prompt-Bausteine as switches; references with upload and drop zone, previous generations, tiles to activate, select or remove; the selected image with a chat that generates, `Enter` sends, closing aborts) driven by an `ImageStudioStore` (`state`, `promptBlocks`, `update`, `generate`, `upload`, `problem`), so a host can keep the state outside the document. `imageStudioFeature()` adds the node view (`ImageStudioView`) and the node's own store `createImageStudioNodeStore({ editor, getPos, backend, documentId })`.
16
+ - Backend port `EditorBackend.images?` (`promptBlocks()`, `generate(body: ImageStudioGenerateBody, signal?)`, `upload(documentId, file)`), capability `images` (probed by the HTTP backend; the memory backend has none and the node view shows „Bildgenerierung ist nicht eingerichtet“). Server port `EditorPorts.images?: ImageProvider` with routes `GET /api/editor/images/prompt-blocks`, `POST /api/editor/images/generate`, `POST /api/editor/images/upload`, mounted and listed in the capabilities only with the port. Guide „Bild-Studio“.
17
+
18
+ ### Changed
19
+
20
+ - `attrComment` and `mediaTokenizer` are exported from `@ecsplico/editor/media/schema` (shared with the Bild-Studio's Markdown form); `attrComment` escapes U+2028 and U+2029 in the JSON comment so such text survives a Markdown round trip.
21
+
3
22
  ## 3.4.0 - 2026-10-21
4
23
 
5
24
  ### Added
@@ -11,17 +11,17 @@ type __VLS_Props = {
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
12
12
  view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
13
13
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
+ ready: (view: EditorView) => any;
14
15
  "update:modelValue": (text: string) => any;
15
16
  save: (text: string) => any;
16
- ready: (view: EditorView) => any;
17
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
+ onReady?: ((view: EditorView) => any) | undefined;
18
19
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
19
20
  onSave?: ((text: string) => any) | undefined;
20
- onReady?: ((view: EditorView) => any) | undefined;
21
21
  }>, {
22
22
  editable: boolean;
23
- wordWrap: boolean;
24
23
  modelValue: string;
24
+ wordWrap: boolean;
25
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
26
  declare const _default: typeof __VLS_export;
27
27
  //#endregion
@@ -15,6 +15,7 @@ const DEFAULT_ABILITY_RULES = {
15
15
  ABILITY.composite,
16
16
  ABILITY.transcript,
17
17
  ABILITY.media,
18
+ ABILITY.imageStudio,
18
19
  ABILITY.record,
19
20
  ABILITY.mention,
20
21
  ABILITY.reference,
@@ -1 +1 @@
1
- {"version":3,"file":"abilities.js","names":[],"sources":["../../src/core/abilities.ts"],"sourcesContent":["import { type AnyExtension, type Editor, Extension, getExtensionField, type InputRule, type KeyboardShortcutCommand } from '@tiptap/core'\nimport { keydownHandler } from '@tiptap/pm/keymap'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport {\n ABILITY,\n type Ability,\n type AbilityRuleSource,\n resolveAbilities,\n ROOT_ABILITIES,\n TEXT_ABILITIES,\n TYPE_ABILITIES,\n} from './schema/abilities'\nimport type { AbilitiesOptions } from './extension-points'\n\nexport interface AbilitiesStorage {\n rules: Map<string, AbilityRuleSource>\n root: readonly Ability[]\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n abilities: AbilitiesStorage\n }\n}\n\n/** Rules for StarterKit/extension nodes that cannot carry `NodeConfig.abilities` themselves. */\nexport const DEFAULT_ABILITY_RULES: Record<string, AbilityRuleSource> = {\n codeBlock: {\n revokes: [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.rule, ABILITY.blockContent, ABILITY.composite, ABILITY.transcript,\n ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiAutocomplete,\n ],\n },\n image: { revokes: TEXT_ABILITIES },\n horizontalRule: { revokes: TEXT_ABILITIES },\n}\n\n/**\n * Abilities at `pos`: chain = ancestors at depth 1…depth, plus `node` when given (an atom starting at `pos`).\n * A `ResolvedPos` addresses a document other than `editor.state.doc`, e.g. a transaction's doc inside plugin `apply`.\n */\nexport function abilitiesAt(editor: Editor, pos: number | ResolvedPos, node?: PMNode): ReadonlySet<Ability> {\n const $pos = typeof pos === 'number' ? editor.state.doc.resolve(pos) : pos\n const chain: PMNode[] = []\n for (let depth = 1; depth <= $pos.depth; depth++) chain.push($pos.node(depth))\n if (node) chain.push(node)\n const { rules, root } = editor.storage.abilities\n return resolveAbilities(rules, root, chain)\n}\n\n/** Abilities of the selection: the selected node, or what both ends of a range allow. */\nexport function abilitiesAtSelection(editor: Editor): ReadonlySet<Ability> {\n const { selection } = editor.state\n if (selection instanceof NodeSelection) return abilitiesAt(editor, selection.from, selection.node)\n const from = abilitiesAt(editor, selection.$from.pos)\n if (selection.empty) return from\n const to = abilitiesAt(editor, selection.$to.pos)\n return new Set([...from].filter(ability => to.has(ability)))\n}\n\nexport function hasAbility(editor: Editor, ability: Ability): boolean {\n return abilitiesAtSelection(editor).has(ability)\n}\n\n/** Only modifier shortcuts are gated; plain Enter/Tab/Backspace keep their structural behaviour everywhere. */\nconst GATED_KEY = /(^|-)(Mod|Alt)-/\n/** How much text before the caret input rules get to see (mirrors TipTap's input-rule plugin). */\nconst MAX_MATCH = 500\n\ninterface GatedInputRule {\n find: InputRule['find']\n ability: Ability\n}\n\n/** Same `this` the ExtensionManager hands to `addKeyboardShortcuts` / `addInputRules` (core dist, `get plugins()`). */\nfunction extensionContext(ext: AnyExtension, editor: Editor) {\n return {\n name: ext.name,\n options: ext.options,\n storage: editor.extensionStorage[ext.name as keyof typeof editor.extensionStorage],\n editor,\n type: editor.schema.nodes[ext.name] ?? editor.schema.marks[ext.name] ?? null,\n }\n}\n\nfunction gatedExtensions(editor: Editor): [AnyExtension, Ability][] {\n return editor.extensionManager.extensions.flatMap((ext): [AnyExtension, Ability][] => {\n const ability = TYPE_ABILITIES[ext.name]\n return ability ? [[ext, ability]] : []\n })\n}\n\nfunction buildKeyBindings(editor: Editor) {\n const abilitiesByKey = new Map<string, Ability[]>()\n for (const [ext, ability] of gatedExtensions(editor)) {\n const addKeyboardShortcuts = getExtensionField<(() => Record<string, KeyboardShortcutCommand>) | undefined>(\n ext, 'addKeyboardShortcuts', extensionContext(ext, editor),\n )\n for (const key of Object.keys(addKeyboardShortcuts?.() ?? {})) {\n if (!GATED_KEY.test(key)) continue\n abilitiesByKey.set(key, [...(abilitiesByKey.get(key) ?? []), ability])\n }\n }\n // Returning true swallows the key, so the type's own binding never runs.\n return Object.fromEntries([...abilitiesByKey].map(([key, abilities]) => [\n key,\n () => {\n const present = abilitiesAtSelection(editor)\n return abilities.some(ability => !present.has(ability))\n },\n ]))\n}\n\nfunction buildInputRules(editor: Editor): GatedInputRule[] {\n return gatedExtensions(editor).flatMap(([ext, ability]) => {\n const addInputRules = getExtensionField<(() => InputRule[]) | undefined>(ext, 'addInputRules', extensionContext(ext, editor))\n return (addInputRules?.() ?? []).map(rule => ({ find: rule.find, ability }))\n })\n}\n\nfunction matches(find: InputRule['find'], text: string): boolean {\n if (typeof find === 'function') return find(text) != null\n find.lastIndex = 0\n return find.exec(text) !== null\n}\n\n/**\n * Hierarchical abilities (see shared/editor/abilities.ts). Resolution reads `editor.storage.abilities`; the plugin\n * swallows `Mod-`/`Alt-` shortcuts and pre-empts input rules of node/mark types whose ability is missing at the caret.\n * Paste, drop and remote changes are not filtered.\n */\nexport const Abilities = Extension.create<AbilitiesOptions, AbilitiesStorage>({\n name: 'abilities',\n // Highest priority: its plugin's handleKeyDown / handleTextInput run before the gated types' keymaps and input rules.\n priority: 1000,\n\n addOptions() {\n return { root: ROOT_ABILITIES, rules: DEFAULT_ABILITY_RULES }\n },\n\n addStorage() {\n return { rules: new Map(), root: this.options.root }\n },\n\n onBeforeCreate() {\n const rules = new Map<string, AbilityRuleSource>()\n for (const ext of this.editor.extensionManager.extensions) {\n const rule = getExtensionField<AbilityRuleSource | undefined>(ext, 'abilities')\n if (rule) rules.set(ext.name, rule)\n }\n // Host/default rules replace a node's own rule as a whole.\n for (const [name, rule] of Object.entries(this.options.rules)) rules.set(name, rule)\n this.storage.rules = rules\n this.storage.root = this.options.root\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n // Built lazily on first use, once per plugin: needs the final extension list and schema.\n let handleKey: ((view: EditorView, event: KeyboardEvent) => boolean) | undefined\n let inputRules: GatedInputRule[] | undefined\n\n return [\n new Plugin({\n key: new PluginKey('abilities'),\n props: {\n handleKeyDown(view, event) {\n handleKey ??= keydownHandler(buildKeyBindings(editor))\n return handleKey(view, event)\n },\n handleTextInput(view, from, to, text) {\n inputRules ??= buildInputRules(editor)\n if (!inputRules.length) return false\n const $from = view.state.doc.resolve(from)\n if (!$from.parent.isTextblock) return false\n const textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, undefined, '\\ufffc') + text\n let present: ReadonlySet<Ability> | undefined\n for (const rule of inputRules) {\n present ??= abilitiesAt(editor, from)\n if (present.has(rule.ability) || !matches(rule.find, textBefore)) continue\n // Type the character literally; the rule's own plugin never sees this input.\n view.dispatch(view.state.tr.insertText(text, from, to))\n return true\n }\n return false\n },\n },\n }),\n ]\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"abilities.js","names":[],"sources":["../../src/core/abilities.ts"],"sourcesContent":["import { type AnyExtension, type Editor, Extension, getExtensionField, type InputRule, type KeyboardShortcutCommand } from '@tiptap/core'\nimport { keydownHandler } from '@tiptap/pm/keymap'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport {\n ABILITY,\n type Ability,\n type AbilityRuleSource,\n resolveAbilities,\n ROOT_ABILITIES,\n TEXT_ABILITIES,\n TYPE_ABILITIES,\n} from './schema/abilities'\nimport type { AbilitiesOptions } from './extension-points'\n\nexport interface AbilitiesStorage {\n rules: Map<string, AbilityRuleSource>\n root: readonly Ability[]\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n abilities: AbilitiesStorage\n }\n}\n\n/** Rules for StarterKit/extension nodes that cannot carry `NodeConfig.abilities` themselves. */\nexport const DEFAULT_ABILITY_RULES: Record<string, AbilityRuleSource> = {\n codeBlock: {\n revokes: [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.rule, ABILITY.blockContent, ABILITY.composite, ABILITY.transcript,\n ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiAutocomplete,\n ],\n },\n image: { revokes: TEXT_ABILITIES },\n horizontalRule: { revokes: TEXT_ABILITIES },\n}\n\n/**\n * Abilities at `pos`: chain = ancestors at depth 1…depth, plus `node` when given (an atom starting at `pos`).\n * A `ResolvedPos` addresses a document other than `editor.state.doc`, e.g. a transaction's doc inside plugin `apply`.\n */\nexport function abilitiesAt(editor: Editor, pos: number | ResolvedPos, node?: PMNode): ReadonlySet<Ability> {\n const $pos = typeof pos === 'number' ? editor.state.doc.resolve(pos) : pos\n const chain: PMNode[] = []\n for (let depth = 1; depth <= $pos.depth; depth++) chain.push($pos.node(depth))\n if (node) chain.push(node)\n const { rules, root } = editor.storage.abilities\n return resolveAbilities(rules, root, chain)\n}\n\n/** Abilities of the selection: the selected node, or what both ends of a range allow. */\nexport function abilitiesAtSelection(editor: Editor): ReadonlySet<Ability> {\n const { selection } = editor.state\n if (selection instanceof NodeSelection) return abilitiesAt(editor, selection.from, selection.node)\n const from = abilitiesAt(editor, selection.$from.pos)\n if (selection.empty) return from\n const to = abilitiesAt(editor, selection.$to.pos)\n return new Set([...from].filter(ability => to.has(ability)))\n}\n\nexport function hasAbility(editor: Editor, ability: Ability): boolean {\n return abilitiesAtSelection(editor).has(ability)\n}\n\n/** Only modifier shortcuts are gated; plain Enter/Tab/Backspace keep their structural behaviour everywhere. */\nconst GATED_KEY = /(^|-)(Mod|Alt)-/\n/** How much text before the caret input rules get to see (mirrors TipTap's input-rule plugin). */\nconst MAX_MATCH = 500\n\ninterface GatedInputRule {\n find: InputRule['find']\n ability: Ability\n}\n\n/** Same `this` the ExtensionManager hands to `addKeyboardShortcuts` / `addInputRules` (core dist, `get plugins()`). */\nfunction extensionContext(ext: AnyExtension, editor: Editor) {\n return {\n name: ext.name,\n options: ext.options,\n storage: editor.extensionStorage[ext.name as keyof typeof editor.extensionStorage],\n editor,\n type: editor.schema.nodes[ext.name] ?? editor.schema.marks[ext.name] ?? null,\n }\n}\n\nfunction gatedExtensions(editor: Editor): [AnyExtension, Ability][] {\n return editor.extensionManager.extensions.flatMap((ext): [AnyExtension, Ability][] => {\n const ability = TYPE_ABILITIES[ext.name]\n return ability ? [[ext, ability]] : []\n })\n}\n\nfunction buildKeyBindings(editor: Editor) {\n const abilitiesByKey = new Map<string, Ability[]>()\n for (const [ext, ability] of gatedExtensions(editor)) {\n const addKeyboardShortcuts = getExtensionField<(() => Record<string, KeyboardShortcutCommand>) | undefined>(\n ext, 'addKeyboardShortcuts', extensionContext(ext, editor),\n )\n for (const key of Object.keys(addKeyboardShortcuts?.() ?? {})) {\n if (!GATED_KEY.test(key)) continue\n abilitiesByKey.set(key, [...(abilitiesByKey.get(key) ?? []), ability])\n }\n }\n // Returning true swallows the key, so the type's own binding never runs.\n return Object.fromEntries([...abilitiesByKey].map(([key, abilities]) => [\n key,\n () => {\n const present = abilitiesAtSelection(editor)\n return abilities.some(ability => !present.has(ability))\n },\n ]))\n}\n\nfunction buildInputRules(editor: Editor): GatedInputRule[] {\n return gatedExtensions(editor).flatMap(([ext, ability]) => {\n const addInputRules = getExtensionField<(() => InputRule[]) | undefined>(ext, 'addInputRules', extensionContext(ext, editor))\n return (addInputRules?.() ?? []).map(rule => ({ find: rule.find, ability }))\n })\n}\n\nfunction matches(find: InputRule['find'], text: string): boolean {\n if (typeof find === 'function') return find(text) != null\n find.lastIndex = 0\n return find.exec(text) !== null\n}\n\n/**\n * Hierarchical abilities (see shared/editor/abilities.ts). Resolution reads `editor.storage.abilities`; the plugin\n * swallows `Mod-`/`Alt-` shortcuts and pre-empts input rules of node/mark types whose ability is missing at the caret.\n * Paste, drop and remote changes are not filtered.\n */\nexport const Abilities = Extension.create<AbilitiesOptions, AbilitiesStorage>({\n name: 'abilities',\n // Highest priority: its plugin's handleKeyDown / handleTextInput run before the gated types' keymaps and input rules.\n priority: 1000,\n\n addOptions() {\n return { root: ROOT_ABILITIES, rules: DEFAULT_ABILITY_RULES }\n },\n\n addStorage() {\n return { rules: new Map(), root: this.options.root }\n },\n\n onBeforeCreate() {\n const rules = new Map<string, AbilityRuleSource>()\n for (const ext of this.editor.extensionManager.extensions) {\n const rule = getExtensionField<AbilityRuleSource | undefined>(ext, 'abilities')\n if (rule) rules.set(ext.name, rule)\n }\n // Host/default rules replace a node's own rule as a whole.\n for (const [name, rule] of Object.entries(this.options.rules)) rules.set(name, rule)\n this.storage.rules = rules\n this.storage.root = this.options.root\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n // Built lazily on first use, once per plugin: needs the final extension list and schema.\n let handleKey: ((view: EditorView, event: KeyboardEvent) => boolean) | undefined\n let inputRules: GatedInputRule[] | undefined\n\n return [\n new Plugin({\n key: new PluginKey('abilities'),\n props: {\n handleKeyDown(view, event) {\n handleKey ??= keydownHandler(buildKeyBindings(editor))\n return handleKey(view, event)\n },\n handleTextInput(view, from, to, text) {\n inputRules ??= buildInputRules(editor)\n if (!inputRules.length) return false\n const $from = view.state.doc.resolve(from)\n if (!$from.parent.isTextblock) return false\n const textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, undefined, '\\ufffc') + text\n let present: ReadonlySet<Ability> | undefined\n for (const rule of inputRules) {\n present ??= abilitiesAt(editor, from)\n if (present.has(rule.ability) || !matches(rule.find, textBefore)) continue\n // Type the character literally; the rule's own plugin never sees this input.\n view.dispatch(view.state.tr.insertText(text, from, to))\n return true\n }\n return false\n },\n },\n }),\n ]\n },\n})\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  //#region src/core/backend-capability.d.ts
2
2
  /** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */
3
- export declare const BACKEND_CAPABILITIES: readonly ["documents", "collaboration", "assets", "users", "tags", "settings", "themes", "ai", "comments"];
3
+ export declare const BACKEND_CAPABILITIES: readonly ["documents", "collaboration", "assets", "users", "tags", "settings", "themes", "ai", "comments", "images"];
4
4
  /** What a backend can do. */
5
5
  export type BackendCapability = typeof BACKEND_CAPABILITIES[number];
6
6
  //#endregion
@@ -9,7 +9,8 @@ const BACKEND_CAPABILITIES = [
9
9
  "settings",
10
10
  "themes",
11
11
  "ai",
12
- "comments"
12
+ "comments",
13
+ "images"
13
14
  ];
14
15
  //#endregion
15
16
  export { BACKEND_CAPABILITIES };
@@ -1 +1 @@
1
- {"version":3,"file":"backend-capability.js","names":[],"sources":["../../src/core/backend-capability.ts"],"sourcesContent":["/** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */\nexport const BACKEND_CAPABILITIES = ['documents', 'collaboration', 'assets', 'users', 'tags', 'settings', 'themes', 'ai', 'comments'] as const\n\n/** What a backend can do. */\nexport type BackendCapability = typeof BACKEND_CAPABILITIES[number]\n"],"mappings":";;AACA,MAAa,uBAAuB;CAAC;CAAa;CAAiB;CAAU;CAAS;CAAQ;CAAY;CAAU;CAAM;AAAU"}
1
+ {"version":3,"file":"backend-capability.js","names":[],"sources":["../../src/core/backend-capability.ts"],"sourcesContent":["/** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */\nexport const BACKEND_CAPABILITIES = ['documents', 'collaboration', 'assets', 'users', 'tags', 'settings', 'themes', 'ai', 'comments', 'images'] as const\n\n/** What a backend can do. */\nexport type BackendCapability = typeof BACKEND_CAPABILITIES[number]\n"],"mappings":";;AACA,MAAa,uBAAuB;CAAC;CAAa;CAAiB;CAAU;CAAS;CAAQ;CAAY;CAAU;CAAM;CAAY;AAAQ"}
@@ -241,5 +241,30 @@ export interface CommentsModel extends CommentsActions {
241
241
  refresh: () => void;
242
242
  dispose: () => void;
243
243
  }
244
+ /** A reference or a previous generation of the Bild-Studio (`images.generate` and `images.upload` answer one). `active` = goes into the next generation. */
245
+ export interface ImageStudioImage {
246
+ /** Host-opaque. */
247
+ id: string;
248
+ url: string;
249
+ title: string;
250
+ active: boolean;
251
+ }
252
+ /** A managed Prompt-Baustein (style, brand, …) of the host. */
253
+ export interface ImageStudioPromptBlock {
254
+ id: string;
255
+ label: string;
256
+ text: string;
257
+ }
258
+ /** Body of the image-generation route (`images.generate`): what the Bild-Studio sends for one chat turn. */
259
+ export interface ImageStudioGenerateBody {
260
+ /** The block's description of the image. */
261
+ description: string;
262
+ /** This turn's instruction; `''` generates from the description alone. */
263
+ instruction: string;
264
+ /** The active Prompt-Bausteine, by the host's ids. */
265
+ promptBlockIds: string[];
266
+ /** The URLs of the active references and generations that go into the generation as reference images. */
267
+ referenceUrls: string[];
268
+ }
244
269
  //#endregion
245
270
  //# sourceMappingURL=backend-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backend-types.js","names":[],"sources":["../../src/core/backend-types.ts"],"sourcesContent":["import type { ReadonlyStore } from './store'\nimport type { AuthorIdentity } from './feature'\n\n// The wire and model types that `EditorBackend` (`./backend`) names in its methods. They live in core, not in the\n// features they belong to, so the port does not depend on any feature or integration; each feature's public entry\n// (`@ecsplico/editor/mentions/schema`, `…/themes/schema`, `…/ai/protocol`, `…/comments/schema`, …) exports the names\n// it always had. Types only: DOM-free, importable from server code.\n\n/** A user as offered for `@` mentions (`GET /api/editor/users`); never carries credentials. */\nexport interface MentionUser {\n id: string\n username: string\n color: string\n}\n\n/** Profile as the client sees it; presets never expose their key. */\nexport interface ModelProfile { id: string, label: string, endpoint: string, model: string, source: 'preset' | 'user' }\n\n/** Model an AI request runs on: a server preset by id, or an OpenAI-compatible endpoint with its own key. */\nexport type ModelRef = { preset: string } | { endpoint: string, model: string, apiKey: string }\n\n/** A theme in the shared library; `builtin` themes are seeded by the host and cannot be deleted. */\nexport interface ThemeRecord {\n id: string\n name: string\n version?: string\n author?: string\n authorUrl?: string\n builtin: boolean\n}\n\n/** One transcript segment sent for cleanup. */\nexport interface TranscriptCleanupSegment {\n /** Block ID of the segment (`blockIdOf`). */\n id: string\n speaker: string\n /** The segment's inline content as Markdown (mentions as `@name`, tags as `#name`). */\n text: string\n}\n/** Body of the transcript-cleanup route. */\nexport interface TranscriptCleanupRequest { segments: TranscriptCleanupSegment[], model: ModelRef }\n/** Cleaned text (single-line inline Markdown) of the segment with block ID `id`. */\nexport interface TranscriptCleanupResult { id: string, text: string }\n/** `cleaned`: only segments whose text changed. `failed`: segments of chunks whose model answer was unusable (left as they are). */\nexport interface TranscriptCleanupResponse { cleaned: TranscriptCleanupResult[], failed: number }\n\n/** A CSL name: `family`/`given` for a person, `literal` for an institution or an unparsed name. */\nexport interface CslName { family?: string, given?: string, literal?: string }\nexport interface CslDate { 'date-parts': number[][] }\n\n/** The CSL item types the editor offers (select options, server-side schema). */\nexport const CSL_TYPES = ['article-journal', 'book', 'chapter', 'paper-conference', 'thesis', 'report', 'webpage', 'article', 'dataset'] as const\nexport type CslType = typeof CSL_TYPES[number]\n\n/** A CSL-JSON item without `id` and `title`: the Baustein's block id and title are authoritative for those and never stored in `meta.bib`. */\nexport interface CslItem {\n type: CslType\n author?: CslName[]\n editor?: CslName[]\n issued?: CslDate\n 'container-title'?: string\n volume?: string\n issue?: string\n page?: string\n publisher?: string\n 'publisher-place'?: string\n DOI?: string\n ISBN?: string\n URL?: string\n abstract?: string\n language?: string\n}\n\n/** What `ai.extract` makes of a stored asset; `auto` picks by the asset's MIME type. */\nexport type ExtractMethod = 'auto' | 'transcription' | 'pdf-text' | 'ocr' | 'description' | 'bibliography'\n/** Body of the extract route: the stored asset `documentId`/`name` and what to make of it. */\nexport interface ExtractRequest {\n documentId: string\n /** The asset to read; absent only for `bibliography` with a `doi` (the work is looked up by DOI alone). */\n name?: string\n method: ExtractMethod\n /** The model of `ocr`, `description` and model-based bibliography extraction. */\n model: ModelRef\n /** Also extract bibliographic data after `pdf-text`/`ocr`. */\n bibliography?: boolean\n /** DOI to look up for `bibliography`; absent = found in the first pages' text. */\n doi?: string\n /** PDF page cap (default 200). `ocr` reads at most 50 pages whatever this allows (one model call per page; `ocr-truncated` warns). */\n maxPages?: number\n}\nexport interface ExtractResponse {\n method: Exclude<ExtractMethod, 'auto'>\n /** The readable content as Markdown (empty for `bibliography`). */\n markdown: string\n /** Transcription only: the timed segments `markdown` was made of. */\n segments?: { start: number, end: number, text: string }[]\n /** When bibliographic data was requested or the method is `bibliography`; `title` is the work's title when found (never stored in `meta.bib`). */\n bib?: CslItem & { title?: string }\n /** Transcription only: the speech-to-text model that wrote it (`TranscriptionConfig.model`), which the client cannot know. */\n model?: string\n /** SHA-256 (hex) of the asset bytes. */\n inputHash: string\n /** SHA-256 (hex) of the stored raw output (`markdown` as the asset `output` holds it). The client does not compare it: it hashes the body it writes (its re-serialised Markdown) into `Derivation.outputHash` itself. */\n outputHash: string\n /** Asset name (same document) of the stored raw Markdown; `null` for `bibliography`. */\n output: string | null\n warnings: string[]\n}\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/** A text range in the editor document. */\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n"],"mappings":";;AAmDA,MAAa,YAAY;CAAC;CAAmB;CAAQ;CAAW;CAAoB;CAAU;CAAU;CAAW;CAAW;AAAS"}
1
+ {"version":3,"file":"backend-types.js","names":[],"sources":["../../src/core/backend-types.ts"],"sourcesContent":["import type { ReadonlyStore } from './store'\nimport type { AuthorIdentity } from './feature'\n\n// The wire and model types that `EditorBackend` (`./backend`) names in its methods. They live in core, not in the\n// features they belong to, so the port does not depend on any feature or integration; each feature's public entry\n// (`@ecsplico/editor/mentions/schema`, `…/themes/schema`, `…/ai/protocol`, `…/comments/schema`, …) exports the names\n// it always had. Types only: DOM-free, importable from server code.\n\n/** A user as offered for `@` mentions (`GET /api/editor/users`); never carries credentials. */\nexport interface MentionUser {\n id: string\n username: string\n color: string\n}\n\n/** Profile as the client sees it; presets never expose their key. */\nexport interface ModelProfile { id: string, label: string, endpoint: string, model: string, source: 'preset' | 'user' }\n\n/** Model an AI request runs on: a server preset by id, or an OpenAI-compatible endpoint with its own key. */\nexport type ModelRef = { preset: string } | { endpoint: string, model: string, apiKey: string }\n\n/** A theme in the shared library; `builtin` themes are seeded by the host and cannot be deleted. */\nexport interface ThemeRecord {\n id: string\n name: string\n version?: string\n author?: string\n authorUrl?: string\n builtin: boolean\n}\n\n/** One transcript segment sent for cleanup. */\nexport interface TranscriptCleanupSegment {\n /** Block ID of the segment (`blockIdOf`). */\n id: string\n speaker: string\n /** The segment's inline content as Markdown (mentions as `@name`, tags as `#name`). */\n text: string\n}\n/** Body of the transcript-cleanup route. */\nexport interface TranscriptCleanupRequest { segments: TranscriptCleanupSegment[], model: ModelRef }\n/** Cleaned text (single-line inline Markdown) of the segment with block ID `id`. */\nexport interface TranscriptCleanupResult { id: string, text: string }\n/** `cleaned`: only segments whose text changed. `failed`: segments of chunks whose model answer was unusable (left as they are). */\nexport interface TranscriptCleanupResponse { cleaned: TranscriptCleanupResult[], failed: number }\n\n/** A CSL name: `family`/`given` for a person, `literal` for an institution or an unparsed name. */\nexport interface CslName { family?: string, given?: string, literal?: string }\nexport interface CslDate { 'date-parts': number[][] }\n\n/** The CSL item types the editor offers (select options, server-side schema). */\nexport const CSL_TYPES = ['article-journal', 'book', 'chapter', 'paper-conference', 'thesis', 'report', 'webpage', 'article', 'dataset'] as const\nexport type CslType = typeof CSL_TYPES[number]\n\n/** A CSL-JSON item without `id` and `title`: the Baustein's block id and title are authoritative for those and never stored in `meta.bib`. */\nexport interface CslItem {\n type: CslType\n author?: CslName[]\n editor?: CslName[]\n issued?: CslDate\n 'container-title'?: string\n volume?: string\n issue?: string\n page?: string\n publisher?: string\n 'publisher-place'?: string\n DOI?: string\n ISBN?: string\n URL?: string\n abstract?: string\n language?: string\n}\n\n/** What `ai.extract` makes of a stored asset; `auto` picks by the asset's MIME type. */\nexport type ExtractMethod = 'auto' | 'transcription' | 'pdf-text' | 'ocr' | 'description' | 'bibliography'\n/** Body of the extract route: the stored asset `documentId`/`name` and what to make of it. */\nexport interface ExtractRequest {\n documentId: string\n /** The asset to read; absent only for `bibliography` with a `doi` (the work is looked up by DOI alone). */\n name?: string\n method: ExtractMethod\n /** The model of `ocr`, `description` and model-based bibliography extraction. */\n model: ModelRef\n /** Also extract bibliographic data after `pdf-text`/`ocr`. */\n bibliography?: boolean\n /** DOI to look up for `bibliography`; absent = found in the first pages' text. */\n doi?: string\n /** PDF page cap (default 200). `ocr` reads at most 50 pages whatever this allows (one model call per page; `ocr-truncated` warns). */\n maxPages?: number\n}\nexport interface ExtractResponse {\n method: Exclude<ExtractMethod, 'auto'>\n /** The readable content as Markdown (empty for `bibliography`). */\n markdown: string\n /** Transcription only: the timed segments `markdown` was made of. */\n segments?: { start: number, end: number, text: string }[]\n /** When bibliographic data was requested or the method is `bibliography`; `title` is the work's title when found (never stored in `meta.bib`). */\n bib?: CslItem & { title?: string }\n /** Transcription only: the speech-to-text model that wrote it (`TranscriptionConfig.model`), which the client cannot know. */\n model?: string\n /** SHA-256 (hex) of the asset bytes. */\n inputHash: string\n /** SHA-256 (hex) of the stored raw output (`markdown` as the asset `output` holds it). The client does not compare it: it hashes the body it writes (its re-serialised Markdown) into `Derivation.outputHash` itself. */\n outputHash: string\n /** Asset name (same document) of the stored raw Markdown; `null` for `bibliography`. */\n output: string | null\n warnings: string[]\n}\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/** A text range in the editor document. */\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A reference or a previous generation of the Bild-Studio (`images.generate` and `images.upload` answer one). `active` = goes into the next generation. */\nexport interface ImageStudioImage {\n /** Host-opaque. */\n id: string\n url: string\n title: string\n active: boolean\n}\n\n/** A managed Prompt-Baustein (style, brand, …) of the host. */\nexport interface ImageStudioPromptBlock {\n id: string\n label: string\n text: string\n}\n\n/** Body of the image-generation route (`images.generate`): what the Bild-Studio sends for one chat turn. */\nexport interface ImageStudioGenerateBody {\n /** The block's description of the image. */\n description: string\n /** This turn's instruction; `''` generates from the description alone. */\n instruction: string\n /** The active Prompt-Bausteine, by the host's ids. */\n promptBlockIds: string[]\n /** The URLs of the active references and generations that go into the generation as reference images. */\n referenceUrls: string[]\n}\n"],"mappings":";;AAmDA,MAAa,YAAY;CAAC;CAAmB;CAAQ;CAAW;CAAoB;CAAU;CAAU;CAAW;CAAW;AAAS"}
@@ -2,7 +2,7 @@ import { ModelSlot } from "../settings/values.js";
2
2
  import { EditorSettings } from "../settings/index.js";
3
3
  import { BackendCapability } from "./backend-capability.js";
4
4
  import { AuthorIdentity } from "./feature.js";
5
- import { CommentsModel, ExtractRequest, ExtractResponse, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from "./backend-types.js";
5
+ import { CommentsModel, ExtractRequest, ExtractResponse, ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from "./backend-types.js";
6
6
  import { Editor, JSONContent } from "@tiptap/core";
7
7
  import { HocuspocusProvider } from "@hocuspocus/provider";
8
8
  import * as Y from "yjs";
@@ -100,6 +100,14 @@ export interface EditorBackend {
100
100
  /** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */
101
101
  extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>;
102
102
  };
103
+ images?: {
104
+ /** The Prompt-Bausteine (style, brand, …) the host manages, in the order it lists them. */
105
+ promptBlocks: () => Promise<ImageStudioPromptBlock[]>;
106
+ /** Generates one image from the description, the instruction, the active Prompt-Bausteine and the reference images; resolves with the stored image (`active: false`). */
107
+ generate: (body: ImageStudioGenerateBody, signal?: AbortSignal) => Promise<ImageStudioImage>;
108
+ /** Stores `file` as a reference image for document `documentId`; resolves with the stored image (`active: true`). */
109
+ upload: (documentId: string, file: File) => Promise<ImageStudioImage>;
110
+ };
103
111
  /** The comments model of document `documentId`. */
104
112
  comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel;
105
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"backend.js","names":[],"sources":["../../src/core/backend.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type * as Y from 'yjs'\nimport type { EditorSettings, ModelSlot } from '../settings'\nimport type { BackendCapability } from './backend-capability'\nimport type { CommentsModel, ExtractRequest, ExtractResponse, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from './backend-types'\nimport type { AuthorIdentity } from './feature'\n\n// The client side of the data layer: everything the editor and its features ask of the host's backend (documents,\n// uploads, mention lookups, settings, themes, AI, collaboration, comments) is one method of `EditorBackend`. Two\n// adapters implement it: HTTP (`@ecsplico/editor/adapters/http`, the `/api/editor` and `/api/ai` routes the Nuxt module\n// and `installEditorServer` serve) and the host's own. A capability the backend does not offer is `undefined`.\n\n/** What a backend can do; the server's `EditorPorts` use the same names. */\nexport type { BackendCapability }\n\n/** One server-sent event of an AI stream (`delta` `{ text }`, `ops` the proposed edit operations, `done`, `error` `{ message }`). `data` is the parsed JSON payload (`{}` without one). */\nexport interface AiStreamEvent {\n event: 'delta' | 'ops' | 'done' | 'error'\n data: unknown\n}\n\n/** The token a collaboration server's `authenticate` receives: a string, or a function (also async) that returns it on each connect. */\nexport type CollaborationToken = string | (() => string) | (() => Promise<string>)\n\n/** A refused or failed backend request; `status` is the HTTP status (or its equivalent) and `message` the server's, when it sent one. */\nexport class BackendError extends Error {\n constructor(message: string, readonly status: number, options?: ErrorOptions) {\n super(message, options)\n this.name = 'BackendError'\n }\n}\n\n/** What a backend needs from the host to give a comments model its anchors and its author. */\nexport interface BackendCommentsOptions {\n /** Who writes: the author of new comments; `null` while nobody is signed in. */\n user: () => AuthorIdentity | null\n /** Whether the user may compose and change comments now. */\n canComment: () => boolean\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Called with every failed request. */\n onError?: (error: Error) => void\n}\n\nexport interface EditorBackend {\n /** Names the backend in error messages (`'http'`, `'memory'`, …). */\n readonly name: string\n /**\n * Settles once the backend knows which capabilities it really has (the HTTP adapter's `probe`, which drops what the\n * server lacks). `createEditorContext` awaits it before it reports `ready`, so a composition that prunes by capability\n * (`composeEditor(…, { backend })`) should run after it too. Never rejects; absent: usable at once.\n */\n readonly ready?: Promise<void>\n documents?: {\n /** The stored content; `null` when the document does not exist (yet). */\n load: (id: string) => Promise<JSONContent | null>\n save: (id: string, content: JSONContent) => Promise<void>\n }\n collaboration?: {\n /** Opens the live session of document `id`; `dispose()` closes the provider and the Y.Doc. */\n connect: (id: string, token?: CollaborationToken) => { ydoc: Y.Doc, provider: HocuspocusProvider, dispose: () => void }\n }\n assets?: {\n /** Stores `file` for document `documentId`; resolves with the URL it is addressed by and, when the backend hashes uploads, the SHA-256 (hex) of its bytes. */\n upload: (documentId: string, file: File) => Promise<{ url: string, sha256?: string }>\n }\n users?: {\n search: (query: string, signal?: AbortSignal) => Promise<MentionUser[]>\n }\n tags?: {\n search: (query: string, signal?: AbortSignal) => Promise<string[]>\n }\n settings?: {\n /** The account's settings; `null` when it has none stored yet. */\n load: () => Promise<EditorSettings | null>\n save: (settings: EditorSettings) => Promise<void>\n }\n themes?: {\n list: () => Promise<ThemeRecord[]>\n /** The stylesheet of theme `id` (unscoped). */\n css: (id: string) => Promise<string>\n /** Adds an Obsidian theme to the shared library: the stylesheet and, optionally, its `manifest.json`; the backend derives name and id. */\n store: (files: { css: File, manifest?: File }) => Promise<ThemeRecord>\n /** Deletes an uploaded theme; rejects for builtins and unknown ids. */\n remove: (id: string) => Promise<boolean>\n }\n ai?: {\n models: () => Promise<{ presets: ModelProfile[], slots: Record<ModelSlot, string> }>\n complete: (body: { before: string, model: ModelRef }, signal: AbortSignal) => Promise<{ text: string }>\n /** Streams the SSE events of `generate` or `chat`; throws `BackendError` (with `status`) on a non-2xx answer before the stream starts. */\n stream: (path: 'generate' | 'chat', body: object, signal: AbortSignal) => AsyncIterable<AiStreamEvent>\n /** Speaker-segment cleanup of a transcript (one JSON answer, not a stream). */\n transcriptCleanup: (body: TranscriptCleanupRequest, signal: AbortSignal) => Promise<TranscriptCleanupResponse>\n /** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */\n extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>\n }\n /** The comments model of document `documentId`. */\n comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel\n}\n"],"mappings":";;AA0BA,IAAa,eAAb,cAAkC,MAAM;CACA;CAAtC,YAAY,SAAiB,QAAyB,SAAwB;EAC5E,MAAM,SAAS,OAAO;EADc,KAAA,SAAA;EAEpC,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"backend.js","names":[],"sources":["../../src/core/backend.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type * as Y from 'yjs'\nimport type { EditorSettings, ModelSlot } from '../settings'\nimport type { BackendCapability } from './backend-capability'\nimport type { CommentsModel, ExtractRequest, ExtractResponse, ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from './backend-types'\nimport type { AuthorIdentity } from './feature'\n\n// The client side of the data layer: everything the editor and its features ask of the host's backend (documents,\n// uploads, mention lookups, settings, themes, AI, collaboration, comments) is one method of `EditorBackend`. Two\n// adapters implement it: HTTP (`@ecsplico/editor/adapters/http`, the `/api/editor` and `/api/ai` routes the Nuxt module\n// and `installEditorServer` serve) and the host's own. A capability the backend does not offer is `undefined`.\n\n/** What a backend can do; the server's `EditorPorts` use the same names. */\nexport type { BackendCapability }\n\n/** One server-sent event of an AI stream (`delta` `{ text }`, `ops` the proposed edit operations, `done`, `error` `{ message }`). `data` is the parsed JSON payload (`{}` without one). */\nexport interface AiStreamEvent {\n event: 'delta' | 'ops' | 'done' | 'error'\n data: unknown\n}\n\n/** The token a collaboration server's `authenticate` receives: a string, or a function (also async) that returns it on each connect. */\nexport type CollaborationToken = string | (() => string) | (() => Promise<string>)\n\n/** A refused or failed backend request; `status` is the HTTP status (or its equivalent) and `message` the server's, when it sent one. */\nexport class BackendError extends Error {\n constructor(message: string, readonly status: number, options?: ErrorOptions) {\n super(message, options)\n this.name = 'BackendError'\n }\n}\n\n/** What a backend needs from the host to give a comments model its anchors and its author. */\nexport interface BackendCommentsOptions {\n /** Who writes: the author of new comments; `null` while nobody is signed in. */\n user: () => AuthorIdentity | null\n /** Whether the user may compose and change comments now. */\n canComment: () => boolean\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Called with every failed request. */\n onError?: (error: Error) => void\n}\n\nexport interface EditorBackend {\n /** Names the backend in error messages (`'http'`, `'memory'`, …). */\n readonly name: string\n /**\n * Settles once the backend knows which capabilities it really has (the HTTP adapter's `probe`, which drops what the\n * server lacks). `createEditorContext` awaits it before it reports `ready`, so a composition that prunes by capability\n * (`composeEditor(…, { backend })`) should run after it too. Never rejects; absent: usable at once.\n */\n readonly ready?: Promise<void>\n documents?: {\n /** The stored content; `null` when the document does not exist (yet). */\n load: (id: string) => Promise<JSONContent | null>\n save: (id: string, content: JSONContent) => Promise<void>\n }\n collaboration?: {\n /** Opens the live session of document `id`; `dispose()` closes the provider and the Y.Doc. */\n connect: (id: string, token?: CollaborationToken) => { ydoc: Y.Doc, provider: HocuspocusProvider, dispose: () => void }\n }\n assets?: {\n /** Stores `file` for document `documentId`; resolves with the URL it is addressed by and, when the backend hashes uploads, the SHA-256 (hex) of its bytes. */\n upload: (documentId: string, file: File) => Promise<{ url: string, sha256?: string }>\n }\n users?: {\n search: (query: string, signal?: AbortSignal) => Promise<MentionUser[]>\n }\n tags?: {\n search: (query: string, signal?: AbortSignal) => Promise<string[]>\n }\n settings?: {\n /** The account's settings; `null` when it has none stored yet. */\n load: () => Promise<EditorSettings | null>\n save: (settings: EditorSettings) => Promise<void>\n }\n themes?: {\n list: () => Promise<ThemeRecord[]>\n /** The stylesheet of theme `id` (unscoped). */\n css: (id: string) => Promise<string>\n /** Adds an Obsidian theme to the shared library: the stylesheet and, optionally, its `manifest.json`; the backend derives name and id. */\n store: (files: { css: File, manifest?: File }) => Promise<ThemeRecord>\n /** Deletes an uploaded theme; rejects for builtins and unknown ids. */\n remove: (id: string) => Promise<boolean>\n }\n ai?: {\n models: () => Promise<{ presets: ModelProfile[], slots: Record<ModelSlot, string> }>\n complete: (body: { before: string, model: ModelRef }, signal: AbortSignal) => Promise<{ text: string }>\n /** Streams the SSE events of `generate` or `chat`; throws `BackendError` (with `status`) on a non-2xx answer before the stream starts. */\n stream: (path: 'generate' | 'chat', body: object, signal: AbortSignal) => AsyncIterable<AiStreamEvent>\n /** Speaker-segment cleanup of a transcript (one JSON answer, not a stream). */\n transcriptCleanup: (body: TranscriptCleanupRequest, signal: AbortSignal) => Promise<TranscriptCleanupResponse>\n /** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */\n extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>\n }\n images?: {\n /** The Prompt-Bausteine (style, brand, …) the host manages, in the order it lists them. */\n promptBlocks: () => Promise<ImageStudioPromptBlock[]>\n /** Generates one image from the description, the instruction, the active Prompt-Bausteine and the reference images; resolves with the stored image (`active: false`). */\n generate: (body: ImageStudioGenerateBody, signal?: AbortSignal) => Promise<ImageStudioImage>\n /** Stores `file` as a reference image for document `documentId`; resolves with the stored image (`active: true`). */\n upload: (documentId: string, file: File) => Promise<ImageStudioImage>\n }\n /** The comments model of document `documentId`. */\n comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel\n}\n"],"mappings":";;AA0BA,IAAa,eAAb,cAAkC,MAAM;CACA;CAAtC,YAAY,SAAiB,QAAyB,SAAwB;EAC5E,MAAM,SAAS,OAAO;EADc,KAAA,SAAA;EAEpC,KAAK,OAAO;CACd;AACF"}
@@ -186,6 +186,23 @@ function createFetchBackend(options = {}) {
186
186
  body,
187
187
  signal
188
188
  })
189
+ },
190
+ images: {
191
+ promptBlocks: () => json(editor("/images/prompt-blocks")),
192
+ generate: (body, signal) => json(editor("/images/generate"), {
193
+ method: "POST",
194
+ body,
195
+ signal
196
+ }),
197
+ upload(documentId, file) {
198
+ const body = new FormData();
199
+ body.append("documentId", documentId);
200
+ body.append("file", file);
201
+ return json(editor("/images/upload"), {
202
+ method: "POST",
203
+ body
204
+ });
205
+ }
189
206
  }
190
207
  };
191
208
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http-backend.js","names":[],"sources":["../../src/core/http-backend.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { $Fetch, FetchOptions } from 'ofetch'\nimport type { EditorSettings } from '../settings'\nimport { AI_API, documentUrl, EDITOR_API } from './api-routes'\nimport { type AiStreamEvent, BackendError, type EditorBackend } from './backend'\nimport type { ExtractResponse, MentionUser } from './backend-types'\n\n// The request-response half of the HTTP backend: documents, assets, users, tags, settings, themes and AI over the\n// `/api/editor` and `/api/ai` routes. `createHttpBackend` (`@ecsplico/editor/adapters/http`) adds the two capabilities that\n// need Yjs and the comments client; `createEditorContext` defaults to this half so no editor bundle carries those.\n// Requests go out through the platform `fetch`; a `$Fetch` (ofetch, Nuxt's `$fetch`) is used only when the host hands one\n// in, so no bundle pulls `ofetch` in unasked (the type import above is erased).\n\nexport interface FetchBackendOptions {\n /** HTTP client (default: the platform `fetch`); Nuxt hosts pass `useNuxtApp().$fetch` (it resolves relative paths while rendering on the server). */\n fetch?: $Fetch\n /** Prefix of every path (`https://api.example.com`); default `''` (same origin). */\n base?: string\n}\n\n/** One request of the transport: `body` is sent as it is when it is a string or `FormData`, else JSON-serialised; `query` becomes the query string. */\nexport interface HttpRequest {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n query?: Record<string, string>\n signal?: AbortSignal | undefined\n}\n\nexport interface HttpTransport {\n /** Sends the request and resolves with the response whatever its status; rejects only on a network failure or abort. */\n send: (url: string, request?: HttpRequest) => Promise<Response>\n /** `response` itself when it is 2xx; otherwise rejects with a `BackendError` (the server's `statusMessage`/`message`, else `HTTP <status>`). */\n check: (response: Response) => Promise<Response>\n /** `send` then `check`. */\n request: (url: string, request?: HttpRequest) => Promise<Response>\n}\n\nfunction platformSend(url: string, { method, headers: extra, body, query, signal }: HttpRequest = {}): Promise<Response> {\n const headers = new Headers(extra)\n let payload: BodyInit | undefined\n if (typeof body === 'string' || (typeof FormData !== 'undefined' && body instanceof FormData)) payload = body\n else if (body !== undefined) {\n headers.set('content-type', 'application/json')\n payload = JSON.stringify(body)\n }\n const target = query ? `${url}?${new URLSearchParams(query)}` : url\n return globalThis.fetch(target, { ...(method && { method }), headers, ...(payload !== undefined && { body: payload }), ...(signal && { signal }) })\n}\n\n/** The request through `fetcher.raw`: the response comes back unread (`stream`), a non-2xx one is a result, not an exception. */\nfunction fetcherSend(fetcher: $Fetch) {\n return (url: string, { method, headers, body, query, signal }: HttpRequest = {}): Promise<Response> => {\n const options: FetchOptions<'stream'> = { responseType: 'stream', ignoreResponseError: true }\n if (method) options.method = method\n if (headers) options.headers = headers\n if (body !== undefined) options.body = body as Record<string, unknown>\n if (query) options.query = query\n if (signal) options.signal = signal\n return fetcher.raw(url, options)\n }\n}\n\n/** The transport of `fetch` (default: the platform `fetch`). */\nexport function createTransport(fetcher?: $Fetch): HttpTransport {\n const send = fetcher ? fetcherSend(fetcher) : platformSend\n async function check(response: Response) {\n if (response.ok) return response\n let message = `HTTP ${response.status}`\n try {\n const json = await response.json() as { statusMessage?: string, message?: string }\n message = json.statusMessage || json.message || message\n }\n catch { /* non-JSON error body */ }\n throw new BackendError(message, response.status)\n }\n return { send, check, request: async (url, request) => check(await send(url, request)) }\n}\n\n/** Drops a response whose body nobody reads, so the connection is released. */\nconst discard = (response: Response) => response.body?.cancel().catch(() => undefined)\n\n/** The event of an SSE block (`event: x\\ndata: {json}`); `data` is `{}` without a payload or with a malformed one. */\nfunction streamEvent(raw: string): AiStreamEvent {\n let event = 'message'\n let data = ''\n for (const line of raw.split('\\n')) {\n if (line.startsWith('event:')) event = line.slice(6).trim()\n else if (line.startsWith('data:')) data += line.slice(5).trimStart()\n }\n let payload: unknown = {}\n if (data) {\n try {\n payload = JSON.parse(data)\n }\n catch {\n payload = {}\n }\n }\n return { event: event as AiStreamEvent['event'], data: payload }\n}\n\n/** The editor's HTTP routes as an `EditorBackend` without `collaboration` and `comments`. */\nexport function createFetchBackend(options: FetchBackendOptions = {}): EditorBackend {\n const base = options.base ?? ''\n const { request, send, check } = createTransport(options.fetch)\n const editor = (path: string) => `${base}${EDITOR_API}${path}`\n const ai = (path: string) => `${base}${AI_API}${path}`\n /** The JSON answer of a request that must succeed. */\n const json = async <T>(url: string, init?: HttpRequest) => (await request(url, init)).json() as Promise<T>\n /** A request that must succeed and whose answer is of no interest. */\n const sent = async (url: string, init?: HttpRequest) => { await discard(await request(url, init)) }\n\n return {\n name: 'http',\n documents: {\n async load(id) {\n const response = await send(`${base}${documentUrl(id)}`)\n if (response.status === 404) {\n await discard(response)\n return null\n }\n return (await check(response)).json() as Promise<JSONContent>\n },\n save: (id, content) => sent(`${base}${documentUrl(id)}`, { method: 'PUT', body: content }),\n },\n assets: {\n upload(documentId, file) {\n const body = new FormData()\n body.append('file', file)\n return json<{ url: string, sha256?: string }>(editor(`/assets/${documentId}`), { method: 'POST', body })\n },\n },\n users: {\n search: (query, signal) => json<MentionUser[]>(editor('/users'), { query: { q: query }, signal }),\n },\n tags: {\n search: (query, signal) => json<string[]>(editor('/tags'), { query: { q: query }, signal }),\n },\n settings: {\n async load() {\n const response = await request(editor('/settings'))\n // 204: the host keeps no settings for this account.\n return response.status === 200 ? await response.json() as EditorSettings | null : null\n },\n save: settings => sent(editor('/settings'), { method: 'PUT', body: settings }),\n },\n themes: {\n list: () => json(editor('/themes')),\n css: async id => (await request(editor(`/themes/${encodeURIComponent(id)}`))).text(),\n store({ css, manifest }) {\n const body = new FormData()\n body.append('css', css)\n if (manifest) body.append('manifest', manifest)\n return json(editor('/themes'), { method: 'POST', body })\n },\n async remove(id) {\n await sent(editor(`/themes/${encodeURIComponent(id)}`), { method: 'DELETE' })\n return true\n },\n },\n ai: {\n models: () => json(ai('/models')),\n complete: (body, signal) => json<{ text: string }>(ai('/complete'), { method: 'POST', body, signal }),\n async* stream(path, body, signal) {\n const response = await request(ai(`/${path}`), { method: 'POST', body, signal })\n if (!response.body) throw new BackendError(`HTTP ${response.status}`, response.status)\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n')\n let index = buffer.indexOf('\\n\\n')\n while (index !== -1) {\n const event = streamEvent(buffer.slice(0, index))\n yield event\n if (event.event === 'done' || event.event === 'error') return\n buffer = buffer.slice(index + 2)\n index = buffer.indexOf('\\n\\n')\n }\n }\n if (buffer.trim()) yield streamEvent(buffer)\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n },\n transcriptCleanup: (body, signal) => json(ai('/transcript-cleanup'), { method: 'POST', body, signal }),\n extract: (body, signal) => json<ExtractResponse>(ai('/extract'), { method: 'POST', body, signal }),\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http-backend.js","names":[],"sources":["../../src/core/http-backend.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { $Fetch, FetchOptions } from 'ofetch'\nimport type { EditorSettings } from '../settings'\nimport { AI_API, documentUrl, EDITOR_API } from './api-routes'\nimport { type AiStreamEvent, BackendError, type EditorBackend } from './backend'\nimport type { ExtractResponse, MentionUser } from './backend-types'\n\n// The request-response half of the HTTP backend: documents, assets, users, tags, settings, themes, AI and images over the\n// `/api/editor` and `/api/ai` routes. `createHttpBackend` (`@ecsplico/editor/adapters/http`) adds the two capabilities that\n// need Yjs and the comments client; `createEditorContext` defaults to this half so no editor bundle carries those.\n// Requests go out through the platform `fetch`; a `$Fetch` (ofetch, Nuxt's `$fetch`) is used only when the host hands one\n// in, so no bundle pulls `ofetch` in unasked (the type import above is erased).\n\nexport interface FetchBackendOptions {\n /** HTTP client (default: the platform `fetch`); Nuxt hosts pass `useNuxtApp().$fetch` (it resolves relative paths while rendering on the server). */\n fetch?: $Fetch\n /** Prefix of every path (`https://api.example.com`); default `''` (same origin). */\n base?: string\n}\n\n/** One request of the transport: `body` is sent as it is when it is a string or `FormData`, else JSON-serialised; `query` becomes the query string. */\nexport interface HttpRequest {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n query?: Record<string, string>\n signal?: AbortSignal | undefined\n}\n\nexport interface HttpTransport {\n /** Sends the request and resolves with the response whatever its status; rejects only on a network failure or abort. */\n send: (url: string, request?: HttpRequest) => Promise<Response>\n /** `response` itself when it is 2xx; otherwise rejects with a `BackendError` (the server's `statusMessage`/`message`, else `HTTP <status>`). */\n check: (response: Response) => Promise<Response>\n /** `send` then `check`. */\n request: (url: string, request?: HttpRequest) => Promise<Response>\n}\n\nfunction platformSend(url: string, { method, headers: extra, body, query, signal }: HttpRequest = {}): Promise<Response> {\n const headers = new Headers(extra)\n let payload: BodyInit | undefined\n if (typeof body === 'string' || (typeof FormData !== 'undefined' && body instanceof FormData)) payload = body\n else if (body !== undefined) {\n headers.set('content-type', 'application/json')\n payload = JSON.stringify(body)\n }\n const target = query ? `${url}?${new URLSearchParams(query)}` : url\n return globalThis.fetch(target, { ...(method && { method }), headers, ...(payload !== undefined && { body: payload }), ...(signal && { signal }) })\n}\n\n/** The request through `fetcher.raw`: the response comes back unread (`stream`), a non-2xx one is a result, not an exception. */\nfunction fetcherSend(fetcher: $Fetch) {\n return (url: string, { method, headers, body, query, signal }: HttpRequest = {}): Promise<Response> => {\n const options: FetchOptions<'stream'> = { responseType: 'stream', ignoreResponseError: true }\n if (method) options.method = method\n if (headers) options.headers = headers\n if (body !== undefined) options.body = body as Record<string, unknown>\n if (query) options.query = query\n if (signal) options.signal = signal\n return fetcher.raw(url, options)\n }\n}\n\n/** The transport of `fetch` (default: the platform `fetch`). */\nexport function createTransport(fetcher?: $Fetch): HttpTransport {\n const send = fetcher ? fetcherSend(fetcher) : platformSend\n async function check(response: Response) {\n if (response.ok) return response\n let message = `HTTP ${response.status}`\n try {\n const json = await response.json() as { statusMessage?: string, message?: string }\n message = json.statusMessage || json.message || message\n }\n catch { /* non-JSON error body */ }\n throw new BackendError(message, response.status)\n }\n return { send, check, request: async (url, request) => check(await send(url, request)) }\n}\n\n/** Drops a response whose body nobody reads, so the connection is released. */\nconst discard = (response: Response) => response.body?.cancel().catch(() => undefined)\n\n/** The event of an SSE block (`event: x\\ndata: {json}`); `data` is `{}` without a payload or with a malformed one. */\nfunction streamEvent(raw: string): AiStreamEvent {\n let event = 'message'\n let data = ''\n for (const line of raw.split('\\n')) {\n if (line.startsWith('event:')) event = line.slice(6).trim()\n else if (line.startsWith('data:')) data += line.slice(5).trimStart()\n }\n let payload: unknown = {}\n if (data) {\n try {\n payload = JSON.parse(data)\n }\n catch {\n payload = {}\n }\n }\n return { event: event as AiStreamEvent['event'], data: payload }\n}\n\n/** The editor's HTTP routes as an `EditorBackend` without `collaboration` and `comments`. */\nexport function createFetchBackend(options: FetchBackendOptions = {}): EditorBackend {\n const base = options.base ?? ''\n const { request, send, check } = createTransport(options.fetch)\n const editor = (path: string) => `${base}${EDITOR_API}${path}`\n const ai = (path: string) => `${base}${AI_API}${path}`\n /** The JSON answer of a request that must succeed. */\n const json = async <T>(url: string, init?: HttpRequest) => (await request(url, init)).json() as Promise<T>\n /** A request that must succeed and whose answer is of no interest. */\n const sent = async (url: string, init?: HttpRequest) => { await discard(await request(url, init)) }\n\n return {\n name: 'http',\n documents: {\n async load(id) {\n const response = await send(`${base}${documentUrl(id)}`)\n if (response.status === 404) {\n await discard(response)\n return null\n }\n return (await check(response)).json() as Promise<JSONContent>\n },\n save: (id, content) => sent(`${base}${documentUrl(id)}`, { method: 'PUT', body: content }),\n },\n assets: {\n upload(documentId, file) {\n const body = new FormData()\n body.append('file', file)\n return json<{ url: string, sha256?: string }>(editor(`/assets/${documentId}`), { method: 'POST', body })\n },\n },\n users: {\n search: (query, signal) => json<MentionUser[]>(editor('/users'), { query: { q: query }, signal }),\n },\n tags: {\n search: (query, signal) => json<string[]>(editor('/tags'), { query: { q: query }, signal }),\n },\n settings: {\n async load() {\n const response = await request(editor('/settings'))\n // 204: the host keeps no settings for this account.\n return response.status === 200 ? await response.json() as EditorSettings | null : null\n },\n save: settings => sent(editor('/settings'), { method: 'PUT', body: settings }),\n },\n themes: {\n list: () => json(editor('/themes')),\n css: async id => (await request(editor(`/themes/${encodeURIComponent(id)}`))).text(),\n store({ css, manifest }) {\n const body = new FormData()\n body.append('css', css)\n if (manifest) body.append('manifest', manifest)\n return json(editor('/themes'), { method: 'POST', body })\n },\n async remove(id) {\n await sent(editor(`/themes/${encodeURIComponent(id)}`), { method: 'DELETE' })\n return true\n },\n },\n ai: {\n models: () => json(ai('/models')),\n complete: (body, signal) => json<{ text: string }>(ai('/complete'), { method: 'POST', body, signal }),\n async* stream(path, body, signal) {\n const response = await request(ai(`/${path}`), { method: 'POST', body, signal })\n if (!response.body) throw new BackendError(`HTTP ${response.status}`, response.status)\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n')\n let index = buffer.indexOf('\\n\\n')\n while (index !== -1) {\n const event = streamEvent(buffer.slice(0, index))\n yield event\n if (event.event === 'done' || event.event === 'error') return\n buffer = buffer.slice(index + 2)\n index = buffer.indexOf('\\n\\n')\n }\n }\n if (buffer.trim()) yield streamEvent(buffer)\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n },\n transcriptCleanup: (body, signal) => json(ai('/transcript-cleanup'), { method: 'POST', body, signal }),\n extract: (body, signal) => json<ExtractResponse>(ai('/extract'), { method: 'POST', body, signal }),\n },\n images: {\n promptBlocks: () => json(editor('/images/prompt-blocks')),\n generate: (body, signal) => json(editor('/images/generate'), { method: 'POST', body, signal }),\n upload(documentId, file) {\n const body = new FormData()\n body.append('documentId', documentId)\n body.append('file', file)\n return json(editor('/images/upload'), { method: 'POST', body })\n },\n },\n }\n}\n"],"mappings":"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"}
@@ -12,6 +12,7 @@ export declare const ABILITY: {
12
12
  readonly composite: "format.composite";
13
13
  readonly transcript: "transcript.segment";
14
14
  readonly media: "insert.media";
15
+ readonly imageStudio: "insert.imageStudio";
15
16
  readonly record: "insert.record";
16
17
  readonly mention: "insert.mention";
17
18
  readonly reference: "insert.reference";
@@ -11,6 +11,7 @@ const ABILITY = {
11
11
  composite: "format.composite",
12
12
  transcript: "transcript.segment",
13
13
  media: "insert.media",
14
+ imageStudio: "insert.imageStudio",
14
15
  record: "insert.record",
15
16
  mention: "insert.mention",
16
17
  reference: "insert.reference",
@@ -38,6 +39,7 @@ const ROOT_ABILITIES = [
38
39
  ABILITY.composite,
39
40
  ABILITY.transcript,
40
41
  ABILITY.media,
42
+ ABILITY.imageStudio,
41
43
  ABILITY.record,
42
44
  ABILITY.mention,
43
45
  ABILITY.reference,
@@ -61,6 +63,7 @@ const TEXT_ABILITIES = [
61
63
  ABILITY.composite,
62
64
  ABILITY.transcript,
63
65
  ABILITY.media,
66
+ ABILITY.imageStudio,
64
67
  ABILITY.record,
65
68
  ABILITY.mention,
66
69
  ABILITY.reference,
@@ -90,6 +93,7 @@ const TYPE_ABILITIES = {
90
93
  audio: ABILITY.media,
91
94
  video: ABILITY.media,
92
95
  file: ABILITY.media,
96
+ imageStudio: ABILITY.imageStudio,
93
97
  mention: ABILITY.mention,
94
98
  reference: ABILITY.reference,
95
99
  tag: ABILITY.tag
@@ -1 +1 @@
1
- {"version":3,"file":"abilities.js","names":[],"sources":["../../../src/core/schema/abilities.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\n\n// Vue-free and server-importable: node schemas shared with the server declare their rule via `NodeConfig.abilities`.\n\n/** Ability ids. Hosts may add their own strings; these are the ones the editor itself checks. */\nexport const ABILITY = {\n marks: 'format.marks',\n headings: 'format.headings',\n lists: 'format.lists',\n blockquote: 'format.blockquote',\n codeBlock: 'format.codeBlock',\n rule: 'format.rule',\n blockContent: 'block.content',\n composite: 'format.composite',\n transcript: 'transcript.segment',\n media: 'insert.media',\n record: 'insert.record',\n mention: 'insert.mention',\n reference: 'insert.reference',\n tag: 'insert.tag',\n aiRewrite: 'ai.rewrite',\n aiGenerate: 'ai.generate',\n aiAutocomplete: 'ai.autocomplete',\n blockDelete: 'block.delete',\n blockDuplicate: 'block.duplicate',\n blockConvert: 'block.convert',\n compositeMeta: 'composite.meta',\n compositeSummarize: 'composite.summarize',\n compositeExport: 'composite.export',\n compositeDerive: 'composite.derive',\n} as const\n\nexport type Ability = (typeof ABILITY)[keyof typeof ABILITY] | (string & {})\n\n/** How a node changes the abilities of everything inside it: `grants`, then `revokes`; `own` only applies when it is the innermost node. */\nexport interface AbilityRule {\n grants?: readonly Ability[]\n revokes?: readonly Ability[]\n own?: readonly Ability[]\n}\n\nexport type AbilityRuleSource = AbilityRule | ((node: PMNode) => AbilityRule)\n\n/** Everything a plain paragraph at document root can do. `composite.*` are `own` abilities of composites only. */\nexport const ROOT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete, ABILITY.blockDelete, ABILITY.blockDuplicate, ABILITY.blockConvert,\n]\n\n/** Text-editing abilities an atom block (image/audio/video) or a tags row can never have. */\nexport const TEXT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete,\n]\n\n/** Node/mark type → ability its creation/toggling needs (toolbar fallback, shortcut + input-rule gate). */\nexport const TYPE_ABILITIES: Readonly<Record<string, Ability>> = {\n bold: ABILITY.marks,\n italic: ABILITY.marks,\n strike: ABILITY.marks,\n code: ABILITY.marks,\n heading: ABILITY.headings,\n bulletList: ABILITY.lists,\n orderedList: ABILITY.lists,\n listItem: ABILITY.lists,\n taskList: ABILITY.lists,\n taskItem: ABILITY.lists,\n blockquote: ABILITY.blockquote,\n codeBlock: ABILITY.codeBlock,\n horizontalRule: ABILITY.rule,\n composite: ABILITY.composite,\n transcriptSegment: ABILITY.transcript,\n image: ABILITY.media,\n audio: ABILITY.media,\n video: ABILITY.media,\n file: ABILITY.media,\n mention: ABILITY.mention,\n reference: ABILITY.reference,\n tag: ABILITY.tag,\n}\n\n/**\n * Abilities at the end of `chain` (ancestors from depth 1 down to and including the innermost node):\n * start with `root`; per node add its `grants`, remove its `revokes`; finally add the innermost node's `own`.\n */\nexport function resolveAbilities(rules: ReadonlyMap<string, AbilityRuleSource>, root: readonly Ability[], chain: readonly PMNode[]): ReadonlySet<Ability> {\n const set = new Set<Ability>(root)\n let own: readonly Ability[] | undefined\n for (const node of chain) {\n const source = rules.get(node.type.name)\n const rule = typeof source === 'function' ? source(node) : source\n for (const ability of rule?.grants ?? []) set.add(ability)\n for (const ability of rule?.revokes ?? []) set.delete(ability)\n own = rule?.own\n }\n for (const ability of own ?? []) set.add(ability)\n return set\n}\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /** Abilities this node grants/revokes for its content (`own`: only when it is the innermost node). Host rules override it. */\n abilities?: AbilityRuleSource\n }\n}\n"],"mappings":";;AAKA,MAAa,UAAU;CACrB,OAAO;CACP,UAAU;CACV,OAAO;CACP,YAAY;CACZ,WAAW;CACX,MAAM;CACN,cAAc;CACd,WAAW;CACX,YAAY;CACZ,OAAO;CACP,QAAQ;CACR,SAAS;CACT,WAAW;CACX,KAAK;CACL,WAAW;CACX,YAAY;CACZ,gBAAgB;CAChB,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;AACnB;;AAcA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CAC/H,QAAQ;CAAgB,QAAQ;CAAa,QAAQ;CAAgB,QAAQ;AAC/E;;AAGA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CAC/H,QAAQ;AACV;;AAGA,MAAa,iBAAoD;CAC/D,MAAM,QAAQ;CACd,QAAQ,QAAQ;CAChB,QAAQ,QAAQ;CAChB,MAAM,QAAQ;CACd,SAAS,QAAQ;CACjB,YAAY,QAAQ;CACpB,aAAa,QAAQ;CACrB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,YAAY,QAAQ;CACpB,WAAW,QAAQ;CACnB,gBAAgB,QAAQ;CACxB,WAAW,QAAQ;CACnB,mBAAmB,QAAQ;CAC3B,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,MAAM,QAAQ;CACd,SAAS,QAAQ;CACjB,WAAW,QAAQ;CACnB,KAAK,QAAQ;AACf;;;;;AAMA,SAAgB,iBAAiB,OAA+C,MAA0B,OAAgD;CACxJ,MAAM,MAAM,IAAI,IAAa,IAAI;CACjC,IAAI;CACJ,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI;EACvC,MAAM,OAAO,OAAO,WAAW,aAAa,OAAO,IAAI,IAAI;EAC3D,KAAK,MAAM,WAAW,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO;EACzD,KAAK,MAAM,WAAW,MAAM,WAAW,CAAC,GAAG,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM;CACd;CACA,KAAK,MAAM,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO;CAChD,OAAO;AACT"}
1
+ {"version":3,"file":"abilities.js","names":[],"sources":["../../../src/core/schema/abilities.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\n\n// Vue-free and server-importable: node schemas shared with the server declare their rule via `NodeConfig.abilities`.\n\n/** Ability ids. Hosts may add their own strings; these are the ones the editor itself checks. */\nexport const ABILITY = {\n marks: 'format.marks',\n headings: 'format.headings',\n lists: 'format.lists',\n blockquote: 'format.blockquote',\n codeBlock: 'format.codeBlock',\n rule: 'format.rule',\n blockContent: 'block.content',\n composite: 'format.composite',\n transcript: 'transcript.segment',\n media: 'insert.media',\n imageStudio: 'insert.imageStudio',\n record: 'insert.record',\n mention: 'insert.mention',\n reference: 'insert.reference',\n tag: 'insert.tag',\n aiRewrite: 'ai.rewrite',\n aiGenerate: 'ai.generate',\n aiAutocomplete: 'ai.autocomplete',\n blockDelete: 'block.delete',\n blockDuplicate: 'block.duplicate',\n blockConvert: 'block.convert',\n compositeMeta: 'composite.meta',\n compositeSummarize: 'composite.summarize',\n compositeExport: 'composite.export',\n compositeDerive: 'composite.derive',\n} as const\n\nexport type Ability = (typeof ABILITY)[keyof typeof ABILITY] | (string & {})\n\n/** How a node changes the abilities of everything inside it: `grants`, then `revokes`; `own` only applies when it is the innermost node. */\nexport interface AbilityRule {\n grants?: readonly Ability[]\n revokes?: readonly Ability[]\n own?: readonly Ability[]\n}\n\nexport type AbilityRuleSource = AbilityRule | ((node: PMNode) => AbilityRule)\n\n/** Everything a plain paragraph at document root can do. `composite.*` are `own` abilities of composites only. */\nexport const ROOT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete, ABILITY.blockDelete, ABILITY.blockDuplicate, ABILITY.blockConvert,\n]\n\n/** Text-editing abilities an atom block (image/audio/video) or a tags row can never have. */\nexport const TEXT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete,\n]\n\n/** Node/mark type → ability its creation/toggling needs (toolbar fallback, shortcut + input-rule gate). */\nexport const TYPE_ABILITIES: Readonly<Record<string, Ability>> = {\n bold: ABILITY.marks,\n italic: ABILITY.marks,\n strike: ABILITY.marks,\n code: ABILITY.marks,\n heading: ABILITY.headings,\n bulletList: ABILITY.lists,\n orderedList: ABILITY.lists,\n listItem: ABILITY.lists,\n taskList: ABILITY.lists,\n taskItem: ABILITY.lists,\n blockquote: ABILITY.blockquote,\n codeBlock: ABILITY.codeBlock,\n horizontalRule: ABILITY.rule,\n composite: ABILITY.composite,\n transcriptSegment: ABILITY.transcript,\n image: ABILITY.media,\n audio: ABILITY.media,\n video: ABILITY.media,\n file: ABILITY.media,\n imageStudio: ABILITY.imageStudio,\n mention: ABILITY.mention,\n reference: ABILITY.reference,\n tag: ABILITY.tag,\n}\n\n/**\n * Abilities at the end of `chain` (ancestors from depth 1 down to and including the innermost node):\n * start with `root`; per node add its `grants`, remove its `revokes`; finally add the innermost node's `own`.\n */\nexport function resolveAbilities(rules: ReadonlyMap<string, AbilityRuleSource>, root: readonly Ability[], chain: readonly PMNode[]): ReadonlySet<Ability> {\n const set = new Set<Ability>(root)\n let own: readonly Ability[] | undefined\n for (const node of chain) {\n const source = rules.get(node.type.name)\n const rule = typeof source === 'function' ? source(node) : source\n for (const ability of rule?.grants ?? []) set.add(ability)\n for (const ability of rule?.revokes ?? []) set.delete(ability)\n own = rule?.own\n }\n for (const ability of own ?? []) set.add(ability)\n return set\n}\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /** Abilities this node grants/revokes for its content (`own`: only when it is the innermost node). Host rules override it. */\n abilities?: AbilityRuleSource\n }\n}\n"],"mappings":";;AAKA,MAAa,UAAU;CACrB,OAAO;CACP,UAAU;CACV,OAAO;CACP,YAAY;CACZ,WAAW;CACX,MAAM;CACN,cAAc;CACd,WAAW;CACX,YAAY;CACZ,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;CACT,WAAW;CACX,KAAK;CACL,WAAW;CACX,YAAY;CACZ,gBAAgB;CAChB,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;AACnB;;AAcA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAa,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CACpJ,QAAQ;CAAgB,QAAQ;CAAa,QAAQ;CAAgB,QAAQ;AAC/E;;AAGA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAa,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CACpJ,QAAQ;AACV;;AAGA,MAAa,iBAAoD;CAC/D,MAAM,QAAQ;CACd,QAAQ,QAAQ;CAChB,QAAQ,QAAQ;CAChB,MAAM,QAAQ;CACd,SAAS,QAAQ;CACjB,YAAY,QAAQ;CACpB,aAAa,QAAQ;CACrB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,YAAY,QAAQ;CACpB,WAAW,QAAQ;CACnB,gBAAgB,QAAQ;CACxB,WAAW,QAAQ;CACnB,mBAAmB,QAAQ;CAC3B,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,MAAM,QAAQ;CACd,aAAa,QAAQ;CACrB,SAAS,QAAQ;CACjB,WAAW,QAAQ;CACnB,KAAK,QAAQ;AACf;;;;;AAMA,SAAgB,iBAAiB,OAA+C,MAA0B,OAAgD;CACxJ,MAAM,MAAM,IAAI,IAAa,IAAI;CACjC,IAAI;CACJ,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI;EACvC,MAAM,OAAO,OAAO,WAAW,aAAa,OAAO,IAAI,IAAI;EAC3D,KAAK,MAAM,WAAW,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO;EACzD,KAAK,MAAM,WAAW,MAAM,WAAW,CAAC,GAAG,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM;CACd;CACA,KAAK,MAAM,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO;CAChD,OAAO;AACT"}
@@ -32,17 +32,17 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
32
32
  binary?: (props: typeof __VLS_26) => any;
33
33
  };
34
34
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
35
+ ready: (target: DocumentSurfaceTarget) => any;
35
36
  "update:modelValue": (text: string) => any;
36
37
  save: (text: string) => any;
37
- ready: (target: DocumentSurfaceTarget) => any;
38
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
+ onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
39
40
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
40
41
  onSave?: ((text: string) => any) | undefined;
41
- onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
42
42
  }>, {
43
43
  editable: boolean;
44
- wordWrap: boolean;
45
44
  modelValue: string;
45
+ wordWrap: boolean;
46
46
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
47
47
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
48
48
  declare const _default: typeof __VLS_export;
@@ -188,6 +188,7 @@ const CHROME_REVOKES = [
188
188
  ABILITY.composite,
189
189
  ABILITY.transcript,
190
190
  ABILITY.media,
191
+ ABILITY.imageStudio,
191
192
  ABILITY.record,
192
193
  ABILITY.aiGenerate,
193
194
  ABILITY.blockDuplicate