Update previous commit for MD051/link-fragments to rename, refactor, add support for HTML anchors, and validate against

markdown-link-check (fixes #253).
This commit is contained in:
David Anson 2022-04-10 05:37:57 +00:00
parent 33ee1cd85e
commit db5d9f6dbb
21 changed files with 355 additions and 181 deletions

View file

@ -107,7 +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
* **[MD051](doc/Rules.md#md051)** *link-fragments* - Link fragments should be valid
<!-- markdownlint-restore -->

View file

@ -43,6 +43,8 @@ var inlineCommentStartRe =
// eslint-disable-next-line max-len
/(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-next-line|configure-file))(?:\s|-->)/ig;
module.exports.inlineCommentStartRe = inlineCommentStartRe;
// Regular expression for matching HTML elements
module.exports.htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g;
// Regular expressions for range matching
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig;
module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
@ -424,7 +426,7 @@ module.exports.forEachHeading = function forEachHeading(params, handler) {
heading = null;
}
else if ((token.type === "inline") && heading) {
handler(heading, token.content);
handler(heading, token.content, token);
}
});
};
@ -3414,9 +3416,8 @@ module.exports = {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, overlapsAnyRange = _a.overlapsAnyRange, unescapeMarkdown = _a.unescapeMarkdown;
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachLine = _a.forEachLine, htmlElementRe = _a.htmlElementRe, overlapsAnyRange = _a.overlapsAnyRange, unescapeMarkdown = _a.unescapeMarkdown;
var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata;
var htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g;
var linkDestinationRe = /]\(\s*$/;
// See https://spec.commonmark.org/0.29/#autolinks
var emailAddressRe =
@ -4394,42 +4395,67 @@ module.exports = {
"use strict";
// @ts-check
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, forEachHeading = _a.forEachHeading, filterTokens = _a.filterTokens;
var _a = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"), addError = _a.addError, escapeForRegExp = _a.escapeForRegExp, filterTokens = _a.filterTokens, forEachLine = _a.forEachLine, forEachHeading = _a.forEachHeading, htmlElementRe = _a.htmlElementRe, overlapsAnyRange = _a.overlapsAnyRange;
var _b = __webpack_require__(/*! ./cache */ "../lib/cache.js"), codeBlockAndSpanRanges = _b.codeBlockAndSpanRanges, lineMetadata = _b.lineMetadata;
// Regular expression for identifying HTML anchor names
var identifierRe = /(?:id|name)\s*=\s*['"]?([^'"\s>]+)/iu;
/**
* Converts a Markdown heading into an HTML fragment
* according to the rules used by GitHub.
* 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.
* @param {Object} inline Inline token for heading.
* @returns {string} Fragment string for heading.
*/
function convertHeadingToHTMLFragment(string) {
return "#" + string
function convertHeadingToHTMLFragment(inline) {
var inlineText = inline.children.map(function (token) { return token.content; }).join("");
return "#" + inlineText
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^-_a-z0-9]/g, "");
}
module.exports = {
"names": ["MD051", "valid-link-fragments"],
"names": ["MD051", "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));
var fragments = new Set();
forEachHeading(params, function (heading, content, inline) {
fragments.add(convertHeadingToHTMLFragment(inline));
});
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]);
var exclusions = codeBlockAndSpanRanges();
forEachLine(lineMetadata(), function (line, lineIndex, inCode) {
var match = null;
// eslint-disable-next-line no-unmodified-loop-condition
while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
var tag = match[0], element = match[2];
if ((element.toLowerCase() === "a") &&
!overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length)) {
var idMatch = identifierRe.exec(tag);
if (idMatch) {
fragments.add("#".concat(idMatch[1]));
}
}
});
}
});
filterTokens(params, "inline", function (token) {
for (var _i = 0, _a = token.children; _i < _a.length; _i++) {
var child = _a[_i];
var attrs = child.attrs, lineNumber = child.lineNumber, line = child.line, type = child.type;
if (type === "link_open") {
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 match = line.match(new RegExp("\\[.*?\\]\\(".concat(escapeForRegExp(context), "\\)")));
if (match) {
context = match[0];
range = [match.index + 1, match[0].length];
}
addError(onError, lineNumber, null, context, range);
}
}
}
});
}
};

View file

@ -56,8 +56,7 @@ 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---first-line-in-a-file-should-be-a-top-level-heading)
offers an improved implementation.
> [MD041/first-line-heading](#md041) 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:
@ -784,8 +783,7 @@ 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---multiple-consecutive-blank-lines)
should also be customized.
blank line, [MD012/no-multiple-blanks](#md012) 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
@ -1993,19 +1991,42 @@ Rationale: Consistent formatting makes it easier to understand a document.
Tags: links
Aliases: valid-link-fragments
Aliases: link-fragments
This rule is triggered if a link fragment does not correspond to a
heading within the document:
This rule is triggered when a link fragment does not correspond to a heading
in the document:
```markdown
# Title
[Link](#invalid-fragment)
[Link](#fragment)
```
To fix this issue, ensure that the heading exists,
here you could replace `#invalid-fragment` by `#title`.
To fix the issue, change the fragment to reference an existing heading:
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).
```markdown
[Link](#title)
```
Alternatively, an HTML `a` tag with an `id` (or a `name`) attribute defines a
valid anchor:
```markdown
<a id="fragment"></a>
```
Some platforms (e.g., [GitHub][github-section-links]) automatically create HTML
anchors for every heading. This makes it easy to link to different sections in
a document. These internal links can break over time as headings are renamed.
Note: Creating anchors for headings is not part of the CommonMark specification.
[github-section-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
<!-- markdownlint-configure-file {
"no-inline-html": {
"allowed_elements": [
"a"
]
}
} -->

View file

@ -18,6 +18,9 @@ const inlineCommentStartRe =
/(<!--\s*markdownlint-(disable|enable|capture|restore|disable-file|enable-file|disable-next-line|configure-file))(?:\s|-->)/ig;
module.exports.inlineCommentStartRe = inlineCommentStartRe;
// Regular expression for matching HTML elements
module.exports.htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g;
// Regular expressions for range matching
module.exports.bareUrlRe = /(?:http|ftp)s?:\/\/[^\s\]"']*(?:\/|[^\s\]"'\W])/ig;
module.exports.listItemMarkerRe = /^([\s>]*)(?:[*+-]|\d+[.)])\s+/;
@ -419,7 +422,7 @@ module.exports.forEachHeading = function forEachHeading(params, handler) {
} else if (token.type === "heading_close") {
heading = null;
} else if ((token.type === "inline") && heading) {
handler(heading, token.content);
handler(heading, token.content, token);
}
});
};

View file

@ -3,11 +3,10 @@
"use strict";
const {
addError, forEachLine, overlapsAnyRange, unescapeMarkdown
addError, forEachLine, htmlElementRe, overlapsAnyRange, unescapeMarkdown
} = require("../helpers");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
const htmlElementRe = /<(([A-Za-z][A-Za-z0-9-]*)(?:\s[^>]*)?)\/?>/g;
const linkDestinationRe = /]\(\s*$/;
// See https://spec.commonmark.org/0.29/#autolinks
const emailAddressRe =

View file

@ -2,45 +2,74 @@
"use strict";
const { addError, forEachHeading, filterTokens } = require("../helpers");
const { addError, escapeForRegExp, filterTokens, forEachLine, forEachHeading,
htmlElementRe, overlapsAnyRange } = require("../helpers");
const { codeBlockAndSpanRanges, lineMetadata } = require("./cache");
// Regular expression for identifying HTML anchor names
const identifierRe = /(?:id|name)\s*=\s*['"]?([^'"\s>]+)/iu;
/**
* Converts a Markdown heading into an HTML fragment
* according to the rules used by GitHub.
* 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.
* @param {Object} inline Inline token for heading.
* @returns {string} Fragment string for heading.
*/
function convertHeadingToHTMLFragment(string) {
return "#" + string
function convertHeadingToHTMLFragment(inline) {
const inlineText = inline.children.map((token) => token.content).join("");
return "#" + inlineText
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^-_a-z0-9]/g, "");
}
module.exports = {
"names": [ "MD051", "valid-link-fragments" ],
"names": [ "MD051", "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));
const fragments = new Set();
forEachHeading(params, (heading, content, inline) => {
fragments.add(convertHeadingToHTMLFragment(inline));
});
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]);
const exclusions = codeBlockAndSpanRanges();
forEachLine(lineMetadata(), (line, lineIndex, inCode) => {
let match = null;
// eslint-disable-next-line no-unmodified-loop-condition
while (!inCode && ((match = htmlElementRe.exec(line)) !== null)) {
const [ tag, , element ] = match;
if (
(element.toLowerCase() === "a") &&
!overlapsAnyRange(exclusions, lineIndex, match.index, match[0].length)
) {
const idMatch = identifierRe.exec(tag);
if (idMatch) {
fragments.add(`#${idMatch[1]}`);
}
}
});
}
});
filterTokens(params, "inline", (token) => {
for (const child of token.children) {
const { attrs, lineNumber, line, type } = child;
if (type === "link_open") {
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;
const match = line.match(
new RegExp(`\\[.*?\\]\\(${escapeForRegExp(context)}\\)`)
);
if (match) {
context = match[0];
range = [ match.index + 1, match[0].length ];
}
addError(onError, lineNumber, null, context, range);
}
}
}
});
}
};

View file

@ -264,6 +264,6 @@
"style": "consistent"
},
// MD051/valid-link-fragments - Link fragments should be valid
// MD051/link-fragments - Link fragments should be valid
"MD051": true
}

View file

@ -238,5 +238,5 @@ MD050:
# Strong style should be consistent
style: "consistent"
# MD051/valid-link-fragments - Link fragments should be valid
# MD051/link-fragments - Link fragments should be valid
MD051: true

View file

@ -879,14 +879,12 @@
"$ref": "#/properties/MD050"
},
"MD051": {
"description": "MD051/valid-link-fragments - Link fragments should be valid",
"description": "MD051/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
"link-fragments": {
"$ref": "#/properties/MD051"
},
"headings": {
"description": "headings - MD001, MD002, MD003, MD018, MD019, MD020, MD021, MD022, MD023, MD024, MD025, MD026, MD036, MD041, MD043",

View file

@ -93,4 +93,6 @@ Strong __with__ underscore style
Strong **with** different style {MD050}
[Missing link fragment](#missing) {MD051}
EOF {MD047}

View file

@ -9,5 +9,6 @@
"names": [
"markdownlint"
]
}
},
"MD051": false
}

View file

@ -314,31 +314,5 @@
"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

@ -0,0 +1,3 @@
# detailed-results-MD051-MD060
A [link with a missing](#fragment)

View file

@ -0,0 +1,3 @@
# detailed-results-MD051-MD060
A [link with a missing](#fragment)

View file

@ -0,0 +1,18 @@
[
{
"errorContext": "[link with a missing](#fragment)",
"errorDetail": null,
"errorRange": [
3,
32
],
"fixInfo": null,
"lineNumber": 3,
"ruleDescription": "Link fragments should be valid",
"ruleInformation": "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/Rules.md#md051",
"ruleNames": [
"MD051",
"link-fragments"
]
}
]

View file

@ -12,17 +12,17 @@
[text]( <> "title" ) {MD042}
[text](#) {MD042} {MD051}
[text](#) {MD042}
[text]( # ) {MD042} {MD051}
[text]( # ) {MD042}
[text](# "title") {MD042} {MD051}
[text](# "title") {MD042}
[text]( # "title" ) {MD042} {MD051}
[text]( # "title" ) {MD042}
[text][frag] {MD042} {MD051}
[text][frag] {MD042}
[text][ frag ] {MD042} {MD051}
[text][ frag ] {MD042}
[frag]: #

94
test/link-fragments.md Normal file
View file

@ -0,0 +1,94 @@
# 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-emphasis)
[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-setext-heading)
[Valid](#namedlink)
[Valid](#idlink)
[Valid](#myident)
[Valid](#HREFandID)
[Valid][goodref]
### Valid H3 Heading
Text
### Valid Heading With Underscores _
Text
### Valid *Heading* With _Emphasis_
Text
### Valid Heading With Quotes ' And Double Quotes "
Text
### 🚀 Valid Heading With Emoji
Text
### Valid Heading 👀 With Emoji 2
Text
### Valid Closed ATX Heading ###
Text
Valid Setext Heading
--------------------
Text
<a name="namedlink"></a>
<a id = idlink></a>
<a id="myident" name="myname"/>
<A href="https://example.com" id="HREFandID">Text</A>
[goodref]: #namedlink
## Invalid Fragments
[Invalid](#invalid-fragment) {MD051}
[Invalid](#myname) {MD051}
[Invalid](#hrefandid) {MD051}
[Invalid][badref] {MD051}
[badref]: #missing
<!-- markdownlint-configure-file {
"emphasis-style": false,
"heading-style": false,
"no-inline-html": false
} -->

View file

@ -35,13 +35,69 @@ async function lintTestRepo(t, globPatterns, configPath, ignoreRes) {
readConfigPromise(configPath, [ jsoncParse, yamlParse ])
]).then((globbyAndReadConfigResults) => {
const [ files, config ] = globbyAndReadConfigResults;
const options = {
files,
config
};
// eslint-disable-next-line no-console
console.log(`${t.title}: Linting ${files.length} files...`);
return markdownlintPromise(options).then((results) => {
return markdownlintPromise({
files,
config,
"resultVersion": 3
// }).then((results) => {
// // Cross-check MD051/link-fragments results with markdown-link-check
// const resultFiles = [];
// const detectedErrors = new Set();
// for (const file of Object.keys(results)) {
// const errors =
// results[file].filter((error) => error.ruleNames[0] === "MD051");
// if (errors.length > 0) {
// resultFiles.push(file);
// }
// for (const error of errors) {
// const fragment = error.errorContext.replace(/^.*\((#.*)\)$/, "$1");
// detectedErrors.add(file + fragment);
// }
// }
// const { readFile } = require("fs").promises;
// const markdownLinkCheck = promisify(require("markdown-link-check"));
// const expectedErrors = new Set();
// return Promise.all(
// resultFiles.map((file) => readFile(file, "utf8")
// // @ts-ignore
// .then((markdown) => markdownLinkCheck(markdown, {
// "ignorePatterns": [
// {
// "pattern": "^[^#]"
// }
// ]
// }))
// .then((mlcResults) => {
// const deadResults =
// mlcResults.filter((result) => result.status === "dead");
// for (const link of deadResults.map((result) => result.link)) {
// expectedErrors.add(file + link);
// }
// })
// )
// ).then(() => {
// const extraErrors = [];
// // @ts-ignore
// for (const detectedError of detectedErrors) {
// if (!expectedErrors.has(detectedError)) {
// extraErrors.push(detectedError);
// }
// }
// t.deepEqual(extraErrors, [], "Extra errors");
// const missingErrors = [];
// // @ts-ignore
// for (const expectedError of expectedErrors) {
// if (!detectedErrors.has(expectedError)) {
// missingErrors.push(expectedError);
// }
// }
// t.deepEqual(missingErrors, [], "Missing errors");
// return results;
// });
}).then((results) => {
// Fail if any issues were found (that aren't ignored)
let resultsString = results.toString();
for (const ignoreRe of (ignoreRes || [])) {
const lengthBefore = resultsString.length;
@ -93,7 +149,8 @@ test("https://github.com/mkdocs/mkdocs", (t) => {
)
];
const configPath = join(rootDir, ".markdownlintrc");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/mochajs/mocha", (t) => {
@ -107,14 +164,16 @@ test("https://github.com/mochajs/mocha", (t) => {
join(rootDir, "example/**/*.md")
];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/pi-hole/docs", (t) => {
const rootDir = "./test-repos/pi-hole-docs";
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
test("https://github.com/webhintio/hint", (t) => {
@ -142,7 +201,8 @@ if (existsSync(dotnetDocsDir)) {
const rootDir = dotnetDocsDir;
const globPatterns = [ join(rootDir, "**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
return lintTestRepo(t, globPatterns, configPath);
const ignoreRes = [ /^[^:]+: \d+: MD051\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}
@ -152,7 +212,7 @@ if (existsSync(v8v8DevDir)) {
const rootDir = v8v8DevDir;
const globPatterns = [ join(rootDir, "src/**/*.md") ];
const configPath = join(rootDir, ".markdownlint.json");
const ignoreRes = [ /^[^:]+: \d+: MD049\/.*$\r?\n?/gm ];
const ignoreRes = [ /^[^:]+: \d+: (MD049|MD051)\/.*$\r?\n?/gm ];
return lintTestRepo(t, globPatterns, configPath, ignoreRes);
});
}

View file

@ -105,10 +105,7 @@ test.cb("projectFilesInlineConfig", (t) => {
t.plan(2);
const options = {
"files": [ "doc/Rules.md" ],
"config": {
"no-inline-html": false,
...require("../.markdownlint.json")
}
"config": require("../.markdownlint.json")
};
markdownlint(options, function callback(err, actual) {
t.falsy(err);
@ -491,10 +488,11 @@ test.cb("styleAll", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 96 ],
"MD047": [ 98 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ]
"MD050": [ 94 ],
"MD051": [ 96 ]
}
};
// @ts-ignore
@ -536,10 +534,11 @@ test.cb("styleRelaxed", (t) => {
"MD042": [ 81 ],
"MD045": [ 85 ],
"MD046": [ 49, 73, 77 ],
"MD047": [ 96 ],
"MD047": [ 98 ],
"MD048": [ 77 ],
"MD049": [ 90 ],
"MD050": [ 94 ]
"MD050": [ 94 ],
"MD051": [ 96 ]
}
};
// @ts-ignore

View file

@ -88,11 +88,11 @@ Text [link(link](#link`link) text `code`. {MD051}
Text [link)link](#link`link) text `code`. {MD051}
Text [link](#link[linklink) text `code`. {MD051}
Text [link](#link[link`link) text `code`. {MD051}
Text [link](#link[linklink) text `code`. {MD051}
Text [link](#link]link`link) text `code`. {MD051}
Text [link](#link[linklink) text `code`. {MD051}
Text [link](#link(link`link) text `code`. {MD038}
Text [`link`](xref:custom.link`1) text `code`.

View file

@ -1,56 +0,0 @@
# 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}