diff --git a/README.md b/README.md index 6b6d718f..1b7ebb6b 100644 --- a/README.md +++ b/README.md @@ -416,15 +416,20 @@ 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 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 0c7458c0..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,6 +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 (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. + - `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). diff --git a/helpers/helpers.js b/helpers/helpers.js index 71643c77..640d6150 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -2,9 +2,12 @@ "use strict"; +const os = require("os"); + // 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?|\n/g; +module.exports.newLineRe = newLineRe; // Regular expression for matching common front matter (YAML and TOML) module.exports.frontMatterRe = @@ -18,8 +21,7 @@ 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.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s]*/ig; module.exports.listItemMarkerRe = /^[\s>]*(?:[*+-]|\d+[.)])\s+/; module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/; @@ -44,6 +46,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; @@ -312,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; @@ -331,19 +339,20 @@ 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; // 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, @@ -351,24 +360,25 @@ module.exports.addErrorDetailIf = function addErrorDetailIf( "Expected: " + expected + "; Actual: " + actual + (detail ? "; " + detail : ""), context, - range); + range, + fixInfo); } }; // 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) { @@ -396,3 +406,127 @@ module.exports.frontMatterHasTitle = return !ignoreFrontMatter && 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 { + "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, lineEnding) { + const { editColumn, deleteCount, insertText } = normalizeFixInfo(fixInfo); + const editIndex = editColumn - 1; + return (deleteCount === -1) ? + null : + line.slice(0, editIndex) + + insertText.replace(/\n/g, lineEnding || "\n") + + 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 + .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); + 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) => { + const { lineNumber, editColumn, deleteCount } = fixInfo; + const lineIndex = lineNumber - 1; + const editIndex = editColumn - 1; + if ( + (lineIndex !== lastLineIndex) || + ((editIndex + deleteCount) < lastEditIndex) || + (deleteCount === -1) + ) { + lines[lineIndex] = applyFix(lines[lineIndex], fixInfo, lineEnding); + } + lastLineIndex = lineIndex; + lastEditIndex = editIndex; + }); + // Return corrected input + return lines.filter((line) => line !== null).join(lineEnding); +}; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 2b80a19a..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(); } }); } @@ -296,6 +305,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); @@ -377,11 +391,43 @@ 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, "context": errorInfo.context || null, - "range": errorInfo.range || null + "range": errorInfo.range || null, + "fixInfo": errorInfo.fixInfo || null }); } // Call (possibly external) rule function @@ -401,7 +447,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]; }) @@ -423,6 +471,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/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/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 55fd1ce8..5ebe08f8 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,23 @@ 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; + const length = match[0].length; + addError( + onError, + lineIndex + 1, + "Column: " + column, + null, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": "".padEnd(length) + }); + } } }); } diff --git a/lib/md011.js b/lib/md011.js index 2d8a855d..da0f1dc1 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -2,20 +2,36 @@ "use strict"; -const { addError, forEachInlineChild, rangeFromRegExp } = require("../helpers"); +const { addError, forEachInlineChild, unescapeMarkdown } = + 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 line = params.lines[lineNumber - 1]; + const column = unescapeMarkdown(line).indexOf(reversedLink) + 1; + const length = reversedLink.length; + addError( + onError, + lineNumber, + reversedLink, + null, + [ column, length ], + { + "editColumn": column, + "deleteCount": length, + "insertText": `[${linkText}](${linkDestination})` + } + ); } }); } 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/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..2ea87fb6 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,})(?:\S)/.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..db69c68f 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; + const rightEscapeReplacement = rightEscape ? `${rightEscape} ` : ""; + 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, + "deleteCount": line.length, + "insertText": + `${leftHash} ${content} ${rightEscapeReplacement}${rightHash}` + } + ); + } } } }); diff --git a/lib/md021.js b/lib/md021.js index d012f7be..5fa972c7 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, + "deleteCount": length, + "insertText": `${leftHash} ${content} ${rightHash}` + } + ); + } } } }); diff --git a/lib/md022.js b/lib/md022.js index 9cb9ee30..d62c5e9d 100644 --- a/lib/md022.js +++ b/lib/md022.js @@ -20,20 +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()); - return; + 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()); - return; + 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/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/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/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/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/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/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..264802e2 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].trimRight(); + 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].trimRight(); + addErrorContext( + onError, + lastIndex + 1, + line.trim(), + null, + null, + null, + { + "lineNumber": lastIndex + 2, + "insertText": `${quotePrefix}\n` + }); } }); } diff --git a/lib/md034.js b/lib/md034.js index 63fdd9f1..bc264773 100644 --- a/lib/md034.js +++ b/lib/md034.js @@ -18,15 +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 - ]; - addErrorContext(onError, lineNumber, bareUrl, null, null, range); + } 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/lib/md037.js b/lib/md037.js index 3f94d252..95cd2c31 100644 --- a/lib/md037.js +++ b/lib/md037.js @@ -4,29 +4,49 @@ 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 ]); + const { content, lineNumber } = token; + const columnsReported = []; + [ leftSpaceRe, rightSpaceRe ].forEach((spaceRe, index) => { + let match = null; + while ((match = spaceRe.exec(content)) !== null) { + const [ fullText, marker ] = match; + const line = params.lines[lineNumber - 1]; + if (line.includes(fullText)) { + const text = fullText.trim(); + const column = line.indexOf(text) + 1; + 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/lib/md038.js b/lib/md038.js index c8bbfab2..dd8bac28 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" ], @@ -22,22 +22,42 @@ 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 = 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; + 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, - context, left, right, [ rangeIndex + 1, rangeLength ]); + onError, + token.lineNumber + lineIndex + rangeLineOffset, + context, + left, + right, + [ rangeIndex + 1, rangeLength ], + { + "editColumn": fixIndex + 1, + "deleteCount": fixLength, + "insertText": fixText + } + ); } }); } diff --git a/lib/md039.js b/lib/md039.js index fb59d67f..f9464cf7 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,13 @@ 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 { children } = token; + let { lineNumber } = token; let inLink = false; let linkText = ""; - token.children.forEach(function forChild(child) { + let lineIndex = 0; + children.forEach((child) => { if (child.type === "link_open") { inLink = true; linkText = ""; @@ -24,10 +26,28 @@ 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 line = params.lines[lineNumber - 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, + `[${linkText}]`, + left, + right, + [ column, length ], + { + "editColumn": column + 1, + "deleteCount": length - 2, + "insertText": linkText.trim() + } + ); } + } else if (child.type === "softbreak") { + lineNumber++; + lineIndex = 0; } else if (inLink) { linkText += child.content; } 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/lib/md047.js b/lib/md047.js index 5d44b3a7..a001b27b 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, + [ lastLine.length, 1 ], + { + "insertText": "\n", + "editColumn": lastLine.length + 1 + } + ); } } }; 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/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/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} - {MD045} {MD047} \ No newline at end of file + {MD045} +## Heading 10 {MD022} + +EOF {MD047} \ No newline at end of file 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 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 new file mode 100644 index 00000000..407f771c --- /dev/null +++ b/test/detailed-results-MD011-MD021.md.fixed @@ -0,0 +1,25 @@ +# 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 D ## + +## 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" ], 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-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, 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-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/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 new file mode 100644 index 00000000..70e70654 --- /dev/null +++ b/test/detailed-results-MD031-MD040.md.fixed @@ -0,0 +1,62 @@ +```js +debugger; +``` + +* List + +Inline
+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 + +\
text
+
+Text
+
+