eslint-plugin-markdown-preferences 0.14.0 → 0.16.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
@@ -13,10 +13,10 @@ A specialized ESLint plugin that helps enforce consistent writing style and form
13
13
 
14
14
  ## 📛 Features
15
15
 
16
- - **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting, linking, and style consistency automatically
17
- - **📖 Professional documentation** - Enforce consistent line breaks, clean up trailing spaces, and organize link definitions for enterprise-ready documentation
18
- - **🎯 Smart terminology management** - Automatically convert specified words into inline code or clickable links based on your configuration
19
- - **⚙️ Highly customizable configuration** - Fine-tune every aspect with granular rule options, word lists, ignore patterns, and flexible thresholds to match your exact requirements
16
+ - **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting, casing, and style consistency automatically
17
+ - **📖 Professional documentation** - Enforce consistent headings, table headers, and organize link definitions for enterprise-ready documentation
18
+ - **🎨 Clean formatting** - Remove trailing spaces, control line breaks, standardize code blocks, and ensure consistent list numbering for polished output
19
+ - **⚙️ Flexible customization** - Configure casing styles (Title Case, Sentence case), code block languages, emoji notation, and more with extensive options
20
20
 
21
21
  **Try it live:** Check out the [Online Demo](https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences) to see the plugin in action!
22
22
 
@@ -37,6 +37,7 @@ npm install --save-dev eslint @eslint/markdown eslint-plugin-markdown-preference
37
37
  ## 📖 Usage
38
38
 
39
39
  <!--USAGE_SECTION_START-->
40
+
40
41
  <!--USAGE_GUIDE_START-->
41
42
 
42
43
  ### Configuration
@@ -75,6 +76,7 @@ See [the rule list](https://ota-meshi.github.io/eslint-plugin-markdown-preferenc
75
76
  Is not supported.
76
77
 
77
78
  <!--USAGE_GUIDE_END-->
79
+
78
80
  <!--USAGE_SECTION_END-->
79
81
 
80
82
  ## ✅ Rules
@@ -86,12 +88,12 @@ The rules with the following star ⭐ are included in the configs.
86
88
 
87
89
  <!--RULES_TABLE_START-->
88
90
 
89
- <!-- prettier-ignore-start -->
90
-
91
91
  ### Preference Rules
92
92
 
93
93
  - Rules to unify the expression and description style of documents.
94
94
 
95
+ <!-- prettier-ignore-start -->
96
+
95
97
  | Rule ID | Description | Fixable | RECOMMENDED |
96
98
  |:--------|:------------|:-------:|:-----------:|
97
99
  | [markdown-preferences/canonical-code-block-language](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html) | enforce canonical language names in code blocks | 🔧 | |
@@ -102,21 +104,28 @@ The rules with the following star ⭐ are included in the configs.
102
104
  | [markdown-preferences/prefer-linked-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html) | enforce the specified word to be a link. | 🔧 | |
103
105
  | [markdown-preferences/table-header-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html) | enforce consistent casing in table header cells. | 🔧 | |
104
106
 
107
+ <!-- prettier-ignore-end -->
108
+
105
109
  ### Stylistic Rules
106
110
 
107
111
  - Rules related to the formatting and visual style of Markdown.
108
112
 
113
+ <!-- prettier-ignore-start -->
114
+
109
115
  | Rule ID | Description | Fixable | RECOMMENDED |
110
116
  |:--------|:------------|:-------:|:-----------:|
111
117
  | [markdown-preferences/atx-headings-closing-sequence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence-length.html) | enforce consistent length for the closing sequence (trailing #s) in ATX headings. | 🔧 | |
112
118
  | [markdown-preferences/atx-headings-closing-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence.html) | enforce consistent use of closing sequence in ATX headings. | 🔧 | |
119
+ | [markdown-preferences/blockquote-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/blockquote-marker-alignment.html) | enforce consistent alignment of blockquote markers | 🔧 | ⭐ |
113
120
  | [markdown-preferences/definitions-last](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html) | require link definitions and footnote definitions to be placed at the end of the document | 🔧 | |
114
121
  | [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
122
+ | [markdown-preferences/list-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html) | enforce consistent alignment of list markers | 🔧 | ⭐ |
115
123
  | [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐ |
116
124
  | [markdown-preferences/no-multiple-empty-lines](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-multiple-empty-lines.html) | disallow multiple empty lines in Markdown files. | 🔧 | |
117
125
  | [markdown-preferences/no-text-backslash-linebreak](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html) | disallow text backslash at the end of a line. | | ⭐ |
118
126
  | [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | disallow trailing whitespace at the end of lines in Markdown files. | 🔧 | |
119
127
  | [markdown-preferences/ordered-list-marker-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-sequence.html) | enforce that ordered list markers use sequential numbers | 🔧 | |
128
+ | [markdown-preferences/padding-line-between-blocks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/padding-line-between-blocks.html) | require or disallow padding lines between blocks | 🔧 | ⭐ |
120
129
  | [markdown-preferences/prefer-autolinks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html) | enforce the use of autolinks for URLs | 🔧 | ⭐ |
121
130
  | [markdown-preferences/prefer-fenced-code-blocks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-fenced-code-blocks.html) | enforce the use of fenced code blocks over indented code blocks | 🔧 | ⭐ |
122
131
  | [markdown-preferences/prefer-link-reference-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html) | enforce using link reference definitions instead of inline links | 🔧 | |
@@ -125,7 +134,13 @@ The rules with the following star ⭐ are included in the configs.
125
134
  <!-- prettier-ignore-end -->
126
135
 
127
136
  <!--RULES_TABLE_END-->
137
+
128
138
  <!--RULES_SECTION_END-->
139
+
140
+ ## 👫 Related Packages
141
+
142
+ - [eslint-plugin-markdown-links](https://github.com/ota-meshi/eslint-plugin-markdown-links) ... ESLint plugin with powerful checking rules related to Markdown links.
143
+
129
144
  <!--DOCS_IGNORE_START-->
130
145
 
131
146
  ## 🍻 Contributing
package/lib/index.d.ts CHANGED
@@ -20,6 +20,11 @@ interface RuleOptions {
20
20
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence-length.html
21
21
  */
22
22
  'markdown-preferences/atx-headings-closing-sequence-length'?: Linter.RuleEntry<MarkdownPreferencesAtxHeadingsClosingSequenceLength>;
23
+ /**
24
+ * enforce consistent alignment of blockquote markers
25
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/blockquote-marker-alignment.html
26
+ */
27
+ 'markdown-preferences/blockquote-marker-alignment'?: Linter.RuleEntry<[]>;
23
28
  /**
24
29
  * enforce canonical language names in code blocks
25
30
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html
@@ -45,6 +50,11 @@ interface RuleOptions {
45
50
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html
46
51
  */
47
52
  'markdown-preferences/heading-casing'?: Linter.RuleEntry<MarkdownPreferencesHeadingCasing>;
53
+ /**
54
+ * enforce consistent alignment of list markers
55
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html
56
+ */
57
+ 'markdown-preferences/list-marker-alignment'?: Linter.RuleEntry<MarkdownPreferencesListMarkerAlignment>;
48
58
  /**
49
59
  * disallow laziness in blockquotes
50
60
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html
@@ -75,6 +85,11 @@ interface RuleOptions {
75
85
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html
76
86
  */
77
87
  'markdown-preferences/ordered-list-marker-start'?: Linter.RuleEntry<MarkdownPreferencesOrderedListMarkerStart>;
88
+ /**
89
+ * require or disallow padding lines between blocks
90
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/padding-line-between-blocks.html
91
+ */
92
+ 'markdown-preferences/padding-line-between-blocks'?: Linter.RuleEntry<MarkdownPreferencesPaddingLineBetweenBlocks>;
78
93
  /**
79
94
  * enforce the use of autolinks for URLs
80
95
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html
@@ -137,6 +152,9 @@ type MarkdownPreferencesHeadingCasing = [] | [{
137
152
  ignorePatterns?: string[];
138
153
  minorWords?: string[];
139
154
  }];
155
+ type MarkdownPreferencesListMarkerAlignment = [] | [{
156
+ align?: ("left" | "right");
157
+ }];
140
158
  type MarkdownPreferencesNoMultipleEmptyLines = [] | [{
141
159
  max?: number;
142
160
  maxEOF?: number;
@@ -149,6 +167,17 @@ type MarkdownPreferencesNoTrailingSpaces = [] | [{
149
167
  type MarkdownPreferencesOrderedListMarkerStart = [] | [{
150
168
  start?: (1 | 0);
151
169
  }];
170
+ type MarkdownPreferencesPaddingLineBetweenBlocks = {
171
+ prev: (("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*") | [("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"), ...(("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"))[]] | {
172
+ type: (("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*") | [("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"), ...(("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"))[]]);
173
+ in?: ("list" | "blockquote" | "footnote-definition");
174
+ });
175
+ next: (("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*") | [("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"), ...(("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"))[]] | {
176
+ type: (("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*") | [("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"), ...(("blockquote" | "code" | "heading" | "html" | "list" | "paragraph" | "thematic-break" | "table" | "link-definition" | "footnote-definition" | "frontmatter" | "*"))[]]);
177
+ in?: ("list" | "blockquote" | "footnote-definition");
178
+ });
179
+ blankLine: ("any" | "never" | "always");
180
+ }[];
152
181
  type MarkdownPreferencesPreferInlineCodeWords = [] | [{
153
182
  words: string[];
154
183
  ignores?: {
@@ -207,7 +236,7 @@ declare namespace meta_d_exports {
207
236
  export { name, version };
208
237
  }
209
238
  declare const name: "eslint-plugin-markdown-preferences";
210
- declare const version: "0.14.0";
239
+ declare const version: "0.16.0";
211
240
  //#endregion
212
241
  //#region src/index.d.ts
213
242
  declare const configs: {
package/lib/index.js CHANGED
@@ -27,14 +27,110 @@ function createRule(ruleName, rule) {
27
27
  };
28
28
  }
29
29
 
30
+ //#endregion
31
+ //#region src/utils/ast.ts
32
+ /**
33
+ * Get the kind of heading.
34
+ */
35
+ function getHeadingKind(sourceCode, node) {
36
+ const loc = sourceCode.getLoc(node);
37
+ if (loc.start.line !== loc.end.line) return "setext";
38
+ return "atx";
39
+ }
40
+ /**
41
+ * Get the kind of code block.
42
+ */
43
+ function getCodeBlockKind(sourceCode, node) {
44
+ const text = sourceCode.getText(node);
45
+ return text.startsWith("```") ? "backtick-fenced" : text.startsWith("~~~") ? "tilde-fenced" : "indented";
46
+ }
47
+ /**
48
+ * Get the kind of link.
49
+ */
50
+ function getLinkKind(sourceCode, node) {
51
+ const text = sourceCode.getText(node);
52
+ return text.startsWith("[") ? "inline" : text.startsWith("<") && text.endsWith(">") ? "autolink" : "gfm-autolink";
53
+ }
54
+ /**
55
+ * Get the marker of a list item.
56
+ */
57
+ function getListItemMarker(sourceCode, node) {
58
+ const item = node.type === "list" ? node.children[0] || node : node;
59
+ const text = sourceCode.getText(item);
60
+ if (text.startsWith("-")) return {
61
+ kind: "-",
62
+ raw: "-"
63
+ };
64
+ if (text.startsWith("*")) return {
65
+ kind: "*",
66
+ raw: "*"
67
+ };
68
+ if (text.startsWith("+")) return {
69
+ kind: "+",
70
+ raw: "+"
71
+ };
72
+ const matchDot = /^(\d+)\./.exec(text);
73
+ if (matchDot) return {
74
+ kind: ".",
75
+ raw: matchDot[0],
76
+ sequence: Number(matchDot[1])
77
+ };
78
+ const matchParen = /^(\d+)\)/.exec(text);
79
+ return {
80
+ kind: ")",
81
+ raw: matchParen[0],
82
+ sequence: Number(matchParen[1])
83
+ };
84
+ }
85
+ /**
86
+ * Get the marker for a thematic break.
87
+ */
88
+ function getThematicBreakMarker(sourceCode, node) {
89
+ const text = sourceCode.getText(node);
90
+ return {
91
+ kind: text.startsWith("-") ? "-" : "*",
92
+ hasSpaces: /\s/u.test(text)
93
+ };
94
+ }
95
+ /**
96
+ * Get the source location from a range in a node.
97
+ */
98
+ function getSourceLocationFromRange(sourceCode, node, range) {
99
+ const [nodeStart] = sourceCode.getRange(node);
100
+ let startLine, startColumn;
101
+ if (nodeStart <= range[0]) {
102
+ const loc = sourceCode.getLoc(node);
103
+ const beforeLines = sourceCode.text.slice(nodeStart, range[0]).split(/\n/u);
104
+ startLine = loc.start.line + beforeLines.length - 1;
105
+ startColumn = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
106
+ } else {
107
+ const beforeLines = sourceCode.text.slice(0, range[0]).split(/\n/u);
108
+ startLine = beforeLines.length;
109
+ startColumn = 1 + (beforeLines.at(-1) || "").length;
110
+ }
111
+ const contentLines = sourceCode.text.slice(range[0], range[1]).split(/\n/u);
112
+ const endLine = startLine + contentLines.length - 1;
113
+ const endColumn = (contentLines.length === 1 ? startColumn : 1) + (contentLines.at(-1) || "").length;
114
+ return {
115
+ start: {
116
+ line: startLine,
117
+ column: startColumn
118
+ },
119
+ end: {
120
+ line: endLine,
121
+ column: endColumn
122
+ }
123
+ };
124
+ }
125
+
30
126
  //#endregion
31
127
  //#region src/utils/atx-heading.ts
32
128
  /**
33
129
  * Parse the closing sequence of an ATX heading.
34
130
  */
35
131
  function parseATXHeadingClosingSequence(sourceCode, node) {
132
+ if (getHeadingKind(sourceCode, node) !== "atx") return null;
36
133
  const loc = sourceCode.getLoc(node);
37
- if (loc.start.line !== loc.end.line) return null;
38
134
  const range = sourceCode.getRange(node);
39
135
  const parsed = parseATXHeadingClosingSequenceFromText(sourceCode.text.slice(...range));
40
136
  if (parsed == null) return { closingSequence: null };
@@ -108,7 +204,7 @@ function parseATXHeadingClosingSequenceFromText(text) {
108
204
 
109
205
  //#endregion
110
206
  //#region src/utils/lines.ts
111
- const cache = /* @__PURE__ */ new WeakMap();
207
+ const cache$1 = /* @__PURE__ */ new WeakMap();
112
208
  var ParsedLines = class {
113
209
  lines;
114
210
  constructor(codeText) {
@@ -150,11 +246,11 @@ var ParsedLines = class {
150
246
  * @param sourceCode source code to parse
151
247
  * @returns parsed lines
152
248
  */
153
- function parseLines(sourceCode) {
154
- const cached = cache.get(sourceCode);
249
+ function getParsedLines(sourceCode) {
250
+ const cached = cache$1.get(sourceCode);
155
251
  if (cached) return cached;
156
252
  const parsedLines = new ParsedLines(sourceCode.text);
157
- cache.set(sourceCode, parsedLines);
253
+ cache$1.set(sourceCode, parsedLines);
158
254
  return parsedLines;
159
255
  }
160
256
 
@@ -228,9 +324,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
228
324
  } };
229
325
  })() : option.mode === "fixed-line-length" ? (() => {
230
326
  const totalLength = option.length || 80;
231
- const lines = parseLines(sourceCode);
232
327
  const getExpected = (_node, parsed) => {
233
- return totalLength - getContentLength(lines, parsed);
328
+ return totalLength - getContentLength(parsed);
234
329
  };
235
330
  return { heading(node) {
236
331
  verifyATXHeadingClosingSequenceLength(node, getExpected);
@@ -246,7 +341,6 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
246
341
  } else verifyATXHeadingClosingSequenceLength(node, getExpected);
247
342
  } };
248
343
  })() : option.mode === "consistent-line-length" ? (() => {
249
- const lines = parseLines(sourceCode);
250
344
  const headings = [];
251
345
  return {
252
346
  heading(node) {
@@ -260,9 +354,9 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
260
354
  "root:exit"() {
261
355
  let mostLongContentHeading = null;
262
356
  for (const heading of headings) {
263
- const contentLength = getContentLength(lines, heading.parsed);
357
+ const contentLength = getContentLength(heading.parsed);
264
358
  if (mostLongContentHeading == null || contentLength > mostLongContentHeading.contentLength) {
265
- const lineLength = getLineLength(lines, heading.parsed);
359
+ const lineLength = getLineLength(heading.parsed);
266
360
  mostLongContentHeading = {
267
361
  ...heading,
268
362
  contentLength,
@@ -273,11 +367,11 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
273
367
  if (!mostLongContentHeading) return;
274
368
  let minLineLength = mostLongContentHeading.lineLength;
275
369
  for (const heading of headings) {
276
- const lineLength = getLineLength(lines, heading.parsed);
370
+ const lineLength = getLineLength(heading.parsed);
277
371
  if (mostLongContentHeading.contentLength < lineLength && lineLength < minLineLength) minLineLength = Math.min(minLineLength, lineLength);
278
372
  }
279
373
  const getExpected = (_node, parsed) => {
280
- return minLineLength - getContentLength(lines, parsed);
374
+ return minLineLength - getContentLength(parsed);
281
375
  };
282
376
  for (const { node } of headings) verifyATXHeadingClosingSequenceLength(node, getExpected);
283
377
  }
@@ -286,7 +380,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
286
380
  /**
287
381
  * Get the content length of the heading.
288
382
  */
289
- function getContentLength(lines, parsed) {
383
+ function getContentLength(parsed) {
384
+ const lines = getParsedLines(sourceCode);
290
385
  const line = lines.get(parsed.closingSequence.loc.start.line);
291
386
  const beforeClosing = sourceCode.text.slice(line.range[0], parsed.closingSequence.range[0]);
292
387
  return getTextWidth(beforeClosing);
@@ -294,7 +389,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
294
389
  /**
295
390
  * Get the line length of the heading.
296
391
  */
297
- function getLineLength(lines, parsed) {
392
+ function getLineLength(parsed) {
393
+ const lines = getParsedLines(sourceCode);
298
394
  const line = lines.get(parsed.closingSequence.loc.start.line);
299
395
  const lineText = sourceCode.text.slice(line.range[0], parsed.closingSequence.range[1]);
300
396
  return getTextWidth(lineText);
@@ -381,83 +477,122 @@ var atx_headings_closing_sequence_default = createRule("atx-headings-closing-seq
381
477
  });
382
478
 
383
479
  //#endregion
384
- //#region src/utils/ast.ts
385
- /**
386
- * Get the kind of code block.
387
- */
388
- function getCodeBlockKind(sourceCode, node) {
389
- const text = sourceCode.getText(node);
390
- return text.startsWith("```") ? "backtick-fenced" : text.startsWith("~~~") ? "tilde-fenced" : "indented";
391
- }
392
- /**
393
- * Get the kind of link.
394
- */
395
- function getLinkKind(sourceCode, node) {
396
- const text = sourceCode.getText(node);
397
- return text.startsWith("[") ? "inline" : text.startsWith("<") && text.endsWith(">") ? "autolink" : "gfm-autolink";
398
- }
399
- /**
400
- * Get the marker of a list item.
401
- */
402
- function getListItemMarker(sourceCode, node) {
403
- const item = node.type === "list" ? node.children[0] || node : node;
404
- const text = sourceCode.getText(item);
405
- if (text.startsWith("-")) return {
406
- kind: "-",
407
- raw: "-"
408
- };
409
- if (text.startsWith("*")) return {
410
- kind: "*",
411
- raw: "*"
412
- };
413
- if (text.startsWith("+")) return {
414
- kind: "+",
415
- raw: "+"
416
- };
417
- const matchDot = /^(\d+)\./.exec(text);
418
- if (matchDot) return {
419
- kind: ".",
420
- raw: matchDot[0],
421
- sequence: Number(matchDot[1])
422
- };
423
- const matchParen = /^(\d+)\)/.exec(text);
424
- return {
425
- kind: ")",
426
- raw: matchParen[0],
427
- sequence: Number(matchParen[1])
428
- };
429
- }
480
+ //#region src/utils/blockquotes.ts
481
+ const cache = /* @__PURE__ */ new WeakMap();
430
482
  /**
431
- * Get the source location from a range in a node.
483
+ * Helper function to get blockquote level information.
432
484
  */
433
- function getSourceLocationFromRange(sourceCode, node, range) {
434
- const [nodeStart] = sourceCode.getRange(node);
435
- let startLine, startColumn;
436
- if (nodeStart <= range[0]) {
437
- const loc = sourceCode.getLoc(node);
438
- const beforeLines = sourceCode.text.slice(nodeStart, range[0]).split(/\n/u);
439
- startLine = loc.start.line + beforeLines.length - 1;
440
- startColumn = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
441
- } else {
442
- const beforeLines = sourceCode.text.slice(0, range[0]).split(/\n/u);
443
- startLine = beforeLines.length;
444
- startColumn = 1 + (beforeLines.at(-1) || "").length;
485
+ function getBlockquoteLevelFromLine(sourceCode, lineNumber) {
486
+ let map = cache.get(sourceCode);
487
+ if (!map) {
488
+ map = /* @__PURE__ */ new Map();
489
+ cache.set(sourceCode, map);
445
490
  }
446
- const contentLines = sourceCode.text.slice(range[0], range[1]).split(/\n/u);
447
- const endLine = startLine + contentLines.length - 1;
448
- const endColumn = (contentLines.length === 1 ? startColumn : 1) + (contentLines.at(-1) || "").length;
449
- return {
450
- start: {
451
- line: startLine,
452
- column: startColumn
453
- },
454
- end: {
455
- line: endLine,
456
- column: endColumn
457
- }
491
+ const cached = map.get(lineNumber);
492
+ if (cached) return cached;
493
+ const lineText = sourceCode.lines[lineNumber - 1];
494
+ let prefix = "";
495
+ let level = 0;
496
+ const blockquoteMarkers = /* @__PURE__ */ new Map();
497
+ for (const c of lineText) {
498
+ if (c === ">") {
499
+ level++;
500
+ blockquoteMarkers.set(level, { index: prefix.length });
501
+ } else if (c.trim()) break;
502
+ prefix += c;
503
+ }
504
+ const result = {
505
+ line: lineNumber,
506
+ prefix,
507
+ level,
508
+ blockquoteMarkers
458
509
  };
510
+ map.set(lineNumber, result);
511
+ return result;
459
512
  }
460
513
 
514
+ //#endregion
515
+ //#region src/rules/blockquote-marker-alignment.ts
516
+ var blockquote_marker_alignment_default = createRule("blockquote-marker-alignment", {
517
+ meta: {
518
+ type: "layout",
519
+ docs: {
520
+ description: "enforce consistent alignment of blockquote markers",
521
+ categories: ["recommended"],
522
+ listCategory: "Stylistic"
523
+ },
524
+ fixable: "whitespace",
525
+ hasSuggestions: false,
526
+ schema: [],
527
+ messages: { inconsistentAlignment: "Blockquote markers should be consistently aligned at the same nesting level." }
528
+ },
529
+ create(context) {
530
+ const sourceCode = context.sourceCode;
531
+ let blockquoteStack = {
532
+ node: sourceCode.ast,
533
+ level: 0,
534
+ upper: null
535
+ };
536
+ return {
537
+ blockquote(node) {
538
+ blockquoteStack = {
539
+ node,
540
+ level: blockquoteStack.level + 1,
541
+ upper: blockquoteStack,
542
+ reported: blockquoteStack.reported
543
+ };
544
+ if (blockquoteStack.reported) return;
545
+ const blockquoteLevel = blockquoteStack.level;
546
+ const loc = sourceCode.getLoc(node);
547
+ const startLine = loc.start.line;
548
+ const endLine = loc.end.line;
549
+ const base = getBlockquoteLevelFromLine(sourceCode, startLine).blockquoteMarkers.get(blockquoteLevel);
550
+ if (!base) return;
551
+ for (let lineNumber = startLine + 1; lineNumber <= endLine; lineNumber++) {
552
+ const marker = getBlockquoteLevelFromLine(sourceCode, lineNumber).blockquoteMarkers.get(blockquoteLevel);
553
+ if (!marker) continue;
554
+ if (base.index === marker.index) continue;
555
+ blockquoteStack.reported = true;
556
+ context.report({
557
+ node,
558
+ loc: {
559
+ start: {
560
+ line: lineNumber,
561
+ column: marker.index + 1
562
+ },
563
+ end: {
564
+ line: lineNumber,
565
+ column: marker.index + 2
566
+ }
567
+ },
568
+ messageId: "inconsistentAlignment",
569
+ fix(fixer) {
570
+ const lines = getParsedLines(sourceCode);
571
+ const line = lines.get(lineNumber);
572
+ if (marker.index < base.index) {
573
+ const addSpaces = " ".repeat(base.index - marker.index);
574
+ return fixer.insertTextBeforeRange([line.range[0] + marker.index, line.range[0] + marker.index], addSpaces);
575
+ }
576
+ if (blockquoteLevel === 1) {
577
+ const expectedSpaces = " ".repeat(base.index);
578
+ return fixer.replaceTextRange([line.range[0], line.range[0] + marker.index], expectedSpaces);
579
+ }
580
+ const itemBefore = line.text.slice(0, line.range[0] + marker.index);
581
+ if (itemBefore.includes(" ")) return null;
582
+ let removeStartIndex = marker.index;
583
+ for (; removeStartIndex > base.index; removeStartIndex--) if (line.text[removeStartIndex - 1] !== " ") break;
584
+ return fixer.removeRange([line.range[0] + removeStartIndex, line.range[0] + marker.index]);
585
+ }
586
+ });
587
+ }
588
+ },
589
+ "blockquote:exit"() {
590
+ blockquoteStack = blockquoteStack.upper;
591
+ }
592
+ };
593
+ }
594
+ });
595
+
461
596
  //#endregion
462
597
  //#region src/rules/canonical-code-block-language.ts
463
598
  const DEFAULT_LANGUAGES = {
@@ -3458,20 +3593,99 @@ var heading_casing_default = createRule("heading-casing", {
3458
3593
  }
3459
3594
  });
3460
3595
 
3596
+ //#endregion
3597
+ //#region src/rules/list-marker-alignment.ts
3598
+ const ALIGN_TO_POSITION_NAME = {
3599
+ left: "start",
3600
+ right: "end"
3601
+ };
3602
+ var list_marker_alignment_default = createRule("list-marker-alignment", {
3603
+ meta: {
3604
+ type: "layout",
3605
+ docs: {
3606
+ description: "enforce consistent alignment of list markers",
3607
+ categories: ["recommended"],
3608
+ listCategory: "Stylistic"
3609
+ },
3610
+ fixable: "whitespace",
3611
+ hasSuggestions: false,
3612
+ schema: [{
3613
+ type: "object",
3614
+ properties: { align: { enum: ["left", "right"] } },
3615
+ additionalProperties: false
3616
+ }],
3617
+ messages: { incorrectAlignment: "List marker alignment is inconsistent. Expected {{expected}} characters of indentation, but got {{actual}}." }
3618
+ },
3619
+ create(context) {
3620
+ const sourceCode = context.sourceCode;
3621
+ const alignPositionName = ALIGN_TO_POSITION_NAME[context.options[0]?.align ?? "left"];
3622
+ /**
3623
+ * Get the marker location of a list item
3624
+ */
3625
+ function getMarkerLocation(node) {
3626
+ const start = sourceCode.getLoc(node).start;
3627
+ const startColumnIndex = start.column - 1;
3628
+ const marker = getListItemMarker(sourceCode, node);
3629
+ return {
3630
+ line: start.line,
3631
+ start: startColumnIndex,
3632
+ end: startColumnIndex + marker.raw.length
3633
+ };
3634
+ }
3635
+ /**
3636
+ * Check if list items have consistent alignment
3637
+ */
3638
+ function checkListAlignment(listNode) {
3639
+ const items = listNode.children;
3640
+ if (items.length <= 1) return;
3641
+ const referenceMarkerLocation = getMarkerLocation(items[0]);
3642
+ for (const item of items.slice(1)) {
3643
+ const markerLocation = getMarkerLocation(item);
3644
+ const diff = markerLocation[alignPositionName] - referenceMarkerLocation[alignPositionName];
3645
+ if (diff === 0) continue;
3646
+ context.report({
3647
+ node: item,
3648
+ loc: {
3649
+ start: {
3650
+ line: markerLocation.line,
3651
+ column: markerLocation.start + 1
3652
+ },
3653
+ end: {
3654
+ line: markerLocation.line,
3655
+ column: markerLocation.end + 1
3656
+ }
3657
+ },
3658
+ messageId: "incorrectAlignment",
3659
+ data: {
3660
+ expected: String(markerLocation.start - diff),
3661
+ actual: String(markerLocation.start)
3662
+ },
3663
+ fix(fixer) {
3664
+ const lines = getParsedLines(sourceCode);
3665
+ const line = lines.get(markerLocation.line);
3666
+ if (diff < 0) {
3667
+ const addSpaces = " ".repeat(-diff);
3668
+ return fixer.insertTextBeforeRange([line.range[0] + markerLocation.start, line.range[0] + markerLocation.start], addSpaces);
3669
+ }
3670
+ const itemBefore = line.text.slice(0, markerLocation.start - diff);
3671
+ if (itemBefore.includes(" ")) return null;
3672
+ const referenceMarkerBefore = lines.get(referenceMarkerLocation.line).text.slice(0, referenceMarkerLocation.start);
3673
+ if (referenceMarkerBefore === itemBefore) {
3674
+ const removeEndIndex = line.range[0] + markerLocation.start;
3675
+ const removeStartIndex = removeEndIndex - diff;
3676
+ return fixer.removeRange([removeStartIndex, removeEndIndex]);
3677
+ }
3678
+ return null;
3679
+ }
3680
+ });
3681
+ }
3682
+ }
3683
+ return { list: checkListAlignment };
3684
+ }
3685
+ });
3686
+
3461
3687
  //#endregion
3462
3688
  //#region src/rules/no-laziness-blockquotes.ts
3463
- /**
3464
- * Helper function to get blockquote line information.
3465
- */
3466
- function getBlockquoteLineInfo(line) {
3467
- const regex = /^\s*(?:>\s*)*/u;
3468
- const match = regex.exec(line.text);
3469
- return {
3470
- line,
3471
- prefix: match[0],
3472
- level: (match[0].match(/>/gu) || []).length
3473
- };
3474
- }
3475
3689
  var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3476
3690
  meta: {
3477
3691
  type: "problem",
@@ -3492,7 +3706,6 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3492
3706
  create(context) {
3493
3707
  const sourceCode = context.sourceCode;
3494
3708
  const checkedLines = /* @__PURE__ */ new Set();
3495
- const lines = parseLines(sourceCode);
3496
3709
  /**
3497
3710
  * Report invalid blockquote lines.
3498
3711
  */
@@ -3516,15 +3729,16 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3516
3729
  for (const group of invalidGroups) {
3517
3730
  const first = group.lines[0];
3518
3731
  const last = group.lines.at(-1);
3732
+ const lines = getParsedLines(sourceCode);
3519
3733
  context.report({
3520
3734
  loc: {
3521
3735
  start: {
3522
- line: first.line.line,
3736
+ line: first.line,
3523
3737
  column: 1
3524
3738
  },
3525
3739
  end: {
3526
- line: last.line.line,
3527
- column: last.line.text.length + 1
3740
+ line: last.line,
3741
+ column: lines.get(last.line).text.length + 1
3528
3742
  }
3529
3743
  },
3530
3744
  messageId: "lazyBlockquoteLine",
@@ -3536,12 +3750,16 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3536
3750
  messageId: "addMarker",
3537
3751
  data: { missingMarkers: `${base.level - group.level}` },
3538
3752
  *fix(fixer) {
3539
- for (const invalidLine of group.lines) yield fixer.replaceTextRange([invalidLine.line.range[0], invalidLine.line.range[0] + invalidLine.prefix.length], base.prefix);
3753
+ for (const invalidLine of group.lines) {
3754
+ const parsedLine = lines.get(invalidLine.line);
3755
+ yield fixer.replaceTextRange([parsedLine.range[0], parsedLine.range[0] + invalidLine.prefix.length], base.prefix);
3756
+ }
3540
3757
  }
3541
3758
  }, {
3542
3759
  messageId: "addLineBreak",
3543
3760
  fix: (fixer) => {
3544
- return fixer.insertTextBeforeRange([first.line.range[0], first.line.range[0]], `${first.prefix.trimEnd()}\n`);
3761
+ const parsedLine = lines.get(first.line);
3762
+ return fixer.insertTextBeforeRange([parsedLine.range[0], parsedLine.range[0]], `${first.prefix.trimEnd()}\n`);
3545
3763
  }
3546
3764
  }]
3547
3765
  });
@@ -3551,7 +3769,7 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3551
3769
  const loc = sourceCode.getLoc(node);
3552
3770
  const startLine = loc.start.line;
3553
3771
  const endLine = loc.end.line;
3554
- const base = getBlockquoteLineInfo(lines.get(startLine));
3772
+ const base = getBlockquoteLevelFromLine(sourceCode, startLine);
3555
3773
  const invalidLines = [];
3556
3774
  for (let lineNumber = startLine + 1; lineNumber <= endLine; lineNumber++) {
3557
3775
  if (checkedLines.has(lineNumber)) {
@@ -3560,8 +3778,7 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
3560
3778
  continue;
3561
3779
  }
3562
3780
  checkedLines.add(lineNumber);
3563
- const line = lines.get(lineNumber);
3564
- const current = getBlockquoteLineInfo(line);
3781
+ const current = getBlockquoteLevelFromLine(sourceCode, lineNumber);
3565
3782
  if (base.level <= current.level) {
3566
3783
  reportInvalidLines(invalidLines, base);
3567
3784
  invalidLines.length = 0;
@@ -3638,7 +3855,7 @@ var no_multiple_empty_lines_default = createRule("no-multiple-empty-lines", {
3638
3855
  toml: addIgnoreLoc,
3639
3856
  json: addIgnoreLoc,
3640
3857
  "root:exit"() {
3641
- const lines = [...parseLines(sourceCode)];
3858
+ const lines = [...getParsedLines(sourceCode)];
3642
3859
  const bofEmptyLines = [];
3643
3860
  while (lines.length) {
3644
3861
  if (lines[0].text.trim()) break;
@@ -3852,7 +4069,7 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
3852
4069
  "root:exit"() {
3853
4070
  const re = /[^\S\n\r]+$/u;
3854
4071
  const skipMatch = /^[^\S\n\r]*$/u;
3855
- const lines = parseLines(sourceCode);
4072
+ const lines = getParsedLines(sourceCode);
3856
4073
  const commentLineNumbers = getCommentLineNumbers();
3857
4074
  for (const lineInfo of lines) {
3858
4075
  const matches = re.exec(lineInfo.text);
@@ -4085,6 +4302,272 @@ var ordered_list_marker_start_default = createRule("ordered-list-marker-start",
4085
4302
  }
4086
4303
  });
4087
4304
 
4305
+ //#endregion
4306
+ //#region src/rules/padding-line-between-blocks.ts
4307
+ /**
4308
+ * Determines whether a blank line must be preserved between two nodes
4309
+ * due to Markdown syntax constraints (e.g., setext headings).
4310
+ */
4311
+ function requiresBlankLineBetween(prev, next, sourceCode) {
4312
+ if (prev.type === "paragraph") {
4313
+ if (next.type === "paragraph" || next.type === "definition") return true;
4314
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4315
+ else if (next.type === "thematicBreak") {
4316
+ const marker = getThematicBreakMarker(sourceCode, next);
4317
+ return marker.kind === "-" && !marker.hasSpaces;
4318
+ }
4319
+ } else if (prev.type === "list") {
4320
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4321
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4322
+ } else if (prev.type === "blockquote") {
4323
+ if (next.type === "paragraph" || next.type === "blockquote" || next.type === "table" || next.type === "definition") return true;
4324
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4325
+ } else if (prev.type === "table") {
4326
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4327
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4328
+ } else if (prev.type === "footnoteDefinition") {
4329
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4330
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4331
+ } else if (prev.type === "html") return true;
4332
+ return false;
4333
+ }
4334
+ const BLOCK_TYPES = [
4335
+ "blockquote",
4336
+ "code",
4337
+ "heading",
4338
+ "html",
4339
+ "list",
4340
+ "paragraph",
4341
+ "thematic-break",
4342
+ "table",
4343
+ "link-definition",
4344
+ "footnote-definition",
4345
+ "frontmatter",
4346
+ "*"
4347
+ ];
4348
+ const BLOCK_TYPE_SCHEMAS = [{
4349
+ type: "string",
4350
+ enum: BLOCK_TYPES
4351
+ }, {
4352
+ type: "array",
4353
+ items: {
4354
+ type: "string",
4355
+ enum: BLOCK_TYPES
4356
+ },
4357
+ minItems: 1
4358
+ }];
4359
+ const SELECTOR_SCHEMA = { oneOf: [...BLOCK_TYPE_SCHEMAS, {
4360
+ type: "object",
4361
+ properties: {
4362
+ type: { oneOf: BLOCK_TYPE_SCHEMAS },
4363
+ in: { enum: [
4364
+ "list",
4365
+ "blockquote",
4366
+ "footnote-definition"
4367
+ ] }
4368
+ },
4369
+ required: ["type"],
4370
+ additionalProperties: false
4371
+ }] };
4372
+ const BLOCK_TYPE_MAP0 = {
4373
+ heading: "heading",
4374
+ paragraph: "paragraph",
4375
+ list: "list",
4376
+ blockquote: "blockquote",
4377
+ code: "code",
4378
+ html: "html",
4379
+ table: "table",
4380
+ thematicBreak: "thematic-break",
4381
+ definition: "link-definition",
4382
+ footnoteDefinition: "footnote-definition",
4383
+ json: "frontmatter",
4384
+ toml: "frontmatter",
4385
+ yaml: "frontmatter"
4386
+ };
4387
+ const BLOCK_TYPE_MAP = BLOCK_TYPE_MAP0;
4388
+ /**
4389
+ * Get the block type of a node
4390
+ */
4391
+ function getBlockType(node) {
4392
+ const nodeType = node.type;
4393
+ const blockType = BLOCK_TYPE_MAP[nodeType];
4394
+ if (blockType) return blockType;
4395
+ return null;
4396
+ }
4397
+ var padding_line_between_blocks_default = createRule("padding-line-between-blocks", {
4398
+ meta: {
4399
+ type: "layout",
4400
+ docs: {
4401
+ description: "require or disallow padding lines between blocks",
4402
+ categories: ["recommended"],
4403
+ listCategory: "Stylistic"
4404
+ },
4405
+ fixable: "whitespace",
4406
+ hasSuggestions: false,
4407
+ schema: {
4408
+ type: "array",
4409
+ items: {
4410
+ type: "object",
4411
+ properties: {
4412
+ prev: SELECTOR_SCHEMA,
4413
+ next: SELECTOR_SCHEMA,
4414
+ blankLine: {
4415
+ type: "string",
4416
+ enum: [
4417
+ "any",
4418
+ "never",
4419
+ "always"
4420
+ ]
4421
+ }
4422
+ },
4423
+ required: [
4424
+ "prev",
4425
+ "next",
4426
+ "blankLine"
4427
+ ],
4428
+ additionalProperties: false
4429
+ }
4430
+ },
4431
+ messages: {
4432
+ expectedBlankLine: "Expected a blank line between {{prevType}} and {{nextType}}.",
4433
+ unexpectedBlankLine: "Unexpected blank line between {{prevType}} and {{nextType}}."
4434
+ }
4435
+ },
4436
+ create(context) {
4437
+ const sourceCode = context.sourceCode;
4438
+ const options = [...context.options || []].reverse();
4439
+ const containerStack = [];
4440
+ /**
4441
+ * Check if the actual type matches the expected type pattern
4442
+ */
4443
+ function matchesType(actualType, block, expected) {
4444
+ if (Array.isArray(expected)) {
4445
+ for (const e of expected) {
4446
+ const matched$1 = matchesType(actualType, block, e);
4447
+ if (matched$1) return matched$1;
4448
+ }
4449
+ return null;
4450
+ }
4451
+ if (typeof expected === "string") return expected === actualType || expected === "*" ? actualType : null;
4452
+ let matched = null;
4453
+ if (Array.isArray(expected.type)) for (const e of expected.type) {
4454
+ matched = matchesType(actualType, block, e);
4455
+ if (matched) break;
4456
+ }
4457
+ else matched = matchesType(actualType, block, expected.type);
4458
+ if (!matched) return null;
4459
+ if (expected.in === "list") {
4460
+ if (containerStack[0]?.type !== "listItem") return null;
4461
+ } else if (expected.in === "blockquote") {
4462
+ if (containerStack[0]?.type !== "blockquote") return null;
4463
+ } else if (expected.in === "footnote-definition") {
4464
+ if (containerStack[0]?.type !== "footnoteDefinition") return null;
4465
+ }
4466
+ return matched;
4467
+ }
4468
+ /**
4469
+ * Get the expected padding between two blocks
4470
+ */
4471
+ function getExpectedPadding(prevBlock, nextBlock) {
4472
+ const prevType = getBlockType(prevBlock);
4473
+ const nextType = getBlockType(nextBlock);
4474
+ if (!prevType || !nextType) return null;
4475
+ for (const rule of options) {
4476
+ const prev = matchesType(prevType, prevBlock, rule.prev);
4477
+ if (!prev) continue;
4478
+ const next = matchesType(nextType, nextBlock, rule.next);
4479
+ if (!next) continue;
4480
+ return {
4481
+ prev,
4482
+ next,
4483
+ blankLine: rule.blankLine
4484
+ };
4485
+ }
4486
+ return null;
4487
+ }
4488
+ /**
4489
+ * Check padding between blocks in a container node
4490
+ */
4491
+ function checkBlockPadding(containerNode) {
4492
+ for (let i = 0; i < containerNode.children.length - 1; i++) {
4493
+ const prevBlock = containerNode.children[i];
4494
+ const nextBlock = containerNode.children[i + 1];
4495
+ const expected = getExpectedPadding(prevBlock, nextBlock);
4496
+ if (expected === null) continue;
4497
+ const prevLoc = sourceCode.getLoc(prevBlock);
4498
+ const nextLoc = sourceCode.getLoc(nextBlock);
4499
+ const actualBlankLine = nextLoc.start.line - prevLoc.end.line - 1;
4500
+ const hasBlankLine = actualBlankLine > 0;
4501
+ let messageId = "expectedBlankLine";
4502
+ if (expected.blankLine === "always") {
4503
+ if (hasBlankLine) continue;
4504
+ let list = null;
4505
+ const stack$1 = [...containerStack];
4506
+ let target$1;
4507
+ while (target$1 = stack$1.shift()) if (target$1.type === "listItem") {
4508
+ list = target$1;
4509
+ break;
4510
+ }
4511
+ if (list && !list.spread) continue;
4512
+ messageId = "expectedBlankLine";
4513
+ } else if (expected.blankLine === "never") {
4514
+ if (!hasBlankLine) continue;
4515
+ if (requiresBlankLineBetween(prevBlock, nextBlock, sourceCode)) continue;
4516
+ messageId = "unexpectedBlankLine";
4517
+ } else continue;
4518
+ const lineLength = sourceCode.lines[nextLoc.start.line - 1].length;
4519
+ let blockquote = null;
4520
+ const stack = [...containerStack];
4521
+ let target;
4522
+ while (target = stack.shift()) if (target.type === "blockquote") {
4523
+ blockquote = target;
4524
+ break;
4525
+ }
4526
+ context.report({
4527
+ node: nextBlock,
4528
+ loc: {
4529
+ start: nextLoc.start,
4530
+ end: {
4531
+ line: nextLoc.start.line,
4532
+ column: lineLength + 1
4533
+ }
4534
+ },
4535
+ messageId,
4536
+ data: {
4537
+ prevType: expected.prev,
4538
+ nextType: expected.next
4539
+ },
4540
+ fix(fixer) {
4541
+ if (expected.blankLine === "always") {
4542
+ let text = "\n";
4543
+ if (blockquote) {
4544
+ const blockquoteLoc = sourceCode.getLoc(blockquote);
4545
+ text += getBlockquoteLevelFromLine(sourceCode, blockquoteLoc.start.line).prefix.trimEnd();
4546
+ }
4547
+ const nextRange = sourceCode.getRange(nextBlock);
4548
+ const startNext = nextRange[0] - nextLoc.start.column;
4549
+ return fixer.insertTextBeforeRange([startNext, startNext], text);
4550
+ }
4551
+ const lines = getParsedLines(sourceCode);
4552
+ const linesToRemove = [];
4553
+ for (let line = prevLoc.end.line + 1; line < nextLoc.start.line; line++) linesToRemove.push(lines.get(line));
4554
+ return linesToRemove.map((line) => fixer.removeRange(line.range));
4555
+ }
4556
+ });
4557
+ }
4558
+ }
4559
+ return {
4560
+ "root, blockquote, listItem, footnoteDefinition"(node) {
4561
+ containerStack.unshift(node);
4562
+ },
4563
+ "root, blockquote, listItem, footnoteDefinition:exit"(node) {
4564
+ checkBlockPadding(node);
4565
+ containerStack.shift();
4566
+ }
4567
+ };
4568
+ }
4569
+ });
4570
+
4088
4571
  //#endregion
4089
4572
  //#region src/rules/prefer-autolinks.ts
4090
4573
  var prefer_autolinks_default = createRule("prefer-autolinks", {
@@ -4140,7 +4623,6 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
4140
4623
  },
4141
4624
  create(context) {
4142
4625
  const sourceCode = context.sourceCode;
4143
- const lines = parseLines(sourceCode);
4144
4626
  return { code(node) {
4145
4627
  const kind = getCodeBlockKind(sourceCode, node);
4146
4628
  if (kind === "backtick-fenced" || kind === "tilde-fenced") return;
@@ -4150,6 +4632,7 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
4150
4632
  messageId: "useFencedCodeBlock",
4151
4633
  fix(fixer) {
4152
4634
  if (!isFixableIndentedCodeBlock(node)) return null;
4635
+ const lines = getParsedLines(sourceCode);
4153
4636
  const startColumnOffset = loc.start.column - 1;
4154
4637
  const removeRanges = [];
4155
4638
  let prefixText = null;
@@ -4185,6 +4668,7 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
4185
4668
  */
4186
4669
  function isFixableIndentedCodeBlock(node) {
4187
4670
  if (!node.value.startsWith(" ")) return true;
4671
+ const lines = getParsedLines(sourceCode);
4188
4672
  const loc = sourceCode.getLoc(node);
4189
4673
  const firstLine = lines.get(loc.start.line);
4190
4674
  const codeBlockFirstLine = normalizePrefix(node.value.split(/\r?\n/u)[0]);
@@ -4808,14 +5292,14 @@ var sort_definitions_default = createRule("sort-definitions", {
4808
5292
  }
4809
5293
  /** Compile order option */
4810
5294
  function compileOption(orderOption) {
4811
- const cache$1 = /* @__PURE__ */ new Map();
5295
+ const cache$2 = /* @__PURE__ */ new Map();
4812
5296
  const compiled = compileOptionWithoutCache(orderOption);
4813
5297
  return {
4814
5298
  match: (node) => {
4815
- const cached = cache$1.get(node);
5299
+ const cached = cache$2.get(node);
4816
5300
  if (cached != null) return cached;
4817
5301
  const result = compiled.match(node);
4818
- cache$1.set(node, result);
5302
+ cache$2.set(node, result);
4819
5303
  return result;
4820
5304
  },
4821
5305
  sort: compiled.sort
@@ -5057,17 +5541,20 @@ var table_header_casing_default = createRule("table-header-casing", {
5057
5541
  const rules$1 = [
5058
5542
  atx_headings_closing_sequence_length_default,
5059
5543
  atx_headings_closing_sequence_default,
5544
+ blockquote_marker_alignment_default,
5060
5545
  canonical_code_block_language_default,
5061
5546
  definitions_last_default,
5062
5547
  emoji_notation_default,
5063
5548
  hard_linebreak_style_default,
5064
5549
  heading_casing_default,
5550
+ list_marker_alignment_default,
5065
5551
  no_laziness_blockquotes_default,
5066
5552
  no_multiple_empty_lines_default,
5067
5553
  no_text_backslash_linebreak_default,
5068
5554
  no_trailing_spaces_default,
5069
5555
  ordered_list_marker_sequence_default,
5070
5556
  ordered_list_marker_start_default,
5557
+ padding_line_between_blocks_default,
5071
5558
  prefer_autolinks_default,
5072
5559
  prefer_fenced_code_blocks_default,
5073
5560
  prefer_inline_code_words_default,
@@ -5099,9 +5586,12 @@ const plugins = {
5099
5586
  }
5100
5587
  };
5101
5588
  const rules$2 = {
5589
+ "markdown-preferences/blockquote-marker-alignment": "error",
5102
5590
  "markdown-preferences/hard-linebreak-style": "error",
5591
+ "markdown-preferences/list-marker-alignment": "error",
5103
5592
  "markdown-preferences/no-laziness-blockquotes": "error",
5104
5593
  "markdown-preferences/no-text-backslash-linebreak": "error",
5594
+ "markdown-preferences/padding-line-between-blocks": "error",
5105
5595
  "markdown-preferences/prefer-autolinks": "error",
5106
5596
  "markdown-preferences/prefer-fenced-code-blocks": "error"
5107
5597
  };
@@ -5114,7 +5604,7 @@ __export(meta_exports, {
5114
5604
  version: () => version
5115
5605
  });
5116
5606
  const name = "eslint-plugin-markdown-preferences";
5117
- const version = "0.14.0";
5607
+ const version = "0.16.0";
5118
5608
 
5119
5609
  //#endregion
5120
5610
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -33,7 +33,7 @@
33
33
  "docs:build": "vitepress build docs",
34
34
  "ts": "node --import=tsx",
35
35
  "mocha": "npm run ts -- ./node_modules/mocha/bin/mocha.js",
36
- "generate:version": "env-cmd -e version npm run update && npm run lint -- --fix",
36
+ "generate:version": "env-cmd -e version -- npm run update && npm run lint -- --fix",
37
37
  "changeset:version": "changeset version && npm run generate:version && git add --all",
38
38
  "changeset:publish": "npm run build && changeset publish"
39
39
  },
@@ -62,14 +62,14 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "emoji-regex-xs": "^2.0.1",
65
- "string-width": "^7.2.0"
65
+ "string-width": "^8.0.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@changesets/changelog-github": "^0.5.1",
69
69
  "@changesets/cli": "^2.28.1",
70
70
  "@changesets/get-release-plan": "^4.0.8",
71
71
  "@eslint/core": "^0.15.0",
72
- "@eslint/markdown": "^7.1.0",
72
+ "@eslint/markdown": "^7.2.0",
73
73
  "@ota-meshi/eslint-plugin": "^0.18.0",
74
74
  "@shikijs/vitepress-twoslash": "^3.0.0",
75
75
  "@types/eslint": "^9.6.1",
@@ -83,8 +83,8 @@
83
83
  "@types/semver": "^7.5.8",
84
84
  "assert": "^2.1.0",
85
85
  "c8": "^10.1.3",
86
- "env-cmd": "^10.1.0",
87
- "eslint": "^9.22.0",
86
+ "env-cmd": "^11.0.0",
87
+ "eslint": "^9.34.0",
88
88
  "eslint-compat-utils": "^0.6.4",
89
89
  "eslint-config-prettier": "^10.1.1",
90
90
  "eslint-plugin-eslint-comments": "^3.2.0",
@@ -93,6 +93,7 @@
93
93
  "eslint-plugin-json-schema-validator": "^5.3.1",
94
94
  "eslint-plugin-jsonc": "^2.19.1",
95
95
  "eslint-plugin-markdown": "^5.1.0",
96
+ "eslint-plugin-markdown-links": "^0.4.0",
96
97
  "eslint-plugin-n": "^17.16.2",
97
98
  "eslint-plugin-node-dependencies": "^1.0.0",
98
99
  "eslint-plugin-prettier": "^5.2.3",