Minimal markdown renderer
Renders markdown to DOM nodes, never to an HTML string. There is no innerHTML in the call path, so there is no injection sink to sanitise. This page is a single self-contained file and works over file://.
Renders markdown to DOM nodes, never to an HTML string. There is no innerHTML in the call path, so there is no injection sink to sanitise. This page is a single self-contained file and works over file://.
Edit the left pane. The right pane is real DOM built by the renderer — not an HTML string that was parsed back. Note what happens to the hostile links and the raw <script> tag near the bottom.
A documented subset, not a CommonMark implementation. Everything below is covered by tests.
| Construct | Example | Renders as |
|---|---|---|
| Headings | # through ###### | h1–h6 |
| Paragraphs | text | p |
| Fenced code | three backticks, then a language | pre > code.language-js |
| Blockquotes | > quoted | blockquote, nestable |
| Unordered lists | - * + | ul, nestable, tight/loose |
| Ordered lists | 1. 1) | ol with start |
| Thematic breaks | --- *** ___ | hr |
| Tables | | a | b | | table with alignment |
| Emphasis | *a* _a_ | em |
| Strong | **a** __a__ | strong |
| Strikethrough | ~~a~~ | del |
| Inline code | backtick-delimited; a doubled backtick escapes a literal one | code |
| Links | [t](url "title") | a, scheme-checked |
| Images |  | img, scheme-checked |
| Autolinks | <https://…> | a |
| Hard breaks | two trailing spaces, or \ | br |
| Escapes | \* | literal |
| Raw HTML | <div> | not supported — escaped to text, deliberate |
| HTML comments | <!-- a --> | not supported — dropped; inline ones stay text |
| Setext headings | a line, then --- | not supported — paragraph + hr |
| Indented code | four leading spaces | not supported — paragraph |
| Reference links | [a][b] | not supported — literal text |
| Footnotes, math, task lists | — | out of scope |
http:, https:, mailto:, and relative. Everything else renders as plain text, not as a dead link. data: is rejected even for images. A urlFilter option runs after the allowlist for callers who need a tighter policy still — a host allowlist, or routing images through a proxy so a render never fetches an attacker-chosen URL directly. Relevant if the markdown itself isn't trusted, e.g. LLM output.