diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index 481c00b5..4fc25b27 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -4297,6 +4297,7 @@ module.exports = { "tags": ["headings", "headers"], "function": function MD043(params, onError) { const requiredHeadings = params.config.headings || params.config.headers; + const matchCase = params.config.match_case || false; if (Array.isArray(requiredHeadings)) { const levels = {}; for (const level of [1, 2, 3, 4, 5, 6]) { @@ -4307,6 +4308,7 @@ module.exports = { let hasError = false; let anyHeadings = false; const getExpected = () => requiredHeadings[i++] || "[None]"; + const handleCase = (str) => (matchCase ? str : str.toLowerCase()); forEachHeading(params, (heading, content) => { if (!hasError) { anyHeadings = true; @@ -4314,7 +4316,7 @@ module.exports = { const expected = getExpected(); if (expected === "*") { const nextExpected = getExpected(); - if (nextExpected.toLowerCase() !== actual.toLowerCase()) { + if (handleCase(nextExpected) !== handleCase(actual)) { matchAny = true; i--; } @@ -4322,7 +4324,7 @@ module.exports = { else if (expected === "+") { matchAny = true; } - else if (expected.toLowerCase() === actual.toLowerCase()) { + else if (handleCase(expected) === handleCase(actual)) { matchAny = false; } else if (matchAny) { diff --git a/doc/Rules.md b/doc/Rules.md index b8fa1842..0c425c3d 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -1696,7 +1696,7 @@ Tags: headings, headers Aliases: required-headings, required-headers -Parameters: headings, headers (array of string; default `null` for disabled) +Parameters: headings, headers, match_case (array of string; default `null` for disabled, boolean; default false) > If `headings` is not provided, `headers` (deprecated) will be used. @@ -1752,6 +1752,10 @@ problematic heading (otherwise, it outputs the last line number of the file). Note that while the `headings` parameter uses the "## Text" ATX heading style for simplicity, a file may use any supported heading style. +By default, the case of headings in the document is not required to match that of +`headings`. To require that case match exactly, set the `match_case` parameter to +`true`. + Rationale: Projects may wish to enforce a consistent document structure across a set of similar content. diff --git a/lib/md043.js b/lib/md043.js index 9b861155..753d11e0 100644 --- a/lib/md043.js +++ b/lib/md043.js @@ -11,6 +11,7 @@ module.exports = { "tags": [ "headings", "headers" ], "function": function MD043(params, onError) { const requiredHeadings = params.config.headings || params.config.headers; + const matchCase = params.config.match_case || false; if (Array.isArray(requiredHeadings)) { const levels = {}; for (const level of [ 1, 2, 3, 4, 5, 6 ]) { @@ -21,6 +22,7 @@ module.exports = { let hasError = false; let anyHeadings = false; const getExpected = () => requiredHeadings[i++] || "[None]"; + const handleCase = (str) => (matchCase ? str : str.toLowerCase()); forEachHeading(params, (heading, content) => { if (!hasError) { anyHeadings = true; @@ -28,13 +30,13 @@ module.exports = { const expected = getExpected(); if (expected === "*") { const nextExpected = getExpected(); - if (nextExpected.toLowerCase() !== actual.toLowerCase()) { + if (handleCase(nextExpected) !== handleCase(actual)) { matchAny = true; i--; } } else if (expected === "+") { matchAny = true; - } else if (expected.toLowerCase() === actual.toLowerCase()) { + } else if (handleCase(expected) === handleCase(actual)) { matchAny = false; } else if (matchAny) { i--; diff --git a/schema/.markdownlint.jsonc b/schema/.markdownlint.jsonc index 71092cab..73788cc0 100644 --- a/schema/.markdownlint.jsonc +++ b/schema/.markdownlint.jsonc @@ -228,7 +228,9 @@ // List of headings "headings": [], // List of headings - "headers": [] + "headers": [], + // Match case of headings + "match_case": false }, // MD044/proper-names - Proper names should have the correct capitalization diff --git a/schema/.markdownlint.yaml b/schema/.markdownlint.yaml index f6e75388..3c427e00 100644 --- a/schema/.markdownlint.yaml +++ b/schema/.markdownlint.yaml @@ -208,6 +208,8 @@ MD043: headings: [] # List of headings headers: [] + # Match case of headings + match_case: false # MD044/proper-names - Proper names should have the correct capitalization MD044: diff --git a/schema/build-config-schema.js b/schema/build-config-schema.js index 495548e2..8854ead8 100644 --- a/schema/build-config-schema.js +++ b/schema/build-config-schema.js @@ -395,6 +395,11 @@ for (const rule of rules) { "pattern": "^(\\*|\\+|#{1,6} .*)$" }, "default": [] + }, + "match_case": { + "description": "Match case of headings", + "type": "boolean", + "default": false } }; break; diff --git a/schema/markdownlint-config-schema.json b/schema/markdownlint-config-schema.json index 1eff90ed..565e27e7 100644 --- a/schema/markdownlint-config-schema.json +++ b/schema/markdownlint-config-schema.json @@ -751,6 +751,11 @@ "pattern": "^(\\*|\\+|#{1,6} .*)$" }, "default": [] + }, + "match_case": { + "description": "Match case of headings", + "type": "boolean", + "default": false } }, "additionalProperties": false diff --git a/test/required-headings-match-case.json b/test/required-headings-match-case.json new file mode 100644 index 00000000..4ca7ad25 --- /dev/null +++ b/test/required-headings-match-case.json @@ -0,0 +1,12 @@ +{ + "MD043": { + "headings": [ + "# Title", + "## First Heading", + "## Second Heading", + "*", + "## Third Heading" + ], + "match_case": true + } +} diff --git a/test/required-headings-match-case.md b/test/required-headings-match-case.md new file mode 100644 index 00000000..6e5debc1 --- /dev/null +++ b/test/required-headings-match-case.md @@ -0,0 +1,9 @@ +# Title + +## First Heading + +## Second Heading + +### Random heading + +## Third Heading diff --git a/test/required-headings-wrong-match-case.json b/test/required-headings-wrong-match-case.json new file mode 100644 index 00000000..4ca7ad25 --- /dev/null +++ b/test/required-headings-wrong-match-case.json @@ -0,0 +1,12 @@ +{ + "MD043": { + "headings": [ + "# Title", + "## First Heading", + "## Second Heading", + "*", + "## Third Heading" + ], + "match_case": true + } +} diff --git a/test/required-headings-wrong-match-case.md b/test/required-headings-wrong-match-case.md new file mode 100644 index 00000000..eb60926e --- /dev/null +++ b/test/required-headings-wrong-match-case.md @@ -0,0 +1,11 @@ +# Title + +## First Heading + +## SECOND HEADING + +{MD043:5} + +### Random heading + +## Third Heading diff --git a/test/snapshots/markdownlint-test-scenarios.js.md b/test/snapshots/markdownlint-test-scenarios.js.md index a1825ae1..41f6a2e0 100644 --- a/test/snapshots/markdownlint-test-scenarios.js.md +++ b/test/snapshots/markdownlint-test-scenarios.js.md @@ -34838,6 +34838,24 @@ Generated by [AVA](https://avajs.dev). `, } +## required-headings-match-case.md + +> Snapshot 1 + + { + errors: [], + fixed: `# Title␊ + ␊ + ## First Heading␊ + ␊ + ## Second Heading␊ + ␊ + ### Random heading␊ + ␊ + ## Third Heading␊ + `, + } + ## required-headings-missing-first.md > Snapshot 1 @@ -35216,6 +35234,41 @@ Generated by [AVA](https://avajs.dev). `, } +## required-headings-wrong-match-case.md + +> Snapshot 1 + + { + errors: [ + { + errorContext: null, + errorDetail: 'Expected: ## Second Heading; Actual: ## SECOND HEADING', + errorRange: null, + fixInfo: null, + lineNumber: 5, + ruleDescription: 'Required heading structure', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md043', + ruleNames: [ + 'MD043', + 'required-headings', + 'required-headers', + ], + }, + ], + fixed: `# Title␊ + ␊ + ## First Heading␊ + ␊ + ## SECOND HEADING␊ + ␊ + {MD043:5}␊ + ␊ + ### Random heading␊ + ␊ + ## Third Heading␊ + `, + } + ## required-headings-zero-or-more-last.md > Snapshot 1 diff --git a/test/snapshots/markdownlint-test-scenarios.js.snap b/test/snapshots/markdownlint-test-scenarios.js.snap index 90f37608..b1eb2b92 100644 Binary files a/test/snapshots/markdownlint-test-scenarios.js.snap and b/test/snapshots/markdownlint-test-scenarios.js.snap differ