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} -![](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/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
HTML + +Bare link + +--- +*** + +*Emphasis* + +Space *inside* emphasis + +Space `inside` code span + +Space [inside](link) text + +``` +``` + +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 +some *space* in **some** emphasis +some _space_ in _some_ emphasis +some __space__ in __some__ emphasis + +Text +text `code +span` text +text. + +Text +text `code +span` text +text. + +* List + +--- + +Text +text ```code +span code +span code``` text +text +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, 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-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/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..a5742c4a --- /dev/null +++ b/test/detailed-results-blanks-around-headings-0-2.md.fixed @@ -0,0 +1,27 @@ +# 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..eb218781 --- /dev/null +++ b/test/detailed-results-blanks-around-headings-3-0.md.fixed @@ -0,0 +1,31 @@ +# 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 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 new file mode 100644 index 00000000..c7a5d40f --- /dev/null +++ b/test/detailed-results-links.md.fixed @@ -0,0 +1,28 @@ +# 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 + +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 ] } ] 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/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/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 + + + + +# + +# + +# + +# + +## + +## + +## + +## 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/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/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/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 30374fc1..224968da 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")(); @@ -34,10 +35,12 @@ function promisify(func, ...args) { function createTestForFile(file) { return function testForFile(test) { - test.expect(1); 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) .then( function configFileExists() { @@ -49,16 +52,56 @@ 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 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 = resultVersion2or3[file]; + result2or3.forEach(function forResult(result) { + const ruleName = result.ruleNames[0]; + const lineNumbers = result0[ruleName] || []; + if (!lineNumbers.includes(result.lineNumber)) { + lineNumbers.push(result.lineNumber); + } + result0[ruleName] = lineNumbers; + }); + return [ result0, result2or3 ]; + } + ); const expectedPromise = detailedResults ? promisify(fs.readFile, resultsFile, helpers.utf8Encoding) .then( @@ -96,11 +139,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 verifyFixes(errors) { + if (detailedResults) { + return test.ok(true); + } + return promisify(fs.readFile, file, helpers.utf8Encoding) + .then( + function applyFixes(content) { + const corrections = helpers.applyFixes(content, errors); + return promisify(markdownlint, { + "strings": { + "input": corrections + }, + "config": mergedConfig, + "resultVersion": 3 + }); + }) + .then( + function checkFixes(newErrors) { + const unfixed = newErrors.input + .filter((error) => !!error.fixInfo); + test.deepEqual(unfixed, [], "Fixable error was not fixed."); + } + ); }) .catch() .then(test.done); @@ -448,6 +515,242 @@ module.exports.resultFormattingV2 = function resultFormattingV2(test) { }); }; +module.exports.resultFormattingV3 = function resultFormattingV3(test) { + test.expect(3); + const options = { + "strings": { + "input": + "# Heading \n" + + "\n" + + "Text\ttext\t\ttext\n" + + "Text * emphasis * text" + }, + "resultVersion": 3 + }; + markdownlint(options, function callback(err, actualResult) { + test.ifError(err); + const expectedResult = { + "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": 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": [ 22, 1 ], + "fixInfo": { + "insertText": "\n", + "editColumn": 23 + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + const actualMessage = actualResult.toString(); + const expectedMessage = + "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: 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(); + }); +}; + +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": { + "editColumn": 10, + "deleteCount": 1, + "insertText": " " + } + }, + { + "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": { + "editColumn": 18, + "deleteCount": 2, + "insertText": " " + } + } + ] + }; + test.deepEqual(actualResult, expectedResult, "Undetected issues."); + test.done(); + }); + }; + module.exports.stringInputLineEndings = function stringInputLineEndings(test) { test.expect(2); const options = { @@ -455,11 +758,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 @@ -470,11 +769,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(); @@ -779,7 +1074,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 ], @@ -793,10 +1088,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 ], @@ -816,7 +1111,7 @@ module.exports.styleAll = function styleAll(test) { "MD042": [ 77 ], "MD045": [ 81 ], "MD046": [ 49, 73 ], - "MD047": [ 81 ] + "MD047": [ 84 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -836,7 +1131,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 ], @@ -844,10 +1139,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 ], @@ -857,7 +1152,7 @@ module.exports.styleRelaxed = function styleRelaxed(test) { "MD042": [ 77 ], "MD045": [ 81 ], "MD046": [ 49, 73 ], - "MD047": [ 81 ] + "MD047": [ 84 ] } }; test.deepEqual(actualResult, expectedResult, "Undetected issues."); @@ -1538,7 +1833,7 @@ module.exports.includesSorted = function includesSorted(test) { }; module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) { - test.expect(94); + test.expect(99); const testCases = [ [ @@ -1621,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) => { @@ -1638,6 +1937,522 @@ 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.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 = [ + [ + "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." + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 1, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "world" + ], + [ + "Hello\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "deleteCount": -1 + } + } + ], + "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, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + } + ], + "Hello zorld" + ], + [ + "Hello world", + [ + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "insertText": "z" + } + }, + { + "fixInfo": { + "lineNumber": 1, + "editColumn": 7, + "deleteCount": 1 + } + } + ], + "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" + ], + [ + "Hello\r\nworld", + [ + { + "lineNumber": 2, + "fixInfo": { + "editColumn": 6, + "insertText": "\n\n" + } + } + ], + "Hello\r\nworld\r\n\r\n" + ] + ]; + testCases.forEach((testCase) => { + const [ input, errors, expected ] = testCase; + const actual = helpers.applyFixes(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", @@ -2531,19 +3346,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": [ { @@ -2565,7 +3393,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."); @@ -2576,17 +3404,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": [ { @@ -2608,7 +3447,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."); @@ -2619,17 +3458,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": [ { diff --git a/test/reversed_link.md b/test/reversed_link.md index 547d4008..b6df2601 100644 --- a/test/reversed_link.md +++ b/test/reversed_link.md @@ -5,3 +5,30 @@ 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} + +`code code +code` +(reversed)[link] {MD011} + +text +text `code +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} 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} 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; +}