diff --git a/.eslintrc.json b/.eslintrc.json index 231a99a6..d8994768 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -223,6 +223,7 @@ "unicorn/no-new-array": "off", "unicorn/no-null": "off", "unicorn/no-unsafe-regex": "off", + "unicorn/no-useless-undefined": "off", "unicorn/prefer-at": "off", "unicorn/prefer-module": "off", "unicorn/prefer-string-replace-all": "off", diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index a5fcc78c..d6a7884d 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -60,10 +60,6 @@ var inlineCommentStartRe = /()/gi; module.exports.inlineCommentStartRe = inlineCommentStartRe; -// Regular expression for matching HTML elements -var htmlElementRe = /<(([A-Za-z][A-Za-z\d-]*)(?:\s[^`>]*)?)\/?>/g; -module.exports.htmlElementRe = htmlElementRe; - // Regular expressions for range matching module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/; module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/; @@ -495,42 +491,14 @@ module.exports.flattenLists = function flattenLists(tokens) { return flattenedLists; }; -/** - * Calls the provided function for each specified inline child token. - * - * @param {Object} params RuleParams instance. - * @param {string} type Token type identifier. - * @param {Function} handler Callback function. - * @returns {void} - */ -function forEachInlineChild(params, type, handler) { - filterTokens(params, "inline", function (token) { - var _iterator4 = _createForOfIteratorHelper(token.children.filter(function (c) { - return c.type === type; - })), - _step4; - try { - for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { - var child = _step4.value; - handler(child, token); - } - } catch (err) { - _iterator4.e(err); - } finally { - _iterator4.f(); - } - }); -} -module.exports.forEachInlineChild = forEachInlineChild; - // Calls the provided function for each heading's content module.exports.forEachHeading = function forEachHeading(params, handler) { var heading = null; - var _iterator5 = _createForOfIteratorHelper(params.parsers.markdownit.tokens), - _step5; + var _iterator4 = _createForOfIteratorHelper(params.parsers.markdownit.tokens), + _step4; try { - for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { - var token = _step5.value; + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var token = _step4.value; if (token.type === "heading_open") { heading = token; } else if (token.type === "heading_close") { @@ -540,9 +508,9 @@ module.exports.forEachHeading = function forEachHeading(params, handler) { } } } catch (err) { - _iterator5.e(err); + _iterator4.e(err); } finally { - _iterator5.f(); + _iterator4.f(); } }; @@ -677,19 +645,19 @@ module.exports.codeBlockAndSpanRanges = function (params, lineMetadata) { var tokenLines = params.lines.slice(token.map[0], token.map[1]); forEachInlineCodeSpan(tokenLines.join("\n"), function (code, lineIndex, columnIndex) { var codeLines = code.split(newLineRe); - var _iterator6 = _createForOfIteratorHelper(codeLines.entries()), - _step6; + var _iterator5 = _createForOfIteratorHelper(codeLines.entries()), + _step5; try { - for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { - var _step6$value = _slicedToArray(_step6.value, 2), - i = _step6$value[0], - line = _step6$value[1]; + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _step5$value = _slicedToArray(_step5.value, 2), + i = _step5$value[0], + line = _step5$value[1]; exclusions.push([token.lineNumber - 1 + lineIndex + i, i ? 0 : columnIndex, line.length]); } } catch (err) { - _iterator6.e(err); + _iterator5.e(err); } finally { - _iterator6.f(); + _iterator5.f(); } }); } @@ -756,11 +724,11 @@ function getReferenceLinkImageData(params) { "definitionLabelString", "gfmFootnoteDefinitionLabelString", // references and shortcuts "gfmFootnoteCall", "image", "link"]); - var _iterator7 = _createForOfIteratorHelper(filteredTokens), - _step7; + var _iterator6 = _createForOfIteratorHelper(filteredTokens), + _step6; try { - for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { - var token = _step7.value; + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var token = _step6.value; var labelPrefix = ""; // eslint-disable-next-line default-case switch (token.type) { @@ -827,9 +795,9 @@ function getReferenceLinkImageData(params) { } } } catch (err) { - _iterator7.e(err); + _iterator6.e(err); } finally { - _iterator7.f(); + _iterator6.f(); } return { references: references, @@ -853,11 +821,11 @@ function getPreferredLineEnding(input, os) { var lf = 0; var crlf = 0; var endings = input.match(newLineRe) || []; - var _iterator8 = _createForOfIteratorHelper(endings), - _step8; + var _iterator7 = _createForOfIteratorHelper(endings), + _step7; try { - for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { - var ending = _step8.value; + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var ending = _step7.value; // eslint-disable-next-line default-case switch (ending) { case "\r": @@ -872,9 +840,9 @@ function getPreferredLineEnding(input, os) { } } } catch (err) { - _iterator8.e(err); + _iterator7.e(err); } finally { - _iterator8.f(); + _iterator7.f(); } var preferredLineEnding = null; if (!cr && !lf && !crlf) { @@ -957,11 +925,11 @@ function applyFixes(input, errors) { lastFixInfo = { "lineNumber": -1 }; - var _iterator9 = _createForOfIteratorHelper(fixInfos), - _step9; + var _iterator8 = _createForOfIteratorHelper(fixInfos), + _step8; try { - for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) { - var fixInfo = _step9.value; + for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { + var fixInfo = _step8.value; 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; @@ -969,9 +937,9 @@ function applyFixes(input, errors) { lastFixInfo = fixInfo; } } catch (err) { - _iterator9.e(err); + _iterator8.e(err); } finally { - _iterator9.f(); + _iterator8.f(); } fixInfos = fixInfos.filter(function (fixInfo) { return fixInfo.lineNumber; @@ -979,11 +947,11 @@ function applyFixes(input, errors) { // Apply all (remaining/updated) fixes var lastLineIndex = -1; var lastEditIndex = -1; - var _iterator10 = _createForOfIteratorHelper(fixInfos), - _step10; + var _iterator9 = _createForOfIteratorHelper(fixInfos), + _step9; try { - for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) { - var _fixInfo = _step10.value; + for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) { + var _fixInfo = _step9.value; var lineNumber = _fixInfo.lineNumber, editColumn = _fixInfo.editColumn, deleteCount = _fixInfo.deleteCount; @@ -998,9 +966,9 @@ function applyFixes(input, errors) { } // Return corrected input } catch (err) { - _iterator10.e(err); + _iterator9.e(err); } finally { - _iterator10.f(); + _iterator9.f(); } return lines.filter(function (line) { return line !== null; @@ -1348,6 +1316,39 @@ function filterByTypes(tokens, allowed) { }); } +/** + * Filter a list of Micromark tokens for HTML tokens. + * + * @param {Token[]} tokens Micromark tokens. + * @returns {Token[]} Filtered tokens. + */ +function filterByHtmlTokens(tokens) { + var result = []; + var pending = [tokens]; + var current = null; + while (current = pending.shift()) { + var _iterator2 = _createForOfIteratorHelper(filterByTypes(current, ["htmlFlow", "htmlText"])), + _step2; + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var token = _step2.value; + if (token.type === "htmlText") { + result.push(token); + } else { + // token.type === "htmlFlow" + // @ts-ignore + pending.push(token.htmlFlowChildren); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + } + return result; +} + /** * Returns a list of all nested child tokens. * @@ -1438,6 +1439,7 @@ function tokenIfType(token, type) { } module.exports = { "parse": micromarkParse, + filterByHtmlTokens: filterByHtmlTokens, filterByPredicate: filterByPredicate, filterByTypes: filterByTypes, flattenedChildren: flattenedChildren, @@ -4855,7 +4857,7 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len var _require = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _require.addError; var _require2 = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"), - filterByTypes = _require2.filterByTypes, + filterByHtmlTokens = _require2.filterByHtmlTokens, getHtmlTagInfo = _require2.getHtmlTagInfo; var nextLinesRe = /[\r\n][\s\S]*$/; module.exports = { @@ -4868,31 +4870,21 @@ module.exports = { allowedElements = allowedElements.map(function (element) { return element.toLowerCase(); }); - var pending = [params.parsers.micromark.tokens]; - var current = null; - while (current = pending.shift()) { - var tokens = current; - var _iterator = _createForOfIteratorHelper(filterByTypes(tokens, ["htmlFlow", "htmlText"])), - _step; - try { - for (_iterator.s(); !(_step = _iterator.n()).done;) { - var token = _step.value; - if (token.type === "htmlText") { - var htmlTagInfo = getHtmlTagInfo(token); - if (htmlTagInfo && !htmlTagInfo.close && !allowedElements.includes(htmlTagInfo.name.toLowerCase())) { - var range = [token.startColumn, token.text.replace(nextLinesRe, "").length]; - addError(onError, token.startLine, "Element: " + htmlTagInfo.name, undefined, range); - } - } else { - // token.type === "htmlFlow" - pending.push(token.htmlFlowChildren); - } + var _iterator = _createForOfIteratorHelper(filterByHtmlTokens(params.parsers.micromark.tokens)), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var token = _step.value; + var htmlTagInfo = getHtmlTagInfo(token); + if (htmlTagInfo && !htmlTagInfo.close && !allowedElements.includes(htmlTagInfo.name.toLowerCase())) { + var range = [token.startColumn, token.text.replace(nextLinesRe, "").length]; + addError(onError, token.startLine, "Element: " + htmlTagInfo.name, undefined, range); } - } catch (err) { - _iterator.e(err); - } finally { - _iterator.f(); } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); } } }; @@ -6098,18 +6090,18 @@ function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symb function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } -function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i["return"] && (_r = _i["return"](), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } var _require = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _require.addError, - addErrorDetailIf = _require.addErrorDetailIf, - escapeForRegExp = _require.escapeForRegExp, - filterTokens = _require.filterTokens, - forEachInlineChild = _require.forEachInlineChild, - forEachHeading = _require.forEachHeading, - htmlElementRe = _require.htmlElementRe; + addErrorDetailIf = _require.addErrorDetailIf; +var _require2 = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"), + filterByHtmlTokens = _require2.filterByHtmlTokens, + filterByTypes = _require2.filterByTypes, + getHtmlTagInfo = _require2.getHtmlTagInfo; // Regular expression for identifying HTML anchor names var idRe = /[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]id[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*=[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*["']?((?:(?![\t-\r "'>\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uD800-\uDFFF\uFEFF])[\s\S]|[\uD800-\uDBFF][\uDC00-\uDFFF])+)/i; @@ -6120,14 +6112,12 @@ var anchorRe = /\{(#[0-9a-z]+(?:[\x2D_][0-9a-z]+)*)\}/g; * Converts a Markdown heading into an HTML fragment according to the rules * used by GitHub. * - * @param {Object} inline Inline token for heading. + * @param {Object} headingText Heading text token. * @returns {string} Fragment string for heading. */ -function convertHeadingToHTMLFragment(inline) { - var inlineText = inline.children.filter(function (token) { - return token.type !== "html_inline"; - }).map(function (token) { - return token.content; +function convertHeadingToHTMLFragment(headingText) { + var inlineText = filterByTypes(headingText.children, ["codeTextData", "data"]).map(function (token) { + return token.text; }).join(""); return "#" + encodeURIComponent(inlineText.toLowerCase() // RegExp source with Ruby's \p{Word} expanded into its General Categories @@ -6141,81 +6131,116 @@ module.exports = { "description": "Link fragments should be valid", "tags": ["links"], "function": function MD051(params, onError) { + var tokens = params.parsers.micromark.tokens; var fragments = new Map(); + // Process headings - forEachHeading(params, function (heading, content, inline) { - var fragment = convertHeadingToHTMLFragment(inline); - var count = fragments.get(fragment) || 0; - if (count) { - fragments.set("".concat(fragment, "-").concat(count), 0); - } - fragments.set(fragment, count + 1); - var match = null; - while ((match = anchorRe.exec(content)) !== null) { - var _match = match, - _match2 = _slicedToArray(_match, 2), - anchor = _match2[1]; - if (!fragments.has(anchor)) { - fragments.set(anchor, 1); + var headingTexts = filterByTypes(tokens, ["atxHeadingText", "setextHeadingText"]); + var _iterator = _createForOfIteratorHelper(headingTexts), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var headingText = _step.value; + var fragment = convertHeadingToHTMLFragment(headingText); + var count = fragments.get(fragment) || 0; + if (count) { + fragments.set("".concat(fragment, "-").concat(count), 0); + } + fragments.set(fragment, count + 1); + var match = null; + while ((match = anchorRe.exec(headingText.text)) !== null) { + var _match = match, + _match2 = _slicedToArray(_match, 2), + anchor = _match2[1]; + if (!fragments.has(anchor)) { + fragments.set(anchor, 1); + } } } - }); - // Process HTML anchors - var processHtmlToken = function processHtmlToken(token) { - var match = null; - while ((match = htmlElementRe.exec(token.content)) !== null) { - var _match3 = match, - _match4 = _slicedToArray(_match3, 3), - tag = _match4[0], - element = _match4[2]; - var anchorMatch = idRe.exec(tag) || element.toLowerCase() === "a" && nameRe.exec(tag); - if (anchorMatch) { - fragments.set("#".concat(anchorMatch[1]), 0); + + // Process HTML anchors + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + var _iterator2 = _createForOfIteratorHelper(filterByHtmlTokens(tokens)), + _step2; + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var token = _step2.value; + var htmlTagInfo = getHtmlTagInfo(token); + if (htmlTagInfo && !htmlTagInfo.close) { + var anchorMatch = idRe.exec(token.text) || htmlTagInfo.name.toLowerCase() === "a" && nameRe.exec(token.text); + if (anchorMatch) { + fragments.set("#".concat(anchorMatch[1]), 0); + } } } - }; - filterTokens(params, "html_block", processHtmlToken); - forEachInlineChild(params, "html_inline", processHtmlToken); - // Process link fragments - forEachInlineChild(params, "link_open", function (token) { - var attrs = token.attrs, - lineNumber = token.lineNumber, - line = token.line; - var href = attrs.find(function (attr) { - return attr[0] === "href"; - }); - var id = href && href[1]; - if (id && id.length > 1 && id[0] === "#" && !fragments.has(id)) { - var context = id; - var range = null; - var fixInfo = null; - var match = line.match(new RegExp("\\[.*?\\]\\(".concat(escapeForRegExp(context), "\\)"))); - if (match) { - var _match5 = _slicedToArray(match, 1); - context = _match5[0]; - var index = match.index; - var length = context.length; - range = [index + 1, length]; - fixInfo = { - "editColumn": index + (length - id.length), - "deleteCount": id.length, - "insertText": null - }; - } - var idLower = id.toLowerCase(); - var mixedCaseKey = _toConsumableArray(fragments.keys()).find(function (key) { - return idLower === key.toLowerCase(); - }); - if (mixedCaseKey) { - (fixInfo || {}).insertText = mixedCaseKey; - addErrorDetailIf(onError, lineNumber, mixedCaseKey, id, undefined, context, range, fixInfo); - } else { - addError(onError, lineNumber, undefined, context, - // @ts-ignore - range); + + // Process link and definition fragments + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + var parentChilds = [["link", "resourceDestinationString"], ["definition", "definitionDestinationString"]]; + for (var _i = 0, _parentChilds = parentChilds; _i < _parentChilds.length; _i++) { + var _parentChilds$_i = _slicedToArray(_parentChilds[_i], 2), + parentType = _parentChilds$_i[0], + definitionType = _parentChilds$_i[1]; + var links = filterByTypes(tokens, [parentType]); + var _iterator3 = _createForOfIteratorHelper(links), + _step3; + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var link = _step3.value; + var definitions = filterByTypes(link.children, [definitionType]); + var _iterator4 = _createForOfIteratorHelper(definitions), + _step4; + try { + var _loop = function _loop() { + var definition = _step4.value; + if (definition.text.length > 1 && definition.text.startsWith("#") && !fragments.has(definition.text)) { + // eslint-disable-next-line no-undef-init + var range = undefined; + // eslint-disable-next-line no-undef-init + var fixInfo = undefined; + if (link.startLine === link.endLine) { + range = [link.startColumn, link.endColumn - link.startColumn]; + fixInfo = { + "editColumn": definition.startColumn, + "deleteCount": definition.endColumn - definition.startColumn + }; + } + var definitionTextLower = definition.text.toLowerCase(); + var mixedCaseKey = _toConsumableArray(fragments.keys()).find(function (key) { + return definitionTextLower === key.toLowerCase(); + }); + if (mixedCaseKey) { + // @ts-ignore + (fixInfo || {}).insertText = mixedCaseKey; + addErrorDetailIf(onError, link.startLine, mixedCaseKey, definition.text, undefined, link.text, range, fixInfo); + } else { + addError(onError, link.startLine, undefined, link.text, range); + } + } + }; + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + _loop(); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); } - }); + } } }; diff --git a/doc-build/md051.md b/doc-build/md051.md index ff56ea6b..fad20ef4 100644 --- a/doc-build/md051.md +++ b/doc-build/md051.md @@ -26,8 +26,8 @@ letters, numbers, `-`, and `_`): [Link](#custom-name) ``` -Alternatively, an HTML `a` tag with an `id` or a `name` attribute can be used to -define a fragment: +Alternatively, any HTML tag with an `id` attribute or an `a` tag with a `name` +attribute can be used to define a fragment: ```markdown diff --git a/doc/Rules.md b/doc/Rules.md index ecb5f183..185646c3 100644 --- a/doc/Rules.md +++ b/doc/Rules.md @@ -2176,8 +2176,8 @@ letters, numbers, `-`, and `_`): [Link](#custom-name) ``` -Alternatively, an HTML `a` tag with an `id` or a `name` attribute can be used to -define a fragment: +Alternatively, any HTML tag with an `id` attribute or an `a` tag with a `name` +attribute can be used to define a fragment: ```markdown diff --git a/doc/md051.md b/doc/md051.md index 4cae1669..c39a1853 100644 --- a/doc/md051.md +++ b/doc/md051.md @@ -34,8 +34,8 @@ letters, numbers, `-`, and `_`): [Link](#custom-name) ``` -Alternatively, an HTML `a` tag with an `id` or a `name` attribute can be used to -define a fragment: +Alternatively, any HTML tag with an `id` attribute or an `a` tag with a `name` +attribute can be used to define a fragment: ```markdown diff --git a/helpers/helpers.js b/helpers/helpers.js index 3aebdcc3..b50f0154 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -18,10 +18,6 @@ const inlineCommentStartRe = /()/gi; module.exports.inlineCommentStartRe = inlineCommentStartRe; -// Regular expression for matching HTML elements -const htmlElementRe = /<(([A-Za-z][A-Za-z\d-]*)(?:\s[^`>]*)?)\/?>/g; -module.exports.htmlElementRe = htmlElementRe; - // Regular expressions for range matching module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/; module.exports.orderedListItemMarkerRe = /^[\s>]*0*(\d+)[.)]/; @@ -444,23 +440,6 @@ module.exports.flattenLists = function flattenLists(tokens) { return flattenedLists; }; -/** - * Calls the provided function for each specified inline child token. - * - * @param {Object} params RuleParams instance. - * @param {string} type Token type identifier. - * @param {Function} handler Callback function. - * @returns {void} - */ -function forEachInlineChild(params, type, handler) { - filterTokens(params, "inline", (token) => { - for (const child of token.children.filter((c) => c.type === type)) { - handler(child, token); - } - }); -} -module.exports.forEachInlineChild = forEachInlineChild; - // Calls the provided function for each heading's content module.exports.forEachHeading = function forEachHeading(params, handler) { let heading = null; diff --git a/helpers/micromark.cjs b/helpers/micromark.cjs index bcb372e9..8bc8a746 100644 --- a/helpers/micromark.cjs +++ b/helpers/micromark.cjs @@ -204,6 +204,30 @@ function filterByTypes(tokens, allowed) { ); } +/** + * Filter a list of Micromark tokens for HTML tokens. + * + * @param {Token[]} tokens Micromark tokens. + * @returns {Token[]} Filtered tokens. + */ +function filterByHtmlTokens(tokens) { + const result = []; + const pending = [ tokens ]; + let current = null; + while ((current = pending.shift())) { + for (const token of filterByTypes(current, [ "htmlFlow", "htmlText" ])) { + if (token.type === "htmlText") { + result.push(token); + } else { + // token.type === "htmlFlow" + // @ts-ignore + pending.push(token.htmlFlowChildren); + } + } + } + return result; +} + /** * Returns a list of all nested child tokens. * @@ -293,6 +317,7 @@ function tokenIfType(token, type) { module.exports = { "parse": micromarkParse, + filterByHtmlTokens, filterByPredicate, filterByTypes, flattenedChildren, diff --git a/lib/md033.js b/lib/md033.js index 4dc39c08..08bbfbfe 100644 --- a/lib/md033.js +++ b/lib/md033.js @@ -3,7 +3,8 @@ "use strict"; const { addError } = require("../helpers"); -const { filterByTypes, getHtmlTagInfo } = require("../helpers/micromark.cjs"); +const { filterByHtmlTokens, getHtmlTagInfo } = + require("../helpers/micromark.cjs"); const nextLinesRe = /[\r\n][\s\S]*$/; @@ -15,34 +16,24 @@ module.exports = { let allowedElements = params.config.allowed_elements; allowedElements = Array.isArray(allowedElements) ? allowedElements : []; allowedElements = allowedElements.map((element) => element.toLowerCase()); - const pending = [ params.parsers.micromark.tokens ]; - let current = null; - while ((current = pending.shift())) { - const tokens = current; - for (const token of filterByTypes(tokens, [ "htmlFlow", "htmlText" ])) { - if (token.type === "htmlText") { - const htmlTagInfo = getHtmlTagInfo(token); - if ( - htmlTagInfo && - !htmlTagInfo.close && - !allowedElements.includes(htmlTagInfo.name.toLowerCase()) - ) { - const range = [ - token.startColumn, - token.text.replace(nextLinesRe, "").length - ]; - addError( - onError, - token.startLine, - "Element: " + htmlTagInfo.name, - undefined, - range - ); - } - } else { - // token.type === "htmlFlow" - pending.push(token.htmlFlowChildren); - } + for (const token of filterByHtmlTokens(params.parsers.micromark.tokens)) { + const htmlTagInfo = getHtmlTagInfo(token); + if ( + htmlTagInfo && + !htmlTagInfo.close && + !allowedElements.includes(htmlTagInfo.name.toLowerCase()) + ) { + const range = [ + token.startColumn, + token.text.replace(nextLinesRe, "").length + ]; + addError( + onError, + token.startLine, + "Element: " + htmlTagInfo.name, + undefined, + range + ); } } } diff --git a/lib/md051.js b/lib/md051.js index 24b80996..c1ed1393 100644 --- a/lib/md051.js +++ b/lib/md051.js @@ -2,8 +2,9 @@ "use strict"; -const { addError, addErrorDetailIf, escapeForRegExp, filterTokens, - forEachInlineChild, forEachHeading, htmlElementRe } = require("../helpers"); +const { addError, addErrorDetailIf } = require("../helpers"); +const { filterByHtmlTokens, filterByTypes, getHtmlTagInfo } = + require("../helpers/micromark.cjs"); // Regular expression for identifying HTML anchor names const idRe = /\sid\s*=\s*['"]?([^'"\s>]+)/iu; @@ -14,14 +15,14 @@ const anchorRe = /\{(#[a-z\d]+(?:[-_][a-z\d]+)*)\}/gu; * Converts a Markdown heading into an HTML fragment according to the rules * used by GitHub. * - * @param {Object} inline Inline token for heading. + * @param {Object} headingText Heading text token. * @returns {string} Fragment string for heading. */ -function convertHeadingToHTMLFragment(inline) { - const inlineText = inline.children - .filter((token) => token.type !== "html_inline") - .map((token) => token.content) - .join(""); +function convertHeadingToHTMLFragment(headingText) { + const inlineText = + filterByTypes(headingText.children, [ "codeTextData", "data" ]) + .map((token) => token.text) + .join(""); return "#" + encodeURIComponent( inlineText .toLowerCase() @@ -42,86 +43,96 @@ module.exports = { "description": "Link fragments should be valid", "tags": [ "links" ], "function": function MD051(params, onError) { + const { tokens } = params.parsers.micromark; const fragments = new Map(); + // Process headings - forEachHeading(params, (heading, content, inline) => { - const fragment = convertHeadingToHTMLFragment(inline); + const headingTexts = filterByTypes( + tokens, + [ "atxHeadingText", "setextHeadingText" ] + ); + for (const headingText of headingTexts) { + const fragment = convertHeadingToHTMLFragment(headingText); const count = fragments.get(fragment) || 0; if (count) { fragments.set(`${fragment}-${count}`, 0); } fragments.set(fragment, count + 1); let match = null; - while ((match = anchorRe.exec(content)) !== null) { + while ((match = anchorRe.exec(headingText.text)) !== null) { const [ , anchor ] = match; if (!fragments.has(anchor)) { fragments.set(anchor, 1); } } - }); + } + // Process HTML anchors - const processHtmlToken = (token) => { - let match = null; - while ((match = htmlElementRe.exec(token.content)) !== null) { - const [ tag, , element ] = match; - const anchorMatch = idRe.exec(tag) || - (element.toLowerCase() === "a" && nameRe.exec(tag)); + for (const token of filterByHtmlTokens(tokens)) { + const htmlTagInfo = getHtmlTagInfo(token); + if (htmlTagInfo && !htmlTagInfo.close) { + const anchorMatch = idRe.exec(token.text) || + (htmlTagInfo.name.toLowerCase() === "a" && nameRe.exec(token.text)); if (anchorMatch) { fragments.set(`#${anchorMatch[1]}`, 0); } } - }; - filterTokens(params, "html_block", processHtmlToken); - forEachInlineChild(params, "html_inline", processHtmlToken); - // Process link fragments - forEachInlineChild(params, "link_open", (token) => { - const { attrs, lineNumber, line } = token; - const href = attrs.find((attr) => attr[0] === "href"); - const id = href && href[1]; - if (id && (id.length > 1) && (id[0] === "#") && !fragments.has(id)) { - let context = id; - let range = null; - let fixInfo = null; - const match = line.match( - new RegExp(`\\[.*?\\]\\(${escapeForRegExp(context)}\\)`) - ); - if (match) { - [ context ] = match; - const index = match.index; - const length = context.length; - range = [ index + 1, length ]; - fixInfo = { - "editColumn": index + (length - id.length), - "deleteCount": id.length, - "insertText": null - }; - } - const idLower = id.toLowerCase(); - const mixedCaseKey = [ ...fragments.keys() ] - .find((key) => idLower === key.toLowerCase()); - if (mixedCaseKey) { - (fixInfo || {}).insertText = mixedCaseKey; - addErrorDetailIf( - onError, - lineNumber, - mixedCaseKey, - id, - undefined, - context, - range, - fixInfo - ); - } else { - addError( - onError, - lineNumber, - undefined, - context, - // @ts-ignore - range - ); + } + + // Process link and definition fragments + const parentChilds = [ + [ "link", "resourceDestinationString" ], + [ "definition", "definitionDestinationString" ] + ]; + for (const [ parentType, definitionType ] of parentChilds) { + const links = filterByTypes(tokens, [ parentType ]); + for (const link of links) { + const definitions = filterByTypes(link.children, [ definitionType ]); + for (const definition of definitions) { + if ( + (definition.text.length > 1) && + definition.text.startsWith("#") && + !fragments.has(definition.text) + ) { + // eslint-disable-next-line no-undef-init + let range = undefined; + // eslint-disable-next-line no-undef-init + let fixInfo = undefined; + if (link.startLine === link.endLine) { + range = [ link.startColumn, link.endColumn - link.startColumn ]; + fixInfo = { + "editColumn": definition.startColumn, + "deleteCount": definition.endColumn - definition.startColumn + }; + } + const definitionTextLower = definition.text.toLowerCase(); + const mixedCaseKey = [ ...fragments.keys() ] + .find((key) => definitionTextLower === key.toLowerCase()); + if (mixedCaseKey) { + // @ts-ignore + (fixInfo || {}).insertText = mixedCaseKey; + addErrorDetailIf( + onError, + link.startLine, + mixedCaseKey, + definition.text, + undefined, + link.text, + range, + fixInfo + ); + } else { + addError( + onError, + link.startLine, + undefined, + link.text, + range + ); + } + } } } - }); + } } }; diff --git a/test/link-fragments.md b/test/link-fragments.md index b7f34a8b..4e919c2a 100644 --- a/test/link-fragments.md +++ b/test/link-fragments.md @@ -12,6 +12,8 @@ [Valid](#valid-heading-with-emphasis) +[Valid](#valid-heading-with-code) + [Valid](#valid-heading-with-quotes--and-double-quotes-) [Valid](#-valid-heading-with-emoji) @@ -70,6 +72,10 @@ Text Text +### Valid Heading With `Code` + +Text + ### Valid Heading With Quotes ' And Double Quotes " Text @@ -157,9 +163,12 @@ Text [Invalid](#not-an-id-should-be-ignored) {MD051} -[Invalid][badref] {MD051} +[Invalid {MD051}](#multi-line +"Title") -[badref]: #missing +[Invalid][badref] + +[badref]: #missing "{MD051}" ## Inconsistent Case Fragments @@ -167,9 +176,12 @@ Text [ALL CAPS](#NAMEDLINK) {MD051} -[MiXeD][mixedref] {MD051} +[Multi-line {MD051}](#NAMEDLINK +"Title") -[mixedref]: #idLINK +[MiXeD][mixedref] + +[mixedref]: #idLINK "{MD051}" ## Valid Named Fragments diff --git a/test/snapshots/markdownlint-test-repos.js.md b/test/snapshots/markdownlint-test-repos.js.md index 36165c43..6accc64c 100644 --- a/test/snapshots/markdownlint-test-repos.js.md +++ b/test/snapshots/markdownlint-test-repos.js.md @@ -55,10 +55,10 @@ Generated by [AVA](https://avajs.dev). > Expected linting violations `test-repos/mochajs-mocha/.github/CODE_OF_CONDUCT.md: 58: MD034/no-bare-urls Bare URL used [Context: "report@lists.openjsf.org"]␊ - test-repos/mochajs-mocha/.github/CONTRIBUTING.md: 11: MD051/link-fragments Link fragments should be valid [Context: "[search](https://github.com/mochajs/mocha/issues/) to see if it's already been reported**. Otherwise, create a [new issue](https://github.com/mochajs/mocha/issues/new). If you can fix the bug yourself, feel free to create a [pull request](#propose-a-change)"]␊ + test-repos/mochajs-mocha/.github/CONTRIBUTING.md: 11: MD051/link-fragments Link fragments should be valid [Context: "[pull request](#propose-a-change)"]␊ test-repos/mochajs-mocha/.github/CONTRIBUTING.md: 41: MD051/link-fragments Link fragments should be valid [Context: "[ask for help](#got-a-question)"]␊ test-repos/mochajs-mocha/MAINTAINERS.md: 34: MD051/link-fragments Link fragments should be valid [Context: "[Projects](#projects)"]␊ - test-repos/mochajs-mocha/PROJECT_CHARTER.md: 51: MD051/link-fragments Link fragments should be valid [Context: "[mochajs organization](https://github.com/mochajs) unless explicitly stated in [§2: Scope](#%c2%a72-scope)"]␊ + test-repos/mochajs-mocha/PROJECT_CHARTER.md: 51: MD051/link-fragments Link fragments should be valid [Context: "[§2: Scope](#%c2%a72-scope)"]␊ test-repos/mochajs-mocha/PROJECT_CHARTER.md: 56: MD051/link-fragments Link fragments should be valid [Context: "[§2: Scope](#%c2%a72-scope)"]␊ test-repos/mochajs-mocha/docs/index.md: 32: MD051/link-fragments Link fragments should be valid [Context: "[global variable leak detection](#-check-leaks)"]␊ test-repos/mochajs-mocha/docs/index.md: 33: MD051/link-fragments Link fragments should be valid [Context: "[optionally run tests that match a regexp](#-grep-regexp-g-regexp)"]␊ @@ -72,7 +72,7 @@ Generated by [AVA](https://avajs.dev). test-repos/mochajs-mocha/docs/index.md: 1257: MD051/link-fragments Link fragments should be valid [Context: "[\`--node-option\`](#-node-option-name-n-name)"]␊ test-repos/mochajs-mocha/docs/index.md: 1263: MD051/link-fragments Link fragments should be valid [Context: "[\`--parallel\`](#-parallel-p)"]␊ test-repos/mochajs-mocha/docs/index.md: 1288: MD051/link-fragments Link fragments should be valid [Context: "[\`--grep\`](#-grep-regexp-g-regexp)"]␊ - test-repos/mochajs-mocha/docs/index.md: 1288: MD051/link-fragments Link fragments should be valid [Context: "[\`--grep\`](#-grep-regexp-g-regexp) or [\`--fgrep\`](#-fgrep-string-f-string)"]␊ + test-repos/mochajs-mocha/docs/index.md: 1288: MD051/link-fragments Link fragments should be valid [Context: "[\`--fgrep\`](#-fgrep-string-f-string)"]␊ test-repos/mochajs-mocha/docs/index.md: 1299: MD051/link-fragments Link fragments should be valid [Context: "[\`--file\`](#-file-filedirectoryglob)"]␊ test-repos/mochajs-mocha/docs/index.md: 1300: MD051/link-fragments Link fragments should be valid [Context: "[\`--sort\`](#-sort-s)"]␊ test-repos/mochajs-mocha/docs/index.md: 1306: MD051/link-fragments Link fragments should be valid [Context: "[globally](#-timeout-ms-t-ms)"]␊ diff --git a/test/snapshots/markdownlint-test-repos.js.snap b/test/snapshots/markdownlint-test-repos.js.snap index 2da876d8..0adad030 100644 Binary files a/test/snapshots/markdownlint-test-repos.js.snap and b/test/snapshots/markdownlint-test-repos.js.snap differ diff --git a/test/snapshots/markdownlint-test-scenarios.js.md b/test/snapshots/markdownlint-test-scenarios.js.md index 9be6d9a9..00bac427 100644 --- a/test/snapshots/markdownlint-test-scenarios.js.md +++ b/test/snapshots/markdownlint-test-scenarios.js.md @@ -23472,7 +23472,7 @@ Generated by [AVA](https://avajs.dev). 31, ], fixInfo: null, - lineNumber: 144, + lineNumber: 150, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23488,7 +23488,7 @@ Generated by [AVA](https://avajs.dev). 36, ], fixInfo: null, - lineNumber: 146, + lineNumber: 152, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23504,7 +23504,7 @@ Generated by [AVA](https://avajs.dev). 28, ], fixInfo: null, - lineNumber: 148, + lineNumber: 154, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23520,7 +23520,7 @@ Generated by [AVA](https://avajs.dev). 18, ], fixInfo: null, - lineNumber: 150, + lineNumber: 156, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23540,7 +23540,7 @@ Generated by [AVA](https://avajs.dev). editColumn: 11, insertText: '#HREFandID', }, - lineNumber: 152, + lineNumber: 158, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23556,7 +23556,7 @@ Generated by [AVA](https://avajs.dev). 34, ], fixInfo: null, - lineNumber: 154, + lineNumber: 160, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23572,7 +23572,7 @@ Generated by [AVA](https://avajs.dev). 34, ], fixInfo: null, - lineNumber: 156, + lineNumber: 162, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23588,7 +23588,7 @@ Generated by [AVA](https://avajs.dev). 39, ], fixInfo: null, - lineNumber: 158, + lineNumber: 164, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23597,11 +23597,28 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#missing', + errorContext: `[Invalid {MD051}](#multi-line␊ + "Title")`, errorDetail: null, errorRange: null, fixInfo: null, - lineNumber: 160, + lineNumber: 166, + ruleDescription: 'Link fragments should be valid', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', + ruleNames: [ + 'MD051', + 'link-fragments', + ], + }, + { + errorContext: '[badref]: #missing "{MD051}"', + errorDetail: null, + errorRange: [ + 1, + 28, + ], + fixInfo: null, + lineNumber: 171, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23621,7 +23638,7 @@ Generated by [AVA](https://avajs.dev). editColumn: 9, insertText: '#valid-fragments', }, - lineNumber: 166, + lineNumber: 175, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23641,7 +23658,7 @@ Generated by [AVA](https://avajs.dev). editColumn: 12, insertText: '#namedlink', }, - lineNumber: 168, + lineNumber: 177, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23650,11 +23667,32 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#idLINK', - errorDetail: 'Expected: #idlink; Actual: #idLINK', + errorContext: `[Multi-line {MD051}](#NAMEDLINK␊ + "Title")`, + errorDetail: 'Expected: #namedlink; Actual: #NAMEDLINK', errorRange: null, fixInfo: null, - lineNumber: 170, + lineNumber: 179, + ruleDescription: 'Link fragments should be valid', + ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', + ruleNames: [ + 'MD051', + 'link-fragments', + ], + }, + { + errorContext: '[mixedref]: #idLINK "{MD051}"', + errorDetail: 'Expected: #idlink; Actual: #idLINK', + errorRange: [ + 1, + 29, + ], + fixInfo: { + deleteCount: 7, + editColumn: 13, + insertText: '#idlink', + }, + lineNumber: 184, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23670,7 +23708,7 @@ Generated by [AVA](https://avajs.dev). 26, ], fixInfo: null, - lineNumber: 207, + lineNumber: 219, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23686,7 +23724,7 @@ Generated by [AVA](https://avajs.dev). 26, ], fixInfo: null, - lineNumber: 209, + lineNumber: 221, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23702,7 +23740,7 @@ Generated by [AVA](https://avajs.dev). 20, ], fixInfo: null, - lineNumber: 211, + lineNumber: 223, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23718,7 +23756,7 @@ Generated by [AVA](https://avajs.dev). 23, ], fixInfo: null, - lineNumber: 213, + lineNumber: 225, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23734,7 +23772,7 @@ Generated by [AVA](https://avajs.dev). 22, ], fixInfo: null, - lineNumber: 215, + lineNumber: 227, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23750,7 +23788,7 @@ Generated by [AVA](https://avajs.dev). 42, ], fixInfo: null, - lineNumber: 217, + lineNumber: 229, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23766,7 +23804,7 @@ Generated by [AVA](https://avajs.dev). 21, ], fixInfo: null, - lineNumber: 219, + lineNumber: 231, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23782,7 +23820,7 @@ Generated by [AVA](https://avajs.dev). 21, ], fixInfo: null, - lineNumber: 221, + lineNumber: 233, ruleDescription: 'Link fragments should be valid', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md051.md', ruleNames: [ @@ -23805,6 +23843,8 @@ Generated by [AVA](https://avajs.dev). ␊ [Valid](#valid-heading-with-emphasis)␊ ␊ + [Valid](#valid-heading-with-code)␊ + ␊ [Valid](#valid-heading-with-quotes--and-double-quotes-)␊ ␊ [Valid](#-valid-heading-with-emoji)␊ @@ -23863,6 +23903,10 @@ Generated by [AVA](https://avajs.dev). ␊ Text␊ ␊ + ### Valid Heading With \`Code\`␊ + ␊ + Text␊ + ␊ ### Valid Heading With Quotes ' And Double Quotes "␊ ␊ Text␊ @@ -23950,9 +23994,12 @@ Generated by [AVA](https://avajs.dev). ␊ [Invalid](#not-an-id-should-be-ignored) {MD051}␊ ␊ - [Invalid][badref] {MD051}␊ + [Invalid {MD051}](#multi-line␊ + "Title")␊ ␊ - [badref]: #missing␊ + [Invalid][badref]␊ + ␊ + [badref]: #missing "{MD051}"␊ ␊ ## Inconsistent Case Fragments␊ ␊ @@ -23960,9 +24007,12 @@ Generated by [AVA](https://avajs.dev). ␊ [ALL CAPS](#namedlink) {MD051}␊ ␊ - [MiXeD][mixedref] {MD051}␊ + [Multi-line {MD051}](#NAMEDLINK␊ + "Title")␊ ␊ - [mixedref]: #idLINK␊ + [MiXeD][mixedref]␊ + ␊ + [mixedref]: #idlink "{MD051}"␊ ␊ ## Valid Named Fragments␊ ␊ @@ -41804,9 +41854,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%60link', + errorContext: '[link](#link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 6, + 18, + ], fixInfo: null, lineNumber: 81, ruleDescription: 'Link fragments should be valid', @@ -41817,9 +41870,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%60link', + errorContext: '[link](#link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 11, + 18, + ], fixInfo: null, lineNumber: 85, ruleDescription: 'Link fragments should be valid', @@ -41830,9 +41886,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%60link', + errorContext: '[link(link](#link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 6, + 23, + ], fixInfo: null, lineNumber: 87, ruleDescription: 'Link fragments should be valid', @@ -41843,9 +41902,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%60link', + errorContext: '[link)link](#link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 6, + 23, + ], fixInfo: null, lineNumber: 89, ruleDescription: 'Link fragments should be valid', @@ -41856,9 +41918,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%5Blink%60link', + errorContext: '[link](#link[link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 6, + 23, + ], fixInfo: null, lineNumber: 91, ruleDescription: 'Link fragments should be valid', @@ -41869,9 +41934,12 @@ Generated by [AVA](https://avajs.dev). ], }, { - errorContext: '#link%5Dlink%60link', + errorContext: '[link](#link]link`link)', errorDetail: null, - errorRange: null, + errorRange: [ + 6, + 23, + ], fixInfo: null, lineNumber: 93, ruleDescription: 'Link fragments should be valid', diff --git a/test/snapshots/markdownlint-test-scenarios.js.snap b/test/snapshots/markdownlint-test-scenarios.js.snap index 76ab131c..d026219a 100644 Binary files a/test/snapshots/markdownlint-test-scenarios.js.snap and b/test/snapshots/markdownlint-test-scenarios.js.snap differ