Enhance MD022/blanks-around-headings with lines_above/lines_below parameters (fixes #143).

This commit is contained in:
David Anson 2019-03-24 21:50:56 -07:00
parent debc08bca1
commit fa04d29485
24 changed files with 278 additions and 24 deletions

View file

@ -52,7 +52,7 @@ Aliases: first-heading-h1, first-header-h1
Parameters: level (number; default 1)
> Note: *MD002 has been deprecated and is disabled by default.*
> [MD041](#md041) offers an improved implementation of the rule.
> [MD041/first-line-heading](#md041) offers an improved implementation.
This rule is intended to ensure document headings start at the top level and
is triggered when the first heading in the document isn't an h1 heading:
@ -622,8 +622,10 @@ Tags: headings, headers, blank_lines
Aliases: blanks-around-headings, blanks-around-headers
Parameters: lines_above, lines_below (number; default 1)
This rule is triggered when headings (any style) are either not preceded or not
followed by a blank line:
followed by at least one blank line:
```markdown
# Heading 1
@ -650,6 +652,12 @@ Rationale: Aside from aesthetic reasons, some parsers, including kramdown, will
not parse headings that don't have a blank line before, and will parse them as
regular text.
The `lines_above` and `lines_below` parameters can be used to specify a different
number of blank lines (including 0) above or below each heading.
Note: If `lines_above` or `lines_below` are configured to require more than one
blank line, [MD012/no-multiple-blanks](#md012) should also be customized.
<a name="md023"></a>
## MD023 - Headings must start at the beginning of the line

View file

@ -39,12 +39,12 @@ module.exports = {
nestingStyles[nesting] = itemStyle;
} else {
shared.addErrorDetailIf(onError, item.lineNumber,
nestingStyles[nesting], itemStyle, null,
nestingStyles[nesting], itemStyle, null, null,
shared.rangeFromRegExp(item.line, shared.listItemMarkerRe));
}
} else {
shared.addErrorDetailIf(onError, item.lineNumber,
expectedStyle, itemStyle, null,
expectedStyle, itemStyle, null, null,
shared.rangeFromRegExp(item.line, shared.listItemMarkerRe));
}
});

View file

@ -18,7 +18,7 @@ module.exports = {
const actualIndent = shared.indentFor(item);
if (list.unordered) {
shared.addErrorDetailIf(onError, item.lineNumber,
expectedIndent, actualIndent, null,
expectedIndent, actualIndent, null, null,
shared.rangeFromRegExp(item.line, shared.listItemMarkerRe));
} else {
const match = shared.orderedListItemMarkerRe.exec(item.line);

View file

@ -13,7 +13,7 @@ module.exports = {
shared.flattenLists().forEach(function forList(list) {
if (list.unordered && !list.nesting) {
shared.addErrorDetailIf(onError, list.open.lineNumber,
0, list.indent, null,
0, list.indent, null, null,
shared.rangeFromRegExp(list.open.line, shared.listItemMarkerRe));
}
});

View file

@ -13,7 +13,7 @@ module.exports = {
shared.flattenLists().forEach(function forList(list) {
if (list.unordered && list.parentsUnordered && list.indent) {
shared.addErrorDetailIf(onError, list.open.lineNumber,
list.parentIndent + optionsIndent, list.indent, null,
list.parentIndent + optionsIndent, list.indent, null, null,
shared.rangeFromRegExp(list.open.line, shared.listItemMarkerRe));
}
});

View file

@ -59,7 +59,7 @@ module.exports = {
longLineRe.test(line) &&
!labelRe.test(line)) {
shared.addErrorDetailIf(onError, lineNumber, lineLength,
line.length, null, shared.rangeFromRegExp(line, longLineRe));
line.length, null, null, shared.rangeFromRegExp(line, longLineRe));
}
});
}

View file

@ -3,18 +3,37 @@
"use strict";
const shared = require("./shared");
const { addErrorContext, filterTokens, isBlankLine } = shared;
const { addErrorDetailIf, filterTokens, isBlankLine } = shared;
module.exports = {
"names": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"description": "Headings should be surrounded by blank lines",
"tags": [ "headings", "headers", "blank_lines" ],
"function": function MD022(params, onError) {
let linesAbove = params.config.lines_above;
if (linesAbove === undefined) {
linesAbove = 1;
}
let linesBelow = params.config.lines_below;
if (linesBelow === undefined) {
linesBelow = 1;
}
const { lines } = params;
filterTokens(params, "heading_open", (token) => {
const [ topIndex, nextIndex ] = token.map;
if (!isBlankLine(lines[topIndex - 1]) || !isBlankLine(lines[nextIndex])) {
addErrorContext(onError, topIndex + 1, lines[topIndex].trim());
for (let i = 0; i < linesAbove; i++) {
if (!isBlankLine(lines[topIndex - i - 1])) {
addErrorDetailIf(onError, topIndex + 1, linesAbove, i, "Above",
lines[topIndex].trim());
return;
}
}
for (let i = 0; i < linesBelow; i++) {
if (!isBlankLine(lines[nextIndex + i])) {
addErrorDetailIf(onError, topIndex + 1, linesBelow, i, "Below",
lines[topIndex].trim());
return;
}
}
});
}

View file

@ -29,7 +29,7 @@ module.exports = {
const match = shared.orderedListItemMarkerRe.exec(item.line);
shared.addErrorDetailIf(onError, item.lineNumber,
String(number), !match || match[1],
"Style: " + listStyleExamples[listStyle],
"Style: " + listStyleExamples[listStyle], null,
shared.rangeFromRegExp(item.line, shared.listItemMarkerRe));
if (listStyle === "ordered") {
number++;

View file

@ -22,7 +22,7 @@ module.exports = {
list.items.forEach(function forItem(item) {
const match = /^[\s>]*\S+(\s+)/.exec(item.line);
shared.addErrorDetailIf(onError, item.lineNumber,
expectedSpaces, (match ? match[1].length : 0), null,
expectedSpaces, (match ? match[1].length : 0), null, null,
shared.rangeFromRegExp(item.line, shared.listItemMarkerRe));
});
});

View file

@ -30,7 +30,7 @@ module.exports = {
const lineNumber = token.lineNumber + index + fenceOffset;
const range = [ match.index + 1, wordMatch.length ];
shared.addErrorDetailIf(onError, lineNumber,
name, match[1], null, range);
name, match[1], null, null, range);
}
}
}

View file

@ -328,14 +328,14 @@ module.exports.addError = addError;
// Adds an error object with details conditionally via the onError callback
module.exports.addErrorDetailIf = function addErrorDetailIf(
onError, lineNumber, expected, actual, detail, range) {
onError, lineNumber, expected, actual, detail, context, range) {
if (expected !== actual) {
addError(
onError,
lineNumber,
"Expected: " + expected + "; Actual: " + actual +
(detail ? "; " + detail : ""),
null,
context,
range);
}
};

View file

@ -151,6 +151,20 @@ rules.forEach(function forRule(rule) {
}
};
break;
case "MD022":
scheme.properties = {
"lines_above": {
"description": "Blank lines above heading",
"type": "integer",
"default": 1
},
"lines_below": {
"description": "Blank lines below heading",
"type": "integer",
"default": 1
}
};
break;
case "MD024":
scheme.properties = {
"allow_different_nesting": {

View file

@ -485,18 +485,66 @@
},
"MD022": {
"description": "MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines",
"type": "boolean",
"default": true
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"lines_above": {
"description": "Blank lines above heading",
"type": "integer",
"default": 1
},
"lines_below": {
"description": "Blank lines below heading",
"type": "integer",
"default": 1
}
},
"additionalProperties": false
},
"blanks-around-headings": {
"description": "MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines",
"type": "boolean",
"default": true
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"lines_above": {
"description": "Blank lines above heading",
"type": "integer",
"default": 1
},
"lines_below": {
"description": "Blank lines below heading",
"type": "integer",
"default": 1
}
},
"additionalProperties": false
},
"blanks-around-headers": {
"description": "MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines",
"type": "boolean",
"default": true
"type": [
"boolean",
"object"
],
"default": true,
"properties": {
"lines_above": {
"description": "Blank lines above heading",
"type": "integer",
"default": 1
},
"lines_below": {
"description": "Blank lines below heading",
"type": "integer",
"default": 1
}
},
"additionalProperties": false
},
"MD023": {
"description": "MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line",

View file

@ -4,7 +4,7 @@
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": null,
"errorDetail": "Expected: 1; Actual: 0; Below",
"errorContext": "# Heading",
"errorRange": null
},

View file

@ -0,0 +1,9 @@
{
"default": true,
"MD003": false,
"MD012": false,
"MD022": {
"lines_above": 0,
"lines_below": 2
}
}

View file

@ -0,0 +1,24 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
---
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,20 @@
[
{
"lineNumber": 8,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 2; Actual: 1; Below",
"errorContext": "## Banana",
"errorRange": null
},
{
"lineNumber": 21,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 2; Actual: 0; Below",
"errorContext": "Elderberry",
"errorRange": null
}
]

View file

@ -0,0 +1,9 @@
{
"default": true,
"MD003": false,
"MD012": false,
"MD022": {
"lines_above": 3,
"lines_below": 0
}
}

View file

@ -0,0 +1,28 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,20 @@
[
{
"lineNumber": 19,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 3; Actual: 2; Above",
"errorContext": "## Durian ##",
"errorRange": null
},
{
"lineNumber": 22,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 3; Actual: 1; Above",
"errorContext": "Elderberry",
"errorRange": null
}
]

View file

@ -0,0 +1,4 @@
{
"default": true,
"MD003": false
}

View file

@ -0,0 +1,22 @@
# Blanks Around Headings
## Apple
Text
## Banana
Text
## Cherry
Text
## Durian ##
Text
---
Elderberry
----------
Text
## Fig

View file

@ -0,0 +1,29 @@
[
{
"lineNumber": 7,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 1; Actual: 0; Below",
"errorContext": "## Banana",
"errorRange": null
},
{
"lineNumber": 13,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 1; Actual: 0; Above",
"errorContext": "## Durian ##",
"errorRange": null
},
{
"lineNumber": 18,
"ruleNames": [ "MD022", "blanks-around-headings", "blanks-around-headers" ],
"ruleDescription": "Headings should be surrounded by blank lines",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md022",
"errorDetail": "Expected: 1; Actual: 0; Above",
"errorContext": "Elderberry",
"errorRange": null
}
]

View file

@ -1152,7 +1152,7 @@ module.exports.readme = function readme(test) {
};
module.exports.doc = function doc(test) {
test.expect(311);
test.expect(312);
fs.readFile("doc/Rules.md", shared.utf8Encoding,
function readFile(err, contents) {
test.ifError(err);