markdownlint 0.29.0 → 0.30.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 +7 -0
- package/demo/markdownlint-browser.js +713 -692
- package/doc/CustomRules.md +2 -0
- package/doc/Rules.md +33 -17
- package/doc/md022.md +12 -6
- package/doc/md026.md +1 -2
- package/doc/md049.md +2 -2
- package/doc/md050.md +2 -2
- package/doc/md051.md +17 -6
- package/helpers/helpers.js +83 -230
- package/helpers/micromark.cjs +145 -34
- package/helpers/package.json +2 -2
- package/helpers/shared.js +7 -0
- package/lib/cache.js +0 -2
- package/lib/constants.js +1 -1
- package/lib/markdownlint.d.ts +4 -0
- package/lib/markdownlint.js +21 -7
- package/lib/md022.js +80 -40
- package/lib/md026.js +19 -12
- package/lib/md032.js +57 -31
- package/lib/md033.js +19 -45
- package/lib/md037.js +74 -161
- package/lib/md045.js +17 -5
- package/lib/md051.js +90 -68
- package/package.json +23 -22
- package/schema/.markdownlint.jsonc +3 -3
- package/schema/.markdownlint.yaml +3 -3
- package/schema/markdownlint-config-schema.json +19 -7
package/lib/md032.js
CHANGED
|
@@ -4,46 +4,72 @@
|
|
|
4
4
|
|
|
5
5
|
const { addErrorContext, blockquotePrefixRe, isBlankLine } =
|
|
6
6
|
require("../helpers");
|
|
7
|
-
const {
|
|
7
|
+
const { filterByPredicate, flattenedChildren } =
|
|
8
|
+
require("../helpers/micromark.cjs");
|
|
9
|
+
|
|
10
|
+
const nonContentTokens = new Set([
|
|
11
|
+
"blockQuoteMarker",
|
|
12
|
+
"blockQuotePrefix",
|
|
13
|
+
"blockQuotePrefixWhitespace",
|
|
14
|
+
"lineEnding",
|
|
15
|
+
"lineEndingBlank",
|
|
16
|
+
"linePrefix",
|
|
17
|
+
"listItemIndent"
|
|
18
|
+
]);
|
|
19
|
+
const isList = (token) => (
|
|
20
|
+
(token.type === "listOrdered") || (token.type === "listUnordered")
|
|
21
|
+
);
|
|
22
|
+
const addBlankLineError = (onError, lines, lineIndex, lineNumber) => {
|
|
23
|
+
const line = lines[lineIndex];
|
|
24
|
+
const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
|
|
25
|
+
addErrorContext(
|
|
26
|
+
onError,
|
|
27
|
+
lineIndex + 1,
|
|
28
|
+
line.trim(),
|
|
29
|
+
null,
|
|
30
|
+
null,
|
|
31
|
+
null,
|
|
32
|
+
{
|
|
33
|
+
lineNumber,
|
|
34
|
+
"insertText": `${quotePrefix}\n`
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
};
|
|
8
38
|
|
|
9
39
|
module.exports = {
|
|
10
40
|
"names": [ "MD032", "blanks-around-lists" ],
|
|
11
41
|
"description": "Lists should be surrounded by blank lines",
|
|
12
42
|
"tags": [ "bullet", "ul", "ol", "blank_lines" ],
|
|
13
43
|
"function": function MD032(params, onError) {
|
|
14
|
-
const { lines } = params;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
44
|
+
const { lines, parsers } = params;
|
|
45
|
+
|
|
46
|
+
// For every top-level list...
|
|
47
|
+
const topLevelLists = filterByPredicate(
|
|
48
|
+
parsers.micromark.tokens,
|
|
49
|
+
isList,
|
|
50
|
+
(token) => (isList(token) ? [] : token.children)
|
|
51
|
+
);
|
|
52
|
+
for (const list of topLevelLists) {
|
|
53
|
+
|
|
54
|
+
// Look for a blank line above the list
|
|
55
|
+
const firstIndex = list.startLine - 1;
|
|
18
56
|
if (!isBlankLine(lines[firstIndex - 1])) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"insertText": `${quotePrefix}\n`
|
|
30
|
-
});
|
|
57
|
+
addBlankLineError(onError, lines, firstIndex);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find the "visual" end of the list
|
|
61
|
+
let endLine = list.endLine;
|
|
62
|
+
for (const child of flattenedChildren(list).reverse()) {
|
|
63
|
+
if (!nonContentTokens.has(child.type)) {
|
|
64
|
+
endLine = child.endLine;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
31
67
|
}
|
|
32
|
-
|
|
68
|
+
|
|
69
|
+
// Look for a blank line below the list
|
|
70
|
+
const lastIndex = endLine - 1;
|
|
33
71
|
if (!isBlankLine(lines[lastIndex + 1])) {
|
|
34
|
-
|
|
35
|
-
const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
|
|
36
|
-
addErrorContext(
|
|
37
|
-
onError,
|
|
38
|
-
lastIndex + 1,
|
|
39
|
-
line.trim(),
|
|
40
|
-
null,
|
|
41
|
-
null,
|
|
42
|
-
null,
|
|
43
|
-
{
|
|
44
|
-
"lineNumber": lastIndex + 2,
|
|
45
|
-
"insertText": `${quotePrefix}\n`
|
|
46
|
-
});
|
|
72
|
+
addBlankLineError(onError, lines, lastIndex, lastIndex + 2);
|
|
47
73
|
}
|
|
48
74
|
}
|
|
49
75
|
}
|
package/lib/md033.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const { addError } = require("../helpers");
|
|
6
|
-
const {
|
|
6
|
+
const { filterByHtmlTokens, getHtmlTagInfo } =
|
|
7
7
|
require("../helpers/micromark.cjs");
|
|
8
8
|
|
|
9
9
|
const nextLinesRe = /[\r\n][\s\S]*$/;
|
|
@@ -16,50 +16,24 @@ module.exports = {
|
|
|
16
16
|
let allowedElements = params.config.allowed_elements;
|
|
17
17
|
allowedElements = Array.isArray(allowedElements) ? allowedElements : [];
|
|
18
18
|
allowedElements = allowedElements.map((element) => element.toLowerCase());
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
token.startLine + offset,
|
|
38
|
-
"Element: " + htmlTagInfo.name,
|
|
39
|
-
undefined,
|
|
40
|
-
range
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
// token.type === "htmlFlow"
|
|
45
|
-
// Re-parse without "htmlFlow" to get only "htmlText" tokens
|
|
46
|
-
const options = {
|
|
47
|
-
"extensions": [
|
|
48
|
-
{
|
|
49
|
-
"disable": {
|
|
50
|
-
"null": [ "codeIndented", "htmlFlow" ]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
};
|
|
55
|
-
// Use lines instead of token.text for accurate columns
|
|
56
|
-
const lines =
|
|
57
|
-
params.lines.slice(token.startLine - 1, token.endLine).join("\n");
|
|
58
|
-
const flowTokens = parse(lines, options);
|
|
59
|
-
pending.push(
|
|
60
|
-
[ token.startLine - 1, flowTokens ]
|
|
61
|
-
);
|
|
62
|
-
}
|
|
19
|
+
for (const token of filterByHtmlTokens(params.parsers.micromark.tokens)) {
|
|
20
|
+
const htmlTagInfo = getHtmlTagInfo(token);
|
|
21
|
+
if (
|
|
22
|
+
htmlTagInfo &&
|
|
23
|
+
!htmlTagInfo.close &&
|
|
24
|
+
!allowedElements.includes(htmlTagInfo.name.toLowerCase())
|
|
25
|
+
) {
|
|
26
|
+
const range = [
|
|
27
|
+
token.startColumn,
|
|
28
|
+
token.text.replace(nextLinesRe, "").length
|
|
29
|
+
];
|
|
30
|
+
addError(
|
|
31
|
+
onError,
|
|
32
|
+
token.startLine,
|
|
33
|
+
"Element: " + htmlTagInfo.name,
|
|
34
|
+
undefined,
|
|
35
|
+
range
|
|
36
|
+
);
|
|
63
37
|
}
|
|
64
38
|
}
|
|
65
39
|
}
|
package/lib/md037.js
CHANGED
|
@@ -2,184 +2,97 @@
|
|
|
2
2
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
|
-
const {
|
|
6
|
-
withinAnyRange } = require("../helpers");
|
|
7
|
-
const { htmlElementRanges, lineMetadata } = require("./cache");
|
|
5
|
+
const { addError } = require("../helpers");
|
|
8
6
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const asteriskListItemMarkerRe = /^([\s>]*)\*(\s+)/;
|
|
12
|
-
const leftSpaceRe = /^\s+/;
|
|
13
|
-
const rightSpaceRe = /\s+$/;
|
|
14
|
-
const tablePipeRe = /\|/;
|
|
15
|
-
const allUnderscoresRe = /_/g;
|
|
7
|
+
const emphasisStartTextRe = /^(\S{1,3})(\s+)\S/;
|
|
8
|
+
const emphasisEndTextRe = /\S(\s+)(\S{1,3})$/;
|
|
16
9
|
|
|
17
10
|
module.exports = {
|
|
18
11
|
"names": [ "MD037", "no-space-in-emphasis" ],
|
|
19
12
|
"description": "Spaces inside emphasis markers",
|
|
20
13
|
"tags": [ "whitespace", "emphasis" ],
|
|
21
14
|
"function": function MD037(params, onError) {
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
emphasisIndex = -1;
|
|
29
|
-
emphasisLength = 0;
|
|
30
|
-
emphasisKind = "";
|
|
31
|
-
effectiveEmphasisLength = 0;
|
|
32
|
-
pendingError = null;
|
|
15
|
+
|
|
16
|
+
// Initialize variables
|
|
17
|
+
const { lines, parsers } = params;
|
|
18
|
+
const emphasisTokensByMarker = new Map();
|
|
19
|
+
for (const marker of [ "_", "__", "___", "*", "**", "***" ]) {
|
|
20
|
+
emphasisTokensByMarker.set(marker, []);
|
|
33
21
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
const pending = [ ...parsers.micromark.tokens ];
|
|
23
|
+
let token = null;
|
|
24
|
+
while ((token = pending.shift())) {
|
|
25
|
+
|
|
26
|
+
// Use reparsed children of htmlFlow tokens
|
|
27
|
+
if (token.type === "htmlFlow") {
|
|
28
|
+
pending.unshift(...token.htmlFlowChildren);
|
|
29
|
+
continue;
|
|
42
30
|
}
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
pending.push(...token.children);
|
|
32
|
+
|
|
33
|
+
// Build lists of bare tokens for each emphasis marker type
|
|
34
|
+
for (const emphasisTokens of emphasisTokensByMarker.values()) {
|
|
35
|
+
emphasisTokens.length = 0;
|
|
45
36
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const contextStart = emphasisIndex - emphasisLength;
|
|
54
|
-
const contextEnd = matchIndex + contextLength;
|
|
55
|
-
const column = contextStart + 1;
|
|
56
|
-
const length = contextEnd - contextStart;
|
|
57
|
-
if (!withinAnyRange(exclusions, lineIndex, column, length)) {
|
|
58
|
-
const context = line.substring(contextStart, contextEnd);
|
|
59
|
-
const leftMarker = line.substring(contextStart, emphasisIndex);
|
|
60
|
-
const rightMarker = match ? (match[2] || match[3]) : "";
|
|
61
|
-
const fixedText = `${leftMarker}${content.trim()}${rightMarker}`;
|
|
62
|
-
return [
|
|
63
|
-
onError,
|
|
64
|
-
lineIndex + 1,
|
|
65
|
-
context,
|
|
66
|
-
leftSpace,
|
|
67
|
-
rightSpace,
|
|
68
|
-
[ column, length ],
|
|
69
|
-
{
|
|
70
|
-
"editColumn": column,
|
|
71
|
-
"deleteCount": length,
|
|
72
|
-
"insertText": fixedText
|
|
73
|
-
}
|
|
74
|
-
];
|
|
37
|
+
for (const child of token.children) {
|
|
38
|
+
const { text, type } = child;
|
|
39
|
+
if ((type === "data") && (text.length <= 3)) {
|
|
40
|
+
const emphasisTokens = emphasisTokensByMarker.get(text);
|
|
41
|
+
if (emphasisTokens) {
|
|
42
|
+
emphasisTokens.push(child);
|
|
43
|
+
}
|
|
75
44
|
}
|
|
76
45
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
onBreak ||
|
|
101
|
-
inMath
|
|
102
|
-
) {
|
|
103
|
-
// Emphasis has no meaning here
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
let patchedLine = line.replace(
|
|
107
|
-
embeddedUnderscoreRe,
|
|
108
|
-
(match) => match.replace(allUnderscoresRe, " ")
|
|
109
|
-
);
|
|
110
|
-
if (onItemStart) {
|
|
111
|
-
// Trim overlapping '*' list item marker
|
|
112
|
-
patchedLine = patchedLine.replace(asteriskListItemMarkerRe, "$1 $2");
|
|
113
|
-
}
|
|
114
|
-
let match = null;
|
|
115
|
-
// Match all emphasis-looking runs in the line...
|
|
116
|
-
while ((match = emphasisRe.exec(patchedLine))) {
|
|
117
|
-
const ignoreMarkersForLine = ignoreMarkersByLine[lineIndex];
|
|
118
|
-
const matchIndex = match.index + match[1].length;
|
|
119
|
-
if (ignoreMarkersForLine.includes(matchIndex)) {
|
|
120
|
-
// Ignore emphasis markers inside code spans and links
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
const matchLength = match[0].length - match[1].length;
|
|
124
|
-
const matchKind = (match[2] || match[3])[0];
|
|
125
|
-
if (emphasisIndex === -1) {
|
|
126
|
-
// New run
|
|
127
|
-
emphasisIndex = matchIndex + matchLength;
|
|
128
|
-
emphasisLength = matchLength;
|
|
129
|
-
emphasisKind = matchKind;
|
|
130
|
-
effectiveEmphasisLength = matchLength;
|
|
131
|
-
} else if (matchKind === emphasisKind) {
|
|
132
|
-
// Matching emphasis markers
|
|
133
|
-
if (matchLength === effectiveEmphasisLength) {
|
|
134
|
-
// Ending an existing run, report any pending error
|
|
135
|
-
if (pendingError) {
|
|
136
|
-
// @ts-ignore
|
|
137
|
-
addErrorContext(...pendingError);
|
|
138
|
-
pendingError = null;
|
|
139
|
-
}
|
|
140
|
-
const error = handleRunEnd(
|
|
141
|
-
line,
|
|
142
|
-
lineIndex,
|
|
143
|
-
effectiveEmphasisLength,
|
|
144
|
-
match,
|
|
145
|
-
matchIndex,
|
|
146
|
-
inTable
|
|
46
|
+
|
|
47
|
+
// Process bare tokens for each emphasis marker type
|
|
48
|
+
for (const emphasisTokens of emphasisTokensByMarker.values()) {
|
|
49
|
+
for (let i = 0; i + 1 < emphasisTokens.length; i += 2) {
|
|
50
|
+
|
|
51
|
+
// Process start token of start/end pair
|
|
52
|
+
const startToken = emphasisTokens[i];
|
|
53
|
+
const startText =
|
|
54
|
+
lines[startToken.startLine - 1].slice(startToken.startColumn - 1);
|
|
55
|
+
const startMatch = startText.match(emphasisStartTextRe);
|
|
56
|
+
if (startMatch) {
|
|
57
|
+
const [ startContext, startMarker, startSpaces ] = startMatch;
|
|
58
|
+
if ((startMarker === startToken.text) && (startSpaces.length > 0)) {
|
|
59
|
+
addError(
|
|
60
|
+
onError,
|
|
61
|
+
startToken.startLine,
|
|
62
|
+
undefined,
|
|
63
|
+
startContext,
|
|
64
|
+
[ startToken.startColumn, startContext.length ],
|
|
65
|
+
{
|
|
66
|
+
"editColumn": startToken.endColumn,
|
|
67
|
+
"deleteCount": startSpaces.length
|
|
68
|
+
}
|
|
147
69
|
);
|
|
148
|
-
if (error) {
|
|
149
|
-
// @ts-ignore
|
|
150
|
-
addErrorContext(...error);
|
|
151
|
-
}
|
|
152
|
-
// Reset
|
|
153
|
-
resetRunTracking();
|
|
154
|
-
} else if (matchLength === 3) {
|
|
155
|
-
// Swap internal run length (1->2 or 2->1)
|
|
156
|
-
effectiveEmphasisLength = matchLength - effectiveEmphasisLength;
|
|
157
|
-
} else if (effectiveEmphasisLength === 3) {
|
|
158
|
-
// Downgrade internal run (3->1 or 3->2)
|
|
159
|
-
effectiveEmphasisLength -= matchLength;
|
|
160
|
-
} else {
|
|
161
|
-
// Upgrade to internal run (1->3 or 2->3)
|
|
162
|
-
effectiveEmphasisLength += matchLength;
|
|
163
70
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Process end token of start/end pair
|
|
74
|
+
const endToken = emphasisTokens[i + 1];
|
|
75
|
+
const endText =
|
|
76
|
+
lines[endToken.startLine - 1].slice(0, endToken.endColumn - 1);
|
|
77
|
+
const endMatch = endText.match(emphasisEndTextRe);
|
|
78
|
+
if (endMatch) {
|
|
79
|
+
const [ endContext, endSpace, endMarker ] = endMatch;
|
|
80
|
+
if ((endMarker === endToken.text) && (endSpace.length > 0)) {
|
|
81
|
+
addError(
|
|
82
|
+
onError,
|
|
83
|
+
endToken.startLine,
|
|
84
|
+
undefined,
|
|
85
|
+
endContext,
|
|
86
|
+
[ endToken.endColumn - endContext.length, endContext.length ],
|
|
87
|
+
{
|
|
88
|
+
"editColumn": endToken.startColumn - endSpace.length,
|
|
89
|
+
"deleteCount": endSpace.length
|
|
90
|
+
}
|
|
91
|
+
);
|
|
168
92
|
}
|
|
169
|
-
} else if (emphasisRe.lastIndex > 1) {
|
|
170
|
-
// Back up one character so RegExp has a chance to match the
|
|
171
|
-
// mis-matched marker (ex: "*text_*")
|
|
172
|
-
emphasisRe.lastIndex--;
|
|
173
93
|
}
|
|
174
94
|
}
|
|
175
|
-
if (emphasisIndex !== -1) {
|
|
176
|
-
pendingError = pendingError ||
|
|
177
|
-
handleRunEnd(line, lineIndex, 0, null, line.length, inTable);
|
|
178
|
-
// Adjust for pending run on new line
|
|
179
|
-
emphasisIndex = 0;
|
|
180
|
-
emphasisLength = 0;
|
|
181
|
-
}
|
|
182
95
|
}
|
|
183
|
-
|
|
96
|
+
}
|
|
184
97
|
}
|
|
185
98
|
};
|
package/lib/md045.js
CHANGED
|
@@ -2,17 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
|
-
const { addError
|
|
5
|
+
const { addError } = require("../helpers");
|
|
6
|
+
const { filterByTypes } = require("../helpers/micromark.cjs");
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
8
9
|
"names": [ "MD045", "no-alt-text" ],
|
|
9
10
|
"description": "Images should have alternate text (alt text)",
|
|
10
11
|
"tags": [ "accessibility", "images" ],
|
|
11
12
|
"function": function MD045(params, onError) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const images = filterByTypes(params.parsers.micromark.tokens, [ "image" ]);
|
|
14
|
+
for (const image of images) {
|
|
15
|
+
const labelTexts = filterByTypes(image.children, [ "labelText" ]);
|
|
16
|
+
if (labelTexts.some((labelText) => labelText.text.length === 0)) {
|
|
17
|
+
const range = (image.startLine === image.endLine) ?
|
|
18
|
+
[ image.startColumn, image.endColumn - image.startColumn ] :
|
|
19
|
+
undefined;
|
|
20
|
+
addError(
|
|
21
|
+
onError,
|
|
22
|
+
image.startLine,
|
|
23
|
+
undefined,
|
|
24
|
+
undefined,
|
|
25
|
+
range
|
|
26
|
+
);
|
|
15
27
|
}
|
|
16
|
-
}
|
|
28
|
+
}
|
|
17
29
|
}
|
|
18
30
|
};
|