Markdown Preview

Heading 1


Heading 2


Heading 3

This is a paragraph with bold and italic text.

  • List item 1

  • List item 2

  • List item 3

  • Numbered item 1

  • Numbered item 2

inline code

const hello = "world";
console.log(hello);

Blockquote text

Link text


| Column 1 | Column 2 |
|----------|----------|
| Cell 1 | Cell 2 |

CommonMark vs GitHub Flavored Markdown: What Actually Differs

Markdown started as a 2004 Perl script plus a loosely written syntax page, which is why implementations disagreed about edge cases for a decade. CommonMark (2014) fixed that: it is an unambiguous specification with more than 650 numbered examples that double as a conformance test suite. When two renderers disagree today, the CommonMark spec is the tiebreaker. GitHub Flavored Markdown (GFM) is a formal superset of CommonMark. It adds exactly five extensions: tables, task list items, strikethrough with two tildes, autolinks for bare URLs, and a tagfilter that strips dangerous raw HTML tags (script, title, iframe, style and a few others). Everything else you see on GitHub — alert blockquotes like [!NOTE], Mermaid diagrams, footnotes, emoji shortcodes — is GitHub product behavior layered on top, not part of the GFM spec, and it will not render on npm, GitLab, or most static site generators. The differences that bite in practice: CommonMark only autolinks URLs wrapped in angle brackets, while GFM linkifies bare www. and https:// text. GFM tables require a delimiter row of dashes under the header row, and a literal pipe inside a table cell must be escaped with a backslash. Task list checkboxes only work as the first thing inside a list item. If a document must render identically on several platforms, stay inside the CommonMark core and treat every extension as an explicit portability decision.
GFM extensions (will NOT render in strict CommonMark):

| Flag | Meaning |
|------|---------|
| -v   | verbose |

- [x] ship parser
- [ ] write docs

~~deprecated~~   www.example.com   <- GFM autolinks bare URLs

CommonMark-portable equivalents:
<https://www.example.com>          <- autolink works everywhere
Cell with \| escaped pipe          <- required inside GFM tables

Emphasis, Lists, and Line Breaks: The Rules Everyone Trips Over

Emphasis looks trivial and is actually the most complicated part of the CommonMark spec — the delimiter-run rules take pages. The practical takeaways: underscores do not create emphasis inside words, so snake_case_names survive untouched, but asterisks do (a*b*c emphasizes the b). Three asterisks nest bold and italic from the outside in. When emphasis fails to render, the cause is almost always whitespace on the wrong side of a delimiter — an opening asterisk followed by a space is not left-flanking, so it stays literal. Lists have two famous traps. First, indentation is relative: a nested item must be indented to the first content column of its parent, not a fixed four spaces. A parent marker of '1. ' means children align at column 3, but '10. ' pushes them to column 4. Second, loose vs tight lists: a single blank line between any two items wraps every item in paragraph tags, visibly increasing vertical spacing — a mixed loose/tight list is the most common cause of 'why does my list spacing look inconsistent'. Ordered lists honor only the first number: 7, 8, 9 renders as a list starting at 7, and the following numbers are ignored. A year followed by a period at the start of a line ('2026. It was...') silently becomes an ordered list — escape the period with a backslash. Line breaks: a single newline is a soft break and renders as a space in files. To force a hard break, end the line with two spaces or a backslash. GitHub comments and issues render single newlines as hard breaks, but README files do not — same syntax, different output, endless confusion.
snake_case_name          -> no emphasis (intraword underscore)
a*b*c                    -> a<em>b</em>c (asterisk works intraword)
* not italic *           -> literal (space after opening delimiter)

7. seven
8. eight                 -> renders as a list starting at 7

2026\. was a good year   -> escaped: stays a paragraph

line one··               (two trailing spaces = hard break)
line two\
line three               (backslash = hard break, survives autoformat)

Why Regex-Based Markdown Converters Break (and What Real Parsers Do)

A chain of string replacements can convert headings and bold text, but Markdown is not a regular language: constructs nest arbitrarily. A code span can contain asterisks that must not become emphasis, a fenced code block can contain lines starting with # that must not become headings, and a blockquote can contain a list that contains another blockquote. Regex pipelines process these in some fixed order, so one construct always ends up corrupting another. That is acceptable for a quick visual preview and dangerous for anything you publish. Spec-compliant parsers work in two phases, as described in the CommonMark spec's parsing strategy appendix: first the document is split into a tree of blocks (paragraphs, lists, quotes, code fences), then inline parsing runs only inside the blocks that allow it. This is exactly why a code fence can safely contain Markdown-looking text — inline rules never enter it. The mainstream JavaScript choices: markdown-it (fast, plugin-rich, CommonMark-compliant), micromark with remark (the smallest correct core plus an AST ecosystem, and the engine behind MDX), and marked (the oldest and fastest, historically looser on edge cases). The C reference implementation, cmark, parses megabytes per second and is what the spec tests were built against. Whatever you choose, run it against the spec's example suite once. A surprising number of libraries that call themselves Markdown parsers fail dozens of the 650+ conformance cases — and those failures are precisely the edge cases your users will eventually type.

Raw HTML and XSS: Sanitize Before You Render

Markdown passes raw HTML through by design — and that flexibility is the single biggest security hazard when rendering user-supplied Markdown. An img tag with an onerror attribute executes JavaScript. A perfectly valid Markdown link can point at a javascript: or data: URL. None of this requires HTML blocks; inline constructs are enough. Treat the renderer's HTML output as untrusted. The defense has three layers. First, disable raw HTML in the parser whenever you do not need it (markdown-it: set html to false; in the remark world, simply do not add rehype-raw). Second, restrict link and image destinations to an allowlist of schemes — http, https, mailto — because parsers consider a javascript: destination syntactically valid Markdown. Third, sanitize the final HTML with a real sanitizer such as DOMPurify; hand-rolled tag-stripping regexes are reliably bypassable with nested or malformed tags. GitHub's own pipeline is a good model: it renders GFM, applies the tagfilter extension (drops script, iframe, style and other risky tags), then runs an attribute-allowlist sanitizer before anything reaches the page. Note the order — sanitize after rendering, because Markdown syntax itself can construct HTML that was not visible in the source. One last trap: previewing untrusted Markdown in your own browser executes any HTML it contains. The sandboxing has to exist in the preview environment too, not just in the published output.
<!-- all of these are "valid Markdown" from a parser's view -->
<img src=x onerror=alert(document.cookie)>
[click me](javascript:alert(1))
[harmless](data:text/html;base64,PHNjcmlwdD4uLi48L3NjcmlwdD4=)

// safe rendering pipeline
const md = markdownit({ html: false, linkify: true });
const dirty = md.render(userInput);
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_URI_REGEXP: /^(https?|mailto):/i
});
container.innerHTML = clean;

Writing Portable Markdown: One Source, Many Renderers

The same .md file may be rendered by GitHub (GFM), your static site generator, npm's registry page, an IDE preview, and pandoc — each with a different engine. Hugo uses Goldmark (CommonMark-compliant); Jekyll defaults to kramdown, which is not CommonMark and adds its own syntax such as attribute lists in curly braces; pandoc has dozens of opt-in extensions. YAML front matter between triple-dash fences is not Markdown at all — renderers that do not expect it will print it as a table or as literal text at the top of your page. Portability rules that pay off: prefer ATX headings (hash marks) over Setext underlines. Always use fenced code blocks with an explicit language tag instead of 4-space indentation — indented code cannot express a language, and it interacts miserably with lists because the indentation is ambiguous. Put blank lines around every block element (lists, tables, fences), since lazy-continuation rules differ between engines. Never rely on hard breaks made of trailing spaces, which editors and formatters routinely strip on save — use a backslash instead. For long-lived documents, one-sentence-per-line is worth adopting: it makes git diffs reviewable at sentence granularity, renders identically because soft breaks collapse to spaces, and avoids the reflow churn where changing one word rewraps an entire paragraph and pollutes the blame history.
---
title: parsed as front matter by Hugo/Jekyll,
       printed as text (or a table!) by others
---

Portable                     Fragile
--------                     -------
# ATX heading                Setext heading underlined with ===
fenced block + language      4-space indented code (no language)
break with backslash \       break with two trailing spaces
blank line before a list     list glued to the previous paragraph
Last updated:

About this tool

A Markdown previewer renders CommonMark / GitHub-flavored Markdown to HTML in real time so you can see how a README, blog post, or documentation page will appear before pushing it. Markdown is the universal language for technical writing — every code repository, ticket system, and modern docs platform speaks it — so a quick preview tool saves a publish-and-fix cycle.

How to use

  1. Type or paste Markdown into the editor on the left.
  2. The preview pane on the right renders updates instantly.
  3. Use standard syntax: # headings, **bold**, `code`, ``` fenced blocks ```, [links](#).
  4. Click Copy HTML to grab the rendered output for use in CMS or email.
  5. Iterate until the layout looks right, then commit the Markdown source to your repo.

Common use cases

  • Drafting a README before pushing to GitHub or GitLab.
  • Previewing a blog post for a static site generator like Hugo or Astro.
  • Composing a release note that will be pasted into GitHub Releases.
  • Checking a Pull Request description renders correctly.
  • Producing HTML email content from a Markdown source for a newsletter.
  • Verifying a documentation page before opening a Docs PR.

Frequently asked questions

Q. Does this support GitHub-flavored extensions like task lists?

A. Yes — checkboxes, fenced code blocks, autolinks, and tables work. Some platform-specific features (alerts, mermaid diagrams) may render differently on GitHub itself.

Q. Why does my code block lose syntax highlighting?

A. The previewer renders structure, not full syntax themes. GitHub, Gitea, and most static site generators apply their own highlighting on top.

Q. Can I render math (LaTeX)?

A. Not in the basic preview. Some Markdown processors integrate KaTeX or MathJax — check whether your target platform supports it before relying on it.

Q. Is anything sent to a server?

A. No. The Markdown is parsed and rendered entirely in your browser.