diff --git a/helpers/helpers.js b/helpers/helpers.js index d5f0c0b9..1b697427 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -19,7 +19,6 @@ const inlineCommentRe = module.exports.inlineCommentRe = inlineCommentRe; // Regular expressions for range matching -module.exports.atxHeadingSpaceRe = /^#+\s*\S/; module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/i; module.exports.listItemMarkerRe = /^[\s>]*(?:[*+-]|\d+[.)])\s+/; module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/; diff --git a/lib/md018.js b/lib/md018.js index 1784260a..5aec9053 100644 --- a/lib/md018.js +++ b/lib/md018.js @@ -2,8 +2,7 @@ "use strict"; -const { addErrorContext, atxHeadingSpaceRe, forEachLine, - rangeFromRegExp } = require("../helpers"); +const { addErrorContext, forEachLine } = require("../helpers"); const { lineMetadata } = require("./cache"); module.exports = { @@ -12,9 +11,20 @@ module.exports = { "tags": [ "headings", "headers", "atx", "spaces" ], "function": function MD018(params, onError) { forEachLine(lineMetadata(), (line, lineIndex, inCode) => { - if (!inCode && /^#+[^#\s]/.test(line) && !/#$/.test(line)) { - addErrorContext(onError, lineIndex + 1, line.trim(), null, - null, rangeFromRegExp(line, atxHeadingSpaceRe)); + if (!inCode && /^#+[^#\s]/.test(line) && !/#\s*$/.test(line)) { + const hashCount = /^#+/.exec(line)[0].length; + addErrorContext( + onError, + lineIndex + 1, + line.trim(), + null, + null, + [ 1, hashCount + 1 ], + { + "editColumn": hashCount + 1, + "insertText": " " + } + ); } }); } diff --git a/lib/md019.js b/lib/md019.js index e42353b8..1505c6b3 100644 --- a/lib/md019.js +++ b/lib/md019.js @@ -2,20 +2,37 @@ "use strict"; -const { addErrorContext, atxHeadingSpaceRe, filterTokens, headingStyleFor, - rangeFromRegExp } = require("../helpers"); +const { addErrorContext, filterTokens, headingStyleFor } = + require("../helpers"); module.exports = { "names": [ "MD019", "no-multiple-space-atx" ], "description": "Multiple spaces after hash on atx style heading", "tags": [ "headings", "headers", "atx", "spaces" ], "function": function MD019(params, onError) { - filterTokens(params, "heading_open", function forToken(token) { - if ((headingStyleFor(token) === "atx") && - /^#+\s\s/.test(token.line)) { - addErrorContext(onError, token.lineNumber, token.line.trim(), - null, null, - rangeFromRegExp(token.line, atxHeadingSpaceRe)); + filterTokens(params, "heading_open", (token) => { + if (headingStyleFor(token) === "atx") { + const { line, lineNumber } = token; + const match = /^(#+)(\s{2,})/.exec(line); + if (match) { + const [ + , + { "length": hashLength }, + { "length": spacesLength } + ] = match; + addErrorContext( + onError, + lineNumber, + line.trim(), + null, + null, + [ 1, hashLength + spacesLength + 1 ], + { + "editColumn": hashLength + 1, + "deleteCount": spacesLength - 1 + } + ); + } } }); } diff --git a/lib/md020.js b/lib/md020.js index 8b881ae0..03d6661f 100644 --- a/lib/md020.js +++ b/lib/md020.js @@ -2,23 +2,59 @@ "use strict"; -const { addErrorContext, forEachLine, rangeFromRegExp } = require("../helpers"); +const { addErrorContext, forEachLine } = require("../helpers"); const { lineMetadata } = require("./cache"); -const atxClosedHeadingNoSpaceRe = /(?:^#+[^#\s])|(?:[^#\s]#+\s*$)/; - module.exports = { "names": [ "MD020", "no-missing-space-closed-atx" ], "description": "No space inside hashes on closed atx style heading", "tags": [ "headings", "headers", "atx_closed", "spaces" ], "function": function MD020(params, onError) { forEachLine(lineMetadata(), (line, lineIndex, inCode) => { - if (!inCode && /^#+[^#]*[^\\]#+$/.test(line)) { - const left = /^#+[^#\s]/.test(line); - const right = /[^#\s]#+$/.test(line); - if (left || right) { - addErrorContext(onError, lineIndex + 1, line.trim(), left, - right, rangeFromRegExp(line, atxClosedHeadingNoSpaceRe)); + if (!inCode) { + const match = /^(#+)(\s*)([^#]+?)(\s*)(\\?)(#+)(\s*)$/.exec(line); + if (match) { + const [ + , + leftHash, + { "length": leftSpaceLength }, + content, + { "length": rightSpaceLength }, + rightEscape, + rightHash, + { "length": trailSpaceLength } + ] = match; + const leftHashLength = leftHash.length; + const rightHashLength = rightHash.length; + const left = !leftSpaceLength; + const right = + (!rightSpaceLength && (!rightEscape || (rightHashLength > 1))) || + (rightEscape && (rightHashLength > 1)); + if (left || right) { + const range = left ? + [ + 1, + leftHashLength + 1 + ] : + [ + line.length - trailSpaceLength - rightHashLength, + rightHashLength + 1 + ]; + addErrorContext( + onError, + lineIndex + 1, + line.trim(), + left, + right, + range, + { + "editColumn": 1, + "deleteLength": line.length, + "insertText": + `${leftHash} ${content} ${rightEscape}${rightHash}` + } + ); + } } } }); diff --git a/lib/md021.js b/lib/md021.js index d012f7be..fcb30a12 100644 --- a/lib/md021.js +++ b/lib/md021.js @@ -2,24 +2,57 @@ "use strict"; -const { addErrorContext, filterTokens, headingStyleFor, rangeFromRegExp } = +const { addErrorContext, filterTokens, headingStyleFor } = require("../helpers"); -const atxClosedHeadingSpaceRe = /(?:^#+\s\s+?\S)|(?:\S\s\s+?#+\s*$)/; - module.exports = { "names": [ "MD021", "no-multiple-space-closed-atx" ], "description": "Multiple spaces inside hashes on closed atx style heading", "tags": [ "headings", "headers", "atx_closed", "spaces" ], "function": function MD021(params, onError) { - filterTokens(params, "heading_open", function forToken(token) { + filterTokens(params, "heading_open", (token) => { if (headingStyleFor(token) === "atx_closed") { - const left = /^#+\s\s/.test(token.line); - const right = /\s\s#+$/.test(token.line); - if (left || right) { - addErrorContext(onError, token.lineNumber, token.line.trim(), - left, right, - rangeFromRegExp(token.line, atxClosedHeadingSpaceRe)); + const { line, lineNumber } = token; + const match = /^(#+)(\s+)([^#]+?)(\s+)(#+)(\s*)$/.exec(line); + if (match) { + const [ + , + leftHash, + { "length": leftSpaceLength }, + content, + { "length": rightSpaceLength }, + rightHash, + { "length": trailSpaceLength } + ] = match; + const left = leftSpaceLength > 1; + const right = rightSpaceLength > 1; + if (left || right) { + const length = line.length; + const leftHashLength = leftHash.length; + const rightHashLength = rightHash.length; + const range = left ? + [ + 1, + leftHashLength + leftSpaceLength + 1 + ] : + [ + length - trailSpaceLength - rightHashLength - rightSpaceLength, + rightSpaceLength + rightHashLength + 1 + ]; + addErrorContext( + onError, + lineNumber, + line.trim(), + left, + right, + range, + { + "editColumn": 1, + "deleteLength": length, + "insertText": `${leftHash} ${content} ${rightHash}` + } + ); + } } } }); diff --git a/test/atx-heading-spacing-trailing-spaces.md b/test/atx-heading-spacing-trailing-spaces.md new file mode 100644 index 00000000..3201e6dc --- /dev/null +++ b/test/atx-heading-spacing-trailing-spaces.md @@ -0,0 +1,19 @@ +# atx-heading-spacing-trailing-spaces + + + +##Heading 1 {MD018} + +## Heading 2 {MD019} + +##Heading 3 {MD020} ## + +## Heading 4 {MD020}## + +##Heading 5 {MD020}## + +## Heading 5 {MD021} ## + +## Heading 6 {MD021} ## + +## Heading 7 {MD021} ## diff --git a/test/break-all-the-rules.md b/test/break-all-the-rules.md index 378aea0f..9c0c5be3 100644 --- a/test/break-all-the-rules.md +++ b/test/break-all-the-rules.md @@ -27,17 +27,17 @@ long line long line long line long line long line long line long line long line # Heading 5 {MD019} #Heading 6 {MD020} # - # Heading 7 {MD021} {MD022} {MD023} {MD003} # + +# Heading 7 {MD021} {MD003} # # Heading 8 # Heading 8 -{MD024:34} - +{MD024:35} Note: Can not break MD025 and MD002 in the same file -# Heading 9 {MD026}. + # Heading 9 {MD023} {MD026}. > {MD027} @@ -78,4 +78,7 @@ code fence without language {MD040:73} {MD046:73} markdownLint {MD044} -![](image.jpg) {MD045} {MD047} \ No newline at end of file +![](image.jpg) {MD045} +## Heading 10 {MD022} + +EOF {MD047} \ No newline at end of file diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 437e5a6a..7957ec4e 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1038,7 +1038,7 @@ module.exports.styleAll = function styleAll(test) { const expectedResult = { "./test/break-all-the-rules.md": { "MD001": [ 3 ], - "MD003": [ 5, 30 ], + "MD003": [ 5, 31 ], "MD004": [ 8 ], "MD005": [ 12 ], "MD006": [ 8 ], @@ -1052,10 +1052,10 @@ module.exports.styleAll = function styleAll(test) { "MD018": [ 25 ], "MD019": [ 27 ], "MD020": [ 29 ], - "MD021": [ 30 ], - "MD022": [ 30 ], - "MD023": [ 30 ], - "MD024": [ 34 ], + "MD021": [ 31 ], + "MD022": [ 82 ], + "MD023": [ 40 ], + "MD024": [ 35 ], "MD026": [ 40 ], "MD027": [ 42 ], "MD028": [ 43 ], @@ -1075,7 +1075,7 @@ module.exports.styleAll = function styleAll(test) { "MD042": [ 77 ], "MD045": [ 81 ], "MD046": [ 49, 73 ], - "MD047": [ 81 ] + "MD047": [ 84 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -1095,7 +1095,7 @@ module.exports.styleRelaxed = function styleRelaxed(test) { const expectedResult = { "./test/break-all-the-rules.md": { "MD001": [ 3 ], - "MD003": [ 5, 30 ], + "MD003": [ 5, 31 ], "MD004": [ 8 ], "MD005": [ 12 ], "MD011": [ 16 ], @@ -1103,10 +1103,10 @@ module.exports.styleRelaxed = function styleRelaxed(test) { "MD018": [ 25 ], "MD019": [ 27 ], "MD020": [ 29 ], - "MD021": [ 30 ], - "MD022": [ 30 ], - "MD023": [ 30 ], - "MD024": [ 34 ], + "MD021": [ 31 ], + "MD022": [ 82 ], + "MD023": [ 40 ], + "MD024": [ 35 ], "MD026": [ 40 ], "MD029": [ 47 ], "MD031": [ 50 ], @@ -1116,7 +1116,7 @@ module.exports.styleRelaxed = function styleRelaxed(test) { "MD042": [ 77 ], "MD045": [ 81 ], "MD046": [ 49, 73 ], - "MD047": [ 81 ] + "MD047": [ 84 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues.");