Skip to content

Commit 3ff13ca

Browse files
committed
wip
1 parent dbe6b64 commit 3ff13ca

23 files changed

Lines changed: 820 additions & 105 deletions

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,14 @@
533533

534534
- Initial release
535535
- Includes tests MD001-MD032
536+
537+
<!-- markdownlint-configure-file {
538+
"required-headings": {
539+
"headings": [
540+
"# Changelog",
541+
"^",
542+
"%% ^\\d+\\.\\d+\\.\\d+$",
543+
"^"
544+
]
545+
}
546+
} -->

‎doc-build/md043.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ To allow optional headings as with the following structure:
3131
### Notes (optional)
3232
```
3333

34-
Use the special value `"*"` meaning "zero or more unspecified headings", the
35-
special value `"+"` meaning "one or more unspecified headings", or the special
36-
value `"?"` meaning "exactly one unspecified heading at any level" and set the
34+
Use the special value `*` meaning "zero or more unspecified headings", the
35+
special value `+` meaning "one or more unspecified headings", or the special
36+
value `?` meaning "exactly one unspecified heading at any level" and set the
3737
`headings` parameter to something like:
3838

3939
```json
@@ -80,8 +80,8 @@ following:
8080
### Features
8181
```
8282

83-
Use the `"^"` special value meaning "if the next heading does not match, go back
84-
to the previous matching marker":
83+
Use the special value `^` meaning "if the next heading does not match, loop
84+
back to the previous matching marker":
8585

8686
```json
8787
[
@@ -94,9 +94,11 @@ to the previous matching marker":
9494
]
9595
```
9696

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 1,000 loops.
97+
The simple form shown above is enough for most scenarios; if multiple loops are
98+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The special
99+
value `?` is supported with loops, but `*` and `+` are not supported and will
100+
result in undefined behavior. To avoid infinite loops, a violation is reported
101+
if more than 1,000 loops are performed.
100102

101103
When an error is detected, this rule outputs the line number of the first
102104
problematic heading (otherwise, it outputs the last line number of the file).

‎doc/Rules.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,9 +1876,9 @@ To allow optional headings as with the following structure:
18761876
### Notes (optional)
18771877
```
18781878

1879-
Use the special value `"*"` meaning "zero or more unspecified headings", the
1880-
special value `"+"` meaning "one or more unspecified headings", or the special
1881-
value `"?"` meaning "exactly one unspecified heading at any level" and set the
1879+
Use the special value `*` meaning "zero or more unspecified headings", the
1880+
special value `+` meaning "one or more unspecified headings", or the special
1881+
value `?` meaning "exactly one unspecified heading at any level" and set the
18821882
`headings` parameter to something like:
18831883

18841884
```json
@@ -1925,8 +1925,8 @@ following:
19251925
### Features
19261926
```
19271927

1928-
Use the `"^"` special value meaning "if the next heading does not match, go back
1929-
to the previous matching marker":
1928+
Use the special value `^` meaning "if the next heading does not match, loop
1929+
back to the previous matching marker":
19301930

19311931
```json
19321932
[
@@ -1939,9 +1939,11 @@ to the previous matching marker":
19391939
]
19401940
```
19411941

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 1,000 loops.
1942+
The simple form shown above is enough for most scenarios; if multiple loops are
1943+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The special
1944+
value `?` is supported with loops, but `*` and `+` are not supported and will
1945+
result in undefined behavior. To avoid infinite loops, a violation is reported
1946+
if more than 1,000 loops are performed.
19451947

19461948
When an error is detected, this rule outputs the line number of the first
19471949
problematic heading (otherwise, it outputs the last line number of the file).

‎doc/md043.md‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ To allow optional headings as with the following structure:
4242
### Notes (optional)
4343
```
4444

45-
Use the special value `"*"` meaning "zero or more unspecified headings", the
46-
special value `"+"` meaning "one or more unspecified headings", or the special
47-
value `"?"` meaning "exactly one unspecified heading at any level" and set the
45+
Use the special value `*` meaning "zero or more unspecified headings", the
46+
special value `+` meaning "one or more unspecified headings", or the special
47+
value `?` meaning "exactly one unspecified heading at any level" and set the
4848
`headings` parameter to something like:
4949

5050
```json
@@ -91,8 +91,8 @@ following:
9191
### Features
9292
```
9393

94-
Use the `"^"` special value meaning "if the next heading does not match, go back
95-
to the previous matching marker":
94+
Use the special value `^` meaning "if the next heading does not match, loop
95+
back to the previous matching marker":
9696

9797
```json
9898
[
@@ -105,9 +105,11 @@ to the previous matching marker":
105105
]
106106
```
107107

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 1,000 loops.
108+
The simple form shown above is enough for most scenarios; if multiple loops are
109+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The special
110+
value `?` is supported with loops, but `*` and `+` are not supported and will
111+
result in undefined behavior. To avoid infinite loops, a violation is reported
112+
if more than 1,000 loops are performed.
111113

112114
When an error is detected, this rule outputs the line number of the first
113115
problematic heading (otherwise, it outputs the last line number of the file).

‎lib/md043.mjs‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// @ts-check
22

3-
import { addError, addErrorContext, addErrorDetailIf } from "../helpers/helpers.cjs";
3+
import { addError, addErrorDetailIf } from "../helpers/helpers.cjs";
44
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
55
import { filterByTypesCached } from "./cache.mjs";
66

77
export const headingsItemsRe = /^([*+?]|\^+|[#%]{1,6}\s+\S.*)$/u;
88
const headingPartsRe = /^([#%]{1,6})\s+(\S.*)$/u;
9+
const loopHeadingRe = /^\^+$/u;
10+
const validExtraHeadingsRe = /^(?:\*|\^+)$/u;
911

1012
const headingIsExpected = (/** @type {string} */ actual, /** @type {string} */ expected, /** @type {boolean} */ matchCase) => {
1113
if (expected.startsWith("%")) {
@@ -43,26 +45,31 @@ export default {
4345
let loopsRemaining = 1000;
4446
let matchAny = false;
4547
let hasError = false;
46-
let anyHeadings = false;
47-
const getExpected = () => requiredHeadings[i++] || "[No heading]";
48+
const getExpected = (/** @type {boolean} */ skipLoopHeadings) => {
49+
let expected = null;
50+
do {
51+
expected = requiredHeadings[i++] || "[No heading]";
52+
// eslint-disable-next-line no-unmodified-loop-condition
53+
} while (skipLoopHeadings && loopHeadingRe.test(expected));
54+
return expected;
55+
};
4856
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
4957
if (!hasError) {
5058
const headingText = getHeadingText(heading);
5159
const headingLevel = getHeadingLevel(heading);
52-
anyHeadings = true;
5360
const actual = `${"".padEnd(headingLevel, "#")} ${headingText}`;
54-
let expected = getExpected();
61+
let expected = getExpected(false);
5562

5663
if (expected.startsWith("^")) {
5764
const save = i;
58-
const nextExpected = getExpected();
65+
const nextExpected = getExpected(true);
5966
if (headingIsExpected(actual, nextExpected, matchCase)) {
6067
expected = nextExpected;
6168
} else {
6269
for (let j = save - 1; j--; j >= 0) {
6370
if (requiredHeadings[j] === expected) {
6471
i = j + 1;
65-
expected = getExpected();
72+
expected = getExpected(false);
6673
loopsRemaining--;
6774
break;
6875
}
@@ -78,7 +85,7 @@ export default {
7885
addError(onError, heading.startLine, "Too many loops");
7986
hasError = true;
8087
} else if (expected === "*") {
81-
const nextExpected = getExpected();
88+
const nextExpected = getExpected(false);
8289
if (!headingIsExpected(actual, nextExpected, matchCase)) {
8390
matchAny = true;
8491
i--;
@@ -102,18 +109,16 @@ export default {
102109
}
103110
}
104111
}
105-
const extraHeadings = requiredHeadings.length - i;
112+
const extraHeadings = requiredHeadings.slice(i);
106113
if (
107114
!hasError &&
108-
((extraHeadings > 1) ||
109-
((extraHeadings === 1) &&
110-
((requiredHeadings[i] !== "*") && (requiredHeadings[i] !== "^")))) &&
111-
(anyHeadings || !requiredHeadings.every((heading) => heading === "*"))
115+
!extraHeadings.every((heading) => validExtraHeadingsRe.test(heading))
112116
) {
113-
addErrorContext(
117+
addErrorDetailIf(
114118
onError,
115119
params.lines.length,
116-
requiredHeadings[i]
120+
requiredHeadings[i],
121+
"[End of file]"
117122
);
118123
}
119124
}

0 commit comments

Comments
 (0)