@aionbuilders/nabu 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +131 -0
  2. package/dist/behaviors/index.d.ts +1 -0
  3. package/dist/behaviors/index.js +1 -0
  4. package/dist/behaviors/text/RichText.svelte +33 -0
  5. package/dist/behaviors/text/RichText.svelte.d.ts +11 -0
  6. package/dist/behaviors/text/index.d.ts +3 -0
  7. package/dist/behaviors/text/index.js +3 -0
  8. package/dist/behaviors/text/rich-text.extension.d.ts +2 -0
  9. package/dist/behaviors/text/rich-text.extension.js +75 -0
  10. package/dist/behaviors/text/text.behavior.svelte.d.ts +103 -0
  11. package/dist/behaviors/text/text.behavior.svelte.js +346 -0
  12. package/dist/blocks/Block.svelte +18 -0
  13. package/dist/blocks/Block.svelte.d.ts +11 -0
  14. package/dist/blocks/Nabu.svelte +31 -0
  15. package/dist/blocks/Nabu.svelte.d.ts +12 -0
  16. package/dist/blocks/block.svelte.d.ts +143 -0
  17. package/dist/blocks/block.svelte.js +364 -0
  18. package/dist/blocks/container.utils.d.ts +28 -0
  19. package/dist/blocks/container.utils.js +114 -0
  20. package/dist/blocks/heading/Heading.svelte +42 -0
  21. package/dist/blocks/heading/Heading.svelte.d.ts +11 -0
  22. package/dist/blocks/heading/heading.svelte.d.ts +45 -0
  23. package/dist/blocks/heading/heading.svelte.js +94 -0
  24. package/dist/blocks/heading/hooks/onBeforeInput.hook.d.ts +3 -0
  25. package/dist/blocks/heading/hooks/onBeforeInput.hook.js +58 -0
  26. package/dist/blocks/heading/index.d.ts +7 -0
  27. package/dist/blocks/heading/index.js +41 -0
  28. package/dist/blocks/index.d.ts +10 -0
  29. package/dist/blocks/index.js +12 -0
  30. package/dist/blocks/list/List.svelte +25 -0
  31. package/dist/blocks/list/List.svelte.d.ts +11 -0
  32. package/dist/blocks/list/ListItem.svelte +45 -0
  33. package/dist/blocks/list/ListItem.svelte.d.ts +11 -0
  34. package/dist/blocks/list/index.d.ts +10 -0
  35. package/dist/blocks/list/index.js +41 -0
  36. package/dist/blocks/list/list-item.svelte.d.ts +50 -0
  37. package/dist/blocks/list/list-item.svelte.js +213 -0
  38. package/dist/blocks/list/list.behavior.svelte.d.ts +23 -0
  39. package/dist/blocks/list/list.behavior.svelte.js +61 -0
  40. package/dist/blocks/list/list.svelte.d.ts +39 -0
  41. package/dist/blocks/list/list.svelte.js +139 -0
  42. package/dist/blocks/megablock.svelte.d.ts +13 -0
  43. package/dist/blocks/megablock.svelte.js +64 -0
  44. package/dist/blocks/nabu.svelte.d.ts +121 -0
  45. package/dist/blocks/nabu.svelte.js +395 -0
  46. package/dist/blocks/paragraph/Paragraph.svelte +38 -0
  47. package/dist/blocks/paragraph/Paragraph.svelte.d.ts +11 -0
  48. package/dist/blocks/paragraph/index.d.ts +7 -0
  49. package/dist/blocks/paragraph/index.js +44 -0
  50. package/dist/blocks/paragraph/paragraph.svelte.d.ts +41 -0
  51. package/dist/blocks/paragraph/paragraph.svelte.js +86 -0
  52. package/dist/blocks/selection.svelte.d.ts +38 -0
  53. package/dist/blocks/selection.svelte.js +143 -0
  54. package/dist/index.d.ts +4 -0
  55. package/dist/index.js +5 -0
  56. package/dist/utils/extensions.d.ts +69 -0
  57. package/dist/utils/extensions.js +43 -0
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.js +2 -0
  60. package/dist/utils/selection.svelte.d.ts +219 -0
  61. package/dist/utils/selection.svelte.js +611 -0
  62. package/package.json +74 -0
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Nabu — Block Editor Engine
2
+
3
+ > **Alpha** — API in active development. Breaking changes expected.
4
+
5
+ Nabu is a modular, local-first block editor engine for **Svelte 5**. It combines the intuitive cross-block selection of document editors (Google Docs, ProseMirror) with the structural flexibility of block editors (Notion), built on a **Single ContentEditable** architecture and a **CRDT core** (Loro).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @aionbuilders/nabu
11
+ # or
12
+ bun add @aionbuilders/nabu
13
+ ```
14
+
15
+ ## Required: Vite Setup
16
+
17
+ Nabu depends on `loro-crdt`, which ships as a WebAssembly module. You must configure your Vite bundler to handle it.
18
+
19
+ **1. Install the required Vite plugins:**
20
+
21
+ ```bash
22
+ npm install -D vite-plugin-wasm vite-plugin-top-level-await
23
+ ```
24
+
25
+ **2. Add them to your `vite.config.js`:**
26
+
27
+ ```js
28
+ import { defineConfig } from 'vite';
29
+ import { sveltekit } from '@sveltejs/kit/vite'; // or svelte()
30
+ import wasm from 'vite-plugin-wasm';
31
+ import topLevelAwait from 'vite-plugin-top-level-await';
32
+
33
+ export default defineConfig({
34
+ plugins: [
35
+ sveltekit(),
36
+ wasm(),
37
+ topLevelAwait()
38
+ ]
39
+ });
40
+ ```
41
+
42
+ Without this setup, Nabu will fail to load.
43
+
44
+ ## Basic Usage
45
+
46
+ ```svelte
47
+ <script>
48
+ import {
49
+ Nabu,
50
+ NabuEditor,
51
+ ParagraphExtension,
52
+ HeadingExtension,
53
+ ListExtension,
54
+ ListItemExtension,
55
+ RichTextExtension
56
+ } from '@aionbuilders/nabu';
57
+
58
+ const engine = new Nabu({
59
+ extensions: [
60
+ ParagraphExtension,
61
+ HeadingExtension,
62
+ ListExtension,
63
+ ListItemExtension,
64
+ RichTextExtension
65
+ ]
66
+ });
67
+ </script>
68
+
69
+ <NabuEditor {engine} />
70
+ ```
71
+
72
+ That's it. The editor is now functional with paragraphs, headings, lists, and rich text formatting.
73
+
74
+ ## Extensions
75
+
76
+ All block types and behaviors are provided as extensions. Pass them to the `Nabu` constructor:
77
+
78
+ | Extension | Provides | Keyboard shortcuts |
79
+ |---|---|---|
80
+ | `ParagraphExtension` | `paragraph` block | — |
81
+ | `HeadingExtension` | `heading` block (h1–h6) | `# ` → h1, `## ` → h2, ... |
82
+ | `ListExtension` | `list` container (bullet/ordered) | — |
83
+ | `ListItemExtension` | `list-item` block | `Tab` indent, `Shift+Tab` unindent |
84
+ | `RichTextExtension` | Inline marks (bold, italic...) | `Ctrl+B/I/U/E`, `Ctrl+Shift+X` |
85
+
86
+ ## Programmatic API
87
+
88
+ ```js
89
+ // Insert blocks
90
+ engine.insert('paragraph', { text: 'Hello' });
91
+ engine.insert('heading', { level: 1, text: 'Title' });
92
+ engine.insert('list', { listType: 'bullet' });
93
+
94
+ // Undo / Redo
95
+ engine.undo();
96
+ engine.redo();
97
+
98
+ // Serialize
99
+ const markdown = engine.serialize('markdown');
100
+ const json = engine.serialize('json');
101
+ ```
102
+
103
+ ## Architecture
104
+
105
+ ### Single ContentEditable
106
+ The entire editor lives in a single `contenteditable` root. This enables seamless cross-block selection, native copy-paste, and consistent browser performance.
107
+
108
+ ### CRDT Core (Loro)
109
+ All state is stored in a [Loro](https://loro.dev) document — a WASM-based CRDT. This provides:
110
+ - **Local-first**: offline editing out of the box
111
+ - **Collaboration-ready**: conflict-free concurrent edits
112
+ - **Rich text**: native delta/mark support via `LoroText`
113
+
114
+ ### Extension System
115
+ Every block type is an extension. You can build custom block types by extending the `Block` or `MegaBlock` base classes and registering them via the `extension()` helper.
116
+
117
+ ### Svelte 5 Runes
118
+ Nabu uses `$state` and `$derived` throughout for fine-grained reactivity — only modified blocks re-render.
119
+
120
+ ---
121
+
122
+ ## Status
123
+
124
+ This is an **alpha release**. The core editing experience is functional, but some features are still in development:
125
+
126
+ - [ ] Persistence (IndexedDB)
127
+ - [ ] Floating toolbar UI
128
+ - [ ] Slash command menu
129
+ - [ ] Link support
130
+
131
+ Contributions and feedback welcome.
@@ -0,0 +1 @@
1
+ export * from "./text";
@@ -0,0 +1 @@
1
+ export * from './text';
@@ -0,0 +1,33 @@
1
+ <script>
2
+ /** @type {{ delta: import('loro-crdt').Delta<string>[] }} */
3
+ let { delta } = $props();
4
+
5
+ /** @param {Record<string, any>} attrs */
6
+ function getClasses(attrs) {
7
+ return [
8
+ attrs.bold && 'nabu-bold',
9
+ attrs.italic && 'nabu-italic',
10
+ attrs.underline && 'nabu-underline',
11
+ attrs.code && 'nabu-code',
12
+ attrs.strikethrough && 'nabu-strikethrough',
13
+ ].filter(Boolean).join(' ');
14
+ }
15
+
16
+ const segments = $derived(delta.length ? delta : [{ insert: '\n' }]);
17
+ </script>
18
+
19
+ {#each segments as op}
20
+ {#if op.attributes && Object.values(op.attributes).some(Boolean)}
21
+ <span class={getClasses(op.attributes)}>{op.insert}</span>
22
+ {:else}
23
+ {op.insert}
24
+ {/if}
25
+ {/each}
26
+
27
+ <style>
28
+ :global(.nabu-bold) { font-weight: bold; }
29
+ :global(.nabu-italic) { font-style: italic; }
30
+ :global(.nabu-underline) { text-decoration: underline; }
31
+ :global(.nabu-strikethrough) { text-decoration: line-through; }
32
+ :global(.nabu-code) { font-family: monospace; background: rgba(135, 131, 120, 0.15); border-radius: 3px; padding: 0.1em 0.3em; }
33
+ </style>
@@ -0,0 +1,11 @@
1
+ export default RichText;
2
+ type RichText = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const RichText: import("svelte").Component<{
7
+ delta: import("loro-crdt").Delta<string>[];
8
+ }, {}, "">;
9
+ type $$ComponentProps = {
10
+ delta: import("loro-crdt").Delta<string>[];
11
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./text.behavior.svelte";
2
+ export { RichTextExtension } from "./rich-text.extension.js";
3
+ export { default as RichText } from "./RichText.svelte";
@@ -0,0 +1,3 @@
1
+ export * from './text.behavior.svelte';
2
+ export { RichTextExtension } from './rich-text.extension.js';
3
+ export { default as RichText } from './RichText.svelte';
@@ -0,0 +1,2 @@
1
+ export const RichTextExtension: Extension;
2
+ import { Extension } from '../../utils/extensions.js';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @import { Nabu } from '../../blocks/nabu.svelte.js'
3
+ */
4
+
5
+ import { tick } from 'svelte';
6
+ import { Extension } from '../../utils/extensions.js';
7
+ import { TextBehavior } from './text.behavior.svelte.js';
8
+
9
+ /** @type {Record<string, string>} */
10
+ const MARK_SHORTCUTS = {
11
+ b: 'bold',
12
+ i: 'italic',
13
+ u: 'underline',
14
+ e: 'code',
15
+ };
16
+
17
+ /**
18
+ * @param {Nabu} nabu
19
+ * @param {KeyboardEvent} event
20
+ */
21
+ function onKeyDown(nabu, event) {
22
+ const ctrl = event.ctrlKey || event.metaKey;
23
+ if (!ctrl) return;
24
+
25
+ const endBlock = nabu.selection.endBlock;
26
+ const startBlock = nabu.selection.startBlock;
27
+ const startFocusData = startBlock?.focus(null, true);
28
+ const endFocusData = endBlock?.focus(null, true);
29
+
30
+ let markName = null;
31
+ if (!event.shiftKey) {
32
+ markName = MARK_SHORTCUTS[event.key.toLowerCase()] ?? null;
33
+ } else if (event.key.toLowerCase() === 'x') {
34
+ markName = 'strikethrough';
35
+ }
36
+
37
+ if (!markName) return;
38
+
39
+ // Collect all targeted block/selection pairs
40
+ const targets = [];
41
+ for (const [, block] of nabu.blocks) {
42
+ if (!block.selected) continue;
43
+ const behavior = block.behaviors?.get('text');
44
+ if (!(behavior instanceof TextBehavior)) continue;
45
+ const sel = behavior.selection;
46
+ if (!sel || sel.isCollapsed) continue;
47
+ targets.push({ behavior, sel });
48
+ }
49
+
50
+ if (!targets.length) return;
51
+
52
+ // Classic editor behavior: remove if mark is fully active across all targets, apply otherwise
53
+ const isFullyActive = targets.every(({ behavior, sel }) => behavior.isMarkActive(markName, sel));
54
+ for (const { behavior, sel } of targets) {
55
+ if (isFullyActive) {
56
+ behavior.removeMark(markName, sel);
57
+ } else {
58
+ behavior.applyMark(markName, true, sel);
59
+ }
60
+ }
61
+ const applied = true;
62
+
63
+ if (applied) {
64
+ nabu.commit();
65
+ if (!startBlock || !endBlock) return;
66
+ nabu.focus({ from: { block: startBlock, offset: startFocusData?.start?.offset }, to: { block: endBlock, offset: endFocusData?.end?.offset } });
67
+
68
+
69
+ return nabu.BREAK;
70
+ }
71
+ }
72
+
73
+ export const RichTextExtension = new Extension('rich-text', {
74
+ hooks: { onKeyDown }
75
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Converts a Loro text delta to inline Markdown.
3
+ * @param {import('loro-crdt').Delta<string>[]} delta
4
+ * @returns {string}
5
+ */
6
+ export function deltaToMarkdown(delta: import("loro-crdt").Delta<string>[]): string;
7
+ /**
8
+ * @typedef {NabuNode<{type: "paragraph", text: LoroText}>} TextNode
9
+ */
10
+ export class TextBehavior {
11
+ /** @param {Block} block @param {LoroText} [container] */
12
+ constructor(block: Block, container?: LoroText);
13
+ block: Block;
14
+ nabu: import("../..").Nabu;
15
+ /** @type {LoroText} */
16
+ container: LoroText;
17
+ text: string;
18
+ delta: import("loro-crdt").Delta<string>[];
19
+ selection: {
20
+ from: number;
21
+ to: number;
22
+ isCollapsed: boolean;
23
+ direction: "forward" | "backward" | "none";
24
+ } | null;
25
+ /**
26
+ * @param {HTMLElement} element
27
+ * @param {NabuSelection} globalSelection
28
+ */
29
+ getSelection(element: HTMLElement, globalSelection: NabuSelection): {
30
+ from: number;
31
+ to: number;
32
+ isCollapsed: boolean;
33
+ direction: "forward" | "backward" | "none";
34
+ } | null;
35
+ /**
36
+ * Calcule l'offset textuel d'un point DOM par rapport au début de ce bloc
37
+ * @param {Node} node
38
+ * @param {number} offset
39
+ * @param {HTMLElement?} [element]
40
+ */
41
+ calculateOffset(node: Node, offset: number, element?: HTMLElement | null): number;
42
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
43
+ handleBeforeInput(event: InputEvent, selection?: ReturnType<TextBehavior["getSelection"]>): any;
44
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
45
+ handleInsertText(event: InputEvent, selection?: ReturnType<TextBehavior["getSelection"]>): void;
46
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
47
+ handleInsertLineBreak(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
48
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
49
+ handleDeleteContentBackward(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
50
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
51
+ handleDeleteContentForward(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
52
+ /** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
53
+ handleInsertParagraph(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): any;
54
+ /**
55
+ * Retrouve le nœud texte et l'offset DOM pour un offset Modèle donné
56
+ * @param {number} targetOffset
57
+ * @param {HTMLElement?} [element]
58
+ */
59
+ getDOMPoint(targetOffset: number, element?: HTMLElement | null): {
60
+ node: Node;
61
+ offset: number;
62
+ } | null;
63
+ /** @param {number} index @param {string} text */
64
+ insert(index: number, text: string): void;
65
+ /** @param {Parameters<Block["delete"]>[0]} [deletion] @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
66
+ delete(deletion?: Parameters<Block["delete"]>[0], selection?: ReturnType<TextBehavior["getSelection"]>): void;
67
+ /** @param {import('loro-crdt').Delta<string>[]} data */
68
+ applyDelta(data?: import("loro-crdt").Delta<string>[]): void;
69
+ /**
70
+ * @param {Parameters<Block["split"]>[0]} [options]
71
+ * @param {ReturnType<TextBehavior["getSelection"]>} [selection]
72
+ * @returns {ReturnType<Block["split"]>}
73
+ */
74
+ split(options?: Parameters<Block["split"]>[0], selection?: ReturnType<TextBehavior["getSelection"]>): ReturnType<Block["split"]>;
75
+ /** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
76
+ applyMark(markName: string, value: any, sel: ReturnType<TextBehavior["getSelection"]>): void;
77
+ /** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
78
+ removeMark(markName: string, sel: ReturnType<TextBehavior["getSelection"]>): void;
79
+ /** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
80
+ isMarkActive(markName: string, sel: ReturnType<TextBehavior["getSelection"]>): boolean;
81
+ /** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
82
+ toggleMark(markName: string, value: any, sel: ReturnType<TextBehavior["getSelection"]>): void;
83
+ /** @returns {string} */
84
+ toMarkdown(): string;
85
+ /**
86
+ * Converts the text delta to a Slate-like JSON format.
87
+ * @returns {{text: string, [mark: string]: any}[]}
88
+ */
89
+ toJSON(): {
90
+ text: string;
91
+ [mark: string]: any;
92
+ }[];
93
+ /** @param {Block} other */
94
+ absorbs(other: Block): boolean;
95
+ }
96
+ export type TextNode = NabuNode<{
97
+ type: "paragraph";
98
+ text: LoroText;
99
+ }>;
100
+ import type { Block } from "../../blocks";
101
+ import { LoroText } from "loro-crdt";
102
+ import type { NabuSelection } from "../../blocks";
103
+ import type { NabuNode } from "../../blocks";