Skip to content

Commit 5be11fb

Browse files
committed
feat: heading-numbering support 0-based sections and multiple positive fixtures
- Treat section as 0-based when first numbered sibling's last segment is 0 (e.g. 0., 0.0., 1.0.). - Rename positive.md to positive_general.md; add positive_heading_numbering_zero.md. - Verifier and tests now support multiple positive_*.md fixtures.
1 parent a279b6b commit 5be11fb

14 files changed

Lines changed: 240 additions & 128 deletions

‎.markdownlint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ascii-only:
3535
allowedPathPatternsEmoji:
3636
- "markdownlint-rules/README.md"
3737
# allowedUnicode:
38-
# - "ń" # additions on top of default (fixtures e.g. positive.md)
38+
# - "ń" # additions on top of default (fixtures e.g. positive_general.md)
3939
# allowedUnicodeReplaceDefault: false # true = use only allowedUnicode list, no default set
4040
allowedEmoji:
4141
- "✅"

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ These targets mirror the GitHub Actions workflows. Run them locally before pushi
3535

3636
### Markdownlint Tests (`make test-markdownlint`)
3737

38-
- **Positive test**: `md_test_files/positive.md` must pass markdownlint (0 errors).
38+
- **Positive test**: `md_test_files/positive_general.md` (and other `positive_*.md`) must pass markdownlint (0 errors).
3939
- **Negative tests**: each `md_test_files/negative_*.md` file must **fail** markdownlint; the test suite fails if any negative file passes.
4040
- Same behavior as the [Markdownlint tests](.github/workflows/markdownlint-tests.yml) workflow.
4141

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Lint and docs-as-code tooling: custom [markdownlint](https://github.com/DavidAns
2121
- Use when: keeping most docs ASCII while allowing Unicode/emoji only in chosen files (e.g. i18n or release notes).
2222
- [heading-numbering.js](markdownlint-rules/heading-numbering.js) - heading numbering.
2323
- Enforce segment count by numbering root, sequential numbering per section, and consistent period style (e.g. `1. Title` vs `1 Title`).
24-
- Use when: docs use numbered headings (e.g. `### 1.2.3 Title`) and you want structure and style consistent.
24+
Default is 1-based (1., 2., 3.); if the first numbered heading in a section starts at 0 (e.g. `0.`, `0.0.`), that section is treated as 0-based and no error is reported.
25+
- Use when: docs use numbered headings (e.g. `### 1.2.3 Title` or 0-based `### 0. Introduction`) and you want structure and style consistent.
2526
- [heading-title-case.js](markdownlint-rules/heading-title-case.js) - heading title case.
2627
- Enforce title case for headings; words in backticks ignored; configurable lowercase words (e.g. vs, and, the).
2728
- Use when: you want consistent capitalization of headings (first/last and major words capped; small words lowercase in the middle).
@@ -40,7 +41,7 @@ Lint and docs-as-code tooling: custom [markdownlint](https://github.com/DavidAns
4041
- Use when: copying the rule files; required by several of the rules above.
4142

4243
- **JS linting** for the rule code: ESLint (recommended + complexity/max-lines + eslint-plugin-security), aligned with the GitHub Actions workflow.
43-
- **Markdownlint fixture tests**: [md_test_files/](md_test_files/README.md) includes positive/negative fixtures with explicit expected errors, verified by `test-scripts/verify_markdownlint_fixtures.py`.
44+
- **Markdownlint fixture tests**: [md_test_files/](md_test_files/README.md) includes positive_*.md and negative_*.md fixtures with explicit expected errors, verified by `test-scripts/verify_markdownlint_fixtures.py`.
4445
- **Rule unit tests**: Node `node:test` unit tests for each custom rule in `test/markdownlint-rules/` (including security tests for defensive regex handling and ReDoS awareness); run with `make test-rules` or `npm run test:rules`.
4546
CI runs `make test-rules-coverage` (fails if any rule file is below 90% line/statement coverage).
4647
- **Python unit tests**: `unittest` tests for [test-scripts/](test-scripts/README.md) in `test-scripts/test_*.py`; run with `make test-python`.
@@ -128,7 +129,7 @@ npm install
128129
- **`Makefile`** - Local targets (kept in sync with the workflows in `.github/workflows/`).
129130
- **`markdownlint-rules/`** - Custom rule modules (`*.js`) and [README](markdownlint-rules/README.md).
130131
Copy into other repos as `.markdownlint-rules`; do not register `utils.js`.
131-
- **`md_test_files/`** - Test fixtures: `positive.md` (must pass), `negative_*.md` (must fail).
132+
- **`md_test_files/`** - Test fixtures: `positive_*.md` (must pass), `negative_*.md` (must fail).
132133
See [md_test_files/README.md](md_test_files/README.md).
133134
- **`package.json`** - npm dependencies (ESLint, markdownlint-cli2, etc.).
134135
- **`test/markdownlint-rules/`** - Node `node:test` unit tests for each custom rule (`*.test.js`); run with `make test-rules`.

‎markdownlint-rules/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ The first occurrence is the reference; duplicates are reported with the line num
327327
The numbering root is the nearest ancestor heading that has no numbering (or document root, level 1).
328328
Example: H2 under doc root -> 1 segment; H3 under unnumbered `## Section` -> 1 segment; H4 under `### 1. First` -> 1 segment.
329329
Headings without a numeric prefix are ignored.
330-
2. **Section-scoped consistency:** For each section (siblings under the same parent), if any sibling has numbering then all siblings at that level must be numbered sequentially (e.g. 1., 2., 3.) and use consistent period style (all `## 1. Title` or all `## 1 Title`).
330+
2. **Section-scoped consistency:** For each section (siblings under the same parent), if any sibling has numbering then all siblings at that level must be numbered sequentially and use consistent period style (all `## 1. Title` or all `## 1 Title`).
331+
**Index base:** Default is 1-based (e.g. 1., 2., 3.). If the first numbered sibling in a section has last segment `0` (e.g. `0.`, `0.0.`, `1.0.`), that section is treated as 0-based (0., 1., 2. or 0.0., 0.1., etc.) and no sequence error is reported.
331332
Unnumbered siblings in a numbered section are reported.
332333

333334
## Shared Helper

‎markdownlint-rules/heading-numbering.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function getSiblings(sorted, parentIndex, i) {
6767
/**
6868
* Expected number for heading at index i within its section (parent prefix + sibling sequence).
6969
* Section-scoped: only used when at least one sibling has numbering.
70-
* 0-based when the first numbered sibling has numbering "0".
70+
* 0-based when the first numbered sibling's last segment is "0" (e.g. "0", "0.0", "1.0").
7171
*/
7272
function getExpectedNumberInSection(sorted, parentIndex, i) {
7373
const h = sorted[i];
@@ -93,7 +93,8 @@ function getExpectedNumberInSection(sorted, parentIndex, i) {
9393
? parseHeadingNumberPrefix(firstNumbered.rawText).numbering
9494
: null;
9595
/* c8 ignore stop */
96-
const startAtZero = firstNumbering === "0";
96+
const lastSegment = firstNumbering.split(".").pop();
97+
const startAtZero = lastSegment === "0";
9798
const nextNum = startAtZero ? myIdx : myIdx + 1;
9899
const prefix = parentNum ? parentNum + "." : "";
99100
return prefix + String(nextNum);

‎md_test_files/README.md‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
# Markdown Test Fixtures
22

3-
- **positive.md** - Examples that pass all markdown standards. Lint should report 0 errors.
3+
- **positive_general.md** - Examples that pass all markdown standards. Lint should report 0 errors.
4+
- **positive_heading_numbering_zero.md** - 0-indexed H2 numbering (## 0., 1., 2. and subsections); passes with 0 errors.
45
- **negative_*.md** - One file per failing scenario; lint each to verify the expected custom rule(s) fail.
56

67
## Negative Fixtures (Custom Rules Only)
78

8-
| File | Expected custom rule(s) |
9-
| ----------------------------------------- | --------------------------------------------------------- |
10-
| negative_anchor_algo_placement.md | allow-custom-anchors (algo placement) |
11-
| negative_anchor_invalid_id.md | allow-custom-anchors (id not in allowedIdPatterns) |
12-
| negative_anchor_multiple.md | allow-custom-anchors (multiple per line) |
13-
| negative_anchor_ref_placement.md | allow-custom-anchors (ref placement) |
14-
| negative_anchor_spec_placement.md | allow-custom-anchors (spec placement) |
15-
| negative_ascii_only.md | ascii-only |
16-
| negative_duplicate_headings_normalized.md | no-duplicate-headings-normalized |
17-
| negative_heading_like.md | no-heading-like-lines |
18-
| negative_heading_numbering.md | heading-numbering (segment, sequence, period, unnumbered) |
19-
| negative_heading_title_case.md | heading-title-case |
20-
| negative_inline_html.md | allow-custom-anchors (attribute, id pattern, end-of-line) |
9+
| File | Expected custom rule(s) |
10+
| ----------------------------------------- | ----------------------------------------------------------------------- |
11+
| negative_anchor_algo_placement.md | allow-custom-anchors (algo placement) |
12+
| negative_anchor_invalid_id.md | allow-custom-anchors (id not in allowedIdPatterns) |
13+
| negative_anchor_multiple.md | allow-custom-anchors (multiple per line) |
14+
| negative_anchor_ref_placement.md | allow-custom-anchors (ref placement) |
15+
| negative_anchor_spec_placement.md | allow-custom-anchors (spec placement) |
16+
| negative_ascii_only.md | ascii-only |
17+
| negative_duplicate_headings_normalized.md | no-duplicate-headings-normalized |
18+
| negative_heading_like.md | no-heading-like-lines |
19+
| negative_heading_numbering.md | heading-numbering (segment, sequence, period, unnumbered, zero-indexed) |
20+
| negative_heading_title_case.md | heading-title-case |
21+
| negative_inline_html.md | allow-custom-anchors (attribute, id pattern, end-of-line) |
2122

2223
Note: some negative fixtures intentionally trigger built-in markdownlint rules in addition to custom rules (e.g. MD031/MD032/MD033), so the test suite can assert multiple errors on specific lines.
2324

‎md_test_files/expected_errors.yml‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
# Used by test-scripts/verify_markdownlint_fixtures.py.
33
# Total expected count is derived from the length of the errors list.
44

5-
positive.md:
5+
positive_general.md:
6+
errors: []
7+
8+
positive_heading_numbering_zero.md:
69
errors: []
710

811
negative_anchor_algo_placement.md:
@@ -140,6 +143,15 @@ negative_heading_numbering.md:
140143
- line: 25
141144
rule: heading-numbering
142145
message_contains: Too Many Segments for H4
146+
- line: 33
147+
rule: heading-numbering
148+
message_contains: expected "1" to match sibling
149+
- line: 37
150+
rule: heading-numbering
151+
message_contains: expected "2" to match sibling
152+
- line: 41
153+
rule: heading-numbering
154+
message_contains: expected "0.1" to match sibling
143155

144156
negative_heading_title_case.md:
145157
errors:

‎md_test_files/negative_heading_numbering.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Lint: `npx markdownlint-cli2 md_test_files/negative_heading_numbering.md`
44

5-
Expect: heading-numbering (segment count, sequence, period style, unnumbered sibling). All examples are H3/H4 under one H2 so only this section is affected.
5+
Expect: heading-numbering (segment count, sequence, period style, unnumbered sibling, zero-indexed violations). First block is H3/H4 under one H2; second block covers zero-indexed numbering errors.
66

77
## Bad Heading Numbering
88

@@ -23,3 +23,19 @@ Wrong segment count: H3 under H2 must have 1 segment, not 2.
2323
### 1.1 Wrong Segment Count
2424

2525
#### 1.1.1 Too Many Segments for H4
26+
27+
## Zero-Indexed Numbering (Negative Examples)
28+
29+
0-based section: first heading is 0., so sequence must be 0, 1, 2, ... Violations below.
30+
31+
### 0. Zero-Based Section
32+
33+
### 2. Skip 1 (0-Based Sequence; Expected 1.)
34+
35+
0-based subsections under 0.: first is 0.0., so next must be 0.1., not 0.2.
36+
37+
### 0. Subsection Root (0-Based)
38+
39+
#### 0.0. First Sub
40+
41+
#### 0.2. Skip 0.1 (Expected 0.1.)
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This file contains examples that should pass the Markdown standards enforced by markdownlint and the Python validators.
44

5-
It is intended to be linted explicitly via `npx markdownlint-cli2 md_test_files/positive.md`.
5+
It is intended to be linted explicitly via `npx markdownlint-cli2 md_test_files/positive_general.md`.
66

77
## Formatting
88

@@ -106,3 +106,23 @@ AP style: hyphenated compounds (each segment capitalized); "to" and "a" lowercas
106106
### 5. Summary: The Results
107107

108108
AP style: first word after a colon is capitalized (subphrase start).
109+
110+
## Zero-Indexed Heading Numbering
111+
112+
When the first numbered heading in a section starts at 0, the rule treats that section as 0-based and does not report errors.
113+
114+
### 0. Introduction (Zero-Indexed)
115+
116+
Content under first 0-based H3.
117+
118+
#### 0.1 Zero Indexed Section
119+
120+
### 1. First Topic
121+
122+
### 2. Second Topic
123+
124+
Subsections under a 0-based H3 also use 0-based numbering when the first subheading is 0.
125+
126+
#### 2.1. First Subsection
127+
128+
#### 2.2. Second Subsection
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Positive Fixture: 0-Indexed H2 Numbering
2+
3+
Lint: `npx markdownlint-cli2 md_test_files/positive_heading_numbering_zero.md`
4+
5+
This file should pass with zero errors. The first H2 is 0-indexed, so the rule treats the top-level section as 0-based.
6+
7+
## 0. Introduction
8+
9+
Content under the first H2 (0-based).
10+
11+
## 1. First Section
12+
13+
Content.
14+
15+
### 1.0 First Section First Subsection
16+
17+
Content.
18+
19+
## 2. Second Section
20+
21+
Subsections under this H2 use 0-based numbering when the first is 2.0.
22+
23+
### 2.0. First Subsection
24+
25+
### 2.1. Second Subsection

0 commit comments

Comments
 (0)