Skip to content

Commit

Permalink
Initial implementation of MD051/valid-link-fragments (refs #253, closes
Browse files Browse the repository at this point in the history
  • Loading branch information
theoludwig authored and DavidAnson committed Apr 16, 2022
1 parent 62f5c85 commit 33ee1cd
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 24 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->

Expand Down Expand Up @@ -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
Expand Down
55 changes: 54 additions & 1 deletion demo/markdownlint-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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();
Expand Down
29 changes: 27 additions & 2 deletions doc/Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 46 additions & 0 deletions lib/md051.js
Original file line number Diff line number Diff line change
@@ -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, "-")

This comment has been minimized.

Copy link
@Mouvedia

Mouvedia Sep 18, 2022

https://github.com/bhollis/maruku uses _ instead of -
should it be an option ?

This comment has been minimized.

Copy link
@DavidAnson

DavidAnson Sep 24, 2022

Owner

This function specifically supports the GitHub scenario per the rule details above.

This comment has been minimized.

Copy link
@Mouvedia

Mouvedia Sep 24, 2022

And I was asking if we could have an option for the separator.
e.g.

{ "separator": "_" }

babelmark seems to indicate that - is the most common so it should be the default.

This comment has been minimized.

Copy link
@DavidAnson

DavidAnson Sep 25, 2022

Owner

The Maruku docs make a point to say it is obsolete: https://github.com/bhollis/maruku#maruku

The {#id} syntax of kramdown may be interesting, but would require more significant changes: https://kramdown.gettalong.org/syntax.html#specifying-a-header-id

.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]);
}
}
});
});
}
};
3 changes: 2 additions & 1 deletion lib/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 4 additions & 1 deletion schema/.markdownlint.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -262,5 +262,8 @@
"MD050": {
// Strong style should be consistent
"style": "consistent"
}
},

// MD051/valid-link-fragments - Link fragments should be valid
"MD051": true
}
5 changes: 4 additions & 1 deletion schema/.markdownlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,7 @@ MD049:
# MD050/strong-style - Strong style should be consistent
MD050:
# Strong style should be consistent
style: "consistent"
style: "consistent"

# MD051/valid-link-fragments - Link fragments should be valid
MD051: true
12 changes: 11 additions & 1 deletion schema/markdownlint-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -914,7 +924,7 @@
"default": true
},
"links": {
"description": "links - MD011, MD034, MD039, MD042",
"description": "links - MD011, MD034, MD039, MD042, MD051",
"type": "boolean",
"default": true
},
Expand Down
26 changes: 26 additions & 0 deletions test/detailed-results-MD041-MD050.results.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
14 changes: 8 additions & 6 deletions test/empty-links.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
6 changes: 3 additions & 3 deletions test/markdownlint-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 7 additions & 7 deletions test/spaces_inside_codespan_elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
Loading

0 comments on commit 33ee1cd

Please sign in to comment.