@ecsplico/editor 3.1.1 → 3.3.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.
- package/CHANGELOG.md +42 -0
- package/dist/adapters/direct-ai.d.ts +36 -0
- package/dist/adapters/direct-ai.js +169 -0
- package/dist/adapters/direct-ai.js.map +1 -0
- package/dist/adapters/memory.js.map +1 -1
- package/dist/code/CodeSurface.vue.d.ts +1 -1
- package/dist/core/block-drag.js +3 -0
- package/dist/core/block-drag.js.map +1 -1
- package/dist/core/block-transfer.d.ts +44 -0
- package/dist/core/block-transfer.js +68 -0
- package/dist/core/block-transfer.js.map +1 -0
- package/dist/core/commands.js +5 -0
- package/dist/core/commands.js.map +1 -1
- package/dist/core/core-feature.d.ts +4 -1
- package/dist/core/core-feature.js +8 -3
- package/dist/core/core-feature.js.map +1 -1
- package/dist/core/editor-ui.d.ts +3 -1
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/extension-points.d.ts +17 -1
- package/dist/core/feature.d.ts +6 -1
- package/dist/core/feature.js.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.js +3 -2
- package/dist/core/schema/index.d.ts +24 -3
- package/dist/core/schema/index.js +21 -4
- package/dist/core/schema/index.js.map +1 -1
- package/dist/core/surface-session.js +3 -1
- package/dist/core/surface-session.js.map +1 -1
- package/dist/core/toolbar-items.js +1 -1
- package/dist/core/toolbar-items.js.map +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +1 -1
- package/dist/features/ai/agent-op-review.js +1 -1
- package/dist/features/ai/agent-ops.js +2 -2
- package/dist/features/ai/ai-apply.js +1 -1
- package/dist/features/ai/assistant.js +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +2 -2
- package/dist/features/ai/components/AiSidebar.js +2 -2
- package/dist/features/ai/components/SettingsAiTab.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/document-chat-model.js +1 -1
- package/dist/features/ai/document-chat.js +2 -2
- package/dist/features/ai/ghost-autocomplete.js +1 -1
- package/dist/features/ai/index.d.ts +4 -3
- package/dist/features/ai/index.js +6 -5
- package/dist/features/ai/protocol/complete.d.ts +19 -0
- package/dist/features/ai/protocol/complete.js +27 -1
- package/dist/features/ai/protocol/complete.js.map +1 -1
- package/dist/features/ai/protocol/generate.d.ts +95 -0
- package/dist/features/ai/protocol/generate.js +58 -0
- package/dist/features/ai/protocol/generate.js.map +1 -0
- package/dist/features/ai/protocol/index.d.ts +4 -3
- package/dist/features/ai/protocol/index.js +6 -5
- package/dist/features/ai/protocol/tasks.d.ts +3 -1
- package/dist/features/ai/protocol/tasks.js +74 -2
- package/dist/features/ai/protocol/tasks.js.map +1 -1
- package/dist/features/ai/react/AiSelectionMenu.js +2 -2
- package/dist/features/ai/react/AiSidebar.js +2 -2
- package/dist/features/ai/react/SettingsAiTab.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/selection-flow.js +1 -1
- package/dist/features/ai/sidebar-flow.js +1 -1
- package/dist/features/block-ui/block-actions.d.ts +19 -3
- package/dist/features/block-ui/block-actions.js +49 -3
- package/dist/features/block-ui/block-actions.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.js +24 -13
- package/dist/features/block-ui/components/BlockActionsOverlay.js.map +1 -1
- package/dist/features/block-ui/components/BlockActionsOverlay.vue.d.ts +12 -5
- package/dist/features/block-ui/components/BlockUi.js +52 -20
- package/dist/features/block-ui/components/BlockUi.js.map +1 -1
- package/dist/features/block-ui/components/BlockUi.vue.d.ts +6 -4
- package/dist/features/block-ui/feature.d.ts +8 -2
- package/dist/features/block-ui/feature.js +7 -2
- package/dist/features/block-ui/feature.js.map +1 -1
- package/dist/features/block-ui/index.d.ts +3 -2
- package/dist/features/block-ui/index.js +2 -2
- package/dist/features/block-ui/react/BlockActionsOverlay.d.ts +14 -7
- package/dist/features/block-ui/react/BlockActionsOverlay.js +25 -13
- package/dist/features/block-ui/react/BlockActionsOverlay.js.map +1 -1
- package/dist/features/block-ui/react/BlockUi.d.ts +6 -4
- package/dist/features/block-ui/react/BlockUi.js +43 -15
- package/dist/features/block-ui/react/BlockUi.js.map +1 -1
- package/dist/features/block-ui/react.d.ts +4 -2
- package/dist/features/block-ui/react.js +3 -3
- package/dist/features/block-ui/react.js.map +1 -1
- package/dist/features/block-ui/vue.d.ts +4 -2
- package/dist/features/block-ui/vue.js +3 -3
- package/dist/features/block-ui/vue.js.map +1 -1
- package/dist/features/composites/baskets.d.ts +27 -0
- package/dist/features/composites/baskets.js +46 -0
- package/dist/features/composites/baskets.js.map +1 -0
- package/dist/features/composites/components/CompositeBasketMenu.js +97 -0
- package/dist/features/composites/components/CompositeBasketMenu.js.map +1 -0
- package/dist/features/composites/components/CompositeChrome.js +79 -23
- package/dist/features/composites/components/CompositeChrome.js.map +1 -1
- package/dist/features/composites/components/CompositeView.js +21 -2
- package/dist/features/composites/components/CompositeView.js.map +1 -1
- package/dist/features/composites/components/DerivationBadge.js +12 -3
- package/dist/features/composites/components/DerivationBadge.js.map +1 -1
- package/dist/features/composites/components/FocusChip.js +53 -0
- package/dist/features/composites/components/FocusChip.js.map +1 -0
- package/dist/features/composites/components/FocusChip.vue.d.ts +14 -0
- package/dist/features/composites/components/SettingsEditingTab.js +40 -4
- package/dist/features/composites/components/SettingsEditingTab.js.map +1 -1
- package/dist/features/composites/derivation.d.ts +2 -1
- package/dist/features/composites/derivation.js +5 -1
- package/dist/features/composites/derivation.js.map +1 -1
- package/dist/features/composites/derived-chrome.js +13 -3
- package/dist/features/composites/derived-chrome.js.map +1 -1
- package/dist/features/composites/feature.d.ts +7 -1
- package/dist/features/composites/feature.js +107 -23
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/focus.d.ts +38 -0
- package/dist/features/composites/focus.js +86 -0
- package/dist/features/composites/focus.js.map +1 -0
- package/dist/features/composites/index.d.ts +7 -3
- package/dist/features/composites/index.js +7 -3
- package/dist/features/composites/kinds.d.ts +6 -1
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/merge.d.ts +24 -0
- package/dist/features/composites/merge.js +47 -0
- package/dist/features/composites/merge.js.map +1 -0
- package/dist/features/composites/meta-keys.d.ts +4 -0
- package/dist/features/composites/meta-keys.js +8 -4
- package/dist/features/composites/meta-keys.js.map +1 -1
- package/dist/features/composites/nodes.d.ts +15 -2
- package/dist/features/composites/nodes.js +61 -5
- package/dist/features/composites/nodes.js.map +1 -1
- package/dist/features/composites/outline.d.ts +4 -0
- package/dist/features/composites/outline.js +3 -0
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeBasketMenu.js +88 -0
- package/dist/features/composites/react/CompositeBasketMenu.js.map +1 -0
- package/dist/features/composites/react/CompositeChrome.js +52 -4
- package/dist/features/composites/react/CompositeChrome.js.map +1 -1
- package/dist/features/composites/react/CompositeView.js +15 -2
- package/dist/features/composites/react/CompositeView.js.map +1 -1
- package/dist/features/composites/react/DerivationBadge.js +10 -1
- package/dist/features/composites/react/DerivationBadge.js.map +1 -1
- package/dist/features/composites/react/FocusChip.d.ts +11 -0
- package/dist/features/composites/react/FocusChip.js +50 -0
- package/dist/features/composites/react/FocusChip.js.map +1 -0
- package/dist/features/composites/react/SettingsEditingTab.js +36 -1
- package/dist/features/composites/react/SettingsEditingTab.js.map +1 -1
- package/dist/features/composites/react.d.ts +2 -1
- package/dist/features/composites/react.js +3 -1
- package/dist/features/composites/react.js.map +1 -1
- package/dist/features/composites/schema.d.ts +5 -3
- package/dist/features/composites/schema.js +8 -4
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/split.d.ts +62 -0
- package/dist/features/composites/split.js +172 -0
- package/dist/features/composites/split.js.map +1 -0
- package/dist/features/composites/vue.d.ts +4 -3
- package/dist/features/composites/vue.js +7 -5
- package/dist/features/composites/vue.js.map +1 -1
- package/dist/features/transfer/components/TransferMenu.js +93 -0
- package/dist/features/transfer/components/TransferMenu.js.map +1 -0
- package/dist/features/transfer/components/TransferMenu.vue.d.ts +15 -0
- package/dist/features/transfer/feature.d.ts +76 -0
- package/dist/features/transfer/feature.js +169 -0
- package/dist/features/transfer/feature.js.map +1 -0
- package/dist/features/transfer/index.d.ts +3 -0
- package/dist/features/transfer/index.js +3 -0
- package/dist/features/transfer/react/TransferMenu.d.ts +12 -0
- package/dist/features/transfer/react/TransferMenu.js +71 -0
- package/dist/features/transfer/react/TransferMenu.js.map +1 -0
- package/dist/features/transfer/react.d.ts +10 -0
- package/dist/features/transfer/react.js +11 -0
- package/dist/features/transfer/react.js.map +1 -0
- package/dist/features/transfer/transforms.d.ts +83 -0
- package/dist/features/transfer/transforms.js +137 -0
- package/dist/features/transfer/transforms.js.map +1 -0
- package/dist/features/transfer/vue.d.ts +9 -0
- package/dist/features/transfer/vue.js +11 -0
- package/dist/features/transfer/vue.js.map +1 -0
- package/dist/i18n/de/ai.js +4 -0
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/composites.js +21 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/integrations.js +16 -0
- package/dist/i18n/de/integrations.js.map +1 -1
- package/dist/i18n/de/transfer.js +17 -0
- package/dist/i18n/de/transfer.js.map +1 -0
- package/dist/i18n/de/tree.js +7 -0
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +57 -0
- package/dist/i18n/de.js +3 -1
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en/ai.js +4 -0
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/composites.js +21 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/integrations.js +16 -0
- package/dist/i18n/en/integrations.js.map +1 -1
- package/dist/i18n/en/transfer.js +17 -0
- package/dist/i18n/en/transfer.js.map +1 -0
- package/dist/i18n/en/tree.js +7 -0
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/i18n/en.js +3 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.js +1 -1
- package/dist/integrations/composite-ai/composite-summary.js +1 -1
- package/dist/integrations/composite-ai/derive.d.ts +21 -0
- package/dist/integrations/composite-ai/derive.js +69 -47
- package/dist/integrations/composite-ai/derive.js.map +1 -1
- package/dist/integrations/composite-ai/index.d.ts +8 -3
- package/dist/integrations/composite-ai/index.js +50 -4
- package/dist/integrations/composite-ai/index.js.map +1 -1
- package/dist/integrations/composite-ai/outline-sanitize.d.ts +48 -0
- package/dist/integrations/composite-ai/outline-sanitize.js +175 -0
- package/dist/integrations/composite-ai/outline-sanitize.js.map +1 -0
- package/dist/integrations/composite-ai/split.d.ts +44 -0
- package/dist/integrations/composite-ai/split.js +187 -0
- package/dist/integrations/composite-ai/split.js.map +1 -0
- package/dist/integrations/composite-ai/transfer-transforms.d.ts +27 -0
- package/dist/integrations/composite-ai/transfer-transforms.js +153 -0
- package/dist/integrations/composite-ai/transfer-transforms.js.map +1 -0
- package/dist/integrations/transcript-ai/protocol.d.ts +13 -0
- package/dist/integrations/transcript-ai/protocol.js +49 -1
- package/dist/integrations/transcript-ai/protocol.js.map +1 -1
- package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +1 -1
- package/dist/nuxt/module.d.ts +114 -0
- package/dist/presets/compose.d.ts +24 -8
- package/dist/presets/compose.js +117 -58
- package/dist/presets/compose.js.map +1 -1
- package/dist/react/compose.js +5 -1
- package/dist/react/compose.js.map +1 -1
- package/dist/react/composed-surface.d.ts +9 -0
- package/dist/react/composed-surface.js +4 -1
- package/dist/react/composed-surface.js.map +1 -1
- package/dist/server/ai.d.ts +2 -5
- package/dist/server/ai.js.map +1 -1
- package/dist/server/handlers/ai.d.ts +4 -75
- package/dist/server/handlers/ai.js +30 -94
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/editor.d.ts +1 -0
- package/dist/server/handlers/index.d.ts +3 -1
- package/dist/server/handlers/index.js +3 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/settings/index.d.ts +8 -0
- package/dist/settings/index.js +16 -4
- package/dist/settings/index.js.map +1 -1
- package/dist/styles/composites.css +41 -0
- package/dist/tree/DocumentTree.js +246 -80
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +8 -0
- package/dist/tree/drop-rules.js.map +1 -1
- package/dist/tree/react/DocumentTree.js +164 -32
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/tree-host.js +41 -2
- package/dist/tree/tree-host.js.map +1 -1
- package/dist/tree/tree-items.js +53 -5
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +122 -10
- package/dist/tree/tree-model.js.map +1 -1
- package/dist/tree/tree-rows.js +20 -7
- package/dist/tree/tree-rows.js.map +1 -1
- package/dist/tree/tree-view-model.d.ts +5 -0
- package/dist/tree/tree-view-model.js +13 -0
- package/dist/tree/tree-view-model.js.map +1 -1
- package/dist/vue/AdvancedEditor.js +6 -0
- package/dist/vue/AdvancedEditor.js.map +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +9 -1
- package/dist/vue/compose.js +5 -1
- package/dist/vue/compose.js.map +1 -1
- package/package.json +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.3.0 - 2026-10-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Kind tone: `CompositeKind.chrome.tone` (a CSS colour or `var(...)`). A Baustein of such a kind gets `data-kind-tone` and `--sk-kind-tone` on its wrapper (Vue and React `CompositeView`), a tinted surface and a left border in the tone (`composites.css`; strength `--sk-kind-tone-mix`, `8%` under `:where(:root)`, `14%` under `:where(.dark)`), and its document-tree rows tint the kind icon (`TreeItem.tone`). Kinds without a tone are unchanged.
|
|
8
|
+
- Merging a Baustein with the next one (`@ecsplico/editor/composites`): the command `mergeCompositeWithNext(pos?)` makes a Baustein absorb the Baustein that directly follows it (body and sub-Bausteine appended; the first keeps title, description, tags, id, kind and meta) in one undo step, refused without a following Baustein, when the first kind does not accept a nested kind, or when read-only. Block action „Mit nächstem zusammenführen“, a merge button on `DocumentTree` rows (Vue and React; `TreeCommand` `{ type: 'mergeBlock', id }`, `TreeModel.canMerge(item)` / `merge(item)`; a host-built `TreeEditor` must now provide `canMerge(id)`), exported `CompositeMerge` and `mergeBody`.
|
|
9
|
+
|
|
10
|
+
## 3.2.0 - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `basketById(baskets, id)` (`/composites`, `/composites/schema`); `basketOf` and `titleHiddenOf` also accept a `CompositeRow`.
|
|
15
|
+
- `clampSplitTargetChars(typed, current)` (`/settings`), shared by both `SettingsEditingTab` files.
|
|
16
|
+
- Block transfer between editor surfaces (`@ecsplico/editor/transfer`, `/transfer/vue`, `/transfer/react`): `transferFeature({ transforms?, defaultTransform? })`, a `BlockDropTarget` extension that accepts a Baustein dragged out of another editor or document tree (`BLOCK_MIME`). A plain drop runs the default transform `copyTransform` (fresh ids on the block and everything inside it, `meta.derivation` with `from: { block, document }`, method `copy`); Shift at the drop opens a menu at the drop point (`TransferMenu`, mounted as an overlay) with the available transforms: `copyTransform`, `summaryTransform` and `bulletsTransform` (exported from `@ecsplico/editor/integrations/composite-ai`, not from `/transfer`: the transfer feature imports no AI code and `requires` `composites`; `composeEditor` adds both to the `transfer` row when `ai` is on, `transferFeature()` alone offers the copy; `TransformContext` and `TransferEnv` carry `app: EditorContext` instead of `modelRef`; new Baustein whose body the model streams in, recorded as a derivation; available only with an AI backend and Markdown, tasks `transfer-summary` / `transfer-bullets`). A host passes its own `BlockTransform` list (`id`, `label`, `available`, `creates`, `run`). Insert guards apply (refusal toast, document unchanged), a drop onto the dragged block's own position in the same document is refused, a same-editor move stays native. Commands `transferBlock({ payload, pos, menu, x?, y? })` and `cancelTransfer()`; `runTransform`, `transferRequest` (pure request-body builder) and `isSelfDrop` are exported. `composeEditor` knows the catalog id `transfer` (in no level list: `interaction: [...INTERACTION.full, 'transfer']`, option `transfer`). `deriveComposite` shares its write-back with the transforms as `writeDerivedBody` (`@ecsplico/editor/integrations/composite-ai`).
|
|
17
|
+
- Document tree: basket filter and focus (`@ecsplico/editor/tree`, Vue and React `DocumentTree`). A paint-bucket button next to the title filter (only with a live editor that offers baskets) shows only the Bausteine of the checked baskets (none checked = all) plus their ancestors, which are dimmed; documents, folders and headings are never hidden. The choice persists under `tip-tree:basket-filter` (`TreeViewModel.basketFilter`, `setBasketShown`, `clearBasketFilter`; `TreeState.basketFilter`, `TreeRowState.dimmed`). Every Baustein row gets a focus button: `TreeModel.focus(key)` / `defocus()` list only the children of the focused row under a breadcrumb (`TreeState.focusKey`, `focusPath`; the first crumb is the document and leaves focus, the others refocus), emit the new `TreeCommand` `{ type: 'focusBlock', id }` after the state changed, and the executor runs `focusComposite(id)` on the editor, so the editor hides everything else. A focused Baustein that disappears (or another editor) resets the focus.
|
|
18
|
+
- Colour baskets and hidden titles on Bausteine. A Baustein can sit in one of the host's baskets (`meta.basket`, an id of `compositeFeature({ baskets })` / `composeEditor({ compositeBaskets })`; default `defaultBaskets(t)`: red, orange, yellow, green, blue, coloured by the `--sk-basket-<id>` CSS variables) and its title can be hidden (`meta.titleHidden`); a hidden title reappears for the rest of the editor session once the caret enters it (`CompositeStorage.revealedTitles`). Both keys are kept by every kind and survive Markdown round trips (`BASKET_KEY`, `TITLE_HIDDEN_KEY`, `keepsMetaKey`). New commands `setCompositeBasket(basket | null, pos?)` (refuses an unknown id, `null` removes the key) and `setCompositeTitleHidden(hidden, pos?)` (showing also ends the reveal); new exports `Basket`, `BASKET_IDS`, `defaultBaskets`, `basketOf`, `titleHiddenOf`, `compositeBasketsOf`; de/en keys `composites.basket.*` and `composites.titleHidden.*`.
|
|
19
|
+
- Block-action placement: `BlockAction.placement` (`'hover' | 'toolbar' | 'both'`, default `'both'`) and `BlockUiOptions.actions` (`'hover'` default, `'toolbar'`, `'both'`, `'none'`) chosen with `blockUiFeature(options)`, `blockUiFeatureCore(ui, options)` or `composeEditor({ blockUi })`. The new toolbar surface shows the actions of the block holding the selection at its corner (`block-actions-toolbar` class; Vue and React); with `'both'` the hover bar yields on the selected block, so one bar shows per block; `'none'` leaves the drag handle and targeting only. Abilities stay permissions: the option only chooses where allowed actions appear. Also exported from `@ecsplico/editor/block-ui`: `selectedBlockOf`, `sameBlock`, `actionSurfacesOf`; `blockActionsFor` takes the `surface` as a fourth argument. `EditorFeature.blockUi` and `EditorEntry.blockUi` carry the options to the overlay. Hosts passing nothing see no change.
|
|
20
|
+
- Editor focus mode on a Baustein (`@ecsplico/editor/composites`): the command `focusComposite(id | null)` hides every sibling of the focused Baustein and of each of its ancestors, so only the Baustein and the titles around it stay (decorations `sk-focus-hidden`, `sk-focus-target`, `sk-focus-ancestor` from the `CompositeFocus` extension; exported `compositeFocusKey`, `compositeFocusOf`, `focusDecorations`). The focus resets by itself when the Baustein is deleted. New block action „Fokussieren“ on every Baustein and a focus chip „Fokus: <Titel> · Aufheben“ (`FocusChip`, Vue and React; `CompositesUi` gains `focusChip`, mounted as an overlay of `compositeFeature`).
|
|
21
|
+
- `CoreContentOptions` (`headings: false | { levels }`, `lists`, `blockquote`, `codeBlock`, `rule`: each `false` to drop it) for the content base: `coreContentFeature(options)`, `coreContentSchemaExtensions(options)` and `composeEditor({ coreContent })`. One options object feeds the editor and the server schema (`starterKitOf` is the shared StarterKit mapping), so they cannot drift; everything stays on by default. A command whose created node type is not in the schema is hidden (`commandState` → `visible: false`) in the toolbar, the `/` menu and the ⌘K palette instead of greyed out; with `headings: { levels }` the heading commands of other levels are hidden.
|
|
22
|
+
- `AdvancedEditor` (Vue and React) and the React `ComposedSurface` take `blockUi` and `coreContent` and pass them to `composeEditor` (read once at setup, like the other composition props).
|
|
23
|
+
- AI splitting and outline cleanup (`@ecsplico/editor/integrations/composite-ai`): the block actions „Mit KI in Unter-Bausteine teilen“ / „Mit KI in Bausteine teilen“ (`splitCompositeWithAi(editor, pos, mode, ai, modelRef, notify, t, { targetChars, defaultKind })`) ask the model (task `split-block`) to insert `##` headings into a Baustein's body and split it at them; the model may only insert headings, so a result whose text minus those headings differs from the original (`bodyUnchanged`) writes nothing and shows a warning. „Gliederung bereinigen (KI)“ (`sanitizeOutline(editor, ai, modelRef, notify, t)`, ⌘K entry and block action on headings; task `outline-sanitize`) sends all headings of the document and applies the repaired outline (no skipped levels, numbering and trailing punctuation removed, empty headings merged into the next) in one undoable step, or nothing when the answer is malformed. The pure parts are exported (`bodyUnchanged`, `splitByInsertedHeadings`, `collectHeadings`, `parseOutlineResponse`, `applyOutline`). Both tasks are in `AI_TASK_IDS` and carry the new `AiTask.programmatic` flag, which keeps tasks that only a block action or command sends out of the AI sidebar.
|
|
24
|
+
- Splitting a Baustein (`@ecsplico/editor/composites`): the commands `splitCompositeAtCaret({ mode })` (in front of the body block holding the caret; refused at the first body block) and `splitCompositeByLength({ mode, targetChars?, pos? })` (parts of about `blocks.splitTargetChars` characters, never cutting inside a block; refused with fewer than two parts) cut a Baustein into sibling Bausteine (`'siblings'`: the original keeps the first part) or sub-Bausteine (`'children'`: of the default kind, else the parent's own kind, refused when neither is accepted). Generated parts get a title from their first sentence and hide it (`meta.titleHidden`). The pure parts are exported (`splitBodyByLength`, `generatedTitle`, `childKindFor`, `splitComposite`, `SplitMode`), the four block actions „Baustein hier teilen“, „In Unter-Bausteine teilen“, „Nach Länge in Unter-Bausteine teilen“ and „Nach Länge in Bausteine teilen“ are offered on Bausteine, and the new setting `blocks.splitTargetChars` (default 1500, 200–20000) has a field in the „Bearbeiten“ tab (Vue and React).
|
|
25
|
+
- Client-safe AI run modules in `@ecsplico/editor/ai/protocol`: `generateBodySchema`, `completeBodySchema` (moved from the server handlers, which keep re-exporting them), `ChatMessage`, `ChatStream`, `GenerateInput`, `generateProblem`, `runGenerate` and `runCompletion`; `runTranscriptCleanup` and `TranscriptCleanupUnreadableError` in the transcript-AI protocol. They take a `ChatStream` and hold the validation, streaming, early-stop and chunk policy, so the server handlers (AI SDK stream) and a browser-direct adapter share one implementation. Server behaviour is unchanged; `ChatMessage` of `@ecsplico/editor/server` is the same type.
|
|
26
|
+
- `@ecsplico/editor/adapters/direct-ai`: `createDirectAi({ presets, slots?, users?, fetch? })`, an `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints (`<endpoint>/chat/completions`, base URL like `PresetConfig.endpoint`) straight from the browser, so `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }` is an editor with AI and no server. It serves `models`, `complete`, `stream('generate')` and `transcriptCleanup` through the shared protocol modules (same prompts, limits and results as the handlers) over a plain `fetch` and a small SSE reader, with no AI SDK in the bundle. A refused model request rejects with a `BackendError` carrying the upstream status before anything streams. Limits: `stream('chat')` (tool calls) and `extract` (transcription, OCR) answer `BackendError` 501 (use the HTTP backend's `ai` for them); the preset keys are readable by the page; the per-user rate limit exists in the server handlers only. Bundle-isolation fixture `adapters-direct-ai`. The guides document the capability locality (`local`, `relay`, `session`) and the new adapter.
|
|
27
|
+
- Block payload for drags between editors and document trees (`@ecsplico/editor/core`): `startBlockDrag` writes the Baustein as `application/x-tip-block` (`BLOCK_MIME`, `BlockTransfer`: document id, block id, kind, title, JSON, Markdown) after `text/plain`; `writeBlockTransfer`, `readBlockTransfer` (null for a missing, unparsable or misshapen payload) and `blockTransferOf(editor, pos)` (Markdown empty without `editor.markdown`). The new `BlockTransferStorage` extension of `coreContentFeature` carries the editor's `documentId` (`editor.storage.blockTransfer`).
|
|
28
|
+
- Foreign Bausteine in the document tree (`@ecsplico/editor/tree`, Vue and React `DocumentTree`): a Baustein dragged in from another editor surface or tree (`BLOCK_MIME`) is accepted on the document root or on a Baustein row in the open document (`DragPayload` `{ type: 'foreign-block' }`, `TreeModel.foreignDragOver(item)`, `TreeModel.dropForeign(item, payload, menu, x, y)`); other targets show the refusal „Hier lassen sich keine Bausteine ablegen“, and an open editor without the transfer feature (no `blockDropTarget` storage or `transferBlock` command: `TreeEditor.canTransfer`, `DropEnv.canTransfer`) refuses every foreign drop (the executor returns `false` instead of throwing, `dropForeign` never rejects). The model emits `TreeCommand` `transferBlock { payload, targetId, menu, x, y }` and the executor runs `editor.commands.transferBlock` at the end of that Baustein (Shift = transform menu). A Baustein row now also writes its block payload (`BLOCK_MIME`) when dragged, so it can be dropped into another surface or tree.
|
|
29
|
+
- Example `examples/vue-two-pane` (`bun run example:two-pane`, port 3047): two document trees over two editors on the bare Nitro server of `vue-nitro-json`, showing nested Bausteine from a Markdown import with AI outline repair, splitting by length, colour baskets with filter and focus in the trees, and dragging Bausteine from one pane into the other with a transform. New guide „Content tree mechanics“ and additions to the composites, document-tree, AI and composition guides and the exports reference.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `--sk-basket-*` now live under `:where(:root)` with dark values under `:where(.dark)` (like `tokens.css`); a host's override now wins at any specificity.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Internal: the composition catalog of `presets/compose` is one table (`CATALOG`, one row per feature: dimension, level lists, builder; its key order is the canonical order). `CONTENT`, `INTERACTION` and `AI` are derived from it with unchanged names and values; `composeEditor` output is identical. Adding a catalog feature is one row, plus a `UiBindings` key and both binding records when the feature is UI-bound.
|
|
38
|
+
- `TreeCommand` gains `focusBlock` and `transferBlock`, and `DragPayload` gains `{ type: 'foreign-block' }`: a host that switches exhaustively over either needs the new cases. `TreeEditor` gains `canTransfer` and `DropEnv` gains `canTransfer`; `TransferDrop` `{ payload, pos, menu, x, y }` is the one type of a drop (`PendingDrop` is `TransferDrop` with `menu` on).
|
|
39
|
+
- `CompositesUi.focusChip` is optional: without it `compositeFeatureCore` mounts no focus chip (the focus mode and its block action stay).
|
|
40
|
+
- `DEFAULT_TRANSFORMS` of `/transfer` is `[copyTransform]`; hosts that built `transferFeature()` by hand and want the AI transforms pass `transforms: [copyTransform, summaryTransform, bulletsTransform]`. `UiBindings` of `presets/compose` gains `transfer-ai`, the AI transforms the layer binds (bound only with `ai` on and no own `transfer.transforms`).
|
|
41
|
+
- `DerivationSource` `{ block }` may carry `document` (the document the source block lives in); the details card reports `stale: null` for a source in another document (it is not looked up here), and the badge shows „aus anderem Dokument“ (`composites.derivation.foreign`, `derivationIsForeign`, `isForeignSource`, `DerivationDetails.foreign`) instead of comparing hashes.
|
|
42
|
+
- A Baustein row of the document tree, and every block dragged with `startBlockDrag`, now writes its block payload (`BLOCK_MIME`) besides `text/plain`, so it can be dropped into another surface or tree. Hosts that read only `text/plain` see no change.
|
|
43
|
+
|
|
44
|
+
|
|
3
45
|
## 3.1.1
|
|
4
46
|
|
|
5
47
|
### Fixed
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ModelSlot } from "../settings/values.js";
|
|
2
|
+
import { EditorBackend } from "../core/backend.js";
|
|
3
|
+
//#region src/adapters/direct-ai.d.ts
|
|
4
|
+
/** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `PresetConfig.endpoint`. */
|
|
5
|
+
export interface DirectAiPreset {
|
|
6
|
+
label: string;
|
|
7
|
+
/** Base URL, e.g. `https://openrouter.ai/api/v1`. */
|
|
8
|
+
endpoint: string;
|
|
9
|
+
model: string;
|
|
10
|
+
/** Sent as `Authorization: Bearer …`. It is part of the page, so anyone who can open the page can read it. */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DirectAiOptions {
|
|
14
|
+
/** The models the editor offers, keyed by preset id. At least one. */
|
|
15
|
+
presets: Record<string, DirectAiPreset>;
|
|
16
|
+
/** Default preset per model slot, answered by `models()`; the first preset for unset slots. */
|
|
17
|
+
slots?: Partial<Record<ModelSlot, string>>;
|
|
18
|
+
/** Usernames offered to tasks with `needsUsers` (`@` mentions); none by default. */
|
|
19
|
+
users?: () => Promise<string[]> | string[];
|
|
20
|
+
/** The `fetch` the model calls go through (default: the platform's). */
|
|
21
|
+
fetch?: typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* An `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints from the browser: `models`,
|
|
25
|
+
* `complete`, `stream('generate')` and `transcriptCleanup`, with the protocol modules the server handlers use.
|
|
26
|
+
* `stream('chat')` and `extract` need a server (tool calls, transcription, OCR) and fail with `BackendError` 501; a
|
|
27
|
+
* host that needs them takes `ai` from `createHttpBackend()` instead.
|
|
28
|
+
*
|
|
29
|
+
* The keys in `presets` are part of the page: whoever can open it can read them, and the per-user rate limit of the
|
|
30
|
+
* server routes does not apply. Use it for local models, tokens the user brings or scoped keys, and keep keys that
|
|
31
|
+
* must stay secret behind the server routes. Combine it with the memory backend:
|
|
32
|
+
* `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createDirectAi(options: DirectAiOptions): NonNullable<EditorBackend['ai']>;
|
|
35
|
+
//#endregion
|
|
36
|
+
//# sourceMappingURL=direct-ai.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { BackendError } from "../core/backend.js";
|
|
2
|
+
import { completeBodySchema, runCompletion } from "../features/ai/protocol/complete.js";
|
|
3
|
+
import { MODEL_SLOTS } from "../settings/values.js";
|
|
4
|
+
import { AI_TASKS } from "../features/ai/protocol/tasks.js";
|
|
5
|
+
import { generateBodySchema, generateProblem, runGenerate } from "../features/ai/protocol/generate.js";
|
|
6
|
+
import { TranscriptCleanupUnreadableError, runTranscriptCleanup, transcriptCleanupRequestSchema } from "../integrations/transcript-ai/protocol.js";
|
|
7
|
+
//#region src/adapters/direct-ai.ts
|
|
8
|
+
/** The most characters of an upstream error body that go into the error message. */
|
|
9
|
+
const MAX_ERROR_DETAIL = 300;
|
|
10
|
+
/** Parses `body` with `schema`; a mismatch is a 400 like the server's validation error. */
|
|
11
|
+
function parseBody(schema, body) {
|
|
12
|
+
const parsed = schema.safeParse(body);
|
|
13
|
+
if (parsed.success) return parsed.data;
|
|
14
|
+
const [issue] = parsed.error.issues;
|
|
15
|
+
throw new BackendError(`Ungültige Anfrage${issue ? `: ${[...issue.path, issue.message].join(" ")}` : ""}`, 400, { cause: parsed.error });
|
|
16
|
+
}
|
|
17
|
+
/** The `data:` payloads of an SSE body, in order; stops at the end of the body. */
|
|
18
|
+
async function* sseData(body) {
|
|
19
|
+
const reader = body.getReader();
|
|
20
|
+
const decoder = new TextDecoder();
|
|
21
|
+
let buffer = "";
|
|
22
|
+
try {
|
|
23
|
+
while (true) {
|
|
24
|
+
const { value, done } = await reader.read();
|
|
25
|
+
if (done) break;
|
|
26
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27
|
+
let newline = buffer.indexOf("\n");
|
|
28
|
+
while (newline !== -1) {
|
|
29
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
30
|
+
buffer = buffer.slice(newline + 1);
|
|
31
|
+
if (line.startsWith("data:")) yield line.slice(5).trim();
|
|
32
|
+
newline = buffer.indexOf("\n");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const rest = (buffer + decoder.decode()).trim();
|
|
36
|
+
if (rest.startsWith("data:")) yield rest.slice(5).trim();
|
|
37
|
+
} finally {
|
|
38
|
+
await reader.cancel().catch(() => void 0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Streams the text deltas of a chat completion from `<endpoint>/chat/completions`. A refused request (non-2xx, or the
|
|
43
|
+
* endpoint unreachable) throws a `BackendError` before anything is yielded, with the upstream status (502 when there
|
|
44
|
+
* was none); an error inside the stream throws a plain `Error`.
|
|
45
|
+
*/
|
|
46
|
+
async function* streamChat(send, { endpoint, model, apiKey }, messages, signal) {
|
|
47
|
+
let response;
|
|
48
|
+
try {
|
|
49
|
+
response = await send(`${endpoint.replace(/\/+$/, "")}/chat/completions`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
"Accept": "text/event-stream",
|
|
54
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model,
|
|
58
|
+
messages,
|
|
59
|
+
stream: true
|
|
60
|
+
}),
|
|
61
|
+
signal
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (signal.aborted) throw error;
|
|
65
|
+
throw new BackendError(`KI-Endpunkt nicht erreichbar: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), 502, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const detail = (await response.text().catch(() => "")).slice(0, MAX_ERROR_DETAIL);
|
|
69
|
+
throw new BackendError(`KI-Endpunkt: ${[response.status, detail || response.statusText].filter(Boolean).join(" ")}`, response.status);
|
|
70
|
+
}
|
|
71
|
+
if (!response.body) throw new BackendError("KI-Endpunkt: leere Antwort", 502);
|
|
72
|
+
for await (const data of sseData(response.body)) {
|
|
73
|
+
if (data === "[DONE]") return;
|
|
74
|
+
let chunk;
|
|
75
|
+
try {
|
|
76
|
+
chunk = JSON.parse(data);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (typeof chunk !== "object" || chunk === null) continue;
|
|
81
|
+
if (chunk.error) {
|
|
82
|
+
const message = typeof chunk.error === "string" ? chunk.error : chunk.error.message;
|
|
83
|
+
throw new Error(typeof message === "string" && message ? message : "KI-Endpunkt: Fehler im Stream");
|
|
84
|
+
}
|
|
85
|
+
const text = chunk.choices?.[0]?.delta?.content;
|
|
86
|
+
if (typeof text === "string" && text) yield text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* An `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints from the browser: `models`,
|
|
91
|
+
* `complete`, `stream('generate')` and `transcriptCleanup`, with the protocol modules the server handlers use.
|
|
92
|
+
* `stream('chat')` and `extract` need a server (tool calls, transcription, OCR) and fail with `BackendError` 501; a
|
|
93
|
+
* host that needs them takes `ai` from `createHttpBackend()` instead.
|
|
94
|
+
*
|
|
95
|
+
* The keys in `presets` are part of the page: whoever can open it can read them, and the per-user rate limit of the
|
|
96
|
+
* server routes does not apply. Use it for local models, tokens the user brings or scoped keys, and keep keys that
|
|
97
|
+
* must stay secret behind the server routes. Combine it with the memory backend:
|
|
98
|
+
* `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.
|
|
99
|
+
*/
|
|
100
|
+
function createDirectAi(options) {
|
|
101
|
+
const ids = Object.keys(options.presets);
|
|
102
|
+
if (!ids.length) throw new Error("createDirectAi: presets is empty");
|
|
103
|
+
const send = options.fetch ?? fetch;
|
|
104
|
+
const resolve = (ref) => {
|
|
105
|
+
if (!("preset" in ref)) return ref;
|
|
106
|
+
const preset = Object.hasOwn(options.presets, ref.preset) ? options.presets[ref.preset] : void 0;
|
|
107
|
+
if (!preset) throw new BackendError("Unbekanntes Modell-Preset", 400);
|
|
108
|
+
return preset;
|
|
109
|
+
};
|
|
110
|
+
const chatOn = (ref) => {
|
|
111
|
+
const model = resolve(ref);
|
|
112
|
+
return (messages, signal) => streamChat(send, model, messages, signal);
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
models: () => Promise.resolve({
|
|
116
|
+
presets: Object.entries(options.presets).map(([id, { label, endpoint, model }]) => ({
|
|
117
|
+
id,
|
|
118
|
+
label,
|
|
119
|
+
endpoint,
|
|
120
|
+
model,
|
|
121
|
+
source: "preset"
|
|
122
|
+
})),
|
|
123
|
+
slots: Object.fromEntries(MODEL_SLOTS.map((slot) => [slot, options.slots?.[slot] ?? ids[0]]))
|
|
124
|
+
}),
|
|
125
|
+
complete: async (body, signal) => {
|
|
126
|
+
const { before, model } = parseBody(completeBodySchema, body);
|
|
127
|
+
return { text: await runCompletion(before, chatOn(model), signal) };
|
|
128
|
+
},
|
|
129
|
+
async *stream(path, body, signal) {
|
|
130
|
+
if (path === "chat") throw new BackendError("Chat braucht einen Server (direct-ai)", 501);
|
|
131
|
+
const { task: taskId, model, ...input } = parseBody(generateBodySchema, body);
|
|
132
|
+
const stream = chatOn(model);
|
|
133
|
+
const task = AI_TASKS[taskId];
|
|
134
|
+
const problem = generateProblem(task, input);
|
|
135
|
+
if (problem) throw new BackendError(problem, 400);
|
|
136
|
+
const users = task.needsUsers ? await options.users?.() : void 0;
|
|
137
|
+
let refused;
|
|
138
|
+
let started = false;
|
|
139
|
+
const guarded = async function* (messages, streamSignal) {
|
|
140
|
+
try {
|
|
141
|
+
for await (const text of stream(messages, streamSignal)) {
|
|
142
|
+
started = true;
|
|
143
|
+
yield text;
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (!started && error instanceof BackendError) refused = error;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
for await (const event of runGenerate(task, input, users, guarded, signal)) {
|
|
151
|
+
if (refused && !signal.aborted) throw refused;
|
|
152
|
+
yield event;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
transcriptCleanup: async (body, signal) => {
|
|
156
|
+
const { segments, model } = parseBody(transcriptCleanupRequestSchema, body);
|
|
157
|
+
try {
|
|
158
|
+
return await runTranscriptCleanup(segments, chatOn(model), signal);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw error instanceof TranscriptCleanupUnreadableError ? new BackendError(error.message, 502, { cause: error }) : error;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
extract: () => Promise.reject(new BackendError("Extraktion braucht einen Server (direct-ai)", 501))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { createDirectAi };
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=direct-ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"direct-ai.js","names":[],"sources":["../../src/adapters/direct-ai.ts"],"sourcesContent":["// i18n-ignore-file: error messages of the browser-direct AI transport (German whatever the editor's language)\nimport type { z } from 'zod'\nimport { BackendError, type EditorBackend } from '../core/backend'\nimport type { ModelProfile, ModelRef } from '../core/backend-types'\nimport { completeBodySchema, runCompletion } from '../features/ai/protocol/complete'\nimport { type ChatMessage, type ChatStream, generateBodySchema, generateProblem, runGenerate } from '../features/ai/protocol/generate'\nimport { AI_TASKS } from '../features/ai/protocol/tasks'\nimport { runTranscriptCleanup, TranscriptCleanupUnreadableError, transcriptCleanupRequestSchema } from '../integrations/transcript-ai/protocol'\nimport { MODEL_SLOTS, type ModelSlot } from '../settings/values'\n\n// The browser-direct adapter of `EditorBackend['ai']`: the model calls of `generate`, `complete`, `models` and\n// `transcriptCleanup` go from the page straight to an OpenAI-compatible chat-completions endpoint, with no `/api/ai`\n// route in between. It runs the same protocol modules the server handlers run (`runGenerate`, `runCompletion`,\n// `runTranscriptCleanup`), so prompts, limits and results are identical. What it cannot do without a server (the\n// document chat's tool calls, `extract`'s transcription and OCR) answers 501.\n\n/** A model the browser calls directly: `endpoint` is the OpenAI-compatible base URL (`<endpoint>/chat/completions` is called), like `PresetConfig.endpoint`. */\nexport interface DirectAiPreset {\n label: string\n /** Base URL, e.g. `https://openrouter.ai/api/v1`. */\n endpoint: string\n model: string\n /** Sent as `Authorization: Bearer …`. It is part of the page, so anyone who can open the page can read it. */\n apiKey?: string\n}\n\nexport interface DirectAiOptions {\n /** The models the editor offers, keyed by preset id. At least one. */\n presets: Record<string, DirectAiPreset>\n /** Default preset per model slot, answered by `models()`; the first preset for unset slots. */\n slots?: Partial<Record<ModelSlot, string>>\n /** Usernames offered to tasks with `needsUsers` (`@` mentions); none by default. */\n users?: () => Promise<string[]> | string[]\n /** The `fetch` the model calls go through (default: the platform's). */\n fetch?: typeof fetch\n}\n\n/** The most characters of an upstream error body that go into the error message. */\nconst MAX_ERROR_DETAIL = 300\n\n/** Parses `body` with `schema`; a mismatch is a 400 like the server's validation error. */\nfunction parseBody<S extends z.ZodType>(schema: S, body: unknown): z.infer<S> {\n const parsed = schema.safeParse(body)\n if (parsed.success) return parsed.data\n const [issue] = parsed.error.issues\n throw new BackendError(`Ungültige Anfrage${issue ? `: ${[...issue.path, issue.message].join(' ')}` : ''}`, 400, { cause: parsed.error })\n}\n\n/** The `data:` payloads of an SSE body, in order; stops at the end of the body. */\nasync function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const reader = 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 })\n let newline = buffer.indexOf('\\n')\n while (newline !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '')\n buffer = buffer.slice(newline + 1)\n if (line.startsWith('data:')) yield line.slice(5).trim()\n newline = buffer.indexOf('\\n')\n }\n }\n const rest = (buffer + decoder.decode()).trim()\n if (rest.startsWith('data:')) yield rest.slice(5).trim()\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n}\n\n/** One chunk of a chat-completions stream: the text delta, or a provider error sent in the stream. */\ninterface CompletionChunk { choices?: { delta?: { content?: unknown } }[], error?: { message?: unknown } | string }\n\n/**\n * Streams the text deltas of a chat completion from `<endpoint>/chat/completions`. A refused request (non-2xx, or the\n * endpoint unreachable) throws a `BackendError` before anything is yielded, with the upstream status (502 when there\n * was none); an error inside the stream throws a plain `Error`.\n */\nasync function* streamChat(send: typeof fetch, { endpoint, model, apiKey }: { endpoint: string, model: string, apiKey?: string }, messages: ChatMessage[], signal: AbortSignal): AsyncGenerator<string> {\n let response: Response\n try {\n response = await send(`${endpoint.replace(/\\/+$/, '')}/chat/completions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },\n body: JSON.stringify({ model, messages, stream: true }),\n signal,\n })\n }\n catch (error) {\n if (signal.aborted) throw error\n throw new BackendError(`KI-Endpunkt nicht erreichbar: ${error instanceof Error ? error.message : String(error)}`.slice(0, 500), 502, { cause: error })\n }\n if (!response.ok) {\n const detail = (await response.text().catch(() => '')).slice(0, MAX_ERROR_DETAIL)\n throw new BackendError(`KI-Endpunkt: ${[response.status, detail || response.statusText].filter(Boolean).join(' ')}`, response.status)\n }\n if (!response.body) throw new BackendError('KI-Endpunkt: leere Antwort', 502)\n\n for await (const data of sseData(response.body)) {\n if (data === '[DONE]') return\n let chunk: CompletionChunk | null\n try {\n chunk = JSON.parse(data) as CompletionChunk | null\n }\n catch {\n continue\n }\n if (typeof chunk !== 'object' || chunk === null) continue\n if (chunk.error) {\n const message = typeof chunk.error === 'string' ? chunk.error : chunk.error.message\n throw new Error(typeof message === 'string' && message ? message : 'KI-Endpunkt: Fehler im Stream')\n }\n const text = chunk.choices?.[0]?.delta?.content\n if (typeof text === 'string' && text) yield text\n }\n}\n\n/**\n * An `EditorBackend['ai']` that calls OpenAI-compatible chat-completion endpoints from the browser: `models`,\n * `complete`, `stream('generate')` and `transcriptCleanup`, with the protocol modules the server handlers use.\n * `stream('chat')` and `extract` need a server (tool calls, transcription, OCR) and fail with `BackendError` 501; a\n * host that needs them takes `ai` from `createHttpBackend()` instead.\n *\n * The keys in `presets` are part of the page: whoever can open it can read them, and the per-user rate limit of the\n * server routes does not apply. Use it for local models, tokens the user brings or scoped keys, and keep keys that\n * must stay secret behind the server routes. Combine it with the memory backend:\n * `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.\n */\nexport function createDirectAi(options: DirectAiOptions): NonNullable<EditorBackend['ai']> {\n const ids = Object.keys(options.presets)\n if (!ids.length) throw new Error('createDirectAi: presets is empty')\n const send = options.fetch ?? fetch\n\n const resolve = (ref: ModelRef) => {\n if (!('preset' in ref)) return ref\n const preset = Object.hasOwn(options.presets, ref.preset) ? options.presets[ref.preset] : undefined\n if (!preset) throw new BackendError('Unbekanntes Modell-Preset', 400)\n return preset\n }\n const chatOn = (ref: ModelRef): ChatStream => {\n const model = resolve(ref)\n return (messages, signal) => streamChat(send, model, messages, signal)\n }\n\n return {\n models: () => Promise.resolve({\n presets: Object.entries(options.presets).map(([id, { label, endpoint, model }]): ModelProfile => ({ id, label, endpoint, model, source: 'preset' })),\n slots: Object.fromEntries(MODEL_SLOTS.map(slot => [slot, options.slots?.[slot] ?? ids[0]!])) as Record<ModelSlot, string>,\n }),\n\n complete: async (body, signal) => {\n const { before, model } = parseBody(completeBodySchema, body)\n return { text: await runCompletion(before, chatOn(model), signal) }\n },\n\n async* stream(path, body, signal) {\n if (path === 'chat') throw new BackendError('Chat braucht einen Server (direct-ai)', 501)\n const { task: taskId, model, ...input } = parseBody(generateBodySchema, body)\n const stream = chatOn(model)\n const task = AI_TASKS[taskId]\n const problem = generateProblem(task, input)\n if (problem) throw new BackendError(problem, 400)\n const users = task.needsUsers ? await options.users?.() : undefined\n\n // `runGenerate` turns a failed model call into an `error` event; a refused request (before the first delta) is\n // rejected instead, like the server's non-2xx answer, so the caller can tell it from a failing stream.\n let refused: BackendError | undefined\n let started = false\n const guarded: ChatStream = async function* (messages, streamSignal) {\n try {\n for await (const text of stream(messages, streamSignal)) {\n started = true\n yield text\n }\n }\n catch (error) {\n if (!started && error instanceof BackendError) refused = error\n throw error\n }\n }\n for await (const event of runGenerate(task, input, users, guarded, signal)) {\n if (refused && !signal.aborted) throw refused\n yield event\n }\n },\n\n transcriptCleanup: async (body, signal) => {\n const { segments, model } = parseBody(transcriptCleanupRequestSchema, body)\n try {\n return await runTranscriptCleanup(segments, chatOn(model), signal)\n }\n catch (error) {\n throw error instanceof TranscriptCleanupUnreadableError ? new BackendError(error.message, 502, { cause: error }) : error\n }\n },\n\n extract: () => Promise.reject(new BackendError('Extraktion braucht einen Server (direct-ai)', 501)),\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","names":[],"sources":["../../src/adapters/memory.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { EditorBackend } from '../core/backend'\nimport type { MentionUser } from '../core/backend-types'\nimport { normalizeTagName } from '../core/tag-name'\nimport type { EditorSettings } from '../settings'\n\n// The in-memory adapter of `EditorBackend` for hosts without a data layer: documents, assets (blob URLs), seeded\n// mention users and tags, and settings that live as long as the page. No AI, themes, collaboration or comments: a\n// feature that needs one of them fails when its surface mounts (see `EditorFeature.backend`)
|
|
1
|
+
{"version":3,"file":"memory.js","names":[],"sources":["../../src/adapters/memory.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { EditorBackend } from '../core/backend'\nimport type { MentionUser } from '../core/backend-types'\nimport { normalizeTagName } from '../core/tag-name'\nimport type { EditorSettings } from '../settings'\n\n// The in-memory adapter of `EditorBackend` for hosts without a data layer: documents, assets (blob URLs), seeded\n// mention users and tags, and settings that live as long as the page. No AI, themes, collaboration or comments: a\n// feature that needs one of them fails when its surface mounts (see `EditorFeature.backend`). A host adds `ai` with\n// `createDirectAi` (`./adapters/direct-ai`): `{ ...createMemoryBackend(), ai: createDirectAi({ presets }) }`.\n\nexport interface MemoryBackendSeed {\n /** Documents by id, as `documents.load` returns them. */\n documents?: Record<string, JSONContent>\n /** The people `@` offers. */\n users?: readonly MentionUser[]\n /** The tags `#` offers (normalized like the server's). */\n tags?: readonly string[]\n}\n\n/** The most suggestions one lookup returns, as the HTTP routes. */\nconst MAX_SUGGESTIONS = 20\n\n/** Prefix matches first, then the other substring matches; the order within each group is kept. */\nfunction prefixFirst<T>(items: readonly T[], needle: string, text: (item: T) => string): T[] {\n const matches = items.filter(item => text(item).includes(needle))\n const prefixed = matches.filter(item => text(item).startsWith(needle))\n return [...prefixed, ...matches.filter(item => !prefixed.includes(item))].slice(0, MAX_SUGGESTIONS)\n}\n\n/**\n * An `EditorBackend` that keeps everything in memory: `documents` (a Map seeded from `seed.documents`; `load` answers a\n * copy, `null` for an unknown id), `assets` (`upload` answers a `blob:` URL of the file, valid for the page's life),\n * `users` and `tags` (case-insensitive lookups over the seed, prefix matches first) and `settings` (`load` answers\n * `null` until something was saved).\n */\nexport function createMemoryBackend(seed: MemoryBackendSeed = {}): EditorBackend {\n const documents = new Map(Object.entries(seed.documents ?? {}).map(([id, content]) => [id, structuredClone(content)]))\n const users = [...(seed.users ?? [])].sort((a, b) => a.username.localeCompare(b.username, 'de'))\n const tags = (seed.tags ?? []).map(normalizeTagName).filter(Boolean)\n let settings: EditorSettings | null = null\n\n return {\n name: 'memory',\n documents: {\n load: id => Promise.resolve(documents.has(id) ? structuredClone(documents.get(id)!) : null),\n save: (id, content) => {\n documents.set(id, structuredClone(content))\n return Promise.resolve()\n },\n },\n assets: {\n upload: (_documentId, file) => Promise.resolve({ url: URL.createObjectURL(file) }),\n },\n users: {\n search: (query) => {\n const needle = query.trim().toLowerCase()\n return Promise.resolve(prefixFirst(users, needle, user => user.username.toLowerCase()).map(({ id, username, color }) => ({ id, username, color })))\n },\n },\n tags: {\n search: (query) => Promise.resolve(prefixFirst(tags, normalizeTagName(query), tag => tag)),\n },\n settings: {\n load: () => Promise.resolve(settings === null ? null : structuredClone(settings)),\n save: (value) => {\n settings = structuredClone(value)\n return Promise.resolve()\n },\n },\n }\n}\n"],"mappings":";;;AAqBA,MAAM,kBAAkB;;AAGxB,SAAS,YAAe,OAAqB,QAAgB,MAAgC;CAC3F,MAAM,UAAU,MAAM,QAAO,SAAQ,KAAK,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC;CAChE,MAAM,WAAW,QAAQ,QAAO,SAAQ,KAAK,IAAI,CAAC,CAAC,WAAW,MAAM,CAAC;CACrE,OAAO,CAAC,GAAG,UAAU,GAAG,QAAQ,QAAO,SAAQ,CAAC,SAAS,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,eAAe;AACpG;;;;;;;AAQA,SAAgB,oBAAoB,OAA0B,CAAC,GAAkB;CAC/E,MAAM,YAAY,IAAI,IAAI,OAAO,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,IAAI,gBAAgB,OAAO,CAAC,CAAC,CAAC;CACrH,MAAM,QAAQ,CAAC,GAAI,KAAK,SAAS,CAAC,CAAE,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,UAAU,IAAI,CAAC;CAC/F,MAAM,QAAQ,KAAK,QAAQ,CAAC,EAAA,CAAG,IAAI,gBAAgB,CAAC,CAAC,OAAO,OAAO;CACnE,IAAI,WAAkC;CAEtC,OAAO;EACL,MAAM;EACN,WAAW;GACT,OAAM,OAAM,QAAQ,QAAQ,UAAU,IAAI,EAAE,IAAI,gBAAgB,UAAU,IAAI,EAAE,CAAE,IAAI,IAAI;GAC1F,OAAO,IAAI,YAAY;IACrB,UAAU,IAAI,IAAI,gBAAgB,OAAO,CAAC;IAC1C,OAAO,QAAQ,QAAQ;GACzB;EACF;EACA,QAAQ,EACN,SAAS,aAAa,SAAS,QAAQ,QAAQ,EAAE,KAAK,IAAI,gBAAgB,IAAI,EAAE,CAAC,EACnF;EACA,OAAO,EACL,SAAS,UAAU;GACjB,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,YAAY;GACxC,OAAO,QAAQ,QAAQ,YAAY,OAAO,SAAQ,SAAQ,KAAK,SAAS,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,UAAU,aAAa;IAAE;IAAI;IAAU;GAAM,EAAE,CAAC;EACpJ,EACF;EACA,MAAM,EACJ,SAAS,UAAU,QAAQ,QAAQ,YAAY,MAAM,iBAAiB,KAAK,IAAG,QAAO,GAAG,CAAC,EAC3F;EACA,UAAU;GACR,YAAY,QAAQ,QAAQ,aAAa,OAAO,OAAO,gBAAgB,QAAQ,CAAC;GAChF,OAAO,UAAU;IACf,WAAW,gBAAgB,KAAK;IAChC,OAAO,QAAQ,QAAQ;GACzB;EACF;CACF;AACF"}
|
|
@@ -19,8 +19,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
19
19
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
20
20
|
onSave?: ((text: string) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
|
-
editable: boolean;
|
|
23
22
|
modelValue: string;
|
|
23
|
+
editable: boolean;
|
|
24
24
|
wordWrap: boolean;
|
|
25
25
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: typeof __VLS_export;
|
package/dist/core/block-drag.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { blockTransferOf, writeBlockTransfer } from "./block-transfer.js";
|
|
1
2
|
import { NodeSelection } from "@tiptap/pm/state";
|
|
2
3
|
//#region src/core/block-drag.ts
|
|
3
4
|
/** The element with every computed style written inline, so it looks the same outside the editor's stylesheet scope. */
|
|
@@ -37,6 +38,8 @@ function startBlockDrag(editor, event, pos) {
|
|
|
37
38
|
document.body.append(preview);
|
|
38
39
|
dataTransfer.clearData();
|
|
39
40
|
dataTransfer.setData("text/plain", node.textContent);
|
|
41
|
+
const transfer = blockTransferOf(editor, pos);
|
|
42
|
+
if (transfer) writeBlockTransfer(dataTransfer, transfer);
|
|
40
43
|
dataTransfer.effectAllowed = "copyMove";
|
|
41
44
|
dataTransfer.setDragImage(preview, direction === "rtl" ? preview.getBoundingClientRect().width : 0, 0);
|
|
42
45
|
const dragging = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-drag.js","names":[],"sources":["../../src/core/block-drag.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { NodeSelection } from '@tiptap/pm/state'\n\n/** The element with every computed style written inline, so it looks the same outside the editor's stylesheet scope. */\nfunction cloneWithStyles(source: HTMLElement): HTMLElement {\n const clone = source.cloneNode(true) as HTMLElement\n const from = [source, ...source.getElementsByTagName('*')]\n const to = [clone, ...clone.getElementsByTagName('*')] as HTMLElement[]\n from.forEach((element, index) => {\n const style = getComputedStyle(element)\n const target = to[index]\n if (target) target.style.cssText = Array.from(style, name => `${name}:${style.getPropertyValue(name)};`).join('')\n })\n return clone\n}\n\n/**\n * Starts a drag of the block at `pos` from a drag source outside the editor's DOM (the kind icon of a composite, a\n * permanent drag handle), the way the upstream drag handle does it: the block becomes the `NodeSelection`, the drag\n * image is a styled clone of the block, and `view.dragging` tells ProseMirror that dropping inside the editor moves the\n * node (`Alt` copies it). Call it from the source's `dragstart`; returns false, and starts nothing, when the editor is\n * read-only or `pos` holds no selectable block.\n */\nexport function startBlockDrag(editor: Editor, event: DragEvent, pos: number): boolean {\n const { view } = editor\n const { dataTransfer } = event\n const node = view.state.doc.nodeAt(pos)\n const dom = view.nodeDOM(pos)\n if (!editor.isEditable || !dataTransfer || !node || !NodeSelection.isSelectable(node) || !(dom instanceof HTMLElement)) return false\n\n const selection = NodeSelection.create(view.state.doc, pos)\n const direction = getComputedStyle(dom).direction\n const clone = cloneWithStyles(dom)\n clone.style.margin = '0'\n const preview = document.createElement('div')\n preview.setAttribute('dir', direction)\n preview.style.position = 'absolute'\n preview.style.top = '-10000px'\n preview.append(clone)\n document.body.append(preview)\n\n dataTransfer.clearData()\n dataTransfer.setData('text/plain', node.textContent)\n dataTransfer.effectAllowed = 'copyMove'\n dataTransfer.setDragImage(preview, direction === 'rtl' ? preview.getBoundingClientRect().width : 0, 0)\n\n // `node` is read by ProseMirror's drop handler to delete exactly the dragged node, but is not in the public type.\n const dragging = { slice: selection.content(), move: true, node: selection } as typeof view.dragging\n view.dragging = dragging\n view.dispatch(view.state.tr.setSelection(selection))\n\n // The source may be gone after a drop (its overlay re-renders), and then sends no `dragend`: listen on the document.\n const end = () => {\n preview.remove()\n if (view.dragging === dragging) view.dragging = null\n document.removeEventListener('drop', end)\n document.removeEventListener('dragend', end)\n }\n document.addEventListener('drop', end)\n document.addEventListener('dragend', end)\n return true\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"block-drag.js","names":[],"sources":["../../src/core/block-drag.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { NodeSelection } from '@tiptap/pm/state'\nimport { blockTransferOf, writeBlockTransfer } from './block-transfer'\n\n/** The element with every computed style written inline, so it looks the same outside the editor's stylesheet scope. */\nfunction cloneWithStyles(source: HTMLElement): HTMLElement {\n const clone = source.cloneNode(true) as HTMLElement\n const from = [source, ...source.getElementsByTagName('*')]\n const to = [clone, ...clone.getElementsByTagName('*')] as HTMLElement[]\n from.forEach((element, index) => {\n const style = getComputedStyle(element)\n const target = to[index]\n if (target) target.style.cssText = Array.from(style, name => `${name}:${style.getPropertyValue(name)};`).join('')\n })\n return clone\n}\n\n/**\n * Starts a drag of the block at `pos` from a drag source outside the editor's DOM (the kind icon of a composite, a\n * permanent drag handle), the way the upstream drag handle does it: the block becomes the `NodeSelection`, the drag\n * image is a styled clone of the block, and `view.dragging` tells ProseMirror that dropping inside the editor moves the\n * node (`Alt` copies it). Call it from the source's `dragstart`; returns false, and starts nothing, when the editor is\n * read-only or `pos` holds no selectable block.\n */\nexport function startBlockDrag(editor: Editor, event: DragEvent, pos: number): boolean {\n const { view } = editor\n const { dataTransfer } = event\n const node = view.state.doc.nodeAt(pos)\n const dom = view.nodeDOM(pos)\n if (!editor.isEditable || !dataTransfer || !node || !NodeSelection.isSelectable(node) || !(dom instanceof HTMLElement)) return false\n\n const selection = NodeSelection.create(view.state.doc, pos)\n const direction = getComputedStyle(dom).direction\n const clone = cloneWithStyles(dom)\n clone.style.margin = '0'\n const preview = document.createElement('div')\n preview.setAttribute('dir', direction)\n preview.style.position = 'absolute'\n preview.style.top = '-10000px'\n preview.append(clone)\n document.body.append(preview)\n\n dataTransfer.clearData()\n dataTransfer.setData('text/plain', node.textContent)\n const transfer = blockTransferOf(editor, pos)\n if (transfer) writeBlockTransfer(dataTransfer, transfer)\n dataTransfer.effectAllowed = 'copyMove'\n dataTransfer.setDragImage(preview, direction === 'rtl' ? preview.getBoundingClientRect().width : 0, 0)\n\n // `node` is read by ProseMirror's drop handler to delete exactly the dragged node, but is not in the public type.\n const dragging = { slice: selection.content(), move: true, node: selection } as typeof view.dragging\n view.dragging = dragging\n view.dispatch(view.state.tr.setSelection(selection))\n\n // The source may be gone after a drop (its overlay re-renders), and then sends no `dragend`: listen on the document.\n const end = () => {\n preview.remove()\n if (view.dragging === dragging) view.dragging = null\n document.removeEventListener('drop', end)\n document.removeEventListener('dragend', end)\n }\n document.addEventListener('drop', end)\n document.addEventListener('dragend', end)\n return true\n}\n"],"mappings":";;;;AAKA,SAAS,gBAAgB,QAAkC;CACzD,MAAM,QAAQ,OAAO,UAAU,IAAI;CACnC,MAAM,OAAO,CAAC,QAAQ,GAAG,OAAO,qBAAqB,GAAG,CAAC;CACzD,MAAM,KAAK,CAAC,OAAO,GAAG,MAAM,qBAAqB,GAAG,CAAC;CACrD,KAAK,SAAS,SAAS,UAAU;EAC/B,MAAM,QAAQ,iBAAiB,OAAO;EACtC,MAAM,SAAS,GAAG;EAClB,IAAI,QAAQ,OAAO,MAAM,UAAU,MAAM,KAAK,QAAO,SAAQ,GAAG,KAAK,GAAG,MAAM,iBAAiB,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE;CAClH,CAAC;CACD,OAAO;AACT;;;;;;;;AASA,SAAgB,eAAe,QAAgB,OAAkB,KAAsB;CACrF,MAAM,EAAE,SAAS;CACjB,MAAM,EAAE,iBAAiB;CACzB,MAAM,OAAO,KAAK,MAAM,IAAI,OAAO,GAAG;CACtC,MAAM,MAAM,KAAK,QAAQ,GAAG;CAC5B,IAAI,CAAC,OAAO,cAAc,CAAC,gBAAgB,CAAC,QAAQ,CAAC,cAAc,aAAa,IAAI,KAAK,EAAE,eAAe,cAAc,OAAO;CAE/H,MAAM,YAAY,cAAc,OAAO,KAAK,MAAM,KAAK,GAAG;CAC1D,MAAM,YAAY,iBAAiB,GAAG,CAAC,CAAC;CACxC,MAAM,QAAQ,gBAAgB,GAAG;CACjC,MAAM,MAAM,SAAS;CACrB,MAAM,UAAU,SAAS,cAAc,KAAK;CAC5C,QAAQ,aAAa,OAAO,SAAS;CACrC,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,MAAM;CACpB,QAAQ,OAAO,KAAK;CACpB,SAAS,KAAK,OAAO,OAAO;CAE5B,aAAa,UAAU;CACvB,aAAa,QAAQ,cAAc,KAAK,WAAW;CACnD,MAAM,WAAW,gBAAgB,QAAQ,GAAG;CAC5C,IAAI,UAAU,mBAAmB,cAAc,QAAQ;CACvD,aAAa,gBAAgB;CAC7B,aAAa,aAAa,SAAS,cAAc,QAAQ,QAAQ,sBAAsB,CAAC,CAAC,QAAQ,GAAG,CAAC;CAGrG,MAAM,WAAW;EAAE,OAAO,UAAU,QAAQ;EAAG,MAAM;EAAM,MAAM;CAAU;CAC3E,KAAK,WAAW;CAChB,KAAK,SAAS,KAAK,MAAM,GAAG,aAAa,SAAS,CAAC;CAGnD,MAAM,YAAY;EAChB,QAAQ,OAAO;EACf,IAAI,KAAK,aAAa,UAAU,KAAK,WAAW;EAChD,SAAS,oBAAoB,QAAQ,GAAG;EACxC,SAAS,oBAAoB,WAAW,GAAG;CAC7C;CACA,SAAS,iBAAiB,QAAQ,GAAG;CACrC,SAAS,iBAAiB,WAAW,GAAG;CACxC,OAAO;AACT"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Editor, Extension, JSONContent } from "@tiptap/core";
|
|
2
|
+
//#region src/core/block-transfer.d.ts
|
|
3
|
+
/** MIME type of the block payload a drag carries next to its plain text; another editor surface or document tree reads it on drop. */
|
|
4
|
+
export declare const BLOCK_MIME = "application/x-tip-block";
|
|
5
|
+
/** A block as it travels between editor surfaces and document trees. */
|
|
6
|
+
export interface BlockTransfer {
|
|
7
|
+
/** Document the block lives in; `null` when its editor has none. */
|
|
8
|
+
documentId: string | null;
|
|
9
|
+
blockId: string | null;
|
|
10
|
+
/** Composite kind id; `null` for every other block. */
|
|
11
|
+
kind: string | null;
|
|
12
|
+
/** Composite: its title text; other blocks: the first 80 characters of their text. */
|
|
13
|
+
title: string;
|
|
14
|
+
json: JSONContent;
|
|
15
|
+
/** The block as Markdown; empty when the editor has no Markdown serializer. */
|
|
16
|
+
markdown: string;
|
|
17
|
+
}
|
|
18
|
+
/** The minimal slice of `DataTransfer` the payload needs (a plain object in tests). */
|
|
19
|
+
type TransferData = Pick<DataTransfer, 'setData' | 'getData' | 'types'>;
|
|
20
|
+
/** Writes `payload` under `BLOCK_MIME`. */
|
|
21
|
+
export declare function writeBlockTransfer(dataTransfer: Pick<DataTransfer, 'setData'>, payload: BlockTransfer): void;
|
|
22
|
+
/** The block payload of a drag, or `null` when it carries none or one that does not parse or has the wrong shape. */
|
|
23
|
+
export declare function readBlockTransfer(dataTransfer: Partial<TransferData>): BlockTransfer | null;
|
|
24
|
+
/**
|
|
25
|
+
* The payload of the block whose own position is `pos`; `null` when `pos` holds no block. The Markdown is empty when
|
|
26
|
+
* `editor.markdown` is unavailable (a headless editor without `DocumentMarkdown`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function blockTransferOf(editor: Editor, pos: number): BlockTransfer | null;
|
|
29
|
+
export interface BlockTransferStorageData {
|
|
30
|
+
documentId: string | null;
|
|
31
|
+
}
|
|
32
|
+
declare module '@tiptap/core' {
|
|
33
|
+
interface Storage {
|
|
34
|
+
/** Absent in an editor without the `BlockTransferStorage` extension. */
|
|
35
|
+
blockTransfer?: BlockTransferStorageData;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Carries the editor's document id so `blockTransferOf` can name where a block comes from (`editor.storage.blockTransfer`). */
|
|
39
|
+
export declare const BlockTransferStorage: Extension<{
|
|
40
|
+
documentId: string | null;
|
|
41
|
+
}, BlockTransferStorageData>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=block-transfer.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { blockIdOf } from "./schema/block-ids.js";
|
|
2
|
+
import { Extension } from "@tiptap/core";
|
|
3
|
+
//#region src/core/block-transfer.ts
|
|
4
|
+
/** MIME type of the block payload a drag carries next to its plain text; another editor surface or document tree reads it on drop. */
|
|
5
|
+
const BLOCK_MIME = "application/x-tip-block";
|
|
6
|
+
const TITLE_LENGTH = 80;
|
|
7
|
+
/** Writes `payload` under `BLOCK_MIME`. */
|
|
8
|
+
function writeBlockTransfer(dataTransfer, payload) {
|
|
9
|
+
dataTransfer.setData(BLOCK_MIME, JSON.stringify(payload));
|
|
10
|
+
}
|
|
11
|
+
const isStringOrNull = (value) => value === null || typeof value === "string";
|
|
12
|
+
/** The block payload of a drag, or `null` when it carries none or one that does not parse or has the wrong shape. */
|
|
13
|
+
function readBlockTransfer(dataTransfer) {
|
|
14
|
+
if (!dataTransfer.types || !Array.from(dataTransfer.types).includes("application/x-tip-block")) return null;
|
|
15
|
+
let value;
|
|
16
|
+
try {
|
|
17
|
+
value = JSON.parse(dataTransfer.getData?.("application/x-tip-block") ?? "");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== "object" || value === null) return null;
|
|
22
|
+
const { documentId, blockId, kind, title, json, markdown } = value;
|
|
23
|
+
if (!isStringOrNull(documentId) || !isStringOrNull(blockId) || !isStringOrNull(kind) || typeof title !== "string" || typeof markdown !== "string") return null;
|
|
24
|
+
if (typeof json !== "object" || json === null || Array.isArray(json) || typeof json.type !== "string") return null;
|
|
25
|
+
return {
|
|
26
|
+
documentId,
|
|
27
|
+
blockId,
|
|
28
|
+
kind,
|
|
29
|
+
title,
|
|
30
|
+
json,
|
|
31
|
+
markdown
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The payload of the block whose own position is `pos`; `null` when `pos` holds no block. The Markdown is empty when
|
|
36
|
+
* `editor.markdown` is unavailable (a headless editor without `DocumentMarkdown`).
|
|
37
|
+
*/
|
|
38
|
+
function blockTransferOf(editor, pos) {
|
|
39
|
+
if (pos < 0 || pos > editor.state.doc.content.size) return null;
|
|
40
|
+
const node = editor.state.doc.nodeAt(pos);
|
|
41
|
+
if (!node || !node.isBlock) return null;
|
|
42
|
+
const json = node.toJSON();
|
|
43
|
+
const composite = node.type.name === "composite";
|
|
44
|
+
const kind = composite ? node.attrs.kind : null;
|
|
45
|
+
return {
|
|
46
|
+
documentId: editor.storage.blockTransfer?.documentId ?? null,
|
|
47
|
+
blockId: blockIdOf(node),
|
|
48
|
+
kind: typeof kind === "string" ? kind : null,
|
|
49
|
+
title: composite ? node.firstChild?.textContent ?? "" : node.textContent.slice(0, TITLE_LENGTH),
|
|
50
|
+
json,
|
|
51
|
+
markdown: editor.markdown?.serialize({
|
|
52
|
+
type: "doc",
|
|
53
|
+
content: [json]
|
|
54
|
+
}) ?? ""
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Carries the editor's document id so `blockTransferOf` can name where a block comes from (`editor.storage.blockTransfer`). */
|
|
58
|
+
const BlockTransferStorage = Extension.create({
|
|
59
|
+
name: "blockTransfer",
|
|
60
|
+
addOptions: () => ({ documentId: null }),
|
|
61
|
+
addStorage() {
|
|
62
|
+
return { documentId: this.options.documentId };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
//#endregion
|
|
66
|
+
export { BLOCK_MIME, BlockTransferStorage, blockTransferOf, readBlockTransfer, writeBlockTransfer };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=block-transfer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-transfer.js","names":[],"sources":["../../src/core/block-transfer.ts"],"sourcesContent":["import { type Editor, Extension, type JSONContent } from '@tiptap/core'\nimport { blockIdOf } from './schema/block-ids'\n\n/** MIME type of the block payload a drag carries next to its plain text; another editor surface or document tree reads it on drop. */\nexport const BLOCK_MIME = 'application/x-tip-block'\n\nconst TITLE_LENGTH = 80\n\n/** A block as it travels between editor surfaces and document trees. */\nexport interface BlockTransfer {\n /** Document the block lives in; `null` when its editor has none. */\n documentId: string | null\n blockId: string | null\n /** Composite kind id; `null` for every other block. */\n kind: string | null\n /** Composite: its title text; other blocks: the first 80 characters of their text. */\n title: string\n json: JSONContent\n /** The block as Markdown; empty when the editor has no Markdown serializer. */\n markdown: string\n}\n\n/** The minimal slice of `DataTransfer` the payload needs (a plain object in tests). */\ntype TransferData = Pick<DataTransfer, 'setData' | 'getData' | 'types'>\n\n/** Writes `payload` under `BLOCK_MIME`. */\nexport function writeBlockTransfer(dataTransfer: Pick<DataTransfer, 'setData'>, payload: BlockTransfer): void {\n dataTransfer.setData(BLOCK_MIME, JSON.stringify(payload))\n}\n\nconst isStringOrNull = (value: unknown): value is string | null => value === null || typeof value === 'string'\n\n/** The block payload of a drag, or `null` when it carries none or one that does not parse or has the wrong shape. */\nexport function readBlockTransfer(dataTransfer: Partial<TransferData>): BlockTransfer | null {\n if (!dataTransfer.types || !Array.from(dataTransfer.types).includes(BLOCK_MIME)) return null\n let value: unknown\n try {\n value = JSON.parse(dataTransfer.getData?.(BLOCK_MIME) ?? '')\n }\n catch {\n return null\n }\n if (typeof value !== 'object' || value === null) return null\n const { documentId, blockId, kind, title, json, markdown } = value as Record<string, unknown>\n if (!isStringOrNull(documentId) || !isStringOrNull(blockId) || !isStringOrNull(kind) || typeof title !== 'string' || typeof markdown !== 'string') return null\n if (typeof json !== 'object' || json === null || Array.isArray(json) || typeof (json as { type?: unknown }).type !== 'string') return null\n return { documentId, blockId, kind, title, json: json as JSONContent, markdown }\n}\n\n/**\n * The payload of the block whose own position is `pos`; `null` when `pos` holds no block. The Markdown is empty when\n * `editor.markdown` is unavailable (a headless editor without `DocumentMarkdown`).\n */\nexport function blockTransferOf(editor: Editor, pos: number): BlockTransfer | null {\n // A position outside the document holds no node (`nodeAt` would throw).\n if (pos < 0 || pos > editor.state.doc.content.size) return null\n const node = editor.state.doc.nodeAt(pos)\n if (!node || !node.isBlock) return null\n const json = node.toJSON() as JSONContent\n const composite = node.type.name === 'composite'\n const kind: unknown = composite ? node.attrs.kind : null\n return {\n documentId: editor.storage.blockTransfer?.documentId ?? null,\n blockId: blockIdOf(node),\n kind: typeof kind === 'string' ? kind : null,\n title: composite ? (node.firstChild?.textContent ?? '') : node.textContent.slice(0, TITLE_LENGTH),\n json,\n markdown: editor.markdown?.serialize({ type: 'doc', content: [json] }) ?? '',\n }\n}\n\nexport interface BlockTransferStorageData {\n documentId: string | null\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n /** Absent in an editor without the `BlockTransferStorage` extension. */\n blockTransfer?: BlockTransferStorageData\n }\n}\n\n/** Carries the editor's document id so `blockTransferOf` can name where a block comes from (`editor.storage.blockTransfer`). */\nexport const BlockTransferStorage = Extension.create<{ documentId: string | null }, BlockTransferStorageData>({\n name: 'blockTransfer',\n addOptions: () => ({ documentId: null }),\n addStorage() {\n return { documentId: this.options.documentId }\n },\n})\n"],"mappings":";;;;AAIA,MAAa,aAAa;AAE1B,MAAM,eAAe;;AAoBrB,SAAgB,mBAAmB,cAA6C,SAA8B;CAC5G,aAAa,QAAQ,YAAY,KAAK,UAAU,OAAO,CAAC;AAC1D;AAEA,MAAM,kBAAkB,UAA2C,UAAU,QAAQ,OAAO,UAAU;;AAGtG,SAAgB,kBAAkB,cAA2D;CAC3F,IAAI,CAAC,aAAa,SAAS,CAAC,MAAM,KAAK,aAAa,KAAK,CAAC,CAAC,SAAA,yBAAmB,GAAG,OAAO;CACxF,IAAI;CACJ,IAAI;EACF,QAAQ,KAAK,MAAM,aAAa,UAAA,yBAAoB,KAAK,EAAE;CAC7D,QACM;EACJ,OAAO;CACT;CACA,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,EAAE,YAAY,SAAS,MAAM,OAAO,MAAM,aAAa;CAC7D,IAAI,CAAC,eAAe,UAAU,KAAK,CAAC,eAAe,OAAO,KAAK,CAAC,eAAe,IAAI,KAAK,OAAO,UAAU,YAAY,OAAO,aAAa,UAAU,OAAO;CAC1J,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,KAAK,OAAQ,KAA4B,SAAS,UAAU,OAAO;CACtI,OAAO;EAAE;EAAY;EAAS;EAAM;EAAa;EAAqB;CAAS;AACjF;;;;;AAMA,SAAgB,gBAAgB,QAAgB,KAAmC;CAEjF,IAAI,MAAM,KAAK,MAAM,OAAO,MAAM,IAAI,QAAQ,MAAM,OAAO;CAC3D,MAAM,OAAO,OAAO,MAAM,IAAI,OAAO,GAAG;CACxC,IAAI,CAAC,QAAQ,CAAC,KAAK,SAAS,OAAO;CACnC,MAAM,OAAO,KAAK,OAAO;CACzB,MAAM,YAAY,KAAK,KAAK,SAAS;CACrC,MAAM,OAAgB,YAAY,KAAK,MAAM,OAAO;CACpD,OAAO;EACL,YAAY,OAAO,QAAQ,eAAe,cAAc;EACxD,SAAS,UAAU,IAAI;EACvB,MAAM,OAAO,SAAS,WAAW,OAAO;EACxC,OAAO,YAAa,KAAK,YAAY,eAAe,KAAM,KAAK,YAAY,MAAM,GAAG,YAAY;EAChG;EACA,UAAU,OAAO,UAAU,UAAU;GAAE,MAAM;GAAO,SAAS,CAAC,IAAI;EAAE,CAAC,KAAK;CAC5E;AACF;;AAcA,MAAa,uBAAuB,UAAU,OAAgE;CAC5G,MAAM;CACN,mBAAmB,EAAE,YAAY,KAAK;CACtC,aAAa;EACX,OAAO,EAAE,YAAY,KAAK,QAAQ,WAAW;CAC/C;AACF,CAAC"}
|
package/dist/core/commands.js
CHANGED
|
@@ -56,6 +56,11 @@ function canCreateAt(editor, $pos, creates) {
|
|
|
56
56
|
* the old state); `can` hooks always read `editor.state`.
|
|
57
57
|
*/
|
|
58
58
|
function commandState(editor, cmd, at, $at) {
|
|
59
|
+
if (cmd.creates && !editor.schema.nodes[cmd.creates.type]) return {
|
|
60
|
+
visible: false,
|
|
61
|
+
enabled: false,
|
|
62
|
+
active: false
|
|
63
|
+
};
|
|
59
64
|
const visible = cmd.visible?.(editor) ?? true;
|
|
60
65
|
if ("component" in cmd) return {
|
|
61
66
|
visible,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","names":[],"sources":["../../src/core/commands.ts"],"sourcesContent":["import type { ChainedCommands, Editor } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\nimport { abilitiesAt, abilitiesAtSelection } from './abilities'\nimport { contributions } from './define-editor'\nimport { type EditorEntry, editorEntryOf } from './editor-ui'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, PlacedToolbarGroup, ToolbarGroupDef } from './command-types'\nimport { byOrder, fail } from './util'\n\nexport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard } from './command-types'\n\n// The guards of an editor are the `insertGuards` of the features of the preset its surface registered\n// (`registerEditor`): looked up by the raw editor instance, built once per entry. An editor without an entry\n// (a headless test editor) has none. Callers holding a framework proxy of the editor pass the raw instance.\nconst guardCache = new WeakMap<EditorEntry, readonly InsertGuard[]>()\n\nfunction guardsOf(editor: Editor): readonly InsertGuard[] {\n const entry = editorEntryOf(editor)\n if (!entry) return []\n let guards = guardCache.get(entry)\n if (!guards) {\n guards = contributions(entry.preset, 'insertGuards').flatMap(factory => factory(entry.ctx))\n guardCache.set(entry, guards)\n }\n return guards\n}\n\n/**\n * A command over a command chain: `run` focuses the editor and executes `command`; the command is disabled while\n * `can()` says it is structurally impossible (also an empty undo history). With `creates`, `commandState` additionally\n * requires the node to fit at the caret.\n */\nexport function command(def: Omit<CommandBase, 'can'> & { command: (chain: ChainedCommands) => ChainedCommands }): CommandDef<never> {\n const { command: chainOf, ...rest } = def\n return {\n ...rest,\n can: editor => chainOf(editor.can().chain()).run(),\n run: (editor) => { chainOf(editor.chain().focus()).run() },\n }\n}\n\n/**\n * Whether every registered insert guard accepts `creates` below the ancestor `$parent` stands for (`$parent.parent` is the\n * node that would receive it). The one rule behind both creation (`canCreateAt`) and conversion targets (`convertBlockTo`).\n */\nexport function guardsAccept(editor: Editor, $parent: ResolvedPos, creates: Creates): boolean {\n return guardsOf(editor).every(guard => guard(editor, $parent, creates))\n}\n\n/**\n * Whether a node like `creates` may be inserted at `$pos`: walks from the caret's depth outward to the first ancestor\n * whose content accepts the type at the caret's index, then every registered insert guard must accept it there.\n */\nexport function canCreateAt(editor: Editor, $pos: ResolvedPos, creates: Creates): boolean {\n const type = editor.schema.nodes[creates.type]\n if (!type) return false\n for (let depth = $pos.depth; depth >= 0; depth--) {\n const index = $pos.index(depth)\n if (!$pos.node(depth).canReplaceWith(index, index, type)) continue\n return guardsAccept(editor, $pos.doc.resolve($pos.start(depth)), creates)\n }\n return false\n}\n\n/**\n * The state of `cmd` for the toolbar (`'selection'`: abilities of the selection) or the `/` menu (`'caret'`). `$at`\n * replaces the caret position (the `/` plugin evaluates inside a transaction's `apply`, where `editor.state` is still\n * the old state); `can` hooks always read `editor.state`.\n */\nexport function commandState(editor: Editor, cmd: CommandDef, at: 'selection' | 'caret', $at?: ResolvedPos): CommandState {\n const visible = cmd.visible?.(editor) ?? true\n if ('component' in cmd) return { visible, enabled: true, active: false }\n const active = cmd.active?.(editor) ?? false\n if (active) return { visible, enabled: true, active }\n const $from = $at ?? editor.state.selection.$from\n if (cmd.ability) {\n const abilities = at === 'selection' && !$at ? abilitiesAtSelection(editor) : abilitiesAt(editor, $from)\n if (!abilities.has(cmd.ability)) return { visible, enabled: false, active }\n }\n const fits = (!cmd.can || cmd.can(editor)) && (!cmd.creates || canCreateAt(editor, $from, cmd.creates))\n return { visible, enabled: fits, active }\n}\n\n/** The commands of the preset's features in feature order. Throws when two commands share an id. */\nexport function commandsOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): CommandDef<C>[] {\n return collect(preset, ctx).commands\n}\n\n// The feature each command came from, for error messages; commands of the host's `commands` transformer have none.\nconst ownerOfCommand = new WeakMap<object, string>()\n\n/**\n * Throws when two of `commands` share an id (`CommandBase.id` is unique per editor), naming the declaring features;\n * a command without a known feature is the host's.\n */\nexport function assertUniqueCommandIds<C>(commands: readonly CommandDef<C>[]): void {\n const seen = new Map<string, CommandDef<C>>()\n for (const cmd of commands) {\n const first = seen.get(cmd.id)\n if (first) fail(`command \"${cmd.id}\" is declared by \"${ownerOfCommand.get(first) ?? 'host'}\" and \"${ownerOfCommand.get(cmd) ?? 'host'}\"`)\n seen.set(cmd.id, cmd)\n }\n}\n\n/**\n * Places `commands` into their toolbar `groups` (every declared group, also empty ones, sorted by `order`; a group's\n * commands sorted by `toolbar.order`, stable). `unplaced` receives each command that has no group to go to: no `toolbar`\n * placement, or one naming a group that is not declared; what that means is the caller's policy.\n */\nexport function placeInGroups<C>(groups: readonly ToolbarGroupDef[], commands: readonly CommandDef<C>[], unplaced: (cmd: CommandDef<C>) => void): PlacedToolbarGroup<C>[] {\n const placed = new Map<string, PlacedToolbarGroup<C>>()\n // `defineEditor` guarantees every group id is declared once.\n for (const group of groups) placed.set(group.id, { ...group, items: [] })\n for (const cmd of commands) {\n const group = cmd.toolbar ? placed.get(cmd.toolbar.group) : undefined\n if (group) group.items.push(cmd)\n else unplaced(cmd)\n }\n return [...placed.values()]\n .sort(byOrder)\n .map(group => ({ ...group, items: [...group.items].sort((a, b) => byOrder(a.toolbar || {}, b.toolbar || {})) }))\n}\n\n/** The toolbar groups the features of `preset` declare, in feature order. */\nexport const toolbarGroupsOf = <C>(preset: EditorPreset<C>): ToolbarGroupDef[] => preset.features.flatMap(feature => feature.toolbarGroups ?? [])\n\n/**\n * The toolbar of `preset`: the groups features declare (`toolbarGroups`) with every command that has a `toolbar`\n * placement, groups and commands sorted by `order` (stable). Throws when a command names a group no feature declares\n * or two commands share an id.\n */\nexport function toolbarOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): PlacedToolbarGroup<C>[] {\n const { commands, groups } = collect(preset, ctx)\n return placeInGroups(groups, commands, (cmd) => {\n if (cmd.toolbar) fail(`command \"${cmd.id}\" of \"${ownerOfCommand.get(cmd) ?? '?'}\" names toolbar group \"${cmd.toolbar.group}\" that no feature declares`)\n })\n}\n\nfunction collect<C>(preset: EditorPreset<C>, ctx: FeatureContext) {\n const commands: CommandDef<C>[] = []\n for (const feature of preset.features) {\n for (const cmd of feature.commands?.(ctx) ?? []) {\n commands.push(cmd)\n ownerOfCommand.set(cmd, feature.id)\n }\n }\n assertUniqueCommandIds(commands)\n return { commands, groups: toolbarGroupsOf(preset) }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"commands.js","names":[],"sources":["../../src/core/commands.ts"],"sourcesContent":["import type { ChainedCommands, Editor } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\nimport { abilitiesAt, abilitiesAtSelection } from './abilities'\nimport { contributions } from './define-editor'\nimport { type EditorEntry, editorEntryOf } from './editor-ui'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, PlacedToolbarGroup, ToolbarGroupDef } from './command-types'\nimport { byOrder, fail } from './util'\n\nexport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard } from './command-types'\n\n// The guards of an editor are the `insertGuards` of the features of the preset its surface registered\n// (`registerEditor`): looked up by the raw editor instance, built once per entry. An editor without an entry\n// (a headless test editor) has none. Callers holding a framework proxy of the editor pass the raw instance.\nconst guardCache = new WeakMap<EditorEntry, readonly InsertGuard[]>()\n\nfunction guardsOf(editor: Editor): readonly InsertGuard[] {\n const entry = editorEntryOf(editor)\n if (!entry) return []\n let guards = guardCache.get(entry)\n if (!guards) {\n guards = contributions(entry.preset, 'insertGuards').flatMap(factory => factory(entry.ctx))\n guardCache.set(entry, guards)\n }\n return guards\n}\n\n/**\n * A command over a command chain: `run` focuses the editor and executes `command`; the command is disabled while\n * `can()` says it is structurally impossible (also an empty undo history). With `creates`, `commandState` additionally\n * requires the node to fit at the caret.\n */\nexport function command(def: Omit<CommandBase, 'can'> & { command: (chain: ChainedCommands) => ChainedCommands }): CommandDef<never> {\n const { command: chainOf, ...rest } = def\n return {\n ...rest,\n can: editor => chainOf(editor.can().chain()).run(),\n run: (editor) => { chainOf(editor.chain().focus()).run() },\n }\n}\n\n/**\n * Whether every registered insert guard accepts `creates` below the ancestor `$parent` stands for (`$parent.parent` is the\n * node that would receive it). The one rule behind both creation (`canCreateAt`) and conversion targets (`convertBlockTo`).\n */\nexport function guardsAccept(editor: Editor, $parent: ResolvedPos, creates: Creates): boolean {\n return guardsOf(editor).every(guard => guard(editor, $parent, creates))\n}\n\n/**\n * Whether a node like `creates` may be inserted at `$pos`: walks from the caret's depth outward to the first ancestor\n * whose content accepts the type at the caret's index, then every registered insert guard must accept it there.\n */\nexport function canCreateAt(editor: Editor, $pos: ResolvedPos, creates: Creates): boolean {\n const type = editor.schema.nodes[creates.type]\n if (!type) return false\n for (let depth = $pos.depth; depth >= 0; depth--) {\n const index = $pos.index(depth)\n if (!$pos.node(depth).canReplaceWith(index, index, type)) continue\n return guardsAccept(editor, $pos.doc.resolve($pos.start(depth)), creates)\n }\n return false\n}\n\n/**\n * The state of `cmd` for the toolbar (`'selection'`: abilities of the selection) or the `/` menu (`'caret'`). `$at`\n * replaces the caret position (the `/` plugin evaluates inside a transaction's `apply`, where `editor.state` is still\n * the old state); `can` hooks always read `editor.state`.\n */\nexport function commandState(editor: Editor, cmd: CommandDef, at: 'selection' | 'caret', $at?: ResolvedPos): CommandState {\n // A node type the schema lacks (`coreContent` dropped it) is gone from every surface, not greyed out.\n if (cmd.creates && !editor.schema.nodes[cmd.creates.type]) return { visible: false, enabled: false, active: false }\n const visible = cmd.visible?.(editor) ?? true\n if ('component' in cmd) return { visible, enabled: true, active: false }\n const active = cmd.active?.(editor) ?? false\n if (active) return { visible, enabled: true, active }\n const $from = $at ?? editor.state.selection.$from\n if (cmd.ability) {\n const abilities = at === 'selection' && !$at ? abilitiesAtSelection(editor) : abilitiesAt(editor, $from)\n if (!abilities.has(cmd.ability)) return { visible, enabled: false, active }\n }\n const fits = (!cmd.can || cmd.can(editor)) && (!cmd.creates || canCreateAt(editor, $from, cmd.creates))\n return { visible, enabled: fits, active }\n}\n\n/** The commands of the preset's features in feature order. Throws when two commands share an id. */\nexport function commandsOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): CommandDef<C>[] {\n return collect(preset, ctx).commands\n}\n\n// The feature each command came from, for error messages; commands of the host's `commands` transformer have none.\nconst ownerOfCommand = new WeakMap<object, string>()\n\n/**\n * Throws when two of `commands` share an id (`CommandBase.id` is unique per editor), naming the declaring features;\n * a command without a known feature is the host's.\n */\nexport function assertUniqueCommandIds<C>(commands: readonly CommandDef<C>[]): void {\n const seen = new Map<string, CommandDef<C>>()\n for (const cmd of commands) {\n const first = seen.get(cmd.id)\n if (first) fail(`command \"${cmd.id}\" is declared by \"${ownerOfCommand.get(first) ?? 'host'}\" and \"${ownerOfCommand.get(cmd) ?? 'host'}\"`)\n seen.set(cmd.id, cmd)\n }\n}\n\n/**\n * Places `commands` into their toolbar `groups` (every declared group, also empty ones, sorted by `order`; a group's\n * commands sorted by `toolbar.order`, stable). `unplaced` receives each command that has no group to go to: no `toolbar`\n * placement, or one naming a group that is not declared; what that means is the caller's policy.\n */\nexport function placeInGroups<C>(groups: readonly ToolbarGroupDef[], commands: readonly CommandDef<C>[], unplaced: (cmd: CommandDef<C>) => void): PlacedToolbarGroup<C>[] {\n const placed = new Map<string, PlacedToolbarGroup<C>>()\n // `defineEditor` guarantees every group id is declared once.\n for (const group of groups) placed.set(group.id, { ...group, items: [] })\n for (const cmd of commands) {\n const group = cmd.toolbar ? placed.get(cmd.toolbar.group) : undefined\n if (group) group.items.push(cmd)\n else unplaced(cmd)\n }\n return [...placed.values()]\n .sort(byOrder)\n .map(group => ({ ...group, items: [...group.items].sort((a, b) => byOrder(a.toolbar || {}, b.toolbar || {})) }))\n}\n\n/** The toolbar groups the features of `preset` declare, in feature order. */\nexport const toolbarGroupsOf = <C>(preset: EditorPreset<C>): ToolbarGroupDef[] => preset.features.flatMap(feature => feature.toolbarGroups ?? [])\n\n/**\n * The toolbar of `preset`: the groups features declare (`toolbarGroups`) with every command that has a `toolbar`\n * placement, groups and commands sorted by `order` (stable). Throws when a command names a group no feature declares\n * or two commands share an id.\n */\nexport function toolbarOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): PlacedToolbarGroup<C>[] {\n const { commands, groups } = collect(preset, ctx)\n return placeInGroups(groups, commands, (cmd) => {\n if (cmd.toolbar) fail(`command \"${cmd.id}\" of \"${ownerOfCommand.get(cmd) ?? '?'}\" names toolbar group \"${cmd.toolbar.group}\" that no feature declares`)\n })\n}\n\nfunction collect<C>(preset: EditorPreset<C>, ctx: FeatureContext) {\n const commands: CommandDef<C>[] = []\n for (const feature of preset.features) {\n for (const cmd of feature.commands?.(ctx) ?? []) {\n commands.push(cmd)\n ownerOfCommand.set(cmd, feature.id)\n }\n }\n assertUniqueCommandIds(commands)\n return { commands, groups: toolbarGroupsOf(preset) }\n}\n"],"mappings":"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"}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { BlockAction } from "./extension-points.js";
|
|
2
2
|
import { EditorFeature, FeatureContext } from "./feature.js";
|
|
3
|
+
import { CoreContentOptions } from "./schema/index.js";
|
|
3
4
|
//#region src/core/core-feature.d.ts
|
|
4
5
|
/** Hover actions every editor with block UI has: „Umwandeln in …“, duplicate, delete (in this order, after features' own). */
|
|
5
6
|
export declare const coreBlockActions: (ctx: FeatureContext) => readonly BlockAction[];
|
|
6
7
|
/**
|
|
7
8
|
* The content base of every editor: StarterKit with StrikeX, block IDs and node flags, the verbatim `OpaqueFence` for
|
|
8
9
|
* Markdown fences of unknown kinds, Markdown, abilities and metadata infrastructure and the standard conversions.
|
|
10
|
+
* `options` drop optional StarterKit structures (headings, lists, blockquote, code block, rule) from the editor and
|
|
11
|
+
* from the server schema alike (`CoreContentOptions`); everything is on by default.
|
|
9
12
|
* StarterKit's undo history is off when a collaboration session brings its own. Its `schemaExtensions` are the base
|
|
10
13
|
* of the server schema (`schemaExtensionsOf` appends `BlockId`). A headless editor (Markdown conversion, viewer,
|
|
11
14
|
* validation) is `coreContentFeature()` plus other features without `coreUiFeature()`.
|
|
12
15
|
* Contributes no components (`EditorFeature<never>`), so it fits a preset of any framework layer.
|
|
13
16
|
*/
|
|
14
|
-
export declare function coreContentFeature(): EditorFeature<never>;
|
|
17
|
+
export declare function coreContentFeature(options?: CoreContentOptions): EditorFeature<never>;
|
|
15
18
|
/**
|
|
16
19
|
* What a framework layer binds to `core-ui`: the ⌘K palette component, mounted as an overlay of the surface (it reads
|
|
17
20
|
* `ui.paletteOpen` and the editor's commands; `EditorCommandPalette` of `/vue` and `/react`). `C` is the layer's component type.
|