markdownlint 0.37.4 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +2 -3
- package/README.md +39 -21
- package/doc/Rules.md +158 -35
- package/doc/md027.md +8 -0
- package/doc/md038.md +23 -11
- package/doc/md041.md +31 -16
- package/doc/md043.md +22 -4
- package/doc/md045.md +9 -1
- package/doc/md051.md +17 -0
- package/doc/md052.md +12 -0
- package/doc/md053.md +3 -3
- package/doc/md059.md +30 -0
- package/helpers/README.md +2 -2
- package/helpers/helpers.cjs +4 -6
- package/helpers/micromark-helpers.cjs +29 -3
- package/helpers/package.json +1 -1
- package/lib/cache.mjs +19 -5
- package/lib/configuration-strict.d.ts +190 -126
- package/lib/constants.mjs +1 -1
- package/lib/exports.d.mts +1 -0
- package/lib/exports.mjs +1 -0
- package/lib/markdownit.cjs +6 -13
- package/lib/markdownlint.d.mts +36 -2
- package/lib/markdownlint.mjs +306 -282
- package/lib/md025.mjs +23 -15
- package/lib/md027.mjs +11 -1
- package/lib/md036.mjs +12 -2
- package/lib/md038.mjs +52 -71
- package/lib/md041.mjs +54 -16
- package/lib/md043.mjs +2 -0
- package/lib/md045.mjs +3 -1
- package/lib/md051.mjs +13 -5
- package/lib/md052.mjs +2 -1
- package/lib/md059.mjs +71 -0
- package/lib/micromark-parse.mjs +14 -3
- package/lib/parse-configuration.mjs +46 -0
- package/lib/rules.mjs +3 -1
- package/package.json +28 -25
- package/schema/.markdownlint.jsonc +82 -60
- package/schema/.markdownlint.yaml +74 -56
- package/schema/markdownlint-config-schema-strict.json +259 -144
- package/schema/markdownlint-config-schema.json +259 -144
package/lib/md025.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
|
|
4
|
-
import { getHeadingLevel, getHeadingText } from "../helpers/micromark-helpers.cjs";
|
|
4
|
+
import { getHeadingLevel, getHeadingText, isDocfxTab, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
7
|
/** @type {import("markdownlint").Rule} */
|
|
@@ -12,24 +12,32 @@ export default {
|
|
|
12
12
|
"parser": "micromark",
|
|
13
13
|
"function": function MD025(params, onError) {
|
|
14
14
|
const level = Number(params.config.level || 1);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
const { tokens } = params.parsers.micromark;
|
|
16
|
+
const matchingHeadings = filterByTypesCached([ "atxHeading", "setextHeading" ])
|
|
17
|
+
.filter((heading) => (level === getHeadingLevel(heading)) && !isDocfxTab(heading));
|
|
18
|
+
if (matchingHeadings.length > 0) {
|
|
19
|
+
const foundFrontMatterTitle =
|
|
20
|
+
frontMatterHasTitle(
|
|
21
|
+
params.frontMatterLines,
|
|
22
|
+
params.config.front_matter_title
|
|
23
|
+
);
|
|
24
|
+
// Front matter title counts as a top-level heading if present
|
|
25
|
+
let hasTopLevelHeading = foundFrontMatterTitle;
|
|
26
|
+
if (!hasTopLevelHeading) {
|
|
27
|
+
// Check if the first matching heading is a top-level heading
|
|
28
|
+
const previousTokens = tokens.slice(0, tokens.indexOf(matchingHeadings[0]));
|
|
29
|
+
hasTopLevelHeading = previousTokens.every(
|
|
30
|
+
(token) => nonContentTokens.has(token.type) || isHtmlFlowComment(token)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (hasTopLevelHeading) {
|
|
34
|
+
// All other matching headings are violations
|
|
35
|
+
for (const heading of matchingHeadings.slice(foundFrontMatterTitle ? 0 : 1)) {
|
|
26
36
|
addErrorContext(
|
|
27
37
|
onError,
|
|
28
38
|
heading.startLine,
|
|
29
|
-
|
|
39
|
+
getHeadingText(heading)
|
|
30
40
|
);
|
|
31
|
-
} else if (heading.startLine === 1) {
|
|
32
|
-
hasTopLevelHeading = true;
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
}
|
package/lib/md027.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { addErrorContext } from "../helpers/helpers.cjs";
|
|
4
|
+
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
|
|
4
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
5
6
|
|
|
7
|
+
/** @type {import("../helpers/micromark-helpers.cjs").TokenType[]} */
|
|
8
|
+
const listTypes = [ "listOrdered", "listUnordered" ];
|
|
9
|
+
|
|
6
10
|
/** @type {import("markdownlint").Rule} */
|
|
7
11
|
export default {
|
|
8
12
|
"names": [ "MD027", "no-multiple-space-blockquote" ],
|
|
@@ -10,6 +14,8 @@ export default {
|
|
|
10
14
|
"tags": [ "blockquote", "whitespace", "indentation" ],
|
|
11
15
|
"parser": "micromark",
|
|
12
16
|
"function": function MD027(params, onError) {
|
|
17
|
+
const listItems = params.config.list_items;
|
|
18
|
+
const includeListItems = (listItems === undefined) ? true : !!listItems;
|
|
13
19
|
const { tokens } = params.parsers.micromark;
|
|
14
20
|
for (const token of filterByTypesCached([ "linePrefix" ])) {
|
|
15
21
|
const parent = token.parent;
|
|
@@ -17,7 +23,11 @@ export default {
|
|
|
17
23
|
const siblings = parent?.children || tokens;
|
|
18
24
|
if (
|
|
19
25
|
!codeIndented &&
|
|
20
|
-
(siblings[siblings.indexOf(token) - 1]?.type === "blockQuotePrefix")
|
|
26
|
+
(siblings[siblings.indexOf(token) - 1]?.type === "blockQuotePrefix") &&
|
|
27
|
+
(includeListItems || (
|
|
28
|
+
!listTypes.includes(siblings[siblings.indexOf(token) + 1]?.type) &&
|
|
29
|
+
!getParentOfType(token, listTypes)
|
|
30
|
+
))
|
|
21
31
|
) {
|
|
22
32
|
const { startColumn, startLine, text } = token;
|
|
23
33
|
const { length } = text;
|
package/lib/md036.mjs
CHANGED
|
@@ -11,6 +11,11 @@ const emphasisTypes = [
|
|
|
11
11
|
[ "strong", "strongText" ]
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
+
const isParagraphChildMeaningful = (token) => !(
|
|
15
|
+
(token.type === "htmlText") ||
|
|
16
|
+
((token.type === "data") && (token.text.trim().length === 0))
|
|
17
|
+
);
|
|
18
|
+
|
|
14
19
|
/** @type {import("markdownlint").Rule} */
|
|
15
20
|
export default {
|
|
16
21
|
"names": [ "MD036", "no-emphasis-as-heading" ],
|
|
@@ -22,9 +27,14 @@ export default {
|
|
|
22
27
|
punctuation = String((punctuation === undefined) ? allPunctuation : punctuation);
|
|
23
28
|
const punctuationRe = new RegExp("[" + punctuation + "]$");
|
|
24
29
|
const paragraphTokens =
|
|
25
|
-
filterByTypesCached([ "paragraph" ])
|
|
30
|
+
filterByTypesCached([ "paragraph" ], true)
|
|
26
31
|
.filter((token) =>
|
|
27
|
-
(token.parent?.type === "content") &&
|
|
32
|
+
(token.parent?.type === "content") &&
|
|
33
|
+
(
|
|
34
|
+
!token.parent?.parent ||
|
|
35
|
+
((token.parent?.parent?.type === "htmlFlow") && !token.parent?.parent?.parent)
|
|
36
|
+
) &&
|
|
37
|
+
(token.children.filter(isParagraphChildMeaningful).length === 1)
|
|
28
38
|
);
|
|
29
39
|
for (const emphasisType of emphasisTypes) {
|
|
30
40
|
const textTokens = getDescendantsByType(paragraphTokens, emphasisType);
|
package/lib/md038.mjs
CHANGED
|
@@ -4,19 +4,6 @@ import { addErrorContext } from "../helpers/helpers.cjs";
|
|
|
4
4
|
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
|
-
const leftSpaceRe = /^\s(?:[^`]|$)/;
|
|
8
|
-
const rightSpaceRe = /[^`]\s$/;
|
|
9
|
-
const trimCodeText = (text, start, end) => {
|
|
10
|
-
text = text.replace(/^\s+$/, "");
|
|
11
|
-
if (start) {
|
|
12
|
-
text = text.replace(/^\s+?(\s`|\S)/, "$1");
|
|
13
|
-
}
|
|
14
|
-
if (end) {
|
|
15
|
-
text = text.replace(/(`\s|\S)\s+$/, "$1");
|
|
16
|
-
}
|
|
17
|
-
return text;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
7
|
/** @type {import("markdownlint").Rule} */
|
|
21
8
|
export default {
|
|
22
9
|
"names": [ "MD038", "no-space-in-code" ],
|
|
@@ -26,65 +13,59 @@ export default {
|
|
|
26
13
|
"function": function MD038(params, onError) {
|
|
27
14
|
const codeTexts = filterByTypesCached([ "codeText" ]);
|
|
28
15
|
for (const codeText of codeTexts) {
|
|
29
|
-
const sequences = getDescendantsByType(codeText, [ "codeTextSequence" ]);
|
|
30
|
-
const startSequence = sequences[0];
|
|
31
|
-
const endSequence = sequences[sequences.length - 1];
|
|
32
16
|
const datas = getDescendantsByType(codeText, [ "codeTextData" ]);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
fixInfo
|
|
86
|
-
);
|
|
87
|
-
}
|
|
17
|
+
if (datas.length > 0) {
|
|
18
|
+
const paddings = getDescendantsByType(codeText, [ "codeTextPadding" ]);
|
|
19
|
+
// Check for extra space at start of code
|
|
20
|
+
const startPadding = paddings[0];
|
|
21
|
+
const startData = datas[0];
|
|
22
|
+
const startMatch = /^(\s+)(\S)/.exec(startData.text) || [ null, "", "" ];
|
|
23
|
+
const startBacktick = (startMatch[2] === "`");
|
|
24
|
+
const startCount = startMatch[1].length - ((startBacktick && !startPadding) ? 1 : 0);
|
|
25
|
+
const startSpaces = startCount > 0;
|
|
26
|
+
// Check for extra space at end of code
|
|
27
|
+
const endPadding = paddings[paddings.length - 1];
|
|
28
|
+
const endData = datas[datas.length - 1];
|
|
29
|
+
const endMatch = /(\S)(\s+)$/.exec(endData.text) || [ null, "", "" ];
|
|
30
|
+
const endBacktick = (endMatch[1] === "`");
|
|
31
|
+
const endCount = endMatch[2].length - ((endBacktick && !endPadding) ? 1 : 0);
|
|
32
|
+
const endSpaces = endCount > 0;
|
|
33
|
+
// Check if safe to remove 1-space padding
|
|
34
|
+
const removePadding = startSpaces && endSpaces && startPadding && endPadding && !startBacktick && !endBacktick;
|
|
35
|
+
const context = codeText.text;
|
|
36
|
+
// If extra space at start, report violation
|
|
37
|
+
if (startSpaces) {
|
|
38
|
+
const startColumn = (removePadding ? startPadding : startData).startColumn;
|
|
39
|
+
const length = startCount + (removePadding ? startPadding.text.length : 0);
|
|
40
|
+
addErrorContext(
|
|
41
|
+
onError,
|
|
42
|
+
startData.startLine,
|
|
43
|
+
context,
|
|
44
|
+
true,
|
|
45
|
+
false,
|
|
46
|
+
[ startColumn, length ],
|
|
47
|
+
{
|
|
48
|
+
"editColumn": startColumn,
|
|
49
|
+
"deleteCount": length
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
// If extra space at end, report violation
|
|
54
|
+
if (endSpaces) {
|
|
55
|
+
const endColumn = (removePadding ? endPadding : endData).endColumn;
|
|
56
|
+
const length = endCount + (removePadding ? endPadding.text.length : 0);
|
|
57
|
+
addErrorContext(
|
|
58
|
+
onError,
|
|
59
|
+
endData.endLine,
|
|
60
|
+
context,
|
|
61
|
+
false,
|
|
62
|
+
true,
|
|
63
|
+
[ endColumn - length, length ],
|
|
64
|
+
{
|
|
65
|
+
"editColumn": endColumn - length,
|
|
66
|
+
"deleteCount": length
|
|
67
|
+
}
|
|
68
|
+
);
|
|
88
69
|
}
|
|
89
70
|
}
|
|
90
71
|
}
|
package/lib/md041.mjs
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
import { addErrorContext, frontMatterHasTitle } from "../helpers/helpers.cjs";
|
|
4
4
|
import { filterByTypes, getHeadingLevel, getHtmlTagInfo, isHtmlFlowComment, nonContentTokens } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
|
|
6
|
+
const headingTagNameRe = /^h[1-6]$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gets the HTML tag name of an htmlFlow token.
|
|
10
|
+
*
|
|
11
|
+
* @param {import("markdownlint").MicromarkToken} token Micromark Token.
|
|
12
|
+
* @returns {string | null} Tag name.
|
|
13
|
+
*/
|
|
14
|
+
function getHtmlFlowTagName(token) {
|
|
15
|
+
const { children, type } = token;
|
|
16
|
+
if (type === "htmlFlow") {
|
|
17
|
+
const htmlTexts = filterByTypes(children, [ "htmlText" ], true);
|
|
18
|
+
const tagInfo = (htmlTexts.length > 0) && getHtmlTagInfo(htmlTexts[0]);
|
|
19
|
+
if (tagInfo) {
|
|
20
|
+
return tagInfo.name.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
/** @type {import("markdownlint").Rule} */
|
|
7
27
|
export default {
|
|
8
28
|
"names": [ "MD041", "first-line-heading", "first-line-h1" ],
|
|
@@ -10,24 +30,42 @@ export default {
|
|
|
10
30
|
"tags": [ "headings" ],
|
|
11
31
|
"parser": "micromark",
|
|
12
32
|
"function": function MD041(params, onError) {
|
|
33
|
+
const allowPreamble = !!params.config.allow_preamble;
|
|
13
34
|
const level = Number(params.config.level || 1);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
35
|
+
const { tokens } = params.parsers.micromark;
|
|
36
|
+
if (
|
|
37
|
+
!frontMatterHasTitle(
|
|
38
|
+
params.frontMatterLines,
|
|
39
|
+
params.config.front_matter_title
|
|
40
|
+
)
|
|
41
|
+
) {
|
|
42
|
+
let errorLineNumber = 0;
|
|
43
|
+
for (const token of tokens) {
|
|
44
|
+
const { startLine, type } = token;
|
|
45
|
+
if (!nonContentTokens.has(type) && !isHtmlFlowComment(token)) {
|
|
46
|
+
let tagName = null;
|
|
47
|
+
if ((type === "atxHeading") || (type === "setextHeading")) {
|
|
48
|
+
// First heading needs to have the expected level
|
|
49
|
+
if (getHeadingLevel(token) !== level) {
|
|
50
|
+
errorLineNumber = startLine;
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
} else if ((tagName = getHtmlFlowTagName(token)) && headingTagNameRe.test(tagName)) {
|
|
54
|
+
// First HTML element needs to have an <h?> with the expected level
|
|
55
|
+
if (tagName !== `h${level}`) {
|
|
56
|
+
errorLineNumber = startLine;
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
} else if (!allowPreamble) {
|
|
60
|
+
// First non-content needs to be a heading with the expected level
|
|
61
|
+
errorLineNumber = startLine;
|
|
62
|
+
break;
|
|
28
63
|
}
|
|
29
|
-
|
|
30
|
-
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (errorLineNumber > 0) {
|
|
67
|
+
addErrorContext(onError, errorLineNumber, params.lines[errorLineNumber - 1]);
|
|
68
|
+
}
|
|
31
69
|
}
|
|
32
70
|
}
|
|
33
71
|
};
|
package/lib/md043.mjs
CHANGED
package/lib/md045.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { getHtmlTagInfo, getDescendantsByType } from "../helpers/micromark-helpe
|
|
|
5
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
7
|
const altRe = getHtmlAttributeRe("alt");
|
|
8
|
+
const ariaHiddenRe = getHtmlAttributeRe("aria-hidden");
|
|
8
9
|
|
|
9
10
|
/** @type {import("markdownlint").Rule} */
|
|
10
11
|
export default {
|
|
@@ -40,7 +41,8 @@ export default {
|
|
|
40
41
|
htmlTagInfo &&
|
|
41
42
|
!htmlTagInfo.close &&
|
|
42
43
|
(htmlTagInfo.name.toLowerCase() === "img") &&
|
|
43
|
-
!altRe.test(text)
|
|
44
|
+
!altRe.test(text) &&
|
|
45
|
+
(ariaHiddenRe.exec(text)?.[1].toLowerCase() !== "true")
|
|
44
46
|
) {
|
|
45
47
|
const range = [
|
|
46
48
|
startColumn,
|
package/lib/md051.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import { addError, getHtmlAttributeRe } from "../helpers/helpers.cjs";
|
|
4
|
-
import { filterByPredicate, filterByTypes, getHtmlTagInfo } from "../helpers/micromark-helpers.cjs";
|
|
4
|
+
import { filterByPredicate, filterByTypes, getHtmlTagInfo, isDocfxTab } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
7
|
// Regular expression for identifying HTML anchor names
|
|
@@ -66,7 +66,10 @@ export default {
|
|
|
66
66
|
"parser": "micromark",
|
|
67
67
|
"function": function MD051(params, onError) {
|
|
68
68
|
const ignoreCase = params.config.ignore_case || false;
|
|
69
|
-
const
|
|
69
|
+
const ignoredPattern = params.config.ignored_pattern || "";
|
|
70
|
+
const ignoredPatternRe = new RegExp(ignoredPattern || "^$");
|
|
71
|
+
/** @type {Map<string, number>} */
|
|
72
|
+
const fragments = new Map([ [ "#top", 0 ] ]);
|
|
70
73
|
|
|
71
74
|
// Process headings
|
|
72
75
|
const headingTexts = filterByTypesCached([ "atxHeadingText", "setextHeadingText" ]);
|
|
@@ -107,18 +110,23 @@ export default {
|
|
|
107
110
|
[ "definition", "definitionDestinationString" ]
|
|
108
111
|
];
|
|
109
112
|
for (const [ parentType, definitionType ] of parentChilds) {
|
|
110
|
-
const links = filterByTypesCached([ parentType ])
|
|
113
|
+
const links = filterByTypesCached([ parentType ])
|
|
114
|
+
.filter(
|
|
115
|
+
(link) => !((link.parent?.type === "atxHeadingText") && isDocfxTab(link.parent.parent))
|
|
116
|
+
);
|
|
111
117
|
for (const link of links) {
|
|
112
118
|
const definitions = filterByTypes(link.children, [ definitionType ]);
|
|
113
119
|
for (const definition of definitions) {
|
|
114
120
|
const { endColumn, startColumn } = definition;
|
|
115
121
|
const text = unescapeStringTokenText(definition);
|
|
116
|
-
const
|
|
122
|
+
const textSliceOne = text.slice(1);
|
|
123
|
+
const encodedText = `#${encodeURIComponent(textSliceOne)}`;
|
|
117
124
|
if (
|
|
118
125
|
(text.length > 1) &&
|
|
119
126
|
text.startsWith("#") &&
|
|
120
127
|
!fragments.has(encodedText) &&
|
|
121
|
-
!lineFragmentRe.test(encodedText)
|
|
128
|
+
!lineFragmentRe.test(encodedText) &&
|
|
129
|
+
!ignoredPatternRe.test(textSliceOne)
|
|
122
130
|
) {
|
|
123
131
|
let context = undefined;
|
|
124
132
|
let range = undefined;
|
package/lib/md052.mjs
CHANGED
|
@@ -13,6 +13,7 @@ export default {
|
|
|
13
13
|
"function": function MD052(params, onError) {
|
|
14
14
|
const { config, lines } = params;
|
|
15
15
|
const shortcutSyntax = config.shortcut_syntax || false;
|
|
16
|
+
const ignoredLabels = new Set(config.ignored_labels || [ "x" ]);
|
|
16
17
|
const { definitions, references, shortcuts } = getReferenceLinkImageData();
|
|
17
18
|
const entries = shortcutSyntax ?
|
|
18
19
|
[ ...references.entries(), ...shortcuts.entries() ] :
|
|
@@ -20,7 +21,7 @@ export default {
|
|
|
20
21
|
// Look for links/images that use an undefined link reference
|
|
21
22
|
for (const reference of entries) {
|
|
22
23
|
const [ label, datas ] = reference;
|
|
23
|
-
if (!definitions.has(label)) {
|
|
24
|
+
if (!definitions.has(label) && !ignoredLabels.has(label)) {
|
|
24
25
|
for (const data of datas) {
|
|
25
26
|
const [ lineIndex, index, length ] = data;
|
|
26
27
|
// Context will be incomplete if reporting for a multi-line link
|
package/lib/md059.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { addErrorContext } from "../helpers/helpers.cjs";
|
|
4
|
+
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
|
|
5
|
+
import { filterByTypesCached } from "./cache.mjs";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
|
|
8
|
+
/** @type {Set<MicromarkTokenType>} */
|
|
9
|
+
const allowedChildrenTypes = new Set([
|
|
10
|
+
"codeText",
|
|
11
|
+
"htmlText"
|
|
12
|
+
]);
|
|
13
|
+
const defaultProhibitedTexts = [
|
|
14
|
+
"click here",
|
|
15
|
+
"here",
|
|
16
|
+
"link",
|
|
17
|
+
"more"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalizes a string by removing extra whitespaces and punctuation.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} str String to normalize.
|
|
24
|
+
* @returns {string} Normalized string.
|
|
25
|
+
*/
|
|
26
|
+
function normalize(str) {
|
|
27
|
+
return str
|
|
28
|
+
.replace(/[\W_]+/g, " ")
|
|
29
|
+
.replace(/\s+/g, " ")
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @type {import("markdownlint").Rule} */
|
|
35
|
+
export default {
|
|
36
|
+
"names": [ "MD059", "descriptive-link-text" ],
|
|
37
|
+
"description": "Link text should be descriptive",
|
|
38
|
+
"tags": [ "accessibility", "links" ],
|
|
39
|
+
"parser": "micromark",
|
|
40
|
+
"function": function MD059(params, onError) {
|
|
41
|
+
const prohibitedTexts = new Set(
|
|
42
|
+
(params.config.prohibited_texts || defaultProhibitedTexts).map(normalize)
|
|
43
|
+
);
|
|
44
|
+
if (prohibitedTexts.size > 0) {
|
|
45
|
+
const links = filterByTypesCached([ "link" ]);
|
|
46
|
+
for (const link of links) {
|
|
47
|
+
const labelTexts = getDescendantsByType(link, [ "label", "labelText" ]);
|
|
48
|
+
for (const labelText of labelTexts) {
|
|
49
|
+
const { children, endColumn, endLine, parent, startColumn, startLine, text } = labelText;
|
|
50
|
+
if (
|
|
51
|
+
!children.some((child) => allowedChildrenTypes.has(child.type)) &&
|
|
52
|
+
prohibitedTexts.has(normalize(text))
|
|
53
|
+
) {
|
|
54
|
+
const range = (startLine === endLine) ?
|
|
55
|
+
[ startColumn, endColumn - startColumn ] :
|
|
56
|
+
undefined;
|
|
57
|
+
addErrorContext(
|
|
58
|
+
onError,
|
|
59
|
+
startLine,
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
parent.text,
|
|
62
|
+
undefined,
|
|
63
|
+
undefined,
|
|
64
|
+
range
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
package/lib/micromark-parse.mjs
CHANGED
|
@@ -20,6 +20,17 @@ import { flatTokensSymbol, htmlFlowSymbol, newLineRe } from "../helpers/shared.c
|
|
|
20
20
|
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
|
|
21
21
|
/** @typedef {import("./micromark-types.d.mts")} */
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Gets the Markdown text for a Micromark token.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} markdown Markdown content.
|
|
27
|
+
* @param {Token} token Micromark token.
|
|
28
|
+
* @returns {string} Token text.
|
|
29
|
+
*/
|
|
30
|
+
function getText(markdown, token) {
|
|
31
|
+
return markdown.slice(token.start.offset, token.end.offset);
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
/**
|
|
24
35
|
* Parse options.
|
|
25
36
|
*
|
|
@@ -126,7 +137,7 @@ export function getEvents(
|
|
|
126
137
|
// Create artificial event list and replicate content
|
|
127
138
|
const text = eventsToReplicate
|
|
128
139
|
.filter((event) => event[0] === "enter")
|
|
129
|
-
.map((event) =>
|
|
140
|
+
.map((event) => getText(markdown, event[1]))
|
|
130
141
|
.join("")
|
|
131
142
|
.trim();
|
|
132
143
|
if ((text.length > 0) && !text.includes("]")) {
|
|
@@ -221,11 +232,11 @@ function parseInternal(
|
|
|
221
232
|
let lines = null;
|
|
222
233
|
let skipHtmlFlowChildren = false;
|
|
223
234
|
for (const event of events) {
|
|
224
|
-
const [ kind, token
|
|
235
|
+
const [ kind, token ] = event;
|
|
225
236
|
const { type, start, end } = token;
|
|
226
237
|
const { "column": startColumn, "line": startLine } = start;
|
|
227
238
|
const { "column": endColumn, "line": endLine } = end;
|
|
228
|
-
const text =
|
|
239
|
+
const text = getText(markdown, token);
|
|
229
240
|
if ((kind === "enter") && !skipHtmlFlowChildren) {
|
|
230
241
|
const previous = current;
|
|
231
242
|
history.push(previous);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a call to parseConfiguration.
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object} ParseConfigurationResult
|
|
7
|
+
* @property {Object | null} config Configuration object if successful.
|
|
8
|
+
* @property {string | null} message Error message if an error occurred.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse the content of a configuration file.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} name Name of the configuration file.
|
|
15
|
+
* @param {string} content Configuration content.
|
|
16
|
+
* @param {import("./markdownlint.mjs").ConfigurationParser[]} [parsers] Parsing function(s).
|
|
17
|
+
* @returns {ParseConfigurationResult} Parse configuration result.
|
|
18
|
+
*/
|
|
19
|
+
export default function parseConfiguration(name, content, parsers) {
|
|
20
|
+
let config = null;
|
|
21
|
+
let message = null;
|
|
22
|
+
const errors = [];
|
|
23
|
+
let index = 0;
|
|
24
|
+
// Try each parser
|
|
25
|
+
const failed = (parsers || [ JSON.parse ]).every((parser) => {
|
|
26
|
+
try {
|
|
27
|
+
const result = parser(content);
|
|
28
|
+
config = (result && (typeof result === "object") && !Array.isArray(result)) ? result : {};
|
|
29
|
+
// Succeeded
|
|
30
|
+
return false;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
errors.push(`Parser ${index++}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
// Failed, try the next parser
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
// Message if unable to parse
|
|
38
|
+
if (failed) {
|
|
39
|
+
errors.unshift(`Unable to parse '${name}'`);
|
|
40
|
+
message = errors.join("; ");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
config,
|
|
44
|
+
message
|
|
45
|
+
};
|
|
46
|
+
}
|
package/lib/rules.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import md054 from "./md054.mjs";
|
|
|
53
53
|
import md055 from "./md055.mjs";
|
|
54
54
|
import md056 from "./md056.mjs";
|
|
55
55
|
import md058 from "./md058.mjs";
|
|
56
|
+
import md059 from "./md059.mjs";
|
|
56
57
|
|
|
57
58
|
const rules = [
|
|
58
59
|
md001,
|
|
@@ -108,7 +109,8 @@ const rules = [
|
|
|
108
109
|
md055,
|
|
109
110
|
md056,
|
|
110
111
|
// md057: See https://github.com/markdownlint/markdownlint
|
|
111
|
-
md058
|
|
112
|
+
md058,
|
|
113
|
+
md059
|
|
112
114
|
];
|
|
113
115
|
for (const rule of rules) {
|
|
114
116
|
const name = rule.names[0].toLowerCase();
|