mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02:00
Update MD024/no-duplicate-heading to allow non-sibling duplicates (fixes #136).
This commit is contained in:
parent
4865301ce9
commit
d76ede1c4f
10 changed files with 246 additions and 15 deletions
19
doc/Rules.md
19
doc/Rules.md
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
# Rules
|
# Rules
|
||||||
|
|
||||||
This document contains a description of all rules, what they are checking for,
|
This document contains a description of all rules, what they are checking for,
|
||||||
|
@ -681,6 +682,8 @@ Tags: headings, headers
|
||||||
|
|
||||||
Aliases: no-duplicate-heading, no-duplicate-header
|
Aliases: no-duplicate-heading, no-duplicate-header
|
||||||
|
|
||||||
|
Parameters: siblings_only, allow_different_nesting (boolean; default `false`)
|
||||||
|
|
||||||
This rule is triggered if there are multiple headings in the document that have
|
This rule is triggered if there are multiple headings in the document that have
|
||||||
the same text:
|
the same text:
|
||||||
|
|
||||||
|
@ -702,6 +705,22 @@ Rationale: Some markdown parses generate anchors for headings based on the
|
||||||
heading name, and having headings with the same content can cause problems with
|
heading name, and having headings with the same content can cause problems with
|
||||||
this.
|
this.
|
||||||
|
|
||||||
|
If the parameter `siblings_only` (alternatively `allow_different_nesting`) is
|
||||||
|
set to `true`, heading duplication is allowed for non-sibling headings (common
|
||||||
|
in change logs):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Change log
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
```
|
||||||
|
|
||||||
<a name="md025"></a>
|
<a name="md025"></a>
|
||||||
|
|
||||||
## MD025 - Multiple top level headings in the same document
|
## MD025 - Multiple top level headings in the same document
|
||||||
|
|
18
lib/md024.js
18
lib/md024.js
|
@ -9,8 +9,24 @@ module.exports = {
|
||||||
"description": "Multiple headings with the same content",
|
"description": "Multiple headings with the same content",
|
||||||
"tags": [ "headings", "headers" ],
|
"tags": [ "headings", "headers" ],
|
||||||
"function": function MD024(params, onError) {
|
"function": function MD024(params, onError) {
|
||||||
const knownContent = [];
|
const siblingsOnly = params.config.siblings_only ||
|
||||||
|
params.config.allow_different_nesting || false;
|
||||||
|
const knownContents = [ null, [] ];
|
||||||
|
let lastLevel = 1;
|
||||||
|
let knownContent = knownContents[lastLevel];
|
||||||
shared.forEachHeading(params, function forHeading(heading, content) {
|
shared.forEachHeading(params, function forHeading(heading, content) {
|
||||||
|
if (siblingsOnly) {
|
||||||
|
const newLevel = heading.tag.slice(1);
|
||||||
|
while (lastLevel < newLevel) {
|
||||||
|
lastLevel++;
|
||||||
|
knownContents[lastLevel] = [];
|
||||||
|
}
|
||||||
|
while (lastLevel > newLevel) {
|
||||||
|
knownContents[lastLevel] = [];
|
||||||
|
lastLevel--;
|
||||||
|
}
|
||||||
|
knownContent = knownContents[newLevel];
|
||||||
|
}
|
||||||
if (knownContent.indexOf(content) === -1) {
|
if (knownContent.indexOf(content) === -1) {
|
||||||
knownContent.push(content);
|
knownContent.push(content);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,6 +25,7 @@ const schema = {
|
||||||
const tags = {};
|
const tags = {};
|
||||||
|
|
||||||
// Add rules
|
// Add rules
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
rules.forEach(function forRule(rule) {
|
rules.forEach(function forRule(rule) {
|
||||||
rule.tags.forEach(function forTag(tag) {
|
rule.tags.forEach(function forTag(tag) {
|
||||||
const tagRules = tags[tag] || [];
|
const tagRules = tags[tag] || [];
|
||||||
|
@ -151,6 +152,20 @@ rules.forEach(function forRule(rule) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "MD024":
|
||||||
|
scheme.properties = {
|
||||||
|
"allow_different_nesting": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"siblings_only": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
case "MD026":
|
case "MD026":
|
||||||
case "MD036":
|
case "MD036":
|
||||||
scheme.properties = {
|
scheme.properties = {
|
||||||
|
|
|
@ -515,18 +515,66 @@
|
||||||
},
|
},
|
||||||
"MD024": {
|
"MD024": {
|
||||||
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
||||||
"type": "boolean",
|
"type": [
|
||||||
"default": true
|
"boolean",
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"default": true,
|
||||||
|
"properties": {
|
||||||
|
"allow_different_nesting": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"siblings_only": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"no-duplicate-heading": {
|
"no-duplicate-heading": {
|
||||||
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
||||||
"type": "boolean",
|
"type": [
|
||||||
"default": true
|
"boolean",
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"default": true,
|
||||||
|
"properties": {
|
||||||
|
"allow_different_nesting": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"siblings_only": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"no-duplicate-header": {
|
"no-duplicate-header": {
|
||||||
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
|
||||||
"type": "boolean",
|
"type": [
|
||||||
"default": true
|
"boolean",
|
||||||
|
"object"
|
||||||
|
],
|
||||||
|
"default": true,
|
||||||
|
"properties": {
|
||||||
|
"allow_different_nesting": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"siblings_only": {
|
||||||
|
"description": "Only check sibling headings",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"MD025": {
|
"MD025": {
|
||||||
"description": "MD025/single-h1 - Multiple top level headings in the same document",
|
"description": "MD025/single-h1 - Multiple top level headings in the same document",
|
||||||
|
|
7
test/heading-duplicate-content-siblings-only.json
Normal file
7
test/heading-duplicate-content-siblings-only.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD003": false,
|
||||||
|
"MD024": {
|
||||||
|
"siblings_only": true
|
||||||
|
}
|
||||||
|
}
|
96
test/heading-duplicate-content-siblings-only.md
Normal file
96
test/heading-duplicate-content-siblings-only.md
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# Heading duplicate content siblings only
|
||||||
|
|
||||||
|
# A
|
||||||
|
|
||||||
|
{MD025:3}
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
{MD024:11}
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
## D
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
### E
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
{MD024:23}
|
||||||
|
|
||||||
|
##### F
|
||||||
|
|
||||||
|
{MD001:27}
|
||||||
|
|
||||||
|
#### G
|
||||||
|
|
||||||
|
##### F
|
||||||
|
|
||||||
|
#### G
|
||||||
|
|
||||||
|
{MD024:35}
|
||||||
|
|
||||||
|
### E
|
||||||
|
|
||||||
|
{MD024:39}
|
||||||
|
|
||||||
|
# A
|
||||||
|
|
||||||
|
{MD024:43} {MD025:43}
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
### C
|
||||||
|
|
||||||
|
## B
|
||||||
|
|
||||||
|
{MD024:51}
|
||||||
|
|
||||||
|
# Heading duplicate content siblings only
|
||||||
|
|
||||||
|
{MD024:55} {MD025:55}
|
||||||
|
|
||||||
|
AA
|
||||||
|
==
|
||||||
|
|
||||||
|
{MD025:59}
|
||||||
|
|
||||||
|
AA
|
||||||
|
--
|
||||||
|
|
||||||
|
BB
|
||||||
|
--
|
||||||
|
|
||||||
|
CC
|
||||||
|
--
|
||||||
|
|
||||||
|
BB
|
||||||
|
--
|
||||||
|
|
||||||
|
{MD024:73}
|
||||||
|
|
||||||
|
BB
|
||||||
|
==
|
||||||
|
|
||||||
|
{MD025:78}
|
||||||
|
|
||||||
|
BB
|
||||||
|
--
|
||||||
|
|
||||||
|
## AAA ##
|
||||||
|
|
||||||
|
### BBB ###
|
||||||
|
|
||||||
|
## BBB ##
|
||||||
|
|
||||||
|
### BBB ###
|
||||||
|
|
||||||
|
## BBB ##
|
||||||
|
|
||||||
|
{MD024:94}
|
6
test/heading_duplicate_content_different_nesting.json
Normal file
6
test/heading_duplicate_content_different_nesting.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD024": {
|
||||||
|
"allow_different_nesting": true
|
||||||
|
}
|
||||||
|
}
|
11
test/heading_duplicate_content_different_nesting.md
Normal file
11
test/heading_duplicate_content_different_nesting.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Change log
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
### Bug fixes
|
13
test/heading_duplicate_content_no_different_nesting.md
Normal file
13
test/heading_duplicate_content_no_different_nesting.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Change log
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
{MD024:11}
|
|
@ -1117,7 +1117,7 @@ module.exports.readme = function readme(test) {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.doc = function doc(test) {
|
module.exports.doc = function doc(test) {
|
||||||
test.expect(310);
|
test.expect(311);
|
||||||
fs.readFile("doc/Rules.md", shared.utf8Encoding,
|
fs.readFile("doc/Rules.md", shared.utf8Encoding,
|
||||||
function readFile(err, contents) {
|
function readFile(err, contents) {
|
||||||
test.ifError(err);
|
test.ifError(err);
|
||||||
|
@ -1131,11 +1131,11 @@ module.exports.doc = function doc(test) {
|
||||||
function testTagsAliasesParams(r) {
|
function testTagsAliasesParams(r) {
|
||||||
r = r || "[NO RULE]";
|
r = r || "[NO RULE]";
|
||||||
test.ok(ruleHasTags,
|
test.ok(ruleHasTags,
|
||||||
"Missing tags for rule " + r.toString() + ".");
|
"Missing tags for rule " + r.names + ".");
|
||||||
test.ok(ruleHasAliases,
|
test.ok(ruleHasAliases,
|
||||||
"Missing aliases for rule " + r.toString() + ".");
|
"Missing aliases for rule " + r.names + ".");
|
||||||
test.ok(!ruleUsesParams,
|
test.ok(!ruleUsesParams,
|
||||||
"Missing parameters for rule " + r.toString() + ".");
|
"Missing parameters for rule " + r.names + ".");
|
||||||
}
|
}
|
||||||
md.parse(contents, {}).forEach(function forToken(token) {
|
md.parse(contents, {}).forEach(function forToken(token) {
|
||||||
if ((token.type === "heading_open") && (token.tag === "h2")) {
|
if ((token.type === "heading_open") && (token.tag === "h2")) {
|
||||||
|
@ -1161,12 +1161,12 @@ module.exports.doc = function doc(test) {
|
||||||
}
|
}
|
||||||
} else if (/^Tags: /.test(token.content) && rule) {
|
} else if (/^Tags: /.test(token.content) && rule) {
|
||||||
test.deepEqual(token.content.split(tagAliasParameterRe).slice(1),
|
test.deepEqual(token.content.split(tagAliasParameterRe).slice(1),
|
||||||
rule.tags, "Tag mismatch for rule " + rule.toString() + ".");
|
rule.tags, "Tag mismatch for rule " + rule.names + ".");
|
||||||
ruleHasTags = true;
|
ruleHasTags = true;
|
||||||
} else if (/^Aliases: /.test(token.content) && rule) {
|
} else if (/^Aliases: /.test(token.content) && rule) {
|
||||||
test.deepEqual(token.content.split(tagAliasParameterRe).slice(1),
|
test.deepEqual(token.content.split(tagAliasParameterRe).slice(1),
|
||||||
rule.names.slice(1),
|
rule.names.slice(1),
|
||||||
"Alias mismatch for rule " + rule.toString() + ".");
|
"Alias mismatch for rule " + rule.names + ".");
|
||||||
ruleHasAliases = true;
|
ruleHasAliases = true;
|
||||||
} else if (/^Parameters: /.test(token.content) && rule) {
|
} else if (/^Parameters: /.test(token.content) && rule) {
|
||||||
let inDetails = false;
|
let inDetails = false;
|
||||||
|
@ -1177,7 +1177,7 @@ module.exports.doc = function doc(test) {
|
||||||
return !inDetails;
|
return !inDetails;
|
||||||
});
|
});
|
||||||
test.deepEqual(parameters, ruleUsesParams,
|
test.deepEqual(parameters, ruleUsesParams,
|
||||||
"Missing parameter for rule " + rule.toString());
|
"Missing parameter for rule " + rule.names);
|
||||||
ruleUsesParams = null;
|
ruleUsesParams = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1185,7 +1185,7 @@ module.exports.doc = function doc(test) {
|
||||||
const ruleLeft = rulesLeft.shift();
|
const ruleLeft = rulesLeft.shift();
|
||||||
test.ok(!ruleLeft,
|
test.ok(!ruleLeft,
|
||||||
"Missing rule documentation for " +
|
"Missing rule documentation for " +
|
||||||
(ruleLeft || "[NO RULE]").toString() + ".");
|
(ruleLeft || { "names": "[NO RULE]" }).names + ".");
|
||||||
if (rule) {
|
if (rule) {
|
||||||
testTagsAliasesParams(rule);
|
testTagsAliasesParams(rule);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue