diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index cf36f566..98a4ced9 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -181,24 +181,6 @@ function isBlankLine(line) { } module.exports.isBlankLine = isBlankLine; -// Returns true iff the sorted array contains the specified element -module.exports.includesSorted = function includesSorted(array, element) { - let left = 0; - let right = array.length - 1; - while (left <= right) { - // eslint-disable-next-line no-bitwise - const mid = (left + right) >> 1; - if (array[mid] < element) { - left = mid + 1; - } else if (array[mid] > element) { - right = mid - 1; - } else { - return true; - } - } - return false; -}; - // Replaces the content of properly-formatted CommonMark comments with "." // This preserves the line/column information for the rest of the document // https://spec.commonmark.org/0.29/#html-blocks @@ -472,20 +454,6 @@ module.exports.flattenLists = function flattenLists(tokens) { return flattenedLists; }; -// Calls the provided function for each heading's content -module.exports.forEachHeading = function forEachHeading(params, handler) { - let heading = null; - for (const token of params.parsers.markdownit.tokens) { - if (token.type === "heading_open") { - heading = token; - } else if (token.type === "heading_close") { - heading = null; - } else if ((token.type === "inline") && heading) { - handler(heading, token.content, token); - } - } -}; - /** * @callback InlineCodeSpanCallback * @param {string} code Code content. @@ -1445,6 +1413,20 @@ function micromarkParse( ); } +/** + * Adds a range of numbers to a set. + * + * @param {Set} set Set of numbers. + * @param {number} start Starting number. + * @param {number} end Ending number. + * @returns {void} + */ +function addRangeToSet(set, start, end) { + for (let i = start; i <= end; i++) { + set.add(i); + } +} + /** * @callback AllowedPredicate * @param {Token} token Micromark token. @@ -1699,6 +1681,7 @@ const nonContentTokens = new Set([ module.exports = { "parse": micromarkParse, + addRangeToSet, filterByPredicate, filterByTypes, getDescendantsByType, @@ -3737,21 +3720,7 @@ module.exports = { const { addError } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); -const { filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"); - -/** - * Adds a range of numbers to a set. - * - * @param {Set} set Set of numbers. - * @param {number} start Starting number. - * @param {number} end Ending number. - * @returns {void} - */ -function addRangeToSet(set, start, end) { - for (let i = start; i <= end; i++) { - set.add(i); - } -} +const { addRangeToSet, filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"); // eslint-disable-next-line jsdoc/valid-types /** @type import("./markdownlint").Rule */ @@ -4061,25 +4030,14 @@ module.exports = { -const { addErrorDetailIf, filterTokens, forEachHeading, forEachLine, - includesSorted } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); -const { lineMetadata, referenceLinkImageData } = __webpack_require__(/*! ./cache */ "../lib/cache.js"); +const { addErrorDetailIf } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); +const { referenceLinkImageData } = __webpack_require__(/*! ./cache */ "../lib/cache.js"); +const { addRangeToSet, filterByTypes, getDescendantsByType } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"); const longLineRePrefix = "^.{"; const longLineRePostfixRelaxed = "}.*\\s.*$"; const longLineRePostfixStrict = "}.+$"; -const linkOrImageOnlyLineRe = /^[es]*(?:lT?L|I)[ES]*$/; const sternModeRe = /^(?:[#>\s]*\s)?\S*$/; -const tokenTypeMap = { - "em_open": "e", - "em_close": "E", - "image": "I", - "link_open": "l", - "link_close": "L", - "strong_open": "s", - "strong_close": "S", - "text": "T" -}; // eslint-disable-next-line jsdoc/valid-types /** @type import("./markdownlint").Rule */ @@ -4087,7 +4045,7 @@ module.exports = { "names": [ "MD013", "line-length" ], "description": "Line length", "tags": [ "line_length" ], - "parser": "markdownit", + "parser": "micromark", "function": function MD013(params, onError) { const lineLength = Number(params.config.line_length || 80); const headingLineLength = @@ -4110,26 +4068,42 @@ module.exports = { const includeTables = (tables === undefined) ? true : !!tables; const headings = params.config.headings; const includeHeadings = (headings === undefined) ? true : !!headings; - const headingLineNumbers = []; - forEachHeading(params, (heading) => { - headingLineNumbers.push(heading.lineNumber); - }); - const linkOnlyLineNumbers = []; - filterTokens(params, "inline", (token) => { - let childTokenTypes = ""; - for (const child of token.children) { - if (child.type !== "text" || child.content !== "") { - childTokenTypes += tokenTypeMap[child.type] || "x"; - } + const { tokens } = params.parsers.micromark; + const headingLineNumbers = new Set(); + for (const heading of filterByTypes(tokens, [ "atxHeading", "setextHeading" ])) { + addRangeToSet(headingLineNumbers, heading.startLine, heading.endLine); + } + const codeBlockLineNumbers = new Set(); + for (const codeBlock of filterByTypes(tokens, [ "codeFenced", "codeIndented" ])) { + addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine); + } + const tableLineNumbers = new Set(); + for (const table of filterByTypes(tokens, [ "table" ])) { + addRangeToSet(tableLineNumbers, table.startLine, table.endLine); + } + const linkLineNumbers = new Set(); + for (const link of filterByTypes(tokens, [ "autolink", "image", "link", "literalAutolink" ])) { + addRangeToSet(linkLineNumbers, link.startLine, link.endLine); + } + const paragraphDataLineNumbers = new Set(); + for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) { + for (const data of getDescendantsByType(paragraph, [ "data" ])) { + addRangeToSet(paragraphDataLineNumbers, data.startLine, data.endLine); } - if (linkOrImageOnlyLineRe.test(childTokenTypes)) { - linkOnlyLineNumbers.push(token.lineNumber); + } + const linkOnlyLineNumbers = new Set(); + for (const lineNumber of linkLineNumbers) { + if (!paragraphDataLineNumbers.has(lineNumber)) { + linkOnlyLineNumbers.add(lineNumber); } - }); - const { definitionLineIndices } = referenceLinkImageData(); - forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence, inTable) => { + } + const definitionLineIndices = new Set(referenceLinkImageData().definitionLineIndices); + for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) { + const line = params.lines[lineIndex]; const lineNumber = lineIndex + 1; - const isHeading = includesSorted(headingLineNumbers, lineNumber); + const isHeading = headingLineNumbers.has(lineNumber); + const inCode = codeBlockLineNumbers.has(lineNumber); + const inTable = tableLineNumbers.has(lineNumber); const length = inCode ? codeLineLength : (isHeading ? headingLineLength : lineLength); @@ -4139,10 +4113,10 @@ module.exports = { if ((includeCodeBlocks || !inCode) && (includeTables || !inTable) && (includeHeadings || !isHeading) && - !includesSorted(definitionLineIndices, lineIndex) && + !definitionLineIndices.has(lineIndex) && (strict || (!(stern && sternModeRe.test(line)) && - !includesSorted(linkOnlyLineNumbers, lineNumber))) && + !linkOnlyLineNumbers.has(lineNumber))) && lengthRe.test(line)) { addErrorDetailIf( onError, @@ -4151,9 +4125,10 @@ module.exports = { line.length, undefined, undefined, - [ length + 1, line.length - length ]); + [ length + 1, line.length - length ] + ); } - }); + } } }; diff --git a/helpers/helpers.js b/helpers/helpers.js index 6b0c5197..5a4f3e3b 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -169,24 +169,6 @@ function isBlankLine(line) { } module.exports.isBlankLine = isBlankLine; -// Returns true iff the sorted array contains the specified element -module.exports.includesSorted = function includesSorted(array, element) { - let left = 0; - let right = array.length - 1; - while (left <= right) { - // eslint-disable-next-line no-bitwise - const mid = (left + right) >> 1; - if (array[mid] < element) { - left = mid + 1; - } else if (array[mid] > element) { - right = mid - 1; - } else { - return true; - } - } - return false; -}; - // Replaces the content of properly-formatted CommonMark comments with "." // This preserves the line/column information for the rest of the document // https://spec.commonmark.org/0.29/#html-blocks @@ -460,20 +442,6 @@ module.exports.flattenLists = function flattenLists(tokens) { return flattenedLists; }; -// Calls the provided function for each heading's content -module.exports.forEachHeading = function forEachHeading(params, handler) { - let heading = null; - for (const token of params.parsers.markdownit.tokens) { - if (token.type === "heading_open") { - heading = token; - } else if (token.type === "heading_close") { - heading = null; - } else if ((token.type === "inline") && heading) { - handler(heading, token.content, token); - } - } -}; - /** * @callback InlineCodeSpanCallback * @param {string} code Code content. diff --git a/helpers/micromark.cjs b/helpers/micromark.cjs index e9f596b6..722b289e 100644 --- a/helpers/micromark.cjs +++ b/helpers/micromark.cjs @@ -231,6 +231,20 @@ function micromarkParse( ); } +/** + * Adds a range of numbers to a set. + * + * @param {Set} set Set of numbers. + * @param {number} start Starting number. + * @param {number} end Ending number. + * @returns {void} + */ +function addRangeToSet(set, start, end) { + for (let i = start; i <= end; i++) { + set.add(i); + } +} + /** * @callback AllowedPredicate * @param {Token} token Micromark token. @@ -485,6 +499,7 @@ const nonContentTokens = new Set([ module.exports = { "parse": micromarkParse, + addRangeToSet, filterByPredicate, filterByTypes, getDescendantsByType, diff --git a/lib/md009.js b/lib/md009.js index fd459625..8ae8e847 100644 --- a/lib/md009.js +++ b/lib/md009.js @@ -3,21 +3,7 @@ "use strict"; const { addError } = require("../helpers"); -const { filterByTypes } = require("../helpers/micromark.cjs"); - -/** - * Adds a range of numbers to a set. - * - * @param {Set} set Set of numbers. - * @param {number} start Starting number. - * @param {number} end Ending number. - * @returns {void} - */ -function addRangeToSet(set, start, end) { - for (let i = start; i <= end; i++) { - set.add(i); - } -} +const { addRangeToSet, filterByTypes } = require("../helpers/micromark.cjs"); // eslint-disable-next-line jsdoc/valid-types /** @type import("./markdownlint").Rule */ diff --git a/lib/md013.js b/lib/md013.js index 877325c9..62afae5d 100644 --- a/lib/md013.js +++ b/lib/md013.js @@ -2,25 +2,14 @@ "use strict"; -const { addErrorDetailIf, filterTokens, forEachHeading, forEachLine, - includesSorted } = require("../helpers"); -const { lineMetadata, referenceLinkImageData } = require("./cache"); +const { addErrorDetailIf } = require("../helpers"); +const { referenceLinkImageData } = require("./cache"); +const { addRangeToSet, filterByTypes, getDescendantsByType } = require("../helpers/micromark.cjs"); const longLineRePrefix = "^.{"; const longLineRePostfixRelaxed = "}.*\\s.*$"; const longLineRePostfixStrict = "}.+$"; -const linkOrImageOnlyLineRe = /^[es]*(?:lT?L|I)[ES]*$/; const sternModeRe = /^(?:[#>\s]*\s)?\S*$/; -const tokenTypeMap = { - "em_open": "e", - "em_close": "E", - "image": "I", - "link_open": "l", - "link_close": "L", - "strong_open": "s", - "strong_close": "S", - "text": "T" -}; // eslint-disable-next-line jsdoc/valid-types /** @type import("./markdownlint").Rule */ @@ -28,7 +17,7 @@ module.exports = { "names": [ "MD013", "line-length" ], "description": "Line length", "tags": [ "line_length" ], - "parser": "markdownit", + "parser": "micromark", "function": function MD013(params, onError) { const lineLength = Number(params.config.line_length || 80); const headingLineLength = @@ -51,26 +40,42 @@ module.exports = { const includeTables = (tables === undefined) ? true : !!tables; const headings = params.config.headings; const includeHeadings = (headings === undefined) ? true : !!headings; - const headingLineNumbers = []; - forEachHeading(params, (heading) => { - headingLineNumbers.push(heading.lineNumber); - }); - const linkOnlyLineNumbers = []; - filterTokens(params, "inline", (token) => { - let childTokenTypes = ""; - for (const child of token.children) { - if (child.type !== "text" || child.content !== "") { - childTokenTypes += tokenTypeMap[child.type] || "x"; - } + const { tokens } = params.parsers.micromark; + const headingLineNumbers = new Set(); + for (const heading of filterByTypes(tokens, [ "atxHeading", "setextHeading" ])) { + addRangeToSet(headingLineNumbers, heading.startLine, heading.endLine); + } + const codeBlockLineNumbers = new Set(); + for (const codeBlock of filterByTypes(tokens, [ "codeFenced", "codeIndented" ])) { + addRangeToSet(codeBlockLineNumbers, codeBlock.startLine, codeBlock.endLine); + } + const tableLineNumbers = new Set(); + for (const table of filterByTypes(tokens, [ "table" ])) { + addRangeToSet(tableLineNumbers, table.startLine, table.endLine); + } + const linkLineNumbers = new Set(); + for (const link of filterByTypes(tokens, [ "autolink", "image", "link", "literalAutolink" ])) { + addRangeToSet(linkLineNumbers, link.startLine, link.endLine); + } + const paragraphDataLineNumbers = new Set(); + for (const paragraph of filterByTypes(tokens, [ "paragraph" ])) { + for (const data of getDescendantsByType(paragraph, [ "data" ])) { + addRangeToSet(paragraphDataLineNumbers, data.startLine, data.endLine); } - if (linkOrImageOnlyLineRe.test(childTokenTypes)) { - linkOnlyLineNumbers.push(token.lineNumber); + } + const linkOnlyLineNumbers = new Set(); + for (const lineNumber of linkLineNumbers) { + if (!paragraphDataLineNumbers.has(lineNumber)) { + linkOnlyLineNumbers.add(lineNumber); } - }); - const { definitionLineIndices } = referenceLinkImageData(); - forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence, inTable) => { + } + const definitionLineIndices = new Set(referenceLinkImageData().definitionLineIndices); + for (let lineIndex = 0; lineIndex < params.lines.length; lineIndex++) { + const line = params.lines[lineIndex]; const lineNumber = lineIndex + 1; - const isHeading = includesSorted(headingLineNumbers, lineNumber); + const isHeading = headingLineNumbers.has(lineNumber); + const inCode = codeBlockLineNumbers.has(lineNumber); + const inTable = tableLineNumbers.has(lineNumber); const length = inCode ? codeLineLength : (isHeading ? headingLineLength : lineLength); @@ -80,10 +85,10 @@ module.exports = { if ((includeCodeBlocks || !inCode) && (includeTables || !inTable) && (includeHeadings || !isHeading) && - !includesSorted(definitionLineIndices, lineIndex) && + !definitionLineIndices.has(lineIndex) && (strict || (!(stern && sternModeRe.test(line)) && - !includesSorted(linkOnlyLineNumbers, lineNumber))) && + !linkOnlyLineNumbers.has(lineNumber))) && lengthRe.test(line)) { addErrorDetailIf( onError, @@ -92,8 +97,9 @@ module.exports = { line.length, undefined, undefined, - [ length + 1, line.length - length ]); + [ length + 1, line.length - length ] + ); } - }); + } } }; diff --git a/test/long_lines.md b/test/long_lines.md index b1bdefeb..c03dc248 100644 --- a/test/long_lines.md +++ b/test/long_lines.md @@ -72,3 +72,15 @@ Long lines inside HTML comments should also produce a violation of the line-leng Long lines inside HTML comments should also + + + +https://example.com/long-line-comprised-entirely-of-a-bare-link-long-line-comprised-entirely-of-a-bare-link + +Long {MD013} + +Long https://example.com/long-line-comprised-mostly-of-a-bare-link-long-line-comprised-mostly-of-a-bare-link {MD013} + + diff --git a/test/markdownlint-test-helpers.js b/test/markdownlint-test-helpers.js index d0663215..8741a5d5 100644 --- a/test/markdownlint-test-helpers.js +++ b/test/markdownlint-test-helpers.js @@ -214,24 +214,6 @@ test("isBlankLine", (t) => { } }); -test("includesSorted", (t) => { - t.plan(154); - const inputs = [ - [ ], - [ 8 ], - [ 7, 11 ], - [ 0, 1, 2, 3, 5, 8, 13 ], - [ 2, 3, 5, 7, 11, 13, 17, 19 ], - [ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 ], - [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ] - ]; - for (const input of inputs) { - for (let i = 0; i <= 21; i++) { - t.is(helpers.includesSorted(input, i), input.includes(i)); - } - } -}); - test("forEachInlineCodeSpan", (t) => { t.plan(99); const testCases = diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 16ff3fca..329dc3a5 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -945,7 +945,7 @@ test("readme", async(t) => { }); test("validateJsonUsingConfigSchemaStrict", async(t) => { - t.plan(178); + t.plan(179); // @ts-ignore const ajv = new Ajv(ajvOptions); const validateSchemaStrict = ajv.compile(configSchemaStrict); diff --git a/test/snapshots/markdownlint-test-scenarios.js.md b/test/snapshots/markdownlint-test-scenarios.js.md index b04891f0..5cd3d55a 100644 --- a/test/snapshots/markdownlint-test-scenarios.js.md +++ b/test/snapshots/markdownlint-test-scenarios.js.md @@ -37472,6 +37472,38 @@ Generated by [AVA](https://avajs.dev). 'line-length', ], }, + { + errorContext: null, + errorDetail: 'Expected: 80; Actual: 113', + errorRange: [ + 81, + 33, + ], + fixInfo: null, + lineNumber: 80, + ruleDescription: 'Line length', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md013.md', + ruleNames: [ + 'MD013', + 'line-length', + ], + }, + { + errorContext: null, + errorDetail: 'Expected: 80; Actual: 116', + errorRange: [ + 81, + 36, + ], + fixInfo: null, + lineNumber: 82, + ruleDescription: 'Line length', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md013.md', + ruleNames: [ + 'MD013', + 'line-length', + ], + }, { errorContext: null, errorDetail: 'Expected: indented; Actual: fenced', @@ -37736,6 +37768,18 @@ Generated by [AVA](https://avajs.dev). ␊ ␊ Long lines inside HTML comments should also ␊ + ␊ + ␊ + ␊ + https://example.com/long-line-comprised-entirely-of-a-bare-link-long-line-comprised-entirely-of-a-bare-link␊ + ␊ + Long {MD013}␊ + ␊ + Long https://example.com/long-line-comprised-mostly-of-a-bare-link-long-line-comprised-mostly-of-a-bare-link {MD013}␊ + ␊ + ␊ `, } diff --git a/test/snapshots/markdownlint-test-scenarios.js.snap b/test/snapshots/markdownlint-test-scenarios.js.snap index df4e947b..fa8113e7 100644 Binary files a/test/snapshots/markdownlint-test-scenarios.js.snap and b/test/snapshots/markdownlint-test-scenarios.js.snap differ