eslint-markdown 0.10.0 → 0.11.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/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  [![npm package eslint-markdown latest version](https://img.shields.io/npm/v/eslint-markdown?label=eslint-markdown@latest&color=6358d4&labelColor=333333&logo=npm)](https://www.npmjs.com/package/eslint-markdown)
9
9
  [![npm package eslint-markdown next version](https://img.shields.io/npm/v/eslint-markdown/next?label=eslint-markdown@next&color=6358d4&labelColor=333333&logo=npm)](https://www.npmjs.com/package/eslint-markdown)
10
+ [![npm package eslint-markdown downloads per month](https://img.shields.io/npm/dm/eslint-markdown?label=downloads&color=6358d4&labelColor=333333&logo=npm)](https://www.npmjs.com/package/eslint-markdown)
10
11
 
11
12
  > [!IMPORTANT]
12
13
  >
@@ -24,6 +24,7 @@ export default function all(plugin: ESLint.Plugin): {
24
24
  readonly 'md/consistent-strong-style': "error";
25
25
  readonly 'md/consistent-thematic-break-style': "error";
26
26
  readonly 'md/consistent-unordered-list-style': "error";
27
+ readonly 'md/no-consecutive-blank-line': "error";
27
28
  readonly 'md/no-control-character': "error";
28
29
  readonly 'md/no-curly-quote': "error";
29
30
  readonly 'md/no-double-punctuation': "error";
@@ -21,6 +21,7 @@ export default function stylistic(plugin: ESLint.Plugin): {
21
21
  readonly 'md/consistent-strong-style': "error";
22
22
  readonly 'md/consistent-thematic-break-style': "error";
23
23
  readonly 'md/consistent-unordered-list-style': "error";
24
+ readonly 'md/no-consecutive-blank-line': "error";
24
25
  readonly 'md/no-tab': "error";
25
26
  };
26
27
  };
@@ -9,6 +9,7 @@ declare const _default: {
9
9
  'consistent-strong-style': import("../core/types.js").RuleModule<import("./consistent-strong-style.js").RuleOptions, "style">;
10
10
  'consistent-thematic-break-style': import("../core/types.js").RuleModule<import("./consistent-thematic-break-style.js").RuleOptions, "style">;
11
11
  'consistent-unordered-list-style': import("../core/types.js").RuleModule<import("./consistent-unordered-list-style.js").RuleOptions, "style">;
12
+ 'no-consecutive-blank-line': import("../core/types.js").RuleModule<import("./no-consecutive-blank-line.js").RuleOptions, "noConsecutiveBlankLine">;
12
13
  'no-control-character': import("../core/types.js").RuleModule<import("./no-control-character.js").RuleOptions, import("./no-control-character.js").MessageIds>;
13
14
  'no-curly-quote': import("../core/types.js").RuleModule<import("./no-curly-quote.js").RuleOptions, "noCurlyQuote">;
14
15
  'no-double-punctuation': import("../core/types.js").RuleModule<import("./no-double-punctuation.js").RuleOptions, import("./no-double-punctuation.js").MessageIds>;
@@ -0,0 +1,60 @@
1
+ declare const _default: {
2
+ meta: {
3
+ type: "layout";
4
+ docs: {
5
+ description: string;
6
+ url: string;
7
+ recommended: boolean;
8
+ stylistic: true;
9
+ };
10
+ fixable: "whitespace";
11
+ schema: {
12
+ type: "object";
13
+ properties: {
14
+ max: {
15
+ type: "integer";
16
+ minimum: number;
17
+ };
18
+ skipCode: {
19
+ oneOf: ({
20
+ type: "boolean";
21
+ items?: never;
22
+ uniqueItems?: never;
23
+ } | {
24
+ type: "array";
25
+ items: {
26
+ type: "string";
27
+ };
28
+ uniqueItems: true;
29
+ })[];
30
+ };
31
+ };
32
+ additionalProperties: false;
33
+ }[];
34
+ defaultOptions: [{
35
+ max: number;
36
+ skipCode: true;
37
+ }];
38
+ messages: {
39
+ noConsecutiveBlankLine: string;
40
+ };
41
+ language: string;
42
+ dialects: string[];
43
+ };
44
+ create(context: import("@eslint/core").RuleContext<{
45
+ LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
46
+ Code: import("@eslint/markdown").MarkdownSourceCode;
47
+ RuleOptions: RuleOptions;
48
+ Node: import("mdast").Node;
49
+ MessageIds: "noConsecutiveBlankLine";
50
+ }>): {
51
+ code(node: import("mdast").Code): void;
52
+ 'root:exit'(): void;
53
+ };
54
+ };
55
+ export default _default;
56
+ export type RuleOptions = [{
57
+ max: number;
58
+ skipCode: boolean | string[];
59
+ }];
60
+ export type MessageIds = "noConsecutiveBlankLine";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-markdown",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️",
@@ -48,6 +48,7 @@ export default function all(plugin) {
48
48
  'md/consistent-strong-style': 'error',
49
49
  'md/consistent-thematic-break-style': 'error',
50
50
  'md/consistent-unordered-list-style': 'error',
51
+ 'md/no-consecutive-blank-line': 'error',
51
52
  'md/no-control-character': 'error',
52
53
  'md/no-curly-quote': 'error',
53
54
  'md/no-double-punctuation': 'error',
@@ -45,6 +45,7 @@ export default function stylistic(plugin) {
45
45
  'md/consistent-strong-style': 'error',
46
46
  'md/consistent-thematic-break-style': 'error',
47
47
  'md/consistent-unordered-list-style': 'error',
48
+ 'md/no-consecutive-blank-line': 'error',
48
49
  'md/no-tab': 'error',
49
50
  },
50
51
  });
@@ -15,6 +15,7 @@ import consistentThematicBreakStyle from './consistent-thematic-break-style.js';
15
15
  import consistentUnorderedListStyle from './consistent-unordered-list-style.js';
16
16
  // import enCapitalization from './en-capitalization.js';
17
17
  // import noBoldParagraph from './no-bold-paragraph.js';
18
+ import noConsecutiveBlankLine from './no-consecutive-blank-line.js';
18
19
  import noControlCharacter from './no-control-character.js';
19
20
  import noCurlyQuote from './no-curly-quote.js';
20
21
  import noDoublePunctuation from './no-double-punctuation.js';
@@ -43,6 +44,7 @@ export default {
43
44
  'consistent-unordered-list-style': consistentUnorderedListStyle,
44
45
  // 'en-capitalization': enCapitalization,
45
46
  // 'no-bold-paragraph': noBoldParagraph,
47
+ 'no-consecutive-blank-line': noConsecutiveBlankLine,
46
48
  'no-control-character': noControlCharacter,
47
49
  'no-curly-quote': noCurlyQuote,
48
50
  'no-double-punctuation': noDoublePunctuation,
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @fileoverview Rule to disallow consecutive blank lines.
3
+ * @author lumir(lumirlumir)
4
+ * @see https://github.com/DavidAnson/markdownlint/blob/main/lib/md012.mjs
5
+ */
6
+
7
+ // --------------------------------------------------------------------------------
8
+ // Import
9
+ // --------------------------------------------------------------------------------
10
+
11
+ import { isBlankLine, SkipRanges } from '../core/ast/index.js';
12
+ import { URL_RULE_DOCS } from '../core/constants.js';
13
+
14
+ // --------------------------------------------------------------------------------
15
+ // Typedef
16
+ // --------------------------------------------------------------------------------
17
+
18
+ /**
19
+ * @import { RuleModule } from '../core/types.js';
20
+ * @typedef {[{ max: number, skipCode: boolean | string[] }]} RuleOptions
21
+ * @typedef {'noConsecutiveBlankLine'} MessageIds
22
+ */
23
+
24
+ // --------------------------------------------------------------------------------
25
+ // Rule Definition
26
+ // --------------------------------------------------------------------------------
27
+
28
+ /** @type {RuleModule<RuleOptions, MessageIds>} */
29
+ export default {
30
+ meta: {
31
+ type: 'layout',
32
+
33
+ docs: {
34
+ description: 'Disallow consecutive blank lines',
35
+ url: URL_RULE_DOCS('no-consecutive-blank-line'),
36
+ recommended: false,
37
+ stylistic: true,
38
+ },
39
+
40
+ fixable: 'whitespace',
41
+
42
+ schema: [
43
+ {
44
+ type: 'object',
45
+ properties: {
46
+ max: {
47
+ type: 'integer',
48
+ minimum: 1,
49
+ },
50
+ skipCode: {
51
+ oneOf: [
52
+ {
53
+ type: 'boolean',
54
+ },
55
+ {
56
+ type: 'array',
57
+ items: {
58
+ type: 'string',
59
+ },
60
+ uniqueItems: true,
61
+ },
62
+ ],
63
+ },
64
+ },
65
+ additionalProperties: false,
66
+ },
67
+ ],
68
+
69
+ defaultOptions: [
70
+ {
71
+ max: 1,
72
+ skipCode: true,
73
+ },
74
+ ],
75
+
76
+ messages: {
77
+ noConsecutiveBlankLine:
78
+ 'More than {{ max }} consecutive blank line(s) are not allowed.',
79
+ },
80
+
81
+ language: 'markdown',
82
+
83
+ dialects: ['commonmark', 'gfm'],
84
+ },
85
+
86
+ create(context) {
87
+ const { options, sourceCode } = context;
88
+ const [{ max, skipCode }] = options;
89
+ const { lines, text } = sourceCode;
90
+
91
+ const skipRanges = new SkipRanges();
92
+
93
+ return {
94
+ code(node) {
95
+ if (
96
+ Array.isArray(skipCode) ? node.lang && skipCode.includes(node.lang) : skipCode
97
+ )
98
+ skipRanges.push(sourceCode.getRange(node)); // Store range information of `Code`.
99
+ },
100
+
101
+ 'root:exit'() {
102
+ let consecutiveBlankLineCount = 0;
103
+
104
+ for (let currentLineIdx = 0; currentLineIdx < lines.length; currentLineIdx++) {
105
+ const startLoc = /** @type {const} */ ({
106
+ line: currentLineIdx + 1,
107
+ column: 1,
108
+ });
109
+ const endLoc = /** @type {const} */ ({
110
+ line: currentLineIdx + 2,
111
+ column: 1,
112
+ });
113
+
114
+ if (
115
+ skipRanges.includes(sourceCode.getIndexFromLoc(startLoc)) ||
116
+ !isBlankLine(lines[currentLineIdx])
117
+ ) {
118
+ consecutiveBlankLineCount = 0;
119
+ } else {
120
+ consecutiveBlankLineCount++;
121
+ }
122
+
123
+ if (max < consecutiveBlankLineCount) {
124
+ const lastAllowedBlankLineIdx =
125
+ currentLineIdx - consecutiveBlankLineCount + max;
126
+
127
+ context.report({
128
+ loc: {
129
+ start: startLoc,
130
+ end: endLoc,
131
+ },
132
+
133
+ data: {
134
+ max,
135
+ },
136
+
137
+ messageId: 'noConsecutiveBlankLine',
138
+
139
+ fix(fixer) {
140
+ /*
141
+ * When the consecutive blank-line run reaches EOF, the fixer must remove the
142
+ * whole excess tail at once instead of removing only the current line.
143
+ *
144
+ * `currentLineIdx + 1 === lines.length` means the current line is the final
145
+ * logical line in `sourceCode.lines`. In a case like `foo\n\n\n ` with
146
+ * `max: 1`, the final line is still a blank line, but it may contain spaces.
147
+ * If we only remove from the current line start, those trailing spaces can
148
+ * survive and produce `foo\n ` instead of the intended `foo\n`.
149
+ *
150
+ * The start of the removal range is calculated from the last blank line that
151
+ * is still allowed by `max`:
152
+ *
153
+ * currentLineIdx - consecutiveBlankLineCount
154
+ * -> zero-based index of the first line in the current blank-line run
155
+ *
156
+ * + max
157
+ * -> zero-based index of the last blank line we want to keep
158
+ *
159
+ * + 1
160
+ * -> convert the zero-based line index to ESLint's one-based loc line
161
+ *
162
+ * The column is `lines[lastAllowedBlankLineIdx].length + 1`, which points to the
163
+ * end of that allowed blank line. The range then ends at `text.length`, so
164
+ * everything after the allowed blank line is removed, including remaining blank
165
+ * lines, line endings, and any spaces on the final blank line.
166
+ *
167
+ * Example with `foo\n\n\n ` and `max: 1`:
168
+ *
169
+ * lines = ['foo', '', '', ' ']
170
+ * currentLineIdx = 3
171
+ * consecutiveBlankLineCount = 3
172
+ * max = 1
173
+ *
174
+ * lastAllowedBlankLineIdx = 3 - 3 + 1 = 1
175
+ *
176
+ * So the fixer removes from the end of line 2 to EOF, producing `foo\n`.
177
+ */
178
+ if (currentLineIdx + 1 === lines.length) {
179
+ return fixer.removeRange([
180
+ sourceCode.getIndexFromLoc({
181
+ line: lastAllowedBlankLineIdx + 1,
182
+ column: lines[lastAllowedBlankLineIdx].length + 1,
183
+ }),
184
+ text.length,
185
+ ]);
186
+ } else {
187
+ return fixer.removeRange([
188
+ sourceCode.getIndexFromLoc(startLoc),
189
+ sourceCode.getIndexFromLoc(endLoc),
190
+ ]);
191
+ }
192
+ },
193
+ });
194
+ }
195
+ }
196
+ },
197
+ };
198
+ },
199
+ };