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
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
Parameters: siblings_only, allow_different_nesting (boolean; default `false`)
This rule is triggered if there are multiple headings in the document that have
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
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>
## MD025 - Multiple top level headings in the same document

View file

@ -9,8 +9,24 @@ module.exports = {
"description": "Multiple headings with the same content",
"tags": [ "headings", "headers" ],
"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) {
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) {
knownContent.push(content);
} else {

View file

@ -25,6 +25,7 @@ const schema = {
const tags = {};
// Add rules
// eslint-disable-next-line complexity
rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) {
const tagRules = tags[tag] || [];
@ -151,6 +152,20 @@ rules.forEach(function forRule(rule) {
}
};
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 "MD036":
scheme.properties = {

View file

@ -515,18 +515,66 @@
},
"MD024": {
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
"type": "boolean",
"default": true
"type": [
"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": {
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
"type": "boolean",
"default": true
"type": [
"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": {
"description": "MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content",
"type": "boolean",
"default": true
"type": [
"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": {
"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) {
test.expect(310);
test.expect(311);
fs.readFile("doc/Rules.md", shared.utf8Encoding,
function readFile(err, contents) {
test.ifError(err);
@ -1131,11 +1131,11 @@ module.exports.doc = function doc(test) {
function testTagsAliasesParams(r) {
r = r || "[NO RULE]";
test.ok(ruleHasTags,
"Missing tags for rule " + r.toString() + ".");
"Missing tags for rule " + r.names + ".");
test.ok(ruleHasAliases,
"Missing aliases for rule " + r.toString() + ".");
"Missing aliases for rule " + r.names + ".");
test.ok(!ruleUsesParams,
"Missing parameters for rule " + r.toString() + ".");
"Missing parameters for rule " + r.names + ".");
}
md.parse(contents, {}).forEach(function forToken(token) {
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) {
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;
} else if (/^Aliases: /.test(token.content) && rule) {
test.deepEqual(token.content.split(tagAliasParameterRe).slice(1),
rule.names.slice(1),
"Alias mismatch for rule " + rule.toString() + ".");
"Alias mismatch for rule " + rule.names + ".");
ruleHasAliases = true;
} else if (/^Parameters: /.test(token.content) && rule) {
let inDetails = false;
@ -1177,7 +1177,7 @@ module.exports.doc = function doc(test) {
return !inDetails;
});
test.deepEqual(parameters, ruleUsesParams,
"Missing parameter for rule " + rule.toString());
"Missing parameter for rule " + rule.names);
ruleUsesParams = null;
}
}
@ -1185,7 +1185,7 @@ module.exports.doc = function doc(test) {
const ruleLeft = rulesLeft.shift();
test.ok(!ruleLeft,
"Missing rule documentation for " +
(ruleLeft || "[NO RULE]").toString() + ".");
(ruleLeft || { "names": "[NO RULE]" }).names + ".");
if (rule) {
testTagsAliasesParams(rule);
}