---
title: "Markdown code block line numbers: a pandoc test"
slug: markdown-code-block-line-numbers
excerpt: "Markdown code block line numbers, checked with three pandoc test files; under Mark the lines with the markdown reader, see the class list pandoc wrote."
quickAnswer: "Grep tallies for the spec, beside a control, appear in Line numbering and the CommonMark spec. Under Mark the lines with the markdown reader, the command for cln.md and the HTML it produced are quoted. HTML from a fence with a start value of 10 is quoted in Begin at line 10 with startFrom. Output with the extension removed sits in With fenced_code_attributes turned off. Under Plain commonmark and commonmark+attributes, the two commonmark runs are compared. For a fence holding only a language name, output appears in A fence with only a language name. In Check the numbering markers with grep, six numbered steps use cat, pandoc and grep."
keyTakeaways:
  - "Under Line numbering and the CommonMark spec, the spec searches and their control are listed."
  - "Quoted HTML for cln.md sits in Mark the lines with the markdown reader."
  - "Pandoc's command and HTML for the startFrom file appear in Begin at line 10 with startFrom."
  - "Results without the attribute extension appear in With fenced_code_attributes turned off."
  - "Plain commonmark output and the plus-attributes run are set out in Plain commonmark and commonmark+attributes."
publishedDate: 2026-10-20
dateModified: 2026-10-20
author: Bobby Huang
cluster: B
role: post
status: published
faq:
  - q: Does Markdown have syntax for code block line numbers?
    a: "Search tallies over the spec, and a control tally, are given under Line numbering and the CommonMark spec."
  - q: Which pandoc reader writes the numberLines class?
    a: "The command and pandoc's HTML for cln.md are quoted in Mark the lines with the markdown reader."
  - q: Can the line count start at 10?
    a: "One run with a start value of 10 sits in Begin at line 10 with startFrom."
  - q: Why did my braces end up as text?
    a: "Under With fenced_code_attributes turned off, pandoc's HTML for the two brace fences is quoted."
related:
  - markdown-code-blocks
  - markdown-custom-heading-id
  - markdown-numbered-headings
---

You paste a long snippet into your notes and want to point a reader at line 7. Counting lines by hand gets old fast, so you look for a way to have the converter mark each line for you.

To test markdown code block line numbers, this post feeds three tiny files to pandoc and changes the reader setting between runs. The pandoc build was 3.8.3, running on a Mac with macOS. This post quotes the classes, ids and attributes pandoc wrote into the HTML. This test stops at the HTML; no viewer was involved. The [code blocks guide](markdown-code-blocks.md) covers fences, info strings and language names in general.

## Line numbering and the CommonMark spec

Five line-numbering terms got zero grep matches in the spec file (CommonMark 0.31.2). Each case-insensitive fixed-string count came back 0, for `numberLines`, `startFrom`, `line number`, `number lines` and `numbering`. For a control, searching the fixed string `info string` matched 9 lines of that file.

The classes and ids in the runs below were written by pandoc.

## The three test files

The first file is `cln.md`. Running `cat cln.md` printed:

````markdown
```{.python .numberLines}
total = 0
for n in range(3):
    total += n
```
````

The braces after the backticks hold two classes, `.python` and `.numberLines`. The second file, `cln-start.md`, opens with `{.python .numberLines startFrom="10"}` and holds two code lines. The third file, `cln-plain.md`, is a fence that names `python` and nothing else, with one code line.

Braces on heading lines are a separate case, tested in the [custom heading id post](markdown-custom-heading-id.md).

## Mark the lines with the markdown reader

The command for this run:

```sh
pandoc -f markdown -t html cln.md
```

Pandoc printed this, with its own line breaks:

```html
<div class="sourceCode" id="cb1"><pre
class="sourceCode numberSource python numberLines"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1"></a>total <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb1-2"><a href="#cb1-2"></a><span class="cf">for</span> n <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">3</span>):</span>
<span id="cb1-3"><a href="#cb1-3"></a>    total <span class="op">+=</span> n</span></code></pre></div>
```

The `pre` element got the class list `sourceCode numberSource python numberLines`. Inside the `code` element, each source line sits in its own `span`, with the ids `cb1-1`, `cb1-2` and `cb1-3`. Each span opens with an empty link whose `href` points at that same id. No brace character appears anywhere in the output.

Run as `pandoc --list-extensions=markdown`, the extension listing held `+fenced_code_attributes` in this test.

## Begin at line 10 with startFrom

The second file adds a start value to the braces:

```sh
pandoc -f markdown -t html cln-start.md
```

Its output:

```html
<div class="sourceCode" id="cb1" data-startFrom="10"><pre
class="sourceCode numberSource python numberLines"><code class="sourceCode python" style="counter-reset: source-line 9;"><span id="cb1-10"><a href="#cb1-10"></a>total <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb1-11"><a href="#cb1-11"></a><span class="bu">print</span>(total)</span></code></pre></div>
```

Three things changed, while the class list on `pre` stayed the same as in the first run. The outer `div` gained `data-startFrom="10"`. The `code` element gained `style="counter-reset: source-line 9;"`. The span ids began at `cb1-10` and went on to `cb1-11`, one for each of the two code lines.

## With fenced_code_attributes turned off

Next, the `markdown` reader ran with that extension switched off:

```sh
pandoc -f markdown-fenced_code_attributes -t html cln.md
```

Pandoc wrote a single line:

```html
<p><code>{.python .numberLines} total = 0 for n in range(3):     total += n</code></p>
```

The fence did not become a code block in this run. Pandoc wrote one inline `code` element inside a paragraph, with the brace text and the three code lines joined on one line. No `sourceCode` class and no line ids were written. The code lines lost their line breaks, so the indent before `total += n` now sits inside a run of five spaces.

The same setting on `cln-start.md` gave `<p><code>{.python .numberLines startFrom="10"} total = 0 print(total)</code></p>`. The start value stayed inside the brace text, and no `data-startFrom` attribute was written.

## Plain commonmark and commonmark+attributes

Plain `commonmark` came next:

```sh
pandoc -f commonmark -t html cln.md
```

Its output:

```html
<pre class="{.python"><code>total = 0
for n in range(3):
    total += n</code></pre>
```

Here the fence stayed a code block. The first word of the info string, `{.python`, went into the class of the `pre` element as written, brace and dot included. The code lines stayed plain text, with no spans and no line ids. The `cln-start.md` run gave the same `pre` class and its two code lines. The extension list from `pandoc --list-extensions=commonmark` showed `-attributes`.

Adding the extension changed that, so in this test the `commonmark` reader needed `+attributes` before it wrote the numbering classes. Swapping in `pandoc -f commonmark+attributes -t html cln.md` gave the same HTML as the `markdown` run above, line for line. The `cln-start.md` run under `commonmark+attributes` matched its `markdown` run too, including `data-startFrom="10"` and the `style` value.

## A fence with only a language name

The third file has no braces:

```sh
pandoc -f markdown -t html cln-plain.md
```

Pandoc printed:

```html
<div class="sourceCode" id="cb1"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>total <span class="op">=</span> <span class="dv">0</span></span></code></pre></div>
```

The four reader settings in this test printed these same two lines for `cln-plain.md`. The class list on `pre` is `sourceCode python`, without `numberSource` or `numberLines`. Pandoc still wrapped the line in a `span` with the id `cb1-1`. This time the empty link carried `aria-hidden="true"` and `tabindex="-1"`, and the runs with `.numberLines` had neither.

A line id showed up here without the numbering class, so the class list is the marker to check.

## Check the numbering markers with grep

Run through these steps when a `.numberLines` fence gives HTML you didn't expect.

1. Show `cln.md` with `cat cln.md`. The braces should sit on the opening fence line, right after the backticks.
2. Convert it with `pandoc -f markdown -t html cln.md` and look for `numberLines` in the class list on the `pre` element.
3. Count the line ids. `pandoc -f markdown -t html cln.md | grep -c 'id="cb1-'` printed `3`. With `-f commonmark` that count was `0`, and with `-f commonmark+attributes` it was `3`.
4. Count the class. `pandoc -f markdown -t html cln.md | grep -c 'numberLines'` printed `1`. With `-f markdown-fenced_code_attributes` it printed `1` as well, because the braces and their contents were kept as inline code. Read the matching line before you trust that count.
5. Check the start value. `pandoc -f markdown -t html cln-start.md | grep -c 'data-startFrom'` printed `1`, and plain `commonmark` printed `0`.
6. Compare a plain fence. `pandoc -f markdown -t html cln-plain.md | grep -c 'numberLines'` printed `0`, while `pandoc -f markdown -t html cln-plain.md | grep -c 'id="cb1-'` printed `1`.

## Where Sashmark fits

Sashmark's v1 is being planned so a person could change words where they sit on the rendered page. When saved, a file would get a clean Round-Trip from that same plan, and an unedited line would end up holding its old bytes. Marking new changes since you last opened the file to read it would fall to a planned Changes View that would need no git. It is in development and has not been released. The app itself has no AI features. See the [planned features](/features), the [help pages](/help) and the [roadmap](/roadmap) for where each part stands.

More at [sashmark.com](https://www.sashmark.com/).
