Live Markdown editor
▶ Run live · View source on GitHub
A live Markdown editor. ~30 lines of kerf, plus marked for parsing and DOMPurify for sanitisation.
Try typing fast — your cursor stays where you put it. That’s the morph at work.
What to look at:
- The editor pane is a
contenteditable. While focused, kerf preserves caret + multi-range selection automatically. The wrapper is also markeddata-morph-skipfor explicit, unconditional protection — the diff never recurses inside. computed(() => DOMPurify.sanitize(marked.parse(source.value)))is memoised. Toggle the source and the parse + sanitise pair runs exactly once, not once per consumer.raw(html.value)injects the cleaned HTML verbatim. No further escaping. Crucially:raw()is the contract that says “trust this string” — DOMPurify is what makes the contract honest.- One
delegate('input', '.editor-input', …)syncs typing back into the source signal. State flows in one direction (DOM → signal); the morph never writes the editor’s content back.
import { signal, computed, mount, raw, delegate } from 'kerfjs';import { marked } from 'marked';import DOMPurify from 'dompurify';
const source = signal('# Try typing fast\n\nThe cursor stays where you put it.');
const html = computed(() => DOMPurify.sanitize(marked.parse(source.value, { async: false }) as string),);
const root = document.getElementById('app')!;
mount(root, () => ( <div class="editor"> <div class="pane editor-pane" data-morph-skip> <div class="editor-input" contenteditable="plaintext-only" spellcheck="false"> {source.value} </div> </div> <article class="pane preview">{raw(html.value)}</article> </div>));
delegate(root, 'input', '.editor-input', (_e, el) => { source.value = (el as HTMLElement).innerText;});