Markdown Ordered Lists: Numbering and Nesting

Use a number, a period, and a space for a Markdown ordered list, and keep substeps indented under their parent. Add blank lines before lists that follow prose. Diagnose four cases separately: normal numbering, a list that will not restart, a child step at the wrong level, and markers left as plain text. Our local renderer comparisons show that changing the source number, adding a separator, and changing a parser option repair different parts of the result. 4

Choose the fix by the list structure

Use the generated HTML to distinguish an unrecognized list, a numbering problem, and a child attached to the wrong parent.

What you see How to identify the case What to change
Normal sequential steps An <ol> contains sibling <li> elements Use period markers and consistent indentation
A later phase keeps counting Both phases are children of the same <ol> Separate phases with an unindented paragraph or heading
A separate list starts at the wrong value A separate <ol> omits start despite a nondefault first marker Use a configuration that preserves the first marker
A substep becomes a main step Its <li> is a sibling of the parent item, outside a nested list Change indentation for the parser and marker width
Numbers remain literal text The text is inside <p>, with no surrounding <ol> Add separation from prose and use a supported marker

A separator can create a new list while leaving the starting value wrong. A configuration can restore the starting value while leaving a child at the wrong level. Counting the visible steps alone misses both failures. 34

Set Markdown ordered list starts and boundaries

Set the first marker to the intended starting value, and close the preceding list when you want a separate sequence.

Repeated markers work for a sequence that starts at the default value. This input produced sequential list items in every configuration we tested:

1. alpha
        1. beta
        1. gamma
        

Output from our repeated-marker sample, unedited:

<ol>
        <li>alpha</li>
        <li>beta</li>
        <li>gamma</li>
        </ol>
        

That output came from markdown-it-py with line breaks enabled. The normal CommonMark configuration produced the same list. The line-break option did not change numbering in this sample. Repeating markers saves renumbering when moving items, while sequential source markers remain useful when someone reads the source directly. 48

A blank line does not restart the sequence. Our gap sample began at a nondefault value and placed another numbered item after an empty line:

3. alpha
        4. beta
        
        8. restart
        

The CommonMark-mode output kept every item in the existing list.

Output from our blank-line sample, unedited (… marks omitted lines):

<ol start="3">
        …
        <li>
        <p>restart</p>
        </li>
        </ol>
        

The later marker supplied another item, not a new starting value. Blank lines can make a list loose, adding paragraph wrappers around its item text. They do not themselves establish a new phase. CommonMark defines the starting value from the initial item and disregards later marker numbers for numbering. 14

An unindented paragraph between the phases created separate lists in our test. Put blank lines around that paragraph so it stands outside the surrounding items:

3. alpha
        4. beta
        
        Next phase.
        
        8. restart
        

Python-Markdown's default configuration separated the containers but omitted their starting values. Its list processor records the initial marker, then emits a starting attribute only when lazy numbering is disabled. The bundled sane_lists extension changes that setting. It preserved the intended starts on this input. 234

Output from our separated-phase sample, unedited:

<ol start="3">
        <li>alpha</li>
        <li>beta</li>
        </ol>
        <p>Next phase.</p>
        <ol start="8">
        <li>restart</li>
        </ol>
        

That is Python-Markdown with the extension enabled. The default configuration emitted the same separate sequences without the starting attributes. Adding the extension repaired the start values. It was not responsible for separating the phases.

Pandoc's GFM reader also retained the phase boundary and starting values on the same input. Its HTML included a numbering-style attribute:

Output from our separated-phase sample, unedited:

<ol start="3" type="1">
        <li>alpha</li>
        <li>beta</li>
        </ol>
        <p>Next phase.</p>
        <ol start="8" type="1">
        <li>restart</li>
        </ol>
        

Pandoc's GFM reader is not a test of GitHub.com, and markdown-it-py is not a test of the VS Code preview. 46

Indent substeps for the parser and parent marker

Use four spaces for a child under a short period marker when sharing the tested examples between these parsers, and reconsider the content column when the parent marker grows wider.

The shorter indentation in this input worked in our CommonMark and Pandoc GFM paths:

1. Parent
           1. Child
        2. Next
        

Output from our nested-step sample, unedited:

<ol>
        <li>Parent
        <ol>
        <li>Child</li>
        </ol>
        </li>
        <li>Next</li>
        </ol>
        

This is markdown-it-py in CommonMark mode. Python-Markdown kept all the text but placed the child alongside its parent:

Output from our nested-step sample, unedited:

<ol>
        <li>Parent</li>
        <li>Child</li>
        <li>Next</li>
        </ol>
        

The difference is step ownership. A child can describe a task inside the parent procedure. A sibling describes another task at the parent level. An export can retain every word yet change that relationship. Enabling sane_lists did not repair this case because that extension retains the parser's indentation convention. 234

Increasing the child indentation to four spaces produced the intended nesting in every tested configuration:

1. Parent
            1. Child
        2. Next
        

Python-Markdown uses a fixed indentation setting for nested blocks. CommonMark derives the required indentation from the parent marker and the spacing before its content. 37

For a wider parent marker, align the child with the parent's text in a CommonMark workflow. The corrected wide-marker input was:

100. Parent
             1. Child
        101. Next
        

Our wide-marker sample with the shorter indentation flattened the child in every configuration. The aligned version nested it in every configuration. This pair does not establish a universal indentation width for arbitrarily deep lists. 4

An explanatory paragraph inside a step needs indentation too. Our indented-note sample kept the note inside the preceding item across the tested configurations. Keep a note indented when it explains that step. Leave a phase description unindented when it should separate sequences. 4

Fix literal markers before adjusting the numbers

Separate a list from preceding prose and use period markers when the parser leaves the intended steps as ordinary text.

A nondefault starting marker immediately after prose remained paragraph text in every tested configuration. The equivalent input with a blank line produced a list everywhere, although default Python-Markdown still omitted the custom starting value. Recognition and start preservation therefore need separate fixes. 4

CommonMark permits a nonempty ordered list to interrupt a paragraph only when it begins with the default starting marker. Python-Markdown also left our default-start interruption sample as a paragraph. Adding separation before a procedure avoids relying on that difference. 147

Symptom Observed cause Repair supported by the tests
Steps after prose remain a paragraph No blank line before a nondefault start Insert a blank line before the list
Parenthesis markers remain visible Python-Markdown did not recognize that marker style Use periods followed by spaces
A year at the start becomes an unwanted step The punctuation matches a list marker Escape the period with a backslash
A note ends the procedure The note sits outside the item Indent the note under its parent step

CommonMark-mode markdown-it-py and Pandoc GFM recognized parenthesis markers in our sample. Python-Markdown did not, with or without the extension. Period markers gave the tested paths a shared syntax. For a literal year followed by a period, escaping the punctuation kept our sample as prose in every configuration. 4

For documents obtained through PDF to Markdown conversion, review the extracted steps against the source document before editing their numbering. Check whether each note and substep belongs to the same parent as in the original.

Keep separate phases outside existing items, keep substeps inside their intended parents, and preserve the initial marker through the renderer's start-aware configuration. If the HTML already has the intended containers, starting attributes, and parent-child relationships, changing source numbers or indentation targets the wrong part of the problem.

How we tested

We compared generated HTML structure on small, controlled inputs, not browser appearance or document-conversion quality.

Tests ran on October 20, 2026 (Asia/Shanghai), using Python 3.11.14. The supplemental run recorded 2026-10-19T23:25:47.744606+00:00 in UTC. The supplied matrix and supplemental results share lab/renderers/results.json. No PDF scorer or OCR benchmark applies to these syntax inputs. 4

Configuration Version Options
markdown-it-commonmark markdown-it-py 4.0.0 MarkdownIt('commonmark')
markdown-it-breaks markdown-it-py 4.0.0 MarkdownIt('commonmark', {'breaks': True})
python-markdown Python-Markdown 3.10.2 extensions=['tables', 'fenced_code'], default indentation
python-sane-lists Python-Markdown 3.10.2 Same extensions plus sane_lists
pandoc-gfm Pandoc 3.11 pandoc -f gfm -t html

The Python packages were already installed in the repository's .venv; we installed nothing. Run the supplemental experiment from the repository root:

.venv/bin/python runs/2026-10-20-markovo-markdown-ordered-list/lab/renderers/run_ordered_lists.py --lab runs/2026-10-20-markovo-markdown-ordered-list/lab
        

The runner saves each source as <sample>.md and each unmodified HTML result as <sample>--<configuration>.txt. Its exact options and counting code are available in the experiment script. 5

Source file Shape represented
ordered-list.md Nondefault start, blank line, later attempted restart
repeat-one.md Repeated default markers
restart-paragraph.md Unindented paragraph separating phases
nested-three.md / nested-four.md Short parent marker with three-space / four-space child
wide-three.md / wide-five.md Wide parent marker with insufficient / aligned child indentation
interrupt-one.md / interrupt-three.md Prose immediately followed by default / nondefault start
separated-three.md Blank line before a nondefault starting marker
parenthesis.md Right-parenthesis delimiters
escaped-number.md Literal year with an escaped period
note-four.md Indented note inside a step

All sources and outputs are under lab/renderers/. The unrelated syntax cases in the supplied matrix are outside this guide. The ordered-list comparison covers 13 inputs across five configurations, representing three implementations rather than five independent parsers.

The table below is generated from results.json. Each cell is ol/li/p/depth; starts: counts of ordered lists, list items, paragraph tags, maximum simultaneously open ordered-list depth, then the start values in document order. A dash means no explicit attribute, not a failed extraction. Default HTML numbering starts at one when that attribute is absent. Pandoc's extra type attribute remains in the raw output and JSON. These counts describe structure, not rendering fidelity. 45

Input CommonMark Breaks Python default Python sane_lists Pandoc GFM
ordered-list 1/3/3/1; 3 1/3/3/1; 3 1/3/2/1; - 1/3/2/1; 3 1/3/3/1; 3
repeat-one 1/3/0/1; - 1/3/0/1; - 1/3/0/1; - 1/3/0/1; - 1/3/0/1; -
restart-paragraph 2/3/1/1; 3,8 2/3/1/1; 3,8 2/3/1/1; -,- 2/3/1/1; 3,8 2/3/1/1; 3,8
nested-three 2/3/0/2; -,- 2/3/0/2; -,- 1/3/0/1; - 1/3/0/1; - 2/3/0/2; -,-
nested-four 2/3/0/2; -,- 2/3/0/2; -,- 2/3/0/2; -,- 2/3/0/2; -,- 2/3/0/2; -,-
wide-three 1/3/0/1; 100 1/3/0/1; 100 1/3/0/1; - 1/3/0/1; 100 1/3/0/1; 100
wide-five 2/3/0/2; 100,- 2/3/0/2; 100,- 2/3/0/2; -,- 2/3/0/2; 100,- 2/3/0/2; 100,-
interrupt-one 1/2/1/1; - 1/2/1/1; - 0/0/1/0; none 0/0/1/0; none 1/2/1/1; -
interrupt-three 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none
separated-three 1/2/1/1; 3 1/2/1/1; 3 1/2/1/1; - 1/2/1/1; 3 1/2/1/1; 3
parenthesis 1/2/0/1; - 1/2/0/1; - 0/0/1/0; none 0/0/1/0; none 1/2/0/1; -
escaped-number 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none 0/0/1/0; none
note-four 1/2/3/1; - 1/2/3/1; - 1/2/3/1; - 1/2/3/1; - 1/2/3/1; -

The full structural summary retains each configuration separately. The source diagnosis records the installed files and hashes: Python-Markdown's OListProcessor.run gates the starting attribute on LAZY_OL; SaneOListProcessor disables that flag; markdown-it-py's list_block handles paragraph interruption separately from creating the list-opening token. We did not reproduce a GitHub issue or test GitHub.com, VS Code, CSS counters, or visual accessibility. 3

Sources

  1. CommonMark 0.31.2: List items and lists
  2. Python-Markdown: Sane Lists
  3. Installed parser source diagnosis
  4. Raw renderer results
  5. Reproducible ordered-list runner
  6. Pandoc: Specifying formats
  7. Python-Markdown: Differences
  8. markdown-it-py documentation