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/lib/md032.js CHANGED
@@ -4,46 +4,72 @@
4
4
 
5
5
  const { addErrorContext, blockquotePrefixRe, isBlankLine } =
6
6
  require("../helpers");
7
- const { flattenedLists } = require("./cache");
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
- const filteredLists = flattenedLists().filter((list) => !list.nesting);
16
- for (const list of filteredLists) {
17
- const firstIndex = list.open.map[0];
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
- const line = lines[firstIndex];
20
- const quotePrefix = line.match(blockquotePrefixRe)[0].trimEnd();
21
- addErrorContext(
22
- onError,
23
- firstIndex + 1,
24
- line.trim(),
25
- null,
26
- null,
27
- null,
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
- const lastIndex = list.lastLineIndex - 1;
68
+
69
+ // Look for a blank line below the list
70
+ const lastIndex = endLine - 1;
33
71
  if (!isBlankLine(lines[lastIndex + 1])) {
34
- const line = lines[lastIndex];
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 { filterByTypes, getHtmlTagInfo, parse } =
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 pending = [ [ 0, params.parsers.micromark.tokens ] ];
20
- let current = null;
21
- while ((current = pending.shift())) {
22
- const [ offset, tokens ] = current;
23
- for (const token of filterByTypes(tokens, [ "htmlFlow", "htmlText" ])) {
24
- if (token.type === "htmlText") {
25
- const htmlTagInfo = getHtmlTagInfo(token);
26
- if (
27
- htmlTagInfo &&
28
- !htmlTagInfo.close &&
29
- !allowedElements.includes(htmlTagInfo.name.toLowerCase())
30
- ) {
31
- const range = [
32
- token.startColumn,
33
- token.text.replace(nextLinesRe, "").length
34
- ];
35
- addError(
36
- onError,
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 { addErrorContext, emphasisMarkersInContent, forEachLine, isBlankLine,
6
- withinAnyRange } = require("../helpers");
7
- const { htmlElementRanges, lineMetadata } = require("./cache");
5
+ const { addError } = require("../helpers");
8
6
 
9
- const emphasisRe = /(^|[^\\]|\\\\)(?:(\*{1,3})|(_{1,3}))/g;
10
- const embeddedUnderscoreRe = /([A-Za-z\d])(_([A-Za-z\d]))+/g;
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
- const exclusions = htmlElementRanges();
23
- // eslint-disable-next-line init-declarations
24
- let effectiveEmphasisLength, emphasisIndex, emphasisKind, emphasisLength,
25
- pendingError = null;
26
- // eslint-disable-next-line jsdoc/require-jsdoc
27
- function resetRunTracking() {
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
- // eslint-disable-next-line jsdoc/require-jsdoc
35
- function handleRunEnd(
36
- line, lineIndex, contextLength, match, matchIndex, inTable
37
- ) {
38
- // Close current run
39
- let content = line.substring(emphasisIndex, matchIndex);
40
- if (!emphasisLength) {
41
- content = content.trimStart();
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
- if (!match) {
44
- content = content.trimEnd();
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 leftSpace = leftSpaceRe.test(content);
47
- const rightSpace = rightSpaceRe.test(content);
48
- if (
49
- (leftSpace || rightSpace) &&
50
- (!inTable || !tablePipeRe.test(content))
51
- ) {
52
- // Report the violation
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
- return null;
78
- }
79
- // Initialize
80
- const ignoreMarkersByLine = emphasisMarkersInContent(params);
81
- resetRunTracking();
82
- forEachLine(
83
- lineMetadata(),
84
- (line, lineIndex, inCode, onFence, inTable, inItem, onBreak, inMath) => {
85
- const onItemStart = (inItem === 1);
86
- if (
87
- inCode ||
88
- onFence ||
89
- inTable ||
90
- onBreak ||
91
- onItemStart ||
92
- isBlankLine(line)
93
- ) {
94
- // Emphasis resets when leaving a block
95
- resetRunTracking();
96
- }
97
- if (
98
- inCode ||
99
- onFence ||
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
- // Back up one character so RegExp has a chance to match the
165
- // next marker (ex: "**star**_underscore_")
166
- if (emphasisRe.lastIndex > 1) {
167
- emphasisRe.lastIndex--;
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, forEachInlineChild } = require("../helpers");
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
- forEachInlineChild(params, "image", function forToken(token) {
13
- if (token.content === "") {
14
- addError(onError, token.lineNumber);
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
  };