diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index 641c098e..3c12f285 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -519,6 +519,36 @@ module.exports.addErrorContext = function addErrorContext(onError, lineNumber, c } addError(onError, lineNumber, null, context, range, fixInfo); }; +/** + * Returns an array of code span ranges. + * + * @param {string[]} lines Lines to scan for code span ranges. + * @returns {number[][]} Array of ranges (line, index, length). + */ +module.exports.inlineCodeSpanRanges = function (lines) { + var exclusions = []; + forEachInlineCodeSpan(lines.join("\n"), function (code, lineIndex, columnIndex) { + var codeLines = code.split(newLineRe); + // eslint-disable-next-line unicorn/no-for-loop + for (var i = 0; i < codeLines.length; i++) { + exclusions.push([lineIndex + i, columnIndex, codeLines[i].length]); + columnIndex = 0; + } + }); + return exclusions; +}; +/** + * Determines whether the specified range overlaps another range. + * + * @param {number[][]} ranges Array of ranges (line, index, length). + * @param {number} lineIndex Line index to check. + * @param {number} index Index to check. + * @param {number} length Length to check. + * @returns {boolean} True iff the specified range overlaps. + */ +module.exports.overlapsAnyRange = function (ranges, lineIndex, index, length) { return (!ranges.every(function (span) { return ((lineIndex !== span[0]) || + (index + length < span[1]) || + (index > span[1] + span[2])); })); }; // Returns a range object for a line by applying a RegExp module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) { var range = null; @@ -2177,30 +2207,30 @@ module.exports = { "use strict"; // @ts-check -var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachInlineChild = _a.forEachInlineChild, unescapeMarkdown = _a.unescapeMarkdown; -var reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g; +var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, inlineCodeSpanRanges = _a.inlineCodeSpanRanges, overlapsAnyRange = _a.overlapsAnyRange; +var lineMetadata = __webpack_require__(/*! ./cache */ "../lib/cache.js").lineMetadata; +var reversedLinkRe = /(? span[1] + span[2])); })) { + if (!overlapsAnyRange(exclusions, lineIndex, index, length_1)) { addErrorDetailIf(onError, lineIndex + 1, name_1, nameMatch, null, null, [index + 1, length_1], { "editColumn": index + 1, "deleteCount": length_1, @@ -3811,9 +3832,6 @@ module.exports = { }); } exclusions.push([lineIndex, index, length_1]); - }; - while ((match = nameRe.exec(line)) !== null) { - _loop_2(); } } }); diff --git a/helpers/helpers.js b/helpers/helpers.js index 5c6f58e6..e0f13406 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -523,6 +523,47 @@ module.exports.addErrorContext = function addErrorContext( addError(onError, lineNumber, null, context, range, fixInfo); }; +/** + * Returns an array of code span ranges. + * + * @param {string[]} lines Lines to scan for code span ranges. + * @returns {number[][]} Array of ranges (line, index, length). + */ +module.exports.inlineCodeSpanRanges = (lines) => { + const exclusions = []; + forEachInlineCodeSpan( + lines.join("\n"), + (code, lineIndex, columnIndex) => { + const codeLines = code.split(newLineRe); + // eslint-disable-next-line unicorn/no-for-loop + for (let i = 0; i < codeLines.length; i++) { + exclusions.push( + [ lineIndex + i, columnIndex, codeLines[i].length ] + ); + columnIndex = 0; + } + } + ); + return exclusions; +}; + +/** + * Determines whether the specified range overlaps another range. + * + * @param {number[][]} ranges Array of ranges (line, index, length). + * @param {number} lineIndex Line index to check. + * @param {number} index Index to check. + * @param {number} length Length to check. + * @returns {boolean} True iff the specified range overlaps. + */ +module.exports.overlapsAnyRange = (ranges, lineIndex, index, length) => ( + !ranges.every((span) => ( + (lineIndex !== span[0]) || + (index + length < span[1]) || + (index > span[1] + span[2]) + )) +); + // Returns a range object for a line by applying a RegExp module.exports.rangeFromRegExp = function rangeFromRegExp(line, regexp) { let range = null; diff --git a/lib/md011.js b/lib/md011.js index 37941d17..ff794e74 100644 --- a/lib/md011.js +++ b/lib/md011.js @@ -2,40 +2,41 @@ "use strict"; -const { addError, forEachInlineChild, unescapeMarkdown } = +const { addError, forEachLine, inlineCodeSpanRanges, overlapsAnyRange } = require("../helpers"); +const { lineMetadata } = require("./cache"); -const reversedLinkRe = /\(([^)]+)\)\[([^\]^][^\]]*)]/g; +const reversedLinkRe = + /(? { - 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; - const range = column ? [ column, length ] : null; - const fixInfo = column ? - { - "editColumn": column, - "deleteCount": length, - "insertText": `[${linkText}](${linkDestination})` - } : - null; - addError( - onError, - lineNumber, - reversedLink, - null, - range, - fixInfo - ); + const exclusions = inlineCodeSpanRanges(params.lines); + forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => { + if (!inCode && !onFence) { + let match = null; + while ((match = reversedLinkRe.exec(line)) !== null) { + const [ reversedLink, linkText, linkDestination ] = match; + const index = match.index; + const length = match[0].length; + if (!overlapsAnyRange(exclusions, lineIndex, index, length)) { + addError( + onError, + lineIndex + 1, + reversedLink, + null, + [ index + 1, length ], + { + "editColumn": index + 1, + "deleteCount": length, + "insertText": `[${linkText}](${linkDestination})` + } + ); + } + } } }); } diff --git a/lib/md044.js b/lib/md044.js index af3b1ce7..365b4aeb 100644 --- a/lib/md044.js +++ b/lib/md044.js @@ -2,8 +2,9 @@ "use strict"; -const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine, linkRe, - linkReferenceRe, newLineRe, forEachInlineCodeSpan } = require("../helpers"); +const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine, + inlineCodeSpanRanges, overlapsAnyRange, linkRe, linkReferenceRe } = + require("../helpers"); const { lineMetadata } = require("./cache"); module.exports = { @@ -36,19 +37,7 @@ module.exports = { } }); if (!includeCodeBlocks) { - forEachInlineCodeSpan( - params.lines.join("\n"), - (code, lineIndex, columnIndex) => { - const codeLines = code.split(newLineRe); - // eslint-disable-next-line unicorn/no-for-loop - for (let i = 0; i < codeLines.length; i++) { - exclusions.push( - [ lineIndex + i, columnIndex, codeLines[i].length ] - ); - columnIndex = 0; - } - } - ); + exclusions.push(...inlineCodeSpanRanges(params.lines)); } for (const name of names) { const escapedName = escapeForRegExp(name); @@ -64,13 +53,7 @@ module.exports = { const [ , leftMatch, nameMatch ] = match; const index = match.index + leftMatch.length; const length = nameMatch.length; - if ( - exclusions.every((span) => ( - (lineIndex !== span[0]) || - (index + length < span[1]) || - (index > span[1] + span[2]) - )) - ) { + if (!overlapsAnyRange(exclusions, lineIndex, index, length)) { addErrorDetailIf( onError, lineIndex + 1, diff --git a/test/reversed-link-issue-with-markdownlint-12.md b/test/reversed-link-issue-with-markdownlint-12.md index bd78a80c..2383d467 100644 --- a/test/reversed-link-issue-with-markdownlint-12.md +++ b/test/reversed-link-issue-with-markdownlint-12.md @@ -4,8 +4,6 @@ |-------------|-----------------| |`(?:\["'\](?<1>\[^"'\]*)["']|(?<1>\S+))`|...| -{MD011:5} - |Pattern|Description| |-------------|-----------------| |`(?:\["'\](?<1>\[^"'\]*)["']\|(?<1>\S+))`|...| diff --git a/test/reversed_link.md b/test/reversed_link.md index d27d69ea..e71c9725 100644 --- a/test/reversed_link.md +++ b/test/reversed_link.md @@ -1,17 +1,26 @@ # reversed_link +Go to [this website](https://www.example.com) + Go to (this website)[https://www.example.com] {MD011} +Go to (this)[website](https://www.example.com) + However, this shouldn't trigger inside code blocks: myObj.getFiles("test")[0] +Nor code fences: + +```js +myObj.getFiles(test)[0]; +``` + Nor inline code: `myobj.getFiles("test")[0]` Two (issues)[https://www.example.com/one] in {MD011} the (same text)[https://www.example.com/two]. {MD011} - Two (issues)[https://www.example.com/three] on the (same line)[https://www.example.com/four]. {MD011} `code code @@ -31,16 +40,40 @@ var IDENT_RE = '([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*'; {MD011} begin: /\B(([\/.])[\w\-.\/=]+)+/, {MD011} -{begin: '%r\\(', end: '\\)[a-z]*'} {MD011} +{begin: '%r\\(', end: '\\)[a-z]*'} return /(?:(?:(^|\/)[!.])|[*?+()|\[\]{}]|[+@]\()/.test(str); {MD011} ## Escaped Parens -(reversed)[link] {MD011} +(reversed)[link] -a ) a ( a )[a]~ {MD011} +a ) a ( a )[a]~ - +a
) a ( a )[a]~
-a
) a ( a )[a]~
{MD011} +## Backslash Escapes + +xxx(xxx)[xxx] {MD011} + +xxx\(xxx)[xxx] + +xxx(xxx\)[xxx] + +xxx(xxx)\[xxx] + +xxx(xxx)[xxx\] + +## Consecutive Links + +text [link](destination) text [link](destination) text +text [link](destination)[link](destination) text +text [link](destination)[link](destination)[link](destination) text + +text (reversed)[link] text (reversed)[link] text {MD011} + +