markdownlint 0.37.4 → 0.39.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 +20 -0
- package/CONTRIBUTING.md +2 -3
- package/README.md +175 -87
- package/doc/Rules.md +273 -47
- package/doc/md001.md +14 -0
- package/doc/md009.md +8 -9
- package/doc/md010.md +6 -0
- package/doc/md027.md +8 -0
- package/doc/md029.md +2 -0
- package/doc/md033.md +7 -1
- 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/md055.md +1 -1
- package/doc/md056.md +1 -1
- package/doc/md059.md +33 -0
- package/doc/md060.md +70 -0
- package/helpers/README.md +2 -2
- package/helpers/helpers.cjs +174 -20
- package/helpers/micromark-helpers.cjs +37 -7
- package/helpers/package.json +1 -1
- package/lib/cache.mjs +25 -7
- package/lib/configuration-strict.d.ts +1494 -292
- package/lib/constants.mjs +6 -5
- package/lib/defer-require.cjs +2 -0
- package/lib/exports.d.mts +2 -2
- package/lib/exports.mjs +2 -2
- package/lib/markdownit.cjs +8 -14
- package/lib/markdownlint.d.mts +136 -52
- package/lib/markdownlint.mjs +469 -409
- package/lib/md001.mjs +6 -2
- package/lib/md004.mjs +7 -18
- package/lib/md007.mjs +2 -1
- package/lib/md009.mjs +9 -5
- package/lib/md010.mjs +1 -1
- package/lib/md022.mjs +5 -2
- package/lib/md025.mjs +23 -15
- package/lib/md027.mjs +11 -1
- package/lib/md029.mjs +25 -14
- package/lib/md031.mjs +0 -1
- package/lib/md032.mjs +3 -1
- package/lib/md033.mjs +28 -20
- package/lib/md034.mjs +2 -1
- package/lib/md036.mjs +16 -4
- package/lib/md037.mjs +10 -7
- package/lib/md038.mjs +52 -71
- package/lib/md041.mjs +54 -16
- package/lib/md042.mjs +4 -2
- package/lib/md043.mjs +4 -2
- package/lib/md044.mjs +1 -1
- package/lib/md045.mjs +3 -1
- package/lib/md046.mjs +6 -6
- package/lib/md051.mjs +13 -5
- package/lib/md052.mjs +2 -1
- package/lib/md053.mjs +1 -1
- package/lib/md054.mjs +7 -5
- package/lib/md055.mjs +7 -6
- package/lib/md056.mjs +1 -1
- package/lib/md059.mjs +71 -0
- package/lib/md060.mjs +121 -0
- package/lib/micromark-parse.mjs +17 -3
- package/lib/node-imports-browser.mjs +2 -0
- package/lib/parse-configuration.mjs +47 -0
- package/lib/rules.mjs +5 -1
- package/package.json +32 -30
- package/schema/.markdownlint.jsonc +97 -62
- package/schema/.markdownlint.yaml +86 -57
- package/schema/markdownlint-config-schema-strict.json +4713 -1436
- package/schema/markdownlint-config-schema.json +4713 -1436
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/md042.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { addErrorContext } from "../helpers/helpers.cjs";
|
|
|
4
4
|
import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
|
+
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
|
|
8
|
+
|
|
7
9
|
/** @type {import("markdownlint").Rule} */
|
|
8
10
|
export default {
|
|
9
11
|
"names": [ "MD042", "no-empty-links" ],
|
|
@@ -12,9 +14,9 @@ export default {
|
|
|
12
14
|
"parser": "micromark",
|
|
13
15
|
"function": function MD042(params, onError) {
|
|
14
16
|
const { definitions } = getReferenceLinkImageData();
|
|
15
|
-
const isReferenceDefinitionHash = (token) => {
|
|
17
|
+
const isReferenceDefinitionHash = (/** @type {MicromarkToken} */ token) => {
|
|
16
18
|
const definition = definitions.get(token.text.trim());
|
|
17
|
-
return (definition && (definition[1] === "#"));
|
|
19
|
+
return Boolean(definition && (definition[1] === "#"));
|
|
18
20
|
};
|
|
19
21
|
const links = filterByTypesCached([ "link" ]);
|
|
20
22
|
for (const link of links) {
|
package/lib/md043.mjs
CHANGED
|
@@ -21,8 +21,8 @@ export default {
|
|
|
21
21
|
let matchAny = false;
|
|
22
22
|
let hasError = false;
|
|
23
23
|
let anyHeadings = false;
|
|
24
|
-
const getExpected = () => requiredHeadings[i++] || "[None]";
|
|
25
|
-
const handleCase = (str) => (matchCase ? str : str.toLowerCase());
|
|
24
|
+
const getExpected = () => String(requiredHeadings[i++] || "[None]");
|
|
25
|
+
const handleCase = (/** @type {string} */ str) => (matchCase ? str : str.toLowerCase());
|
|
26
26
|
for (const heading of filterByTypesCached([ "atxHeading", "setextHeading" ])) {
|
|
27
27
|
if (!hasError) {
|
|
28
28
|
const headingText = getHeadingText(heading);
|
|
@@ -38,6 +38,8 @@ export default {
|
|
|
38
38
|
}
|
|
39
39
|
} else if (expected === "+") {
|
|
40
40
|
matchAny = true;
|
|
41
|
+
} else if (expected === "?") {
|
|
42
|
+
// Allow current, match next
|
|
41
43
|
} else if (handleCase(expected) === handleCase(actual)) {
|
|
42
44
|
matchAny = false;
|
|
43
45
|
} else if (matchAny) {
|
package/lib/md044.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export default {
|
|
|
17
17
|
"function": function MD044(params, onError) {
|
|
18
18
|
let names = params.config.names;
|
|
19
19
|
names = Array.isArray(names) ? names : [];
|
|
20
|
-
names.sort((a, b) => (b.length - a.length) || a.localeCompare(b));
|
|
20
|
+
names.sort((/** @type {string} */ a, /** @type {string} */ b) => (b.length - a.length) || a.localeCompare(b));
|
|
21
21
|
if (names.length === 0) {
|
|
22
22
|
// Nothing to check; avoid doing any work
|
|
23
23
|
return;
|
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/md046.mjs
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
import { addErrorDetailIf } from "../helpers/helpers.cjs";
|
|
4
4
|
import { filterByTypesCached } from "./cache.mjs";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
6
|
+
/** @typedef {import("markdownlint").MicromarkTokenType} MicromarkTokenType */
|
|
7
|
+
|
|
8
|
+
const tokenTypeToStyle = (/** @type {MicromarkTokenType} */ tokenType) => (tokenType === "codeFenced") ? "fenced" : "indented";
|
|
10
9
|
|
|
11
10
|
/** @type {import("markdownlint").Rule} */
|
|
12
11
|
export default {
|
|
@@ -19,13 +18,14 @@ export default {
|
|
|
19
18
|
for (const token of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
|
|
20
19
|
const { startLine, type } = token;
|
|
21
20
|
if (expectedStyle === "consistent") {
|
|
22
|
-
expectedStyle = tokenTypeToStyle
|
|
21
|
+
expectedStyle = tokenTypeToStyle(type);
|
|
23
22
|
}
|
|
24
23
|
addErrorDetailIf(
|
|
25
24
|
onError,
|
|
26
25
|
startLine,
|
|
27
26
|
expectedStyle,
|
|
28
|
-
tokenTypeToStyle
|
|
27
|
+
tokenTypeToStyle(type)
|
|
28
|
+
);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
};
|
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/md053.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export default {
|
|
|
16
16
|
const lines = params.lines;
|
|
17
17
|
const { references, shortcuts, definitions, duplicateDefinitions } =
|
|
18
18
|
getReferenceLinkImageData();
|
|
19
|
-
const singleLineDefinition = (line) => (
|
|
19
|
+
const singleLineDefinition = (/** @type {string} */ line) => (
|
|
20
20
|
line.replace(linkReferenceDefinitionRe, "").trim().length > 0
|
|
21
21
|
);
|
|
22
22
|
const deleteFixInfo = {
|
package/lib/md054.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { getDescendantsByType } from "../helpers/micromark-helpers.cjs";
|
|
|
5
5
|
import { getReferenceLinkImageData, filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
7
|
const backslashEscapeRe = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g;
|
|
8
|
-
const removeBackslashEscapes = (text) => text.replace(backslashEscapeRe, "$1");
|
|
8
|
+
const removeBackslashEscapes = (/** @type {string} **/ text) => text.replace(backslashEscapeRe, "$1");
|
|
9
9
|
const autolinkDisallowedRe = /[ <>]/;
|
|
10
|
-
const autolinkAble = (destination) => {
|
|
10
|
+
const autolinkAble = (/** @type {string} */ destination) => {
|
|
11
11
|
try {
|
|
12
12
|
// eslint-disable-next-line no-new
|
|
13
13
|
new URL(destination);
|
|
@@ -73,9 +73,11 @@ export default {
|
|
|
73
73
|
const referenceString = getDescendantsByType(link, [ "reference", "referenceString" ])[0]?.text;
|
|
74
74
|
const isCollapsed = (referenceString === undefined);
|
|
75
75
|
const definition = definitions.get(referenceString || label);
|
|
76
|
-
destination = definition && definition[1];
|
|
77
|
-
isError =
|
|
78
|
-
|
|
76
|
+
destination = (definition && definition[1]) || "";
|
|
77
|
+
isError = Boolean(
|
|
78
|
+
destination &&
|
|
79
|
+
(isShortcut ? !shortcut : (isCollapsed ? !collapsed : !full))
|
|
80
|
+
);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
if (isError) {
|
package/lib/md055.mjs
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
import { addErrorDetailIf } from "../helpers/helpers.cjs";
|
|
4
4
|
import { filterByTypesCached } from "./cache.mjs";
|
|
5
5
|
|
|
6
|
+
/** @typedef {import("markdownlint").MicromarkToken} MicromarkToken */
|
|
7
|
+
/** @typedef {import("micromark-extension-gfm-table")} */
|
|
8
|
+
|
|
6
9
|
const whitespaceTypes = new Set([ "linePrefix", "whitespace" ]);
|
|
7
|
-
const ignoreWhitespace = (tokens) => tokens.filter(
|
|
10
|
+
const ignoreWhitespace = (/** @type {MicromarkToken[]} */ tokens) => tokens.filter(
|
|
8
11
|
(token) => !whitespaceTypes.has(token.type)
|
|
9
12
|
);
|
|
10
|
-
const firstOrNothing = (items) => items[0];
|
|
11
|
-
const lastOrNothing = (items) => items[items.length - 1];
|
|
12
|
-
const makeRange = (start, end) => [ start, end - start + 1 ];
|
|
13
|
-
|
|
14
|
-
/** @typedef {import("micromark-extension-gfm-table")} */
|
|
13
|
+
const firstOrNothing = (/** @type {MicromarkToken[]} */ items) => items[0];
|
|
14
|
+
const lastOrNothing = (/** @type {MicromarkToken[]} */ items) => items[items.length - 1];
|
|
15
|
+
const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
|
|
15
16
|
|
|
16
17
|
/** @type {import("markdownlint").Rule} */
|
|
17
18
|
export default {
|
package/lib/md056.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { addErrorDetailIf } from "../helpers/helpers.cjs";
|
|
|
4
4
|
import { getParentOfType } from "../helpers/micromark-helpers.cjs";
|
|
5
5
|
import { filterByTypesCached } from "./cache.mjs";
|
|
6
6
|
|
|
7
|
-
const makeRange = (start, end) => [ start, end - start + 1 ];
|
|
7
|
+
const makeRange = (/** @type {number} */ start, /** @type {number} */ end) => [ start, end - start + 1 ];
|
|
8
8
|
|
|
9
9
|
/** @typedef {import("micromark-extension-gfm-table")} */
|
|
10
10
|
|
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/md060.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { filterByTypes } from "../helpers/micromark-helpers.cjs";
|
|
4
|
+
import { filterByTypesCached } from "./cache.mjs";
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("micromark-extension-gfm-table")} */
|
|
7
|
+
/** @typedef {import("markdownlint").RuleOnErrorInfo} RuleOnErrorInfo */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Adds a RuleOnErrorInfo object to a list of RuleOnErrorInfo objects.
|
|
11
|
+
*
|
|
12
|
+
* @param {RuleOnErrorInfo[]} errors List of errors.
|
|
13
|
+
* @param {number} lineNumber Line number.
|
|
14
|
+
* @param {number} column Column number.
|
|
15
|
+
* @param {string} detail Detail message.
|
|
16
|
+
*/
|
|
17
|
+
function addError(errors, lineNumber, column, detail) {
|
|
18
|
+
errors.push({
|
|
19
|
+
lineNumber,
|
|
20
|
+
detail,
|
|
21
|
+
"range": [ column, 1 ]
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @type {import("markdownlint").Rule} */
|
|
26
|
+
export default {
|
|
27
|
+
"names": [ "MD060", "table-column-style" ],
|
|
28
|
+
"description": "Table column style",
|
|
29
|
+
"tags": [ "table" ],
|
|
30
|
+
"parser": "micromark",
|
|
31
|
+
"function": function MD060(params, onError) {
|
|
32
|
+
const style = String(params.config.style || "any");
|
|
33
|
+
const styleAlignedAllowed = (style === "any") || (style === "aligned");
|
|
34
|
+
const styleCompactAllowed = (style === "any") || (style === "compact");
|
|
35
|
+
const styleTightAllowed = (style === "any") || (style === "tight");
|
|
36
|
+
|
|
37
|
+
// Scan all tables/rows
|
|
38
|
+
const tables = filterByTypesCached([ "table" ]);
|
|
39
|
+
for (const table of tables) {
|
|
40
|
+
const rows = filterByTypes(table.children, [ "tableDelimiterRow", "tableRow" ]);
|
|
41
|
+
const headingRow = rows[0];
|
|
42
|
+
|
|
43
|
+
// Determine errors for style "aligned"
|
|
44
|
+
/** @type {RuleOnErrorInfo[]} */
|
|
45
|
+
const errorsIfAligned = [];
|
|
46
|
+
if (styleAlignedAllowed) {
|
|
47
|
+
const headingDividerColumns = filterByTypes(headingRow.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn);
|
|
48
|
+
for (const row of rows.slice(1)) {
|
|
49
|
+
const remainingHeadingDividerColumns = new Set(headingDividerColumns);
|
|
50
|
+
const rowDividerColumns = filterByTypes(row.children, [ "tableCellDivider" ]).map((divider) => divider.startColumn);
|
|
51
|
+
for (const dividerColumn of rowDividerColumns) {
|
|
52
|
+
if ((remainingHeadingDividerColumns.size > 0) && !remainingHeadingDividerColumns.delete(dividerColumn)) {
|
|
53
|
+
addError(errorsIfAligned, row.startLine, dividerColumn, "Table pipe does not align with heading for style \"aligned\"");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine errors for styles "compact" and "tight"
|
|
60
|
+
/** @type {RuleOnErrorInfo[]} */
|
|
61
|
+
const errorsIfCompact = [];
|
|
62
|
+
/** @type {RuleOnErrorInfo[]} */
|
|
63
|
+
const errorsIfTight = [];
|
|
64
|
+
if (
|
|
65
|
+
(styleCompactAllowed || styleTightAllowed) &&
|
|
66
|
+
!(styleAlignedAllowed && (errorsIfAligned.length === 0))
|
|
67
|
+
) {
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const tokensOfInterest = filterByTypes(row.children, [ "tableCellDivider", "tableContent", "whitespace" ]);
|
|
70
|
+
for (let i = 0; i < tokensOfInterest.length; i++) {
|
|
71
|
+
const { startColumn, startLine, type } = tokensOfInterest[i];
|
|
72
|
+
if (type === "tableCellDivider") {
|
|
73
|
+
const previous = tokensOfInterest[i - 1];
|
|
74
|
+
if (previous) {
|
|
75
|
+
if (previous.type === "whitespace") {
|
|
76
|
+
if (previous.text.length !== 1) {
|
|
77
|
+
addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the left for style \"compact\"");
|
|
78
|
+
}
|
|
79
|
+
addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the left for style \"tight\"");
|
|
80
|
+
} else {
|
|
81
|
+
addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the left for style \"compact\"");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const next = tokensOfInterest[i + 1];
|
|
85
|
+
if (next) {
|
|
86
|
+
if (next.type === "whitespace") {
|
|
87
|
+
if (next.endColumn !== row.endColumn) {
|
|
88
|
+
if (next.text.length !== 1) {
|
|
89
|
+
addError(errorsIfCompact, startLine, startColumn, "Table pipe has extra space to the right for style \"compact\"");
|
|
90
|
+
}
|
|
91
|
+
addError(errorsIfTight, startLine, startColumn, "Table pipe has space to the right for style \"tight\"");
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
addError(errorsIfCompact, startLine, startColumn, "Table pipe is missing space to the right for style \"compact\"");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Report errors for whatever (allowed) style has the fewest
|
|
103
|
+
let errorInfos = errorsIfAligned;
|
|
104
|
+
if (
|
|
105
|
+
styleCompactAllowed &&
|
|
106
|
+
((errorsIfCompact.length < errorInfos.length) || !styleAlignedAllowed)
|
|
107
|
+
) {
|
|
108
|
+
errorInfos = errorsIfCompact;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
styleTightAllowed &&
|
|
112
|
+
((errorsIfTight.length < errorInfos.length) || (!styleAlignedAllowed && !styleCompactAllowed))
|
|
113
|
+
) {
|
|
114
|
+
errorInfos = errorsIfTight;
|
|
115
|
+
}
|
|
116
|
+
for (const errorInfo of errorInfos) {
|
|
117
|
+
onError(errorInfo);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
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("]")) {
|
|
@@ -201,7 +212,9 @@ function parseInternal(
|
|
|
201
212
|
const events = getEvents(markdown, micromarkParseOptions);
|
|
202
213
|
|
|
203
214
|
// Create Token objects
|
|
215
|
+
/** @type {MicromarkToken[]} */
|
|
204
216
|
const document = [];
|
|
217
|
+
/** @type {MicromarkToken[]} */
|
|
205
218
|
let flatTokens = [];
|
|
206
219
|
/** @type {MicromarkToken} */
|
|
207
220
|
const root = {
|
|
@@ -221,11 +234,11 @@ function parseInternal(
|
|
|
221
234
|
let lines = null;
|
|
222
235
|
let skipHtmlFlowChildren = false;
|
|
223
236
|
for (const event of events) {
|
|
224
|
-
const [ kind, token
|
|
237
|
+
const [ kind, token ] = event;
|
|
225
238
|
const { type, start, end } = token;
|
|
226
239
|
const { "column": startColumn, "line": startLine } = start;
|
|
227
240
|
const { "column": endColumn, "line": endLine } = end;
|
|
228
|
-
const text =
|
|
241
|
+
const text = getText(markdown, token);
|
|
229
242
|
if ((kind === "enter") && !skipHtmlFlowChildren) {
|
|
230
243
|
const previous = current;
|
|
231
244
|
history.push(previous);
|
|
@@ -271,6 +284,7 @@ function parseInternal(
|
|
|
271
284
|
);
|
|
272
285
|
current.children = tokens;
|
|
273
286
|
// Avoid stack overflow of Array.push(...spread)
|
|
287
|
+
// @ts-ignore
|
|
274
288
|
// eslint-disable-next-line unicorn/prefer-spread
|
|
275
289
|
flatTokens = flatTokens.concat(tokens[flatTokensSymbol]);
|
|
276
290
|
}
|
|
@@ -6,8 +6,10 @@ const throwForSync = () => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export const fs = {
|
|
9
|
+
// @ts-ignore
|
|
9
10
|
"access": (path, callback) => callback(getError()),
|
|
10
11
|
"accessSync": throwForSync,
|
|
12
|
+
// @ts-ignore
|
|
11
13
|
"readFile": (path, options, callback) => callback(getError()),
|
|
12
14
|
"readFileSync": throwForSync
|
|
13
15
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a call to parseConfiguration.
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object} ParseConfigurationResult
|
|
7
|
+
* @property {import("markdownlint").Configuration | 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").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
|
+
// eslint-disable-next-line jsdoc/reject-any-type
|
|
32
|
+
} catch(/** @type {any} */ error) {
|
|
33
|
+
errors.push(`Parser ${index++}: ${error?.message}`);
|
|
34
|
+
}
|
|
35
|
+
// Failed, try the next parser
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
// Message if unable to parse
|
|
39
|
+
if (failed) {
|
|
40
|
+
errors.unshift(`Unable to parse '${name}'`);
|
|
41
|
+
message = errors.join("; ");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
config,
|
|
45
|
+
message
|
|
46
|
+
};
|
|
47
|
+
}
|
package/lib/rules.mjs
CHANGED
|
@@ -53,6 +53,8 @@ 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";
|
|
57
|
+
import md060 from "./md060.mjs";
|
|
56
58
|
|
|
57
59
|
const rules = [
|
|
58
60
|
md001,
|
|
@@ -108,7 +110,9 @@ const rules = [
|
|
|
108
110
|
md055,
|
|
109
111
|
md056,
|
|
110
112
|
// md057: See https://github.com/markdownlint/markdownlint
|
|
111
|
-
md058
|
|
113
|
+
md058,
|
|
114
|
+
md059,
|
|
115
|
+
md060
|
|
112
116
|
];
|
|
113
117
|
for (const rule of rules) {
|
|
114
118
|
const name = rule.names[0].toLowerCase();
|