Update MD024/no-duplicate-heading to allow non-sibling duplicates (fixes #136).

This commit is contained in:
David Anson 2018-07-19 21:49:30 -07:00
parent 4865301ce9
commit d76ede1c4f
10 changed files with 246 additions and 15 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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 = {

View file

@ -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",

View file

@ -0,0 +1,7 @@
{
"default": true,
"MD003": false,
"MD024": {
"siblings_only": true
}
}

View 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}

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD024": {
"allow_different_nesting": true
}
}

View file

@ -0,0 +1,11 @@
# Change log
## 2.0.0
### Bug fixes
### Features
## 1.0.0
### Bug fixes

View file

@ -0,0 +1,13 @@
# Change log
## 2.0.0
### Bug fixes
### Features
## 1.0.0
### Bug fixes
{MD024:11}

View file

@ -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);
} }