From cdd87e647f6399a62bb49790cc4beca5755fd1ed Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 16 Aug 2019 19:56:52 -0700 Subject: [PATCH 01/29] Add infrastructure for rules to include fix information when logging violations, update MD047 (refs #80). --- README.md | 10 +- doc/CustomRules.md | 4 + helpers/helpers.js | 33 +++++-- lib/markdownlint.js | 6 +- lib/md047.js | 12 ++- test/markdownlint-test.js | 202 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 248 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6b6d718f..72c2745c 100644 --- a/README.md +++ b/README.md @@ -416,15 +416,19 @@ Specifies which version of the `result` object to return (see the "Usage" sectio below for examples). Passing a `resultVersion` of `0` corresponds to the original, simple format where -each error is identified by rule name and line number. This is deprecated. +each error is identified by rule name and line number. *This is deprecated.* Passing a `resultVersion` of `1` corresponds to a detailed format where each error includes information about the line number, rule name, alias, description, as well -as any additional detail or context that is available. This is deprecated. +as any additional detail or context that is available. *This is deprecated.* Passing a `resultVersion` of `2` corresponds to a detailed format where each error includes information about the line number, rule names, description, as well as any -additional detail or context that is available. This is the default. +additional detail or context that is available. *This is the default.* + +Passing a `resultVersion` of `3` corresponds to the detailed version `2` format +with additional information about fixes for certain errors. All errors for each +line are included (other versions collapse multiple errors for the same rule). ##### options.markdownItPlugins diff --git a/doc/CustomRules.md b/doc/CustomRules.md index 0c7458c0..44671ed4 100644 --- a/doc/CustomRules.md +++ b/doc/CustomRules.md @@ -52,6 +52,10 @@ A rule is implemented as an `Object` with four required properties: - `details` is an optional `String` with information about what caused the error. - `context` is an optional `String` with relevant text surrounding the error location. - `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error. + - `fixInfo` is an optional `Object` with information about how to fix the error: + - `editColumn` is an optional `Number` specifying the 1-based column number of the edit. + - `deleteCount` is an optional `Number` specifying the count of characters to delete with the edit. + - `insertText` is an optional `String` specifying text to insert as part of the edit. The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers). diff --git a/helpers/helpers.js b/helpers/helpers.js index 71643c77..34688053 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -4,7 +4,8 @@ // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js -module.exports.newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/; +const newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/; +module.exports.newLineRe = newLineRe; // Regular expression for matching common front matter (YAML and TOML) module.exports.frontMatterRe = @@ -331,12 +332,13 @@ module.exports.forEachInlineCodeSpan = }; // Adds a generic error object via the onError callback -function addError(onError, lineNumber, detail, context, range) { +function addError(onError, lineNumber, detail, context, range, fixInfo) { onError({ - "lineNumber": lineNumber, - "detail": detail, - "context": context, - "range": range + lineNumber, + detail, + context, + range, + fixInfo }); } module.exports.addError = addError; @@ -396,3 +398,22 @@ module.exports.frontMatterHasTitle = return !ignoreFrontMatter && frontMatterLines.some((line) => frontMatterTitleRe.test(line)); }; + +// Applies as many fixes as possible to the input +module.exports.fixErrors = function fixErrors(input, errors) { + const lines = input.split(newLineRe); + errors.filter((error) => !!error.fixInfo).forEach((error) => { + const { lineNumber, fixInfo } = error; + const editColumn = fixInfo.editColumn || 1; + const deleteCount = fixInfo.deleteCount || 0; + const insertText = fixInfo.insertText || ""; + const lineIndex = lineNumber - 1; + const editIndex = editColumn - 1; + const line = lines[lineIndex]; + lines[lineIndex] = + line.slice(0, editIndex) + + insertText + + line.slice(editIndex + deleteCount); + }); + return lines.join("\n"); +}; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 2b80a19a..28647194 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -381,7 +381,8 @@ function lintContent( "lineNumber": errorInfo.lineNumber + frontMatterLines.length, "detail": errorInfo.detail || null, "context": errorInfo.context || null, - "range": errorInfo.range || null + "range": errorInfo.range || null, + "fixInfo": errorInfo.fixInfo || null }); } // Call (possibly external) rule function @@ -423,6 +424,9 @@ function lintContent( errorObject.errorDetail = error.detail; errorObject.errorContext = error.context; errorObject.errorRange = error.range; + if (resultVersion === 3) { + errorObject.fixInfo = error.fixInfo; + } return errorObject; }); if (filteredErrors.length) { diff --git a/lib/md047.js b/lib/md047.js index 5d44b3a7..a1f41525 100644 --- a/lib/md047.js +++ b/lib/md047.js @@ -12,7 +12,17 @@ module.exports = { const lastLineNumber = params.lines.length; const lastLine = params.lines[lastLineNumber - 1]; if (!isBlankLine(lastLine)) { - addError(onError, lastLineNumber); + addError( + onError, + lastLineNumber, + null, + null, + null, + { + "insertText": "\n", + "editColumn": lastLine.length + 1 + } + ); } } }; diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 30374fc1..bac9f388 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -34,10 +34,11 @@ function promisify(func, ...args) { function createTestForFile(file) { return function testForFile(test) { - test.expect(1); + test.expect(2); const detailedResults = /[/\\]detailed-results-/.test(file); const resultsFile = file.replace(/\.md$/, ".results.json"); const configFile = file.replace(/\.md$/, ".json"); + let mergedConfig = null; const actualPromise = promisify(fs.stat, configFile) .then( function configFileExists() { @@ -49,16 +50,29 @@ function createTestForFile(file) { }) .then( function lintWithConfig(config) { - const mergedConfig = { + mergedConfig = { ...defaultConfig, ...config }; return promisify(markdownlint, { "files": [ file ], "config": mergedConfig, - "resultVersion": detailedResults ? 2 : 0 + "resultVersion": detailedResults ? 2 : 3 }); - }); + }) + .then( + function convertResultVersion2To0(resultVersion2) { + const result0 = {}; + const result2or3 = resultVersion2[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + lineNumbers.push(result.lineNumber); + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); const expectedPromise = detailedResults ? promisify(fs.readFile, resultsFile, helpers.utf8Encoding) .then( @@ -96,11 +110,35 @@ function createTestForFile(file) { Promise.all([ actualPromise, expectedPromise ]) .then( function compareResults(fulfillments) { - const actual = fulfillments[0]; - const results = fulfillments[1]; - const expected = {}; - expected[file] = results; + const [ [ actual0, actual2or3 ], expected ] = fulfillments; + const actual = detailedResults ? actual2or3 : actual0; test.deepEqual(actual, expected, "Line numbers are not correct."); + return actual2or3; + }) + .then( + function verifyFixErrors(errors) { + if (detailedResults) { + return test.ok(true); + } + return promisify(fs.readFile, file, helpers.utf8Encoding) + .then( + function applyFixErrors(content) { + const corrections = helpers.fixErrors(content, errors); + return promisify(markdownlint, { + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixErrors(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); }) .catch() .then(test.done); @@ -448,6 +486,43 @@ module.exports.resultFormattingV2 = function resultFormattingV2(test) { }); }; +module.exports.resultFormattingV3 = function resultFormattingV3(test) { + test.expect(3); + const options = { + "strings": { + "input": "# Heading" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { + "lineNumber": 1, + "ruleNames": [ "MD047", "single-trailing-newline" ], + "ruleDescription": "Files should end with a single newline character", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, + "errorDetail": null, + "errorContext": null, + "errorRange": null, + "fixInfo": { + "insertText": "\n", + "editColumn": 10 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "input: 1: MD047/single-trailing-newline" + + " Files should end with a single newline character"; + test.equal(actualMessage, expectedMessage, "Incorrect message."); + test.done(); + }); +}; + module.exports.stringInputLineEndings = function stringInputLineEndings(test) { test.expect(2); const options = { @@ -1638,6 +1713,117 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { test.done(); }; +module.exports.fixErrors = function fixErrors(test) { + test.expect(8); + const testCases = [ + [ + "Hello world.", + [], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": {} + } + ], + "Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "insertText": "Very " + } + } + ], + "Very Hello world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "insertText": "big " + } + } + ], + "Hello big world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": 6 + } + } + ], + "world." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 7, + "deleteCount": 5, + "insertText": "there" + } + } + ], + "Hello there." + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 12, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 6, + "deleteCount": 1 + } + } + ], + "Helloworld" + ], + [ + "Hello world.", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 13, + "insertText": " Hi." + } + } + ], + "Hello world. Hi." + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.fixErrors(input, errors); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.done(); +}; + module.exports.configSingle = function configSingle(test) { test.expect(2); markdownlint.readConfig("./test/config/config-child.json", From 679c83e23b8e32a5bb4a375361ce5d35c3752b31 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 21 Aug 2019 21:02:09 -0700 Subject: [PATCH 02/29] Update resultVersion 3 to report all issues on a line; update MD010/no-hard-tabs to log all issues. --- lib/markdownlint.js | 9 ++- lib/md010.js | 18 ++++-- test/markdownlint-test.js | 120 +++++++++++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 28647194..33ae82f7 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -296,6 +296,11 @@ function lineNumberComparison(a, b) { return a.lineNumber - b.lineNumber; } +// Function to return true for all inputs +function filterAllValues() { + return true; +} + // Function to return unique values from a sorted errors array function uniqueFilterForSortedErrors(value, index, array) { return (index === 0) || (value.lineNumber > array[index - 1].lineNumber); @@ -402,7 +407,9 @@ function lintContent( if (errors.length) { errors.sort(lineNumberComparison); const filteredErrors = errors - .filter(uniqueFilterForSortedErrors) + .filter((resultVersion === 3) ? + filterAllValues : + uniqueFilterForSortedErrors) .filter(function removeDisabledRules(error) { return enabledRulesPerLineNumber[error.lineNumber][ruleName]; }) diff --git a/lib/md010.js b/lib/md010.js index 55fd1ce8..e1bf1a6e 100644 --- a/lib/md010.js +++ b/lib/md010.js @@ -2,10 +2,10 @@ "use strict"; -const { addError, forEachLine, rangeFromRegExp } = require("../helpers"); +const { addError, forEachLine } = require("../helpers"); const { lineMetadata } = require("./cache"); -const tabRe = /\t+/; +const tabRe = /\t+/g; module.exports = { "names": [ "MD010", "no-hard-tabs" ], @@ -15,9 +15,17 @@ module.exports = { const codeBlocks = params.config.code_blocks; const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks; forEachLine(lineMetadata(), (line, lineIndex, inCode) => { - if (tabRe.test(line) && (!inCode || includeCodeBlocks)) { - addError(onError, lineIndex + 1, "Column: " + (line.indexOf("\t") + 1), - null, rangeFromRegExp(line, tabRe)); + if (!inCode || includeCodeBlocks) { + let match = null; + while ((match = tabRe.exec(line)) !== null) { + const column = match.index + 1; + addError( + onError, + lineIndex + 1, + "Column: " + column, + null, + [ column, match[0].length ]); + } } }); } diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index bac9f388..b19c5d72 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -67,7 +67,9 @@ function createTestForFile(file) { result2or3.forEach(function forResult(result) { const ruleName = result.ruleNames[0]; const lineNumbers = result0[ruleName] || []; - lineNumbers.push(result.lineNumber); + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } result0[ruleName] = lineNumbers; }); return [ result0, result2or3 ]; @@ -523,6 +525,122 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { }); }; +module.exports.onePerLineResultVersion0 = + function onePerLineResultVersion0(test) { + test.expect(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 0 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": { + "MD010": [ 1 ] + } + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); + }; + +module.exports.onePerLineResultVersion1 = + function onePerLineResultVersion1(test) { + test.expect(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 1 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { "lineNumber": 1, + "ruleName": "MD010", + "ruleAlias": "no-hard-tabs", + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); + }; + +module.exports.onePerLineResultVersion2 = + function onePerLineResultVersion2(test) { + test.expect(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 2 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ] } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); + }; + +module.exports.manyPerLineResultVersion3 = + function manyPerLineResultVersion3(test) { + test.expect(2); + const options = { + "strings": { + "input": "# Heading\theading\t\theading\n" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "input": [ + { "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 1 ], + "fixInfo": null }, + { "lineNumber": 1, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": + `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 18", + "errorContext": null, + "errorRange": [ 18, 2 ], + "fixInfo": null } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); + }; + module.exports.stringInputLineEndings = function stringInputLineEndings(test) { test.expect(2); const options = { From 2cd27c58f2df6f215aa935ba33261d288cb950cb Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 24 Aug 2019 22:55:51 -0700 Subject: [PATCH 03/29] Update MD009/MD010/MD012/MD028 to report fixInfo for violations. --- helpers/helpers.js | 15 +++-- lib/md009.js | 24 ++++--- lib/md010.js | 8 ++- lib/md012.js | 12 +++- lib/md028.js | 19 +++++- test/blockquote_blank_lines.md | 2 +- test/ignore-comments.md | 2 + test/markdownlint-test.js | 114 +++++++++++++++++++++++++++++---- 8 files changed, 165 insertions(+), 31 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 34688053..58f5313a 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -345,7 +345,7 @@ 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, context, range) { + onError, lineNumber, expected, actual, detail, context, range, fixInfo) { if (expected !== actual) { addError( onError, @@ -353,7 +353,8 @@ module.exports.addErrorDetailIf = function addErrorDetailIf( "Expected: " + expected + "; Actual: " + actual + (detail ? "; " + detail : ""), context, - range); + range, + fixInfo); } }; @@ -411,9 +412,11 @@ module.exports.fixErrors = function fixErrors(input, errors) { const editIndex = editColumn - 1; const line = lines[lineIndex]; lines[lineIndex] = - line.slice(0, editIndex) + - insertText + - line.slice(editIndex + deleteCount); + (deleteCount === -1) ? + null : + line.slice(0, editIndex) + + insertText + + line.slice(editIndex + deleteCount); }); - return lines.join("\n"); + return lines.filter((line) => line !== null).join("\n"); }; diff --git a/lib/md009.js b/lib/md009.js index 0fef5a88..a3e858ed 100644 --- a/lib/md009.js +++ b/lib/md009.js @@ -2,12 +2,10 @@ "use strict"; -const { addError, filterTokens, forEachLine, includesSorted, rangeFromRegExp } = +const { addError, filterTokens, forEachLine, includesSorted } = require("../helpers"); const { lineMetadata } = require("./cache"); -const trailingSpaceRe = /\s+$/; - module.exports = { "names": [ "MD009", "no-trailing-spaces" ], "description": "Trailing spaces", @@ -34,14 +32,22 @@ module.exports = { forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => { inFencedCode += onFence; const lineNumber = lineIndex + 1; - if ((!inCode || inFencedCode) && trailingSpaceRe.test(line) && + const trailingSpaces = line.length - line.trimRight().length; + if ((!inCode || inFencedCode) && trailingSpaces && !includesSorted(listItemLineNumbers, lineNumber)) { - const actual = line.length - line.trimRight().length; - if (expected !== actual) { - addError(onError, lineNumber, + if (expected !== trailingSpaces) { + const column = line.length - trailingSpaces + 1; + addError( + onError, + lineNumber, "Expected: " + (expected === 0 ? "" : "0 or ") + - expected + "; Actual: " + actual, - null, rangeFromRegExp(line, trailingSpaceRe)); + expected + "; Actual: " + trailingSpaces, + null, + [ column, trailingSpaces ], + { + "editColumn": column, + "deleteCount": trailingSpaces + }); } } }); diff --git a/lib/md010.js b/lib/md010.js index e1bf1a6e..5ebe08f8 100644 --- a/lib/md010.js +++ b/lib/md010.js @@ -19,12 +19,18 @@ module.exports = { let match = null; while ((match = tabRe.exec(line)) !== null) { const column = match.index + 1; + const length = match[0].length; addError( onError, lineIndex + 1, "Column: " + column, null, - [ column, match[0].length ]); + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": "".padEnd(length) + }); } } }); diff --git a/lib/md012.js b/lib/md012.js index fca04750..3eb6052b 100644 --- a/lib/md012.js +++ b/lib/md012.js @@ -15,7 +15,17 @@ module.exports = { forEachLine(lineMetadata(), (line, lineIndex, inCode) => { count = (inCode || line.trim().length) ? 0 : count + 1; if (maximum < count) { - addErrorDetailIf(onError, lineIndex + 1, maximum, count); + addErrorDetailIf( + onError, + lineIndex + 1, + maximum, + count, + null, + null, + null, + { + "deleteCount": -1 + }); } }); } diff --git a/lib/md028.js b/lib/md028.js index c6d3a372..0ae7cdd3 100644 --- a/lib/md028.js +++ b/lib/md028.js @@ -10,12 +10,29 @@ module.exports = { "tags": [ "blockquote", "whitespace" ], "function": function MD028(params, onError) { let prevToken = {}; + let prevLineNumber = null; params.tokens.forEach(function forToken(token) { if ((token.type === "blockquote_open") && (prevToken.type === "blockquote_close")) { - addError(onError, token.lineNumber - 1); + for ( + let lineNumber = prevLineNumber; + lineNumber < token.lineNumber; + lineNumber++) { + addError( + onError, + lineNumber, + null, + null, + null, + { + "deleteCount": -1 + }); + } } prevToken = token; + if (token.type === "blockquote_open") { + prevLineNumber = token.map[1] + 1; + } }); } }; diff --git a/test/blockquote_blank_lines.md b/test/blockquote_blank_lines.md index e163a761..d21d4383 100644 --- a/test/blockquote_blank_lines.md +++ b/test/blockquote_blank_lines.md @@ -26,6 +26,6 @@ Some text Expected errors: -{MD028:5} {MD028:8} {MD028:10} {MD028:17} +{MD028:5} {MD028:7} {MD028:8} {MD028:10} {MD028:17} {MD009:10} (trailing space is intentional) {MD012:8} (multiple blank lines are intentional) diff --git a/test/ignore-comments.md b/test/ignore-comments.md index 8c3d14f0..b155fb61 100644 --- a/test/ignore-comments.md +++ b/test/ignore-comments.md @@ -2,6 +2,8 @@ Hard tab {MD010} +Hard tabs hard tabs {MD010} + diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index b19c5d72..9aa77b36 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -492,7 +492,10 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { test.expect(3); const options = { "strings": { - "input": "# Heading" + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext" }, "resultVersion": 3 }; @@ -502,6 +505,47 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { "input": [ { "lineNumber": 1, + "ruleNames": [ "MD009", "no-trailing-spaces" ], + "ruleDescription": "Trailing spaces", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md009`, + "errorDetail": "Expected: 0 or 2; Actual: 3", + "errorContext": null, + "errorRange": [ 10, 3 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 3 + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 5", + "errorContext": null, + "errorRange": [ 5, 1 ], + "fixInfo": { + "editColumn": 5, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 3, + "ruleNames": [ "MD010", "no-hard-tabs" ], + "ruleDescription": "Hard tabs", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, + "errorDetail": "Column: 10", + "errorContext": null, + "errorRange": [ 10, 2 ], + "fixInfo": { + "editColumn": 10, + "deleteCount": 2, + "insertText": " " + } + }, + { + "lineNumber": 3, "ruleNames": [ "MD047", "single-trailing-newline" ], "ruleDescription": "Files should end with a single newline character", "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, @@ -510,7 +554,7 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { "errorRange": null, "fixInfo": { "insertText": "\n", - "editColumn": 10 + "editColumn": 16 } } ] @@ -518,7 +562,13 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { test.deepEqual(actualResult, expectedResult, "Undetected issues."); const actualMessage = actualResult.toString(); const expectedMessage = - "input: 1: MD047/single-trailing-newline" + + "input: 1: MD009/no-trailing-spaces" + + " Trailing spaces [Expected: 0 or 2; Actual: 3]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 5]\n" + + "input: 3: MD010/no-hard-tabs" + + " Hard tabs [Column: 10]\n" + + "input: 3: MD047/single-trailing-newline" + " Files should end with a single newline character"; test.equal(actualMessage, expectedMessage, "Incorrect message."); test.done(); @@ -559,7 +609,8 @@ module.exports.onePerLineResultVersion1 = test.ifError(err); const expectedResult = { "input": [ - { "lineNumber": 1, + { + "lineNumber": 1, "ruleName": "MD010", "ruleAlias": "no-hard-tabs", "ruleDescription": "Hard tabs", @@ -567,7 +618,8 @@ module.exports.onePerLineResultVersion1 = `${homepage}/blob/v${version}/doc/Rules.md#md010`, "errorDetail": "Column: 10", "errorContext": null, - "errorRange": [ 10, 1 ] } + "errorRange": [ 10, 1 ] + } ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -588,14 +640,16 @@ module.exports.onePerLineResultVersion2 = test.ifError(err); const expectedResult = { "input": [ - { "lineNumber": 1, + { + "lineNumber": 1, "ruleNames": [ "MD010", "no-hard-tabs" ], "ruleDescription": "Hard tabs", "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md010`, "errorDetail": "Column: 10", "errorContext": null, - "errorRange": [ 10, 1 ] } + "errorRange": [ 10, 1 ] + } ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -616,7 +670,8 @@ module.exports.manyPerLineResultVersion3 = test.ifError(err); const expectedResult = { "input": [ - { "lineNumber": 1, + { + "lineNumber": 1, "ruleNames": [ "MD010", "no-hard-tabs" ], "ruleDescription": "Hard tabs", "ruleInformation": @@ -624,8 +679,14 @@ module.exports.manyPerLineResultVersion3 = "errorDetail": "Column: 10", "errorContext": null, "errorRange": [ 10, 1 ], - "fixInfo": null }, - { "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "lineNumber": 1, "ruleNames": [ "MD010", "no-hard-tabs" ], "ruleDescription": "Hard tabs", "ruleInformation": @@ -633,7 +694,12 @@ module.exports.manyPerLineResultVersion3 = "errorDetail": "Column: 18", "errorContext": null, "errorRange": [ 18, 2 ], - "fixInfo": null } + "fixInfo": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } ] }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -1832,7 +1898,7 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { }; module.exports.fixErrors = function fixErrors(test) { - test.expect(8); + test.expect(10); const testCases = [ [ "Hello world.", @@ -1932,6 +1998,30 @@ module.exports.fixErrors = function fixErrors(test) { } ], "Hello world. Hi." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "Hello" ] ]; testCases.forEach((testCase) => { From a062e7c6bd929c5447efabdd447ccbedd04d87a8 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 28 Aug 2019 21:47:07 -0700 Subject: [PATCH 04/29] Update MD022/MD031/MD032 to report fixInfo for violations, normalize input to fixErrors. --- doc/CustomRules.md | 1 + helpers/helpers.js | 76 ++++++++---- lib/md022.js | 29 ++++- lib/md031.js | 20 +++- lib/md032.js | 29 ++++- test/markdownlint-test.js | 240 +++++++++++++++++++++++++++++++++++++- 6 files changed, 357 insertions(+), 38 deletions(-) diff --git a/doc/CustomRules.md b/doc/CustomRules.md index 44671ed4..964554f8 100644 --- a/doc/CustomRules.md +++ b/doc/CustomRules.md @@ -53,6 +53,7 @@ A rule is implemented as an `Object` with four required properties: - `context` is an optional `String` with relevant text surrounding the error location. - `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error. - `fixInfo` is an optional `Object` with information about how to fix the error: + - `lineNumber` is an optional `Number` specifying the 1-based line number of the edit. - `editColumn` is an optional `Number` specifying the 1-based column number of the edit. - `deleteCount` is an optional `Number` specifying the count of characters to delete with the edit. - `insertText` is an optional `String` specifying text to insert as part of the edit. diff --git a/helpers/helpers.js b/helpers/helpers.js index 58f5313a..f407e7ef 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -359,19 +359,19 @@ module.exports.addErrorDetailIf = function addErrorDetailIf( }; // Adds an error object with context via the onError callback -module.exports.addErrorContext = - function addErrorContext(onError, lineNumber, context, left, right, range) { - if (context.length <= 30) { - // Nothing to do - } else if (left && right) { - context = context.substr(0, 15) + "..." + context.substr(-15); - } else if (right) { - context = "..." + context.substr(-30); - } else { - context = context.substr(0, 30) + "..."; - } - addError(onError, lineNumber, null, context, range); - }; +module.exports.addErrorContext = function addErrorContext( + onError, lineNumber, context, left, right, range, fixInfo) { + if (context.length <= 30) { + // Nothing to do + } else if (left && right) { + context = context.substr(0, 15) + "..." + context.substr(-15); + } else if (right) { + context = "..." + context.substr(-30); + } else { + context = context.substr(0, 30) + "..."; + } + addError(onError, lineNumber, null, context, range, fixInfo); +}; // Returns a range object for a line by applying a RegExp module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) { @@ -403,20 +403,46 @@ module.exports.frontMatterHasTitle = // Applies as many fixes as possible to the input module.exports.fixErrors = function fixErrors(input, errors) { const lines = input.split(newLineRe); - errors.filter((error) => !!error.fixInfo).forEach((error) => { - const { lineNumber, fixInfo } = error; - const editColumn = fixInfo.editColumn || 1; - const deleteCount = fixInfo.deleteCount || 0; - const insertText = fixInfo.insertText || ""; + // Normalize fixInfo objects + const fixInfos = errors.filter((error) => !!error.fixInfo).map((error) => { + const { fixInfo } = error; + return { + "lineNumber": fixInfo.lineNumber || error.lineNumber, + "editColumn": fixInfo.editColumn || 1, + "deleteCount": fixInfo.deleteCount || 0, + "insertText": fixInfo.insertText || "" + }; + }); + // Sort bottom-to-top, deletes last, right-to-left, long-to-short + fixInfos.sort((a, b) => ( + (b.lineNumber - a.lineNumber) || + ((a.deleteCount === -1) ? 1 : ((b.deleteCount === -1) ? -1 : 0)) || + (b.editColumn - a.editColumn) || + (b.insertText.length - a.insertText.length) + )); + // Apply all fixes + let lastLineIndex = -1; + let lastEditIndex = -1; + fixInfos.forEach((fixInfo) => { + const { lineNumber, editColumn, deleteCount, insertText } = fixInfo; const lineIndex = lineNumber - 1; const editIndex = editColumn - 1; - const line = lines[lineIndex]; - lines[lineIndex] = - (deleteCount === -1) ? - null : - line.slice(0, editIndex) + - insertText + - line.slice(editIndex + deleteCount); + if ( + (lineIndex !== lastLineIndex) || + ((editIndex + deleteCount) < lastEditIndex) || + (deleteCount === -1) + ) { + const line = lines[lineIndex]; + lines[lineIndex] = + (deleteCount === -1) ? + null : + line.slice(0, editIndex) + + insertText + + line.slice(editIndex + deleteCount); + } + lastLineIndex = lineIndex; + lastEditIndex = editIndex; }); + // Return corrected input return lines.filter((line) => line !== null).join("\n"); }; diff --git a/lib/md022.js b/lib/md022.js index 9cb9ee30..65e8fc52 100644 --- a/lib/md022.js +++ b/lib/md022.js @@ -22,16 +22,33 @@ module.exports = { const [ topIndex, nextIndex ] = token.map; for (let i = 0; i < linesAbove; i++) { if (!isBlankLine(lines[topIndex - i - 1])) { - addErrorDetailIf(onError, topIndex + 1, linesAbove, i, "Above", - lines[topIndex].trim()); - return; + addErrorDetailIf( + onError, + topIndex + 1, + linesAbove, + i, + "Above", + lines[topIndex].trim(), + null, + { + "insertText": "\n" + }); } } for (let i = 0; i < linesBelow; i++) { if (!isBlankLine(lines[nextIndex + i])) { - addErrorDetailIf(onError, topIndex + 1, linesBelow, i, "Below", - lines[topIndex].trim()); - return; + addErrorDetailIf( + onError, + topIndex + 1, + linesBelow, + i, + "Below", + lines[topIndex].trim(), + null, + { + "lineNumber": nextIndex + 1, + "insertText": "\n" + }); } } }); diff --git a/lib/md031.js b/lib/md031.js index 99201456..34c052cd 100644 --- a/lib/md031.js +++ b/lib/md031.js @@ -14,10 +14,22 @@ module.exports = { const includeListItems = (listItems === undefined) ? true : !!listItems; const { lines } = params; forEachLine(lineMetadata(), (line, i, inCode, onFence, inTable, inItem) => { - if ((((onFence > 0) && !isBlankLine(lines[i - 1])) || - ((onFence < 0) && !isBlankLine(lines[i + 1]))) && - (includeListItems || !inItem)) { - addErrorContext(onError, i + 1, lines[i].trim()); + const onTopFence = (onFence > 0); + const onBottomFence = (onFence < 0); + if ((includeListItems || !inItem) && + ((onTopFence && !isBlankLine(lines[i - 1])) || + (onBottomFence && !isBlankLine(lines[i + 1])))) { + addErrorContext( + onError, + i + 1, + lines[i].trim(), + null, + null, + null, + { + "lineNumber": i + (onTopFence ? 1 : 2), + "insertText": "\n" + }); } }); } diff --git a/lib/md032.js b/lib/md032.js index a0f54786..607f167a 100644 --- a/lib/md032.js +++ b/lib/md032.js @@ -5,6 +5,8 @@ const { addErrorContext, isBlankLine } = require("../helpers"); const { flattenedLists } = require("./cache"); +const quotePrefixRe = /^[>\s]*/; + module.exports = { "names": [ "MD032", "blanks-around-lists" ], "description": "Lists should be surrounded by blank lines", @@ -14,11 +16,34 @@ module.exports = { flattenedLists().filter((list) => !list.nesting).forEach((list) => { const firstIndex = list.open.map[0]; if (!isBlankLine(lines[firstIndex - 1])) { - addErrorContext(onError, firstIndex + 1, lines[firstIndex].trim()); + const line = lines[firstIndex]; + const quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); + addErrorContext( + onError, + firstIndex + 1, + line.trim(), + null, + null, + null, + { + "insertText": `${quotePrefix}\n` + }); } const lastIndex = list.lastLineIndex - 1; if (!isBlankLine(lines[lastIndex + 1])) { - addErrorContext(onError, lastIndex + 1, lines[lastIndex].trim()); + const line = lines[lastIndex]; + const quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); + addErrorContext( + onError, + lastIndex + 1, + line.trim(), + null, + null, + null, + { + "lineNumber": lastIndex + 2, + "insertText": `${quotePrefix}\n` + }); } }); } diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 9aa77b36..537d5c7a 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1898,7 +1898,7 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { }; module.exports.fixErrors = function fixErrors(test) { - test.expect(10); + test.expect(23); const testCases = [ [ "Hello world.", @@ -2022,6 +2022,244 @@ module.exports.fixErrors = function fixErrors(test) { } ], "Hello" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "Hello" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello world", + [ + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 10, + "deleteCount": 1 + } + }, + { + "lineNumber": 1, + "fixInfo": { + "editColumn": 4, + "deleteCount": 1 + } + } + ], + "Helo word" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "Big " + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "fixInfo": { + "lineNumber": 1, + "deleteCount": -1 + } + }, + { + "fixInfo": { + "lineNumber": 2, + "deleteCount": -1 + } + } + ], + "" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "aa" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "b" + } + } + ], + "aaHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "insertText": "a" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "insertText": "bb" + } + } + ], + "bbHello world" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 6, + "insertText": " big" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello big orld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 2 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 8, + "deleteCount": 2 + } + } + ], + "Hello wld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zworld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "Hello zworld" ] ]; testCases.forEach((testCase) => { From 84e972c72c4e13112766169b12a6cb3988beccf4 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 28 Aug 2019 21:52:13 -0700 Subject: [PATCH 05/29] Replace string.trimEnd with string.trimRight for Node 8. --- lib/md032.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/md032.js b/lib/md032.js index 607f167a..264802e2 100644 --- a/lib/md032.js +++ b/lib/md032.js @@ -17,7 +17,7 @@ module.exports = { const firstIndex = list.open.map[0]; if (!isBlankLine(lines[firstIndex - 1])) { const line = lines[firstIndex]; - const quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); + const quotePrefix = line.match(quotePrefixRe)[0].trimRight(); addErrorContext( onError, firstIndex + 1, @@ -32,7 +32,7 @@ module.exports = { const lastIndex = list.lastLineIndex - 1; if (!isBlankLine(lines[lastIndex + 1])) { const line = lines[lastIndex]; - const quotePrefix = line.match(quotePrefixRe)[0].trimEnd(); + const quotePrefix = line.match(quotePrefixRe)[0].trimRight(); addErrorContext( onError, lastIndex + 1, From 5e73aa1d9d4a65d8fe5853ae6b5693eeaa58f1dd Mon Sep 17 00:00:00 2001 From: David Anson Date: Mon, 2 Sep 2019 15:35:43 -0700 Subject: [PATCH 06/29] Update MD006/MD023 to report fixInfo for violations, process input to fixErrors. --- helpers/helpers.js | 50 +++++++++++++++++++++++++++++++------- lib/md006.js | 17 ++++++++++--- lib/md023.js | 26 ++++++++++++++++---- test/lists-with-nesting.md | 6 ++--- test/markdownlint-test.js | 20 ++++++++++++--- 5 files changed, 94 insertions(+), 25 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index f407e7ef..d5f0c0b9 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -404,7 +404,7 @@ module.exports.frontMatterHasTitle = module.exports.fixErrors = function fixErrors(input, errors) { const lines = input.split(newLineRe); // Normalize fixInfo objects - const fixInfos = errors.filter((error) => !!error.fixInfo).map((error) => { + let fixInfos = errors.filter((error) => !!error.fixInfo).map((error) => { const { fixInfo } = error; return { "lineNumber": fixInfo.lineNumber || error.lineNumber, @@ -413,14 +413,46 @@ module.exports.fixErrors = function fixErrors(input, errors) { "insertText": fixInfo.insertText || "" }; }); - // Sort bottom-to-top, deletes last, right-to-left, long-to-short - fixInfos.sort((a, b) => ( - (b.lineNumber - a.lineNumber) || - ((a.deleteCount === -1) ? 1 : ((b.deleteCount === -1) ? -1 : 0)) || - (b.editColumn - a.editColumn) || - (b.insertText.length - a.insertText.length) - )); - // Apply all fixes + // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short + fixInfos.sort((a, b) => { + const aDeletingLine = (a.deleteCount === -1); + const bDeletingLine = (b.deleteCount === -1); + return ( + (b.lineNumber - a.lineNumber) || + (aDeletingLine ? 1 : (bDeletingLine ? -1 : 0)) || + (b.editColumn - a.editColumn) || + (b.insertText.length - a.insertText.length) + ); + }); + // Remove duplicate entries (needed for following collapse step) + let lastFixInfo = {}; + fixInfos = fixInfos.filter((fixInfo) => { + const unique = ( + (fixInfo.lineNumber !== lastFixInfo.lineNumber) || + (fixInfo.editColumn !== lastFixInfo.editColumn) || + (fixInfo.deleteCount !== lastFixInfo.deleteCount) || + (fixInfo.insertText !== lastFixInfo.insertText) + ); + lastFixInfo = fixInfo; + return unique; + }); + // Collapse insert/no-delete and no-insert/delete for same line/column + lastFixInfo = {}; + fixInfos.forEach((fixInfo) => { + if ( + (fixInfo.lineNumber === lastFixInfo.lineNumber) && + (fixInfo.editColumn === lastFixInfo.editColumn) && + !fixInfo.insertText && + (fixInfo.deleteCount > 0) && + lastFixInfo.insertText && + !lastFixInfo.deleteCount) { + fixInfo.insertText = lastFixInfo.insertText; + lastFixInfo.lineNumber = 0; + } + lastFixInfo = fixInfo; + }); + fixInfos = fixInfos.filter((fixInfo) => fixInfo.lineNumber); + // Apply all (remaining/updated) fixes let lastLineIndex = -1; let lastEditIndex = -1; fixInfos.forEach((fixInfo) => { diff --git a/lib/md006.js b/lib/md006.js index 2117ffaa..673bb1f0 100644 --- a/lib/md006.js +++ b/lib/md006.js @@ -13,10 +13,19 @@ module.exports = { "tags": [ "bullet", "ul", "indentation" ], "function": function MD006(params, onError) { flattenedLists().forEach((list) => { - if (list.unordered && !list.nesting) { - addErrorDetailIf(onError, list.open.lineNumber, - 0, list.indent, null, null, - rangeFromRegExp(list.open.line, listItemMarkerRe)); + if (list.unordered && !list.nesting && (list.indent !== 0)) { + const { lineNumber, line } = list.open; + addErrorDetailIf( + onError, + lineNumber, + 0, + list.indent, + null, + null, + rangeFromRegExp(line, listItemMarkerRe), + { + "deleteCount": line.length - line.trimLeft().length + }); } }); } diff --git a/lib/md023.js b/lib/md023.js index bf29d513..92d4c00f 100644 --- a/lib/md023.js +++ b/lib/md023.js @@ -2,8 +2,7 @@ "use strict"; -const { addErrorContext, filterTokens, rangeFromRegExp } = - require("../helpers"); +const { addErrorContext, filterTokens } = require("../helpers"); const spaceBeforeHeadingRe = /^((?:\s+)|(?:[>\s]+\s\s))[^>\s]/; @@ -13,9 +12,26 @@ module.exports = { "tags": [ "headings", "headers", "spaces" ], "function": function MD023(params, onError) { filterTokens(params, "heading_open", function forToken(token) { - if (spaceBeforeHeadingRe.test(token.line)) { - addErrorContext(onError, token.lineNumber, token.line, null, - null, rangeFromRegExp(token.line, spaceBeforeHeadingRe)); + const { lineNumber, line } = token; + const match = line.match(spaceBeforeHeadingRe); + if (match) { + const [ prefixAndFirstChar, prefix ] = match; + let deleteCount = prefix.length; + const prefixLengthNoSpace = prefix.trimRight().length; + if (prefixLengthNoSpace) { + deleteCount -= prefixLengthNoSpace - 1; + } + addErrorContext( + onError, + lineNumber, + line, + null, + null, + [ 1, prefixAndFirstChar.length ], + { + "editColumn": prefixLengthNoSpace + 1, + "deleteCount": deleteCount + }); } }); } diff --git a/test/lists-with-nesting.md b/test/lists-with-nesting.md index defdf813..5c89a8c9 100644 --- a/test/lists-with-nesting.md +++ b/test/lists-with-nesting.md @@ -16,13 +16,11 @@ - one {MD032} 1. two {MD032} - 1. three {MD032} -- four {MD032} +- three {MD032} 1. one {MD032} - two {MD006} {MD032} - - three {MD032} -1. four {MD032} +1. three {MD032} ## Correct nesting, same type diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 537d5c7a..437e5a6a 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1898,7 +1898,7 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { }; module.exports.fixErrors = function fixErrors(test) { - test.expect(23); + test.expect(24); const testCases = [ [ "Hello world.", @@ -2221,6 +2221,20 @@ module.exports.fixErrors = function fixErrors(test) { ], "Hello wld" ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1, + "insertText": "z" + } + } + ], + "Hello zorld" + ], [ "Hello world", [ @@ -2239,7 +2253,7 @@ module.exports.fixErrors = function fixErrors(test) { } } ], - "Hello zworld" + "Hello zorld" ], [ "Hello world", @@ -2259,7 +2273,7 @@ module.exports.fixErrors = function fixErrors(test) { } } ], - "Hello zworld" + "Hello zorld" ] ]; testCases.forEach((testCase) => { From 620853f200c07945a7a5ef155eb71be7115b2111 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 4 Sep 2019 22:18:23 -0700 Subject: [PATCH 07/29] Update MD011/MD034 to report fixInfo for violations. --- lib/md011.js | 28 +++++++++++++++++++++------- lib/md034.js | 15 ++++++++++++++- test/reversed_link.md | 6 ++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/md011.js b/lib/md011.js index 2d8a855d..f58abbd4 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -2,20 +2,34 @@ "use strict"; -const { addError, forEachInlineChild, rangeFromRegExp } = require("../helpers"); +const { addError, forEachInlineChild } = require("../helpers"); -const reversedLinkRe = /\([^)]+\)\[[^\]^][^\]]*]/; +const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g; module.exports = { "names": [ "MD011", "no-reversed-links" ], "description": "Reversed link syntax", "tags": [ "links" ], "function": function MD011(params, onError) { - forEachInlineChild(params, "text", function forToken(token) { - const match = reversedLinkRe.exec(token.content); - if (match) { - addError(onError, token.lineNumber, match[0], null, - rangeFromRegExp(token.line, reversedLinkRe)); + forEachInlineChild(params, "text", (token) => { + const { lineNumber, content } = token; + let match = null; + while ((match = reversedLinkRe.exec(content)) !== null) { + const [ reversedLink, linkText, linkDestination ] = match; + const column = match.index + 1; + const length = reversedLink.length; + addError( + onError, + lineNumber, + reversedLink, + null, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": `[${linkText}](${linkDestination})` + } + ); } }); } diff --git a/lib/md034.js b/lib/md034.js index 63fdd9f1..50fb147d 100644 --- a/lib/md034.js +++ b/lib/md034.js @@ -26,7 +26,20 @@ module.exports = { line.indexOf(content) + match.index + 1, bareUrl.length ]; - addErrorContext(onError, lineNumber, bareUrl, null, null, range); + const fixInfo = range ? { + "editColumn": range[0], + "deleteCount": range[1], + "insertText": `<${bareUrl}>` + } : null; + addErrorContext( + onError, + lineNumber, + bareUrl, + null, + null, + range, + fixInfo + ); } }); }); diff --git a/test/reversed_link.md b/test/reversed_link.md index 547d4008..20b3295f 100644 --- a/test/reversed_link.md +++ b/test/reversed_link.md @@ -5,3 +5,9 @@ However, this shouldn't trigger inside code blocks: myObj.getFiles("test")[0] Nor inline code: `myobj.getFiles("test")[0]` + +Two (issues)[https://www.example.com/one] in {MD011} {MD034} +the (same text)[https://www.example.com/two]. {MD011} {MD034} + + +Two (issues)[https://www.example.com/three] on the (same line)[https://www.example.com/four]. {MD011} {MD034} From c8a74bd72cc38838a4b1ed435a5e5cc8900f9c33 Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 6 Sep 2019 22:35:33 -0700 Subject: [PATCH 08/29] Update MD037/MD038/MD039 to report fixInfo for violations. --- lib/md037.js | 46 ++++++++++++++++++++++++++++++---------------- lib/md038.js | 22 ++++++++++++++++------ lib/md039.js | 27 ++++++++++++++++++++------- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/lib/md037.js b/lib/md037.js index 3f94d252..1e4b77c3 100644 --- a/lib/md037.js +++ b/lib/md037.js @@ -4,29 +4,43 @@ const { addErrorContext, forEachInlineChild } = require("../helpers"); +const leftSpaceRe = /(?:^|\s)(\*\*?|__?)\s.*[^\\]\1/g; +const rightSpaceRe = /(?:^|[^\\])(\*\*?|__?).+\s\1(?:\s|$)/g; + module.exports = { "names": [ "MD037", "no-space-in-emphasis" ], "description": "Spaces inside emphasis markers", "tags": [ "whitespace", "emphasis" ], "function": function MD037(params, onError) { forEachInlineChild(params, "text", (token) => { - let left = true; - let match = /(?:^|\s)(\*\*?|__?)\s.*[^\\]\1/.exec(token.content); - if (!match) { - left = false; - match = /(?:^|[^\\])(\*\*?|__?).+\s\1(?:\s|$)/.exec(token.content); - } - if (match) { - const fullText = match[0]; - const line = params.lines[token.lineNumber - 1]; - if (line.includes(fullText)) { - const text = fullText.trim(); - const column = line.indexOf(text) + 1; - const length = text.length; - addErrorContext(onError, token.lineNumber, - text, left, !left, [ column, length ]); + [ leftSpaceRe, rightSpaceRe ].forEach((spaceRe, index) => { + let match = null; + while ((match = spaceRe.exec(token.content)) !== null) { + const [ fullText, marker ] = match; + const line = params.lines[token.lineNumber - 1]; + if (line.includes(fullText)) { + const text = fullText.trim(); + const column = line.indexOf(text) + 1; + const length = text.length; + const markerLength = marker.length; + const emphasized = text.slice(markerLength, length - markerLength); + const fixedText = `${marker}${emphasized.trim()}${marker}`; + addErrorContext( + onError, + token.lineNumber, + text, + index === 0, + index !== 0, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": fixedText + } + ); + } } - } + }); }); } }; diff --git a/lib/md038.js b/lib/md038.js index c8bbfab2..424623a2 100644 --- a/lib/md038.js +++ b/lib/md038.js @@ -5,8 +5,8 @@ const { addErrorContext, filterTokens, forEachInlineCodeSpan, newLineRe } = require("../helpers"); -const startRe = /^\s([^`]|$)/; -const endRe = /[^`]\s$/; +const leftSpaceRe = /^\s([^`]|$)/; +const rightSpaceRe = /[^`]\s$/; module.exports = { "names": [ "MD038", "no-space-in-code" ], @@ -23,8 +23,8 @@ module.exports = { let rangeLength = code.length + (2 * tickCount); let rangeLineOffset = 0; const codeLines = code.split(newLineRe); - const left = startRe.test(code); - const right = !left && endRe.test(code); + const left = leftSpaceRe.test(code); + const right = !left && rightSpaceRe.test(code); if (right && (codeLines.length > 1)) { rangeIndex = 0; rangeLineOffset = codeLines.length - 1; @@ -36,8 +36,18 @@ module.exports = { const context = tokenLines[lineIndex + rangeLineOffset] .substring(rangeIndex, rangeIndex + rangeLength); addErrorContext( - onError, token.lineNumber + lineIndex + rangeLineOffset, - context, left, right, [ rangeIndex + 1, rangeLength ]); + onError, + token.lineNumber + lineIndex + rangeLineOffset, + context, + left, + right, + [ rangeIndex + 1, rangeLength ], + { + "editColumn": rangeIndex + (left ? tickCount : 0) + 1, + "deleteCount": rangeLength - (right ? tickCount : 0), + "insertText": code.trim() + } + ); } }); } diff --git a/lib/md039.js b/lib/md039.js index fb59d67f..146ebd90 100644 --- a/lib/md039.js +++ b/lib/md039.js @@ -2,8 +2,7 @@ "use strict"; -const { addErrorContext, filterTokens, rangeFromRegExp } = - require("../helpers"); +const { addErrorContext, filterTokens } = require("../helpers"); const spaceInLinkRe = /\[(?:\s+(?:[^\]]*?)\s*|(?:[^\]]*?)\s+)](?=\(\S*\))/; @@ -12,10 +11,11 @@ module.exports = { "description": "Spaces inside link text", "tags": [ "whitespace", "links" ], "function": function MD039(params, onError) { - filterTokens(params, "inline", function forToken(token) { + filterTokens(params, "inline", (token) => { + const { line, lineNumber, children } = token; let inLink = false; let linkText = ""; - token.children.forEach(function forChild(child) { + children.forEach((child) => { if (child.type === "link_open") { inLink = true; linkText = ""; @@ -24,9 +24,22 @@ module.exports = { const left = linkText.trimLeft().length !== linkText.length; const right = linkText.trimRight().length !== linkText.length; if (left || right) { - addErrorContext(onError, token.lineNumber, - "[" + linkText + "]", left, right, - rangeFromRegExp(token.line, spaceInLinkRe)); + const match = line.match(spaceInLinkRe); + const column = match.index + 1; + const length = match[0].length; + addErrorContext( + onError, + lineNumber, + `[${linkText}]`, + left, + right, + [ column, length ], + { + "editColumn": column + 1, + "deleteCount": length - 2, + "insertText": linkText.trim() + } + ); } } else if (inLink) { linkText += child.content; From 316bfeadaad5c93b53dfeab60438108fcfa0c282 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sun, 8 Sep 2019 16:51:00 -0700 Subject: [PATCH 09/29] Update MD018/MD019/MD020/MD021 to report fixInfo for violations. --- helpers/helpers.js | 1 - lib/md018.js | 20 ++++++-- lib/md019.js | 33 ++++++++++--- lib/md020.js | 54 +++++++++++++++++---- lib/md021.js | 53 ++++++++++++++++---- test/atx-heading-spacing-trailing-spaces.md | 19 ++++++++ test/break-all-the-rules.md | 13 +++-- test/markdownlint-test.js | 24 ++++----- 8 files changed, 167 insertions(+), 50 deletions(-) create mode 100644 test/atx-heading-spacing-trailing-spaces.md 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."); From 00a7e765eceb33f9310d9e0308a2f6e8e18f040d Mon Sep 17 00:00:00 2001 From: David Anson Date: Mon, 9 Sep 2019 22:03:59 -0700 Subject: [PATCH 10/29] Update MD027/MD044 to report fixInfo for violations. --- lib/md027.js | 56 +++++++++++-------- lib/md044.js | 27 ++++++++- .../detailed-results-MD022-MD030.results.json | 2 +- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/lib/md027.js b/lib/md027.js index f3210f42..ef56d3f1 100644 --- a/lib/md027.js +++ b/lib/md027.js @@ -2,9 +2,9 @@ "use strict"; -const { addErrorContext, newLineRe, rangeFromRegExp } = require("../helpers"); +const { addErrorContext, newLineRe } = require("../helpers"); -const spaceAfterBlockQuote = /^\s*(?:>\s+)+\S/; +const spaceAfterBlockQuoteRe = /^((?:\s*>)+)(\s{2,})\S/; module.exports = { "names": [ "MD027", "no-multiple-space-blockquote" ], @@ -13,31 +13,43 @@ module.exports = { "function": function MD027(params, onError) { let blockquoteNesting = 0; let listItemNesting = 0; - params.tokens.forEach(function forToken(token) { - if (token.type === "blockquote_open") { + params.tokens.forEach((token) => { + const { content, lineNumber, type } = token; + if (type === "blockquote_open") { blockquoteNesting++; - } else if (token.type === "blockquote_close") { + } else if (type === "blockquote_close") { blockquoteNesting--; - } else if (token.type === "list_item_open") { + } else if (type === "list_item_open") { listItemNesting++; - } else if (token.type === "list_item_close") { + } else if (type === "list_item_close") { listItemNesting--; - } else if ((token.type === "inline") && (blockquoteNesting > 0)) { - const multipleSpaces = listItemNesting ? - /^(\s*>)+\s\s+>/.test(token.line) : - /^(\s*>)+\s\s/.test(token.line); - if (multipleSpaces) { - addErrorContext(onError, token.lineNumber, token.line, null, - null, rangeFromRegExp(token.line, spaceAfterBlockQuote)); - } - token.content.split(newLineRe) - .forEach(function forLine(line, offset) { - if (/^\s/.test(line)) { - addErrorContext(onError, token.lineNumber + offset, - "> " + line, null, null, - rangeFromRegExp(line, spaceAfterBlockQuote)); + } else if ((type === "inline") && blockquoteNesting) { + const lineCount = content.split(newLineRe).length; + for (let i = 0; i < lineCount; i++) { + const line = params.lines[lineNumber + i - 1]; + const match = line.match(spaceAfterBlockQuoteRe); + if (match) { + const [ + fullMatch, + { "length": blockquoteLength }, + { "length": spaceLength } + ] = match; + if (!listItemNesting || (fullMatch[fullMatch.length - 1] === ">")) { + addErrorContext( + onError, + lineNumber + i, + line, + null, + null, + [ 1, fullMatch.length ], + { + "editColumn": blockquoteLength + 1, + "deleteCount": spaceLength - 1 + } + ); } - }); + } + } } }); } diff --git a/lib/md044.js b/lib/md044.js index 24a64a4c..ff0974a5 100644 --- a/lib/md044.js +++ b/lib/md044.js @@ -29,9 +29,30 @@ module.exports = { .replace(/^\W*/, "").replace(/\W*$/, ""); if (!names.includes(wordMatch)) { const lineNumber = token.lineNumber + index + fenceOffset; - const range = [ match.index + 1, wordMatch.length ]; - addErrorDetailIf(onError, lineNumber, - name, match[1], null, null, range); + const fullLine = params.lines[lineNumber - 1]; + let matchIndex = match.index; + const matchLength = wordMatch.length; + const fullLineWord = + fullLine.slice(matchIndex, matchIndex + matchLength); + if (fullLineWord !== wordMatch) { + // Attempt to fix bad offset due to inline content + matchIndex = fullLine.indexOf(wordMatch); + } + const range = [ matchIndex + 1, matchLength ]; + addErrorDetailIf( + onError, + lineNumber, + name, + match[1], + null, + null, + range, + { + "editColumn": matchIndex + 1, + "deleteCount": matchLength, + "insertText": name + } + ); } } } diff --git a/test/detailed-results-MD022-MD030.results.json b/test/detailed-results-MD022-MD030.results.json index 35a7f7de..a0a3ad89 100644 --- a/test/detailed-results-MD022-MD030.results.json +++ b/test/detailed-results-MD022-MD030.results.json @@ -78,7 +78,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md027", "errorDetail": null, "errorContext": "> > > Multiple spaces, multip...", - "errorRange": [ 1, 8 ] + "errorRange": [ 1, 4 ] }, { "lineNumber": 9, From 0502e370dead6cc01e25374da6821ae434ce15f0 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 11 Sep 2019 22:59:42 -0700 Subject: [PATCH 11/29] Update MD026/MD030 to report fixInfo for violations. --- lib/md026.js | 29 ++++++++++++++----- lib/md030.js | 28 ++++++++++++++---- ...results-MD030-warning-message.results.json | 4 +-- test/heading_trailing_punctuation.md | 7 +++++ test/spaces_after_list_marker.md | 2 +- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/lib/md026.js b/lib/md026.js index 88881255..4c07e7be 100644 --- a/lib/md026.js +++ b/lib/md026.js @@ -2,8 +2,8 @@ "use strict"; -const { addError, allPunctuation, escapeForRegExp, forEachHeading, - rangeFromRegExp } = require("../helpers"); +const { addError, allPunctuation, escapeForRegExp, forEachHeading } = + require("../helpers"); module.exports = { "names": [ "MD026", "no-trailing-punctuation" ], @@ -15,13 +15,26 @@ module.exports = { punctuation = allPunctuation; } const trailingPunctuationRe = - new RegExp("[" + escapeForRegExp(punctuation) + "]$"); - forEachHeading(params, (heading, content) => { - const match = trailingPunctuationRe.exec(content); + new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$"); + forEachHeading(params, (heading) => { + const { line, lineNumber } = heading; + const trimmedLine = line.replace(/[\s#]*$/, ""); + const match = trailingPunctuationRe.exec(trimmedLine); if (match) { - addError(onError, heading.lineNumber, - "Punctuation: '" + match[0] + "'", null, - rangeFromRegExp(heading.line, trailingPunctuationRe)); + const fullMatch = match[0]; + const column = match.index + 1; + const length = fullMatch.length; + addError( + onError, + lineNumber, + `Punctuation: '${fullMatch}'`, + null, + [ column, length ], + { + "editColumn": column, + "deleteCount": length + } + ); } }); } diff --git a/lib/md030.js b/lib/md030.js index e776a678..aa5c9ba3 100644 --- a/lib/md030.js +++ b/lib/md030.js @@ -2,8 +2,7 @@ "use strict"; -const { addErrorDetailIf, listItemMarkerRe, rangeFromRegExp } = - require("../helpers"); +const { addErrorDetailIf } = require("../helpers"); const { flattenedLists } = require("./cache"); module.exports = { @@ -22,10 +21,27 @@ module.exports = { (allSingle ? ulSingle : ulMulti) : (allSingle ? olSingle : olMulti); list.items.forEach((item) => { - const match = /^[\s>]*\S+(\s+)/.exec(item.line); - addErrorDetailIf(onError, item.lineNumber, - expectedSpaces, (match ? match[1].length : 0), null, null, - rangeFromRegExp(item.line, listItemMarkerRe)); + const { line, lineNumber } = item; + const match = /^[\s>]*\S+(\s*)/.exec(line); + const [ { "length": matchLength }, { "length": actualSpaces } ] = match; + let fixInfo = null; + if ((expectedSpaces !== actualSpaces) && (line.length > matchLength)) { + fixInfo = { + "editColumn": matchLength - actualSpaces + 1, + "deleteCount": actualSpaces, + "insertText": "".padEnd(expectedSpaces) + }; + } + addErrorDetailIf( + onError, + lineNumber, + expectedSpaces, + actualSpaces, + null, + null, + [ 1, matchLength ], + fixInfo + ); }); }); } diff --git a/test/detailed-results-MD030-warning-message.results.json b/test/detailed-results-MD030-warning-message.results.json index 772cf613..1d3ed425 100644 --- a/test/detailed-results-MD030-warning-message.results.json +++ b/test/detailed-results-MD030-warning-message.results.json @@ -6,7 +6,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md030", "errorDetail": "Expected: 1; Actual: 0", "errorContext": null, - "errorRange": null + "errorRange": [1, 1] }, { "lineNumber": 5, @@ -15,7 +15,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md030", "errorDetail": "Expected: 1; Actual: 0", "errorContext": null, - "errorRange": null + "errorRange": [1, 2] }, { "lineNumber": 11, diff --git a/test/heading_trailing_punctuation.md b/test/heading_trailing_punctuation.md index 0286b5b2..320a3a30 100644 --- a/test/heading_trailing_punctuation.md +++ b/test/heading_trailing_punctuation.md @@ -23,3 +23,10 @@ ## Heading/Full-Width {MD026} ! ## Heading/Full-Width {MD026} ? + + + +## Heading {MD026} alternate ? ## + +Heading {MD026} alternate too ? +------------------------------- diff --git a/test/spaces_after_list_marker.md b/test/spaces_after_list_marker.md index f9eee739..6c71da5f 100644 --- a/test/spaces_after_list_marker.md +++ b/test/spaces_after_list_marker.md @@ -34,7 +34,7 @@ List with multiple paragraphs and incorrect spacing * Foo {MD030} - Here is the second paragraph + Here is the second paragraph * Bar {MD030} From 5895ea62cb4f93b1aa6b684beb43bdf97ab46f7e Mon Sep 17 00:00:00 2001 From: David Anson Date: Thu, 12 Sep 2019 21:50:40 -0700 Subject: [PATCH 12/29] Rename and refactor applyFix from fixErrors for one-off scenarios. --- helpers/helpers.js | 48 +++++++++++++++++++++++---------------- test/markdownlint-test.js | 12 +++++----- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 1b697427..96d5361f 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -399,19 +399,35 @@ module.exports.frontMatterHasTitle = frontMatterLines.some((line) => frontMatterTitleRe.test(line)); }; -// Applies as many fixes as possible to the input -module.exports.fixErrors = function fixErrors(input, errors) { +// Normalizes the fields of a fixInfo object +function normalizeFixInfo(fixInfo, lineNumber) { + return { + "lineNumber": fixInfo.lineNumber || lineNumber, + "editColumn": fixInfo.editColumn || 1, + "deleteCount": fixInfo.deleteCount || 0, + "insertText": fixInfo.insertText || "" + }; +} + +// Fixes the specifide error on a line +function applyFix(line, fixInfo) { + const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); + const editIndex = editColumn - 1; + return (deleteCount === -1) ? + null : + line.slice(0, editIndex) + + insertText + + line.slice(editIndex + deleteCount); +} +module.exports.applyFix = applyFix; + +// Applies as many fixes as possible to the input lines +module.exports.applyFixes = function applyFixes(input, errors) { const lines = input.split(newLineRe); // Normalize fixInfo objects - let fixInfos = errors.filter((error) => !!error.fixInfo).map((error) => { - const { fixInfo } = error; - return { - "lineNumber": fixInfo.lineNumber || error.lineNumber, - "editColumn": fixInfo.editColumn || 1, - "deleteCount": fixInfo.deleteCount || 0, - "insertText": fixInfo.insertText || "" - }; - }); + let fixInfos = errors + .filter((error) => error.fixInfo) + .map((error) => normalizeFixInfo(error.fixInfo, error.lineNumber)); // Sort bottom-to-top, line-deletes last, right-to-left, long-to-short fixInfos.sort((a, b) => { const aDeletingLine = (a.deleteCount === -1); @@ -455,7 +471,7 @@ module.exports.fixErrors = function fixErrors(input, errors) { let lastLineIndex = -1; let lastEditIndex = -1; fixInfos.forEach((fixInfo) => { - const { lineNumber, editColumn, deleteCount, insertText } = fixInfo; + const { lineNumber, editColumn, deleteCount } = fixInfo; const lineIndex = lineNumber - 1; const editIndex = editColumn - 1; if ( @@ -463,13 +479,7 @@ module.exports.fixErrors = function fixErrors(input, errors) { ((editIndex + deleteCount) < lastEditIndex) || (deleteCount === -1) ) { - const line = lines[lineIndex]; - lines[lineIndex] = - (deleteCount === -1) ? - null : - line.slice(0, editIndex) + - insertText + - line.slice(editIndex + deleteCount); + lines[lineIndex] = applyFix(lines[lineIndex], fixInfo); } lastLineIndex = lineIndex; lastEditIndex = editIndex; diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 7957ec4e..e6afd83a 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -118,14 +118,14 @@ function createTestForFile(file) { return actual2or3; }) .then( - function verifyFixErrors(errors) { + function verifyFixes(errors) { if (detailedResults) { return test.ok(true); } return promisify(fs.readFile, file, helpers.utf8Encoding) .then( - function applyFixErrors(content) { - const corrections = helpers.fixErrors(content, errors); + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); return promisify(markdownlint, { "strings": { "input": corrections @@ -135,7 +135,7 @@ function createTestForFile(file) { }); }) .then( - function checkFixErrors(newErrors) { + function checkFixes(newErrors) { const unfixed = newErrors.input .filter((error) => !!error.fixInfo); test.deepEqual(unfixed, [], "Fixable error was not fixed."); @@ -1897,7 +1897,7 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { test.done(); }; -module.exports.fixErrors = function fixErrors(test) { +module.exports.applyFixes = function applyFixes(test) { test.expect(24); const testCases = [ [ @@ -2278,7 +2278,7 @@ module.exports.fixErrors = function fixErrors(test) { ]; testCases.forEach((testCase) => { const [ input, errors, expected ] = testCase; - const actual = helpers.fixErrors(input, errors); + const actual = helpers.applyFixes(input, errors); test.equal(actual, expected, "Incorrect fix applied."); }); test.done(); From 65f6d389781999effbe0258e8b2420ac6ccd67fa Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 14 Sep 2019 13:39:27 -0700 Subject: [PATCH 13/29] Validate errorInfo.fixInfo object/properties in extension calls to onError. --- helpers/helpers.js | 5 +++ lib/markdownlint.js | 31 +++++++++++++++ test/markdownlint-test.js | 81 ++++++++++++++++++++++++++++----------- 3 files changed, 95 insertions(+), 22 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 96d5361f..7ed093fc 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -44,6 +44,11 @@ module.exports.isEmptyString = function isEmptyString(str) { return str.length === 0; }; +// Returns true iff the input is an object +module.exports.isObject = function isObject(obj) { + return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj); +}; + // Returns true iff the input line is blank (no content) // Example: Contains nothing, whitespace, or comments const blankLineRe = />|(?:)/g; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 33ae82f7..2fc296b8 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -382,6 +382,37 @@ function lintContent( lines[errorInfo.lineNumber - 1].length))) { throwError("range"); } + const fixInfo = errorInfo.fixInfo; + if (fixInfo) { + if (!helpers.isObject(fixInfo)) { + throwError("fixInfo"); + } + if ((fixInfo.lineNumber !== undefined) && + (!helpers.isNumber(fixInfo.lineNumber) || + (fixInfo.lineNumber < 1) || + (fixInfo.lineNumber > lines.length))) { + throwError("fixInfo.lineNumber"); + } + const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber; + if ((fixInfo.editColumn !== undefined) && + (!helpers.isNumber(fixInfo.editColumn) || + (fixInfo.editColumn < 1) || + (fixInfo.editColumn > + lines[effectiveLineNumber - 1].length + 1))) { + throwError("fixInfo.editColumn"); + } + if ((fixInfo.deleteCount !== undefined) && + (!helpers.isNumber(fixInfo.deleteCount) || + (fixInfo.deleteCount < -1) || + (fixInfo.deleteCount > + lines[effectiveLineNumber - 1].length))) { + throwError("fixInfo.deleteCount"); + } + if ((fixInfo.insertText !== undefined) && + !helpers.isString(fixInfo.insertText)) { + throwError("fixInfo.insertText"); + } + } errors.push({ "lineNumber": errorInfo.lineNumber + frontMatterLines.length, "detail": errorInfo.detail || null, diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index e6afd83a..21d03f06 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -3177,19 +3177,32 @@ module.exports.customRulesOnErrorNull = function customRulesOnErrorNull(test) { }; module.exports.customRulesOnErrorBad = function customRulesOnErrorBad(test) { - test.expect(44); + test.expect(84); [ - [ "lineNumber", [ null, "string" ] ], - [ "detail", [ 10, [] ] ], - [ "context", [ 10, [] ] ], - [ "range", [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] ] + [ "lineNumber", null, [ null, "string" ] ], + [ "detail", null, [ 10, [] ] ], + [ "context", null, [ 10, [] ] ], + [ "range", null, [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] ], + [ "fixInfo", null, [ 10, "string" ] ], + [ "fixInfo", "lineNumber", [ null, "string" ] ], + [ "fixInfo", "editColumn", [ null, "string" ] ], + [ "fixInfo", "deleteCount", [ null, "string" ] ], + [ "fixInfo", "insertText", [ 10, [] ] ] ].forEach(function forProperty(property) { - const propertyName = property[0]; - property[1].forEach(function forPropertyValue(propertyValue) { + const [ propertyName, subPropertyName, propertyValues ] = property; + propertyValues.forEach(function forPropertyValue(propertyValue) { const badObject = { "lineNumber": 1 }; - badObject[propertyName] = propertyValue; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } const options = { "customRules": [ { @@ -3211,7 +3224,7 @@ module.exports.customRulesOnErrorBad = function customRulesOnErrorBad(test) { test.ok(err, "Did not get an error for bad object."); test.ok(err instanceof Error, "Error not instance of Error."); test.equal(err.message, - "Property '" + propertyName + "' of onError parameter is incorrect.", + "Property '" + propertyNames + "' of onError parameter is incorrect.", "Incorrect message for bad object."); return true; }, "Did not get exception for bad object."); @@ -3222,17 +3235,28 @@ module.exports.customRulesOnErrorBad = function customRulesOnErrorBad(test) { module.exports.customRulesOnErrorInvalid = function customRulesOnErrorInvalid(test) { - test.expect(36); + test.expect(68); [ - [ "lineNumber", [ -1, 0, 3, 4 ] ], - [ "range", [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] ] + [ "lineNumber", null, [ -1, 0, 3, 4 ] ], + [ "range", null, [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] ], + [ "fixInfo", "lineNumber", [ -1, 0, 3, 4 ] ], + [ "fixInfo", "editColumn", [ 0, 6 ] ], + [ "fixInfo", "deleteCount", [ -2, 5 ] ] ].forEach(function forProperty(property) { - const propertyName = property[0]; - property[1].forEach(function forPropertyValue(propertyValue) { + const [ propertyName, subPropertyName, propertyValues ] = property; + propertyValues.forEach(function forPropertyValue(propertyValue) { const badObject = { "lineNumber": 1 }; - badObject[propertyName] = propertyValue; + let propertyNames = null; + if (subPropertyName) { + badObject[propertyName] = {}; + badObject[propertyName][subPropertyName] = propertyValue; + propertyNames = `${propertyName}.${subPropertyName}`; + } else { + badObject[propertyName] = propertyValue; + propertyNames = propertyName; + } const options = { "customRules": [ { @@ -3254,7 +3278,7 @@ module.exports.customRulesOnErrorInvalid = test.ok(err, "Did not get an error for invalid object."); test.ok(err instanceof Error, "Error not instance of Error."); test.equal(err.message, - `Property '${propertyName}' of onError parameter is incorrect.`, + `Property '${propertyNames}' of onError parameter is incorrect.`, "Incorrect message for invalid object."); return true; }, "Did not get exception for invalid object."); @@ -3265,17 +3289,30 @@ module.exports.customRulesOnErrorInvalid = module.exports.customRulesOnErrorValid = function customRulesOnErrorValid(test) { - test.expect(7); + test.expect(24); [ - [ "lineNumber", [ 1, 2 ] ], - [ "range", [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] ] + [ "lineNumber", null, [ 1, 2 ] ], + [ "range", null, [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] ], + [ "fixInfo", "lineNumber", [ 1, 2 ] ], + [ "fixInfo", "editColumn", [ 1, 2, 4, 5 ] ], + [ "fixInfo", "deleteCount", [ -1, 0, 1, 4 ] ], + [ + "fixInfo", + "insertText", + [ "", "1", "123456", "\n", "\nText", "Text\n", "\nText\n" ] + ] ].forEach(function forProperty(property) { - const propertyName = property[0]; - property[1].forEach(function forPropertyValue(propertyValue) { + const [ propertyName, subPropertyName, propertyValues ] = property; + propertyValues.forEach(function forPropertyValue(propertyValue) { const goodObject = { "lineNumber": 1 }; - goodObject[propertyName] = propertyValue; + if (subPropertyName) { + goodObject[propertyName] = {}; + goodObject[propertyName][subPropertyName] = propertyValue; + } else { + goodObject[propertyName] = propertyValue; + } const options = { "customRules": [ { From 220a1d78a938bc5211636670015159636443e58d Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 14 Sep 2019 13:53:35 -0700 Subject: [PATCH 14/29] Update newline handling to match latest CommonMark specification. --- helpers/helpers.js | 2 +- test/l-sep-as-line-break.md | 6 ------ test/markdownlint-test.js | 12 ++---------- 3 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 test/l-sep-as-line-break.md diff --git a/helpers/helpers.js b/helpers/helpers.js index 7ed093fc..cb0e86c4 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -4,7 +4,7 @@ // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js -const newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/; +const newLineRe = /\r\n?|\n/; module.exports.newLineRe = newLineRe; // Regular expression for matching common front matter (YAML and TOML) diff --git a/test/l-sep-as-line-break.md b/test/l-sep-as-line-break.md deleted file mode 100644 index 468a8f8c..00000000 --- a/test/l-sep-as-line-break.md +++ /dev/null @@ -1,6 +0,0 @@ -# Heading - -```text -hello
 -world
 -``` diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 21d03f06..12e46d16 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -714,11 +714,7 @@ module.exports.stringInputLineEndings = function stringInputLineEndings(test) { "cr": "One\rTwo\r#Three\n", "lf": "One\nTwo\n#Three\n", "crlf": "One\r\nTwo\r\n#Three\n", - "mixed": "One\rTwo\n#Three\n", - "crnel": "One\r\u0085Two\r\u0085#Three\n", - "snl": "One\u2424Two\u2424#Three\n", - "lsep": "One\u2028Two\u2028#Three\n", - "nel": "One\u0085Two\u0085#Three\n" + "mixed": "One\rTwo\n#Three\n" }, "config": defaultConfig, "resultVersion": 0 @@ -729,11 +725,7 @@ module.exports.stringInputLineEndings = function stringInputLineEndings(test) { "cr": { "MD018": [ 3 ] }, "lf": { "MD018": [ 3 ] }, "crlf": { "MD018": [ 3 ] }, - "mixed": { "MD018": [ 3 ] }, - "crnel": { "MD018": [ 3 ] }, - "snl": { "MD018": [ 3 ] }, - "lsep": { "MD018": [ 3 ] }, - "nel": { "MD018": [ 3 ] } + "mixed": { "MD018": [ 3 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); test.done(); From b77a56255fdcb6cdc29ff70889e06223e51c879d Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 14 Sep 2019 22:31:08 -0700 Subject: [PATCH 15/29] Update applyFix/es to preserve the dominant line ending for each input. --- helpers/helpers.js | 47 +++++++++++++++++++++++--- test/markdownlint-test.js | 71 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index cb0e86c4..30a0a1dd 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -2,9 +2,11 @@ "use strict"; +const os = require("os"); + // Regular expression for matching common newline characters // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js -const newLineRe = /\r\n?|\n/; +const newLineRe = /\r\n?|\n/g; module.exports.newLineRe = newLineRe; // Regular expression for matching common front matter (YAML and TOML) @@ -404,6 +406,40 @@ module.exports.frontMatterHasTitle = frontMatterLines.some((line) => frontMatterTitleRe.test(line)); }; +// Gets the most common line ending, falling back to platform default +function getPreferredLineEnding(input) { + let cr = 0; + let lf = 0; + let crlf = 0; + const endings = input.match(newLineRe) || []; + endings.forEach((ending) => { + // eslint-disable-next-line default-case + switch (ending) { + case "\r": + cr++; + break; + case "\n": + lf++; + break; + case "\r\n": + crlf++; + break; + } + }); + let preferredLineEnding = null; + if (!cr && !lf && !crlf) { + preferredLineEnding = os.EOL; + } else if ((lf >= crlf) && (lf >= cr)) { + preferredLineEnding = "\n"; + } else if (crlf >= cr) { + preferredLineEnding = "\r\n"; + } else { + preferredLineEnding = "\r"; + } + return preferredLineEnding; +} +module.exports.getPreferredLineEnding = getPreferredLineEnding; + // Normalizes the fields of a fixInfo object function normalizeFixInfo(fixInfo, lineNumber) { return { @@ -415,19 +451,20 @@ function normalizeFixInfo(fixInfo, lineNumber) { } // Fixes the specifide error on a line -function applyFix(line, fixInfo) { +function applyFix(line, fixInfo, lineEnding) { const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); const editIndex = editColumn - 1; return (deleteCount === -1) ? null : line.slice(0, editIndex) + - insertText + + insertText.replace("\n", lineEnding) + line.slice(editIndex + deleteCount); } module.exports.applyFix = applyFix; // Applies as many fixes as possible to the input lines module.exports.applyFixes = function applyFixes(input, errors) { + const lineEnding = getPreferredLineEnding(input); const lines = input.split(newLineRe); // Normalize fixInfo objects let fixInfos = errors @@ -484,11 +521,11 @@ module.exports.applyFixes = function applyFixes(input, errors) { ((editIndex + deleteCount) < lastEditIndex) || (deleteCount === -1) ) { - lines[lineIndex] = applyFix(lines[lineIndex], fixInfo); + lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); } lastLineIndex = lineIndex; lastEditIndex = editIndex; }); // Return corrected input - return lines.filter((line) => line !== null).join("\n"); + return lines.filter((line) => line !== null).join(lineEnding); }; diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 12e46d16..1c39d2e0 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1,6 +1,7 @@ "use strict"; const fs = require("fs"); +const os = require("os"); const path = require("path"); const { URL } = require("url"); const md = require("markdown-it")(); @@ -1889,8 +1890,37 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { test.done(); }; +module.exports.getPreferredLineEnding = function getPreferredLineEnding(test) { + test.expect(17); + const testCases = [ + [ "", os.EOL ], + [ "\r", "\r" ], + [ "\n", "\n" ], + [ "\r\n", "\r\n" ], + [ "t\rt\nt", "\n" ], + [ "t\nt\rt", "\n" ], + [ "t\r\nt\nt", "\n" ], + [ "t\nt\r\nt", "\n" ], + [ "t\r\nt\rt", "\r\n" ], + [ "t\rt\r\nt", "\r\n" ], + [ "t\r\nt\rt\nt", "\n" ], + [ "t\r\nt\r\nt\r\nt", "\r\n" ], + [ "t\nt\nt\nt", "\n" ], + [ "t\rt\rt\rt", "\r" ], + [ "t\r\nt\nt\r\nt", "\r\n" ], + [ "t\nt\r\nt\nt", "\n" ], + [ "t\rt\t\rt", "\r" ] + ]; + testCases.forEach((testCase) => { + const [ input, expected ] = testCase; + const actual = helpers.getPreferredLineEnding(input); + test.equal(actual, expected, "Incorrect line ending returned."); + }); + test.done(); +}; + module.exports.applyFixes = function applyFixes(test) { - test.expect(24); + test.expect(27); const testCases = [ [ "Hello world.", @@ -2266,6 +2296,45 @@ module.exports.applyFixes = function applyFixes(test) { } ], "Hello zorld" + ], + [ + "Hello\nworld\nhello\rworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\nworld\nhello\nworld\n" + ], + [ + "Hello\r\nworld\r\nhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\r\nworld\r\nhello\r\nworld\r\n" + ], + [ + "Hello\rworld\rhello\nworld", + [ + { + "fixInfo": { + "lineNumber": 4, + "editColumn": 6, + "insertText": "\n" + } + } + ], + "Hello\rworld\rhello\rworld\r" ] ]; testCases.forEach((testCase) => { From 52939a6d7e83132e709d7ca30525485b13c191e8 Mon Sep 17 00:00:00 2001 From: David Anson Date: Mon, 16 Sep 2019 21:48:34 -0700 Subject: [PATCH 16/29] Add validation of applyFixes via result snapshots. --- test/detailed-results-MD001-MD010.md.fixed | 30 +++++ test/detailed-results-MD011-MD021.md.fixed | 23 ++++ test/detailed-results-MD022-MD030.md.fixed | 19 ++++ ...led-results-MD030-warning-message.md.fixed | 13 +++ test/detailed-results-MD031-MD040.md.fixed | 66 +++++++++++ test/detailed-results-MD041-MD050.md.fixed | 27 +++++ ...esults-blanks-around-headings-0-2.md.fixed | 26 +++++ ...esults-blanks-around-headings-3-0.md.fixed | 30 +++++ ...ed-results-blanks-around-headings.md.fixed | 26 +++++ test/detailed-results-front-matter.md.fixed | 6 + test/detailed-results-html-tags.md.fixed | 107 ++++++++++++++++++ test/detailed-results-links.md.fixed | 26 +++++ ...s-ordered-list-item-prefix-single.md.fixed | 3 + ...lts-ordered-list-item-prefix-zero.md.fixed | 19 ++++ test/markdownlint-test.js | 32 +++++- 15 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 test/detailed-results-MD001-MD010.md.fixed create mode 100644 test/detailed-results-MD011-MD021.md.fixed create mode 100644 test/detailed-results-MD022-MD030.md.fixed create mode 100644 test/detailed-results-MD030-warning-message.md.fixed create mode 100644 test/detailed-results-MD031-MD040.md.fixed create mode 100644 test/detailed-results-MD041-MD050.md.fixed create mode 100644 test/detailed-results-blanks-around-headings-0-2.md.fixed create mode 100644 test/detailed-results-blanks-around-headings-3-0.md.fixed create mode 100644 test/detailed-results-blanks-around-headings.md.fixed create mode 100644 test/detailed-results-front-matter.md.fixed create mode 100644 test/detailed-results-html-tags.md.fixed create mode 100644 test/detailed-results-links.md.fixed create mode 100644 test/detailed-results-ordered-list-item-prefix-single.md.fixed create mode 100644 test/detailed-results-ordered-list-item-prefix-zero.md.fixed diff --git a/test/detailed-results-MD001-MD010.md.fixed b/test/detailed-results-MD001-MD010.md.fixed new file mode 100644 index 00000000..428b8d44 --- /dev/null +++ b/test/detailed-results-MD001-MD010.md.fixed @@ -0,0 +1,30 @@ +## One + +#### Two + +### Three ### + +* Alpha + * Bravo + +- Charlie + +* Delta + * Echo + +Text + +Text text + + 1. One + 2. Two + 3. Three + 4. Four +5. Five + 6. Six + 7. Seven + 8. Eight + 9. Nine +10. Ten + 11. Eleven +12. Twelve diff --git a/test/detailed-results-MD011-MD021.md.fixed b/test/detailed-results-MD011-MD021.md.fixed new file mode 100644 index 00000000..04a11e86 --- /dev/null +++ b/test/detailed-results-MD011-MD021.md.fixed @@ -0,0 +1,23 @@ +# Top level heading + + + +A [reversed](link) example. + +123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 + +## 123456789 123456789 123456789 123456789 123456789 123456789 + + $ command with no output + +## No space A + +## Multiple spaces B + +## No space C ####No space C ## + +## No space D #### No space D## + +## Multiple spaces E #### Multiple spaces E ## + +## Multiple spaces F #### Multiple spaces F ## diff --git a/test/detailed-results-MD022-MD030.md.fixed b/test/detailed-results-MD022-MD030.md.fixed new file mode 100644 index 00000000..ec0b4b97 --- /dev/null +++ b/test/detailed-results-MD022-MD030.md.fixed @@ -0,0 +1,19 @@ +# Heading + +Text + +# Heading + +## Another heading + +> Multiple spaces +> Blank line above + +1. Alpha +3. Beta + +> > Multiple spaces, multiple blockquotes +> > +> > > Multiple spaces, multiple blockquotes +> > > +> > > Multiple spaces, multiple blockquotes diff --git a/test/detailed-results-MD030-warning-message.md.fixed b/test/detailed-results-MD030-warning-message.md.fixed new file mode 100644 index 00000000..b7b7834b --- /dev/null +++ b/test/detailed-results-MD030-warning-message.md.fixed @@ -0,0 +1,13 @@ +# + +- + +1. + +- a + +1. a + +- a + +1. a diff --git a/test/detailed-results-MD031-MD040.md.fixed b/test/detailed-results-MD031-MD040.md.fixed new file mode 100644 index 00000000..5effe1cf --- /dev/null +++ b/test/detailed-results-MD031-MD040.md.fixed @@ -0,0 +1,66 @@ +```js +debugger; +``` + +* List + +Inline
HTML + +Bare link + +--- +*** + +*Emphasis* + +Space *inside* emphasis + +Space `insidecode span + +Space [inside](link) text + +``` +``` + +space ``insideode +space `inside` of `codeelements +`space` inside `of` code `elements +space ``inside`` of ``codelements +``` embedded backtick +embedded backtick``` + +some *space* in *some* emphasis +some *space* in *some* emphasis +some *space* in **some** emphasis +some _space_ in _some_ emphasis +some __space__ in __some__ emphasis + +Text +text `code +span +span` text +text. + +Text +text `code +code +span` text +text. + +* List + +--- + +Text +text ```code +span code +code +span code +span code``` text +text +text text ````code +span code +span +span code +span```` text +text. diff --git a/test/detailed-results-MD041-MD050.md.fixed b/test/detailed-results-MD041-MD050.md.fixed new file mode 100644 index 00000000..adc358df --- /dev/null +++ b/test/detailed-results-MD041-MD050.md.fixed @@ -0,0 +1,27 @@ +Not a heading + +An [empty]() link + +An [empty](#) link with fragment + +An [empty](<>) link with angle brackets + +This is a test file for the markdownlint package. + +This is a paragraph +about markdownlint +that capitalizes the +name wrong twice: +markdownlint. + +A [normal](link) and an [empty one]() and a [fragment](#one). + +An image without alternate text ![](image.jpg) + +```text +Fenced code +``` + + Indented code + +Missing newline character diff --git a/test/detailed-results-blanks-around-headings-0-2.md.fixed b/test/detailed-results-blanks-around-headings-0-2.md.fixed new file mode 100644 index 00000000..2356153d --- /dev/null +++ b/test/detailed-results-blanks-around-headings-0-2.md.fixed @@ -0,0 +1,26 @@ +# Blanks Around Headings + + +## Apple + + +Text +## Banana + + +Text +## Cherry + + +Text +## Durian ## + + +Text + +--- +Elderberry +---------- + +Text +## Fig diff --git a/test/detailed-results-blanks-around-headings-3-0.md.fixed b/test/detailed-results-blanks-around-headings-3-0.md.fixed new file mode 100644 index 00000000..6b7d263a --- /dev/null +++ b/test/detailed-results-blanks-around-headings-3-0.md.fixed @@ -0,0 +1,30 @@ +# Blanks Around Headings + + + +## Apple +Text + + + +## Banana +Text + + + +## Cherry +Text + + + +## Durian ## +Text + + +Elderberry +---------- +Text + + + +## Fig diff --git a/test/detailed-results-blanks-around-headings.md.fixed b/test/detailed-results-blanks-around-headings.md.fixed new file mode 100644 index 00000000..0271c369 --- /dev/null +++ b/test/detailed-results-blanks-around-headings.md.fixed @@ -0,0 +1,26 @@ +# Blanks Around Headings + +## Apple + +Text + +## Banana + +Text + +## Cherry + +Text + +## Durian ## + +Text + +--- + +Elderberry +---------- + +Text + +## Fig diff --git a/test/detailed-results-front-matter.md.fixed b/test/detailed-results-front-matter.md.fixed new file mode 100644 index 00000000..3658daee --- /dev/null +++ b/test/detailed-results-front-matter.md.fixed @@ -0,0 +1,6 @@ +--- +front: matter +--- +Text + +Text diff --git a/test/detailed-results-html-tags.md.fixed b/test/detailed-results-html-tags.md.fixed new file mode 100644 index 00000000..0bbf4773 --- /dev/null +++ b/test/detailed-results-html-tags.md.fixed @@ -0,0 +1,107 @@ +# Detailed HTML Results + +Text + +Block block + +Text inline inline text + +Text + +Block block + +Text inline inline text + +Text + +

+Block +block block block +block +block block block +block +block block block block block +block block block block block +

+ +Text + +Block block + +Text inline inline text + +Text + +Block block + +Text inline inline text + +Text + +Text inline text inline text inline text + +Text inline text inline text inline text + +Text + +\Block block\ + +\\Block block\\ + +Block block + +Text \inline inline\ text + +Text \\inline inline\\ text + +Text inline inline text + +Text + +> Text inline inline text +> text inline inline text + +Text + + Text inline inline text + text inline inline text + +Text + +```html +Text inline inline text +text inline inline text +``` + +Text + +`` + +Text ```` text + +Text `` text ```` text `````` text + +Text `` text inline text + +Text ``text text`` text + +Text + +Text inline text +text text + +Text + + is an email autolink. + +Another email autolink: . + +Text + + is an HTML element. + +But is not an autolink or HTML element. +And neither is . +Nor <123abc>. + +Text diff --git a/test/detailed-results-links.md.fixed b/test/detailed-results-links.md.fixed new file mode 100644 index 00000000..cadb3eb7 --- /dev/null +++ b/test/detailed-results-links.md.fixed @@ -0,0 +1,26 @@ +# Detailed Link Results + +Text text + +Text text text + +Text text text + +Text `code https://example.com/code code` text text + +> Text text text + +Text +text +text +text + +```text +Code https://example.com/code?type=fence code +``` + + Code https://example.com/code?type=indent code + +Text more text still more text done + +Text more \* text https://example.com/same more \[ text done diff --git a/test/detailed-results-ordered-list-item-prefix-single.md.fixed b/test/detailed-results-ordered-list-item-prefix-single.md.fixed new file mode 100644 index 00000000..1a6959b5 --- /dev/null +++ b/test/detailed-results-ordered-list-item-prefix-single.md.fixed @@ -0,0 +1,3 @@ +# Ordered list examples + +9. Item diff --git a/test/detailed-results-ordered-list-item-prefix-zero.md.fixed b/test/detailed-results-ordered-list-item-prefix-zero.md.fixed new file mode 100644 index 00000000..f8eac253 --- /dev/null +++ b/test/detailed-results-ordered-list-item-prefix-zero.md.fixed @@ -0,0 +1,19 @@ +# Ordered list examples + +text + +0. Item +0. Item +0. Item + +text + +1. Item +1. Item +1. Item + +text + +1. Item +2. Item +3. Item diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 1c39d2e0..f73e8390 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -35,9 +35,10 @@ function promisify(func, ...args) { function createTestForFile(file) { return function testForFile(test) { - test.expect(2); const detailedResults = /[/\\]detailed-results-/.test(file); + test.expect(detailedResults ? 3 : 2); const resultsFile = file.replace(/\.md$/, ".results.json"); + const fixedFile = file.replace(/\.md$/, ".md.fixed"); const configFile = file.replace(/\.md$/, ".json"); let mergedConfig = null; const actualPromise = promisify(fs.stat, configFile) @@ -62,9 +63,34 @@ function createTestForFile(file) { }); }) .then( - function convertResultVersion2To0(resultVersion2) { + function diffFixedFiles(resultVersion2or3) { + return detailedResults ? + Promise.all([ + promisify(markdownlint, { + "files": [ file ], + "config": mergedConfig, + "resultVersion": 3 + }), + promisify(fs.readFile, file, helpers.utf8Encoding), + promisify(fs.readFile, fixedFile, helpers.utf8Encoding) + ]) + .then(function validateApplyFixes(fulfillments) { + const [ resultVersion3, content, expected ] = fulfillments; + const errors = resultVersion3[file]; + const actual = helpers.applyFixes(content, errors); + // Uncomment the following line to update *.md.fixed files + // fs.writeFileSync(fixedFile, actual, helpers.utf8Encoding); + test.equal(actual, expected, + "Unexpected output from applyFixes."); + return resultVersion2or3; + }) : + resultVersion2or3; + } + ) + .then( + function convertResultVersion2To0(resultVersion2or3) { const result0 = {}; - const result2or3 = resultVersion2[file]; + const result2or3 = resultVersion2or3[file]; result2or3.forEach(function forResult(result) { const ruleName = result.ruleNames[0]; const lineNumbers = result0[ruleName] || []; From 677255a484e4556e361d38deb763f962195c75dd Mon Sep 17 00:00:00 2001 From: David Anson Date: Mon, 16 Sep 2019 22:34:49 -0700 Subject: [PATCH 17/29] Fix applyFix handling of multiple line endings; MD022 handling of multiple lines. --- helpers/helpers.js | 2 +- lib/md022.js | 54 ++++++++++--------- ...esults-blanks-around-headings-0-2.md.fixed | 1 + ...esults-blanks-around-headings-3-0.md.fixed | 1 + test/markdownlint-test.js | 15 +++++- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 30a0a1dd..6cd386ac 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -457,7 +457,7 @@ function applyFix(line, fixInfo, lineEnding) { return (deleteCount === -1) ? null : line.slice(0, editIndex) + - insertText.replace("\n", lineEnding) + + insertText.replace(/\n/g, lineEnding) + line.slice(editIndex + deleteCount); } module.exports.applyFix = applyFix; diff --git a/lib/md022.js b/lib/md022.js index 65e8fc52..d62c5e9d 100644 --- a/lib/md022.js +++ b/lib/md022.js @@ -20,37 +20,41 @@ module.exports = { const { lines } = params; filterTokens(params, "heading_open", (token) => { const [ topIndex, nextIndex ] = token.map; + let actualAbove = 0; for (let i = 0; i < linesAbove; i++) { - if (!isBlankLine(lines[topIndex - i - 1])) { - addErrorDetailIf( - onError, - topIndex + 1, - linesAbove, - i, - "Above", - lines[topIndex].trim(), - null, - { - "insertText": "\n" - }); + if (isBlankLine(lines[topIndex - i - 1])) { + actualAbove++; } } + addErrorDetailIf( + onError, + topIndex + 1, + linesAbove, + actualAbove, + "Above", + lines[topIndex].trim(), + null, + { + "insertText": "".padEnd(linesAbove - actualAbove, "\n") + }); + let actualBelow = 0; for (let i = 0; i < linesBelow; i++) { - if (!isBlankLine(lines[nextIndex + i])) { - addErrorDetailIf( - onError, - topIndex + 1, - linesBelow, - i, - "Below", - lines[topIndex].trim(), - null, - { - "lineNumber": nextIndex + 1, - "insertText": "\n" - }); + if (isBlankLine(lines[nextIndex + i])) { + actualBelow++; } } + addErrorDetailIf( + onError, + topIndex + 1, + linesBelow, + actualBelow, + "Below", + lines[topIndex].trim(), + null, + { + "lineNumber": nextIndex + 1, + "insertText": "".padEnd(linesBelow - actualBelow, "\n") + }); }); } }; diff --git a/test/detailed-results-blanks-around-headings-0-2.md.fixed b/test/detailed-results-blanks-around-headings-0-2.md.fixed index 2356153d..a5742c4a 100644 --- a/test/detailed-results-blanks-around-headings-0-2.md.fixed +++ b/test/detailed-results-blanks-around-headings-0-2.md.fixed @@ -22,5 +22,6 @@ Text Elderberry ---------- + Text ## Fig diff --git a/test/detailed-results-blanks-around-headings-3-0.md.fixed b/test/detailed-results-blanks-around-headings-3-0.md.fixed index 6b7d263a..eb218781 100644 --- a/test/detailed-results-blanks-around-headings-3-0.md.fixed +++ b/test/detailed-results-blanks-around-headings-3-0.md.fixed @@ -21,6 +21,7 @@ Text Text + Elderberry ---------- Text diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index f73e8390..1890bf23 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1946,7 +1946,7 @@ module.exports.getPreferredLineEnding = function getPreferredLineEnding(test) { }; module.exports.applyFixes = function applyFixes(test) { - test.expect(27); + test.expect(28); const testCases = [ [ "Hello world.", @@ -2361,6 +2361,19 @@ module.exports.applyFixes = function applyFixes(test) { } ], "Hello\rworld\rhello\rworld\r" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" ] ]; testCases.forEach((testCase) => { From 33e379708421b5aa1f65fdc79e0053e14142c43e Mon Sep 17 00:00:00 2001 From: David Anson Date: Thu, 19 Sep 2019 21:39:59 -0700 Subject: [PATCH 18/29] Fix broken MD020/MD021/MD038 scenarios. --- lib/md020.js | 12 ++++++------ lib/md021.js | 2 +- lib/md038.js | 18 ++++++++++++++---- test/detailed-results-MD011-MD021.md.fixed | 8 ++++---- test/detailed-results-MD031-MD040.md.fixed | 20 +++++++------------- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/lib/md020.js b/lib/md020.js index 03d6661f..db69c68f 100644 --- a/lib/md020.js +++ b/lib/md020.js @@ -12,7 +12,8 @@ module.exports = { "function": function MD020(params, onError) { forEachLine(lineMetadata(), (line, lineIndex, inCode) => { if (!inCode) { - const match = /^(#+)(\s*)([^#]+?)(\s*)(\\?)(#+)(\s*)$/.exec(line); + const match = + /^(#+)(\s*)([^#]+?[^#\\])(\s*)((?:\\#)?)(#+)(\s*)$/.exec(line); if (match) { const [ , @@ -27,9 +28,8 @@ module.exports = { const leftHashLength = leftHash.length; const rightHashLength = rightHash.length; const left = !leftSpaceLength; - const right = - (!rightSpaceLength && (!rightEscape || (rightHashLength > 1))) || - (rightEscape && (rightHashLength > 1)); + const right = !rightSpaceLength || rightEscape; + const rightEscapeReplacement = rightEscape ? `${rightEscape} ` : ""; if (left || right) { const range = left ? [ @@ -49,9 +49,9 @@ module.exports = { range, { "editColumn": 1, - "deleteLength": line.length, + "deleteCount": line.length, "insertText": - `${leftHash} ${content} ${rightEscape}${rightHash}` + `${leftHash} ${content} ${rightEscapeReplacement}${rightHash}` } ); } diff --git a/lib/md021.js b/lib/md021.js index fcb30a12..5fa972c7 100644 --- a/lib/md021.js +++ b/lib/md021.js @@ -48,7 +48,7 @@ module.exports = { range, { "editColumn": 1, - "deleteLength": length, + "deleteCount": length, "insertText": `${leftHash} ${content} ${rightHash}` } ); diff --git a/lib/md038.js b/lib/md038.js index 424623a2..dd8bac28 100644 --- a/lib/md038.js +++ b/lib/md038.js @@ -22,19 +22,29 @@ module.exports = { let rangeIndex = columnIndex - tickCount; let rangeLength = code.length + (2 * tickCount); let rangeLineOffset = 0; + let fixIndex = columnIndex; + let fixLength = code.length; const codeLines = code.split(newLineRe); const left = leftSpaceRe.test(code); const right = !left && rightSpaceRe.test(code); if (right && (codeLines.length > 1)) { rangeIndex = 0; rangeLineOffset = codeLines.length - 1; + fixIndex = 0; } if (left || right) { + const codeLinesRange = codeLines[rangeLineOffset]; if (codeLines.length > 1) { - rangeLength = codeLines[rangeLineOffset].length + tickCount; + rangeLength = codeLinesRange.length + tickCount; + fixLength = codeLinesRange.length; } const context = tokenLines[lineIndex + rangeLineOffset] .substring(rangeIndex, rangeIndex + rangeLength); + const codeLinesRangeTrim = codeLinesRange.trim(); + const fixText = + (codeLinesRangeTrim.startsWith("`") ? " " : "") + + codeLinesRangeTrim + + (codeLinesRangeTrim.endsWith("`") ? " " : ""); addErrorContext( onError, token.lineNumber + lineIndex + rangeLineOffset, @@ -43,9 +53,9 @@ module.exports = { right, [ rangeIndex + 1, rangeLength ], { - "editColumn": rangeIndex + (left ? tickCount : 0) + 1, - "deleteCount": rangeLength - (right ? tickCount : 0), - "insertText": code.trim() + "editColumn": fixIndex + 1, + "deleteCount": fixLength, + "insertText": fixText } ); } diff --git a/test/detailed-results-MD011-MD021.md.fixed b/test/detailed-results-MD011-MD021.md.fixed index 04a11e86..dfe511a3 100644 --- a/test/detailed-results-MD011-MD021.md.fixed +++ b/test/detailed-results-MD011-MD021.md.fixed @@ -14,10 +14,10 @@ A [reversed](link) example. ## Multiple spaces B -## No space C ####No space C ## +## No space C ## -## No space D #### No space D## +## No space D ## -## Multiple spaces E #### Multiple spaces E ## +## Multiple spaces E ## -## Multiple spaces F #### Multiple spaces F ## +## Multiple spaces F ## diff --git a/test/detailed-results-MD031-MD040.md.fixed b/test/detailed-results-MD031-MD040.md.fixed index 5effe1cf..a28211ef 100644 --- a/test/detailed-results-MD031-MD040.md.fixed +++ b/test/detailed-results-MD031-MD040.md.fixed @@ -15,19 +15,19 @@ Bare link Space *inside* emphasis -Space `insidecode span +Space `inside` code span Space [inside](link) text ``` ``` -space ``insideode -space `inside` of `codeelements -`space` inside `of` code `elements -space ``inside`` of ``codelements -``` embedded backtick -embedded backtick``` +space ``inside`` code +space `inside` of `code` elements +`space` inside `of` code `elements` +space ``inside`` of ``code`` elements +`` ` embedded backtick`` +``embedded backtick` `` some *space* in *some* emphasis some *space* in *some* emphasis @@ -37,13 +37,11 @@ some __space__ in __some__ emphasis Text text `code -span span` text text. Text text `code -code span` text text. @@ -54,13 +52,9 @@ text. Text text ```code span code -code -span code span code``` text text text text ````code span code -span -span code span```` text text. From d974e78e3f509567076612db46a2f8e5436df4d4 Mon Sep 17 00:00:00 2001 From: David Anson Date: Thu, 19 Sep 2019 21:49:42 -0700 Subject: [PATCH 19/29] Update README.md and CustomRules.md documentation of new fixInfo behavior. --- README.md | 5 +++-- doc/CustomRules.md | 12 ++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 72c2745c..1b7ebb6b 100644 --- a/README.md +++ b/README.md @@ -427,8 +427,9 @@ includes information about the line number, rule names, description, as well as additional detail or context that is available. *This is the default.* Passing a `resultVersion` of `3` corresponds to the detailed version `2` format -with additional information about fixes for certain errors. All errors for each -line are included (other versions collapse multiple errors for the same rule). +with additional information about how to fix automatically-fixable errors. In this +mode, all errors that occur on each line are reported (other versions report only +the first error for each rule). ##### options.markdownItPlugins diff --git a/doc/CustomRules.md b/doc/CustomRules.md index 964554f8..78b59dfc 100644 --- a/doc/CustomRules.md +++ b/doc/CustomRules.md @@ -43,7 +43,8 @@ A rule is implemented as an `Object` with four required properties: - `function` is a synchronous `Function` that implements the rule and is passed two parameters: - `params` is an `Object` with properties that describe the content being analyzed: - `name` is a `String` that identifies the input file/string. - - `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token) with added `line` and `lineNumber` properties. + - `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token) + with added `line` and `lineNumber` properties. - `lines` is an `Array` of `String` values corresponding to the lines of the input file/string. - `frontMatterLines` is an `Array` of `String` values corresponding to any front matter (not present in `lines`). - `config` is an `Object` corresponding to the rule's entry in `options.config` (if present). @@ -52,11 +53,14 @@ A rule is implemented as an `Object` with four required properties: - `details` is an optional `String` with information about what caused the error. - `context` is an optional `String` with relevant text surrounding the error location. - `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error. - - `fixInfo` is an optional `Object` with information about how to fix the error: + - `fixInfo` is an optional `Object` with information about how to fix the error (all properties are optional, but + at least one of `deleteCount` and `insertText` should be present; when applying a fix, the delete should be + performed before the insert): - `lineNumber` is an optional `Number` specifying the 1-based line number of the edit. - `editColumn` is an optional `Number` specifying the 1-based column number of the edit. - - `deleteCount` is an optional `Number` specifying the count of characters to delete with the edit. - - `insertText` is an optional `String` specifying text to insert as part of the edit. + - `deleteCount` is an optional `Number` specifying the count of characters to delete. + - `insertText` is an optional `String` specifying the text to insert. `\n` is the platform-independent way to add + a line break; line breaks should be added at the beginning of a line instead of at the end). The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers). From 4843e277c04a4d23f1f10a471b764a85d9eca9e7 Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 20 Sep 2019 21:50:44 -0700 Subject: [PATCH 20/29] Update applyFix to make line ending optional, MD047 to highlight only last character. --- helpers/helpers.js | 2 +- lib/md047.js | 2 +- .../detailed-results-MD041-MD050.results.json | 2 +- test/markdownlint-test.js | 50 ++++++++++++++++++- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 6cd386ac..9f850303 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -457,7 +457,7 @@ function applyFix(line, fixInfo, lineEnding) { return (deleteCount === -1) ? null : line.slice(0, editIndex) + - insertText.replace(/\n/g, lineEnding) + + insertText.replace(/\n/g, lineEnding || "\n") + line.slice(editIndex + deleteCount); } module.exports.applyFix = applyFix; diff --git a/lib/md047.js b/lib/md047.js index a1f41525..a001b27b 100644 --- a/lib/md047.js +++ b/lib/md047.js @@ -17,7 +17,7 @@ module.exports = { lastLineNumber, null, null, - null, + [ lastLine.length, 1 ], { "insertText": "\n", "editColumn": lastLine.length + 1 diff --git a/test/detailed-results-MD041-MD050.results.json b/test/detailed-results-MD041-MD050.results.json index b7b80366..5151f974 100644 --- a/test/detailed-results-MD041-MD050.results.json +++ b/test/detailed-results-MD041-MD050.results.json @@ -105,6 +105,6 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md047", "errorDetail": null, "errorContext": null, - "errorRange": null + "errorRange": [ 25, 1 ] } ] \ No newline at end of file diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 1890bf23..c34dc559 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -578,7 +578,7 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, "errorDetail": null, "errorContext": null, - "errorRange": null, + "errorRange": [ 15, 1 ], "fixInfo": { "insertText": "\n", "editColumn": 16 @@ -1945,6 +1945,54 @@ module.exports.getPreferredLineEnding = function getPreferredLineEnding(test) { test.done(); }; +module.exports.applyFix = function applyFix(test) { + test.expect(4); + const testCases = [ + [ + "Hello world.", + { + "editColumn": 12, + "deleteCount": 1 + }, + undefined, + "Hello world" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + undefined, + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\n", + "Hello world.\n" + ], + [ + "Hello world.", + { + "editColumn": 13, + "insertText": "\n" + }, + "\r\n", + "Hello world.\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ line, fixInfo, lineEnding, expected ] = testCase; + const actual = helpers.applyFix(line, fixInfo, lineEnding); + test.equal(actual, expected, "Incorrect fix applied."); + }); + test.done(); +}; + module.exports.applyFixes = function applyFixes(test) { test.expect(28); const testCases = [ From a75f1ecf2aededbbda26627cd80c742b56c61741 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 21 Sep 2019 21:05:58 -0700 Subject: [PATCH 21/29] Fix MD039/no-space-in-links range for multiple issues on the same line. --- lib/md039.js | 6 ++++-- test/detailed-results-MD031-MD040.md | 2 ++ test/detailed-results-MD031-MD040.md.fixed | 2 ++ .../detailed-results-MD031-MD040.results.json | 19 ++++++++++++++----- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/md039.js b/lib/md039.js index 146ebd90..55c33b8d 100644 --- a/lib/md039.js +++ b/lib/md039.js @@ -15,6 +15,7 @@ module.exports = { const { line, lineNumber, children } = token; let inLink = false; let linkText = ""; + let lineIndex = 0; children.forEach((child) => { if (child.type === "link_open") { inLink = true; @@ -24,9 +25,10 @@ module.exports = { const left = linkText.trimLeft().length !== linkText.length; const right = linkText.trimRight().length !== linkText.length; if (left || right) { - const match = line.match(spaceInLinkRe); - const column = match.index + 1; + const match = line.slice(lineIndex).match(spaceInLinkRe); + const column = match.index + lineIndex + 1; const length = match[0].length; + lineIndex = column + length - 1; addErrorContext( onError, lineNumber, diff --git a/test/detailed-results-MD031-MD040.md b/test/detailed-results-MD031-MD040.md index d59ebb47..31b18371 100644 --- a/test/detailed-results-MD031-MD040.md +++ b/test/detailed-results-MD031-MD040.md @@ -56,3 +56,5 @@ text text ```` code span code span```` text text. + +Text [ space](link) text [space ](link) text [ space ](link) text. diff --git a/test/detailed-results-MD031-MD040.md.fixed b/test/detailed-results-MD031-MD040.md.fixed index a28211ef..70e70654 100644 --- a/test/detailed-results-MD031-MD040.md.fixed +++ b/test/detailed-results-MD031-MD040.md.fixed @@ -58,3 +58,5 @@ text text ````code span code span```` text text. + +Text [space](link) text [space](link) text [space](link) text. diff --git a/test/detailed-results-MD031-MD040.results.json b/test/detailed-results-MD031-MD040.results.json index d6cfe843..13da02e5 100644 --- a/test/detailed-results-MD031-MD040.results.json +++ b/test/detailed-results-MD031-MD040.results.json @@ -33,7 +33,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md033", "errorDetail": "Element: hr", "errorContext": null, - "errorRange": [7, 5] + "errorRange": [ 7, 5 ] }, { "lineNumber": 8, @@ -42,7 +42,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md034", "errorDetail": null, "errorContext": "https://example.com", - "errorRange": [6, 19] + "errorRange": [ 6, 19 ] }, { "lineNumber": 11, @@ -69,7 +69,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md037", "errorDetail": null, "errorContext": "* inside *", - "errorRange": [7, 10] + "errorRange": [ 7, 10 ] }, { "lineNumber": 31, @@ -123,7 +123,7 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md038", "errorDetail": null, "errorContext": "` inside `", - "errorRange": [7, 10] + "errorRange": [ 7, 10 ] }, { "lineNumber": 24, @@ -222,7 +222,16 @@ "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", "errorDetail": null, "errorContext": "[ inside ]", - "errorRange": [7, 10] + "errorRange": [ 7, 10 ] + }, + { + "lineNumber": 60, + "ruleNames": [ "MD039", "no-space-in-links" ], + "ruleDescription": "Spaces inside link text", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md039", + "errorDetail": null, + "errorContext": "[ space]", + "errorRange": [ 6, 8 ] }, { "lineNumber": 21, From 737126cf93253f5507b0ffd4e0c1af874f396478 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sun, 22 Sep 2019 21:31:02 -0700 Subject: [PATCH 22/29] Update MD034/no-bare-urls to report multiple instances on the same line. --- helpers/helpers.js | 2 +- lib/md034.js | 45 ++++++++++++------------ test/detailed-results-links.md | 2 ++ test/detailed-results-links.md.fixed | 2 ++ test/detailed-results-links.results.json | 9 +++++ 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index 9f850303..d4729549 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -21,7 +21,7 @@ const inlineCommentRe = module.exports.inlineCommentRe = inlineCommentRe; // Regular expressions for range matching -module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/i; +module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/ig; module.exports.listItemMarkerRe = /^[\s>]*(?:[*+-]|\d+[.)])\s+/; module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/; diff --git a/lib/md034.js b/lib/md034.js index 50fb147d..bc264773 100644 --- a/lib/md034.js +++ b/lib/md034.js @@ -18,28 +18,29 @@ module.exports = { inLink = true; } else if (type === "link_close") { inLink = false; - } else if ((type === "text") && !inLink && - (match = bareUrlRe.exec(content))) { - const [ bareUrl ] = match; - const index = line.indexOf(content); - const range = (index === -1) ? null : [ - line.indexOf(content) + match.index + 1, - bareUrl.length - ]; - const fixInfo = range ? { - "editColumn": range[0], - "deleteCount": range[1], - "insertText": `<${bareUrl}>` - } : null; - addErrorContext( - onError, - lineNumber, - bareUrl, - null, - null, - range, - fixInfo - ); + } else if ((type === "text") && !inLink) { + while ((match = bareUrlRe.exec(content)) !== null) { + const [ bareUrl ] = match; + const index = line.indexOf(content); + const range = (index === -1) ? null : [ + line.indexOf(content) + match.index + 1, + bareUrl.length + ]; + const fixInfo = range ? { + "editColumn": range[0], + "deleteCount": range[1], + "insertText": `<${bareUrl}>` + } : null; + addErrorContext( + onError, + lineNumber, + bareUrl, + null, + null, + range, + fixInfo + ); + } } }); }); diff --git a/test/detailed-results-links.md b/test/detailed-results-links.md index 502d2ecb..0928701e 100644 --- a/test/detailed-results-links.md +++ b/test/detailed-results-links.md @@ -24,3 +24,5 @@ Code https://example.com/code?type=fence code Text more text https://example.com/same still more text done Text more \* text https://example.com/same more \[ text done + +Text https://example.com/first more text https://example.com/second still more text https://example.com/third done diff --git a/test/detailed-results-links.md.fixed b/test/detailed-results-links.md.fixed index cadb3eb7..c7a5d40f 100644 --- a/test/detailed-results-links.md.fixed +++ b/test/detailed-results-links.md.fixed @@ -24,3 +24,5 @@ Code https://example.com/code?type=fence code Text more text still more text done Text more \* text https://example.com/same more \[ text done + +Text more text still more text done diff --git a/test/detailed-results-links.results.json b/test/detailed-results-links.results.json index 1b0ac4b0..05b4eaaf 100644 --- a/test/detailed-results-links.results.json +++ b/test/detailed-results-links.results.json @@ -88,5 +88,14 @@ "errorDetail": null, "errorContext": "https://example.com/same", "errorRange": null + }, + { + "lineNumber": 28, + "ruleNames": [ "MD034", "no-bare-urls" ], + "ruleDescription": "Bare URL used", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md034", + "errorDetail": null, + "errorContext": "https://example.com/first", + "errorRange": [ 6, 25 ] } ] From dc8e746cd21be5422c2729c2172116e6468b7e96 Mon Sep 17 00:00:00 2001 From: David Anson Date: Sun, 22 Sep 2019 21:58:57 -0700 Subject: [PATCH 23/29] Update MD037/no-space-in-emphasis to report left+right errors only once. --- lib/md037.js | 44 ++++++++++++++++++++++----------------- test/markdownlint-test.js | 27 +++++++++++++++++++----- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/md037.js b/lib/md037.js index 1e4b77c3..95cd2c31 100644 --- a/lib/md037.js +++ b/lib/md037.js @@ -13,31 +13,37 @@ module.exports = { "tags": [ "whitespace", "emphasis" ], "function": function MD037(params, onError) { forEachInlineChild(params, "text", (token) => { + const { content, lineNumber } = token; + const columnsReported = []; [ leftSpaceRe, rightSpaceRe ].forEach((spaceRe, index) => { let match = null; - while ((match = spaceRe.exec(token.content)) !== null) { + while ((match = spaceRe.exec(content)) !== null) { const [ fullText, marker ] = match; - const line = params.lines[token.lineNumber - 1]; + const line = params.lines[lineNumber - 1]; if (line.includes(fullText)) { const text = fullText.trim(); const column = line.indexOf(text) + 1; - const length = text.length; - const markerLength = marker.length; - const emphasized = text.slice(markerLength, length - markerLength); - const fixedText = `${marker}${emphasized.trim()}${marker}`; - addErrorContext( - onError, - token.lineNumber, - text, - index === 0, - index !== 0, - [ column, length ], - { - "editColumn": column, - "deleteCount": length, - "insertText": fixedText - } - ); + if (!columnsReported.includes(column)) { + const length = text.length; + const markerLength = marker.length; + const emphasized = + text.slice(markerLength, length - markerLength); + const fixedText = `${marker}${emphasized.trim()}${marker}`; + addErrorContext( + onError, + lineNumber, + text, + index === 0, + index !== 0, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": fixedText + } + ); + columnsReported.push(column); + } } } }); diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index c34dc559..05d84696 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -522,7 +522,8 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { "input": "# Heading \n" + "\n" + - "Text\ttext\t\ttext" + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" }, "resultVersion": 3 }; @@ -572,16 +573,30 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { } }, { - "lineNumber": 3, + "lineNumber": 4, + "ruleNames": [ "MD037", "no-space-in-emphasis" ], + "ruleDescription": "Spaces inside emphasis markers", + "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md037`, + "errorDetail": null, + "errorContext": "* emphasis *", + "errorRange": [ 6, 12 ], + "fixInfo": { + "editColumn": 6, + "deleteCount": 12, + "insertText": "*emphasis*" + } + }, + { + "lineNumber": 4, "ruleNames": [ "MD047", "single-trailing-newline" ], "ruleDescription": "Files should end with a single newline character", "ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`, "errorDetail": null, "errorContext": null, - "errorRange": [ 15, 1 ], + "errorRange": [ 22, 1 ], "fixInfo": { "insertText": "\n", - "editColumn": 16 + "editColumn": 23 } } ] @@ -595,7 +610,9 @@ module.exports.resultFormattingV3 = function resultFormattingV3(test) { " Hard tabs [Column: 5]\n" + "input: 3: MD010/no-hard-tabs" + " Hard tabs [Column: 10]\n" + - "input: 3: MD047/single-trailing-newline" + + "input: 4: MD037/no-space-in-emphasis" + + " Spaces inside emphasis markers [Context: \"* emphasis *\"]\n" + + "input: 4: MD047/single-trailing-newline" + " Files should end with a single newline character"; test.equal(actualMessage, expectedMessage, "Incorrect message."); test.done(); From 1ac2e6c3e84cf9e84d8786b08f0e964fd6c055f4 Mon Sep 17 00:00:00 2001 From: David Anson Date: Tue, 24 Sep 2019 22:40:41 -0700 Subject: [PATCH 24/29] Fix MD019/no-multiple-space-atx to ignore headings without content. --- lib/md019.js | 2 +- test/headings-without-content.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/headings-without-content.md diff --git a/lib/md019.js b/lib/md019.js index 1505c6b3..2ea87fb6 100644 --- a/lib/md019.js +++ b/lib/md019.js @@ -13,7 +13,7 @@ module.exports = { filterTokens(params, "heading_open", (token) => { if (headingStyleFor(token) === "atx") { const { line, lineNumber } = token; - const match = /^(#+)(\s{2,})/.exec(line); + const match = /^(#+)(\s{2,})(?:\S)/.exec(line); if (match) { const [ , diff --git a/test/headings-without-content.md b/test/headings-without-content.md new file mode 100644 index 00000000..0bc6e3e4 --- /dev/null +++ b/test/headings-without-content.md @@ -0,0 +1,20 @@ +# Headings Without Content + + + + +# + +# + +# + +# + +## + +## + +## + +## From 33cb1a71ec8bd8e2edda3ed6d2a112bff69c956a Mon Sep 17 00:00:00 2001 From: David Anson Date: Tue, 24 Sep 2019 23:00:30 -0700 Subject: [PATCH 25/29] Fix MD039/no-space-in-links to handle links within a multi-line paragraph. --- lib/md039.js | 7 ++++++- test/spaces_inside_link_text.md | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/md039.js b/lib/md039.js index 55c33b8d..f9464cf7 100644 --- a/lib/md039.js +++ b/lib/md039.js @@ -12,7 +12,8 @@ module.exports = { "tags": [ "whitespace", "links" ], "function": function MD039(params, onError) { filterTokens(params, "inline", (token) => { - const { line, lineNumber, children } = token; + const { children } = token; + let { lineNumber } = token; let inLink = false; let linkText = ""; let lineIndex = 0; @@ -25,6 +26,7 @@ module.exports = { const left = linkText.trimLeft().length !== linkText.length; const right = linkText.trimRight().length !== linkText.length; if (left || right) { + const line = params.lines[lineNumber - 1]; const match = line.slice(lineIndex).match(spaceInLinkRe); const column = match.index + lineIndex + 1; const length = match[0].length; @@ -43,6 +45,9 @@ module.exports = { } ); } + } else if (child.type === "softbreak") { + lineNumber++; + lineIndex = 0; } else if (inLink) { linkText += child.content; } diff --git a/test/spaces_inside_link_text.md b/test/spaces_inside_link_text.md index 6b3a6dc1..76f2f420 100644 --- a/test/spaces_inside_link_text.md +++ b/test/spaces_inside_link_text.md @@ -30,3 +30,14 @@ The following shouldn't break anything: [![Screenshot.png](/images/Screenshot.png)](/images/Screenshot.png) + +function CodeButNotCode(input) { + return input.replace(/[- ]([a-z])/g, "one"); // {MD039} +} + +function MoreCodeButNotCode(input) { + input = input.replace(/[- ]([a-z])/g, "two"); // {MD039} + input = input.toLowerCase(); + input = input.replace(/[- ]([a-z])/g, "three"); // {MD039} + return input; +} From a5f03d02d0d3e6fcac74efa35a4ad6dddbf1e998 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 25 Sep 2019 21:59:37 -0700 Subject: [PATCH 26/29] Fix forEachInlineCodeSpan to handle lines ending with '\' better. --- helpers/helpers.js | 3 ++- test/line-breaks-inside-code-spans.md | 5 +++++ test/markdownlint-test.js | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/helpers/helpers.js b/helpers/helpers.js index d4729549..640d6150 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -319,7 +319,8 @@ module.exports.forEachInlineCodeSpan = currentLine++; currentColumn = 0; } else if ((char === "\\") && - ((startIndex === -1) || (startColumn === -1))) { + ((startIndex === -1) || (startColumn === -1)) && + (input[index + 1] !== "\n")) { // Escape character outside code, skip next index++; currentColumn += 2; diff --git a/test/line-breaks-inside-code-spans.md b/test/line-breaks-inside-code-spans.md index 76a69eaf..eff1e069 100644 --- a/test/line-breaks-inside-code-spans.md +++ b/test/line-breaks-inside-code-spans.md @@ -66,3 +66,8 @@ text ` code {MD038} span code span` text text. + +" +Text `code +code code `text` {MD038} diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 05d84696..224968da 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -1833,7 +1833,7 @@ module.exports.includesSorted = function includesSorted(test) { }; module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { - test.expect(94); + test.expect(99); const testCases = [ [ @@ -1916,6 +1916,10 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { [ "text \\` text `code`", [ [ "code", 0, 14, 1 ] ] + ], + [ + "text\\\n`code`", + [ [ "code", 1, 1, 1 ] ] ] ]; testCases.forEach((testCase) => { From 3632ce28b90830b608ebf340b3659d35693830e2 Mon Sep 17 00:00:00 2001 From: David Anson Date: Thu, 26 Sep 2019 22:29:01 -0700 Subject: [PATCH 27/29] Fix highlighting of MD011/no-reversed-links when preceeded by decorated text. --- lib/md011.js | 3 ++- test/detailed-results-MD011-MD021.md | 2 ++ test/detailed-results-MD011-MD021.md.fixed | 2 ++ test/detailed-results-MD011-MD021.results.json | 9 +++++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/md011.js b/lib/md011.js index f58abbd4..36bff74e 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -16,7 +16,8 @@ module.exports = { let match = null; while ((match = reversedLinkRe.exec(content)) !== null) { const [ reversedLink, linkText, linkDestination ] = match; - const column = match.index + 1; + const line = params.lines[lineNumber - 1]; + const column = line.indexOf(reversedLink) + 1; const length = reversedLink.length; addError( onError, diff --git a/test/detailed-results-MD011-MD021.md b/test/detailed-results-MD011-MD021.md index 2e49bd73..c7213301 100644 --- a/test/detailed-results-MD011-MD021.md +++ b/test/detailed-results-MD011-MD021.md @@ -22,3 +22,5 @@ A (reversed)[link] example. ## Multiple spaces E ## ## Multiple spaces F ## + +*Another* (reversed)[link] example. diff --git a/test/detailed-results-MD011-MD021.md.fixed b/test/detailed-results-MD011-MD021.md.fixed index dfe511a3..407f771c 100644 --- a/test/detailed-results-MD011-MD021.md.fixed +++ b/test/detailed-results-MD011-MD021.md.fixed @@ -21,3 +21,5 @@ A [reversed](link) example. ## Multiple spaces E ## ## Multiple spaces F ## + +*Another* [reversed](link) example. diff --git a/test/detailed-results-MD011-MD021.results.json b/test/detailed-results-MD011-MD021.results.json index acca18f4..954d9ea7 100644 --- a/test/detailed-results-MD011-MD021.results.json +++ b/test/detailed-results-MD011-MD021.results.json @@ -8,6 +8,15 @@ "errorContext": null, "errorRange": [3, 16] }, + { + "lineNumber": 26, + "ruleNames": [ "MD011", "no-reversed-links" ], + "ruleDescription": "Reversed link syntax", + "ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md011", + "errorDetail": "(reversed)[link]", + "errorContext": null, + "errorRange": [11, 16] + }, { "lineNumber": 7, "ruleNames": [ "MD012", "no-multiple-blanks" ], From 40ac584482ec51eda7935a5f3576e4b776f767ff Mon Sep 17 00:00:00 2001 From: David Anson Date: Fri, 27 Sep 2019 21:18:16 -0700 Subject: [PATCH 28/29] Update annotateTokens to handle multi-line code spans. --- lib/markdownlint.js | 9 +++++++++ test/reversed_link.md | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 2fc296b8..e788767f 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -172,11 +172,20 @@ function annotateTokens(tokens, lines) { } // Annotate children with lineNumber let lineNumber = token.lineNumber; + const codeSpanExtraLines = []; + helpers.forEachInlineCodeSpan( + token.content, + function handleInlineCodeSpan(code) { + codeSpanExtraLines.push(code.split(helpers.newLineRe).length - 1); + } + ); (token.children || []).forEach(function forChild(child) { child.lineNumber = lineNumber; child.line = lines[lineNumber - 1]; if ((child.type === "softbreak") || (child.type === "hardbreak")) { lineNumber++; + } else if (child.type === "code_inline") { + lineNumber += codeSpanExtraLines.shift(); } }); } diff --git a/test/reversed_link.md b/test/reversed_link.md index 20b3295f..d642ba4c 100644 --- a/test/reversed_link.md +++ b/test/reversed_link.md @@ -11,3 +11,14 @@ the (same text)[https://www.example.com/two]. {MD011} {MD034} Two (issues)[https://www.example.com/three] on the (same line)[https://www.example.com/four]. {MD011} {MD034} + +`code code +code` +(reversed)[link] {MD011} + +text +text `code +code code +code` text +text +text (reversed)[link] text {MD011} From 6587ba72616931b4025be48aa27895722accc92e Mon Sep 17 00:00:00 2001 From: David Anson Date: Sat, 28 Sep 2019 14:30:16 -0700 Subject: [PATCH 29/29] Fix MD011/no-reversed-links to better handle escaped RegExp content in reversed links. --- lib/md011.js | 5 +++-- test/reversed_link.md | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/md011.js b/lib/md011.js index 36bff74e..da0f1dc1 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -2,7 +2,8 @@ "use strict"; -const { addError, forEachInlineChild } = require("../helpers"); +const { addError, forEachInlineChild, unescapeMarkdown } = + require("../helpers"); const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g; @@ -17,7 +18,7 @@ module.exports = { while ((match = reversedLinkRe.exec(content)) !== null) { const [ reversedLink, linkText, linkDestination ] = match; const line = params.lines[lineNumber - 1]; - const column = line.indexOf(reversedLink) + 1; + const column = unescapeMarkdown(line).indexOf(reversedLink) + 1; const length = reversedLink.length; addError( onError, diff --git a/test/reversed_link.md b/test/reversed_link.md index d642ba4c..b6df2601 100644 --- a/test/reversed_link.md +++ b/test/reversed_link.md @@ -22,3 +22,13 @@ code code code` text text text (reversed)[link] text {MD011} + +## Escaped JavaScript Content + +var IDENT_RE = '([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*'; {MD011} + +begin: /\B(([\/.])[\w\-.\/=]+)+/, {MD011} + +{begin: '%r\\(', end: '\\)[a-z]*'} {MD011} + +return /(?:(?:(^|\/)[!.])|[*?+()|\[\]{}]|[+@]\()/.test(str); {MD011}