NABI NOTE
Docs

How it works

Three ideas carry the rest. Values travel one path each way, edits leave through one gate, and the layers stack bottom-up.


Values travel one path each way

Going in and coming out, both pass through the same filter.

sourceHtmlhtml option · setHtml() · paste
filterallow-list
soulthe document itself
flutterthe live DOM being edited
On the way in — whatever HTML the host hands over goes through the filter and becomes the document itself before anything reaches the screen.
flutterthe DOM on screen right now
filterthe same allow-list
soulderived again
outputHtmlonChange · getHtml() · clipboard
On the way out — the live DOM is never shipped as-is. It goes back through the filter, and the freshly derived document gets its outer layer put on.

Four properties fall out of that one picture.

  • getHtml() never hands back the incidental markup a browser leaves behind while editing. It re-filters the current DOM every time.
  • The filter is idempotent. Feeding an onChange value straight back into setHtml() does nothing at all.
  • The round trip is lossless. What gets put on for the way out comes off on the way in.
  • Values are always compared soul against soul. Comparing an output string with the document itself always differs, and the caret jumps.

Why the same document goes by four names, and what each one means, is in the Glossary.

Rendering HTML you stored

The safety of getHtml() is about the string you just made. When you render HTML that went to a database and came back, you still need sanitizing on your side.


Edits leave through one gate

Every path that changes the document goes through the same gate: toolbar buttons, input rules, paste, setHtml(), and undo alike.

an editcommand · wing · paste
the gateevery path that changes the document
restore invariants
undo snapshot
refresh statetoolbar · marks · selection
onChangeto the host
Four things happen together inside the gate. Restoring history takes the same path, only without leaving a new snapshot.

So an edit that skips the gate cannot be undone and never reaches the host. That is why a wing that touched the document directly must call context.commit().


The layers stack bottom-up

editorthe facade and its controllers — they know one shared surface, never each other
uifloating boxes — placement, stacking order, the UI wings are handed
wingsthe wings that ship in the box — they use only the contract and the flower
inputbrowser events become commands — typing, paste, files, input rules
flowerwhere wings get their nectar — the one door into core functionality
commandspure editing functions — they change the document and answer whether they did
corepure document logic — DOM, selection, normalizing, filtering, history
contractthe wing contract — schema, context, handle, registry, locale, icons
A lower layer never knows an upper one. This is not a promise written in a document but a rule a test enforces mechanically — a forbidden import fails with the file name attached.

The layer worth pausing on is flower. It is the only door through which a wing reaches core functionality, and the wings that ship in the box use the same door as anyone else's. There is no shortcut reserved for the built-ins.

New features are wings. If it feels like a core file has to change, that is usually a sign that the contract is missing something.


Next