What this markdown preview does
Markdown is a plain-text formatting syntax invented by John Gruber in 2004 with a single goal: a document should be readable as-is, before anything converts it. Two decades later it is the format of README files, changelogs, issue trackers, static site generators, chat clients and most API documentation. A markdown preview tool answers the immediate question — what will this actually look like — and a markdown to HTML converter answers the next one: what markup does it become.
This page does both. Type on the left, see the rendered result and switch the view to read the exact HTML produced. The HTML pane is not a re-implementation; it is a serialisation of the very DOM you are looking at, so the two panes cannot disagree.
A subset, stated plainly
Markdown has no single definition. Gruber's original description was prose plus a Perl script, and it left enough ambiguity that every implementation diverged. CommonMark exists to fix that with a specification long enough to be genuinely difficult to implement, and GitHub Flavored Markdown layers tables, strikethrough, task lists and autolinking on top. This tool implements the common subset that covers the overwhelming majority of real documents, and it does not pretend otherwise.
| Supported | Not supported |
|---|---|
ATX headings # … ###### and setext underlines | Reference-style links and images [a][b] |
| Bold, italic, strikethrough, inline code | Footnotes and definition lists |
Fenced code blocks with ``` or ~~~ | Indented (four-space) code blocks |
| Ordered and unordered lists, one level of nesting | Task list checkboxes |
| Blockquotes, including nested | Raw HTML blocks and inline HTML |
| Horizontal rules and pipe tables | Entity references such as © |
Links, images, and <https://…> autolinks | Column alignment in tables |
| Backslash escapes and hard line breaks | CommonMark's full emphasis-flanking rules |
If a document renders differently here and on GitHub, assume GitHub is correct. This is a preview and a conversion aid, not a reference implementation, and treating it as one would be the mistake.
Why rendering Markdown safely is harder than it looks
Markdown converters are a classic source of cross-site scripting, and the reason is structural. The output of a converter is markup. Anything that reaches the output unescaped becomes markup. And the input, in every interesting deployment, is written by someone other than the person running the converter — a comment, a profile bio, a pull request description, a webhook payload rendered into an admin dashboard.
There are three separate ways to get this wrong, and most vulnerable implementations get at least two.
1. Honouring raw HTML
Gruber's original Markdown deliberately allows inline HTML to pass through untouched, and many libraries
keep that behaviour by default. It is a reasonable choice for a single-author blog and a catastrophic one
for user-generated content, because <script>alert(1)</script> is valid Markdown
that a permissive renderer will happily emit. This tool does not honour raw HTML at all. Every character of
the source that is not a Markdown construct becomes a text node, so an <iframe>, an
<img onerror> or a <script> shows up on screen as the characters you
typed. Paste one into the box above and watch it appear as text.
2. Building output as a string
The usual architecture is to build an HTML string and hand it to innerHTML. That works, and it
means the escaping is only as good as its weakest branch — one place where a heading's text, a link title
or a table cell is concatenated without escaping, and the whole thing is exploitable. This parser never
produces an HTML string for display. It calls document.createElement and sets
textContent, so escaping is not a step that can be forgotten; it is a property of the API. The
HTML pane is generated afterwards by walking that finished DOM and escaping as it goes, which means the
markup you copy is a description of what was rendered rather than the thing that produced it.
3. Trusting URLs
Even a converter that escapes text perfectly will happily write your URL into an href. And
[click me](javascript:alert(document.cookie)) is a well-formed Markdown link. The
javascript: scheme executes on click; data:text/html loads an attacker-controlled
document; data:image/svg+xml in an image source can carry script in some contexts. So before
any URL is used here it is stripped of control characters and surrounding whitespace, its scheme is
extracted and lowercased, and anything that is not http, https,
mailto, tel or ftp is refused outright. A rejected link renders as
plain text with no anchor; a rejected image renders as its alt text. Relative paths and
#fragment links have no scheme and pass through untouched.
Two details in that check matter more than they look. Stripping control characters first defeats the old
java script: trick, where a tab inside the scheme is ignored by the URL parser but breaks a
naive prefix test. And because attributes are set with setAttribute rather than written into
markup, an HTML entity such as javascript: is never decoded into anything, so it
cannot re-form into a live scheme.
If you are shipping a Markdown pipeline of your own, the takeaway is: escape first, never render raw HTML from untrusted authors, sanitise URL schemes, and add a Content Security Policy as the layer that catches the bug you missed. Rendering someone else's Markdown is accepting someone else's input, and the same instincts apply as when you handle a request body — see the notes on authenticating and trusting callers and the reasons a webhook payload should never be treated as friendly.
Markdown syntax quick reference
| You write | You get |
|---|---|
# Title … ###### Small | Headings, levels one to six |
**bold** or __bold__ | Strong emphasis |
*italic* or _italic_ | Emphasis |
~~struck~~ | Strikethrough |
`code` | Inline code |
| Three backticks, then lines, then three backticks | A fenced code block |
[text](https://example.com) | A link |
 | An image |
- item or * item | An unordered list |
1. item | An ordered list |
> quoted | A blockquote |
--- on its own line | A horizontal rule |
| a | b | then | --- | --- | | A table |
| Two spaces at end of line | A hard line break |
\*literal\* | An escaped character |
Two rules cause most of the confusion people bring to a preview tool. First, blank lines are structural: a list, a table or a code fence that follows a paragraph without a blank line between them will usually be swallowed into that paragraph. Second, a single newline is not a line break. Markdown joins consecutive lines into one paragraph, which is what makes hand-wrapped source files work. To force a break, end the line with two spaces — invisible, which is why many people prefer to leave a blank line and start a new paragraph instead.
Inline code is the escape hatch worth remembering. Anything inside backticks is taken literally, so
`*not italic*` renders with its asterisks intact. When your code sample itself contains
backticks, use more of them on the outside than appear on the inside — the parser matches a run of
backticks with an equal run.
Where Markdown meets API work
Markdown is the connective tissue of a documented API. OpenAPI descriptions accept Markdown in nearly every
description field, so the sentence you write in a schema becomes rendered prose in whatever
documentation viewer your team uses — and a stray underscore in a field name can silently turn half a
paragraph italic. Previewing the string before it goes into the spec is faster than regenerating a docs
site to find out. If you work with specifications directly, the notes on
Swagger and the OpenAPI toolchain cover where those descriptions
surface.
The same applies to changelogs, pull request templates, chat notifications and error messages that end up in a Markdown-rendering surface. A release note posted to a chat webhook is Markdown in a JSON string, which means it is double-escaped and easy to break; formatting the payload with the JSON formatter first makes the backslashes visible. And when you need filler to see how a long document behaves in a template, the lorem ipsum generator will produce paragraphs already wrapped in tags.
One more practical case: reading API responses that contain Markdown. Plenty of services return description, body or content fields written in Markdown, and pasting one into this preview is the quickest way to confirm the escaping survived the round trip. Send the request with the API tester, copy the field out of the response, and drop it in above. If it renders wrong, the problem is usually one of encoding rather than syntax — check the Content-Type header and, if the body arrived percent-encoded, run it back through the URL decoder before judging the Markdown.
Frequently asked questions
Does this markdown preview support full CommonMark or GitHub Flavored Markdown?
No, and it says so in the tool as well. This is a hand-written parser covering the common subset: ATX and setext headings, bold, italic, strikethrough, inline code, fenced code blocks, links, images, ordered and unordered lists including one level of nesting, blockquotes, horizontal rules and pipe tables. It does not implement reference-style links, footnotes, definition lists, task lists, HTML blocks, entity references, autolink extensions, or the many edge cases CommonMark specifies for overlapping emphasis. If a document renders differently on GitHub, GitHub is right.
Is raw HTML inside my Markdown rendered?
No. Raw HTML is displayed as literal text. That is a deliberate security decision rather than a missing
feature. Real Markdown permits inline HTML, which means any renderer that honours it is one careless input
away from cross-site scripting. This tool builds its output by creating DOM nodes and setting
textContent, never by assigning an HTML string, so a <script> tag, an
<iframe> or an onerror attribute in the source can only ever become visible
characters on the page.
What happens to a javascript: link in Markdown?
It is rejected and the link text is rendered as plain text with no anchor at all. Before any URL is used,
control characters and whitespace are stripped, the scheme is extracted and lowercased, and anything outside
http, https, mailto, tel and ftp is refused.
The same check runs on image sources, so a data: URL cannot smuggle an SVG containing script.
Relative URLs and fragment links are allowed through unchanged.
Can I use this as a markdown to HTML converter?
Yes. The HTML pane shows the exact markup produced from your Markdown, generated by walking the rendered DOM and escaping every text value, and the copy button puts it on your clipboard. It emits plain semantic tags with no classes, no inline styles and no wrapper elements, so it drops into a template or an email body cleanly.
Why does my Markdown table not render?
A table needs a header row followed immediately by a delimiter row made of dashes and pipes, such as three dashes between each pair of pipes. Without that second line the block is just a paragraph containing pipe characters. Column alignment colons are parsed but ignored here, because applying alignment would require inline styles. A blank line before the table is also required if a paragraph precedes it.
Is my document uploaded anywhere?
No. The parser is plain JavaScript in this page and there is no network request, no logging and no storage. You can paste an unpublished README, a draft changelog or an internal runbook and it stays on your machine, and the tool keeps working with the connection turned off.
Related tools and reference
HTML Encoder
Escape or unescape markup by hand, and see exactly which characters matter.
Lorem Ipsum Generator
Placeholder paragraphs to test how a long Markdown document lays out.
JSON Formatter
Unpick Markdown that arrived inside a JSON string field.
Diff Checker
Compare two revisions of a README or changelog line by line.
Regex Tester
Build the pattern you were about to use to parse Markdown — and see why you should not.
HTTP Headers
Content-Type, charset and the headers that decide how text is interpreted.