Reimplement MD039/no-space-in-links using micromark tokens.

This commit is contained in:
David Anson 2024-08-03 17:39:30 -07:00
parent 92a19b6f36
commit daa155d5a1
6 changed files with 265 additions and 124 deletions

View file

@ -5656,10 +5656,9 @@ module.exports = {
const { addErrorContext, filterTokens } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"); const { addErrorContext } = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js");
const { filterByTypes } = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs");
const spaceInLinkRe = const { referenceLinkImageData } = __webpack_require__(/*! ./cache */ "../lib/cache.js");
/\[(?:\s[^\]]*|[^\]]*?\s)\](?=(\([^)]*\)|\[[^\]]*\]))/;
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("./markdownlint").Rule */ /** @type import("./markdownlint").Rule */
@ -5667,60 +5666,55 @@ module.exports = {
"names": [ "MD039", "no-space-in-links" ], "names": [ "MD039", "no-space-in-links" ],
"description": "Spaces inside link text", "description": "Spaces inside link text",
"tags": [ "whitespace", "links" ], "tags": [ "whitespace", "links" ],
"parser": "markdownit", "parser": "micromark",
"function": function MD039(params, onError) { "function": function MD039(params, onError) {
filterTokens(params, "inline", (token) => { const { definitions } = referenceLinkImageData();
const { children } = token; const labels = filterByTypes(
let { lineNumber } = token; params.parsers.micromark.tokens,
let inLink = false; [ "label" ]
let linkText = ""; ).filter((label) => label.parent?.type === "link");
let lineIndex = 0; for (const label of labels) {
for (const child of children) { const labelTexts = filterByTypes(label.children, [ "labelText" ]);
const { content, markup, type } = child; for (const labelText of labelTexts) {
if (type === "link_open") { const leftSpace =
inLink = true; labelText.text.trimStart().length !== labelText.text.length;
linkText = ""; const rightSpace =
} else if (type === "link_close") { labelText.text.trimEnd().length !== labelText.text.length;
inLink = false; if (
const left = linkText.trimStart().length !== linkText.length; (leftSpace || rightSpace) &&
const right = linkText.trimEnd().length !== linkText.length; // Ignore non-shortcut link content "[ text ]"
if (left || right) { ((label.parent?.children.length !== 1) || definitions.has(labelText.text.trim()))
const line = params.lines[lineNumber - 1]; ) {
let range = null; // eslint-disable-next-line no-undef-init
let fixInfo = null; let range = undefined;
const match = line.slice(lineIndex).match(spaceInLinkRe); if (label.startLine === label.endLine) {
if (match) { const labelColumn = label.startColumn;
// @ts-ignore const labelLength = label.endColumn - label.startColumn;
const column = match.index + lineIndex + 1; range = [ labelColumn, labelLength ];
const length = match[0].length;
range = [ column, length ];
fixInfo = {
"editColumn": column + 1,
"deleteCount": length - 2,
"insertText": linkText.trim()
};
lineIndex = column + length - 1;
}
addErrorContext(
onError,
lineNumber,
`[${linkText}]`,
left,
right,
range,
fixInfo
);
} }
} else if ((type === "softbreak") || (type === "hardbreak")) { // eslint-disable-next-line no-undef-init
lineNumber++; let fixInfo = undefined;
lineIndex = 0; if (labelText.startLine === labelText.endLine) {
} else if (inLink) { const textColumn = labelText.startColumn;
linkText += type.endsWith("_inline") ? const textLength = labelText.endColumn - labelText.startColumn;
`${markup}${content}${markup}` : fixInfo = {
(content || markup); "editColumn": textColumn,
"deleteCount": textLength,
"insertText": labelText.text.trim()
};
}
addErrorContext(
onError,
labelText.startLine,
label.text.replace(/\s+/g, " "),
leftSpace,
rightSpace,
range,
fixInfo
);
} }
} }
}); }
} }
}; };

View file

@ -2,10 +2,9 @@
"use strict"; "use strict";
const { addErrorContext, filterTokens } = require("../helpers"); const { addErrorContext } = require("../helpers");
const { filterByTypes } = require("../helpers/micromark.cjs");
const spaceInLinkRe = const { referenceLinkImageData } = require("./cache");
/\[(?:\s[^\]]*|[^\]]*?\s)\](?=(\([^)]*\)|\[[^\]]*\]))/;
// eslint-disable-next-line jsdoc/valid-types // eslint-disable-next-line jsdoc/valid-types
/** @type import("./markdownlint").Rule */ /** @type import("./markdownlint").Rule */
@ -13,59 +12,54 @@ module.exports = {
"names": [ "MD039", "no-space-in-links" ], "names": [ "MD039", "no-space-in-links" ],
"description": "Spaces inside link text", "description": "Spaces inside link text",
"tags": [ "whitespace", "links" ], "tags": [ "whitespace", "links" ],
"parser": "markdownit", "parser": "micromark",
"function": function MD039(params, onError) { "function": function MD039(params, onError) {
filterTokens(params, "inline", (token) => { const { definitions } = referenceLinkImageData();
const { children } = token; const labels = filterByTypes(
let { lineNumber } = token; params.parsers.micromark.tokens,
let inLink = false; [ "label" ]
let linkText = ""; ).filter((label) => label.parent?.type === "link");
let lineIndex = 0; for (const label of labels) {
for (const child of children) { const labelTexts = filterByTypes(label.children, [ "labelText" ]);
const { content, markup, type } = child; for (const labelText of labelTexts) {
if (type === "link_open") { const leftSpace =
inLink = true; labelText.text.trimStart().length !== labelText.text.length;
linkText = ""; const rightSpace =
} else if (type === "link_close") { labelText.text.trimEnd().length !== labelText.text.length;
inLink = false; if (
const left = linkText.trimStart().length !== linkText.length; (leftSpace || rightSpace) &&
const right = linkText.trimEnd().length !== linkText.length; // Ignore non-shortcut link content "[ text ]"
if (left || right) { ((label.parent?.children.length !== 1) || definitions.has(labelText.text.trim()))
const line = params.lines[lineNumber - 1]; ) {
let range = null; // eslint-disable-next-line no-undef-init
let fixInfo = null; let range = undefined;
const match = line.slice(lineIndex).match(spaceInLinkRe); if (label.startLine === label.endLine) {
if (match) { const labelColumn = label.startColumn;
// @ts-ignore const labelLength = label.endColumn - label.startColumn;
const column = match.index + lineIndex + 1; range = [ labelColumn, labelLength ];
const length = match[0].length;
range = [ column, length ];
fixInfo = {
"editColumn": column + 1,
"deleteCount": length - 2,
"insertText": linkText.trim()
};
lineIndex = column + length - 1;
}
addErrorContext(
onError,
lineNumber,
`[${linkText}]`,
left,
right,
range,
fixInfo
);
} }
} else if ((type === "softbreak") || (type === "hardbreak")) { // eslint-disable-next-line no-undef-init
lineNumber++; let fixInfo = undefined;
lineIndex = 0; if (labelText.startLine === labelText.endLine) {
} else if (inLink) { const textColumn = labelText.startColumn;
linkText += type.endsWith("_inline") ? const textLength = labelText.endColumn - labelText.startColumn;
`${markup}${content}${markup}` : fixInfo = {
(content || markup); "editColumn": textColumn,
"deleteCount": textLength,
"insertText": labelText.text.trim()
};
}
addErrorContext(
onError,
labelText.startLine,
label.text.replace(/\s+/g, " "),
leftSpace,
rightSpace,
range,
fixInfo
);
} }
} }
}); }
} }
}; };

View file

@ -1104,20 +1104,13 @@ test("someCustomRulesHaveValidUrl", (t) => {
}); });
test("markdownItPluginsSingle", (t) => new Promise((resolve) => { test("markdownItPluginsSingle", (t) => new Promise((resolve) => {
t.plan(2); t.plan(4);
markdownlint({ markdownlint({
"strings": { "strings": {
"string": "# Heading\n\nText [ link ](https://example.com)\n" "string": "# Heading\n\nText\n"
}, },
"markdownItPlugins": [ "markdownItPlugins": [
[ [ pluginInline, "check_text_plugin", "text", () => t.true(true) ]
pluginInline,
"trim_text_plugin",
"text",
function iterator(tokens, index) {
tokens[index].content = tokens[index].content.trim();
}
]
] ]
}, function callback(err, actual) { }, function callback(err, actual) {
t.falsy(err); t.falsy(err);

View file

@ -51692,11 +51692,11 @@ Generated by [AVA](https://avajs.dev).
], ],
}, },
{ {
errorContext: '[ link with leading space]', errorContext: '[ link with lea...space {MD039} ]',
errorDetail: null, errorDetail: null,
errorRange: null, errorRange: null,
fixInfo: null, fixInfo: null,
lineNumber: 52, lineNumber: 51,
ruleDescription: 'Spaces inside link text', ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md', ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [ ruleNames: [
@ -51784,6 +51784,126 @@ Generated by [AVA](https://avajs.dev).
'no-space-in-links', 'no-space-in-links',
], ],
}, },
{
errorContext: '[ref ]',
errorDetail: null,
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 4,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 68,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
{
errorContext: '[ ref]',
errorDetail: null,
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 4,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 70,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
{
errorContext: '[ ref ]',
errorDetail: null,
errorRange: [
1,
7,
],
fixInfo: {
deleteCount: 5,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 72,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
{
errorContext: '[ref ]',
errorDetail: null,
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 4,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 76,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
{
errorContext: '[ ref]',
errorDetail: null,
errorRange: [
1,
6,
],
fixInfo: {
deleteCount: 4,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 78,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
{
errorContext: '[ ref ]',
errorDetail: null,
errorRange: [
1,
7,
],
fixInfo: {
deleteCount: 5,
editColumn: 2,
insertText: 'ref',
},
lineNumber: 80,
ruleDescription: 'Spaces inside link text',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md039.md',
ruleNames: [
'MD039',
'no-space-in-links',
],
},
], ],
fixed: `# Spaces Inside Link Text␊ fixed: `# Spaces Inside Link Text␊
@ -51835,8 +51955,8 @@ Generated by [AVA](https://avajs.dev).
[with](spaces) ␊ [with](spaces) ␊
[error]({MD039})␊ [error]({MD039})␊
Wrapped [ link with leading space␊ Wrapped [ link with leading space {MD039}
](https://example.com) {MD039} ](https://example.com)␊
Non-wrapped [link with leading space](https://example.com) {MD039}␊ Non-wrapped [link with leading space](https://example.com) {MD039}␊
@ -51850,7 +51970,27 @@ Generated by [AVA](https://avajs.dev).
[link][ref] {MD039}␊ [link][ref] {MD039}␊
[ref]␊
[ref] {MD039}␊
[ref] {MD039}␊
[ref] {MD039}␊
[ref][]␊
[ref][] {MD039}␊
[ref][] {MD039}␊
[ref][] {MD039}␊
[ref]: https://example.com␊ [ref]: https://example.com␊
Not a link, just [ text in ] brackets␊
Images are ![ not links ](image.jpg)␊
`, `,
} }

View file

@ -48,8 +48,8 @@ function MoreCodeButNotCode(input) {
[with](spaces) [with](spaces)
[error ]({MD039}) [error ]({MD039})
Wrapped [ link with leading space Wrapped [ link with leading space {MD039}
](https://example.com) {MD039} ](https://example.com)
Non-wrapped [ link with leading space](https://example.com) {MD039} Non-wrapped [ link with leading space](https://example.com) {MD039}
@ -63,4 +63,24 @@ Non-wrapped [ link with leading space](https://example.com) {MD039}
[ link ][ref] {MD039} [ link ][ref] {MD039}
[ref]
[ref ] {MD039}
[ ref] {MD039}
[ ref ] {MD039}
[ref][]
[ref ][] {MD039}
[ ref][] {MD039}
[ ref ][] {MD039}
[ref]: https://example.com [ref]: https://example.com
Not a link, just [ text in ] brackets
Images are ![ not links ](image.jpg)