Add MD043 required-headers "Required header structure" (fixes #22).

This commit is contained in:
David Anson 2016-07-02 22:37:52 -07:00
parent 2612a96ae8
commit c8ecec1953
26 changed files with 316 additions and 10 deletions

View file

@ -82,6 +82,7 @@ playground for learning and exploring.
* **MD040** *fenced-code-language* - Fenced code blocks should have a language specified
* **MD041** *first-line-h1* - First line in file should be a top level header
* **MD042** *no-empty-links* - No empty links
* **MD043** *required-headers* - Required header structure
See [Rules.md](doc/Rules.md) for more details.
@ -96,7 +97,7 @@ See [Rules.md](doc/Rules.md) for more details.
* **emphasis** - MD036, MD037
* **hard_tab** - MD010
* **headers** - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023,
MD024, MD025, MD026, MD036, MD041
MD024, MD025, MD026, MD036, MD041, MD043
* **hr** - MD035
* **html** - MD033
* **indentation** - MD005, MD006, MD007, MD027

View file

@ -222,7 +222,7 @@ Tags: whitespace
Aliases: no-trailing-spaces
Parameters: br_spaces (number; default: 0)
Parameters: br_spaces (number; default 0)
This rule is triggered on any lines that end with whitespace. To fix this,
find the line that is triggered and remove any trailing spaces from the end.
@ -651,7 +651,7 @@ Tags: ol, ul, whitespace
Aliases: list-marker-space
Parameters: ul_single, ol_single, ul_multi, ol_multi (number, default 1)
Parameters: ul_single, ol_single, ul_multi, ol_multi (number; default 1)
This rule checks for the number of spaces between a list marker (e.g. '`-`',
'`*`', '`+`' or '`1.`') and the text of the list item.
@ -1026,3 +1026,54 @@ Empty fragments will trigger this rule:
But non-empty fragments will not:
[a valid fragment](#fragment)
## MD043 - Required header structure
Tags: headers
Aliases: required-headers
Parameters: headers (array of string; default `null` for disabled)
This rule is triggered when the headers in a file do not match the array of
headers passed to the rule. It can be used to enforce a standard header
structure for a set of files.
To require exactly the following structure:
# Head
## Item
### Detail
Set the `headers` parameter to:
[
"# Head",
"## Item",
"### Detail"
]
To allow optional headers as with the following structure:
# Head
## Item
### Detail (optional)
## Foot
### Notes (optional)
Use the special value `"*"` meaning "one or more unspecified headers" and set
the `headers` parameter to:
[
"# Head",
"## Item",
"*",
"## Foot",
"*"
]
When an error is detected, this rule outputs the line number of the first
problematic header (otherwise, it outputs the last line number of the file).
Note that while the `headers` parameter uses the "## Text" ATX header style for
simplicity, a file may use any supported header style.

View file

@ -898,7 +898,7 @@ module.exports = [
"desc": "No empty links",
"tags": [ "links" ],
"aliases": [ "no-empty-links" ],
"func": function MD034(params, errors) {
"func": function MD042(params, errors) {
forEachInlineChild(params, "link_open", function forToken(token) {
token.attrs.forEach(function forAttr(attr) {
if (attr[0] === "href" && (!attr[1] || (attr[1] === "#"))) {
@ -907,5 +907,41 @@ module.exports = [
});
});
}
},
{
"name": "MD043",
"desc": "Required header structure",
"tags": [ "headers" ],
"aliases": [ "required-headers" ],
"func": function MD043(params, errors) {
var requiredHeaders = params.options.headers;
if (requiredHeaders) {
var levels = {};
[ 1, 2, 3, 4, 5, 6 ].forEach(function forLevel(level) {
levels["h" + level] = "######".substr(-level);
});
var i = 0;
var optional = false;
forEachHeading(params, function forHeading(heading, content) {
if (!errors.length) {
var actual = levels[heading.tag] + " " + content;
var expected = requiredHeaders[i++] || "";
if (expected === "*") {
optional = true;
} else if (expected.toLowerCase() === actual.toLowerCase()) {
optional = false;
} else if (optional) {
i--;
} else {
errors.push(heading.lineNumber);
}
}
});
if ((i < requiredHeaders.length) && !errors.length) {
errors.push(params.lines.length);
}
}
}
}
];

View file

@ -1,4 +1,11 @@
{
"default": true,
"MD041": true
"MD041": true,
"MD043": {
"headers": [
"## Header 1 {MD002} {MD041}",
"#### Header 2 {MD001}",
"# Broken"
]
}
}

View file

@ -2,7 +2,7 @@
#### Header 2 {MD001}
# Header 3 {MD003} #
# Header 3 {MD003} {MD043} #
* list
+ list {MD004} {MD006} {MD007} {MD030}

View file

@ -632,6 +632,44 @@ module.exports.customFrontMatter = function customFrontMatter(test) {
});
};
module.exports.readmeHeaders = function readmeHeaders(test) {
test.expect(2);
markdownlint({
"files": "README.md",
"config": {
"default": false,
"MD043": {
"headers": [
"# markdownlint",
"## Install",
"## Overview",
"### Related",
"## Demonstration",
"## Rules / Aliases",
"## Tags",
"## Configuration",
"## API",
"### options",
"#### options.files",
"#### options.strings",
"#### options.frontMatter",
"#### options.config",
"### callback",
"### result",
"## Usage",
"## Browser",
"## History"
]
}
}
}, function callback(err, result) {
test.ifError(err);
var expected = { "README.md": {} };
test.deepEqual(result, expected, "Unexpected issues.");
test.done();
});
};
module.exports.filesArrayNotModified = function filesArrayNotModified(test) {
test.expect(2);
var files = [
@ -733,7 +771,7 @@ module.exports.missingStringValue = function missingStringValue(test) {
};
module.exports.ruleNamesUpperCase = function ruleNamesUpperCase(test) {
test.expect(38);
test.expect(39);
rules.forEach(function forRule(rule) {
test.equal(rule.name, rule.name.toUpperCase(), "Rule name not upper-case.");
});
@ -741,7 +779,7 @@ module.exports.ruleNamesUpperCase = function ruleNamesUpperCase(test) {
};
module.exports.uniqueAliases = function uniqueAliases(test) {
test.expect(76);
test.expect(78);
var tags = [];
rules.forEach(function forRule(rule) {
Array.prototype.push.apply(tags, rule.tags);
@ -758,7 +796,7 @@ module.exports.uniqueAliases = function uniqueAliases(test) {
};
module.exports.readme = function readme(test) {
test.expect(99);
test.expect(101);
var tagToRules = {};
rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) {
@ -819,7 +857,7 @@ module.exports.readme = function readme(test) {
};
module.exports.doc = function doc(test) {
test.expect(281);
test.expect(289);
fs.readFile("doc/Rules.md", shared.utf8Encoding,
function readFile(err, contents) {
test.ifError(err);

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD043": {
"headers": [ "*" ]
}
}

View file

@ -0,0 +1,11 @@
# One
## Two
### THREE
#### four
##### Five
###### SiX

View file

@ -0,0 +1,13 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"## Two",
"### Three",
"## Four",
"## Five",
"### Six"
]
}
}

View file

@ -0,0 +1,11 @@
# One
## Two
### THREE
## four
## Five
### SiX

View file

@ -0,0 +1,10 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"## Two",
"### Three"
]
}
}

View file

@ -0,0 +1,5 @@
text
## Two {MD002} {MD043}
### Three

View file

@ -0,0 +1,10 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"## Two",
"### Three"
]
}
}

View file

@ -0,0 +1,7 @@
One
===
Two
---
{MD043}

View file

@ -0,0 +1,11 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"## Two",
"### Three",
"#### Four"
]
}
}

View file

@ -0,0 +1,5 @@
# One #
### Three {MD001} {MD043} ###
#### Four ####

View file

@ -0,0 +1,6 @@
{
"default": true,
"MD043": {
"headers": []
}
}

View file

@ -0,0 +1,5 @@
# One {MD043}
## Two
### Three

View file

@ -0,0 +1,10 @@
{
"default": true,
"MD043": {
"headers": [
"*",
"### Three",
"#### Four"
]
}
}

View file

@ -0,0 +1,7 @@
# One
## Two
### Three
#### Four

View file

@ -0,0 +1,10 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"## Two",
"*"
]
}
}

View file

@ -0,0 +1,7 @@
# One
## Two
### Three
#### Four

View file

@ -0,0 +1,12 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"*",
"### Three",
"*",
"##### Five"
]
}
}

View file

@ -0,0 +1,9 @@
# One #
## Two ##
### Three ###
#### Four ####
##### Five #####

View file

@ -0,0 +1,11 @@
{
"default": true,
"MD043": {
"headers": [
"# One",
"*",
"*",
"#### Four"
]
}
}

View file

@ -0,0 +1,7 @@
# One
## Two
### Three
#### Four