@ecsplico/editor 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/dist/code/CodeSurface.vue.d.ts +2 -2
  3. package/dist/document/DocumentSurface.vue.d.ts +2 -2
  4. package/dist/features/ai/protocol/generate.d.ts +1 -1
  5. package/dist/features/composites/kinds.d.ts +6 -0
  6. package/dist/features/composites/kinds.js.map +1 -1
  7. package/dist/features/composites/merge.d.ts +4 -3
  8. package/dist/features/composites/merge.js +29 -3
  9. package/dist/features/composites/merge.js.map +1 -1
  10. package/dist/features/image-studio/node-core.d.ts +46 -0
  11. package/dist/features/image-studio/node-core.js +79 -0
  12. package/dist/features/image-studio/node-core.js.map +1 -0
  13. package/dist/features/image-studio/node-store.d.ts +3 -11
  14. package/dist/features/image-studio/node-store.js +15 -44
  15. package/dist/features/image-studio/node-store.js.map +1 -1
  16. package/dist/features/image-studio/react/ImageStudioChat.js +101 -0
  17. package/dist/features/image-studio/react/ImageStudioChat.js.map +1 -0
  18. package/dist/features/image-studio/react/ImageStudioDialog.d.ts +19 -0
  19. package/dist/features/image-studio/react/ImageStudioDialog.js +357 -0
  20. package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -0
  21. package/dist/features/image-studio/react/ImageStudioTile.js +51 -0
  22. package/dist/features/image-studio/react/ImageStudioTile.js.map +1 -0
  23. package/dist/features/image-studio/react/ImageStudioView.js +126 -0
  24. package/dist/features/image-studio/react/ImageStudioView.js.map +1 -0
  25. package/dist/features/image-studio/react/store.d.ts +37 -0
  26. package/dist/features/image-studio/react/store.js +46 -0
  27. package/dist/features/image-studio/react/store.js.map +1 -0
  28. package/dist/features/image-studio/react.d.ts +18 -0
  29. package/dist/features/image-studio/react.js +21 -0
  30. package/dist/features/image-studio/react.js.map +1 -0
  31. package/dist/markdown/MarkdownSurface.vue.d.ts +6 -6
  32. package/dist/server/handlers/ai.d.ts +2 -2
  33. package/dist/vue/AdvancedEditor.vue.d.ts +5 -5
  34. package/dist/vue/SimpleEditor.vue.d.ts +4 -4
  35. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  36. package/package.json +6 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.6.0 - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - `CompositeKind.nestedAfterContent?: boolean` (default `false`): the sub-Bausteine of a Baustein of that kind follow its own content, and `mergeCompositeWithNext` keeps that order. Merging A = `[Ca, Na]` and B = `[Cb, Nb]` (C the leading blocks of the body that are not Bausteine, N the rest from the first sub-Baustein on) gives `[Ca, Cb, Na, Nb]`; without the flag the result stays `[Ca, Na, Cb, Nb]`. Still one undo step, the empty-paragraph rule and the `accepts` check are unchanged.
8
+
9
+ ## 3.5.1 - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - 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.
14
+ - `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.
15
+
3
16
  ## 3.5.0 - 2026-10-21
4
17
 
5
18
  ### Added
@@ -12,8 +25,6 @@
12
25
 
13
26
  - `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.
14
27
 
15
- React mirror of the dialog and node view follows in 3.5.1.
16
-
17
28
  ## 3.4.0 - 2026-10-21
18
29
 
19
30
  ### Added
@@ -11,12 +11,12 @@ 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;
15
14
  "update:modelValue": (text: string) => any;
15
+ ready: (view: EditorView) => any;
16
16
  save: (text: string) => any;
17
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
- onReady?: ((view: EditorView) => any) | undefined;
19
18
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
19
+ onReady?: ((view: EditorView) => any) | undefined;
20
20
  onSave?: ((text: string) => any) | undefined;
21
21
  }>, {
22
22
  modelValue: string;
@@ -32,12 +32,12 @@ 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;
36
35
  "update:modelValue": (text: string) => any;
36
+ ready: (target: DocumentSurfaceTarget) => any;
37
37
  save: (text: string) => any;
38
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
- onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
40
39
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
40
+ onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
41
41
  onSave?: ((text: string) => any) | undefined;
42
42
  }>, {
43
43
  modelValue: string;
@@ -16,8 +16,8 @@ export type ChatStream = (messages: ChatMessage[], signal: AbortSignal) => Async
16
16
  /** Body schema of the generate request (`POST /api/ai/generate`, `EditorBackend['ai'].stream('generate', …)`). */
17
17
  export declare const generateBodySchema: z.ZodObject<{
18
18
  task: z.ZodEnum<{
19
- generate: "generate";
20
19
  custom: "custom";
20
+ generate: "generate";
21
21
  continue: "continue";
22
22
  reformulate: "reformulate";
23
23
  "transcript-summary": "transcript-summary";
@@ -62,6 +62,12 @@ export interface CompositeKind {
62
62
  * Only new moves are checked; documents that already nest against it stay as they are.
63
63
  */
64
64
  accepts?: readonly string[] | 'all';
65
+ /**
66
+ * Sub-Bausteine follow the Baustein's own content; a merge keeps that order. `mergeCompositeWithNext` merging A = [Ca, Na] and B = [Cb, Nb]
67
+ * (C the leading blocks of the body that are not Bausteine, N the rest from the first sub-Baustein on) gives [Ca, Cb, Na, Nb] instead
68
+ * of [Ca, Na, Cb, Nb]. Default `false`.
69
+ */
70
+ nestedAfterContent?: boolean;
65
71
  /** Present on source-holding kinds: the media block types the kind's source may be; `sourceOf` returns the first body child of one of these. */
66
72
  source?: readonly (typeof SOURCE_MEDIA_TYPES)[number][];
67
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"kinds.js","names":[],"sources":["../../../src/features/composites/kinds.ts"],"sourcesContent":["// Vue-free and server-importable: the kind registry of the composite block family („Bausteine“).\nimport { createTranslator, type Translate } from '../../i18n'\n\n/** Media block types a source-holding kind's source may be (`CompositeKind.source`). */\nexport const SOURCE_MEDIA_TYPES = ['image', 'audio', 'video', 'file'] as const\n\n/** One metadata field of a composite kind, stored under `key` in the composite's `meta` attribute. */\nexport interface CompositeMetaField {\n key: string\n label: string\n /**\n * `bibliography`: CSL-JSON item under `meta.bib`; `derivation`: read-only provenance of a derived Baustein; `reference`: block id of another Baustein;\n * `multiselect`: several of `options`, stored as `string[]` (the key is removed when none is chosen).\n */\n type: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'multiselect' | 'reference' | 'bibliography' | 'derivation'\n /** Choices of a `select` or `multiselect` field (required for `multiselect`). */\n options?: { value: string, label: string }[]\n /** `reference`: kind ids the field may point to; absent = any Baustein. */\n targets?: readonly string[]\n}\n\n/**\n * What a kind tells the model beyond its fields. `purpose` goes into every AI request's block model (chat, generate\n * tasks); `instructions`, `instructionsField` and `skeleton` are read only by „Baustein füllen (KI)“ (`fillComposite`).\n */\nexport interface CompositeKindAi {\n /** One or two German sentences: what a Baustein of this kind is for and what its body holds. */\n purpose: string\n /** Default instruction for filling the body of a Baustein of this kind (what to write, structure, tone). */\n instructions?: string\n /** Key of a `textarea` field of this kind; a Baustein whose value is non-empty replaces `instructions` with it. */\n instructionsField?: string\n /** Target text (Vorlage) as Markdown; `{key}` placeholders are replaced by the Baustein's `meta[key]` before it goes to the model, unknown ones stay. */\n skeleton?: string\n}\n\n/** A composite kind: what its `kind` attribute selects (label, icon, chrome placeholders, meta fields). */\nexport interface CompositeKind {\n id: string\n label: string\n icon: string\n titlePlaceholder: string\n descriptionPlaceholder: string\n fields: CompositeMetaField[]\n /**\n * How much of the chrome the kind shows. Absent: all of it. `tags: false` hides the tag row of this kind (the node stays in the schema, empty).\n * `tone`: a CSS colour or `var(...)` that sets the kind apart: a Baustein of this kind gets `data-kind-tone` and `--sk-kind-tone` on its wrapper\n * (a tinted surface and a left border in the tone) and its tree rows tint the kind icon with it. Absent: the kind looks as before.\n * `controls: false` makes the chrome of a Baustein of this kind read-only although the editor is editable (for a kind whose attributes a host\n * guard refuses to change): no basket menu, status menu, title-visibility toggle, tag editing or drag handle on the kind icon, and a read-only\n * details popover; an existing basket, status and tags still show as chips. Default `true`.\n */\n chrome?: { tags?: boolean, tone?: string, controls?: boolean }\n /** Absent: the kind has no AI brief; „Baustein füllen (KI)“ is not offered for it. */\n ai?: CompositeKindAi\n /**\n * Baustein kinds this kind accepts as nested Bausteine: a list of kind ids, or `'all'`. Absent: `folder` and `document`\n * accept `'all'`, every other kind accepts none. Non-Baustein content (paragraphs, lists, ...) is always allowed.\n * Only new moves are checked; documents that already nest against it stay as they are.\n */\n accepts?: readonly string[] | 'all'\n /** Present on source-holding kinds: the media block types the kind's source may be; `sourceOf` returns the first body child of one of these. */\n source?: readonly (typeof SOURCE_MEDIA_TYPES)[number][]\n}\n\n/**\n * The built-in kinds in the language of `t`. The default set is produced from the translator (labels, placeholders, field\n * labels and status choices are catalog messages), so it is built per translator and memoized; hosts that pass their own\n * kinds (`compositeFeature({ kinds })`, `Composite.configure({ kinds })`) keep their labels untouched. Ids, field keys and\n * stored values (`open`, `doing`, `done`) do not depend on the language.\n */\nexport function defaultCompositeKinds(t: Translate): readonly CompositeKind[] {\n let kinds = DEFAULT_KINDS.get(t)\n if (!kinds) DEFAULT_KINDS.set(t, kinds = buildDefaultKinds(t))\n return kinds\n}\n\nconst DEFAULT_KINDS = new WeakMap<Translate, readonly CompositeKind[]>()\n\nfunction buildDefaultKinds(t: Translate): readonly CompositeKind[] {\n return [\n { id: 'section', label: t('composites.kind.section.label'), icon: 'i-lucide-layers', titlePlaceholder: t('composites.kind.section.title'), descriptionPlaceholder: t('composites.kind.section.description'), fields: [], accepts: 'all' },\n { id: 'task', label: t('composites.kind.task.label'), icon: 'i-lucide-square-check', titlePlaceholder: t('composites.kind.task.title'), descriptionPlaceholder: t('composites.kind.task.description'), fields: [\n { key: 'status', label: t('composites.kind.task.field.status'), type: 'select', options: [\n { value: 'open', label: t('composites.kind.task.status.open') },\n { value: 'doing', label: t('composites.kind.task.status.doing') },\n { value: 'done', label: t('composites.kind.task.status.done') },\n ] },\n { key: 'due', label: t('composites.kind.task.field.due'), type: 'date' },\n { key: 'assignee', label: t('composites.kind.task.field.assignee'), type: 'text' },\n ] },\n // i18n-ignore: model-facing kind purpose (German prompt text, not UI)\n { id: 'note', label: t('composites.kind.note.label'), icon: 'i-lucide-sticky-note', titlePlaceholder: t('composites.kind.note.title'), descriptionPlaceholder: t('composites.kind.note.description'), fields: [], ai: { purpose: 'Eigene Notiz zu einer Quelle oder einem Thema: Zusammenfassung, Gedanken, offene Fragen.' } },\n { id: 'document', label: t('composites.kind.document.label'), icon: 'i-lucide-file-text', titlePlaceholder: t('composites.kind.document.title'), descriptionPlaceholder: t('composites.kind.document.description'), source: [...SOURCE_MEDIA_TYPES], fields: [\n { key: 'filename', label: t('composites.kind.document.field.filename'), type: 'text' },\n { key: 'mimeType', label: t('composites.kind.document.field.mimeType'), type: 'text' },\n { key: 'size', label: t('composites.kind.document.field.size'), type: 'number' },\n { key: 'importedAt', label: t('composites.kind.document.field.importedAt'), type: 'date' },\n ] },\n // i18n-ignore-start: model-facing kind purposes and instructions (German prompt text, not UI)\n { id: 'recording', label: t('composites.kind.recording.label'), icon: 'i-lucide-mic', titlePlaceholder: t('composites.kind.recording.title'), descriptionPlaceholder: t('composites.kind.recording.description'), accepts: 'all', source: ['audio', 'video'], fields: [\n ...importFields(t, 'recording'),\n { key: 'recordedAt', label: t('composites.kind.recording.field.recordedAt'), type: 'date' },\n { key: 'participants', label: t('composites.kind.recording.field.participants'), type: 'text' },\n { key: 'durationSeconds', label: t('composites.kind.recording.field.durationSeconds'), type: 'number' },\n ], ai: { purpose: 'Aufnahme (Audio oder Video) als Quelle; der Inhalt hält Lesefassungen wie das Transkript und eigene Notizen.' } },\n { id: 'paper', label: t('composites.kind.paper.label'), icon: 'i-lucide-scroll-text', titlePlaceholder: t('composites.kind.paper.title'), descriptionPlaceholder: t('composites.kind.paper.description'), accepts: 'all', source: ['file'], fields: [\n ...importFields(t, 'paper'),\n { key: 'bib', label: t('composites.kind.paper.field.bib'), type: 'bibliography' },\n { key: 'doi', label: t('composites.kind.paper.field.doi'), type: 'text' },\n ], ai: { purpose: 'Wissenschaftliche Arbeit als Quelle (PDF) mit Literaturangabe; der Inhalt hält Volltext, Zusammenfassungen und Notizen.' } },\n { id: 'book', label: t('composites.kind.book.label'), icon: 'i-lucide-book', titlePlaceholder: t('composites.kind.book.title'), descriptionPlaceholder: t('composites.kind.book.description'), accepts: 'all', source: ['file'], fields: [\n ...importFields(t, 'book'),\n { key: 'bib', label: t('composites.kind.book.field.bib'), type: 'bibliography' },\n { key: 'isbn', label: t('composites.kind.book.field.isbn'), type: 'text' },\n ], ai: { purpose: 'Buch als Quelle (PDF) mit Literaturangabe; der Inhalt hält den Volltext, gegliedert in Kapitel, und Notizen.' } },\n { id: 'figure', label: t('composites.kind.figure.label'), icon: 'i-lucide-image', titlePlaceholder: t('composites.kind.figure.title'), descriptionPlaceholder: t('composites.kind.figure.description'), accepts: 'all', source: ['image'], fields: [\n ...importFields(t, 'figure'),\n { key: 'credit', label: t('composites.kind.figure.field.credit'), type: 'text' },\n ], ai: { purpose: 'Bild als Quelle; der Inhalt hält die Bildbeschreibung und eigene Notizen.' } },\n { id: 'rendition', label: t('composites.kind.rendition.label'), icon: 'i-lucide-captions', titlePlaceholder: t('composites.kind.rendition.title'), descriptionPlaceholder: t('composites.kind.rendition.description'), accepts: 'all', fields: [\n { key: 'derivation', label: t('composites.kind.rendition.field.derivation'), type: 'derivation' },\n { key: 'language', label: t('composites.kind.rendition.field.language'), type: 'text' },\n ], ai: {\n purpose: 'Lesbare Fassung einer Quelle (Transkript, Volltext, Beschreibung, Übersetzung)',\n instructions: 'Übertrage den Inhalt vollständig und gut lesbar; nichts weglassen, nichts hinzuerfinden.',\n } },\n // i18n-ignore-end\n { id: 'folder', label: t('composites.kind.folder.label'), icon: 'i-lucide-folder', titlePlaceholder: t('composites.kind.folder.title'), descriptionPlaceholder: t('composites.kind.folder.description'), fields: [] },\n ]\n}\n\n/** The four fields the import of a file fills in (`wrapImport` of `composite-media`), shared by every source-holding kind. */\nfunction importFields(t: Translate, kind: 'recording' | 'paper' | 'book' | 'figure'): CompositeMetaField[] {\n return [\n { key: 'filename', label: t(`composites.kind.${kind}.field.filename`), type: 'text' },\n { key: 'mimeType', label: t(`composites.kind.${kind}.field.mimeType`), type: 'text' },\n { key: 'size', label: t(`composites.kind.${kind}.field.size`), type: 'number' },\n { key: 'importedAt', label: t(`composites.kind.${kind}.field.importedAt`), type: 'date' },\n ]\n}\n\n/** The built-in kinds in the default language, for server-side and context-free code (`Composite`'s default option, an editor without a context). */\nexport const fallbackCompositeKinds = (): readonly CompositeKind[] => defaultCompositeKinds(DEFAULT_TRANSLATE)\n\nconst DEFAULT_TRANSLATE = createTranslator()\n\n/** Kind `id` from `kinds`; an unknown id gets the `section` kind's presentation (else the first kind's) under its own id and label. */\nexport function compositeKind(kinds: readonly CompositeKind[], id: string): CompositeKind {\n const known = kinds.find(kind => kind.id === id)\n if (known) return known\n const base = kinds.find(kind => kind.id === 'section') ?? kinds[0] ?? fallbackCompositeKinds()[0]!\n return { ...base, id, label: id }\n}\n\n/** Kinds that accept every Baustein kind when their definition sets no `accepts`. */\nconst ACCEPTS_ALL_BY_DEFAULT: Record<string, true> = { folder: true, document: true }\n\n/**\n * Whether a Baustein of kind `parentKind` accepts one of kind `childKind` as nested Baustein. The `accepts` of the kind\n * in `kinds` decides; without one (or for a kind `kinds` does not list) only `folder` and `document` accept all.\n */\nexport function kindAccepts(kinds: readonly CompositeKind[], parentKind: string, childKind: string): boolean {\n const accepts = kinds.find(kind => kind.id === parentKind)?.accepts\n if (accepts === undefined) return Object.hasOwn(ACCEPTS_ALL_BY_DEFAULT, parentKind)\n return accepts === 'all' || accepts.includes(childKind)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"kinds.js","names":[],"sources":["../../../src/features/composites/kinds.ts"],"sourcesContent":["// Vue-free and server-importable: the kind registry of the composite block family („Bausteine“).\nimport { createTranslator, type Translate } from '../../i18n'\n\n/** Media block types a source-holding kind's source may be (`CompositeKind.source`). */\nexport const SOURCE_MEDIA_TYPES = ['image', 'audio', 'video', 'file'] as const\n\n/** One metadata field of a composite kind, stored under `key` in the composite's `meta` attribute. */\nexport interface CompositeMetaField {\n key: string\n label: string\n /**\n * `bibliography`: CSL-JSON item under `meta.bib`; `derivation`: read-only provenance of a derived Baustein; `reference`: block id of another Baustein;\n * `multiselect`: several of `options`, stored as `string[]` (the key is removed when none is chosen).\n */\n type: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'multiselect' | 'reference' | 'bibliography' | 'derivation'\n /** Choices of a `select` or `multiselect` field (required for `multiselect`). */\n options?: { value: string, label: string }[]\n /** `reference`: kind ids the field may point to; absent = any Baustein. */\n targets?: readonly string[]\n}\n\n/**\n * What a kind tells the model beyond its fields. `purpose` goes into every AI request's block model (chat, generate\n * tasks); `instructions`, `instructionsField` and `skeleton` are read only by „Baustein füllen (KI)“ (`fillComposite`).\n */\nexport interface CompositeKindAi {\n /** One or two German sentences: what a Baustein of this kind is for and what its body holds. */\n purpose: string\n /** Default instruction for filling the body of a Baustein of this kind (what to write, structure, tone). */\n instructions?: string\n /** Key of a `textarea` field of this kind; a Baustein whose value is non-empty replaces `instructions` with it. */\n instructionsField?: string\n /** Target text (Vorlage) as Markdown; `{key}` placeholders are replaced by the Baustein's `meta[key]` before it goes to the model, unknown ones stay. */\n skeleton?: string\n}\n\n/** A composite kind: what its `kind` attribute selects (label, icon, chrome placeholders, meta fields). */\nexport interface CompositeKind {\n id: string\n label: string\n icon: string\n titlePlaceholder: string\n descriptionPlaceholder: string\n fields: CompositeMetaField[]\n /**\n * How much of the chrome the kind shows. Absent: all of it. `tags: false` hides the tag row of this kind (the node stays in the schema, empty).\n * `tone`: a CSS colour or `var(...)` that sets the kind apart: a Baustein of this kind gets `data-kind-tone` and `--sk-kind-tone` on its wrapper\n * (a tinted surface and a left border in the tone) and its tree rows tint the kind icon with it. Absent: the kind looks as before.\n * `controls: false` makes the chrome of a Baustein of this kind read-only although the editor is editable (for a kind whose attributes a host\n * guard refuses to change): no basket menu, status menu, title-visibility toggle, tag editing or drag handle on the kind icon, and a read-only\n * details popover; an existing basket, status and tags still show as chips. Default `true`.\n */\n chrome?: { tags?: boolean, tone?: string, controls?: boolean }\n /** Absent: the kind has no AI brief; „Baustein füllen (KI)“ is not offered for it. */\n ai?: CompositeKindAi\n /**\n * Baustein kinds this kind accepts as nested Bausteine: a list of kind ids, or `'all'`. Absent: `folder` and `document`\n * accept `'all'`, every other kind accepts none. Non-Baustein content (paragraphs, lists, ...) is always allowed.\n * Only new moves are checked; documents that already nest against it stay as they are.\n */\n accepts?: readonly string[] | 'all'\n /**\n * Sub-Bausteine follow the Baustein's own content; a merge keeps that order. `mergeCompositeWithNext` merging A = [Ca, Na] and B = [Cb, Nb]\n * (C the leading blocks of the body that are not Bausteine, N the rest from the first sub-Baustein on) gives [Ca, Cb, Na, Nb] instead\n * of [Ca, Na, Cb, Nb]. Default `false`.\n */\n nestedAfterContent?: boolean\n /** Present on source-holding kinds: the media block types the kind's source may be; `sourceOf` returns the first body child of one of these. */\n source?: readonly (typeof SOURCE_MEDIA_TYPES)[number][]\n}\n\n/**\n * The built-in kinds in the language of `t`. The default set is produced from the translator (labels, placeholders, field\n * labels and status choices are catalog messages), so it is built per translator and memoized; hosts that pass their own\n * kinds (`compositeFeature({ kinds })`, `Composite.configure({ kinds })`) keep their labels untouched. Ids, field keys and\n * stored values (`open`, `doing`, `done`) do not depend on the language.\n */\nexport function defaultCompositeKinds(t: Translate): readonly CompositeKind[] {\n let kinds = DEFAULT_KINDS.get(t)\n if (!kinds) DEFAULT_KINDS.set(t, kinds = buildDefaultKinds(t))\n return kinds\n}\n\nconst DEFAULT_KINDS = new WeakMap<Translate, readonly CompositeKind[]>()\n\nfunction buildDefaultKinds(t: Translate): readonly CompositeKind[] {\n return [\n { id: 'section', label: t('composites.kind.section.label'), icon: 'i-lucide-layers', titlePlaceholder: t('composites.kind.section.title'), descriptionPlaceholder: t('composites.kind.section.description'), fields: [], accepts: 'all' },\n { id: 'task', label: t('composites.kind.task.label'), icon: 'i-lucide-square-check', titlePlaceholder: t('composites.kind.task.title'), descriptionPlaceholder: t('composites.kind.task.description'), fields: [\n { key: 'status', label: t('composites.kind.task.field.status'), type: 'select', options: [\n { value: 'open', label: t('composites.kind.task.status.open') },\n { value: 'doing', label: t('composites.kind.task.status.doing') },\n { value: 'done', label: t('composites.kind.task.status.done') },\n ] },\n { key: 'due', label: t('composites.kind.task.field.due'), type: 'date' },\n { key: 'assignee', label: t('composites.kind.task.field.assignee'), type: 'text' },\n ] },\n // i18n-ignore: model-facing kind purpose (German prompt text, not UI)\n { id: 'note', label: t('composites.kind.note.label'), icon: 'i-lucide-sticky-note', titlePlaceholder: t('composites.kind.note.title'), descriptionPlaceholder: t('composites.kind.note.description'), fields: [], ai: { purpose: 'Eigene Notiz zu einer Quelle oder einem Thema: Zusammenfassung, Gedanken, offene Fragen.' } },\n { id: 'document', label: t('composites.kind.document.label'), icon: 'i-lucide-file-text', titlePlaceholder: t('composites.kind.document.title'), descriptionPlaceholder: t('composites.kind.document.description'), source: [...SOURCE_MEDIA_TYPES], fields: [\n { key: 'filename', label: t('composites.kind.document.field.filename'), type: 'text' },\n { key: 'mimeType', label: t('composites.kind.document.field.mimeType'), type: 'text' },\n { key: 'size', label: t('composites.kind.document.field.size'), type: 'number' },\n { key: 'importedAt', label: t('composites.kind.document.field.importedAt'), type: 'date' },\n ] },\n // i18n-ignore-start: model-facing kind purposes and instructions (German prompt text, not UI)\n { id: 'recording', label: t('composites.kind.recording.label'), icon: 'i-lucide-mic', titlePlaceholder: t('composites.kind.recording.title'), descriptionPlaceholder: t('composites.kind.recording.description'), accepts: 'all', source: ['audio', 'video'], fields: [\n ...importFields(t, 'recording'),\n { key: 'recordedAt', label: t('composites.kind.recording.field.recordedAt'), type: 'date' },\n { key: 'participants', label: t('composites.kind.recording.field.participants'), type: 'text' },\n { key: 'durationSeconds', label: t('composites.kind.recording.field.durationSeconds'), type: 'number' },\n ], ai: { purpose: 'Aufnahme (Audio oder Video) als Quelle; der Inhalt hält Lesefassungen wie das Transkript und eigene Notizen.' } },\n { id: 'paper', label: t('composites.kind.paper.label'), icon: 'i-lucide-scroll-text', titlePlaceholder: t('composites.kind.paper.title'), descriptionPlaceholder: t('composites.kind.paper.description'), accepts: 'all', source: ['file'], fields: [\n ...importFields(t, 'paper'),\n { key: 'bib', label: t('composites.kind.paper.field.bib'), type: 'bibliography' },\n { key: 'doi', label: t('composites.kind.paper.field.doi'), type: 'text' },\n ], ai: { purpose: 'Wissenschaftliche Arbeit als Quelle (PDF) mit Literaturangabe; der Inhalt hält Volltext, Zusammenfassungen und Notizen.' } },\n { id: 'book', label: t('composites.kind.book.label'), icon: 'i-lucide-book', titlePlaceholder: t('composites.kind.book.title'), descriptionPlaceholder: t('composites.kind.book.description'), accepts: 'all', source: ['file'], fields: [\n ...importFields(t, 'book'),\n { key: 'bib', label: t('composites.kind.book.field.bib'), type: 'bibliography' },\n { key: 'isbn', label: t('composites.kind.book.field.isbn'), type: 'text' },\n ], ai: { purpose: 'Buch als Quelle (PDF) mit Literaturangabe; der Inhalt hält den Volltext, gegliedert in Kapitel, und Notizen.' } },\n { id: 'figure', label: t('composites.kind.figure.label'), icon: 'i-lucide-image', titlePlaceholder: t('composites.kind.figure.title'), descriptionPlaceholder: t('composites.kind.figure.description'), accepts: 'all', source: ['image'], fields: [\n ...importFields(t, 'figure'),\n { key: 'credit', label: t('composites.kind.figure.field.credit'), type: 'text' },\n ], ai: { purpose: 'Bild als Quelle; der Inhalt hält die Bildbeschreibung und eigene Notizen.' } },\n { id: 'rendition', label: t('composites.kind.rendition.label'), icon: 'i-lucide-captions', titlePlaceholder: t('composites.kind.rendition.title'), descriptionPlaceholder: t('composites.kind.rendition.description'), accepts: 'all', fields: [\n { key: 'derivation', label: t('composites.kind.rendition.field.derivation'), type: 'derivation' },\n { key: 'language', label: t('composites.kind.rendition.field.language'), type: 'text' },\n ], ai: {\n purpose: 'Lesbare Fassung einer Quelle (Transkript, Volltext, Beschreibung, Übersetzung)',\n instructions: 'Übertrage den Inhalt vollständig und gut lesbar; nichts weglassen, nichts hinzuerfinden.',\n } },\n // i18n-ignore-end\n { id: 'folder', label: t('composites.kind.folder.label'), icon: 'i-lucide-folder', titlePlaceholder: t('composites.kind.folder.title'), descriptionPlaceholder: t('composites.kind.folder.description'), fields: [] },\n ]\n}\n\n/** The four fields the import of a file fills in (`wrapImport` of `composite-media`), shared by every source-holding kind. */\nfunction importFields(t: Translate, kind: 'recording' | 'paper' | 'book' | 'figure'): CompositeMetaField[] {\n return [\n { key: 'filename', label: t(`composites.kind.${kind}.field.filename`), type: 'text' },\n { key: 'mimeType', label: t(`composites.kind.${kind}.field.mimeType`), type: 'text' },\n { key: 'size', label: t(`composites.kind.${kind}.field.size`), type: 'number' },\n { key: 'importedAt', label: t(`composites.kind.${kind}.field.importedAt`), type: 'date' },\n ]\n}\n\n/** The built-in kinds in the default language, for server-side and context-free code (`Composite`'s default option, an editor without a context). */\nexport const fallbackCompositeKinds = (): readonly CompositeKind[] => defaultCompositeKinds(DEFAULT_TRANSLATE)\n\nconst DEFAULT_TRANSLATE = createTranslator()\n\n/** Kind `id` from `kinds`; an unknown id gets the `section` kind's presentation (else the first kind's) under its own id and label. */\nexport function compositeKind(kinds: readonly CompositeKind[], id: string): CompositeKind {\n const known = kinds.find(kind => kind.id === id)\n if (known) return known\n const base = kinds.find(kind => kind.id === 'section') ?? kinds[0] ?? fallbackCompositeKinds()[0]!\n return { ...base, id, label: id }\n}\n\n/** Kinds that accept every Baustein kind when their definition sets no `accepts`. */\nconst ACCEPTS_ALL_BY_DEFAULT: Record<string, true> = { folder: true, document: true }\n\n/**\n * Whether a Baustein of kind `parentKind` accepts one of kind `childKind` as nested Baustein. The `accepts` of the kind\n * in `kinds` decides; without one (or for a kind `kinds` does not list) only `folder` and `document` accept all.\n */\nexport function kindAccepts(kinds: readonly CompositeKind[], parentKind: string, childKind: string): boolean {\n const accepts = kinds.find(kind => kind.id === parentKind)?.accepts\n if (accepts === undefined) return Object.hasOwn(ACCEPTS_ALL_BY_DEFAULT, parentKind)\n return accepts === 'all' || accepts.includes(childKind)\n}\n"],"mappings":";;;AAIA,MAAa,qBAAqB;CAAC;CAAS;CAAS;CAAS;AAAM;;;;;;;AAyEpE,SAAgB,sBAAsB,GAAwC;CAC5E,IAAI,QAAQ,cAAc,IAAI,CAAC;CAC/B,IAAI,CAAC,OAAO,cAAc,IAAI,GAAG,QAAQ,kBAAkB,CAAC,CAAC;CAC7D,OAAO;AACT;AAEA,MAAM,gCAAgB,IAAI,QAA6C;AAEvE,SAAS,kBAAkB,GAAwC;CACjE,OAAO;EACL;GAAE,IAAI;GAAW,OAAO,EAAE,+BAA+B;GAAG,MAAM;GAAmB,kBAAkB,EAAE,+BAA+B;GAAG,wBAAwB,EAAE,qCAAqC;GAAG,QAAQ,CAAC;GAAG,SAAS;EAAM;EACxO;GAAE,IAAI;GAAQ,OAAO,EAAE,4BAA4B;GAAG,MAAM;GAAyB,kBAAkB,EAAE,4BAA4B;GAAG,wBAAwB,EAAE,kCAAkC;GAAG,QAAQ;IAC7M;KAAE,KAAK;KAAU,OAAO,EAAE,mCAAmC;KAAG,MAAM;KAAU,SAAS;MACvF;OAAE,OAAO;OAAQ,OAAO,EAAE,kCAAkC;MAAE;MAC9D;OAAE,OAAO;OAAS,OAAO,EAAE,mCAAmC;MAAE;MAChE;OAAE,OAAO;OAAQ,OAAO,EAAE,kCAAkC;MAAE;KAChE;IAAE;IACF;KAAE,KAAK;KAAO,OAAO,EAAE,gCAAgC;KAAG,MAAM;IAAO;IACvE;KAAE,KAAK;KAAY,OAAO,EAAE,qCAAqC;KAAG,MAAM;IAAO;GACnF;EAAE;EAEF;GAAE,IAAI;GAAQ,OAAO,EAAE,4BAA4B;GAAG,MAAM;GAAwB,kBAAkB,EAAE,4BAA4B;GAAG,wBAAwB,EAAE,kCAAkC;GAAG,QAAQ,CAAC;GAAG,IAAI,EAAE,SAAS,2FAA2F;EAAE;EAC9T;GAAE,IAAI;GAAY,OAAO,EAAE,gCAAgC;GAAG,MAAM;GAAsB,kBAAkB,EAAE,gCAAgC;GAAG,wBAAwB,EAAE,sCAAsC;GAAG,QAAQ,CAAC,GAAG,kBAAkB;GAAG,QAAQ;IAC3P;KAAE,KAAK;KAAY,OAAO,EAAE,yCAAyC;KAAG,MAAM;IAAO;IACrF;KAAE,KAAK;KAAY,OAAO,EAAE,yCAAyC;KAAG,MAAM;IAAO;IACrF;KAAE,KAAK;KAAQ,OAAO,EAAE,qCAAqC;KAAG,MAAM;IAAS;IAC/E;KAAE,KAAK;KAAc,OAAO,EAAE,2CAA2C;KAAG,MAAM;IAAO;GAC3F;EAAE;EAEF;GAAE,IAAI;GAAa,OAAO,EAAE,iCAAiC;GAAG,MAAM;GAAgB,kBAAkB,EAAE,iCAAiC;GAAG,wBAAwB,EAAE,uCAAuC;GAAG,SAAS;GAAO,QAAQ,CAAC,SAAS,OAAO;GAAG,QAAQ;IACpQ,GAAG,aAAa,GAAG,WAAW;IAC9B;KAAE,KAAK;KAAc,OAAO,EAAE,4CAA4C;KAAG,MAAM;IAAO;IAC1F;KAAE,KAAK;KAAgB,OAAO,EAAE,8CAA8C;KAAG,MAAM;IAAO;IAC9F;KAAE,KAAK;KAAmB,OAAO,EAAE,iDAAiD;KAAG,MAAM;IAAS;GACxG;GAAG,IAAI,EAAE,SAAS,+GAA+G;EAAE;EACnI;GAAE,IAAI;GAAS,OAAO,EAAE,6BAA6B;GAAG,MAAM;GAAwB,kBAAkB,EAAE,6BAA6B;GAAG,wBAAwB,EAAE,mCAAmC;GAAG,SAAS;GAAO,QAAQ,CAAC,MAAM;GAAG,QAAQ;IAClP,GAAG,aAAa,GAAG,OAAO;IAC1B;KAAE,KAAK;KAAO,OAAO,EAAE,iCAAiC;KAAG,MAAM;IAAe;IAChF;KAAE,KAAK;KAAO,OAAO,EAAE,iCAAiC;KAAG,MAAM;IAAO;GAC1E;GAAG,IAAI,EAAE,SAAS,0HAA0H;EAAE;EAC9I;GAAE,IAAI;GAAQ,OAAO,EAAE,4BAA4B;GAAG,MAAM;GAAiB,kBAAkB,EAAE,4BAA4B;GAAG,wBAAwB,EAAE,kCAAkC;GAAG,SAAS;GAAO,QAAQ,CAAC,MAAM;GAAG,QAAQ;IACvO,GAAG,aAAa,GAAG,MAAM;IACzB;KAAE,KAAK;KAAO,OAAO,EAAE,gCAAgC;KAAG,MAAM;IAAe;IAC/E;KAAE,KAAK;KAAQ,OAAO,EAAE,iCAAiC;KAAG,MAAM;IAAO;GAC3E;GAAG,IAAI,EAAE,SAAS,+GAA+G;EAAE;EACnI;GAAE,IAAI;GAAU,OAAO,EAAE,8BAA8B;GAAG,MAAM;GAAkB,kBAAkB,EAAE,8BAA8B;GAAG,wBAAwB,EAAE,oCAAoC;GAAG,SAAS;GAAO,QAAQ,CAAC,OAAO;GAAG,QAAQ,CACjP,GAAG,aAAa,GAAG,QAAQ,GAC3B;IAAE,KAAK;IAAU,OAAO,EAAE,qCAAqC;IAAG,MAAM;GAAO,CACjF;GAAG,IAAI,EAAE,SAAS,4EAA4E;EAAE;EAChG;GAAE,IAAI;GAAa,OAAO,EAAE,iCAAiC;GAAG,MAAM;GAAqB,kBAAkB,EAAE,iCAAiC;GAAG,wBAAwB,EAAE,uCAAuC;GAAG,SAAS;GAAO,QAAQ,CAC7O;IAAE,KAAK;IAAc,OAAO,EAAE,4CAA4C;IAAG,MAAM;GAAa,GAChG;IAAE,KAAK;IAAY,OAAO,EAAE,0CAA0C;IAAG,MAAM;GAAO,CACxF;GAAG,IAAI;IACL,SAAS;IACT,cAAc;GAChB;EAAE;EAEF;GAAE,IAAI;GAAU,OAAO,EAAE,8BAA8B;GAAG,MAAM;GAAmB,kBAAkB,EAAE,8BAA8B;GAAG,wBAAwB,EAAE,oCAAoC;GAAG,QAAQ,CAAC;EAAE;CACtN;AACF;;AAGA,SAAS,aAAa,GAAc,MAAuE;CACzG,OAAO;EACL;GAAE,KAAK;GAAY,OAAO,EAAE,mBAAmB,KAAK,gBAAgB;GAAG,MAAM;EAAO;EACpF;GAAE,KAAK;GAAY,OAAO,EAAE,mBAAmB,KAAK,gBAAgB;GAAG,MAAM;EAAO;EACpF;GAAE,KAAK;GAAQ,OAAO,EAAE,mBAAmB,KAAK,YAAY;GAAG,MAAM;EAAS;EAC9E;GAAE,KAAK;GAAc,OAAO,EAAE,mBAAmB,KAAK,kBAAkB;GAAG,MAAM;EAAO;CAC1F;AACF;;AAGA,MAAa,+BAAyD,sBAAsB,iBAAiB;AAE7G,MAAM,oBAAoB,iBAAiB;;AAG3C,SAAgB,cAAc,OAAiC,IAA2B;CACxF,MAAM,QAAQ,MAAM,MAAK,SAAQ,KAAK,OAAO,EAAE;CAC/C,IAAI,OAAO,OAAO;CAElB,OAAO;EAAE,GADI,MAAM,MAAK,SAAQ,KAAK,OAAO,SAAS,KAAK,MAAM,MAAM,uBAAuB,CAAC,CAAC;EAC7E;EAAI,OAAO;CAAG;AAClC;;AAGA,MAAM,yBAA+C;CAAE,QAAQ;CAAM,UAAU;AAAK;;;;;AAMpF,SAAgB,YAAY,OAAiC,YAAoB,WAA4B;CAC3G,MAAM,UAAU,MAAM,MAAK,SAAQ,KAAK,OAAO,UAAU,CAAC,EAAE;CAC5D,IAAI,YAAY,KAAA,GAAW,OAAO,OAAO,OAAO,wBAAwB,UAAU;CAClF,OAAO,YAAY,SAAS,QAAQ,SAAS,SAAS;AACxD"}
@@ -3,16 +3,17 @@ import { Extension } from "@tiptap/core";
3
3
  import { Fragment, Node as Node$1 } from "@tiptap/pm/model";
4
4
  //#region src/features/composites/merge.d.ts
5
5
  /**
6
- * What a merge appends to the end of the first Baustein's body: the whole body of `next`, its own sub-Bausteine
7
- * included, in document order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.
6
+ * What a merge appends to the first Baustein's body: the whole body of `next`, its own sub-Bausteine included, in document
7
+ * order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.
8
8
  * Null when `first`'s kind does not accept the kind of a Baustein nested in that body (the merge is refused).
9
+ * Where it goes is the command's call: at the end, or, for a kind with `nestedAfterContent`, split by `splitAtSubComposites`.
9
10
  */
10
11
  export declare function mergeBody(kinds: readonly CompositeKind[], first: Node$1, next: Node$1): Fragment | null;
11
12
  declare module '@tiptap/core' {
12
13
  interface Commands<ReturnType> {
13
14
  compositeMerge: {
14
15
  /**
15
- * The Baustein at `pos` (default: the caret's innermost one) absorbs the Baustein that directly follows it in the same parent: that one's body, with its sub-Bausteine, is appended to the end of this one's body and the following Baustein is gone. Title, description, tags, id, kind and meta stay those of the first. One transaction. Refused when no Baustein follows, when the first kind does not accept a Baustein nested in the following one, when either Baustein has a kind with `chrome.controls === false` (host-managed), or when the editor is not editable.
16
+ * The Baustein at `pos` (default: the caret's innermost one) absorbs the Baustein that directly follows it in the same parent: that one's body, with its sub-Bausteine, is appended to the end of this one's body and the following Baustein is gone. For a kind with `nestedAfterContent` the following body's own content (the leading blocks that are not Bausteine) goes in right before the first one's first sub-Baustein and only its sub-Bausteine go to the end, so the content stays ahead of the sub-Bausteine. Title, description, tags, id, kind and meta stay those of the first. One transaction. Refused when no Baustein follows, when the first kind does not accept a Baustein nested in the following one, when either Baustein has a kind with `chrome.controls === false` (host-managed), or when the editor is not editable.
16
17
  */
17
18
  mergeCompositeWithNext: (pos?: number) => ReturnType;
18
19
  };
@@ -5,9 +5,10 @@ import { Extension } from "@tiptap/core";
5
5
  import { Fragment } from "@tiptap/pm/model";
6
6
  //#region src/features/composites/merge.ts
7
7
  /**
8
- * What a merge appends to the end of the first Baustein's body: the whole body of `next`, its own sub-Bausteine
9
- * included, in document order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.
8
+ * What a merge appends to the first Baustein's body: the whole body of `next`, its own sub-Bausteine included, in document
9
+ * order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.
10
10
  * Null when `first`'s kind does not accept the kind of a Baustein nested in that body (the merge is refused).
11
+ * Where it goes is the command's call: at the end, or, for a kind with `nestedAfterContent`, split by `splitAtSubComposites`.
11
12
  */
12
13
  function mergeBody(kinds, first, next) {
13
14
  let chrome = 0;
@@ -18,6 +19,20 @@ function mergeBody(kinds, first, next) {
18
19
  const only = body.childCount === 1 ? body.firstChild : null;
19
20
  return only?.type.name === "paragraph" && only.content.size === 0 ? Fragment.empty : body;
20
21
  }
22
+ /**
23
+ * Splits a merge body (see `mergeBody`) at its first sub-Baustein: `content` is the leading run of blocks that are not
24
+ * Bausteine, `nested` the rest from the first sub-Baustein on (empty when there is none).
25
+ */
26
+ function splitAtSubComposites(body) {
27
+ let at = 0;
28
+ while (at < body.childCount && body.child(at).type.name !== "composite") at++;
29
+ let size = 0;
30
+ for (let i = 0; i < at; i++) size += body.child(i).nodeSize;
31
+ return {
32
+ content: body.cut(0, size),
33
+ nested: body.cut(size)
34
+ };
35
+ }
21
36
  /** Client-only, no schema: the merge command of Bausteine. */
22
37
  const CompositeMerge = Extension.create({
23
38
  name: "compositeMerge",
@@ -36,7 +51,18 @@ const CompositeMerge = Extension.create({
36
51
  if (!appended) return false;
37
52
  if (dispatch) {
38
53
  const nextPos = found.pos + found.node.nodeSize;
39
- tr.insert(nextPos - 1, appended);
54
+ const end = nextPos - 1;
55
+ if (compositeKind(kinds, String(found.node.attrs.kind)).nestedAfterContent && appended.size > 0) {
56
+ const { content, nested } = splitAtSubComposites(appended);
57
+ let subPos = found.pos + 1;
58
+ for (let i = 0; i < found.node.childCount; i++) {
59
+ const child = found.node.child(i);
60
+ if (i >= 3 && child.type.name === "composite") break;
61
+ subPos += child.nodeSize;
62
+ }
63
+ tr.insert(end, nested);
64
+ tr.insert(subPos, content);
65
+ } else tr.insert(end, appended);
40
66
  const shifted = nextPos + appended.size;
41
67
  tr.delete(shifted, shifted + next.nodeSize);
42
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"merge.js","names":[],"sources":["../../../src/features/composites/merge.ts"],"sourcesContent":["// Merging a Baustein with its next sibling: the counterpart of the split. The pure part (`mergeBody`) knows nothing about\n// the editor; the `CompositeMerge` extension wraps it into the command `mergeCompositeWithNext`.\nimport { Extension } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport { outermostComposites } from './accepted-children'\nimport { type CompositeKind, compositeKind, kindAccepts } from './kinds'\nimport { COMPOSITE_CHROME_COUNT, compositeAt, compositeKindsOf } from './nodes'\n\n/**\n * What a merge appends to the end of the first Baustein's body: the whole body of `next`, its own sub-Bausteine\n * included, in document order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.\n * Null when `first`'s kind does not accept the kind of a Baustein nested in that body (the merge is refused).\n */\nexport function mergeBody(kinds: readonly CompositeKind[], first: PMNode, next: PMNode): Fragment | null {\n let chrome = 0\n for (let i = 0; i < COMPOSITE_CHROME_COUNT; i++) chrome += next.child(i).nodeSize\n const body = next.content.cut(chrome)\n const firstKind = String(first.attrs.kind)\n for (let i = 0; i < body.childCount; i++) {\n for (const child of outermostComposites(body.child(i))) {\n if (!kindAccepts(kinds, firstKind, String(child.attrs.kind))) return null\n }\n }\n const only = body.childCount === 1 ? body.firstChild! : null\n return only?.type.name === 'paragraph' && only.content.size === 0 ? Fragment.empty : body\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n compositeMerge: {\n /**\n * The Baustein at `pos` (default: the caret's innermost one) absorbs the Baustein that directly follows it in the same parent: that one's body, with its sub-Bausteine, is appended to the end of this one's body and the following Baustein is gone. Title, description, tags, id, kind and meta stay those of the first. One transaction. Refused when no Baustein follows, when the first kind does not accept a Baustein nested in the following one, when either Baustein has a kind with `chrome.controls === false` (host-managed), or when the editor is not editable.\n */\n mergeCompositeWithNext: (pos?: number) => ReturnType\n }\n }\n}\n\n/** Client-only, no schema: the merge command of Bausteine. */\nexport const CompositeMerge = Extension.create({\n name: 'compositeMerge',\n addCommands() {\n return {\n mergeCompositeWithNext: pos => ({ state, tr, dispatch }) => {\n if (!this.editor.isEditable) return false\n const found = compositeAt(state, pos)\n if (!found) return false\n const $pos = state.doc.resolve(found.pos)\n const next = $pos.parent.maybeChild($pos.index() + 1)\n if (next?.type.name !== 'composite') return false\n const kinds = compositeKindsOf(this.editor)\n // A kind with `chrome.controls === false` is host-managed: neither absorbing nor being absorbed is the editor's call.\n const hostManaged = (node: PMNode) => compositeKind(kinds, String(node.attrs.kind)).chrome?.controls === false\n if (hostManaged(found.node) || hostManaged(next)) return false\n const appended = mergeBody(kinds, found.node, next)\n if (!appended) return false\n if (dispatch) {\n const nextPos = found.pos + found.node.nodeSize\n // The body goes in at the end of the first Baustein, then the second one is cut out behind it.\n tr.insert(nextPos - 1, appended)\n const shifted = nextPos + appended.size\n tr.delete(shifted, shifted + next.nodeSize)\n }\n return true\n },\n }\n },\n})\n"],"mappings":";;;;;;;;;;;AAaA,SAAgB,UAAU,OAAiC,OAAe,MAA+B;CACvG,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAA,GAA4B,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC;CACzE,MAAM,OAAO,KAAK,QAAQ,IAAI,MAAM;CACpC,MAAM,YAAY,OAAO,MAAM,MAAM,IAAI;CACzC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,YAAY,KACnC,KAAK,MAAM,SAAS,oBAAoB,KAAK,MAAM,CAAC,CAAC,GACnD,IAAI,CAAC,YAAY,OAAO,WAAW,OAAO,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO;CAGzE,MAAM,OAAO,KAAK,eAAe,IAAI,KAAK,aAAc;CACxD,OAAO,MAAM,KAAK,SAAS,eAAe,KAAK,QAAQ,SAAS,IAAI,SAAS,QAAQ;AACvF;;AAcA,MAAa,iBAAiB,UAAU,OAAO;CAC7C,MAAM;CACN,cAAc;EACZ,OAAO,EACL,yBAAwB,SAAQ,EAAE,OAAO,IAAI,eAAe;GAC1D,IAAI,CAAC,KAAK,OAAO,YAAY,OAAO;GACpC,MAAM,QAAQ,YAAY,OAAO,GAAG;GACpC,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,GAAG;GACxC,MAAM,OAAO,KAAK,OAAO,WAAW,KAAK,MAAM,IAAI,CAAC;GACpD,IAAI,MAAM,KAAK,SAAS,aAAa,OAAO;GAC5C,MAAM,QAAQ,iBAAiB,KAAK,MAAM;GAE1C,MAAM,eAAe,SAAiB,cAAc,OAAO,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,aAAa;GACzG,IAAI,YAAY,MAAM,IAAI,KAAK,YAAY,IAAI,GAAG,OAAO;GACzD,MAAM,WAAW,UAAU,OAAO,MAAM,MAAM,IAAI;GAClD,IAAI,CAAC,UAAU,OAAO;GACtB,IAAI,UAAU;IACZ,MAAM,UAAU,MAAM,MAAM,MAAM,KAAK;IAEvC,GAAG,OAAO,UAAU,GAAG,QAAQ;IAC/B,MAAM,UAAU,UAAU,SAAS;IACnC,GAAG,OAAO,SAAS,UAAU,KAAK,QAAQ;GAC5C;GACA,OAAO;EACT,EACF;CACF;AACF,CAAC"}
1
+ {"version":3,"file":"merge.js","names":[],"sources":["../../../src/features/composites/merge.ts"],"sourcesContent":["// Merging a Baustein with its next sibling: the counterpart of the split. The pure part (`mergeBody`) knows nothing about\n// the editor; the `CompositeMerge` extension wraps it into the command `mergeCompositeWithNext`.\nimport { Extension } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport { outermostComposites } from './accepted-children'\nimport { type CompositeKind, compositeKind, kindAccepts } from './kinds'\nimport { COMPOSITE_CHROME_COUNT, compositeAt, compositeKindsOf } from './nodes'\n\n/**\n * What a merge appends to the first Baustein's body: the whole body of `next`, its own sub-Bausteine included, in document\n * order. A body that is one empty paragraph (a Baustein nothing was written in) adds nothing.\n * Null when `first`'s kind does not accept the kind of a Baustein nested in that body (the merge is refused).\n * Where it goes is the command's call: at the end, or, for a kind with `nestedAfterContent`, split by `splitAtSubComposites`.\n */\nexport function mergeBody(kinds: readonly CompositeKind[], first: PMNode, next: PMNode): Fragment | null {\n let chrome = 0\n for (let i = 0; i < COMPOSITE_CHROME_COUNT; i++) chrome += next.child(i).nodeSize\n const body = next.content.cut(chrome)\n const firstKind = String(first.attrs.kind)\n for (let i = 0; i < body.childCount; i++) {\n for (const child of outermostComposites(body.child(i))) {\n if (!kindAccepts(kinds, firstKind, String(child.attrs.kind))) return null\n }\n }\n const only = body.childCount === 1 ? body.firstChild! : null\n return only?.type.name === 'paragraph' && only.content.size === 0 ? Fragment.empty : body\n}\n\n/**\n * Splits a merge body (see `mergeBody`) at its first sub-Baustein: `content` is the leading run of blocks that are not\n * Bausteine, `nested` the rest from the first sub-Baustein on (empty when there is none).\n */\nfunction splitAtSubComposites(body: Fragment): { content: Fragment, nested: Fragment } {\n let at = 0\n while (at < body.childCount && body.child(at).type.name !== 'composite') at++\n let size = 0\n for (let i = 0; i < at; i++) size += body.child(i).nodeSize\n return { content: body.cut(0, size), nested: body.cut(size) }\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n compositeMerge: {\n /**\n * The Baustein at `pos` (default: the caret's innermost one) absorbs the Baustein that directly follows it in the same parent: that one's body, with its sub-Bausteine, is appended to the end of this one's body and the following Baustein is gone. For a kind with `nestedAfterContent` the following body's own content (the leading blocks that are not Bausteine) goes in right before the first one's first sub-Baustein and only its sub-Bausteine go to the end, so the content stays ahead of the sub-Bausteine. Title, description, tags, id, kind and meta stay those of the first. One transaction. Refused when no Baustein follows, when the first kind does not accept a Baustein nested in the following one, when either Baustein has a kind with `chrome.controls === false` (host-managed), or when the editor is not editable.\n */\n mergeCompositeWithNext: (pos?: number) => ReturnType\n }\n }\n}\n\n/** Client-only, no schema: the merge command of Bausteine. */\nexport const CompositeMerge = Extension.create({\n name: 'compositeMerge',\n addCommands() {\n return {\n mergeCompositeWithNext: pos => ({ state, tr, dispatch }) => {\n if (!this.editor.isEditable) return false\n const found = compositeAt(state, pos)\n if (!found) return false\n const $pos = state.doc.resolve(found.pos)\n const next = $pos.parent.maybeChild($pos.index() + 1)\n if (next?.type.name !== 'composite') return false\n const kinds = compositeKindsOf(this.editor)\n // A kind with `chrome.controls === false` is host-managed: neither absorbing nor being absorbed is the editor's call.\n const hostManaged = (node: PMNode) => compositeKind(kinds, String(node.attrs.kind)).chrome?.controls === false\n if (hostManaged(found.node) || hostManaged(next)) return false\n const appended = mergeBody(kinds, found.node, next)\n if (!appended) return false\n if (dispatch) {\n const nextPos = found.pos + found.node.nodeSize\n const end = nextPos - 1\n // `nestedAfterContent`: the content of the second body goes ahead of the first one's sub-Bausteine, the sub-Bausteine of the second one to the end.\n if (compositeKind(kinds, String(found.node.attrs.kind)).nestedAfterContent && appended.size > 0) {\n const { content, nested } = splitAtSubComposites(appended)\n let subPos = found.pos + 1\n for (let i = 0; i < found.node.childCount; i++) {\n const child = found.node.child(i)\n if (i >= COMPOSITE_CHROME_COUNT && child.type.name === 'composite') break\n subPos += child.nodeSize\n }\n // Back to front, so no position moves under the next insert.\n tr.insert(end, nested)\n tr.insert(subPos, content)\n }\n else {\n // The body goes in at the end of the first Baustein, then the second one is cut out behind it.\n tr.insert(end, appended)\n }\n const shifted = nextPos + appended.size\n tr.delete(shifted, shifted + next.nodeSize)\n }\n return true\n },\n }\n },\n})\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,UAAU,OAAiC,OAAe,MAA+B;CACvG,IAAI,SAAS;CACb,KAAK,IAAI,IAAI,GAAG,IAAA,GAA4B,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC;CACzE,MAAM,OAAO,KAAK,QAAQ,IAAI,MAAM;CACpC,MAAM,YAAY,OAAO,MAAM,MAAM,IAAI;CACzC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,YAAY,KACnC,KAAK,MAAM,SAAS,oBAAoB,KAAK,MAAM,CAAC,CAAC,GACnD,IAAI,CAAC,YAAY,OAAO,WAAW,OAAO,MAAM,MAAM,IAAI,CAAC,GAAG,OAAO;CAGzE,MAAM,OAAO,KAAK,eAAe,IAAI,KAAK,aAAc;CACxD,OAAO,MAAM,KAAK,SAAS,eAAe,KAAK,QAAQ,SAAS,IAAI,SAAS,QAAQ;AACvF;;;;;AAMA,SAAS,qBAAqB,MAAyD;CACrF,IAAI,KAAK;CACT,OAAO,KAAK,KAAK,cAAc,KAAK,MAAM,EAAE,CAAC,CAAC,KAAK,SAAS,aAAa;CACzE,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC;CACnD,OAAO;EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;EAAG,QAAQ,KAAK,IAAI,IAAI;CAAE;AAC9D;;AAcA,MAAa,iBAAiB,UAAU,OAAO;CAC7C,MAAM;CACN,cAAc;EACZ,OAAO,EACL,yBAAwB,SAAQ,EAAE,OAAO,IAAI,eAAe;GAC1D,IAAI,CAAC,KAAK,OAAO,YAAY,OAAO;GACpC,MAAM,QAAQ,YAAY,OAAO,GAAG;GACpC,IAAI,CAAC,OAAO,OAAO;GACnB,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,GAAG;GACxC,MAAM,OAAO,KAAK,OAAO,WAAW,KAAK,MAAM,IAAI,CAAC;GACpD,IAAI,MAAM,KAAK,SAAS,aAAa,OAAO;GAC5C,MAAM,QAAQ,iBAAiB,KAAK,MAAM;GAE1C,MAAM,eAAe,SAAiB,cAAc,OAAO,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,QAAQ,aAAa;GACzG,IAAI,YAAY,MAAM,IAAI,KAAK,YAAY,IAAI,GAAG,OAAO;GACzD,MAAM,WAAW,UAAU,OAAO,MAAM,MAAM,IAAI;GAClD,IAAI,CAAC,UAAU,OAAO;GACtB,IAAI,UAAU;IACZ,MAAM,UAAU,MAAM,MAAM,MAAM,KAAK;IACvC,MAAM,MAAM,UAAU;IAEtB,IAAI,cAAc,OAAO,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,sBAAsB,SAAS,OAAO,GAAG;KAC/F,MAAM,EAAE,SAAS,WAAW,qBAAqB,QAAQ;KACzD,IAAI,SAAS,MAAM,MAAM;KACzB,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK,YAAY,KAAK;MAC9C,MAAM,QAAQ,MAAM,KAAK,MAAM,CAAC;MAChC,IAAI,KAAA,KAA+B,MAAM,KAAK,SAAS,aAAa;MACpE,UAAU,MAAM;KAClB;KAEA,GAAG,OAAO,KAAK,MAAM;KACrB,GAAG,OAAO,QAAQ,OAAO;IAC3B,OAGE,GAAG,OAAO,KAAK,QAAQ;IAEzB,MAAM,UAAU,UAAU,SAAS;IACnC,GAAG,OAAO,SAAS,UAAU,KAAK,QAAQ;GAC5C;GACA,OAAO;EACT,EACF;CACF;AACF,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { ReadonlyStore } from "../../core/store.js";
2
+ import { EditorBackend } from "../../core/backend.js";
3
+ import { ImageStudioImage, ImageStudioPromptBlock } from "../../core/backend-types.js";
4
+ import { ImageStudioState } from "./state.js";
5
+ import { Editor } from "@tiptap/core";
6
+ //#region src/features/image-studio/node-core.d.ts
7
+ export interface ImageStudioNodeCoreOptions {
8
+ editor: Editor;
9
+ /** Position of the `imageStudio` node (a node view's `getPos`); `undefined` once the node is gone. */
10
+ getPos: () => number | undefined;
11
+ /** The backend's image port; `undefined` (backend without the `images` capability) makes `generate` and `upload` reject. */
12
+ backend: EditorBackend['images'];
13
+ /** Document whose package stores the uploaded references. */
14
+ documentId: string;
15
+ }
16
+ /** What a view renders from a node's studio: the observable part of an `ImageStudioNodeCore`. */
17
+ export interface ImageStudioNodeSnapshot {
18
+ readonly state: ImageStudioState;
19
+ /** The managed list of Prompt-Bausteine, as the backend answered; empty until then. */
20
+ readonly promptBlocks: readonly ImageStudioPromptBlock[];
21
+ /** The Prompt-Bausteine could not be loaded: the message. */
22
+ readonly problem: string | undefined;
23
+ }
24
+ /**
25
+ * The framework-free store of one `imageStudio` node (`createImageStudioNodeCore`): an observable `ImageStudioNodeSnapshot`
26
+ * (`get`/`subscribe`, see `ReadonlyStore`; every change replaces the snapshot) and the actions the dialog needs.
27
+ * The Vue and React bindings wrap it into the store shape of their dialog.
28
+ */
29
+ export interface ImageStudioNodeCore extends ReadonlyStore<ImageStudioNodeSnapshot> {
30
+ /** Writes `next` into the node's `studio` attribute as one transaction (`src` follows the selected image). */
31
+ update: (next: ImageStudioState) => void;
32
+ generate: (instruction: string, signal: AbortSignal) => Promise<ImageStudioImage>;
33
+ upload: (file: File) => Promise<ImageStudioImage>;
34
+ /** Stops following the editor and ignores the answers still outstanding; call when the node view unmounts. */
35
+ dispose: () => void;
36
+ }
37
+ /**
38
+ * The core of the store of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every
39
+ * editor transaction, so undo, collaboration and other writers show), `update` writes it back through
40
+ * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and
41
+ * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Listeners run synchronously. Call `dispose`
42
+ * when the view goes.
43
+ */
44
+ export declare function createImageStudioNodeCore(options: ImageStudioNodeCoreOptions): ImageStudioNodeCore;
45
+ //#endregion
46
+ //# sourceMappingURL=node-core.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { EMPTY_IMAGE_STUDIO_STATE, normalizeImageStudioState } from "./state.js";
2
+ //#region src/features/image-studio/node-core.ts
3
+ const NO_IMAGES = "@ecsplico/editor: the backend has no images capability";
4
+ /**
5
+ * The core of the store of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every
6
+ * editor transaction, so undo, collaboration and other writers show), `update` writes it back through
7
+ * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and
8
+ * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Listeners run synchronously. Call `dispose`
9
+ * when the view goes.
10
+ */
11
+ function createImageStudioNodeCore(options) {
12
+ const { editor, getPos, backend, documentId } = options;
13
+ let snapshot = {
14
+ state: EMPTY_IMAGE_STUDIO_STATE,
15
+ promptBlocks: [],
16
+ problem: void 0
17
+ };
18
+ const listeners = /* @__PURE__ */ new Set();
19
+ let disposed = false;
20
+ let source;
21
+ function publish(patch) {
22
+ snapshot = {
23
+ ...snapshot,
24
+ ...patch
25
+ };
26
+ for (const listener of [...listeners]) listener(snapshot);
27
+ }
28
+ function sync() {
29
+ const pos = getPos();
30
+ const node = pos === void 0 || editor.isDestroyed ? null : editor.state.doc.nodeAt(pos);
31
+ if (node?.type.name !== "imageStudio" || node.attrs.studio === source) return;
32
+ source = node.attrs.studio;
33
+ publish({ state: normalizeImageStudioState(source) });
34
+ }
35
+ editor.on("transaction", sync);
36
+ sync();
37
+ if (backend) backend.promptBlocks().then((blocks) => {
38
+ if (!disposed) publish({ promptBlocks: blocks });
39
+ }, (error) => {
40
+ if (!disposed) publish({ problem: error instanceof Error ? error.message : String(error) });
41
+ });
42
+ return {
43
+ get: () => snapshot,
44
+ subscribe(listener) {
45
+ listeners.add(listener);
46
+ return () => {
47
+ listeners.delete(listener);
48
+ };
49
+ },
50
+ update(next) {
51
+ const pos = getPos();
52
+ if (pos === void 0 || editor.isDestroyed) return;
53
+ editor.commands.setImageStudioState(pos, next);
54
+ },
55
+ generate(instruction, signal) {
56
+ if (!backend) return Promise.reject(/* @__PURE__ */ new Error(NO_IMAGES));
57
+ const { description, promptBlockIds, references, generations } = snapshot.state;
58
+ const referenceUrls = [...references, ...generations].filter((image) => image.active).map((image) => image.url);
59
+ return backend.generate({
60
+ description,
61
+ instruction,
62
+ promptBlockIds: [...promptBlockIds],
63
+ referenceUrls
64
+ }, signal);
65
+ },
66
+ upload(file) {
67
+ return backend ? backend.upload(documentId, file) : Promise.reject(/* @__PURE__ */ new Error(NO_IMAGES));
68
+ },
69
+ dispose() {
70
+ disposed = true;
71
+ listeners.clear();
72
+ editor.off("transaction", sync);
73
+ }
74
+ };
75
+ }
76
+ //#endregion
77
+ export { createImageStudioNodeCore };
78
+
79
+ //# sourceMappingURL=node-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-core.js","names":[],"sources":["../../../src/features/image-studio/node-core.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { EditorBackend } from '../../core/backend'\nimport type { ReadonlyStore } from '../../core/store'\nimport { EMPTY_IMAGE_STUDIO_STATE, type ImageStudioImage, type ImageStudioPromptBlock, type ImageStudioState, normalizeImageStudioState } from './state'\n\nexport interface ImageStudioNodeCoreOptions {\n editor: Editor\n /** Position of the `imageStudio` node (a node view's `getPos`); `undefined` once the node is gone. */\n getPos: () => number | undefined\n /** The backend's image port; `undefined` (backend without the `images` capability) makes `generate` and `upload` reject. */\n backend: EditorBackend['images']\n /** Document whose package stores the uploaded references. */\n documentId: string\n}\n\n/** What a view renders from a node's studio: the observable part of an `ImageStudioNodeCore`. */\nexport interface ImageStudioNodeSnapshot {\n readonly state: ImageStudioState\n /** The managed list of Prompt-Bausteine, as the backend answered; empty until then. */\n readonly promptBlocks: readonly ImageStudioPromptBlock[]\n /** The Prompt-Bausteine could not be loaded: the message. */\n readonly problem: string | undefined\n}\n\n/**\n * The framework-free store of one `imageStudio` node (`createImageStudioNodeCore`): an observable `ImageStudioNodeSnapshot`\n * (`get`/`subscribe`, see `ReadonlyStore`; every change replaces the snapshot) and the actions the dialog needs.\n * The Vue and React bindings wrap it into the store shape of their dialog.\n */\nexport interface ImageStudioNodeCore extends ReadonlyStore<ImageStudioNodeSnapshot> {\n /** Writes `next` into the node's `studio` attribute as one transaction (`src` follows the selected image). */\n update: (next: ImageStudioState) => void\n generate: (instruction: string, signal: AbortSignal) => Promise<ImageStudioImage>\n upload: (file: File) => Promise<ImageStudioImage>\n /** Stops following the editor and ignores the answers still outstanding; call when the node view unmounts. */\n dispose: () => void\n}\n\n// What a store without the backend's `images` port rejects with (the node view does not open one without the port).\nconst NO_IMAGES = '@ecsplico/editor: the backend has no images capability'\n\n/**\n * The core of the store of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every\n * editor transaction, so undo, collaboration and other writers show), `update` writes it back through\n * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and\n * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Listeners run synchronously. Call `dispose`\n * when the view goes.\n */\nexport function createImageStudioNodeCore(options: ImageStudioNodeCoreOptions): ImageStudioNodeCore {\n const { editor, getPos, backend, documentId } = options\n let snapshot: ImageStudioNodeSnapshot = { state: EMPTY_IMAGE_STUDIO_STATE, promptBlocks: [], problem: undefined }\n const listeners = new Set<(value: ImageStudioNodeSnapshot) => void>()\n let disposed = false\n // The attribute value `state` was normalized from: a transaction that left it alone keeps `state` as it is.\n let source: unknown\n\n function publish(patch: Partial<ImageStudioNodeSnapshot>): void {\n snapshot = { ...snapshot, ...patch }\n // A copy: listeners may (un)subscribe while being notified.\n for (const listener of [...listeners]) listener(snapshot)\n }\n\n function sync(): void {\n const pos = getPos()\n const node = pos === undefined || editor.isDestroyed ? null : editor.state.doc.nodeAt(pos)\n if (node?.type.name !== 'imageStudio' || node.attrs.studio === source) return\n source = node.attrs.studio\n publish({ state: normalizeImageStudioState(source) })\n }\n\n editor.on('transaction', sync)\n sync()\n\n if (backend) {\n backend.promptBlocks().then(\n (blocks) => { if (!disposed) publish({ promptBlocks: blocks }) },\n (error: unknown) => { if (!disposed) publish({ problem: error instanceof Error ? error.message : String(error) }) },\n )\n }\n\n return {\n get: () => snapshot,\n subscribe(listener) {\n listeners.add(listener)\n return () => { listeners.delete(listener) }\n },\n update(next) {\n const pos = getPos()\n if (pos === undefined || editor.isDestroyed) return\n editor.commands.setImageStudioState(pos, next)\n },\n generate(instruction, signal) {\n if (!backend) return Promise.reject(new Error(NO_IMAGES))\n const { description, promptBlockIds, references, generations } = snapshot.state\n const referenceUrls = [...references, ...generations].filter(image => image.active).map(image => image.url)\n return backend.generate({ description, instruction, promptBlockIds: [...promptBlockIds], referenceUrls }, signal)\n },\n upload(file): Promise<ImageStudioImage> {\n return backend ? backend.upload(documentId, file) : Promise.reject(new Error(NO_IMAGES))\n },\n dispose() {\n disposed = true\n listeners.clear()\n editor.off('transaction', sync)\n },\n }\n}\n"],"mappings":";;AAuCA,MAAM,YAAY;;;;;;;;AASlB,SAAgB,0BAA0B,SAA0D;CAClG,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;CAChD,IAAI,WAAoC;EAAE,OAAO;EAA0B,cAAc,CAAC;EAAG,SAAS,KAAA;CAAU;CAChH,MAAM,4BAAY,IAAI,IAA8C;CACpE,IAAI,WAAW;CAEf,IAAI;CAEJ,SAAS,QAAQ,OAA+C;EAC9D,WAAW;GAAE,GAAG;GAAU,GAAG;EAAM;EAEnC,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG,SAAS,QAAQ;CAC1D;CAEA,SAAS,OAAa;EACpB,MAAM,MAAM,OAAO;EACnB,MAAM,OAAO,QAAQ,KAAA,KAAa,OAAO,cAAc,OAAO,OAAO,MAAM,IAAI,OAAO,GAAG;EACzF,IAAI,MAAM,KAAK,SAAS,iBAAiB,KAAK,MAAM,WAAW,QAAQ;EACvE,SAAS,KAAK,MAAM;EACpB,QAAQ,EAAE,OAAO,0BAA0B,MAAM,EAAE,CAAC;CACtD;CAEA,OAAO,GAAG,eAAe,IAAI;CAC7B,KAAK;CAEL,IAAI,SACF,QAAQ,aAAa,CAAC,CAAC,MACpB,WAAW;EAAE,IAAI,CAAC,UAAU,QAAQ,EAAE,cAAc,OAAO,CAAC;CAAE,IAC9D,UAAmB;EAAE,IAAI,CAAC,UAAU,QAAQ,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,CAAC;CAAE,CACpH;CAGF,OAAO;EACL,WAAW;EACX,UAAU,UAAU;GAClB,UAAU,IAAI,QAAQ;GACtB,aAAa;IAAE,UAAU,OAAO,QAAQ;GAAE;EAC5C;EACA,OAAO,MAAM;GACX,MAAM,MAAM,OAAO;GACnB,IAAI,QAAQ,KAAA,KAAa,OAAO,aAAa;GAC7C,OAAO,SAAS,oBAAoB,KAAK,IAAI;EAC/C;EACA,SAAS,aAAa,QAAQ;GAC5B,IAAI,CAAC,SAAS,OAAO,QAAQ,uBAAO,IAAI,MAAM,SAAS,CAAC;GACxD,MAAM,EAAE,aAAa,gBAAgB,YAAY,gBAAgB,SAAS;GAC1E,MAAM,gBAAgB,CAAC,GAAG,YAAY,GAAG,WAAW,CAAC,CAAC,QAAO,UAAS,MAAM,MAAM,CAAC,CAAC,KAAI,UAAS,MAAM,GAAG;GAC1G,OAAO,QAAQ,SAAS;IAAE;IAAa;IAAa,gBAAgB,CAAC,GAAG,cAAc;IAAG;GAAc,GAAG,MAAM;EAClH;EACA,OAAO,MAAiC;GACtC,OAAO,UAAU,QAAQ,OAAO,YAAY,IAAI,IAAI,QAAQ,uBAAO,IAAI,MAAM,SAAS,CAAC;EACzF;EACA,UAAU;GACR,WAAW;GACX,UAAU,MAAM;GAChB,OAAO,IAAI,eAAe,IAAI;EAChC;CACF;AACF"}
@@ -1,16 +1,7 @@
1
- import { EditorBackend } from "../../core/backend.js";
1
+ import { ImageStudioNodeCoreOptions } from "./node-core.js";
2
2
  import { ImageStudioStore } from "./store.js";
3
- import { Editor } from "@tiptap/core";
4
3
  //#region src/features/image-studio/node-store.d.ts
5
- export interface ImageStudioNodeStoreOptions {
6
- editor: Editor;
7
- /** Position of the `imageStudio` node (a node view's `getPos`); `undefined` once the node is gone. */
8
- getPos: () => number | undefined;
9
- /** The backend's image port; `undefined` (backend without the `images` capability) makes `generate` and `upload` reject. */
10
- backend: EditorBackend['images'];
11
- /** Document whose package stores the uploaded references. */
12
- documentId: string;
13
- }
4
+ export type ImageStudioNodeStoreOptions = ImageStudioNodeCoreOptions;
14
5
  /** A store over one node, with the function that detaches it from the editor. */
15
6
  export interface ImageStudioNodeStore extends ImageStudioStore {
16
7
  /** Stops following the editor and ignores the answers still outstanding; call when the node view unmounts. */
@@ -21,6 +12,7 @@ export interface ImageStudioNodeStore extends ImageStudioStore {
21
12
  * editor transaction, so undo, collaboration and other writers show), `update` writes it back through
22
13
  * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and
23
14
  * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Call `dispose` when the view goes.
15
+ * The Vue wrapper of `createImageStudioNodeCore`.
24
16
  */
25
17
  export declare function createImageStudioNodeStore(options: ImageStudioNodeStoreOptions): ImageStudioNodeStore;
26
18
  //#endregion
@@ -1,61 +1,32 @@
1
- import { EMPTY_IMAGE_STUDIO_STATE, normalizeImageStudioState } from "./state.js";
1
+ import { createImageStudioNodeCore } from "./node-core.js";
2
2
  import { shallowRef } from "vue";
3
3
  //#region src/features/image-studio/node-store.ts
4
- const NO_IMAGES = "@ecsplico/editor: the backend has no images capability";
5
4
  /**
6
5
  * The `ImageStudioStore` of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every
7
6
  * editor transaction, so undo, collaboration and other writers show), `update` writes it back through
8
7
  * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and
9
8
  * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Call `dispose` when the view goes.
9
+ * The Vue wrapper of `createImageStudioNodeCore`.
10
10
  */
11
11
  function createImageStudioNodeStore(options) {
12
- const { editor, getPos, backend, documentId } = options;
13
- const state = shallowRef(EMPTY_IMAGE_STUDIO_STATE);
14
- const promptBlocks = shallowRef([]);
15
- const problem = shallowRef(void 0);
16
- let disposed = false;
17
- let source;
18
- function sync() {
19
- const pos = getPos();
20
- const node = pos === void 0 || editor.isDestroyed ? null : editor.state.doc.nodeAt(pos);
21
- if (node?.type.name !== "imageStudio" || node.attrs.studio === source) return;
22
- source = node.attrs.studio;
23
- state.value = normalizeImageStudioState(source);
24
- }
25
- editor.on("transaction", sync);
26
- sync();
27
- if (backend) backend.promptBlocks().then((blocks) => {
28
- if (!disposed) promptBlocks.value = blocks;
29
- }, (error) => {
30
- if (!disposed) problem.value = error instanceof Error ? error.message : String(error);
12
+ const core = createImageStudioNodeCore(options);
13
+ const initial = core.get();
14
+ const state = shallowRef(initial.state);
15
+ const promptBlocks = shallowRef(initial.promptBlocks);
16
+ const problem = shallowRef(initial.problem);
17
+ core.subscribe((snapshot) => {
18
+ state.value = snapshot.state;
19
+ promptBlocks.value = snapshot.promptBlocks;
20
+ problem.value = snapshot.problem;
31
21
  });
32
22
  return {
33
23
  state,
34
24
  promptBlocks,
35
25
  problem,
36
- update(next) {
37
- const pos = getPos();
38
- if (pos === void 0 || editor.isDestroyed) return;
39
- editor.commands.setImageStudioState(pos, next);
40
- },
41
- generate(instruction, signal) {
42
- if (!backend) return Promise.reject(/* @__PURE__ */ new Error(NO_IMAGES));
43
- const { description, promptBlockIds, references, generations } = state.value;
44
- const referenceUrls = [...references, ...generations].filter((image) => image.active).map((image) => image.url);
45
- return backend.generate({
46
- description,
47
- instruction,
48
- promptBlockIds: [...promptBlockIds],
49
- referenceUrls
50
- }, signal);
51
- },
52
- upload(file) {
53
- return backend ? backend.upload(documentId, file) : Promise.reject(/* @__PURE__ */ new Error(NO_IMAGES));
54
- },
55
- dispose() {
56
- disposed = true;
57
- editor.off("transaction", sync);
58
- }
26
+ update: core.update,
27
+ generate: core.generate,
28
+ upload: core.upload,
29
+ dispose: core.dispose
59
30
  };
60
31
  }
61
32
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"node-store.js","names":[],"sources":["../../../src/features/image-studio/node-store.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { shallowRef } from 'vue'\nimport type { EditorBackend } from '../../core/backend'\nimport type { ImageStudioStore } from './store'\nimport { type ImageStudioImage, type ImageStudioPromptBlock, type ImageStudioState, EMPTY_IMAGE_STUDIO_STATE, normalizeImageStudioState } from './state'\n\nexport interface ImageStudioNodeStoreOptions {\n editor: Editor\n /** Position of the `imageStudio` node (a node view's `getPos`); `undefined` once the node is gone. */\n getPos: () => number | undefined\n /** The backend's image port; `undefined` (backend without the `images` capability) makes `generate` and `upload` reject. */\n backend: EditorBackend['images']\n /** Document whose package stores the uploaded references. */\n documentId: string\n}\n\n/** A store over one node, with the function that detaches it from the editor. */\nexport interface ImageStudioNodeStore extends ImageStudioStore {\n /** Stops following the editor and ignores the answers still outstanding; call when the node view unmounts. */\n dispose: () => void\n}\n\n// What a store without the backend's `images` port rejects with (the node view does not open one without the port).\nconst NO_IMAGES = '@ecsplico/editor: the backend has no images capability'\n\n/**\n * The `ImageStudioStore` of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every\n * editor transaction, so undo, collaboration and other writers show), `update` writes it back through\n * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and\n * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Call `dispose` when the view goes.\n */\nexport function createImageStudioNodeStore(options: ImageStudioNodeStoreOptions): ImageStudioNodeStore {\n const { editor, getPos, backend, documentId } = options\n const state = shallowRef<ImageStudioState>(EMPTY_IMAGE_STUDIO_STATE)\n const promptBlocks = shallowRef<readonly ImageStudioPromptBlock[]>([])\n const problem = shallowRef<string | undefined>(undefined)\n let disposed = false\n // The attribute value `state` was normalized from: a transaction that left it alone keeps `state` as it is.\n let source: unknown\n\n function sync(): void {\n const pos = getPos()\n const node = pos === undefined || editor.isDestroyed ? null : editor.state.doc.nodeAt(pos)\n if (node?.type.name !== 'imageStudio' || node.attrs.studio === source) return\n source = node.attrs.studio\n state.value = normalizeImageStudioState(source)\n }\n\n editor.on('transaction', sync)\n sync()\n\n if (backend) {\n backend.promptBlocks().then(\n (blocks) => { if (!disposed) promptBlocks.value = blocks },\n (error: unknown) => { if (!disposed) problem.value = error instanceof Error ? error.message : String(error) },\n )\n }\n\n return {\n state,\n promptBlocks,\n problem,\n update(next) {\n const pos = getPos()\n if (pos === undefined || editor.isDestroyed) return\n editor.commands.setImageStudioState(pos, next)\n },\n generate(instruction, signal) {\n if (!backend) return Promise.reject(new Error(NO_IMAGES))\n const { description, promptBlockIds, references, generations } = state.value\n const referenceUrls = [...references, ...generations].filter(image => image.active).map(image => image.url)\n return backend.generate({ description, instruction, promptBlockIds: [...promptBlockIds], referenceUrls }, signal)\n },\n upload(file): Promise<ImageStudioImage> {\n return backend ? backend.upload(documentId, file) : Promise.reject(new Error(NO_IMAGES))\n },\n dispose() {\n disposed = true\n editor.off('transaction', sync)\n },\n }\n}\n"],"mappings":";;;AAuBA,MAAM,YAAY;;;;;;;AAQlB,SAAgB,2BAA2B,SAA4D;CACrG,MAAM,EAAE,QAAQ,QAAQ,SAAS,eAAe;CAChD,MAAM,QAAQ,WAA6B,wBAAwB;CACnE,MAAM,eAAe,WAA8C,CAAC,CAAC;CACrE,MAAM,UAAU,WAA+B,KAAA,CAAS;CACxD,IAAI,WAAW;CAEf,IAAI;CAEJ,SAAS,OAAa;EACpB,MAAM,MAAM,OAAO;EACnB,MAAM,OAAO,QAAQ,KAAA,KAAa,OAAO,cAAc,OAAO,OAAO,MAAM,IAAI,OAAO,GAAG;EACzF,IAAI,MAAM,KAAK,SAAS,iBAAiB,KAAK,MAAM,WAAW,QAAQ;EACvE,SAAS,KAAK,MAAM;EACpB,MAAM,QAAQ,0BAA0B,MAAM;CAChD;CAEA,OAAO,GAAG,eAAe,IAAI;CAC7B,KAAK;CAEL,IAAI,SACF,QAAQ,aAAa,CAAC,CAAC,MACpB,WAAW;EAAE,IAAI,CAAC,UAAU,aAAa,QAAQ;CAAO,IACxD,UAAmB;EAAE,IAAI,CAAC,UAAU,QAAQ,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CAAE,CAC9G;CAGF,OAAO;EACL;EACA;EACA;EACA,OAAO,MAAM;GACX,MAAM,MAAM,OAAO;GACnB,IAAI,QAAQ,KAAA,KAAa,OAAO,aAAa;GAC7C,OAAO,SAAS,oBAAoB,KAAK,IAAI;EAC/C;EACA,SAAS,aAAa,QAAQ;GAC5B,IAAI,CAAC,SAAS,OAAO,QAAQ,uBAAO,IAAI,MAAM,SAAS,CAAC;GACxD,MAAM,EAAE,aAAa,gBAAgB,YAAY,gBAAgB,MAAM;GACvE,MAAM,gBAAgB,CAAC,GAAG,YAAY,GAAG,WAAW,CAAC,CAAC,QAAO,UAAS,MAAM,MAAM,CAAC,CAAC,KAAI,UAAS,MAAM,GAAG;GAC1G,OAAO,QAAQ,SAAS;IAAE;IAAa;IAAa,gBAAgB,CAAC,GAAG,cAAc;IAAG;GAAc,GAAG,MAAM;EAClH;EACA,OAAO,MAAiC;GACtC,OAAO,UAAU,QAAQ,OAAO,YAAY,IAAI,IAAI,QAAQ,uBAAO,IAAI,MAAM,SAAS,CAAC;EACzF;EACA,UAAU;GACR,WAAW;GACX,OAAO,IAAI,eAAe,IAAI;EAChC;CACF;AACF"}
1
+ {"version":3,"file":"node-store.js","names":[],"sources":["../../../src/features/image-studio/node-store.ts"],"sourcesContent":["import { shallowRef } from 'vue'\nimport { createImageStudioNodeCore, type ImageStudioNodeCoreOptions } from './node-core'\nimport type { ImageStudioPromptBlock, ImageStudioState } from './state'\nimport type { ImageStudioStore } from './store'\n\nexport type ImageStudioNodeStoreOptions = ImageStudioNodeCoreOptions\n\n/** A store over one node, with the function that detaches it from the editor. */\nexport interface ImageStudioNodeStore extends ImageStudioStore {\n /** Stops following the editor and ignores the answers still outstanding; call when the node view unmounts. */\n dispose: () => void\n}\n\n/**\n * The `ImageStudioStore` of one `imageStudio` node: `state` follows the node's `studio` attribute (re-read on every\n * editor transaction, so undo, collaboration and other writers show), `update` writes it back through\n * `setImageStudioState` as one transaction (one undo step; the node's `src` follows the selected image), and\n * `generate`, `upload` and the Prompt-Bausteine go to `backend.images`. Call `dispose` when the view goes.\n * The Vue wrapper of `createImageStudioNodeCore`.\n */\nexport function createImageStudioNodeStore(options: ImageStudioNodeStoreOptions): ImageStudioNodeStore {\n const core = createImageStudioNodeCore(options)\n const initial = core.get()\n const state = shallowRef<ImageStudioState>(initial.state)\n const promptBlocks = shallowRef<readonly ImageStudioPromptBlock[]>(initial.promptBlocks)\n const problem = shallowRef<string | undefined>(initial.problem)\n core.subscribe((snapshot) => {\n state.value = snapshot.state\n promptBlocks.value = snapshot.promptBlocks\n problem.value = snapshot.problem\n })\n return {\n state,\n promptBlocks,\n problem,\n update: core.update,\n generate: core.generate,\n upload: core.upload,\n dispose: core.dispose,\n }\n}\n"],"mappings":";;;;;;;;;;AAoBA,SAAgB,2BAA2B,SAA4D;CACrG,MAAM,OAAO,0BAA0B,OAAO;CAC9C,MAAM,UAAU,KAAK,IAAI;CACzB,MAAM,QAAQ,WAA6B,QAAQ,KAAK;CACxD,MAAM,eAAe,WAA8C,QAAQ,YAAY;CACvF,MAAM,UAAU,WAA+B,QAAQ,OAAO;CAC9D,KAAK,WAAW,aAAa;EAC3B,MAAM,QAAQ,SAAS;EACvB,aAAa,QAAQ,SAAS;EAC9B,QAAQ,QAAQ,SAAS;CAC3B,CAAC;CACD,OAAO;EACL;EACA;EACA;EACA,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,SAAS,KAAK;CAChB;AACF"}