codr.tools
Tools

Markdown to HTML previewer

Render Markdown to HTML in your browser, with a live preview beside the generated source. CommonMark or GitHub Flavored Markdown, raw HTML escaped rather than rendered, and dangerous link schemes dropped.

Markdown flavour
Markdown flavour
Show
Output view

Raw HTML is escaped, so a tag you type here is shown as text rather than rendered.

Preview

Loading the Markdown parser…

Render Markdown to HTML in your browser, with a live preview beside the generated source. CommonMark or GitHub Flavored Markdown, raw HTML escaped rather than rendered, and dangerous link schemes dropped.

How to use this tool#

  1. Type or paste Markdown into the input box. Rendering happens as you type, so there is no convert button: the HTML updates on every keystroke and the preview catches up as soon as you pause. The example is there so you can see what the output looks like before you commit anything.
  2. Switch between Preview and HTML. Preview is the rendered document. Links in it look like links but do not go anywhere when clicked; their addresses are in the HTML view. HTML is the exact markup that produced it, character for character.
  3. Choose a flavour. GitHub Flavored adds tables, task lists, strikethrough, bare-URL links and footnotes. CommonMark is the plain standard those extensions sit on top of, which is what you want when you are checking whether a document is portable.
  4. Copy the HTML. The copy button always gives you the whole output, even when the box is showing a shortened version of a very long document.
  5. Read the notes under the boxes. They appear when raw HTML in your input was escaped, when a link or image URL was dropped for using a scheme that is not allowed, and when the preview is fetching images from the web.

The conversion runs in your browser, after the page has loaded. Nothing you paste is sent anywhere, because there is nowhere to send it: this site is static files with no backend.

Markdown, CommonMark and GFM#

Markdown was written by John Gruber in 2004 as a way to write for the web without writing HTML. He put the goal plainly on theoriginal project page: "The overriding design goal for Markdown's formatting syntax is to make it as readable as possible." A list should look like a list in the source, and nothing should be wrapped in angle brackets.

What it did not have was a specification. The original release was a Perl script and a page of prose, and the prose left a lot unsaid. Does a list item need a blank line before it? What does***a**b* mean? Every implementation answered differently, and the same document stopped rendering the same way in different places.

CommonMark is the answer to that: a precise specification, written mainly by John MacFarlane, with a suite of test cases so that two conforming parsers produce the same HTML for the same input. The current version is 0.31.2, published in January 2024, and it is the base almost every modern Markdown tool follows.

GitHub Flavored Markdown, usually shortened to GFM, is CommonMark plus five extensions GitHub wrote down in a spec of its own, version 0.29-gfm, dated April 2019. Its first rule is the one that matters: "GFM is a strict superset of CommonMark." Every CommonMark document is a valid GFM document and means the same thing, and anything GitHub supports beyond that is an extension.

This tool renders both. Switch to CommonMark when you want to know what a document does outside GitHub, for example in a static site generator that follows the base specification only.

The syntax, and the HTML it produces#

Everything below works in both flavours. The first box is what you write and the second is exactly what comes out, which is worth reading once: most Markdown surprises are easier to understand as "that is not the HTML I expected" than as a rule to memorise.

markdown
# Heading 1
## Heading 2

Plain text with *emphasis*, **strong emphasis** and `inline code`.

A second paragraph. This line and
the next one become one paragraph,
because a single newline is just a space.

- A bullet
- Another bullet
  - Nested, indented by two spaces

1. First
2. Second

> A block quote.

[A link](https://example.com "with a title")

![Alt text](https://example.com/photo.png)

```sql
select 1;
```

---
html
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<p>Plain text with <em>emphasis</em>, <strong>strong emphasis</strong> and <code>inline code</code>.</p>
<p>A second paragraph. This line and
the next one become one paragraph,
because a single newline is just a space.</p>
<ul>
<li>A bullet</li>
<li>Another bullet
<ul>
<li>Nested, indented by two spaces</li>
</ul>
</li>
</ul>
<ol>
<li>First</li>
<li>Second</li>
</ol>
<blockquote>
<p>A block quote.</p>
</blockquote>
<p><a href="https://example.com" title="with a title">A link</a></p>
<p><img src="https://example.com/photo.png" alt="Alt text" /></p>
<pre><code class="language-sql">select 1;
</code></pre>
<hr />

Three details in there catch people out. A single newline inside a paragraph is only a space, so the three source lines become one paragraph. A nested list has to reach the column where its parent's text starts, which is two spaces after a - marker. And a fenced block's info string becomes aclass="language-…" on the <code> element, which is what syntax highlighters look for. Nothing highlights it for you here; the class is the hook.

What GitHub Flavored Markdown adds#

markdown
| Left | Middle | Right |
| :--- | :----: | ----: |
| a    |   b    |     c |

- [x] Done
- [ ] Not done

~~Struck out~~

Bare links: https://example.com and me@example.com
html
<table>
<thead>
<tr>
<th align="left">Left</th>
<th align="center">Middle</th>
<th align="right">Right</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">a</td>
<td align="center">b</td>
<td align="right">c</td>
</tr>
</tbody>
</table>
<ul>
<li><input type="checkbox" disabled="" checked="" /> Done</li>
<li><input type="checkbox" disabled="" /> Not done</li>
</ul>
<p><del>Struck out</del></p>
<p>Bare links: <a href="https://example.com">https://example.com</a> and <a href="mailto:me@example.com">me@example.com</a></p>
  • Tables. A header row, a delimiter row of dashes, then the body. Colons in the delimiter row set the alignment, and they become an align attribute on every cell in the column. The outer pipes are optional; the delimiter row is not.
  • Task lists. - [ ] and - [x] at the start of a list item become a disabled checkbox. There must be a space after the closing bracket, and a capital [X] works as well as a lower case one.
  • Strikethrough. ~~text~~. A single tilde is accepted here as well, which not every implementation does.
  • Autolinks. A bare https://…, a www. address and an email address all become links without any brackets. A bare example.com with no scheme and no www. does not, which is the rule that surprises people.

Those four, plus a filter on nine raw HTML tag names, are the whole of the GFM specification.Footnotes are not in it: text[^1] with a matching[^1]: the note is a github.com feature, added in September 2021, several years after the spec was last revised. It is common enough that this tool renders it in GFM mode as well, building a numbered reference and a footnote section at the end with a link back to where you were reading. Treat it as a GitHub feature rather than a portable one.

Turn the flavour to CommonMark and all of that is inert: a table is a paragraph full of pipes, a task list item keeps its literal brackets, ~~text~~ keeps its tildes, and a bare URL stays plain text. That is the quickest way to find out whether a README depends on GitHub to look right.

How this page keeps the preview safe#

A Markdown previewer has a problem that a JSON formatter does not: to show you the rendered document it has to turn your input into HTML and hand it to the browser to draw. If any of that HTML comes from your input unchecked, the tool has handed your browser over to whoever wrote the Markdown.

Markdown makes that easy, because the syntax deliberately lets raw HTML through. The CommonMark specification says so in as many words: "An HTML block is a group of lines that is treated as raw HTML (and will not be escaped in HTML output)." A conforming parser, asked politely, will hand you a <script> tag. GFM narrows that a little with itstagfilter extension, which escapes nine tag names, among themscript, style, iframe and textarea, and leaves every other tag untouched. Even GitHub does not rely on it alone: its own spec notes that github.com performs "additional post-processing and sanitization after GFM is converted to HTML to ensure security".

Four layers apply here. The first two are the parser's own rules rather than a cleanup pass afterwards, the third checks the parser's work, and the fourth seals off where the result is drawn.

Raw HTML is escaped, never rendered

Anything that looks like a tag is written out as text. A < becomes&lt; and the tag appears in the preview as the characters you typed.

text
Markdown in:

Press <kbd>Ctrl</kbd> to continue.

<div class="note">A raw block.</div>

<script>alert(1)</script>

HTML out:

<p>Press &lt;kbd&gt;Ctrl&lt;/kbd&gt; to continue.</p>
&lt;div class=&quot;note&quot;&gt;A raw block.&lt;/div&gt;
&lt;script&gt;alert(1)&lt;/script&gt;

So a <script> cannot run, an onerror attribute cannot exist, and a <style> block cannot repaint the page. The cost is that the raw HTML tricks people use in READMEs, such as <kbd> keys, a centred image or a<details> block, are shown as text here. That is a deliberate trade, and the note under the boxes tells you when it happened so you are never guessing.

Link and image URLs are limited to known schemes

A Markdown link can hold any URL, including javascript:, which executes when the link is clicked. Only http, https, mailto,xmpp, irc and ircs survive here, which is the set github.com allows. Images are stricter still: http and https only, so a data: image is dropped as well. Anything else is emptied, leaving a link that goes nowhere.

text
Markdown in:

[Looks harmless](javascript:alert(document.cookie))
![Badge](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==)
[A real link](https://example.com/docs)
[A relative link](/docs/index.html)

HTML out:

<p><a href="">Looks harmless</a>
<img src="" alt="Badge" />
<a href="https://example.com/docs">A real link</a>
<a href="/docs/index.html">A relative link</a></p>

This is the normal posture rather than an unusual one. cmark, the CommonMark reference implementation, empties the same schemes by default and calls the flag that turns them back on --unsafe.

A relative path or a fragment has no scheme, so it is kept untouched. A colon that comes after a/, ? or # is part of the path or the query rather than a scheme, so /go?to=javascript:x is kept as written. Wherever you use the HTML, following that link goes to /go on the site it is published on, with the rest as an ordinary query parameter.

Belt and braces

Because the parser is the only thing standing between your input and the page, the tool does not take its word for it. Every piece of HTML it generates is checked against a list of the elements and attributes the parser is able to produce, and against the scheme rule above, before it is shown. If anything unexpected ever appeared, the preview would refuse to render and show an error instead of markup. It is not a sanitizer, and it does not try to clean anything up. It is a second opinion that fails closed.

The preview is drawn in a sealed frame

The checked HTML is not placed in this page at all. It is shown in a frame of its own, an<iframe> with an empty sandbox attribute, which is the strictest setting a browser offers: nothing inside can run a script, submit a form, open a window or navigate this page. The browser also gives the frame an origin of its own that matches no real site, so it cannot read this page, its storage or its cookies. The frame's document also carries its own Content Security Policy, which allows its stylesheet and images from the web and refuses everything else.

Two things follow from that. Links in the preview are drawn as links but cannot be followed, because a sealed frame has nowhere safe to send you; the addresses are all in the HTML view. And the preview is set in your system's fonts rather than this site's, because the frame is not allowed to download them.

Gotchas that catch everyone#

One newline is not a line break

Markdown joins consecutive lines into one paragraph. To force a break inside a paragraph you need a backslash at the end of the line, or two trailing spaces. The backslash is the better habit: trailing whitespace is invisible, editors strip it on save, and reviewers cannot see it in a diff.

markdown
A single newline
does nothing.

A backslash at the end of a line\
forces a break.

Two trailing spaces do the same,
but you cannot see them in a diff.

Indentation inside a list is measured from the text

A paragraph, a code block or a nested list that belongs to a list item has to be indented to where that item's text starts, which is two spaces for - and three for1. . Get it wrong by one space and the block escapes the list.

markdown
1. Step one

   A paragraph that belongs to step one:
   three spaces of indentation, matching
   the width of "1. ".

2. Step two

- A bullet

  A paragraph inside it needs two spaces,
  matching the width of "- ".

Tables need the delimiter row, and pad the ragged ones

A table is only a table when the second line is a row of dashes. Without it you get a paragraph full of pipe characters. Once it is a table, the header row decides the column count: short rows are padded with empty cells and extra cells are dropped.

markdown
| Works | Because |
| ----- | ------- |
| yes   | the second row is a delimiter row |

| Broken |
| the delimiter row is missing, so this |
| is one paragraph of text |

| Ragged | Rows |
| ------ | ---- |
| padded |
| too | many | cells |

A literal pipe inside a cell has to be written \|, even inside a code span, because the table is split into cells before the cell contents are parsed.

Underscores inside a word do nothing

snake_case_name stays as it is. CommonMark only treats _ as emphasis at the edge of a word, which is exactly so that identifiers survive. Asterisks have no such rule, so a*b*c does put b in italics.

A heading needs a space after the hashes

#NoSpace is a paragraph that starts with a hash. # Heading is a heading. The same goes for a task list item: - [x]done keeps its brackets, and- [x] done gets a checkbox.

Front matter is not Markdown

A post that opens with a --- block of YAML comes out looking wrong here, and that is correct behaviour rather than a bug. Front matter is a convention of static site generators, not part of any Markdown specification, so a plain parser reads the opening --- as a thematic break and the closing one as a setext heading underline, which turns your metadata into an <h2>.

text
Markdown in:

---
title: A post
tags: [a, b]
---

# Body

HTML out:

<hr />
<h2>title: A post
tags: [a, b]</h2>
<h1>Body</h1>

Delete the block before pasting, and check the metadata itself with theYAML to JSON converter if that is what you were really after.

Character references are resolved

&copy; comes out as ©, and &#65; as A. If you want the literal text, escape the ampersand with a backslash. The same applies to link destinations, which is why an obfuscated &#106;avascript: URL is still recognised and dropped.

Using the HTML you copy#

What you get is a fragment, not a page: no <html>,<head> or <body>, and no styling. Drop it inside a container in your own template and it inherits your CSS. If you want a standalone file, wrap it yourself, or use pandoc --standalone, which is what that flag is for.

There is no styling attached because Markdown carries none. The nice look you are used to on GitHub is GitHub's stylesheet, not something in the document. Headings come out as plain<h1> to <h6>, and a table is a bare<table> with no classes at all.

Two things worth knowing before you paste the output into a page. The heading levels are whatever the author wrote, so a document that starts at # will inject a second<h1> into a page that already has one. And the ids that footnotes generate are global to the page, so two rendered documents on the same page can collide.

Converting on the command line#

pandoc

shell
# Markdown to a standalone HTML page
pandoc -f gfm -t html --standalone -o out.html in.md

# Just the fragment, which is what this tool shows
pandoc -f gfm -t html -o out.html in.md

# Strict CommonMark instead of GitHub's dialect
pandoc -f commonmark -t html -o out.html in.md

# Pandoc's own default dialect is neither: -f markdown means
# pandoc-flavoured Markdown, with footnotes, definition lists
# and a lot more. Name the input format explicitly.

cmark

shell
# cmark: the CommonMark reference implementation, in C
cmark in.md > out.html

# It is safe by default, and its man page spells out what that means:
#   --safe   Omit raw HTML and potentially dangerous URLs (default).
#            Raw HTML is replaced by a placeholder comment and
#            potentially dangerous URLs are replaced by empty strings.
# --unsafe turns both back on. The README's advice if you do: "we
# recommend you use a HTML sanitizer specific to your needs to
# protect against XSS attacks."

# cmark-gfm, GitHub's fork, takes the extensions one at a time
cmark-gfm -e table -e strikethrough -e autolink \
          -e tasklist -e tagfilter -e footnotes in.md > out.html

# The names are singular: table, not tables
cmark-gfm --list-extensions

Python

shell
# pip install markdown
python -m markdown in.md > out.html

# Extensions are named one at a time. Tables and fenced code blocks
# are the two that get you closest to GFM.
python -m markdown -x tables -x fenced_code in.md > out.html

# Its own docs carry the warning this page exists to make concrete:
# "The Python-Markdown library does not sanitize its HTML output. If
# you are processing Markdown input from an untrusted source, it is
# your responsibility to ensure that it is properly sanitized."

Node

javascript
// npm install micromark micromark-extension-gfm
import { micromark } from 'micromark';
import { gfm, gfmHtml } from 'micromark-extension-gfm';

const html = micromark(markdown, {
  extensions: [gfm()],
  htmlExtensions: [gfmHtml()],
});

// No option is passed for raw HTML or for link protocols, so
// allowDangerousHtml and allowDangerousProtocol stay false.
// That is the whole safety story, and it is the same call this
// page makes.

Questions people ask#

Can I convert HTML back to Markdown here?

No. This tool goes one way. The reverse is a genuinely different job: HTML has constructs Markdown cannot express, so any converter has to decide what to drop or leave as raw HTML.pandoc -f html -t gfm does it well if you need it.

Why is my <kbd> tag showing as text?

Because raw HTML is escaped here on purpose, so that a preview of a document you did not write cannot run code in your browser. GitHub renders a small allowed set of tags after filtering the dangerous ones. This page takes the simpler and safer route of rendering none of them. The note under the boxes tells you when your input contained some.

Why can I not click the links in the preview?

Because the preview is a sealed frame, and no link inside it is allowed to take you anywhere. Switch to the HTML view to see where each one points. If a link there has an emptyhref="", its URL used a scheme that is not on the allowed list, so it was emptied. That is javascript: and vbscript:, which execute, anddata: and file:, which point at content the page has no business loading. The note counts how many were dropped.

Does this match what GitHub shows?

For the syntax, very closely: the same extension set, the same table alignment, the same autolink rules, and footnotes on top. It differs in two places on purpose. GitHub renders a filtered subset of raw HTML and this page renders none. And the additions github.com has made since the spec was last revised in 2019, such as alert blocks written > [!NOTE], which arrived at the end of 2023, and @user mentions, are not part of GFM, so they come out as ordinary text here.

Is my document uploaded anywhere?

No. The parser runs in your browser and the site has no backend to receive anything. The one request the page can make on your behalf is fetching an image your own Markdown points at, and a note appears when that happens. Disconnect from the network after the page has loaded and the tool keeps working, which is the quickest way to prove it.

What about very large files?

They are converted in your browser like anything else, so the limit is your machine rather than an upload size. A README or a long article is instant. The HTML box shows the first 200,000 characters to keep scrolling smooth, while copy gives you the whole thing.

Which Markdown does a static site generator use?

It varies, and it is worth checking before you rely on an extension. Many generators follow CommonMark and switch GFM on by default; others ship their own dialect with footnotes, definition lists or front matter on top. Preview a document here in CommonMark mode first: if it still looks right, it will travel anywhere.

  • Convert YAML to JSON and JSON back to YAML in your browser, following YAML 1.2, with anchors and merge keys resolved, multi-document streams, and syntax errors reported by line and column.

  • Compare two texts or two JSON documents in your browser: a line diff with word and character highlighting inside changed lines, unified and side-by-side views, options to ignore whitespace, case and blank lines, and a copyable unified patch.

  • Convert CSV to JSON and JSON back to CSV in your browser, following RFC 4180, with the delimiter detected, quoted fields and line breaks handled, optional type inference, and errors reported by line and column.

  • Convert identifiers and text to camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, Train-Case, dot.case, path/case and four more at once, one line at a time, with acronyms and digits split the way style guides expect.