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 * **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 * **MD041** *first-line-h1* - First line in file should be a top level header
* **MD042** *no-empty-links* - No empty links * **MD042** *no-empty-links* - No empty links
* **MD043** *required-headers* - Required header structure
See [Rules.md](doc/Rules.md) for more details. 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 * **emphasis** - MD036, MD037
* **hard_tab** - MD010 * **hard_tab** - MD010
* **headers** - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, * **headers** - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023,
MD024, MD025, MD026, MD036, MD041 MD024, MD025, MD026, MD036, MD041, MD043
* **hr** - MD035 * **hr** - MD035
* **html** - MD033 * **html** - MD033
* **indentation** - MD005, MD006, MD007, MD027 * **indentation** - MD005, MD006, MD007, MD027

View file

@ -222,7 +222,7 @@ Tags: whitespace
Aliases: no-trailing-spaces 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, 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. 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 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. '`-`', This rule checks for the number of spaces between a list marker (e.g. '`-`',
'`*`', '`+`' or '`1.`') and the text of the list item. '`*`', '`+`' 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: But non-empty fragments will not:
[a valid fragment](#fragment) [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", "desc": "No empty links",
"tags": [ "links" ], "tags": [ "links" ],
"aliases": [ "no-empty-links" ], "aliases": [ "no-empty-links" ],
"func": function MD034(params, errors) { "func": function MD042(params, errors) {
forEachInlineChild(params, "link_open", function forToken(token) { forEachInlineChild(params, "link_open", function forToken(token) {
token.attrs.forEach(function forAttr(attr) { token.attrs.forEach(function forAttr(attr) {
if (attr[0] === "href" && (!attr[1] || (attr[1] === "#"))) { 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, "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 2 {MD001}
# Header 3 {MD003} # # Header 3 {MD003} {MD043} #
* list * list
+ list {MD004} {MD006} {MD007} {MD030} + 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) { module.exports.filesArrayNotModified = function filesArrayNotModified(test) {
test.expect(2); test.expect(2);
var files = [ var files = [
@ -733,7 +771,7 @@ module.exports.missingStringValue = function missingStringValue(test) {
}; };
module.exports.ruleNamesUpperCase = function ruleNamesUpperCase(test) { module.exports.ruleNamesUpperCase = function ruleNamesUpperCase(test) {
test.expect(38); test.expect(39);
rules.forEach(function forRule(rule) { rules.forEach(function forRule(rule) {
test.equal(rule.name, rule.name.toUpperCase(), "Rule name not upper-case."); 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) { module.exports.uniqueAliases = function uniqueAliases(test) {
test.expect(76); test.expect(78);
var tags = []; var tags = [];
rules.forEach(function forRule(rule) { rules.forEach(function forRule(rule) {
Array.prototype.push.apply(tags, rule.tags); Array.prototype.push.apply(tags, rule.tags);
@ -758,7 +796,7 @@ module.exports.uniqueAliases = function uniqueAliases(test) {
}; };
module.exports.readme = function readme(test) { module.exports.readme = function readme(test) {
test.expect(99); test.expect(101);
var tagToRules = {}; var tagToRules = {};
rules.forEach(function forRule(rule) { rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) { rule.tags.forEach(function forTag(tag) {
@ -819,7 +857,7 @@ module.exports.readme = function readme(test) {
}; };
module.exports.doc = function doc(test) { module.exports.doc = function doc(test) {
test.expect(281); test.expect(289);
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);

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