Add new MD054/link-image-style rule (in-progress PR, no generated files).

This commit is contained in:
Tommy G 2023-10-24 21:07:46 -07:00 committed by David Anson
parent 20b8af5054
commit 460836445c
14 changed files with 296 additions and 11 deletions

View file

@ -135,6 +135,7 @@ playground for learning and exploring.
- **[MD051](doc/md051.md)** *link-fragments* - Link fragments should be valid - **[MD051](doc/md051.md)** *link-fragments* - Link fragments should be valid
- **[MD052](doc/md052.md)** *reference-links-images* - Reference links and images should use a label that is defined - **[MD052](doc/md052.md)** *reference-links-images* - Reference links and images should use a label that is defined
- **[MD053](doc/md053.md)** *link-image-reference-definitions* - Link and image reference definitions should be needed - **[MD053](doc/md053.md)** *link-image-reference-definitions* - Link and image reference definitions should be needed
- **[MD054](doc/md054.md)** *link-image-style* - Link and image style
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
@ -176,11 +177,12 @@ rules at once.
`MD043` `MD043`
- **`hr`** - `MD035` - **`hr`** - `MD035`
- **`html`** - `MD033` - **`html`** - `MD033`
- **`images`** - `MD045`, `MD052`, `MD053` - **`images`** - `MD045`, `MD052`, `MD053`, `MD054`
- **`indentation`** - `MD005`, `MD006`, `MD007`, `MD027` - **`indentation`** - `MD005`, `MD006`, `MD007`, `MD027`
- **`language`** - `MD040` - **`language`** - `MD040`
- **`line_length`** - `MD013` - **`line_length`** - `MD013`
- **`links`** - `MD011`, `MD034`, `MD039`, `MD042`, `MD051`, `MD052`, `MD053` - **`links`** - `MD011`, `MD034`, `MD039`, `MD042`, `MD051`, `MD052`, `MD053`,
`MD054`
- **`ol`** - `MD029`, `MD030`, `MD032` - **`ol`** - `MD029`, `MD030`, `MD032`
- **`spaces`** - `MD018`, `MD019`, `MD020`, `MD021`, `MD023` - **`spaces`** - `MD018`, `MD019`, `MD020`, `MD021`, `MD023`
- **`spelling`** - `MD044` - **`spelling`** - `MD044`

View file

@ -1,7 +1,7 @@
Links and images in Markdown can provide the link destination or image source Links and images in Markdown can provide the link destination or image source
at the time of use or can define it elsewhere and use a label for reference. at the time of use or can use a label to reference a definition elsewhere in
The reference format is convenient for keeping paragraph text clutter-free the document. The latter reference format is convenient for keeping paragraph
and makes it easy to reuse the same URL in multiple places. text clutter-free and makes it easy to reuse the same URL in multiple places.
Because link and image reference definitions are located separately from Because link and image reference definitions are located separately from
where they are used, there are two scenarios where a definition can be where they are used, there are two scenarios where a definition can be

11
doc-build/md054.md Normal file
View file

@ -0,0 +1,11 @@
Links and images in Markdown can provide the link destination or image source
at the time of use or can use a label to reference a definition elsewhere in
the document. The latter reference format is convenient for keeping paragraph
text clutter-free and makes it easy to reuse the same URL in multiple places.
This rule can be used to enforce a link or image style for the document that:
- `inline`: provides the link destination or image source at the time of use
- `reference`: defines the link destination or image source elsewhere to be
referenced by label
- or `consistent`: Requires the same style be used everywhere in the document

View file

@ -8,7 +8,8 @@ module.exports.fixableRuleNames = [
"MD011", "MD012", "MD014", "MD018", "MD019", "MD020", "MD011", "MD012", "MD014", "MD018", "MD019", "MD020",
"MD021", "MD022", "MD023", "MD026", "MD027", "MD030", "MD021", "MD022", "MD023", "MD026", "MD027", "MD030",
"MD031", "MD032", "MD034", "MD037", "MD038", "MD039", "MD031", "MD032", "MD034", "MD037", "MD038", "MD039",
"MD044", "MD047", "MD049", "MD050", "MD051", "MD053" "MD044", "MD047", "MD049", "MD050", "MD051", "MD053",
"MD054"
]; ];
module.exports.homepage = "https://github.com/DavidAnson/markdownlint"; module.exports.homepage = "https://github.com/DavidAnson/markdownlint";
module.exports.version = "0.31.1"; module.exports.version = "0.31.1";

134
lib/md054.js Normal file
View file

@ -0,0 +1,134 @@
// @ts-check
"use strict";
const { addErrorContext } = require("../helpers");
const { filterByTypes, filterByPredicate, getTokenTextByType } =
require("../helpers/micromark.cjs");
const isInlineLink = ({ children }) => children.some(
({ type }) => type === "resource"
);
const isAutolink = ({ type }) => type === "autolink";
const getNestedTokenTextByType = (tokens, type) => getTokenTextByType(
filterByTypes(tokens, [ type ]),
type
);
const escapeParentheses = (unescaped) => unescaped
.replaceAll("(", "\\(")
.replaceAll(")", "\\)");
const escapeSquares = (unescaped) => unescaped
.replaceAll("[", "\\[")
.replaceAll("]", "\\]");
const escapeAngles = (unescaped) => unescaped
.replaceAll("<", "\\<")
.replaceAll(">", "\\>");
const unescapeParentheses = (escaped) => escaped
.replaceAll("\\(", "(")
.replaceAll("\\)", ")");
const unescapeAngles = (escaped) => escaped
.replaceAll("\\<", "<")
.replaceAll("\\>", ">");
const referenceLinkDestination = (link, tokens) => {
const reference = getNestedTokenTextByType([ link ], "reference");
const id = reference && reference !== "[]" ?
reference.replace(/^\[/, "").replace(/\]$/, "") :
getNestedTokenTextByType([ link ], "labelText");
const definition = filterByPredicate(
filterByTypes(tokens, [ "definition" ]),
(d) => getNestedTokenTextByType([ d ], "definitionLabelString") === id
);
return getNestedTokenTextByType(definition, "definitionDestination");
};
const inlineLinkDestination = (link) => {
const text = getNestedTokenTextByType([ link ], "resourceDestination");
return text && unescapeParentheses(text);
};
const autolinkDestination = (link) => {
const text = getNestedTokenTextByType([ link ], "autolinkProtocol");
return text && unescapeAngles(text);
};
const autolinkFixInfo = (tokens, link) => {
if (isAutolink(link)) {
return null;
}
const destination = isInlineLink(link) ?
inlineLinkDestination(link) :
referenceLinkDestination(link, tokens);
return {
"editColumn": link.startColumn,
"insertText": `<${escapeAngles(destination)}>`,
"deleteCount": link.endColumn - link.startColumn
};
};
const inlineFixInfo = (tokens, link) => {
if (isInlineLink(link)) {
return null;
}
const destination = isAutolink(link) ?
autolinkDestination(link) :
referenceLinkDestination(link, tokens);
return {
"editColumn": link.startColumn,
"insertText":
`[${escapeSquares(destination)}](${escapeParentheses(destination)})`,
"deleteCount": link.endColumn - link.startColumn
};
};
module.exports = {
"names": [ "MD054", "link-image-style" ],
"description": "Link and image style",
"tags": [ "images", "links" ],
"function": ({ parsers, config }, onError) => {
const style = String(config.style || "mixed");
const links = filterByTypes(
parsers.micromark.tokens,
[ "autolink", "link", "image" ]
);
for (const link of links) {
const inlineLink = isInlineLink(link);
const autolink = isAutolink(link);
const range = [ link.startColumn, link.endColumn - link.startColumn ];
let fixInfo = null;
if (style === "autolink_only") {
fixInfo = autolinkFixInfo(parsers.micromark.tokens, link);
} else if (style === "inline_only") {
fixInfo = inlineFixInfo(parsers.micromark.tokens, link);
}
if (
fixInfo ||
(style === "reference_only" && (inlineLink || autolink)) ||
(style === "inline_or_reference" && autolink) ||
(style === "inline_or_autolink" && !(inlineLink || autolink)) ||
(style === "reference_or_autolink" && inlineLink)
) {
addErrorContext(
onError,
link.startLine,
link.text,
null,
null,
range,
fixInfo
);
}
}
}
};

View file

@ -52,7 +52,8 @@ const rules = [
...require("./md049-md050"), ...require("./md049-md050"),
require("./md051"), require("./md051"),
require("./md052"), require("./md052"),
require("./md053") require("./md053"),
require("./md054")
]; ];
for (const rule of rules) { for (const rule of rules) {
const name = rule.names[0].toLowerCase(); const name = rule.names[0].toLowerCase();

View file

@ -519,6 +519,24 @@ for (const rule of rules) {
} }
}; };
break; break;
case "MD054":
scheme.properties = {
"style": {
"description": "Link or image style should be consistent",
"type": "string",
"enum": [
"mixed",
"autolink_only",
"inline_only",
"reference_only",
"inline_or_reference",
"inline_or_autolink",
"reference_or_autolink"
],
"default": "mixed"
}
};
break;
default: default:
custom = false; custom = false;
break; break;

View file

@ -0,0 +1,20 @@
# Autolink Link Style
Text [url](https://example.com) text {MD054}
Text ![url](https://example.com) text {MD054}
Text [url] text {MD054}
Text ![url] text {MD054}
Text [text][url] text {MD054}
Text ![text][url] text {MD054}
Text <https://example.com> text
Text [url][] text {MD054}
[url]: https://example.com
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"link-image-reference-definitions": false,
"link-image-style": {
"style": "autolink_only"
}
} -->

View file

@ -0,0 +1,20 @@
# Inline Link Style
Text [url](https://example.com) text
Text ![url](https://example.com) text
Text [url] {MD054} text
Text ![url] {MD054} text
Text [text][url] {MD054} text
Text ![text][url] {MD054} text
Text <https://example.com> {MD054} text
Text [url][] text {MD054}
[url]: https://example.com
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"link-image-reference-definitions": false,
"link-image-style": {
"style": "inline_only"
}
} -->

View file

@ -0,0 +1,20 @@
# Inline Link Style
Text [url](https://example.com) text
Text ![url](https://example.com) text
Text [url] {MD054} text
Text ![url] {MD054} text
Text [text][url] {MD054} text
Text ![text][url] {MD054} text
Text <https://example.com> text
Text [url][] text {MD054}
[url]: https://example.com
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"link-image-reference-definitions": false,
"link-image-style": {
"style": "inline_or_autolink"
}
} -->

View file

@ -0,0 +1,20 @@
# Inline Link Style
Text [url](https://example.com) text
Text ![url](https://example.com) text
Text [url] text
Text ![url] text
Text [text][url] text
Text ![text][url] text
Text <https://example.com> {MD054} text
Text [url][] text
[url]: https://example.com
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"link-image-reference-definitions": false,
"link-image-style": {
"style": "inline_or_reference"
}
} -->

View file

@ -0,0 +1,18 @@
# Reference Link Style
Text [url {MD054}](https://example.com) text
Text ![url {MD054}](https://example.com) text
Text [url] text
Text ![url] text
Text [text][url] text
Text ![text][url] text
Text <https://example.com> {MD054} text
Text [url][] text
[url]: https://example.com
<!-- markdownlint-configure-file {
"link-image-style": {
"style": "reference_only"
}
} -->

View file

@ -0,0 +1,20 @@
# Inline Link Style
Text [url](https://example.com) text {MD054}
Text ![url](https://example.com) text {MD054}
Text [url] text
Text ![url] text
Text [text][url] text
Text ![text][url] text
Text <https://example.com> text
Text [url][] text
[url]: https://example.com
<!-- markdownlint-configure-file {
"no-bare-urls": false,
"link-image-reference-definitions": false,
"link-image-style": {
"style": "reference_or_autolink"
}
} -->

View file

@ -82,7 +82,7 @@ test("projectFiles", (t) => {
"schema/*.md" "schema/*.md"
])) ]))
.then((files) => { .then((files) => {
t.is(files.length, 58); t.is(files.length, 59);
const options = { const options = {
files, files,
"config": require("../.markdownlint.json") "config": require("../.markdownlint.json")
@ -839,7 +839,7 @@ test("customFileSystemAsync", (t) => new Promise((resolve) => {
})); }));
test("readme", async(t) => { test("readme", async(t) => {
t.plan(124); t.plan(126);
const tagToRules = {}; const tagToRules = {};
for (const rule of rules) { for (const rule of rules) {
for (const tag of rule.tags) { for (const tag of rule.tags) {
@ -914,7 +914,7 @@ test("readme", async(t) => {
}); });
test("validateJsonUsingConfigSchemaStrict", async(t) => { test("validateJsonUsingConfigSchemaStrict", async(t) => {
t.plan(162); t.plan(168);
const { addSchema, validate } = const { addSchema, validate } =
// eslint-disable-next-line n/file-extension-in-import // eslint-disable-next-line n/file-extension-in-import
await import("@hyperjump/json-schema/draft-07"); await import("@hyperjump/json-schema/draft-07");
@ -1046,7 +1046,7 @@ test("validateConfigExampleJson", async(t) => {
}); });
test("allBuiltInRulesHaveValidUrl", (t) => { test("allBuiltInRulesHaveValidUrl", (t) => {
t.plan(147); t.plan(150);
for (const rule of rules) { for (const rule of rules) {
// @ts-ignore // @ts-ignore
t.truthy(rule.information); t.truthy(rule.information);