Initial implementation of MD051/valid-link-fragments (refs #253, closes #495).

This commit is contained in:
Divlo 2022-01-26 00:21:08 +01:00 committed by David Anson
parent 62f5c85238
commit 33ee1cd85e
13 changed files with 250 additions and 24 deletions

View file

@ -107,6 +107,7 @@ playground for learning and exploring.
* **[MD048](doc/Rules.md#md048)** *code-fence-style* - Code fence style
* **[MD049](doc/Rules.md#md049)** *emphasis-style* - Emphasis style should be consistent
* **[MD050](doc/Rules.md#md050)** *strong-style* - Strong style should be consistent
* **[MD051](doc/Rules.md#md051)** *valid-link-fragments* - Link fragments should be valid
<!-- markdownlint-restore -->
@ -142,7 +143,7 @@ rules at once.
* **indentation** - MD005, MD006, MD007, MD027
* **language** - MD040
* **line_length** - MD013
* **links** - MD011, MD034, MD039, MD042
* **links** - MD011, MD034, MD039, MD042, MD051
* **ol** - MD029, MD030, MD032
* **spaces** - MD018, MD019, MD020, MD021, MD023
* **spelling** - MD044

View file

@ -4383,6 +4383,58 @@ module.exports = {
};
/***/ }),
/***/ "../lib/md051.js":
/*!***********************!*\
!*** ../lib/md051.js ***!
\***********************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachHeading = _a.forEachHeading, filterTokens = _a.filterTokens;
/**
* Converts a Markdown heading into an HTML fragment
* according to the rules used by GitHub.
*
* @param {string} string The string to convert.
* @returns {string} The converted string.
*/
function convertHeadingToHTMLFragment(string) {
return "#" + string
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^-_a-z0-9]/g, "");
}
module.exports = {
"names": ["MD051", "valid-link-fragments"],
"description": "Link fragments should be valid",
"tags": ["links"],
"function": function MD051(params, onError) {
var validLinkFragments = [];
forEachHeading(params, function (_heading, content) {
validLinkFragments.push(convertHeadingToHTMLFragment(content));
});
filterTokens(params, "inline", function (token) {
token.children.forEach(function (child) {
var lineNumber = child.lineNumber, type = child.type, attrs = child.attrs;
if (type === "link_open") {
var href = attrs.find(function (attr) { return attr[0] === "href"; });
if (href !== undefined &&
href[1].startsWith("#") &&
!validLinkFragments.includes(href[1])) {
var detail = "Link Fragment is invalid";
addError(onError, lineNumber, detail, href[1]);
}
}
});
});
}
};
/***/ }),
/***/ "../lib/rules.js":
@ -4441,7 +4493,8 @@ var rules = [
__webpack_require__(/*! ./md047 */ "../lib/md047.js"),
__webpack_require__(/*! ./md048 */ "../lib/md048.js"),
__webpack_require__(/*! ./md049 */ "../lib/md049.js"),
__webpack_require__(/*! ./md050 */ "../lib/md050.js")
__webpack_require__(/*! ./md050 */ "../lib/md050.js"),
__webpack_require__(/*! ./md051 */ "../lib/md051.js")
];
rules.forEach(function (rule) {
var name = rule.names[0].toLowerCase();

View file

@ -56,7 +56,8 @@ Aliases: first-heading-h1, first-header-h1
Parameters: level (number; default 1)
> Note: *MD002 has been deprecated and is disabled by default.*
> [MD041/first-line-heading](#md041) offers an improved implementation.
> [MD041/first-line-heading](#md041---first-line-in-a-file-should-be-a-top-level-heading)
offers an improved implementation.
This rule is intended to ensure document headings start at the top level and
is triggered when the first heading in the document isn't an h1 heading:
@ -783,7 +784,8 @@ The `lines_above` and `lines_below` parameters can be used to specify a differen
number of blank lines (including 0) above or below each heading.
Note: If `lines_above` or `lines_below` are configured to require more than one
blank line, [MD012/no-multiple-blanks](#md012) should also be customized.
blank line, [MD012/no-multiple-blanks](#md012---multiple-consecutive-blank-lines)
should also be customized.
Rationale: Aside from aesthetic reasons, some parsers, including `kramdown`, will
not parse headings that don't have a blank line before, and will parse them as
@ -1984,3 +1986,26 @@ The configured strong style can be a specific symbol to use ("asterisk",
"underscore"), or can require that usage be consistent within the document.
Rationale: Consistent formatting makes it easier to understand a document.
<a name="md051"></a>
## MD051 - Link fragments should be valid
Tags: links
Aliases: valid-link-fragments
This rule is triggered if a link fragment does not correspond to a
heading within the document:
```markdown
# Title
[Link](#invalid-fragment)
```
To fix this issue, ensure that the heading exists,
here you could replace `#invalid-fragment` by `#title`.
It's not part of the CommonMark specification,
for example [GitHub turn headings into links](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#section-links).

46
lib/md051.js Normal file
View file

@ -0,0 +1,46 @@
// @ts-check
"use strict";
const { addError, forEachHeading, filterTokens } = require("../helpers");
/**
* Converts a Markdown heading into an HTML fragment
* according to the rules used by GitHub.
*
* @param {string} string The string to convert.
* @returns {string} The converted string.
*/
function convertHeadingToHTMLFragment(string) {
return "#" + string
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^-_a-z0-9]/g, "");
}
module.exports = {
"names": [ "MD051", "valid-link-fragments" ],
"description": "Link fragments should be valid",
"tags": [ "links" ],
"function": function MD051(params, onError) {
const validLinkFragments = [];
forEachHeading(params, (_heading, content) => {
validLinkFragments.push(convertHeadingToHTMLFragment(content));
});
filterTokens(params, "inline", (token) => {
token.children.forEach((child) => {
const { lineNumber, type, attrs } = child;
if (type === "link_open") {
const href = attrs.find((attr) => attr[0] === "href");
if (href !== undefined &&
href[1].startsWith("#") &&
!validLinkFragments.includes(href[1])
) {
const detail = "Link Fragment is invalid";
addError(onError, lineNumber, detail, href[1]);
}
}
});
});
}
};

View file

@ -50,7 +50,8 @@ const rules = [
require("./md047"),
require("./md048"),
require("./md049"),
require("./md050")
require("./md050"),
require("./md051")
];
rules.forEach((rule) => {
const name = rule.names[0].toLowerCase();

View file

@ -262,5 +262,8 @@
"MD050": {
// Strong style should be consistent
"style": "consistent"
}
},
// MD051/valid-link-fragments - Link fragments should be valid
"MD051": true
}

View file

@ -237,3 +237,6 @@ MD049:
MD050:
# Strong style should be consistent
style: "consistent"
# MD051/valid-link-fragments - Link fragments should be valid
MD051: true

View file

@ -878,6 +878,16 @@
"strong-style": {
"$ref": "#/properties/MD050"
},
"MD051": {
"description": "MD051/valid-link-fragments - Link fragments should be valid",
"type": "boolean",
"default": true
},
"valid-link-fragments": {
"description": "MD051/valid-link-fragments - Link fragments should be valid",
"type": "boolean",
"default": true
},
"headings": {
"description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043",
"type": "boolean",
@ -914,7 +924,7 @@
"default": true
},
"links": {
"description": "links - MD011, MD034, MD039, MD042",
"description": "links - MD011, MD034, MD039, MD042, MD051",
"type": "boolean",
"default": true
},

View file

@ -314,5 +314,31 @@
"MD050",
"strong-style"
]
},
{
"errorContext": "#",
"errorDetail": "Link Fragment is invalid",
"errorRange": null,
"fixInfo": null,
"lineNumber": 5,
"ruleDescription": "Link fragments should be valid",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md051",
"ruleNames": [
"MD051",
"valid-link-fragments"
]
},
{
"errorContext": "#one",
"errorDetail": "Link Fragment is invalid",
"errorRange": null,
"fixInfo": null,
"lineNumber": 17,
"ruleDescription": "Link fragments should be valid",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md051",
"ruleNames": [
"MD051",
"valid-link-fragments"
]
}
]

View file

@ -12,22 +12,24 @@
[text]( <> "title" ) {MD042}
[text](#) {MD042}
[text](#) {MD042} {MD051}
[text]( # ) {MD042}
[text]( # ) {MD042} {MD051}
[text](# "title") {MD042}
[text](# "title") {MD042} {MD051}
[text]( # "title" ) {MD042}
[text]( # "title" ) {MD042} {MD051}
[text][frag] {MD042}
[text][frag] {MD042} {MD051}
[text][ frag ] {MD042}
[text][ frag ] {MD042} {MD051}
[frag]: #
## Non-empty links
### frag
[text](link)
[text]( link )

View file

@ -841,7 +841,7 @@ test.cb("customFileSystemAsync", (t) => {
});
test.cb("readme", (t) => {
t.plan(119);
t.plan(121);
const tagToRules = {};
rules.forEach(function forRule(rule) {
rule.tags.forEach(function forTag(tag) {
@ -917,7 +917,7 @@ test.cb("readme", (t) => {
});
test.cb("rules", (t) => {
t.plan(352);
t.plan(359);
fs.readFile("doc/Rules.md", "utf8",
(err, contents) => {
t.falsy(err);
@ -1094,7 +1094,7 @@ test("validateConfigExampleJson", async(t) => {
});
test("allBuiltInRulesHaveValidUrl", (t) => {
t.plan(138);
t.plan(141);
rules.forEach(function forRule(rule) {
t.truthy(rule.information);
t.true(Object.getPrototypeOf(rule.information) === URL.prototype);

View file

@ -78,21 +78,21 @@ Text [link](https://example.com/link`link`link`link) text `code`.
Text [link](https://example.com/link "title`title") text `code`.
Text [link](#link`link) text `code`.
Text [link](#link`link) text `code`. {MD051}
Text [link] (#link`link) text `code`. {MD038}
Text [link[link](#link`link) text `code`.
Text [link[link](#link`link) text `code`. {MD051}
Text [link(link](#link`link) text `code`.
Text [link(link](#link`link) text `code`. {MD051}
Text [link)link](#link`link) text `code`.
Text [link)link](#link`link) text `code`. {MD051}
Text [link](#link[link`link) text `code`.
Text [link](#link[linklink) text `code`. {MD051}
Text [link](#link]link`link) text `code`.
Text [link](#link[linklink) text `code`. {MD051}
Text [link](#link(link`link) text `code`. {MD038}
Text [link](#link[linklink) text `code`. {MD051}
Text [`link`](xref:custom.link`1) text `code`.

View file

@ -0,0 +1,56 @@
# Valid/Invalid Link Fragments
## Valid Fragments
[Valid](#validinvalid-link-fragments)
[Valid](#valid-fragments)
[Valid](#valid-h3-heading)
[Valid](#valid-heading-with-underscores-_)
[Valid](#valid-heading-with-quotes--and-double-quotes-)
[Valid](#-valid-heading-with-emoji)
[Valid](#valid-heading--with-emoji-2)
[Valid](#valid-closed-atx-heading)
[Valid](#valid-setex-heading)
### Valid H3 Heading
Text
### Valid Heading With Underscores _
Text
### Valid Heading With Quotes ' And Double Quotes "
Text
### 🚀 Valid Heading With Emoji
Text
### Valid Heading 👀 With Emoji 2
Text
<!-- markdownlint-disable-next-line MD003 -->
### Valid Closed ATX Heading ###
Text
<!-- markdownlint-disable-next-line MD003 -->
Valid Setex Heading
----
Text
## Invalid Fragments
[Invalid](#invalid-fragments-not-exist) {MD051}