Skip to content

Commit 75d3db6

Browse files
committed
wip
1 parent 3ff13ca commit 75d3db6

11 files changed

Lines changed: 194 additions & 113 deletions

‎doc-build/md043.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,18 @@ back to the previous matching marker":
8787
[
8888
"# Changelog",
8989
"^",
90-
"%% \\d+\\.\\d+\\.\\d+",
90+
"%% ^\\d+\\.\\d+\\.\\d+$",
9191
"### Fixes",
9292
"### Features",
9393
"^"
9494
]
9595
```
9696

9797
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.
98+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The prefix `%`
99+
and the special value `?` are supported with loops, but `*` and `+` are not and
100+
will result in undefined behavior. To avoid infinite loops, a violation is
101+
reported if more than 1,000 loops are performed.
102102

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

‎doc/Rules.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,18 +1932,18 @@ back to the previous matching marker":
19321932
[
19331933
"# Changelog",
19341934
"^",
1935-
"%% \\d+\\.\\d+\\.\\d+",
1935+
"%% ^\\d+\\.\\d+\\.\\d+$",
19361936
"### Fixes",
19371937
"### Features",
19381938
"^"
19391939
]
19401940
```
19411941

19421942
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.
1943+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The prefix `%`
1944+
and the special value `?` are supported with loops, but `*` and `+` are not and
1945+
will result in undefined behavior. To avoid infinite loops, a violation is
1946+
reported if more than 1,000 loops are performed.
19471947

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

‎doc/md043.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,18 @@ back to the previous matching marker":
9898
[
9999
"# Changelog",
100100
"^",
101-
"%% \\d+\\.\\d+\\.\\d+",
101+
"%% ^\\d+\\.\\d+\\.\\d+$",
102102
"### Fixes",
103103
"### Features",
104104
"^"
105105
]
106106
```
107107

108108
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.
109+
needed, the values `^^`, `^^^`, etc. can be used in conjunction. The prefix `%`
110+
and the special value `?` are supported with loops, but `*` and `+` are not and
111+
will result in undefined behavior. To avoid infinite loops, a violation is
112+
reported if more than 1,000 loops are performed.
113113

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

‎lib/md043.mjs‎

Lines changed: 62 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { addError, 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;
8-
const headingPartsRe = /^([#%]{1,6})\s+(\S.*)$/u;
7+
export const headingsItemsRe = /^(?:[*+?]|\^+|[#%]{1,6}\s+\S.*)$/u;
8+
const prefixedHeadingPartsRe = /^([#%]{1,6})\s+(\S.*)$/u;
99
const loopHeadingRe = /^\^+$/u;
10-
const validExtraHeadingsRe = /^(?:\*|\^+)$/u;
10+
const canMatchNothingHeadingsRe = /^(?:\*|\^+)$/u;
1111

1212
const headingIsExpected = (/** @type {string} */ actual, /** @type {string} */ expected, /** @type {boolean} */ matchCase) => {
1313
if (expected.startsWith("%")) {
1414
// Regular expression comparison
15-
const [ , actualLevel, actualText ] = headingPartsRe.exec(actual) || [];
16-
const [ , expectedLevel, expectedText ] = headingPartsRe.exec(expected) || [];
15+
const [ , actualLevel, actualText ] = prefixedHeadingPartsRe.exec(actual) || [];
16+
const [ , expectedLevel, expectedText ] = prefixedHeadingPartsRe.exec(expected) || [];
1717
return (
1818
(actualLevel.length === expectedLevel.length) &&
1919
// eslint-disable-next-line unicorn/no-unreadable-new-expression
@@ -36,14 +36,13 @@ export default {
3636
/** @type {string[] | null | undefined} */
3737
const headings = params.config.headings;
3838
if (!headings) {
39-
// Nothing to check
4039
return;
4140
}
4241
const requiredHeadings = headings.map((heading) => headingsItemsRe.test(heading) ? heading : "[Invalid syntax]");
43-
const matchCase = params.config.match_case || false;
42+
const matchCase = Boolean(params.config.match_case);
4443
let i = 0;
45-
let loopsRemaining = 1000;
46-
let matchAny = false;
44+
let loopsAllowed = 1000;
45+
let allowAnyMatchFallback = false;
4746
let hasError = false;
4847
const getExpected = (/** @type {boolean} */ skipLoopHeadings) => {
4948
let expected = null;
@@ -54,72 +53,70 @@ export default {
5453
return expected;
5554
};
5655
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
57-
if (!hasError) {
58-
const headingText = getHeadingText(heading);
59-
const headingLevel = getHeadingLevel(heading);
60-
const actual = `${"".padEnd(headingLevel, "#")} ${headingText}`;
61-
let expected = getExpected(false);
62-
63-
if (expected.startsWith("^")) {
64-
const save = i;
65-
const nextExpected = getExpected(true);
66-
if (headingIsExpected(actual, nextExpected, matchCase)) {
67-
expected = nextExpected;
68-
} else {
69-
for (let j = save - 1; j--; j >= 0) {
70-
if (requiredHeadings[j] === expected) {
71-
i = j + 1;
72-
expected = getExpected(false);
73-
loopsRemaining--;
74-
break;
75-
}
76-
}
77-
if (i < 0) {
78-
// Nothing to skip back to; continue from current location
79-
i = save;
56+
const headingText = getHeadingText(heading);
57+
const headingLevel = getHeadingLevel(heading);
58+
const actual = `${"".padEnd(headingLevel, "#")} ${headingText}`;
59+
let expected = getExpected(false);
60+
if (expected.startsWith("^")) {
61+
// Handle loop
62+
const current = i;
63+
const nextExpected = getExpected(true);
64+
if (headingIsExpected(actual, nextExpected, matchCase)) {
65+
// No need to loop (again)
66+
expected = nextExpected;
67+
} else {
68+
// Loop to previous matching marker
69+
let j = current - 1;
70+
for (; j--; j >= 0) {
71+
if (requiredHeadings[j] === expected) {
72+
i = j + 1;
73+
expected = getExpected(false);
74+
loopsAllowed--;
75+
break;
8076
}
8177
}
82-
}
83-
84-
if (loopsRemaining < 0) {
85-
addError(onError, heading.startLine, "Too many loops");
86-
hasError = true;
87-
} else if (expected === "*") {
88-
const nextExpected = getExpected(false);
89-
if (!headingIsExpected(actual, nextExpected, matchCase)) {
90-
matchAny = true;
91-
i--;
78+
if (j < 0) {
79+
expected = nextExpected;
9280
}
93-
} else if (expected === "+") {
94-
matchAny = true;
95-
} else if (expected === "?") {
96-
// Allow current, match next
97-
} else if (headingIsExpected(actual, expected, matchCase)) {
98-
matchAny = false;
99-
} else if (matchAny) {
81+
}
82+
}
83+
if (loopsAllowed < 0) {
84+
// Too many loops
85+
addError(onError, heading.startLine, "Too many loops");
86+
hasError = true;
87+
break;
88+
} else if (expected === "*") {
89+
// Handle "zero or more of anything"
90+
const nextExpected = getExpected(true);
91+
if (!headingIsExpected(actual, nextExpected, matchCase)) {
92+
allowAnyMatchFallback = true;
10093
i--;
101-
} else {
102-
addErrorDetailIf(
103-
onError,
104-
heading.startLine,
105-
expected,
106-
actual
107-
);
108-
hasError = true;
10994
}
95+
} else if (expected === "+") {
96+
// Handle "one or more of anything"
97+
allowAnyMatchFallback = true;
98+
} else if (expected === "?") {
99+
// Handle "exactly one of anything"
100+
} else if (headingIsExpected(actual, expected, matchCase)) {
101+
// Handle successful match
102+
allowAnyMatchFallback = false;
103+
} else if (allowAnyMatchFallback) {
104+
// Handle "anything" match by restoring index
105+
i--;
106+
} else {
107+
// Report heading mismatch
108+
addErrorDetailIf(onError, heading.startLine, expected, actual);
109+
hasError = true;
110+
break;
110111
}
111112
}
112-
const extraHeadings = requiredHeadings.slice(i);
113+
const remainingRequiredHeadings = requiredHeadings.slice(i).filter((heading) => !canMatchNothingHeadingsRe.test(heading));
113114
if (
114115
!hasError &&
115-
!extraHeadings.every((heading) => validExtraHeadingsRe.test(heading))
116+
(remainingRequiredHeadings.length > 0)
116117
) {
117-
addErrorDetailIf(
118-
onError,
119-
params.lines.length,
120-
requiredHeadings[i],
121-
"[End of file]"
122-
);
118+
// Report unmatched heading(s)
119+
addErrorDetailIf(onError, params.lines.length, remainingRequiredHeadings[0], "[End of file]");
123120
}
124121
}
125122
};

‎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
},

0 commit comments

Comments
 (0)