Svelte 5 · contenteditable markdown

@kevinpeckham/woof-editor

A WYSIWYG surface that serializes every edit back through barkdown's property-tested round-trip codec — what you edit is exactly the markdown you save, with no drift.

bun add @kevinpeckham/woof-editor

Live playground

Serialized markdown (live)

# Try editing this

Every edit round-trips through **barkdown**'s codec — *italic*, ~~strikethrough~~, `inline code`, and a [link](https://github.com/kevinpeckham/woof-editor) all serialize back to the exact markdown a parser would produce.

Select text for the bubble menu, right-click for the context menu, or click the gutter chip to the left of any block to change its type.

## Two kinds of lists

- Bulleted items work as expected
- So does **emphasis** inside a bullet
- Undo, redo, and save are wired to the toolbar above

1. Numbered lists too
2. Keyboard shortcuts are listed below the editor

> Round-trip means the markdown you started with and the markdown you get back after editing are structurally identical — no drift, no diff noise.

```ts
const editor = new MarkdownEditorState({ markdown: "# Hello" });
```

This editor understands footnotes, too[^1].

[^1]: Click the footnote number to edit its definition inline.

This page styles the document via the class prop; menus are themed with two CSS variables.

⌘/Ctrl+Z undo ⌘/Ctrl+Shift+Z redo ⌘/Ctrl+Alt+0/2/3/4 block types

Why another markdown WYSIWYG

Most contenteditable markdown editors round-trip lossily: markdown renders to DOM, you edit the DOM, and a separate serializer writes markdown back out through a different path than the one that read it in. Content drifts a little on every save — extra whitespace, quote-style changes, list markers that flip.

woof-editor serializes through @kevinpeckham/barkdown instead, whose DOM → markdown writer is property-tested to invert marked's parser: toMarkdown(toDom(md)) === md for every canonical input.

That guarantee is what lets you store editor output as canonical markdown in a database or a git repo — CMS bodies that survive edit → save → reload cycles without accumulating cruft.

Usage

<script lang="ts">
  import { MarkdownEditor, MarkdownEditorState } from "@kevinpeckham/woof-editor";

  const editor = new MarkdownEditorState({
    markdown: "# Hello\n\nEditable markdown, round-tripped through barkdown.",
  });

  async function save() {
    await fetch("/api/articles/123", {
      method: "PUT",
      body: JSON.stringify({ markdown: editor.markdownCurrent }),
    });
    editor.markAsSaved(); // flips `hasEdits` to false without re-seeding the DOM
  }
</script>

<MarkdownEditor {editor} />

<button disabled={!editor.canUndo} onclick={() => editor.undo()}>Undo</button>
<button disabled={!editor.canRedo} onclick={() => editor.redo()}>Redo</button>
<button disabled={!editor.hasEdits} onclick={save}>Save</button>

MIT · GitHub · npm · built with woof-editor v0.2.0