Markdown NumberingAutomatically number Markdown headings based on their logical hierarchy. This VS Code extension adds and removes heading numbers such as:
It supports configurable starting levels, skipped headings, skipped subtrees, existing numbering, missing heading levels, fenced code blocks, and HTML comments. Features
CommandsThe extension provides two commands. Markdown Numbering: Add Heading NumbersAdds or recalculates heading numbers throughout the Markdown document. Example:
becomes:
Markdown Numbering: Remove Heading NumbersRemoves heading numbers generated in the supported numbering format. Example:
becomes:
The remove command also respects fenced code blocks, HTML comments, ConfigurationThe extension provides one setting:
Default:
The valid range is Start from H1With:
all headings participate in numbering. Input:
Output:
Start from H2With:
H1 headings remain unnumbered. Input:
Output:
Start from H3With:
H1 and H2 headings remain unnumbered. Input:
Output:
Logical HierarchyNumbering is based on the logical hierarchy, not simply on the numeric difference between physical heading levels. For example:
is treated logically as:
Therefore the result is:
Intermediate heading levels do not need to physically exist. Missing Heading LevelsMarkdown headings can jump directly from one level to another. For example:
The extension treats the headings as parent and child according to their physical order and hierarchy. The result is:
The physical heading level is preserved. Only the numbering hierarchy is logical. Skipping a HeadingA heading can be excluded from numbering with:
Example:
Result:
Behavior of
|
| Rule | Current heading | Children | Consumes number |
|---|---|---|---|
| No rule | Numbered | Normal | Yes |
skip |
Unnumbered | Included | No |
skip-all |
Unnumbered | Excluded | No |
Use skip when only the current heading should remain manually controlled.
Use skip-all when the entire section should be manually controlled.
Fenced Code Blocks
Headings inside fenced code blocks are ignored.
Both backtick and tilde fences are supported.
Example:
# Real Heading
```md
# Fake Heading
## Fake Section
### Fake Child
```
## Real Section
Result:
# 1. Real Heading
```md
# Fake Heading
## Fake Section
### Fake Child
```
## 1.1. Real Section
The headings inside the code block are not considered part of the document hierarchy.
Tilde fences
Tilde fences are also ignored:
# Real Heading
Fake Heading
Fake Section
## Real Section
Only the real Markdown headings are processed.
HTML Comments
Headings inside multiline HTML comments are ignored.
Example:
<!--
# Old Project
## Old Section
### Old Details
-->
# Real Project
## Real Section
### Real Details
Result:
<!--
# Old Project
## Old Section
### Old Details
-->
# 1. Real Project
## 1.1. Real Section
### 1.1.1. Real Details
This is useful for keeping alternative outlines, examples, drafts, or manually numbered sections in the document without having the extension modify them.
Inline HTML comments on headings
An HTML comment at the end of a real heading is still processed when it is an extension rule.
For example:
# Introduction <!-- skip -->
is recognized as a real heading with the skip rule.
This is intentionally different from a multiline HTML comment block.
Removing Numbering
The remove command follows the same protection rules as the numbering command.
It does not modify headings inside:
- fenced code blocks
- multiline HTML comments
skipheadingsskip-allsubtrees
For example:
# 1. Real Heading
```md
# 99. Example
## 99.1. Example section
```
1. Real Section
becomes:
```markdown
# Real Heading
```md
# 99. Example
## 99.1. Example section
Real Section
The numbers inside the fenced code block remain unchanged.
---
## Existing Numbering
Running the numbering command on an already numbered document recalculates the numbers.
This means stale numbering is corrected automatically.
Input:
```markdown
# 99. Root
## 99.99. Old Section
### 99.99.99. Old Child
# 100. Root
Result:
# 1. Root
## 1.1. Old Section
### 1.1.1. Old Child
# 2. Root
The existing numbers are treated as generated numbering rather than as authoritative numbering.
Numbering Format
Generated numbers use a trailing dot:
1.
1.1.
1.1.1.
1.1.1.1.
For example:
# 1. Chapter
## 1.1. Section
### 1.1.1. Topic
Multi-digit numbers are supported:
## 1.10. Section 10
## 1.11. Section 11
## 1.12. Section 12
Important: Numbers That Look Like Generated Numbers
The extension recognizes a heading number when the heading begins with this pattern:
number(s) followed by a dot and whitespace
For example:
# 1. Introduction
# 1.2. Installation
# 2026. Project Roadmap
The last example is important.
Because:
# 2026. Project Roadmap
matches the same syntactic pattern as generated numbering, the extension will interpret 2026. as a heading number and may remove or replace it when numbering is applied.
Therefore, do not use a heading beginning with a standalone numeric value followed by a period and whitespace if that number is intended to be part of the heading title.
For example, this:
# 2026. Important Information
can be interpreted as existing numbering.
If 2026 is intended to be part of the title, prefer:
# 2026 - Important Information
or:
# 2026 Important Information
or another format that does not match the generated numbering pattern.
Why?
The extension intentionally recognizes patterns such as:
1.
1.2.
1.10.
99.99.
2026.
as possible existing heading numbers.
The extension cannot distinguish syntactically between:
# 2026. Project Roadmap
and:
# 2026. Important Information
because both have the same structure.
Multiple Root Sections
Numbering continues across multiple H1 sections.
Example:
# First
## Section
# Second
## Section
# Third
## Section
produces:
# 1. First
## 1.1. Section
# 2. Second
## 2.1. Section
# 3. Third
## 3.1. Section
Numbering does not restart at each H1.
The same behavior applies when startLevel is greater than 1.
Combining startLevel with Skip Rules
The startLevel setting and skip rules work together.
For example, with:
"md-numbering.startLevel": 2
and:
# Project
## Section
### Skipped <!-- skip -->
#### Details
### Next
the result is:
# Project
## 1. Section
### Skipped <!-- skip -->
#### 1.1. Details
### 1.2. Next
The skipped heading does not consume a number.
What the Extension Does Not Process
The extension only recognizes ATX-style Markdown headings from H1 through H6:
# H1
## H2
### H3
#### H4
##### H5
###### H6
A line with seven or more # characters is not treated as a heading by the extension:
####### Not a heading
Such lines remain unchanged.
Recommended Usage
A typical document might look like this:
# Project
## Introduction
### Background
### Goals
## Implementation
### Architecture
#### Components
#### Data Flow
## Appendix <!-- skip-all -->
### Manually numbered material
After running Markdown Numbering: Add Heading Numbers, it becomes:
# 1. Project
## 1.1. Introduction
### 1.1.1. Background
### 1.1.2. Goals
## 1.2. Implementation
### 1.2.1. Architecture
#### 1.2.1.1. Components
#### 1.2.1.2. Data Flow
## Appendix <!-- skip-all -->
### Manually numbered material
The appendix remains completely under manual control.
Development
This project is written in TypeScript.
Install dependencies
pnpm install
Compile
pnpm compile
Run tests
pnpm test
Run tests in watch mode
pnpm test:watch
Compile in watch mode
pnpm watch
Project Structure
The extension is divided into small modules, each responsible for one part of the numbering process.
src/
├── extension.ts
├── parser.ts
├── hierarchy.ts
├── numbering.ts
├── renderer.ts
├── remove.ts
├── rules.ts
└── types.ts
extension.ts
Registers the VS Code commands and connects the document-processing pipeline.
parser.ts
Reads the Markdown document and identifies headings.
It also ignores:
- fenced code blocks
- multiline HTML comments
and recognizes numbering rules such as:
<!-- skip -->
and:
<!-- skip-all -->
hierarchy.ts
Builds the logical heading hierarchy.
This is what allows the extension to correctly handle missing physical heading levels and skipped headings.
numbering.ts
Assigns logical numbers according to the configured startLevel.
renderer.ts
Applies the calculated numbers back to the original Markdown document while preserving the original document structure.
remove.ts
Removes generated heading numbers while protecting:
- fenced code blocks
- HTML comments
skipheadingsskip-allsubtrees
rules.ts
Contains the logic for recognizing and removing heading numbering and rule comments.
types.ts
Contains the shared TypeScript types used by the extension.
Testing
The project contains unit tests covering normal numbering and edge cases.
The test suite covers:
- normal H1-H6 numbering
startLevel- missing physical heading levels
- multiple root sections
- multi-digit numbers
- existing numbering
skipskip-all- consecutive skips
- skip-all boundaries
- fenced code blocks
- HTML comments
- invalid H7 headings
- empty and non-heading lines
- removal of generated numbering
- idempotent numbering
- idempotent removal
- combinations of existing numbering and skip rules
Run the complete test suite with:
pnpm test
Design Principles
The extension follows a few important principles.
Preserve user-controlled sections
skip and skip-all exist specifically so users can maintain their own numbering or formatting in selected sections.
Never modify code examples
Fenced code blocks are treated as content, not as Markdown structure.
Never modify commented-out Markdown
Multiline HTML comments can contain complete Markdown examples or old document structures and are therefore left untouched.
Separate physical and logical hierarchy
The Markdown heading level determines the physical structure, while the extension uses a logical hierarchy for numbering.
This allows documents with missing intermediate heading levels to still receive consistent numbering.
Recalculate instead of trusting existing numbers
Existing generated-looking numbers are removed and recalculated so that the numbering remains correct after headings are added, removed, reordered, or skipped.
License
This project is licensed under the MIT License.
About
Markdown Numbering is an open-source project created and maintained by DXWIZ.
Learn more about DXWIZ at dxwiz.com.
For questions or support, visit our Contact page.