markdownlint 0.28.2 → 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.
@@ -4,9 +4,11 @@
4
4
 
5
5
  // @ts-ignore
6
6
  const {
7
- gfmAutolinkLiteral, gfmFootnote, gfmTable, parse, postprocess, preprocess
7
+ gfmAutolinkLiteral, gfmFootnote, gfmTable, math, parse, postprocess,
8
+ preprocess
8
9
  // @ts-ignore
9
10
  } = require("markdownlint-micromark");
11
+ const { newLineRe } = require("./shared.js");
10
12
 
11
13
  /**
12
14
  * Markdown token.
@@ -19,27 +21,40 @@ const {
19
21
  * @property {number} endColumn End column (1-based).
20
22
  * @property {string} text Token text.
21
23
  * @property {Token[]} children Child tokens.
24
+ * @property {Token[]} [htmlFlowChildren] Child tokens for htmlFlow.
22
25
  */
23
26
 
24
27
  /**
25
28
  * Parses a Markdown document and returns Micromark events.
26
29
  *
27
30
  * @param {string} markdown Markdown document.
28
- * @param {Object} [options] Options for micromark.
31
+ * @param {Object} [micromarkOptions] Options for micromark.
32
+ * @param {boolean} [referencesDefined] Treat references as defined.
29
33
  * @returns {Object[]} Micromark events.
30
34
  */
31
- function getMicromarkEvents(markdown, options = {}) {
35
+ function getMicromarkEvents(
36
+ markdown,
37
+ micromarkOptions = {},
38
+ referencesDefined = true
39
+ ) {
32
40
 
33
41
  // Customize options object to add useful extensions
34
- options.extensions = options.extensions || [];
35
- options.extensions.push(gfmAutolinkLiteral, gfmFootnote(), gfmTable);
42
+ micromarkOptions.extensions = micromarkOptions.extensions || [];
43
+ micromarkOptions.extensions.push(
44
+ gfmAutolinkLiteral(),
45
+ gfmFootnote(),
46
+ gfmTable(),
47
+ math()
48
+ );
36
49
 
37
50
  // Use micromark to parse document into Events
38
51
  const encoding = undefined;
39
52
  const eol = true;
40
- const parseContext = parse(options);
41
- // Customize ParseContext to treat all references as defined
42
- parseContext.defined.includes = (searchElement) => searchElement.length > 0;
53
+ const parseContext = parse(micromarkOptions);
54
+ if (referencesDefined) {
55
+ // Customize ParseContext to treat all references as defined
56
+ parseContext.defined.includes = (searchElement) => searchElement.length > 0;
57
+ }
43
58
  const chunks = preprocess()(markdown, encoding, eol);
44
59
  const events = postprocess(parseContext.document().write(chunks));
45
60
  return events;
@@ -49,13 +64,21 @@ function getMicromarkEvents(markdown, options = {}) {
49
64
  * Parses a Markdown document and returns (frozen) tokens.
50
65
  *
51
66
  * @param {string} markdown Markdown document.
52
- * @param {Object} [options] Options for micromark.
67
+ * @param {Object} micromarkOptions Options for micromark.
68
+ * @param {boolean} referencesDefined Treat references as defined.
69
+ * @param {number} lineDelta Offset to apply to start/end line.
53
70
  * @returns {Token[]} Micromark tokens (frozen).
54
71
  */
55
- function micromarkParse(markdown, options = {}) {
56
-
72
+ function micromarkParseWithOffset(
73
+ markdown,
74
+ micromarkOptions,
75
+ referencesDefined,
76
+ lineDelta
77
+ ) {
57
78
  // Use micromark to parse document into Events
58
- const events = getMicromarkEvents(markdown, options);
79
+ const events = getMicromarkEvents(
80
+ markdown, micromarkOptions, referencesDefined
81
+ );
59
82
 
60
83
  // Create Token objects
61
84
  const document = [];
@@ -63,29 +86,50 @@ function micromarkParse(markdown, options = {}) {
63
86
  "children": document
64
87
  };
65
88
  const history = [ current ];
89
+ let reparseOptions = null;
90
+ let lines = null;
66
91
  for (const event of events) {
67
92
  const [ kind, token, context ] = event;
68
93
  const { type, start, end } = token;
69
94
  const { "column": startColumn, "line": startLine } = start;
70
95
  const { "column": endColumn, "line": endLine } = end;
71
- let text = null;
72
- try {
73
- text = context.sliceSerialize(token);
74
- } catch {
75
- // https://github.com/micromark/micromark/issues/131
76
- }
96
+ const text = context.sliceSerialize(token);
77
97
  if (kind === "enter") {
78
98
  const previous = current;
79
99
  history.push(previous);
80
100
  current = {
81
101
  type,
82
- startLine,
102
+ "startLine": startLine + lineDelta,
83
103
  startColumn,
84
- endLine,
104
+ "endLine": endLine + lineDelta,
85
105
  endColumn,
86
106
  text,
87
107
  "children": []
88
108
  };
109
+ if (current.type === "htmlFlow") {
110
+ if (!reparseOptions || !lines) {
111
+ reparseOptions = {
112
+ ...micromarkOptions,
113
+ "extensions": [
114
+ {
115
+ "disable": {
116
+ "null": [ "codeIndented", "htmlFlow" ]
117
+ }
118
+ }
119
+ ]
120
+ };
121
+ lines = markdown.split(newLineRe);
122
+ }
123
+ const reparseMarkdown = lines
124
+ .slice(current.startLine - 1, current.endLine)
125
+ .join("\n");
126
+ current.htmlFlowChildren = micromarkParseWithOffset(
127
+ reparseMarkdown,
128
+ reparseOptions,
129
+ referencesDefined,
130
+ current.startLine - 1
131
+ );
132
+ }
89
133
  previous.children.push(current);
90
134
  } else if (kind === "exit") {
91
135
  Object.freeze(current.children);
@@ -100,6 +144,27 @@ function micromarkParse(markdown, options = {}) {
100
144
  return document;
101
145
  }
102
146
 
147
+ /**
148
+ * Parses a Markdown document and returns (frozen) tokens.
149
+ *
150
+ * @param {string} markdown Markdown document.
151
+ * @param {Object} [micromarkOptions] Options for micromark.
152
+ * @param {boolean} [referencesDefined] Treat references as defined.
153
+ * @returns {Token[]} Micromark tokens (frozen).
154
+ */
155
+ function micromarkParse(
156
+ markdown,
157
+ micromarkOptions = {},
158
+ referencesDefined = true
159
+ ) {
160
+ return micromarkParseWithOffset(
161
+ markdown,
162
+ micromarkOptions,
163
+ referencesDefined,
164
+ 0
165
+ );
166
+ }
167
+
103
168
  /**
104
169
  * Filter a list of Micromark tokens by predicate.
105
170
  *
@@ -139,6 +204,68 @@ function filterByTypes(tokens, allowed) {
139
204
  );
140
205
  }
141
206
 
207
+ /**
208
+ * Filter a list of Micromark tokens for HTML tokens.
209
+ *
210
+ * @param {Token[]} tokens Micromark tokens.
211
+ * @returns {Token[]} Filtered tokens.
212
+ */
213
+ function filterByHtmlTokens(tokens) {
214
+ const result = [];
215
+ const pending = [ tokens ];
216
+ let current = null;
217
+ while ((current = pending.shift())) {
218
+ for (const token of filterByTypes(current, [ "htmlFlow", "htmlText" ])) {
219
+ if (token.type === "htmlText") {
220
+ result.push(token);
221
+ } else {
222
+ // token.type === "htmlFlow"
223
+ // @ts-ignore
224
+ pending.push(token.htmlFlowChildren);
225
+ }
226
+ }
227
+ }
228
+ return result;
229
+ }
230
+
231
+ /**
232
+ * Returns a list of all nested child tokens.
233
+ *
234
+ * @param {Token} parent Micromark token.
235
+ * @returns {Token[]} Flattened children.
236
+ */
237
+ function flattenedChildren(parent) {
238
+ const result = [];
239
+ const pending = [ ...parent.children ];
240
+ let token = null;
241
+ while ((token = pending.shift())) {
242
+ result.push(token);
243
+ pending.unshift(...token.children);
244
+ }
245
+ return result;
246
+ }
247
+
248
+ /**
249
+ * Gets the heading level of a Micromark heading tokan.
250
+ *
251
+ * @param {Token} heading Micromark heading token.
252
+ * @returns {number} Heading level.
253
+ */
254
+ function getHeadingLevel(heading) {
255
+ const headingSequence = filterByTypes(
256
+ heading.children,
257
+ [ "atxHeadingSequence", "setextHeadingLineSequence" ]
258
+ );
259
+ let level = 1;
260
+ const { text } = headingSequence[0];
261
+ if (text[0] === "#") {
262
+ level = Math.min(text.length, 6);
263
+ } else if (text[0] === "-") {
264
+ level = 2;
265
+ }
266
+ return level;
267
+ }
268
+
142
269
  /**
143
270
  * Gets information about the tag in an HTML token.
144
271
  *
@@ -198,12 +325,27 @@ function matchAndGetTokensByType(tokens, matchTypes, resultTypes) {
198
325
  return result;
199
326
  }
200
327
 
328
+ /**
329
+ * Returns the specified token iff it is of the desired type.
330
+ *
331
+ * @param {Token} token Micromark token candidate.
332
+ * @param {string} type Desired type.
333
+ * @returns {Token | null} Token instance.
334
+ */
335
+ function tokenIfType(token, type) {
336
+ return (token && (token.type === type)) ? token : null;
337
+ }
338
+
201
339
  module.exports = {
202
340
  "parse": micromarkParse,
341
+ filterByHtmlTokens,
203
342
  filterByPredicate,
204
343
  filterByTypes,
344
+ flattenedChildren,
345
+ getHeadingLevel,
205
346
  getHtmlTagInfo,
206
347
  getMicromarkEvents,
207
348
  getTokenTextByType,
208
- matchAndGetTokensByType
349
+ matchAndGetTokensByType,
350
+ tokenIfType
209
351
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdownlint-rule-helpers",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "A collection of markdownlint helper functions for custom rules",
5
5
  "main": "./helpers.js",
6
6
  "exports": "./helpers.js",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "bugs": "https://github.com/DavidAnson/markdownlint/issues",
15
15
  "engines": {
16
- "node": ">=14.18.0"
16
+ "node": ">=16"
17
17
  },
18
18
  "dependencies": {
19
19
  "markdownlint-micromark": "0.1.2"
@@ -0,0 +1,7 @@
1
+ // @ts-check
2
+
3
+ "use strict";
4
+
5
+ // Regular expression for matching common newline characters
6
+ // See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
7
+ module.exports.newLineRe = /\r\n?|\n/g;
package/lib/cache.js CHANGED
@@ -15,8 +15,6 @@ module.exports.codeBlockAndSpanRanges =
15
15
  () => map.get("codeBlockAndSpanRanges");
16
16
  module.exports.flattenedLists =
17
17
  () => map.get("flattenedLists");
18
- module.exports.htmlElementRanges =
19
- () => map.get("htmlElementRanges");
20
18
  module.exports.lineMetadata =
21
19
  () => map.get("lineMetadata");
22
20
  module.exports.referenceLinkImageData =
package/lib/constants.js CHANGED
@@ -11,4 +11,4 @@ module.exports.fixableRuleNames = [
11
11
  "MD044", "MD047", "MD049", "MD050", "MD051", "MD053"
12
12
  ];
13
13
  module.exports.homepage = "https://github.com/DavidAnson/markdownlint";
14
- module.exports.version = "0.28.2";
14
+ module.exports.version = "0.30.0";
@@ -218,6 +218,10 @@ type RuleOnErrorInfo = {
218
218
  * Context for the error.
219
219
  */
220
220
  context?: string;
221
+ /**
222
+ * Link to more information.
223
+ */
224
+ information?: URL;
221
225
  /**
222
226
  * Column number (1-based) and length.
223
227
  */
@@ -59,7 +59,7 @@ function validateRuleList(ruleList, synchronous) {
59
59
  if (
60
60
  !result &&
61
61
  rule.information &&
62
- (Object.getPrototypeOf(rule.information) !== URL.prototype)
62
+ !helpers.isUrl(rule.information)
63
63
  ) {
64
64
  result = newError("information");
65
65
  }
@@ -481,7 +481,7 @@ function getEnabledRulesPerLineNumber(
481
481
  applyEnableDisable(
482
482
  action,
483
483
  parameter,
484
- enabledRulesPerLineNumber[nextLineNumber] || {}
484
+ enabledRulesPerLineNumber[nextLineNumber]
485
485
  );
486
486
  }
487
487
  }
@@ -583,14 +583,11 @@ function lintContent(
583
583
  helpers.codeBlockAndSpanRanges(paramsBase, lineMetadata);
584
584
  const flattenedLists =
585
585
  helpers.flattenLists(paramsBase.parsers.markdownit.tokens);
586
- const htmlElementRanges =
587
- helpers.htmlElementRanges(paramsBase, lineMetadata);
588
586
  const referenceLinkImageData =
589
587
  helpers.getReferenceLinkImageData(paramsBase);
590
588
  cache.set({
591
589
  codeBlockAndSpanRanges,
592
590
  flattenedLists,
593
- htmlElementRanges,
594
591
  lineMetadata,
595
592
  referenceLinkImageData
596
593
  });
@@ -629,6 +626,10 @@ function lintContent(
629
626
  !helpers.isString(errorInfo.context)) {
630
627
  throwError("context");
631
628
  }
629
+ if (errorInfo.information &&
630
+ !helpers.isUrl(errorInfo.information)) {
631
+ throwError("information");
632
+ }
632
633
  if (errorInfo.range &&
633
634
  (!Array.isArray(errorInfo.range) ||
634
635
  (errorInfo.range.length !== 2) ||
@@ -681,12 +682,13 @@ function lintContent(
681
682
  cleanFixInfo.insertText = fixInfo.insertText;
682
683
  }
683
684
  }
685
+ const information = errorInfo.information || rule.information;
684
686
  results.push({
685
687
  lineNumber,
686
688
  "ruleName": rule.names[0],
687
689
  "ruleNames": rule.names,
688
690
  "ruleDescription": rule.description,
689
- "ruleInformation": rule.information ? rule.information.href : null,
691
+ "ruleInformation": information ? information.href : null,
690
692
  "errorDetail": errorInfo.detail || null,
691
693
  "errorContext": errorInfo.context || null,
692
694
  "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
@@ -864,8 +866,19 @@ function lintInput(options, synchronous, callback) {
864
866
  // Normalize inputs
865
867
  options = options || {};
866
868
  callback = callback || function noop() {};
869
+ const customRuleList =
870
+ [ options.customRules || [] ]
871
+ .flat()
872
+ .map((rule) => ({
873
+ "names": helpers.cloneIfArray(rule.names),
874
+ "description": rule.description,
875
+ "information": helpers.cloneIfUrl(rule.information),
876
+ "tags": helpers.cloneIfArray(rule.tags),
877
+ "asynchronous": rule.asynchronous,
878
+ "function": rule.function
879
+ }));
867
880
  // eslint-disable-next-line unicorn/prefer-spread
868
- const ruleList = rules.concat(options.customRules || []);
881
+ const ruleList = rules.concat(customRuleList);
869
882
  const ruleErr = validateRuleList(ruleList, synchronous);
870
883
  if (ruleErr) {
871
884
  callback(ruleErr);
@@ -1314,6 +1327,7 @@ module.exports = markdownlint;
1314
1327
  * @property {number} lineNumber Line number (1-based).
1315
1328
  * @property {string} [detail] Detail about the error.
1316
1329
  * @property {string} [context] Context for the error.
1330
+ * @property {URL} [information] Link to more information.
1317
1331
  * @property {number[]} [range] Column number (1-based) and length.
1318
1332
  * @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
1319
1333
  */
package/lib/md022.js CHANGED
@@ -2,8 +2,25 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addErrorDetailIf, blockquotePrefixRe, filterTokens, isBlankLine } =
5
+ const { addErrorDetailIf, blockquotePrefixRe, isBlankLine } =
6
6
  require("../helpers");
7
+ const { filterByTypes, getHeadingLevel } =
8
+ require("../helpers/micromark.cjs");
9
+
10
+ const defaultLines = 1;
11
+
12
+ const getLinesFunction = (linesParam) => {
13
+ if (Array.isArray(linesParam)) {
14
+ const linesArray = new Array(6).fill(defaultLines);
15
+ for (const [ index, value ] of [ ...linesParam.entries() ].slice(0, 6)) {
16
+ linesArray[index] = value;
17
+ }
18
+ return (heading) => linesArray[getHeadingLevel(heading) - 1];
19
+ }
20
+ // Coerce linesParam to a number
21
+ const lines = (linesParam === undefined) ? defaultLines : Number(linesParam);
22
+ return () => lines;
23
+ };
7
24
 
8
25
  const getBlockQuote = (str, count) => (
9
26
  (str || "")
@@ -19,50 +36,73 @@ module.exports = {
19
36
  "description": "Headings should be surrounded by blank lines",
20
37
  "tags": [ "headings", "headers", "blank_lines" ],
21
38
  "function": function MD022(params, onError) {
22
- let linesAbove = params.config.lines_above;
23
- linesAbove = Number((linesAbove === undefined) ? 1 : linesAbove);
24
- let linesBelow = params.config.lines_below;
25
- linesBelow = Number((linesBelow === undefined) ? 1 : linesBelow);
26
- const { lines } = params;
27
- filterTokens(params, "heading_open", (token) => {
28
- const [ topIndex, nextIndex ] = token.map;
29
- let actualAbove = 0;
30
- for (let i = 0; i < linesAbove; i++) {
31
- if (isBlankLine(lines[topIndex - i - 1])) {
39
+ const getLinesAbove = getLinesFunction(params.config.lines_above);
40
+ const getLinesBelow = getLinesFunction(params.config.lines_below);
41
+ const { lines, parsers } = params;
42
+ const headings = filterByTypes(
43
+ parsers.micromark.tokens,
44
+ [ "atxHeading", "setextHeading" ]
45
+ );
46
+ for (const heading of headings) {
47
+ const { startLine, endLine } = heading;
48
+ const line = lines[startLine - 1].trim();
49
+
50
+ // Check lines above
51
+ const linesAbove = getLinesAbove(heading);
52
+ if (linesAbove >= 0) {
53
+ let actualAbove = 0;
54
+ for (
55
+ let i = 0;
56
+ (i < linesAbove) && isBlankLine(lines[startLine - 2 - i]);
57
+ i++
58
+ ) {
32
59
  actualAbove++;
33
60
  }
61
+ addErrorDetailIf(
62
+ onError,
63
+ startLine,
64
+ linesAbove,
65
+ actualAbove,
66
+ "Above",
67
+ line,
68
+ null,
69
+ {
70
+ "insertText": getBlockQuote(
71
+ lines[startLine - 2],
72
+ linesAbove - actualAbove
73
+ )
74
+ }
75
+ );
34
76
  }
35
- addErrorDetailIf(
36
- onError,
37
- topIndex + 1,
38
- linesAbove,
39
- actualAbove,
40
- "Above",
41
- lines[topIndex].trim(),
42
- null,
43
- {
44
- "insertText":
45
- getBlockQuote(lines[topIndex - 1], linesAbove - actualAbove)
46
- });
47
- let actualBelow = 0;
48
- for (let i = 0; i < linesBelow; i++) {
49
- if (isBlankLine(lines[nextIndex + i])) {
77
+
78
+ // Check lines below
79
+ const linesBelow = getLinesBelow(heading);
80
+ if (linesBelow >= 0) {
81
+ let actualBelow = 0;
82
+ for (
83
+ let i = 0;
84
+ (i < linesBelow) && isBlankLine(lines[endLine + i]);
85
+ i++
86
+ ) {
50
87
  actualBelow++;
51
88
  }
89
+ addErrorDetailIf(
90
+ onError,
91
+ startLine,
92
+ linesBelow,
93
+ actualBelow,
94
+ "Below",
95
+ line,
96
+ null,
97
+ {
98
+ "lineNumber": endLine + 1,
99
+ "insertText": getBlockQuote(
100
+ lines[endLine],
101
+ linesBelow - actualBelow
102
+ )
103
+ }
104
+ );
52
105
  }
53
- addErrorDetailIf(
54
- onError,
55
- topIndex + 1,
56
- linesBelow,
57
- actualBelow,
58
- "Below",
59
- lines[topIndex].trim(),
60
- null,
61
- {
62
- "lineNumber": nextIndex + 1,
63
- "insertText":
64
- getBlockQuote(lines[nextIndex], linesBelow - actualBelow)
65
- });
66
- });
106
+ }
67
107
  }
68
108
  };
package/lib/md026.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const { addError, allPunctuationNoQuestion, escapeForRegExp, forEachHeading } =
6
- require("../helpers");
5
+ const { addError, allPunctuationNoQuestion, endOfLineGemojiCodeRe,
6
+ endOfLineHtmlEntityRe, escapeForRegExp } = require("../helpers");
7
+ const { filterByTypes } = require("../helpers/micromark.cjs");
7
8
 
8
- const endOfLineHtmlEntityRe = /&#?[\da-zA-Z]+;$/;
9
9
 
10
10
  module.exports = {
11
11
  "names": [ "MD026", "no-trailing-punctuation" ],
@@ -18,19 +18,26 @@ module.exports = {
18
18
  );
19
19
  const trailingPunctuationRe =
20
20
  new RegExp("\\s*[" + escapeForRegExp(punctuation) + "]+$");
21
- forEachHeading(params, (heading) => {
22
- const { line, lineNumber } = heading;
23
- const trimmedLine = line.replace(/([^\s#])[\s#]+$/, "$1");
24
- const match = trailingPunctuationRe.exec(trimmedLine);
25
- if (match && !endOfLineHtmlEntityRe.test(trimmedLine)) {
21
+ const headings = filterByTypes(
22
+ params.parsers.micromark.tokens,
23
+ [ "atxHeadingText", "setextHeadingText" ]
24
+ );
25
+ for (const heading of headings) {
26
+ const { endLine, startColumn, text } = heading;
27
+ const match = trailingPunctuationRe.exec(text);
28
+ if (
29
+ match &&
30
+ !endOfLineHtmlEntityRe.test(text) &&
31
+ !endOfLineGemojiCodeRe.test(text)
32
+ ) {
26
33
  const fullMatch = match[0];
27
- const column = match.index + 1;
34
+ const column = startColumn + match.index;
28
35
  const length = fullMatch.length;
29
36
  addError(
30
37
  onError,
31
- lineNumber,
38
+ endLine,
32
39
  `Punctuation: '${fullMatch}'`,
33
- null,
40
+ undefined,
34
41
  [ column, length ],
35
42
  {
36
43
  "editColumn": column,
@@ -38,6 +45,6 @@ module.exports = {
38
45
  }
39
46
  );
40
47
  }
41
- });
48
+ }
42
49
  }
43
50
  };