Markdown tabs vs spaces: how a tab counts
Bobby Huang ·
Markdown tabs vs spaces comes down to one short section of the CommonMark spec about tab stops. This page puts that section in plain words and tests it with pandoc on indented code, list items, numbered items and block quotes, using files that hold real tab bytes. It also looks at a tab between two words, and the section on testing a file with tabs gives a short checklist for your own file.
Key takeaways
- The section on what the spec says puts the tab rule in plain words and explains what a tab stop is.
- Indented code has a section of its own, with tested examples and a note on a pandoc option.
- List items and numbered items each get worked cases that show where a tab puts the text.
- Block quotes get two tested cases: one tab and two tabs.
- The section on testing a file with tabs has a numbered checklist for checking a suspect file with pandoc.
You indented a line with a tab, and the rendered page put it somewhere you didn't plan. A second paragraph turned into a code block, or a line meant for a quote showed up as code.
A tab in Markdown isn't a fixed four spaces. What it adds depends on where it starts on the line. You'll get the CommonMark rule behind markdown tabs vs spaces, a pandoc test of each case, and a note on where one tab and four spaces give the same result and where they don't.
How the examples are written
This post writes each tab byte as →. Read each → as one tab. If you copy an example into a test file, type a real tab there.
Every result quoted below came from pandoc 3.8.3, run as pandoc -f commonmark -t html test.md on a file holding real tab bytes, not arrows.
What the spec says
CommonMark 0.31.2 has a short section called "Tabs." It says tabs in a line are not expanded to spaces. Where spaces help define block structure, though, a tab behaves as if it were replaced by spaces with a tab stop of 4 characters.
A tab stop of 4 means a tab doesn't add four spaces each time. It moves the position forward to the next multiple of 4, counted from the beginning of the line. So the count depends on what sits before it:
- A tab at the very start of a line counts as four spaces.
- Two spaces and then a tab still reach position 4. The tab counts as two.
- A
>and then a tab: the>uses one position, so the tab counts as three. The spec spells this case out.
The same section adds that tabs which aren't doing structural work, such as a tab between two words in a code block, pass through as literal tabs.
Indented code
The spec's own first example is a tab in place of four spaces at the start of an indented code block. Here's a test file:
→Jane→Doe→→Holdings
The opening tab reaches position 4. Pandoc returns a <pre><code> element holding Jane, Doe and Holdings. The three tabs between the words aren't structure, so the spec keeps them as tabs in the code. Why that indent makes code is explained by the code blocks post.
Pandoc's default differs on that last point. Its manual says it converts tabs to spaces before parsing, and that the --preserve-tabs option keeps them in code blocks and code spans. The test bears this out. With pandoc -f commonmark -t html test.md, spaces sit between the words. With pandoc -f commonmark -t html --preserve-tabs test.md, the tab bytes come through.
Starting from two spaces works too:
→Jane Doe
The tab moves from position 2 to position 4, so pandoc still gives a code block containing Jane Doe.
You can also mix the two inside one block. A first line indented with four spaces and a second line indented with one tab both count as four, and pandoc puts them in a single code block. For fences, info strings and other code block rules, see fenced and indented code.
List items
For indenting a later paragraph under a list item, see the indenting section of the numbered list post and the nesting guide. This section covers how a tab counts toward that indent.
- Jane Doe
→Doe Holdings
The item's text starts at position 2. The tab on the last line reaches position 4, which is far enough. Pandoc puts Doe Holdings in the same <li> as a second <p>. The spec says this has exactly the same effect as four spaces, and a test file using four spaces gave identical output.
Now add a second tab:
- Jane Doe
→→Doe Holdings
Two tabs reach position 8, six past where the text starts. For this input, pandoc's output was a <pre><code> element in the <li>, with two spaces before Doe Holdings. That matches the spec's example for this case. For why an indent inside an item becomes code, open the fenced and indented code post.
A tab right after the marker is fine on its own. For -→Jane Doe, the tab moves from position 1 to position 4, and pandoc returns a plain list item with the text Jane Doe.
Numbered items
The tab stop gives a numbered list an odd property. Compare these two files:
1.→Jane Doe
→Doe Holdings
10.→Jane Doe
→Doe Holdings
After 1. the tab fills two positions. After 10. it fills one. Both land at position 4, so the item text starts at the same place in each. A continuation line that opens with one tab also reaches position 4, and pandoc places Doe Holdings in the item as a second paragraph in both files. With spaces, the gap after the marker is whatever you typed, so you'd count it by hand.
Block quotes
The nesting guide covers the block quote marker itself. When a tab follows a > that opens the line, the tab counts as three spaces.
>→Jane Doe
Pandoc reads this as a <blockquote> holding an ordinary <p> with Jane Doe.
>→→Jane Doe
The second tab adds four more. For this input, pandoc gave a <pre><code> element in the <blockquote>, with two spaces before Jane Doe, which is the result the spec gives for this example. For quotes holding lists and lists holding quotes, open the nesting guide.
A tab between two words
Outside block structure, the spec doesn't expand tabs. In a paragraph, a tab is just a character. For Jane→Doe in a plain paragraph, pandoc returned <p>Jane Doe</p> with a single space, and adding --preserve-tabs did not change that. Pandoc's manual says tabs in regular text are always treated as spaces. Other viewers may show the gap differently, so don't use a mid-line tab to line things up. Columns belong in a table.
Testing a file with tabs
When a page renders a block in a place you didn't plan, test the lines in isolation.
- Copy the lines around the problem into a scratch file, such as
test.md. - Run
pandoc -f commonmark -t html test.mdand read the HTML. - Check for
<pre><code>where you expected<p>, or a<p>that sits outside the<li>or<blockquote>you meant it for. - For each tab that starts a line or follows a marker, work out the position it reaches using the tab stop of 4.
- Replace those tabs with the matching number of spaces, run pandoc again, and compare.
With spaces, the parser counts each space character in the file as one position. A tab's share depends on its starting position, so one tab can count as four, three, two or one. If you keep tabs, keep them for lines where you've checked the count. If the file comes from an agent, say which you want in the instructions you give it. Our post on phrasing instructions for an agent helps with that kind of rule.
Where Sashmark fits
Planned as a desktop tool, Sashmark is meant for checking Markdown that your agents write. It is in development and has not been released. On the plan for v1: code blocks, lists and block quotes drawn as formatted text, with edits made straight on that view and untouched lines saved exactly as they were. The app itself has no AI features. If an agent replaces the file's text, the plan is for the page to reload and hold your place, with a Changes View to mark what's different since your last read. The roadmap shows status, the help pages carry how-to notes, and planned features shows the scope.
Watch sashmark.com for news.
Questions
- Can one tab replace four spaces in Markdown?
- The section on what the spec says has the tab-stop explanation and examples.
- Why did my tab-indented line turn into a code block?
- The list item and block quote sections show the tab counts that lead to a code block.
- Why does pandoc print spaces where my tabs were?
- The indented code section covers pandoc's default and the option that changes it.