npm.io
0.5.1 • Published 2d ago

markloom

Licence
MIT
Version
0.5.1
Deps
7
Size
1.2 MB
Vulns
0
Weekly
0

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/plain as MD; text/html as 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, contentEditable fully owned via beforeinput → 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

Keywords