eslint-plugin-markdown-preferences 0.15.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
@@ -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,6 +125,7 @@ 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 | 🔧 | |
@@ -127,7 +134,13 @@ The rules with the following star ⭐ are included in the configs.
127
134
  <!-- prettier-ignore-end -->
128
135
 
129
136
  <!--RULES_TABLE_END-->
137
+
130
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
+
131
144
  <!--DOCS_IGNORE_START-->
132
145
 
133
146
  ## 🍻 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
@@ -162,6 +167,17 @@ type MarkdownPreferencesNoTrailingSpaces = [] | [{
162
167
  type MarkdownPreferencesOrderedListMarkerStart = [] | [{
163
168
  start?: (1 | 0);
164
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
+ }[];
165
181
  type MarkdownPreferencesPreferInlineCodeWords = [] | [{
166
182
  words: string[];
167
183
  ignores?: {
@@ -220,7 +236,7 @@ declare namespace meta_d_exports {
220
236
  export { name, version };
221
237
  }
222
238
  declare const name: "eslint-plugin-markdown-preferences";
223
- declare const version: "0.15.0";
239
+ declare const version: "0.16.0";
224
240
  //#endregion
225
241
  //#region src/index.d.ts
226
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 };
@@ -497,84 +593,6 @@ var blockquote_marker_alignment_default = createRule("blockquote-marker-alignmen
497
593
  }
498
594
  });
499
595
 
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
596
  //#endregion
579
597
  //#region src/rules/canonical-code-block-language.ts
580
598
  const DEFAULT_LANGUAGES = {
@@ -4284,6 +4302,272 @@ var ordered_list_marker_start_default = createRule("ordered-list-marker-start",
4284
4302
  }
4285
4303
  });
4286
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
+
4287
4571
  //#endregion
4288
4572
  //#region src/rules/prefer-autolinks.ts
4289
4573
  var prefer_autolinks_default = createRule("prefer-autolinks", {
@@ -5270,6 +5554,7 @@ const rules$1 = [
5270
5554
  no_trailing_spaces_default,
5271
5555
  ordered_list_marker_sequence_default,
5272
5556
  ordered_list_marker_start_default,
5557
+ padding_line_between_blocks_default,
5273
5558
  prefer_autolinks_default,
5274
5559
  prefer_fenced_code_blocks_default,
5275
5560
  prefer_inline_code_words_default,
@@ -5306,6 +5591,7 @@ const rules$2 = {
5306
5591
  "markdown-preferences/list-marker-alignment": "error",
5307
5592
  "markdown-preferences/no-laziness-blockquotes": "error",
5308
5593
  "markdown-preferences/no-text-backslash-linebreak": "error",
5594
+ "markdown-preferences/padding-line-between-blocks": "error",
5309
5595
  "markdown-preferences/prefer-autolinks": "error",
5310
5596
  "markdown-preferences/prefer-fenced-code-blocks": "error"
5311
5597
  };
@@ -5318,7 +5604,7 @@ __export(meta_exports, {
5318
5604
  version: () => version
5319
5605
  });
5320
5606
  const name = "eslint-plugin-markdown-preferences";
5321
- const version = "0.15.0";
5607
+ const version = "0.16.0";
5322
5608
 
5323
5609
  //#endregion
5324
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.15.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",