eslint-plugin-markdown-preferences 0.15.0 → 0.17.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
@@ -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,10 +104,14 @@ 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. | 🔧 | |
@@ -119,15 +125,26 @@ The rules with the following star ⭐ are included in the configs.
119
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. | | ⭐ |
120
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. | 🔧 | |
121
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 | 🔧 | ⭐ |
122
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 | 🔧 | ⭐ |
123
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 | 🔧 | ⭐ |
124
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 | 🔧 | |
132
+ | [markdown-preferences/setext-heading-underline-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html) | enforce setext heading underline length | 🔧 | |
125
133
  | [markdown-preferences/sort-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html) | enforce a specific order for link definitions and footnote definitions | 🔧 | |
134
+ | [markdown-preferences/thematic-break-character-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-character-style.html) | enforce consistent character style for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
135
+ | [markdown-preferences/thematic-break-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-length.html) | enforce consistent length for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
136
+ | [markdown-preferences/thematic-break-sequence-pattern](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-sequence-pattern.html) | enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
126
137
 
127
138
  <!-- prettier-ignore-end -->
128
139
 
129
140
  <!--RULES_TABLE_END-->
141
+
130
142
  <!--RULES_SECTION_END-->
143
+
144
+ ## 👫 Related Packages
145
+
146
+ - [eslint-plugin-markdown-links](https://github.com/ota-meshi/eslint-plugin-markdown-links) ... ESLint plugin with powerful checking rules related to Markdown links.
147
+
131
148
  <!--DOCS_IGNORE_START-->
132
149
 
133
150
  ## 🍻 Contributing
package/lib/index.d.ts CHANGED
@@ -85,6 +85,11 @@ interface RuleOptions {
85
85
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html
86
86
  */
87
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>;
88
93
  /**
89
94
  * enforce the use of autolinks for URLs
90
95
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html
@@ -110,6 +115,11 @@ interface RuleOptions {
110
115
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
111
116
  */
112
117
  'markdown-preferences/prefer-linked-words'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkedWords>;
118
+ /**
119
+ * enforce setext heading underline length
120
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html
121
+ */
122
+ 'markdown-preferences/setext-heading-underline-length'?: Linter.RuleEntry<MarkdownPreferencesSetextHeadingUnderlineLength>;
113
123
  /**
114
124
  * enforce a specific order for link definitions and footnote definitions
115
125
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
@@ -120,6 +130,21 @@ interface RuleOptions {
120
130
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
121
131
  */
122
132
  'markdown-preferences/table-header-casing'?: Linter.RuleEntry<MarkdownPreferencesTableHeaderCasing>;
133
+ /**
134
+ * enforce consistent character style for thematic breaks (horizontal rules) in Markdown.
135
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-character-style.html
136
+ */
137
+ 'markdown-preferences/thematic-break-character-style'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakCharacterStyle>;
138
+ /**
139
+ * enforce consistent length for thematic breaks (horizontal rules) in Markdown.
140
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-length.html
141
+ */
142
+ 'markdown-preferences/thematic-break-length'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakLength>;
143
+ /**
144
+ * enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown.
145
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-sequence-pattern.html
146
+ */
147
+ 'markdown-preferences/thematic-break-sequence-pattern'?: Linter.RuleEntry<MarkdownPreferencesThematicBreakSequencePattern>;
123
148
  }
124
149
  type MarkdownPreferencesAtxHeadingsClosingSequence = [] | [{
125
150
  closingSequence?: ("always" | "never");
@@ -162,6 +187,17 @@ type MarkdownPreferencesNoTrailingSpaces = [] | [{
162
187
  type MarkdownPreferencesOrderedListMarkerStart = [] | [{
163
188
  start?: (1 | 0);
164
189
  }];
190
+ type MarkdownPreferencesPaddingLineBetweenBlocks = {
191
+ 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" | "*"))[]] | {
192
+ 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" | "*"))[]]);
193
+ in?: ("list" | "blockquote" | "footnote-definition");
194
+ });
195
+ 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" | "*"))[]] | {
196
+ 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" | "*"))[]]);
197
+ in?: ("list" | "blockquote" | "footnote-definition");
198
+ });
199
+ blankLine: ("any" | "never" | "always");
200
+ }[];
165
201
  type MarkdownPreferencesPreferInlineCodeWords = [] | [{
166
202
  words: string[];
167
203
  ignores?: {
@@ -189,6 +225,11 @@ type MarkdownPreferencesPreferLinkedWords = [] | [{
189
225
  }[];
190
226
  [k: string]: unknown | undefined;
191
227
  }];
228
+ type MarkdownPreferencesSetextHeadingUnderlineLength = [] | [{
229
+ mode?: ("exact" | "minimum" | "consistent" | "consistent-line-length");
230
+ align?: ("any" | "exact" | "minimum" | "length");
231
+ length?: number;
232
+ }];
192
233
  type MarkdownPreferencesSortDefinitions = [] | [{
193
234
  order?: (string | [string, ...(string)[]] | {
194
235
  match: (string | [string, ...(string)[]]);
@@ -202,6 +243,15 @@ type MarkdownPreferencesTableHeaderCasing = [] | [{
202
243
  ignorePatterns?: string[];
203
244
  minorWords?: string[];
204
245
  }];
246
+ type MarkdownPreferencesThematicBreakCharacterStyle = [] | [{
247
+ style?: ("-" | "*" | "_");
248
+ }];
249
+ type MarkdownPreferencesThematicBreakLength = [] | [{
250
+ length?: number;
251
+ }];
252
+ type MarkdownPreferencesThematicBreakSequencePattern = [] | [{
253
+ pattern: (string | string | string);
254
+ }];
205
255
  declare namespace recommended_d_exports {
206
256
  export { files, language, languageOptions, name$1 as name, plugins, rules$1 as rules };
207
257
  }
@@ -220,7 +270,7 @@ declare namespace meta_d_exports {
220
270
  export { name, version };
221
271
  }
222
272
  declare const name: "eslint-plugin-markdown-preferences";
223
- declare const version: "0.15.0";
273
+ declare const version: "0.17.0";
224
274
  //#endregion
225
275
  //#region src/index.d.ts
226
276
  declare const configs: {
package/lib/index.js CHANGED
@@ -27,14 +27,111 @@ 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).trimEnd();
90
+ return {
91
+ kind: text.startsWith("-") ? "-" : text.startsWith("*") ? "*" : "_",
92
+ hasSpaces: /\s/u.test(text),
93
+ text
94
+ };
95
+ }
96
+ /**
97
+ * Get the source location from a range in a node.
98
+ */
99
+ function getSourceLocationFromRange(sourceCode, node, range) {
100
+ const [nodeStart] = sourceCode.getRange(node);
101
+ let startLine, startColumn;
102
+ if (nodeStart <= range[0]) {
103
+ const loc = sourceCode.getLoc(node);
104
+ const beforeLines = sourceCode.text.slice(nodeStart, range[0]).split(/\n/u);
105
+ startLine = loc.start.line + beforeLines.length - 1;
106
+ startColumn = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
107
+ } else {
108
+ const beforeLines = sourceCode.text.slice(0, range[0]).split(/\n/u);
109
+ startLine = beforeLines.length;
110
+ startColumn = 1 + (beforeLines.at(-1) || "").length;
111
+ }
112
+ const contentLines = sourceCode.text.slice(range[0], range[1]).split(/\n/u);
113
+ const endLine = startLine + contentLines.length - 1;
114
+ const endColumn = (contentLines.length === 1 ? startColumn : 1) + (contentLines.at(-1) || "").length;
115
+ return {
116
+ start: {
117
+ line: startLine,
118
+ column: startColumn
119
+ },
120
+ end: {
121
+ line: endLine,
122
+ column: endColumn
123
+ }
124
+ };
125
+ }
126
+
30
127
  //#endregion
31
128
  //#region src/utils/atx-heading.ts
32
129
  /**
33
130
  * Parse the closing sequence of an ATX heading.
34
131
  */
35
132
  function parseATXHeadingClosingSequence(sourceCode, node) {
133
+ if (getHeadingKind(sourceCode, node) !== "atx") return null;
36
134
  const loc = sourceCode.getLoc(node);
37
- if (loc.start.line !== loc.end.line) return null;
38
135
  const range = sourceCode.getRange(node);
39
136
  const parsed = parseATXHeadingClosingSequenceFromText(sourceCode.text.slice(...range));
40
137
  if (parsed == null) return { closingSequence: null };
@@ -159,8 +256,22 @@ function getParsedLines(sourceCode) {
159
256
  }
160
257
 
161
258
  //#endregion
162
- //#region src/rules/atx-headings-closing-sequence-length.ts
259
+ //#region src/utils/get-text-width.ts
163
260
  let segmenter;
261
+ /**
262
+ * Get the width of a text string.
263
+ */
264
+ function getTextWidth(text) {
265
+ if (!text.includes(" ")) return stringWidth(text);
266
+ if (!segmenter) segmenter = new Intl.Segmenter("en");
267
+ let width = 0;
268
+ for (const { segment: c } of segmenter.segment(text)) if (c === " ") width += 4 - width % 4;
269
+ else width += stringWidth(c);
270
+ return width;
271
+ }
272
+
273
+ //#endregion
274
+ //#region src/rules/atx-headings-closing-sequence-length.ts
164
275
  var atx_headings_closing_sequence_length_default = createRule("atx-headings-closing-sequence-length", {
165
276
  meta: {
166
277
  type: "layout",
@@ -301,17 +412,6 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
301
412
  }
302
413
  }
303
414
  });
304
- /**
305
- * Get the width of a text string.
306
- */
307
- function getTextWidth(text) {
308
- if (!text.includes(" ")) return stringWidth(text);
309
- if (!segmenter) segmenter = new Intl.Segmenter("en");
310
- let width = 0;
311
- for (const { segment: c } of segmenter.segment(text)) if (c === " ") width += 4 - width % 4;
312
- else width += stringWidth(c);
313
- return width;
314
- }
315
415
 
316
416
  //#endregion
317
417
  //#region src/rules/atx-headings-closing-sequence.ts
@@ -497,84 +597,6 @@ var blockquote_marker_alignment_default = createRule("blockquote-marker-alignmen
497
597
  }
498
598
  });
499
599
 
500
- //#endregion
501
- //#region src/utils/ast.ts
502
- /**
503
- * Get the kind of code block.
504
- */
505
- function getCodeBlockKind(sourceCode, node) {
506
- const text = sourceCode.getText(node);
507
- return text.startsWith("```") ? "backtick-fenced" : text.startsWith("~~~") ? "tilde-fenced" : "indented";
508
- }
509
- /**
510
- * Get the kind of link.
511
- */
512
- function getLinkKind(sourceCode, node) {
513
- const text = sourceCode.getText(node);
514
- return text.startsWith("[") ? "inline" : text.startsWith("<") && text.endsWith(">") ? "autolink" : "gfm-autolink";
515
- }
516
- /**
517
- * Get the marker of a list item.
518
- */
519
- function getListItemMarker(sourceCode, node) {
520
- const item = node.type === "list" ? node.children[0] || node : node;
521
- const text = sourceCode.getText(item);
522
- if (text.startsWith("-")) return {
523
- kind: "-",
524
- raw: "-"
525
- };
526
- if (text.startsWith("*")) return {
527
- kind: "*",
528
- raw: "*"
529
- };
530
- if (text.startsWith("+")) return {
531
- kind: "+",
532
- raw: "+"
533
- };
534
- const matchDot = /^(\d+)\./.exec(text);
535
- if (matchDot) return {
536
- kind: ".",
537
- raw: matchDot[0],
538
- sequence: Number(matchDot[1])
539
- };
540
- const matchParen = /^(\d+)\)/.exec(text);
541
- return {
542
- kind: ")",
543
- raw: matchParen[0],
544
- sequence: Number(matchParen[1])
545
- };
546
- }
547
- /**
548
- * Get the source location from a range in a node.
549
- */
550
- function getSourceLocationFromRange(sourceCode, node, range) {
551
- const [nodeStart] = sourceCode.getRange(node);
552
- let startLine, startColumn;
553
- if (nodeStart <= range[0]) {
554
- const loc = sourceCode.getLoc(node);
555
- const beforeLines = sourceCode.text.slice(nodeStart, range[0]).split(/\n/u);
556
- startLine = loc.start.line + beforeLines.length - 1;
557
- startColumn = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
558
- } else {
559
- const beforeLines = sourceCode.text.slice(0, range[0]).split(/\n/u);
560
- startLine = beforeLines.length;
561
- startColumn = 1 + (beforeLines.at(-1) || "").length;
562
- }
563
- const contentLines = sourceCode.text.slice(range[0], range[1]).split(/\n/u);
564
- const endLine = startLine + contentLines.length - 1;
565
- const endColumn = (contentLines.length === 1 ? startColumn : 1) + (contentLines.at(-1) || "").length;
566
- return {
567
- start: {
568
- line: startLine,
569
- column: startColumn
570
- },
571
- end: {
572
- line: endLine,
573
- column: endColumn
574
- }
575
- };
576
- }
577
-
578
600
  //#endregion
579
601
  //#region src/rules/canonical-code-block-language.ts
580
602
  const DEFAULT_LANGUAGES = {
@@ -4284,6 +4306,272 @@ var ordered_list_marker_start_default = createRule("ordered-list-marker-start",
4284
4306
  }
4285
4307
  });
4286
4308
 
4309
+ //#endregion
4310
+ //#region src/rules/padding-line-between-blocks.ts
4311
+ /**
4312
+ * Determines whether a blank line must be preserved between two nodes
4313
+ * due to Markdown syntax constraints (e.g., setext headings).
4314
+ */
4315
+ function requiresBlankLineBetween(prev, next, sourceCode) {
4316
+ if (prev.type === "paragraph") {
4317
+ if (next.type === "paragraph" || next.type === "definition") return true;
4318
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4319
+ else if (next.type === "thematicBreak") {
4320
+ const marker = getThematicBreakMarker(sourceCode, next);
4321
+ return marker.kind === "-" && !marker.hasSpaces;
4322
+ }
4323
+ } else if (prev.type === "list") {
4324
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4325
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4326
+ } else if (prev.type === "blockquote") {
4327
+ if (next.type === "paragraph" || next.type === "blockquote" || next.type === "table" || next.type === "definition") return true;
4328
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4329
+ } else if (prev.type === "table") {
4330
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4331
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4332
+ } else if (prev.type === "footnoteDefinition") {
4333
+ if (next.type === "paragraph" || next.type === "table" || next.type === "definition") return true;
4334
+ else if (next.type === "heading") return getHeadingKind(sourceCode, next) === "setext";
4335
+ } else if (prev.type === "html") return true;
4336
+ return false;
4337
+ }
4338
+ const BLOCK_TYPES = [
4339
+ "blockquote",
4340
+ "code",
4341
+ "heading",
4342
+ "html",
4343
+ "list",
4344
+ "paragraph",
4345
+ "thematic-break",
4346
+ "table",
4347
+ "link-definition",
4348
+ "footnote-definition",
4349
+ "frontmatter",
4350
+ "*"
4351
+ ];
4352
+ const BLOCK_TYPE_SCHEMAS = [{
4353
+ type: "string",
4354
+ enum: BLOCK_TYPES
4355
+ }, {
4356
+ type: "array",
4357
+ items: {
4358
+ type: "string",
4359
+ enum: BLOCK_TYPES
4360
+ },
4361
+ minItems: 1
4362
+ }];
4363
+ const SELECTOR_SCHEMA = { oneOf: [...BLOCK_TYPE_SCHEMAS, {
4364
+ type: "object",
4365
+ properties: {
4366
+ type: { oneOf: BLOCK_TYPE_SCHEMAS },
4367
+ in: { enum: [
4368
+ "list",
4369
+ "blockquote",
4370
+ "footnote-definition"
4371
+ ] }
4372
+ },
4373
+ required: ["type"],
4374
+ additionalProperties: false
4375
+ }] };
4376
+ const BLOCK_TYPE_MAP0 = {
4377
+ heading: "heading",
4378
+ paragraph: "paragraph",
4379
+ list: "list",
4380
+ blockquote: "blockquote",
4381
+ code: "code",
4382
+ html: "html",
4383
+ table: "table",
4384
+ thematicBreak: "thematic-break",
4385
+ definition: "link-definition",
4386
+ footnoteDefinition: "footnote-definition",
4387
+ json: "frontmatter",
4388
+ toml: "frontmatter",
4389
+ yaml: "frontmatter"
4390
+ };
4391
+ const BLOCK_TYPE_MAP = BLOCK_TYPE_MAP0;
4392
+ /**
4393
+ * Get the block type of a node
4394
+ */
4395
+ function getBlockType(node) {
4396
+ const nodeType = node.type;
4397
+ const blockType = BLOCK_TYPE_MAP[nodeType];
4398
+ if (blockType) return blockType;
4399
+ return null;
4400
+ }
4401
+ var padding_line_between_blocks_default = createRule("padding-line-between-blocks", {
4402
+ meta: {
4403
+ type: "layout",
4404
+ docs: {
4405
+ description: "require or disallow padding lines between blocks",
4406
+ categories: ["recommended"],
4407
+ listCategory: "Stylistic"
4408
+ },
4409
+ fixable: "whitespace",
4410
+ hasSuggestions: false,
4411
+ schema: {
4412
+ type: "array",
4413
+ items: {
4414
+ type: "object",
4415
+ properties: {
4416
+ prev: SELECTOR_SCHEMA,
4417
+ next: SELECTOR_SCHEMA,
4418
+ blankLine: {
4419
+ type: "string",
4420
+ enum: [
4421
+ "any",
4422
+ "never",
4423
+ "always"
4424
+ ]
4425
+ }
4426
+ },
4427
+ required: [
4428
+ "prev",
4429
+ "next",
4430
+ "blankLine"
4431
+ ],
4432
+ additionalProperties: false
4433
+ }
4434
+ },
4435
+ messages: {
4436
+ expectedBlankLine: "Expected a blank line between {{prevType}} and {{nextType}}.",
4437
+ unexpectedBlankLine: "Unexpected blank line between {{prevType}} and {{nextType}}."
4438
+ }
4439
+ },
4440
+ create(context) {
4441
+ const sourceCode = context.sourceCode;
4442
+ const options = [...context.options || []].reverse();
4443
+ const containerStack = [];
4444
+ /**
4445
+ * Check if the actual type matches the expected type pattern
4446
+ */
4447
+ function matchesType(actualType, block, expected) {
4448
+ if (Array.isArray(expected)) {
4449
+ for (const e of expected) {
4450
+ const matched$1 = matchesType(actualType, block, e);
4451
+ if (matched$1) return matched$1;
4452
+ }
4453
+ return null;
4454
+ }
4455
+ if (typeof expected === "string") return expected === actualType || expected === "*" ? actualType : null;
4456
+ let matched = null;
4457
+ if (Array.isArray(expected.type)) for (const e of expected.type) {
4458
+ matched = matchesType(actualType, block, e);
4459
+ if (matched) break;
4460
+ }
4461
+ else matched = matchesType(actualType, block, expected.type);
4462
+ if (!matched) return null;
4463
+ if (expected.in === "list") {
4464
+ if (containerStack[0]?.type !== "listItem") return null;
4465
+ } else if (expected.in === "blockquote") {
4466
+ if (containerStack[0]?.type !== "blockquote") return null;
4467
+ } else if (expected.in === "footnote-definition") {
4468
+ if (containerStack[0]?.type !== "footnoteDefinition") return null;
4469
+ }
4470
+ return matched;
4471
+ }
4472
+ /**
4473
+ * Get the expected padding between two blocks
4474
+ */
4475
+ function getExpectedPadding(prevBlock, nextBlock) {
4476
+ const prevType = getBlockType(prevBlock);
4477
+ const nextType = getBlockType(nextBlock);
4478
+ if (!prevType || !nextType) return null;
4479
+ for (const rule of options) {
4480
+ const prev = matchesType(prevType, prevBlock, rule.prev);
4481
+ if (!prev) continue;
4482
+ const next = matchesType(nextType, nextBlock, rule.next);
4483
+ if (!next) continue;
4484
+ return {
4485
+ prev,
4486
+ next,
4487
+ blankLine: rule.blankLine
4488
+ };
4489
+ }
4490
+ return null;
4491
+ }
4492
+ /**
4493
+ * Check padding between blocks in a container node
4494
+ */
4495
+ function checkBlockPadding(containerNode) {
4496
+ for (let i = 0; i < containerNode.children.length - 1; i++) {
4497
+ const prevBlock = containerNode.children[i];
4498
+ const nextBlock = containerNode.children[i + 1];
4499
+ const expected = getExpectedPadding(prevBlock, nextBlock);
4500
+ if (expected === null) continue;
4501
+ const prevLoc = sourceCode.getLoc(prevBlock);
4502
+ const nextLoc = sourceCode.getLoc(nextBlock);
4503
+ const actualBlankLine = nextLoc.start.line - prevLoc.end.line - 1;
4504
+ const hasBlankLine = actualBlankLine > 0;
4505
+ let messageId = "expectedBlankLine";
4506
+ if (expected.blankLine === "always") {
4507
+ if (hasBlankLine) continue;
4508
+ let list = null;
4509
+ const stack$1 = [...containerStack];
4510
+ let target$1;
4511
+ while (target$1 = stack$1.shift()) if (target$1.type === "listItem") {
4512
+ list = target$1;
4513
+ break;
4514
+ }
4515
+ if (list && !list.spread) continue;
4516
+ messageId = "expectedBlankLine";
4517
+ } else if (expected.blankLine === "never") {
4518
+ if (!hasBlankLine) continue;
4519
+ if (requiresBlankLineBetween(prevBlock, nextBlock, sourceCode)) continue;
4520
+ messageId = "unexpectedBlankLine";
4521
+ } else continue;
4522
+ const lineLength = sourceCode.lines[nextLoc.start.line - 1].length;
4523
+ let blockquote = null;
4524
+ const stack = [...containerStack];
4525
+ let target;
4526
+ while (target = stack.shift()) if (target.type === "blockquote") {
4527
+ blockquote = target;
4528
+ break;
4529
+ }
4530
+ context.report({
4531
+ node: nextBlock,
4532
+ loc: {
4533
+ start: nextLoc.start,
4534
+ end: {
4535
+ line: nextLoc.start.line,
4536
+ column: lineLength + 1
4537
+ }
4538
+ },
4539
+ messageId,
4540
+ data: {
4541
+ prevType: expected.prev,
4542
+ nextType: expected.next
4543
+ },
4544
+ fix(fixer) {
4545
+ if (expected.blankLine === "always") {
4546
+ let text = "\n";
4547
+ if (blockquote) {
4548
+ const blockquoteLoc = sourceCode.getLoc(blockquote);
4549
+ text += getBlockquoteLevelFromLine(sourceCode, blockquoteLoc.start.line).prefix.trimEnd();
4550
+ }
4551
+ const nextRange = sourceCode.getRange(nextBlock);
4552
+ const startNext = nextRange[0] - nextLoc.start.column;
4553
+ return fixer.insertTextBeforeRange([startNext, startNext], text);
4554
+ }
4555
+ const lines = getParsedLines(sourceCode);
4556
+ const linesToRemove = [];
4557
+ for (let line = prevLoc.end.line + 1; line < nextLoc.start.line; line++) linesToRemove.push(lines.get(line));
4558
+ return linesToRemove.map((line) => fixer.removeRange(line.range));
4559
+ }
4560
+ });
4561
+ }
4562
+ }
4563
+ return {
4564
+ "root, blockquote, listItem, footnoteDefinition"(node) {
4565
+ containerStack.unshift(node);
4566
+ },
4567
+ "root, blockquote, listItem, footnoteDefinition:exit"(node) {
4568
+ checkBlockPadding(node);
4569
+ containerStack.shift();
4570
+ }
4571
+ };
4572
+ }
4573
+ });
4574
+
4287
4575
  //#endregion
4288
4576
  //#region src/rules/prefer-autolinks.ts
4289
4577
  var prefer_autolinks_default = createRule("prefer-autolinks", {
@@ -4835,6 +5123,360 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
4835
5123
  }
4836
5124
  });
4837
5125
 
5126
+ //#endregion
5127
+ //#region src/utils/setext-heading.ts
5128
+ /**
5129
+ * Parse the setext heading.
5130
+ */
5131
+ function parseSetextHeading(sourceCode, node) {
5132
+ if (getHeadingKind(sourceCode, node) !== "setext") return null;
5133
+ const lines = getParsedLines(sourceCode);
5134
+ const contentLines = [];
5135
+ const nodeLoc = sourceCode.getLoc(node);
5136
+ for (let lineNumber = nodeLoc.start.line; lineNumber < nodeLoc.end.line; lineNumber++) {
5137
+ const content = parseContent(lines.get(lineNumber));
5138
+ contentLines.push(content);
5139
+ }
5140
+ const underline = parseUnderline(lines.get(nodeLoc.end.line));
5141
+ if (!underline) return null;
5142
+ return {
5143
+ contentLines,
5144
+ underline
5145
+ };
5146
+ }
5147
+ /**
5148
+ * Parse the content line of a setext heading.
5149
+ */
5150
+ function parseContent(line) {
5151
+ let prefix = "";
5152
+ let spaceBefore = "";
5153
+ let suffix = "";
5154
+ for (let index = 0; index < line.text.length; index++) {
5155
+ const c = line.text[index];
5156
+ if (!c.trim()) {
5157
+ spaceBefore += c;
5158
+ continue;
5159
+ }
5160
+ if (c === ">" && spaceBefore.length < 4) {
5161
+ prefix += spaceBefore + c;
5162
+ spaceBefore = "";
5163
+ continue;
5164
+ }
5165
+ suffix = line.text.slice(index);
5166
+ break;
5167
+ }
5168
+ const content = suffix.trimEnd();
5169
+ const spaceAfter = suffix.slice(content.length);
5170
+ return {
5171
+ text: content,
5172
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
5173
+ loc: {
5174
+ start: {
5175
+ line: line.line,
5176
+ column: prefix.length + spaceBefore.length + 1
5177
+ },
5178
+ end: {
5179
+ line: line.line,
5180
+ column: prefix.length + spaceBefore.length + content.length + 1
5181
+ }
5182
+ },
5183
+ raws: {
5184
+ prefix,
5185
+ spaceBefore,
5186
+ spaceAfter
5187
+ }
5188
+ };
5189
+ }
5190
+ /**
5191
+ * Parse the underline of a setext heading.
5192
+ */
5193
+ function parseUnderline(line) {
5194
+ let marker = null;
5195
+ let underlineText = "";
5196
+ let prefix = "";
5197
+ let spaceBefore = "";
5198
+ let spaceAfter = "";
5199
+ for (let index = line.text.length - 1; index >= 0; index--) {
5200
+ const c = line.text[index];
5201
+ if (!marker) {
5202
+ if (c === "=" || c === "-") {
5203
+ underlineText = c + underlineText;
5204
+ marker = c;
5205
+ } else if (!c.trim()) spaceAfter = c + spaceAfter;
5206
+ else return null;
5207
+ continue;
5208
+ }
5209
+ if (c === marker) {
5210
+ underlineText = c + spaceBefore + underlineText;
5211
+ spaceBefore = "";
5212
+ } else if (!c.trim()) spaceBefore = c + spaceBefore;
5213
+ else {
5214
+ prefix = line.text.slice(0, index + 1);
5215
+ break;
5216
+ }
5217
+ }
5218
+ if (!marker) return null;
5219
+ const underlineLoc = {
5220
+ start: {
5221
+ line: line.line,
5222
+ column: prefix.length + spaceBefore.length + 1
5223
+ },
5224
+ end: {
5225
+ line: line.line,
5226
+ column: prefix.length + spaceBefore.length + underlineText.length + 1
5227
+ }
5228
+ };
5229
+ return {
5230
+ text: underlineText,
5231
+ range: [line.range[0] + prefix.length + spaceBefore.length, line.range[1] - line.linebreak.length - spaceAfter.length],
5232
+ loc: underlineLoc,
5233
+ marker,
5234
+ raws: {
5235
+ prefix,
5236
+ spaceBefore,
5237
+ spaceAfter
5238
+ }
5239
+ };
5240
+ }
5241
+
5242
+ //#endregion
5243
+ //#region src/rules/setext-heading-underline-length.ts
5244
+ var setext_heading_underline_length_default = createRule("setext-heading-underline-length", {
5245
+ meta: {
5246
+ type: "layout",
5247
+ docs: {
5248
+ description: "enforce setext heading underline length",
5249
+ categories: [],
5250
+ listCategory: "Stylistic"
5251
+ },
5252
+ fixable: "whitespace",
5253
+ schema: [{
5254
+ type: "object",
5255
+ properties: {
5256
+ mode: {
5257
+ type: "string",
5258
+ enum: [
5259
+ "exact",
5260
+ "minimum",
5261
+ "consistent",
5262
+ "consistent-line-length"
5263
+ ]
5264
+ },
5265
+ align: {
5266
+ type: "string",
5267
+ enum: [
5268
+ "any",
5269
+ "exact",
5270
+ "minimum",
5271
+ "length"
5272
+ ]
5273
+ },
5274
+ length: {
5275
+ type: "integer",
5276
+ minimum: 1
5277
+ }
5278
+ },
5279
+ additionalProperties: false
5280
+ }],
5281
+ messages: {
5282
+ exactLength: "Setext heading underline should be exactly the same length as the heading text.",
5283
+ minimumLength: "Setext heading underline should be at least as long as the heading text.",
5284
+ consistentAny: "Setext heading underline should be consistent with other underlines in the document.",
5285
+ consistentExact: "Setext heading underline should be exactly the same length as the longest heading text in the document.",
5286
+ consistentMinimum: "Setext heading underline should be at least as long as the longest heading text in the document.",
5287
+ consistentLength: "Setext heading underline should be {{expectedLength}} characters long for consistency.",
5288
+ consistentLineLengthAny: "Setext heading underline should be consistent in line length with other underlines in the document.",
5289
+ consistentLineLengthExact: "Setext heading underline should be exactly the same line length as the longest heading line in the document.",
5290
+ consistentLineLengthMinimum: "Setext heading underline should be at least as long as the longest heading line in the document.",
5291
+ consistentLineLengthLength: "Setext heading underline should be {{expectedLength}} characters long for line length consistency."
5292
+ }
5293
+ },
5294
+ create(context) {
5295
+ const sourceCode = context.sourceCode;
5296
+ const options = context.options[0] || {};
5297
+ const mode = options.mode || "exact";
5298
+ const parsedSetextHeadings = /* @__PURE__ */ new Map();
5299
+ /**
5300
+ * Get the parsed setext heading for a specific heading.
5301
+ */
5302
+ function getParsedSetextHeading(heading) {
5303
+ const cached = parsedSetextHeadings.get(heading);
5304
+ if (cached) return cached;
5305
+ const underline = parseSetextHeading(sourceCode, heading);
5306
+ if (!underline) return null;
5307
+ parsedSetextHeadings.set(heading, underline);
5308
+ return underline;
5309
+ }
5310
+ /**
5311
+ * Helper function to report errors for both regular and blockquote headings
5312
+ */
5313
+ function reportError(node, messageId, expectedLength) {
5314
+ const parsed = getParsedSetextHeading(node);
5315
+ if (!parsed) return;
5316
+ context.report({
5317
+ node,
5318
+ messageId,
5319
+ loc: parsed.underline.loc,
5320
+ data: { expectedLength: String(expectedLength) },
5321
+ fix(fixer) {
5322
+ const newUnderline = parsed.underline.marker.repeat(expectedLength);
5323
+ return fixer.replaceTextRange(parsed.underline.range, newUnderline);
5324
+ }
5325
+ });
5326
+ }
5327
+ if (mode === "exact" || mode === "minimum") return { heading(node) {
5328
+ const parsed = getParsedSetextHeading(node);
5329
+ if (!parsed) return;
5330
+ const expectedLength = getMaxHeaderTextWidth(parsed);
5331
+ if (expectedLength < 1) return;
5332
+ if (mode === "exact") {
5333
+ if (parsed.underline.text.length !== expectedLength) reportError(node, "exactLength", expectedLength);
5334
+ } else if (mode === "minimum") {
5335
+ if (parsed.underline.text.length < expectedLength) reportError(node, "minimumLength", expectedLength);
5336
+ }
5337
+ } };
5338
+ if (mode === "consistent") {
5339
+ const align = options.align || "exact";
5340
+ const fixedLength = options.length || 0;
5341
+ const setextHeadings = [];
5342
+ return {
5343
+ heading(node) {
5344
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5345
+ setextHeadings.push(node);
5346
+ },
5347
+ "root:exit"() {
5348
+ if (setextHeadings.length === 0) return;
5349
+ let expectedLength = 0;
5350
+ if (align === "any") {
5351
+ if (setextHeadings.length < 2) return;
5352
+ for (const node of setextHeadings) {
5353
+ const parsed = getParsedSetextHeading(node);
5354
+ if (!parsed) continue;
5355
+ expectedLength = parsed.underline.text.length;
5356
+ break;
5357
+ }
5358
+ } else if (align === "exact") for (const node of setextHeadings) {
5359
+ const parsed = getParsedSetextHeading(node);
5360
+ if (!parsed) continue;
5361
+ expectedLength = Math.max(expectedLength, getMaxHeaderTextWidth(parsed));
5362
+ }
5363
+ else if (align === "minimum") {
5364
+ let maxTextWidth = 0;
5365
+ for (const node of setextHeadings) {
5366
+ const parsed = getParsedSetextHeading(node);
5367
+ if (!parsed) continue;
5368
+ maxTextWidth = Math.max(maxTextWidth, getMaxHeaderTextWidth(parsed));
5369
+ expectedLength = Math.max(expectedLength, parsed.underline.text.length);
5370
+ }
5371
+ if (expectedLength < maxTextWidth) expectedLength = maxTextWidth;
5372
+ else for (const node of setextHeadings) {
5373
+ const parsed = getParsedSetextHeading(node);
5374
+ if (!parsed) continue;
5375
+ if (maxTextWidth <= parsed.underline.text.length) expectedLength = Math.min(expectedLength, parsed.underline.text.length);
5376
+ }
5377
+ } else if (align === "length") expectedLength = fixedLength;
5378
+ else return;
5379
+ if (!expectedLength || expectedLength < 1) return;
5380
+ for (const node of setextHeadings) {
5381
+ const parsed = getParsedSetextHeading(node);
5382
+ if (!parsed) continue;
5383
+ if (parsed.underline.text.length === expectedLength) continue;
5384
+ if (align === "any") reportError(node, "consistentAny", expectedLength);
5385
+ else if (align === "exact") reportError(node, "consistentExact", expectedLength);
5386
+ else if (align === "minimum") reportError(node, "consistentMinimum", expectedLength);
5387
+ else if (align === "length") reportError(node, "consistentLength", expectedLength);
5388
+ }
5389
+ }
5390
+ };
5391
+ }
5392
+ if (mode === "consistent-line-length") {
5393
+ const align = options.align || "exact";
5394
+ const fixedLength = options.length || 0;
5395
+ const setextHeadings = [];
5396
+ return {
5397
+ heading(node) {
5398
+ if (getHeadingKind(sourceCode, node) !== "setext") return;
5399
+ setextHeadings.push(node);
5400
+ },
5401
+ "root:exit"() {
5402
+ if (setextHeadings.length === 0) return;
5403
+ let minimumRequiredLineLength = 1;
5404
+ for (const node of setextHeadings) {
5405
+ const parsed = getParsedSetextHeading(node);
5406
+ if (!parsed) continue;
5407
+ minimumRequiredLineLength = Math.max(minimumRequiredLineLength, parsed.underline.raws.prefix.length + parsed.underline.raws.spaceBefore.length + 1);
5408
+ }
5409
+ let expectedLineLength = minimumRequiredLineLength;
5410
+ if (align === "any") {
5411
+ if (setextHeadings.length < 2) return;
5412
+ for (const node of setextHeadings) {
5413
+ const parsed = getParsedSetextHeading(node);
5414
+ if (!parsed) continue;
5415
+ expectedLineLength = Math.max(parsed.underline.loc.end.column - 1, minimumRequiredLineLength);
5416
+ break;
5417
+ }
5418
+ } else if (align === "exact") for (const node of setextHeadings) {
5419
+ const parsed = getParsedSetextHeading(node);
5420
+ if (!parsed) continue;
5421
+ expectedLineLength = Math.max(expectedLineLength, getMaxHeaderLineWidth(parsed));
5422
+ }
5423
+ else if (align === "minimum") {
5424
+ let maxLineWidth = 0;
5425
+ for (const node of setextHeadings) {
5426
+ const parsed = getParsedSetextHeading(node);
5427
+ if (!parsed) continue;
5428
+ maxLineWidth = Math.max(maxLineWidth, getMaxHeaderLineWidth(parsed));
5429
+ expectedLineLength = Math.max(expectedLineLength, parsed.underline.loc.end.column - 1);
5430
+ }
5431
+ if (expectedLineLength < maxLineWidth) expectedLineLength = maxLineWidth;
5432
+ else for (const node of setextHeadings) {
5433
+ const parsed = getParsedSetextHeading(node);
5434
+ if (!parsed) continue;
5435
+ if (maxLineWidth <= parsed.underline.loc.end.column - 1) expectedLineLength = Math.min(expectedLineLength, parsed.underline.loc.end.column - 1);
5436
+ }
5437
+ } else if (align === "length") expectedLineLength = Math.max(fixedLength, minimumRequiredLineLength);
5438
+ else return;
5439
+ if (!expectedLineLength || expectedLineLength < 1) return;
5440
+ for (const node of setextHeadings) {
5441
+ const parsed = getParsedSetextHeading(node);
5442
+ if (!parsed) continue;
5443
+ const expectedLength = expectedLineLength - parsed.underline.raws.prefix.length - parsed.underline.raws.spaceBefore.length;
5444
+ if (parsed.underline.text.length === expectedLength) continue;
5445
+ if (align === "any") reportError(node, "consistentLineLengthAny", expectedLength);
5446
+ else if (align === "exact") reportError(node, "consistentLineLengthExact", expectedLength);
5447
+ else if (align === "minimum") reportError(node, "consistentLineLengthMinimum", expectedLength);
5448
+ else if (align === "length") reportError(node, "consistentLineLengthLength", expectedLength);
5449
+ }
5450
+ }
5451
+ };
5452
+ }
5453
+ return {};
5454
+ /**
5455
+ * Get the maximum width of header lines.
5456
+ */
5457
+ function getMaxHeaderTextWidth(parsed) {
5458
+ let maxWidth = 0;
5459
+ for (const contentLine of parsed.contentLines) {
5460
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
5461
+ const prefixWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore);
5462
+ maxWidth = Math.max(maxWidth, lineWidth - prefixWidth);
5463
+ }
5464
+ return maxWidth;
5465
+ }
5466
+ /**
5467
+ * Get the maximum width of header lines.
5468
+ */
5469
+ function getMaxHeaderLineWidth(parsed) {
5470
+ let maxLineWidth = 0;
5471
+ for (const contentLine of parsed.contentLines) {
5472
+ const lineWidth = getTextWidth(contentLine.raws.prefix + contentLine.raws.spaceBefore + contentLine.text);
5473
+ maxLineWidth = Math.max(maxLineWidth, lineWidth);
5474
+ }
5475
+ return maxLineWidth;
5476
+ }
5477
+ }
5478
+ });
5479
+
4838
5480
  //#endregion
4839
5481
  //#region src/rules/sort-definitions.ts
4840
5482
  var sort_definitions_default = createRule("sort-definitions", {
@@ -5252,6 +5894,217 @@ var table_header_casing_default = createRule("table-header-casing", {
5252
5894
  }
5253
5895
  });
5254
5896
 
5897
+ //#endregion
5898
+ //#region src/rules/thematic-break-character-style.ts
5899
+ var thematic_break_character_style_default = createRule("thematic-break-character-style", {
5900
+ meta: {
5901
+ type: "layout",
5902
+ docs: {
5903
+ description: "enforce consistent character style for thematic breaks (horizontal rules) in Markdown.",
5904
+ categories: [],
5905
+ listCategory: "Stylistic"
5906
+ },
5907
+ fixable: "code",
5908
+ hasSuggestions: false,
5909
+ schema: [{
5910
+ type: "object",
5911
+ properties: { style: {
5912
+ type: "string",
5913
+ enum: [
5914
+ "-",
5915
+ "*",
5916
+ "_"
5917
+ ]
5918
+ } },
5919
+ additionalProperties: false
5920
+ }],
5921
+ messages: { unexpected: "Thematic break should use '{{expected}}' but found '{{actual}}'." }
5922
+ },
5923
+ create(context) {
5924
+ const option = context.options[0];
5925
+ const style = option?.style || "-";
5926
+ return { thematicBreak(node) {
5927
+ const marker = getThematicBreakMarker(context.sourceCode, node);
5928
+ if (marker.kind !== style) context.report({
5929
+ node,
5930
+ messageId: "unexpected",
5931
+ data: {
5932
+ expected: style,
5933
+ actual: marker.kind
5934
+ },
5935
+ fix(fixer) {
5936
+ const range = context.sourceCode.getRange(node);
5937
+ const text = context.sourceCode.getText(node);
5938
+ const rep = text.replaceAll(marker.kind, style);
5939
+ return fixer.replaceTextRange(range, rep);
5940
+ }
5941
+ });
5942
+ } };
5943
+ }
5944
+ });
5945
+
5946
+ //#endregion
5947
+ //#region src/utils/thematic-break.ts
5948
+ /**
5949
+ * Check if the pattern is valid within the thematic break string.
5950
+ */
5951
+ function isValidThematicBreakPattern(pattern, text) {
5952
+ for (let i = 0; i < text.length; i += pattern.length) {
5953
+ const subSequence = text.slice(i, i + pattern.length);
5954
+ if (subSequence === pattern) continue;
5955
+ if (subSequence.length < pattern.length && pattern.startsWith(subSequence)) continue;
5956
+ return false;
5957
+ }
5958
+ return true;
5959
+ }
5960
+ /**
5961
+ * Create a thematic break string from a pattern and length.
5962
+ */
5963
+ function createThematicBreakFromPattern(pattern, length) {
5964
+ const mark = pattern[0];
5965
+ let candidate = pattern.repeat(Math.floor(length / pattern.length));
5966
+ if (candidate.length < length) candidate += pattern.slice(0, length - candidate.length);
5967
+ candidate = candidate.trim();
5968
+ if (candidate.length !== length) return null;
5969
+ let markCount = 0;
5970
+ for (const c of candidate) {
5971
+ if (c !== mark) continue;
5972
+ markCount++;
5973
+ if (markCount >= 3) return candidate;
5974
+ }
5975
+ return null;
5976
+ }
5977
+
5978
+ //#endregion
5979
+ //#region src/rules/thematic-break-length.ts
5980
+ var thematic_break_length_default = createRule("thematic-break-length", {
5981
+ meta: {
5982
+ type: "layout",
5983
+ docs: {
5984
+ description: "enforce consistent length for thematic breaks (horizontal rules) in Markdown.",
5985
+ categories: [],
5986
+ listCategory: "Stylistic"
5987
+ },
5988
+ fixable: "code",
5989
+ hasSuggestions: false,
5990
+ schema: [{
5991
+ type: "object",
5992
+ properties: { length: {
5993
+ type: "integer",
5994
+ minimum: 3
5995
+ } },
5996
+ additionalProperties: false
5997
+ }],
5998
+ messages: { unexpected: "Thematic break should be {{expected}} characters, but found {{actual}}." }
5999
+ },
6000
+ create(context) {
6001
+ const option = context.options[0] || {};
6002
+ const expectedLength = option.length ?? 3;
6003
+ const sourceCode = context.sourceCode;
6004
+ return { thematicBreak(node) {
6005
+ const marker = getThematicBreakMarker(sourceCode, node);
6006
+ if (marker.text.length === expectedLength) return;
6007
+ context.report({
6008
+ node,
6009
+ messageId: "unexpected",
6010
+ data: {
6011
+ expected: String(expectedLength),
6012
+ actual: String(marker.text.length)
6013
+ },
6014
+ fix(fixer) {
6015
+ const sequence = replacementSequence(marker);
6016
+ if (!sequence) return null;
6017
+ return fixer.replaceText(node, sequence);
6018
+ }
6019
+ });
6020
+ } };
6021
+ /**
6022
+ * Replace the sequence in the thematic break marker with the expected length.
6023
+ */
6024
+ function replacementSequence(marker) {
6025
+ if (marker.hasSpaces) {
6026
+ const pattern = inferSequencePattern(marker.text);
6027
+ if (pattern) return createThematicBreakFromPattern(pattern, expectedLength);
6028
+ return null;
6029
+ }
6030
+ return marker.kind.repeat(expectedLength);
6031
+ }
6032
+ /**
6033
+ * Infer sequence pattern from the original string.
6034
+ */
6035
+ function inferSequencePattern(original) {
6036
+ for (let length = 2; length < original.length; length++) {
6037
+ const pattern = original.slice(0, length);
6038
+ if (isValidThematicBreakPattern(pattern, original)) return pattern;
6039
+ }
6040
+ return null;
6041
+ }
6042
+ }
6043
+ });
6044
+
6045
+ //#endregion
6046
+ //#region src/rules/thematic-break-sequence-pattern.ts
6047
+ var thematic_break_sequence_pattern_default = createRule("thematic-break-sequence-pattern", {
6048
+ meta: {
6049
+ type: "layout",
6050
+ docs: {
6051
+ description: "enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown.",
6052
+ categories: [],
6053
+ listCategory: "Stylistic"
6054
+ },
6055
+ fixable: "code",
6056
+ hasSuggestions: false,
6057
+ schema: [{
6058
+ type: "object",
6059
+ properties: { pattern: { anyOf: [
6060
+ {
6061
+ type: "string",
6062
+ minLength: 1,
6063
+ pattern: "^\\-[ \\-]*$"
6064
+ },
6065
+ {
6066
+ type: "string",
6067
+ minLength: 1,
6068
+ pattern: "^\\*[ *]*$"
6069
+ },
6070
+ {
6071
+ type: "string",
6072
+ minLength: 1,
6073
+ pattern: "^_[ _]*$"
6074
+ }
6075
+ ] } },
6076
+ required: ["pattern"],
6077
+ additionalProperties: false
6078
+ }],
6079
+ messages: { inconsistentPattern: "Thematic break does not match the preferred repeating pattern '{{pattern}}'." }
6080
+ },
6081
+ create(context) {
6082
+ const option = context.options[0] || {};
6083
+ const pattern = option.pattern ?? "-";
6084
+ const sourceCode = context.sourceCode;
6085
+ const patterns = {
6086
+ "-": pattern.replaceAll(/[*_]/gu, "-"),
6087
+ "*": pattern.replaceAll(/[-_]/gu, "*"),
6088
+ _: pattern.replaceAll(/[*-]/gu, "_")
6089
+ };
6090
+ return { thematicBreak(node) {
6091
+ const marker = getThematicBreakMarker(sourceCode, node);
6092
+ const patternForKind = patterns[marker.kind];
6093
+ if (isValidThematicBreakPattern(patternForKind, marker.text)) return;
6094
+ context.report({
6095
+ node,
6096
+ messageId: "inconsistentPattern",
6097
+ data: { pattern },
6098
+ fix(fixer) {
6099
+ const replacement = createThematicBreakFromPattern(patternForKind, marker.text.length);
6100
+ if (!replacement) return null;
6101
+ return fixer.replaceText(node, replacement);
6102
+ }
6103
+ });
6104
+ } };
6105
+ }
6106
+ });
6107
+
5255
6108
  //#endregion
5256
6109
  //#region src/utils/rules.ts
5257
6110
  const rules$1 = [
@@ -5270,13 +6123,18 @@ const rules$1 = [
5270
6123
  no_trailing_spaces_default,
5271
6124
  ordered_list_marker_sequence_default,
5272
6125
  ordered_list_marker_start_default,
6126
+ padding_line_between_blocks_default,
5273
6127
  prefer_autolinks_default,
5274
6128
  prefer_fenced_code_blocks_default,
5275
6129
  prefer_inline_code_words_default,
5276
6130
  prefer_link_reference_definitions_default,
5277
6131
  prefer_linked_words_default,
6132
+ setext_heading_underline_length_default,
5278
6133
  sort_definitions_default,
5279
- table_header_casing_default
6134
+ table_header_casing_default,
6135
+ thematic_break_character_style_default,
6136
+ thematic_break_length_default,
6137
+ thematic_break_sequence_pattern_default
5280
6138
  ];
5281
6139
 
5282
6140
  //#endregion
@@ -5306,6 +6164,7 @@ const rules$2 = {
5306
6164
  "markdown-preferences/list-marker-alignment": "error",
5307
6165
  "markdown-preferences/no-laziness-blockquotes": "error",
5308
6166
  "markdown-preferences/no-text-backslash-linebreak": "error",
6167
+ "markdown-preferences/padding-line-between-blocks": "error",
5309
6168
  "markdown-preferences/prefer-autolinks": "error",
5310
6169
  "markdown-preferences/prefer-fenced-code-blocks": "error"
5311
6170
  };
@@ -5318,7 +6177,7 @@ __export(meta_exports, {
5318
6177
  version: () => version
5319
6178
  });
5320
6179
  const name = "eslint-plugin-markdown-preferences";
5321
- const version = "0.15.0";
6180
+ const version = "0.17.0";
5322
6181
 
5323
6182
  //#endregion
5324
6183
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -33,8 +33,8 @@
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",
37
- "changeset:version": "changeset version && npm run generate:version && git add --all",
36
+ "generate:version": "env-cmd -e version -- npm run update && npm run lint -- --fix",
37
+ "changeset:version": "env-cmd -e version -- changeset version && npm run generate:version && git add --all",
38
38
  "changeset:publish": "npm run build && changeset publish"
39
39
  },
40
40
  "repository": {
@@ -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",