Skip to content

Commit 4613a9d

Browse files
committed
wip
1 parent 4d5bb09 commit 4613a9d

9 files changed

Lines changed: 198 additions & 8 deletions

‎doc-build/md043.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,37 @@ at least 4 letters:
6767
]
6868
```
6969

70+
To handle repeating heading sections (common in `CHANGELOG` files) like the
71+
following:
72+
73+
```markdown
74+
# Changelog
75+
## 1.0.0
76+
### Fixes
77+
### Features
78+
## 2.0.0
79+
### Fixes
80+
### Features
81+
```
82+
83+
Use the `"^"` special value meaning "if the next heading does not match, go back
84+
to the previous matching marker":
85+
86+
```json
87+
[
88+
"# Changelog",
89+
"^",
90+
"%% \\d+\\.\\d+\\.\\d+",
91+
"### Fixes",
92+
"### Features",
93+
"^"
94+
]
95+
```
96+
97+
The simple form shown above works for most scenarios; if multiple loops are
98+
needed, the value `"^^"`, `"^^^"`, etc. can be used in conjunction. To avoid
99+
infinite loops, matching fails if there are more than 10,000 loops.
100+
70101
When an error is detected, this rule outputs the line number of the first
71102
problematic heading (otherwise, it outputs the last line number of the file).
72103

‎doc/Rules.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,6 +1912,37 @@ at least 4 letters:
19121912
]
19131913
```
19141914

1915+
To handle repeating heading sections (common in `CHANGELOG` files) like the
1916+
following:
1917+
1918+
```markdown
1919+
# Changelog
1920+
## 1.0.0
1921+
### Fixes
1922+
### Features
1923+
## 2.0.0
1924+
### Fixes
1925+
### Features
1926+
```
1927+
1928+
Use the `"^"` special value meaning "if the next heading does not match, go back
1929+
to the previous matching marker":
1930+
1931+
```json
1932+
[
1933+
"# Changelog",
1934+
"^",
1935+
"%% \\d+\\.\\d+\\.\\d+",
1936+
"### Fixes",
1937+
"### Features",
1938+
"^"
1939+
]
1940+
```
1941+
1942+
The simple form shown above works for most scenarios; if multiple loops are
1943+
needed, the value `"^^"`, `"^^^"`, etc. can be used in conjunction. To avoid
1944+
infinite loops, matching fails if there are more than 10,000 loops.
1945+
19151946
When an error is detected, this rule outputs the line number of the first
19161947
problematic heading (otherwise, it outputs the last line number of the file).
19171948

‎doc/md043.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ at least 4 letters:
7878
]
7979
```
8080

81+
To handle repeating heading sections (common in `CHANGELOG` files) like the
82+
following:
83+
84+
```markdown
85+
# Changelog
86+
## 1.0.0
87+
### Fixes
88+
### Features
89+
## 2.0.0
90+
### Fixes
91+
### Features
92+
```
93+
94+
Use the `"^"` special value meaning "if the next heading does not match, go back
95+
to the previous matching marker":
96+
97+
```json
98+
[
99+
"# Changelog",
100+
"^",
101+
"%% \\d+\\.\\d+\\.\\d+",
102+
"### Fixes",
103+
"### Features",
104+
"^"
105+
]
106+
```
107+
108+
The simple form shown above works for most scenarios; if multiple loops are
109+
needed, the value `"^^"`, `"^^^"`, etc. can be used in conjunction. To avoid
110+
infinite loops, matching fails if there are more than 10,000 loops.
111+
81112
When an error is detected, this rule outputs the line number of the first
82113
problematic heading (otherwise, it outputs the last line number of the file).
83114

‎lib/md043.mjs‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { addErrorContext, addErrorDetailIf } from "../helpers/helpers.cjs";
44
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
55
import { filterByTypesCached } from "./cache.mjs";
66

7-
export const headingsItemsRe = /^([*+?]|[#%]{1,6}\s+\S.*)$/u;
7+
export const headingsItemsRe = /^([*+?]|\^+|[#%]{1,6}\s+\S.*)$/u;
88
const headingPartsRe = /^([#%]{1,6})\s+(\S.*)$/u;
99

1010
const headingIsExpected = (/** @type {string} */ actual, /** @type {string} */ expected, /** @type {boolean} */ matchCase) => {
@@ -50,7 +50,28 @@ export default {
5050
const headingLevel = getHeadingLevel(heading);
5151
anyHeadings = true;
5252
const actual = `${"".padEnd(headingLevel, "#")} ${headingText}`;
53-
const expected = getExpected();
53+
let expected = getExpected();
54+
55+
if (expected.startsWith("^")) {
56+
const save = i;
57+
const nextExpected = getExpected();
58+
if (headingIsExpected(actual, nextExpected, matchCase)) {
59+
expected = nextExpected;
60+
} else {
61+
for (let j = save - 1; j--; j >= 0) {
62+
if (requiredHeadings[j] === expected) {
63+
i = j + 1;
64+
expected = getExpected();
65+
break;
66+
}
67+
}
68+
if (i < 0) {
69+
// Nothing to skip back to; continue from current location
70+
i = save;
71+
}
72+
}
73+
}
74+
5475
if (expected === "*") {
5576
const nextExpected = getExpected();
5677
if (!headingIsExpected(actual, nextExpected, matchCase)) {
@@ -80,7 +101,8 @@ export default {
80101
if (
81102
!hasError &&
82103
((extraHeadings > 1) ||
83-
((extraHeadings === 1) && (requiredHeadings[i] !== "*"))) &&
104+
((extraHeadings === 1) &&
105+
((requiredHeadings[i] !== "*") && (requiredHeadings[i] !== "^")))) &&
84106
(anyHeadings || !requiredHeadings.every((heading) => heading === "*"))
85107
) {
86108
addErrorContext(

‎schema/markdownlint-config-schema-strict.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,7 +3265,7 @@
32653265
],
32663266
"items": {
32673267
"type": "string",
3268-
"pattern": "^([*+?]|[#%]{1,6}\\s+\\S.*)$"
3268+
"pattern": "^([*+?]|\\^+|[#%]{1,6}\\s+\\S.*)$"
32693269
},
32703270
"default": null
32713271
},
@@ -3317,7 +3317,7 @@
33173317
],
33183318
"items": {
33193319
"type": "string",
3320-
"pattern": "^([*+?]|[#%]{1,6}\\s+\\S.*)$"
3320+
"pattern": "^([*+?]|\\^+|[#%]{1,6}\\s+\\S.*)$"
33213321
},
33223322
"default": null
33233323
},

‎schema/markdownlint-config-schema.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,7 +3265,7 @@
32653265
],
32663266
"items": {
32673267
"type": "string",
3268-
"pattern": "^([*+?]|[#%]{1,6}\\s+\\S.*)$"
3268+
"pattern": "^([*+?]|\\^+|[#%]{1,6}\\s+\\S.*)$"
32693269
},
32703270
"default": null
32713271
},
@@ -3317,7 +3317,7 @@
33173317
],
33183318
"items": {
33193319
"type": "string",
3320-
"pattern": "^([*+?]|[#%]{1,6}\\s+\\S.*)$"
3320+
"pattern": "^([*+?]|\\^+|[#%]{1,6}\\s+\\S.*)$"
33213321
},
33223322
"default": null
33233323
},

‎test/markdownlint-test-scenarios.mjs.snapshot‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54429,6 +54429,48 @@ exports[`markdownlint-test-scenarios.mjs > required-headings-all-present.md 1`]
5442954429
}
5443054430
`;
5443154431

54432+
exports[`markdownlint-test-scenarios.mjs > required-headings-changelog-example.md 1`] = `
54433+
{
54434+
"errors": [],
54435+
"fixed": [
54436+
"# Changelog",
54437+
"",
54438+
"## 1.0.0",
54439+
"",
54440+
"### Fixes",
54441+
"",
54442+
"### Features",
54443+
"",
54444+
"## 2.0.0",
54445+
"",
54446+
"### Fixes",
54447+
"",
54448+
"### Features",
54449+
"",
54450+
"## 2.1.0",
54451+
"",
54452+
"### Fixes",
54453+
"",
54454+
"### Features",
54455+
"",
54456+
"<!-- markdownlint-configure-file {",
54457+
" \\"required-headings\\": {",
54458+
" \\"headings\\": [",
54459+
" \\"# Changelog\\",",
54460+
" \\"^\\",",
54461+
" \\"%% \\\\\\\\d+\\\\\\\\.\\\\\\\\d+\\\\\\\\.\\\\\\\\d+\\",",
54462+
" \\"### Fixes\\",",
54463+
" \\"### Features\\",",
54464+
" \\"^\\"",
54465+
" ]",
54466+
" },",
54467+
" \\"no-duplicate-heading\\": false",
54468+
"} -->",
54469+
""
54470+
]
54471+
}
54472+
`;
54473+
5443254474
exports[`markdownlint-test-scenarios.mjs > required-headings-match-case.md 1`] = `
5443354475
{
5443454476
"errors": [],

‎test/markdownlint-test.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@ test.suite(import.meta.url.replace(/^.*?\/(?<name>[^/]*)$/u, "$<name>"), () => {
10941094
});
10951095

10961096
test("validateJsonUsingConfigSchemaStrict", async(t) => {
1097-
t.plan(240);
1097+
t.plan(241);
10981098
// @ts-ignore
10991099
const ajv = new Ajv(ajvOptions);
11001100
const validateSchemaStrict = ajv.compile(configSchemaStrict);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Changelog
2+
3+
## 1.0.0
4+
5+
### Fixes
6+
7+
### Features
8+
9+
## 2.0.0
10+
11+
### Fixes
12+
13+
### Features
14+
15+
## 2.1.0
16+
17+
### Fixes
18+
19+
### Features
20+
21+
<!-- markdownlint-configure-file {
22+
"required-headings": {
23+
"headings": [
24+
"# Changelog",
25+
"^",
26+
"%% \\d+\\.\\d+\\.\\d+",
27+
"### Fixes",
28+
"### Features",
29+
"^"
30+
]
31+
},
32+
"no-duplicate-heading": false
33+
} -->

0 commit comments

Comments
 (0)