markloom
markloom
The loom for Markdown — rich text that stays honest with MD, and that AI can write into without a second model.
Most editors pick one: a nice WYSIWYG that mangled MD on export, or a MD source that never becomes a real structured tree. Product teams re-glue paste, IME, undo, and AI cards. Agents stream text into a DOM they don’t own.
markloom is one document model: a schema-constrained tree, reversible Steps, Markdown as an isomorphic projection (not “export later”), and an optional AI transaction on real blocks. Kernel without DOM; view when you need an editor.
npm i markloom
import { Editor } from 'markloom/view'
import 'markloom/view/theme.css'
const ed = new Editor(root, doc, {
onChange: d => save(d), // or persist with docToMd(d)
})
What you get
1. Markdown as a first-class citizen (lossless where it matters)
- Tree Markdown isomorphism via mdast:
mdToDoc/docToMd. - Dirty-block local re-stringify; clean blocks keep source spans so untouched prose doesn’t get reformatted away.
- Paste
text/plainas MD;text/htmlas structured blocks — one pipeline, not two half-broken converters. - AI and humans can both speak Markdown; the document remains a real tree (headings, lists, code, marks…), not a single string with hope.
This is not “we support export to .md.” The model is built so MD and the editor share the same shape.
2. Real structure, reversible edits
- Document = ordered tree under schema meta-bounds (depth / type budget) — expressive nested content without unbounded entropy.
- Every change is a
Step: insert/delete text, replace node, splice children. apply ∘ invert = id: undo, history, and AI rollback are the same mechanism — not a parallel “AI history.”- Stable block ids + path dual: agents address
id, paths move when structure moves. diffById: block-level added / changed / removed / same for accept–reject UIs.
3. Fully controlled editor (DOM is projection)
markloom/view: Preact,contentEditablefully owned viabeforeinput→ Steps.- Model is the only truth; DOM cannot drift into a second document.
- IME handled as a first-class path (composition aligned to the model, not “hope the browser is fine”).
- Slash menu, bubble format bar, drag handle, gaps around atoms — product surface without forking the kernel.
4. AI write as a document transaction
One options field: /ai, format bar, real draft blocks, accept / undo — same tree and history. Example below; full transports in ai.md.
5. Use the piece you need
markloom tree, Steps, history, MD, appendText / diffById
markloom/view editor + view/theme.css
markloom/ai AI controller + stream helper + ai/theme.css
markloom/collab realtime OT client (pair with worker/)
markloom/react optional React wrapper (peer react, optional)
Kernel only (agents, SSG, no browser):
import { apply, appendText, mdToDoc, docToMd, diffById } from 'markloom'
const doc = mdToDoc(source)
const next = apply(doc, appendText(doc, blockId, token))
await save(docToMd(next))
Editor + AI (one example; proxy / provider / custom → ai.md):
npm i markloom ai @ai-sdk/openai
import { docToMd } from 'markloom'
import { Editor } from 'markloom/view'
import { streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import 'markloom/view/theme.css'
import 'markloom/ai/theme.css'
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! })
new Editor(el, doc, {
onChange: d => save(docToMd(d)),
ai: {
stream: async (input, onToken) => {
const result = streamText({
model: openai('gpt-4o-mini'),
prompt: input.context,
abortSignal: input.signal,
})
for await (const t of result.textStream) onToken(t)
},
},
})
Realtime collab (one field; worker + protocol → collab.md):
new Editor(el, doc, {
collab: { url: 'ws://127.0.0.1:8787', room: 'demo', name: 'Alice' },
})
Why this stack
| Problem | Typical approach | markloom |
|---|---|---|
| MD round-trip ruins formatting | Best-effort export | Isomorphic projection + clean-block spans |
| AI preview ≠ document | Overlay then paste | Stream into real blocks; accept = commit |
| Undo of AI is messy | Multiple history systems | One Step history; AI amend + undo |
| Agent can’t address blocks | CSS selectors / offsets | Stable id + pathOfId |
| Want headless transforms | Fork the editor | Zero-DOM kernel |
Runtime dependencies
Preact, @floating-ui/dom, unified/mdast GFM (mdast-util-*, micromark-extension-gfm).
No ProseMirror / Tiptap.
| Optional | When |
|---|---|
react ≥ 18 |
Only if you use markloom/react |
Limits (honest)
- Collaboration: client + Cloudflare Worker shipped (
collab: { url, room }— see collab.md). TP2 / auth / multi-caret chrome still thin. - Tables are atoms (full grid model inside; not free nesting in the prose grammar).
License
MIT