Markdown

Markdown Cheat Sheet: CommonMark vs GitHub Syntax, Tested

2026-10-13 · 10 min read · Preview Markdown free →

I added a Markdown Preview to ToolPry because I kept hitting the same problem: a README looked perfect in my editor, then rendered as a wall of pipes and asterisks somewhere else. Markdown is not one language. There is CommonMark, a precise specification, and there is GitHub Flavored Markdown (GFM), which its spec describes as a strict superset of CommonMark that adds tables, task lists, strikethrough, extended autolinks and a raw-HTML filter. Most renderers follow one of the two, with small differences on top.

So instead of writing another cheat sheet from memory, I ran every example below through two real parsers: the reference CommonMark implementation (commonmark.js 0.31.2) and marked 9.1.6, the library ToolPry's preview uses with GFM enabled. Where they disagree, I say so.

Short version: headings, emphasis, lists, code, links and images work everywhere. Tables, task lists, strikethrough and bare-URL links are GFM extensions. They work on GitHub and in ToolPry's preview, but a strict CommonMark renderer prints them as plain text.

Quick Reference

ElementSyntaxCommonMarkGFM / marked
Heading# Title … ###### TitleYesYes
Bold / italic**bold**, *italic*YesYes
Hard line breaktwo trailing spaces, or \ at line endYesYes
Ordered list1. itemYesYes
Fenced code```js … ```YesYes
Link / image[text](url), ![alt](src)YesYes
Autolink<https://example.com>YesYes
Bare URL / www. linkwww.example.comNo (plain text)Yes
Table| A | B | + delimiter rowNo (plain text)Yes
Task list- [ ] todo, - [x] doneNo (plain text)Yes
Strikethrough~~text~~No (plain text)Yes

Headings Need a Space After the #

An ATX heading is one to six # characters followed by a space, a tab or the end of the line. That space is not optional. I tested #Heading in both parsers and both rendered it as a normal paragraph containing the literal text #Heading. This is a common reason a heading "doesn't work" after copying text from a chat app that strips spaces.

# Page title
## Section
### Subsection

#NotAHeading   <- renders as a paragraph

You can add closing hashes (## Section ##) if you like the look, but they must be preceded by a space and they change nothing in the output.

Emphasis: Why snake_case Survives but foo*bar*baz Doesn't

Both * and _ create emphasis: one for italics, two for bold. The difference shows up inside words. When I ran snake_case_name and foo*bar*baz through both parsers, the underscores stayed literal but the asterisks turned "bar" into italics. Underscores inside a word do not open emphasis; asterisks do.

That rule is a feature: it means Python identifiers and file names like my_file_name.txt are safe in prose. My habit is to use * for emphasis and _ for nothing, so I never have to think about it. If you need a literal asterisk, escape it: \*not emphasis\* renders as *not emphasis*.

Line Breaks: The Invisible Trap

A single newline inside a paragraph is a soft break. Both parsers kept line one and line two in the same paragraph, which a browser displays on one line. To force a <br> you need a hard break, and CommonMark gives you two ways:

  • End the line with two or more spaces.
  • End the line with a backslash \.

I recommend the backslash. Trailing spaces are invisible, many editors strip them on save, and code review tools often flag them. A backslash is visible and survives every editor.

One more nuance: marked has a breaks option that turns every single newline into <br>. Its documentation notes that this matches how GitHub renders comments, but not how it renders Markdown files. ToolPry's preview keeps the default (breaks off), so it behaves like a rendered README rather than a comment box.

Lists: Only the First Number Matters

Ordered lists take their starting number from the first item. When I tested 1. 1. 1., both parsers produced a normal list numbered 1, 2, 3. When I started a list at 3., both emitted <ol start="3"> and ignored the numbers on later items. Writing every item as 1. is a common trick that keeps diffs small when you reorder steps.

Nesting is where people get stuck: nested content must be indented to line up with the text of the parent item, not with its marker. For - one, the text starts two characters in, so two spaces of indentation are enough:

- Parent item
  - Child item (2 spaces)
  - Another child

1. Step one
   Continuation paragraph (3 spaces, because "1. " is 3 characters)

Ordered list numbers can be at most nine digits, and leading zeros are dropped, so 003. starts the list at 3.

Code: Inline Spans and Fenced Blocks

Wrap inline code in backticks: `npm install`. For blocks, open a fence with at least three backticks or three tildes, optionally followed by an info string, which most renderers use for syntax highlighting:

```javascript
const ok = JSON.parse(input);
```

Three rules from the CommonMark spec save real debugging time. The closing fence must use the same character as the opening one; you cannot open with backticks and close with tildes. It must be at least as long as the opening fence, which lets you wrap a block that itself contains three backticks inside a four-backtick fence. And an unclosed fence runs to the end of its container (usually the rest of the document), so a single missing fence can swallow everything below it. Backslash escapes do not work inside code, which is what you want: code is shown exactly as written.

Links, Images and Autolinks

[ToolPry](https://toolpry.com/ "Optional title")
![Alt text describing the image](/images/diagram.png)
<https://example.com>

The angle-bracket form is a CommonMark autolink and works everywhere. Bare URLs are different: in my test, www.example.com and https://example.com typed as plain text stayed plain text in CommonMark but became clickable links in marked. That is the GFM autolink extension at work. If your Markdown has to render in more than one place, use angle brackets or the full [text](url) form.

Always write real alt text for images. It is what screen readers announce, and it is shown when the image fails to load.

Tables (GFM)

A table is a header row, a delimiter row of hyphens, and zero or more data rows. Colons in the delimiter row set alignment: on the left for left-aligned, on the right for right-aligned, both for centred.

| Tool      | Runs in browser | Price |
| :-------- | :-------------: | ----: |
| ToolPry   |       Yes       |  Free |

The GFM spec has a few rules worth knowing. The header and the delimiter row must have the same number of cells, otherwise no table is recognised at all. Data rows are more forgiving: missing cells are filled in as empty and extra cells are ignored. Leading and trailing pipes are optional, though I always include them for readability. To put a literal pipe inside a cell, including inside inline code, escape it as \|. A table ends at the first blank line.

When I fed the same table to the CommonMark reference parser, it printed the pipes as a plain paragraph, which is what happens when a GFM README is rendered by a strict CommonMark tool.

Task Lists and Strikethrough (GFM)

- [ ] Write the draft
- [x] Check every rule in a real parser
~~Old instruction~~

In marked, [ ] became an unchecked box and both [x] and [X] became checked boxes, rendered as disabled checkboxes. Strikethrough worked with one tilde (~one~) and with two (~~two~~), but three tildes were left as literal text. I stick to two tildes because that is what most people recognise. In CommonMark, all of these stay plain text.

Raw HTML and Why Your Renderer Must Sanitize

Markdown lets you mix in raw HTML, which is handy for things Markdown cannot express, like <details> blocks. It is also a security problem. In my test, both parsers passed <script>alert(1)</script> straight through into the HTML output. CommonMark specifies this behaviour. The GFM spec adds a “tagfilter” that escapes <script> and a few other tags, but marked does not implement it, and a tag filter is not a sanitizer anyway. The marked documentation removed its built-in sanitize option in version 8 and recommends running the output through a sanitizer such as DOMPurify.

If you render user-submitted Markdown on a website, sanitize the HTML output. Converting Markdown to HTML and inserting it directly into the page is a cross-site scripting hole.

ToolPry's preview does this: every render is passed through the DOMPurify sanitizer (version 3.4.17) before it reaches the page, which strips scripts and event-handler attributes. Keep your own sanitizer up to date too. DOMPurify regularly fixes newly discovered bypasses, which is why I upgraded ToolPry's copy while writing this article.

Using ToolPry's Markdown Preview

The Markdown Preview renders as you type, entirely in your browser. Nothing you paste is uploaded, which matters when you are drafting internal docs or release notes. It uses marked 9.1.6 with GFM on and breaks off, so tables, task lists and strikethrough render the way they do in a GitHub README. To check whether a table or nested list will render before you commit it, paste it there first.

Related tools: escape characters for HTML with the HTML Entity Encoder, or count words in a draft with the Word Counter.

Frequently Asked Questions

What is the difference between CommonMark and GitHub Flavored Markdown?

CommonMark is a precise specification of core Markdown: headings, emphasis, lists, code, links, images and block quotes. GitHub Flavored Markdown (GFM) is a strict superset of CommonMark that adds tables, task list items, strikethrough, extended autolinks for bare URLs, and a filter for some raw HTML tags. A document that only uses CommonMark features renders the same in both.

Why is my Markdown table not rendering?

Tables are a GFM extension, so a strict CommonMark renderer shows them as plain text. If your renderer does support GFM, check that the header row and the delimiter row (the row of hyphens) have exactly the same number of cells, that there is a blank line before the table, and that literal pipe characters inside cells are escaped as \|.

How do I add a line break in Markdown without starting a new paragraph?

End the line with a backslash or with two or more trailing spaces. Both create a hard line break (an HTML br element). The backslash is more reliable because editors often strip trailing spaces automatically.

Why does my numbered list start at the wrong number?

Markdown uses only the first item's number as the start value. A list that begins with 3. starts at 3 no matter what numbers follow. If you want 1, 2, 3, make the first item 1. — you can even write every item as 1. and the output will still count up.

Is it safe to render user-submitted Markdown as HTML?

Only if you sanitize the resulting HTML. Markdown allows raw HTML, and parsers such as marked pass script tags through unchanged by design. Run the HTML output through a sanitizer such as DOMPurify before inserting it into a page.

Sources