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.
@@ -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,37 @@ 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.
29
- * @param {boolean} [refsDefined] Whether to treat references as defined.
31
+ * @param {Object} [micromarkOptions] Options for micromark.
32
+ * @param {boolean} [referencesDefined] Treat references as defined.
30
33
  * @returns {Object[]} Micromark events.
31
34
  */
32
- function getMicromarkEvents(markdown, options = {}, refsDefined = true) {
35
+ function getMicromarkEvents(
36
+ markdown,
37
+ micromarkOptions = {},
38
+ referencesDefined = true
39
+ ) {
33
40
 
34
41
  // Customize options object to add useful extensions
35
- options.extensions = options.extensions || [];
36
- options.extensions.push(gfmAutolinkLiteral, gfmFootnote(), gfmTable);
42
+ micromarkOptions.extensions = micromarkOptions.extensions || [];
43
+ micromarkOptions.extensions.push(
44
+ gfmAutolinkLiteral(),
45
+ gfmFootnote(),
46
+ gfmTable(),
47
+ math()
48
+ );
37
49
 
38
50
  // Use micromark to parse document into Events
39
51
  const encoding = undefined;
40
52
  const eol = true;
41
- const parseContext = parse(options);
42
- if (refsDefined) {
53
+ const parseContext = parse(micromarkOptions);
54
+ if (referencesDefined) {
43
55
  // Customize ParseContext to treat all references as defined
44
56
  parseContext.defined.includes = (searchElement) => searchElement.length > 0;
45
57
  }
@@ -52,15 +64,21 @@ function getMicromarkEvents(markdown, options = {}, refsDefined = true) {
52
64
  * Parses a Markdown document and returns (frozen) tokens.
53
65
  *
54
66
  * @param {string} markdown Markdown document.
55
- * @param {Object} [options] Options for micromark.
56
- * @param {boolean} [refsDefined] Whether to treat references as defined.
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.
57
70
  * @returns {Token[]} Micromark tokens (frozen).
58
71
  */
59
- function micromarkParse(markdown, options = {}, refsDefined = true) {
60
-
72
+ function micromarkParseWithOffset(
73
+ markdown,
74
+ micromarkOptions,
75
+ referencesDefined,
76
+ lineDelta
77
+ ) {
61
78
  // Use micromark to parse document into Events
62
- const events =
63
- getMicromarkEvents(markdown, options, refsDefined);
79
+ const events = getMicromarkEvents(
80
+ markdown, micromarkOptions, referencesDefined
81
+ );
64
82
 
65
83
  // Create Token objects
66
84
  const document = [];
@@ -68,6 +86,8 @@ function micromarkParse(markdown, options = {}, refsDefined = true) {
68
86
  "children": document
69
87
  };
70
88
  const history = [ current ];
89
+ let reparseOptions = null;
90
+ let lines = null;
71
91
  for (const event of events) {
72
92
  const [ kind, token, context ] = event;
73
93
  const { type, start, end } = token;
@@ -79,13 +99,37 @@ function micromarkParse(markdown, options = {}, refsDefined = true) {
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,24 +144,26 @@ function micromarkParse(markdown, options = {}, refsDefined = true) {
100
144
  return document;
101
145
  }
102
146
 
103
- // /**
104
- // * Log the structure of a Micromark token list.
105
- // *
106
- // * @param {Token[]} tokens Micromark tokens.
107
- // * @param {number} depth Tree depth.
108
- // * @returns {void}
109
- // */
110
- // function consoleLogTokens(tokens, depth = 0) {
111
- // for (const token of tokens) {
112
- // const { children, text, type } = token;
113
- // console.log(
114
- // `${" ".repeat(depth * 2)}${type} ${text.replace(/\n/g, "\\n")}`
115
- // );
116
- // if (children.length > 0) {
117
- // consoleLogTokens(children, depth + 1);
118
- // }
119
- // }
120
- // }
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
+ }
121
167
 
122
168
  /**
123
169
  * Filter a list of Micromark tokens by predicate.
@@ -158,6 +204,68 @@ function filterByTypes(tokens, allowed) {
158
204
  );
159
205
  }
160
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
+
161
269
  /**
162
270
  * Gets information about the tag in an HTML token.
163
271
  *
@@ -230,8 +338,11 @@ function tokenIfType(token, type) {
230
338
 
231
339
  module.exports = {
232
340
  "parse": micromarkParse,
341
+ filterByHtmlTokens,
233
342
  filterByPredicate,
234
343
  filterByTypes,
344
+ flattenedChildren,
345
+ getHeadingLevel,
235
346
  getHtmlTagInfo,
236
347
  getMicromarkEvents,
237
348
  getTokenTextByType,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdownlint-rule-helpers",
3
- "version": "0.20.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.29.0";
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
  };