Nabu
The editor engine. It owns the document (a Loro CRDT), the block tree and its registry, the selection, the commands and the
extensions, and runs the event, clipboard and drag and drop pipelines; NabuEditor renders it. Create one with createEditor
(or a preset factory such as createDocumentEditor) rather than new Nabu(...), then read and write the content through
serialize, importMarkdown, importJSON, exec and transact. The members documented as public are the stable contract of
@aionbuilders/nabu/extend; internals is the unstable door to the rest.
Related guide: Your first editor →
Import
import { Nabu } from '@aionbuilders/nabu';Constructor
new Nabu(init?: NabuInit)| Parameter | Type | Default | Description |
|---|---|---|---|
initoptional | NabuInit | {} | n/a |
Static members
static BREAK
static BREAK: symbolWhat a hook or an onBeforeInput handler returns to stop the engine (and the hooks after it).
static CONTINUE
static CONTINUE: symbolWhat a hook returns to let the engine go on (same as returning nothing).
Properties
blocks
blocks: SvelteMap<string, Block>Every live block of the document, by tree id.
blocksByType
blocksByType: SvelteMap<string, SvelteSet<Block>>The live blocks of each type (reactive sets, by block type).
BREAK
BREAK: symbolNabu.BREAK, reachable from the instance a hook receives.
children
children: Block[]The root blocks of the document, in order (reactive).
commands
commands: CommandRegistryCommands and keyboard shortcuts (ADR 017): list(), get(id), has(id), run(id, data), offer(id). A plain
snapshot API; the set of commands is fixed once the editor is built.
components
components: SvelteMap<string, Component<any, any, string>>The Svelte components registered by the loaded extensions, by block type.
CONTINUE
CONTINUE: symbolNabu.CONTINUE, reachable from the instance a hook receives.
debugging
debugging: booleanWhen true, nabu.warn() logs to the console ([Nabu]).
defaultBlockType
defaultBlockType: string | nullType of the default text block (declared by an extension with defaultBlock: true): created by the
engine when it needs an empty text block of its own, e.g. a caret after a trailing void block.
doc
doc: LoroDoc<Record<string, Container>>The Loro document behind the editor (public: connect it to a server with doc.subscribeLocalUpdates and importRemote).
extensions
extensions: Extension[]The extensions the editor was built with, in order.
placeholders
placeholders: ResolvedPlaceholdersResolved placeholders option (see NabuInit). Reassign to change it at runtime.
positions
positions: numberNumber of caret positions in the document (the sum of the root blocks').
registry
registry: SvelteMap<string, typeof Block>The block classes registered by the loaded extensions, by block type.
selection
selection: NabuSelectionThe text selection of the editor (a reactive wrapper of the DOM selection, in terms of blocks).
serializers
serializers: Map<string, (nabu: Nabu) => any>Root-level serializers. Each function receives the Nabu instance and returns the serialized document.
slashCommands
slashCommands: SlashCommand[]Slash-menu entries contributed by extensions (Extension#slashCommands); the slash menu lists ONLY these (plus the
integrator's extras), sorted by group / order, keeping those whose when(engine, block) holds at the caret.
spellcheck
spellcheck: booleanResolved spellcheck option (see NabuInit). Reactive: assign to change it at runtime.
systems
systems: SvelteMap<string, { start?: () => void; stop?: () => void; [key: string]: any }>Registered subsystems. Each system should implement:
start(): void— called when the editor mountsstop(): void— called when the editor unmounts
tabHint
tabHint: string | falseAnnouncement of the Escape-then-Tab way out (option tabHint), false for none. Reactive.
tree
tree: LoroTree<NabuNodeData>The document tree (Loro): one node per block, node.data holds the block's type and props.
commandAnnouncement readonly
readonly commandAnnouncement: stringText of the polite live region of the editor (prompts and outcomes of confirmations). Reactive.
composing readonly
readonly composing: booleanTrue during an IME composition: the browser owns the DOM, the caret must not be touched.
composingBlock readonly
readonly composingBlock: Block | nullThe block holding the running composition, or null. Reactive.
internals Unstable readonly
readonly internals: NabuInternalsThe door to everything the engine uses internally that the stable contract (the members tagged public in their JSDoc) does not cover: the
command-confirm controller, the action bus, the hook and rule registries, the clipboard / drop / event pipelines, undo
capture, stored marks, the selection's repair machinery. They are the same references the engine runs on, not copies.
The door itself is part of the stable contract; @unstable is everything behind it: nothing reached through internals is a
promise, it may change, move or disappear in ANY minor version.
Prefer the stable members; use this when you must, and pin the version of the package. The set of keys is frozen by a
test, so a change shows up in the changelog.
isEmpty readonly
readonly isEmpty: booleanTrue when the document has no block.
lang
lang: string | undefinedResolved lang option (see NabuInit): a valid BCP 47 tag, or undefined (inherit the page language).
Reactive: assign to change it at runtime; an invalid value is ignored (warning) and leaves it unchanged.
lastVisibleBlock readonly
readonly lastVisibleBlock: Block | nullThe deepest last block of the document that can hold a caret in the rendered document: follows the last child at each level, but never enters a block that hides its children.
pendingCommand readonly
readonly pendingCommand: PendingCommand | nullThe command waiting for the user's confirmation (ADR 017 §3.4), reactive; null when none:
{ id, strategy: 'hold' | 'invite' | 'choose', label, display, message, target?, progress, options?, selectedIndex }.
progress (hold, 0 → 1) and selectedIndex (choose) are reactive fields of that object.
readonly
readonly: booleanRead-only mode (see the readonly option of NabuInit). Reactive: assign to switch it on or off at runtime. While true
the user cannot change the document: input, paste, cut, drop, drag moves and commands (shortcuts, commands.run,
exec of a command, undo / redo) are ignored by the engine, and the UI components hide themselves. Programmatic
edits by the host (insert, importJSON...) are not blocked. View state that is not part of the document (a toggle
opened to read it) stays possible and is never written to the document.
tab
tab: TabModeResolved tab option (see NabuInit): what Tab / Shift+Tab do in a plain text block. Reactive: assign to change it at runtime;
an unknown value warns and gives the default of the editor (its preset).
Methods
cancelPendingCommand()
cancelPendingCommand(): voidCancel the pending confirmation, if any: nothing runs.
commit()
commit(options?: object): Promise<void>Commit the pending changes as one Loro transaction.
| Parameter | Type | Default | Description |
|---|---|---|---|
optionsoptional | object | {} |
|
options.originoptional | string | n/a | n/a |
createPasteBlock()
createPasteBlock(pb: PasteBlock, parentId: string): Block | nullCreates a block inside a container, recursively handling nested children. Public: part of the paste
toolbox a block's acceptPaste hook builds on. No commit.
| Parameter | Type | Description |
|---|---|---|
pb | PasteBlock | n/a |
parentId | string | n/a |
Returns Block | null
the created block
delete()
delete(block: Block): voidDelete a block (and its children) from the tree. No commit: call commit() afterwards.
| Parameter | Type | Description |
|---|---|---|
block | Block | n/a |
exec()
exec(topic: string, data?: any): Promise<PulseEvent>Dispatch an action to all registered handlers for the given topic.
Re-syncs the model selection from the DOM first: selectionchange is async, so right after a
keyboard-only selection change (Shift+Arrow, Home/End, Ctrl+A) the model would be stale.
| Parameter | Type | Description |
|---|---|---|
topic | string | n/a |
dataoptional | any | n/a |
focus()
focus(options?: object): Promise<void>Place the DOM + model selection. Asynchronous by nature: it waits for Svelte to flush pending state
(tick()), because the target blocks' DOM may not be rendered yet (e.g. right after a commit/insert).
The returned promise resolves once the selection has been set, i.e. once nabu.selection reflects it:
await nabu.focus(...) before exec(...) / typing. Fire-and-forget callers are unaffected.
| Parameter | Type | Description |
|---|---|---|
optionsoptional | object | n/a |
options.fromoptional | { block?: Block; offset?: number } | n/a |
options.from.blockoptional | Block | n/a |
options.from.offsetoptional | number | n/a |
options.offsetoptional | number | n/a |
options.tooptional | { block?: Block; offset?: number } | n/a |
options.to.blockoptional | Block | n/a |
options.to.offsetoptional | number | n/a |
importFragment()
importFragment(fragment: PasteFragment): Promise<void>Replace the entire document with the given PasteFragment. Unlike insertFragment(), this does not depend on a live DOM selection: blocks are created at the root through the same registry pipeline as paste.
| Parameter | Type | Description |
|---|---|---|
fragment | PasteFragment | n/a |
importJSON()
importJSON(json: string | { assets?: Record<string, EmbeddedAsset>; blocks: {
children?: any[];
content?: string | { text: string; [mark: string]: any }[];
props?: Record<string, any>;
type: string;
[key: string]: any;
}[]; version?: string | number }): Promise<void>Replace the entire document with a serialize('json') export. An export of a version this editor does not know
is refused with a warning (the document is left untouched), like the clipboard does; a block type that is not
registered is degraded to the default text block (degradeUnknownBlocks), nothing is dropped.
| Parameter | Type | Description |
|---|---|---|
json | string | { assets?: Record<string, EmbeddedAsset>; blocks: {
children?: any[];
content?: string | { text: string; [mark: string]: any }[];
props?: Record<string, any>;
type: string;
[key: string]: any;
}[]; version?: string | number } | n/a |
importMarkdown()
importMarkdown(markdown: string): Promise<void>Replace the entire document with content parsed from a Markdown string. Requires MarkdownPasteExtension (or any text/plain interpreter) to be loaded.
| Parameter | Type | Description |
|---|---|---|
markdown | string | n/a |
importRemote()
importRemote(bytes: Uint8Array<ArrayBufferLike>): ImportStatusImport changes made ELSEWHERE (another tab, a peer, a stored copy of the document: ADR 010 multi-tab sync). They are the
document's own history, not the user's actions: Loro's UndoManager records local commits only, so an import is never an undo
step (undoing the user's own edits afterwards still works, Loro transforms them). The DOM follows through the normal Loro
events. The caret of the local user stays on the same CHARACTER: it is captured as Loro cursors before the import and put
back after the render (a text node rewritten by Svelte would otherwise send it to the start of the block); the page is
never scrolled for it. Idempotent (importing what the document already has changes nothing).
Called DURING an IME composition it is applied at once (not held): the model is safe, because the composition anchor is a Loro
cursor that follows the edit, and the caret restore is skipped meanwhile (the browser owns it). (persistence/sync.js still
holds the updates it receives while composing, which keeps the composing text node untouched by the render.)
| Parameter | Type | Description |
|---|---|---|
bytes | Uint8Array<ArrayBufferLike> | a Loro snapshot or update |
insert()
insert(
type: string,
props?: Record<string, any>,
parentId?: string | null,
index?: number | null
): BlockInsert a new block of a registered type and commit (throws when the type is not registered).
| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | n/a | Block type ( |
propsoptional | Record<string, any> | {} | Initial props, read by the class's |
parentIdoptional | string | null | null | Id of the parent block ( |
indexoptional | number | null | null | Position among the siblings ( |
Returns Block
The created block
insertPasteBlock()
insertPasteBlock(pb: PasteBlock, afterNode: any): anyInserts a PasteBlock after afterNode. Handles both leaf blocks (wrapOrphan)
and container blocks (creates container, recurses into children).
Returns the outer node to use as the next prevNode. Public: part of the paste toolbox a block's
acceptPaste hook builds on. No commit.
| Parameter | Type | Description |
|---|---|---|
pb | PasteBlock | n/a |
afterNode | any | n/a |
isMarkActive()
isMarkActive(markName: string): booleanTrue when every selected text run carries the given mark. Reactive: reads block.selected and the behaviors' selections/deltas, so it can be used directly in component templates (e.g. FloatingToolbar).
| Parameter | Type | Description |
|---|---|---|
markName | string | n/a |
isolateUndoStep()
isolateUndoStep(): voidMake the NEXT commit its own undo step (and the typing right after it another one), even though it holds no structural op: for a non-structural change that must undo on its own, e.g. a flag toggled from the keyboard, which would otherwise merge with the typing around it. Call it just before commit().
openTrailingCaret()
openTrailingCaret(): Promise<Block | null>The document must always offer a caret after its last block. Call it on user intent (ArrowDown / ArrowRight leaving the document, a click below the last block): when the last block holds text, the caret goes to its end; when it does not (a void block such as a divider / image, or a container whose end is not a text position), an empty default text block is appended at the root, in one undo step, and takes the caret. Never called eagerly on load.
Returns Promise<Block | null>
The block now holding the caret
pendingCommandOn()
pendingCommandOn(target: any): PendingCommand | nullThe pending confirmation when it is ABOUT target (what confirm.target(ctx) returned: usually a block), else null.
Reactive: a block that draws its own hold progress reads nabu.pendingCommandOn(block)?.progress.
| Parameter | Type | Description |
|---|---|---|
target | any | n/a |
redo()
redo(): voidRedo the last undone operation. The selection comes back where that operation ended.
resolveDocumentOffset()
resolveDocumentOffset(globalOffset: number): { block: Block; offset: number } | nullThe block and local offset that hold a document-wide caret offset (positions counted from the start of the document).
| Parameter | Type | Description |
|---|---|---|
globalOffset | number | n/a |
serialize()
serialize(format: string): anySerialize the document with the root serializer registered for format ('markdown', 'json', or one an extension
adds with serializers); null (and a warning) for an unknown format.
| Parameter | Type | Description |
|---|---|---|
format | string | n/a |
shortcut()
shortcut(block: Block, trigger: string, transform: (behavior: any) => void): Promise<void>Apply a markdown BLOCK shortcut (# , - , > , --- …) so that undo gives back exactly what
the user typed, trigger included: one Ctrl+Z after # leaves a paragraph "# " with the caret
after the space; a second one removes the typing as usual.
trigger(the swallowed space) is inserted at the caret and committed like normal typing;transformruns (delete the prefix incl. the trigger, change/replace the block…) and is committed as its own structural undo step (see commit()), whose stored caret is right after the trigger. Both commits run in the same synchronous turn, so the intermediate "# " is never painted.
For a trigger that is not text (``` + Enter), pass trigger = '': nothing is inserted and
undo restores the text typed before the key (the fence line, without the Enter).
| Parameter | Type | Description |
|---|---|---|
block | Block | the block the shortcut was typed in |
trigger | string | text of the trigger key, inserted before transforming ('' = none) |
transform | (behavior: any) => void | mutations only (it may commit itself, e.g. via transformTo) |
Returns Promise<void>
resolves immediately, doing nothing, when block.transformsEnabled === false
transact()
transact(mutateFn: () => void, cursorIntent?: { block: Block; offset: number } | null): Promise<void>Run Loro mutations as a single transaction, then optionally restore the cursor. Centralizes the "mutate → commit → setCursor" dance used by most actions.
| Parameter | Type | Default | Description |
|---|---|---|---|
mutateFn | () => void | n/a | Mutations Loro ici. Ne PAS appeler commit() dedans. |
cursorIntentoptional | { block: Block; offset: number } | null | null | n/a |
trigger()
trigger(hookName: string, ...args: any[]): voidRun the hookName hooks of every extension, in order, until one returns BREAK. Each is called as hook(nabu, ...args).
| Parameter | Type | Description |
|---|---|---|
hookName | string | n/a |
...args | any[] | n/a |
undo()
undo(): voidUndo the last operation. The selection as it was before that operation comes back (see ADR 015).
warn()
warn(...args: any[]): voidLog a diagnostic to the console when debugging is on. Silent otherwise.
| Parameter | Type | Description |
|---|---|---|
...args | any[] | n/a |
Source: src/lib/blocks/nabu.svelte.js:160