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


