eslint-plugin-md-style 0.1.0-beta.2 β†’ 0.1.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.
Files changed (3) hide show
  1. package/README.md +17 -5
  2. package/dist/index.mjs +243 -149
  3. package/package.json +8 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/eslint-plugin-md-style)](https://www.npmjs.com/package/eslint-plugin-md-style)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/eslint-plugin-md-style)](https://www.npmjs.com/package/eslint-plugin-md-style)
5
- [![codecov](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style/graph/badge.svg)](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style)
5
+ [![codecov](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style/graph/badge.svg?branch=main)](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style?branch=main)
6
6
 
7
7
  ESLint plugin for enforcing style rules in Markdown-based documentation.
8
8
 
@@ -18,6 +18,13 @@ It currently ships:
18
18
 
19
19
  ## Quick Start
20
20
 
21
+ ### Version Requirements
22
+
23
+ - `eslint`: `^9.30.0` or `^10.0.0`
24
+ - `@antfu/eslint-config`: `^7.5.0` when used
25
+
26
+ This plugin is designed for ESLint flat config. If you use `@antfu/eslint-config`, make sure its version satisfies the requirement above.
27
+
21
28
  Install the required packages:
22
29
 
23
30
  ```bash
@@ -61,9 +68,9 @@ export default [
61
68
  plugins: {
62
69
  'md-style': mdStyle,
63
70
  },
64
- language: 'md-style/commonmark',
71
+ language: 'md-style/gfm',
65
72
  rules: {
66
- 'md-style/space-between-link': 'error',
73
+ 'md-style/space-between-inline-element': 'error',
67
74
  'md-style/valid-heading-anchor': 'error',
68
75
  },
69
76
  },
@@ -113,14 +120,19 @@ export default antfu(
113
120
 
114
121
  | Rule | Included in `recommended` | Autofix |
115
122
  | --- | --- | --- |
116
- | `md-style/space-between-link` | βœ… | πŸ”§ |
123
+ | `md-style/space-between-inline-element` | βœ… | πŸ”§ |
117
124
  | `md-style/valid-heading-anchor` | βœ… | πŸ”§ |
118
125
 
119
126
  ## Why `@eslint/markdown` Is Required
120
127
 
121
128
  This plugin builds on top of `@eslint/markdown` rather than replacing it.
122
129
 
123
- `@eslint/markdown` provides the Markdown processor and language support. This plugin re-exports those capabilities through its own plugin entry and adds documentation style rules on top, including the `md-style/commonmark` language used by the bundled configs.
130
+ `@eslint/markdown` provides the Markdown processor and language support. This plugin re-exports those capabilities through its own plugin entry and adds documentation style rules on top, including the `md-style/gfm` language used by the bundled configs.
131
+
132
+ ## References
133
+
134
+ - [W3C Manual of Style](https://www.w3.org/guide/manual-of-style/)
135
+ - [δΈ­ζ–‡ζŽ’η‰ˆθ¦ζ±‚](https://w3c.github.io/clreq/)
124
136
 
125
137
  ## License
126
138
 
package/dist/index.mjs CHANGED
@@ -10,12 +10,57 @@ function createRule({ create, defaultOptions, meta }) {
10
10
  }
11
11
  };
12
12
  }
13
+
14
+ //#endregion
15
+ //#region src/utils/ast.ts
16
+ /**
17
+ * Checks whether an unknown value behaves like an mdast parent node.
18
+ *
19
+ * This intentionally accepts unknown values because ESLint's ancestor API does
20
+ * not expose mdast-specific types.
21
+ */
22
+ function hasChildren(node) {
23
+ return !!node && typeof node === "object" && "children" in node && Array.isArray(node.children);
24
+ }
25
+ function isTableCell(node) {
26
+ return node.type === "tableCell";
27
+ }
28
+ const INLINE_ELEMENT_TYPES = new Set([
29
+ "link",
30
+ "image",
31
+ "inlineCode",
32
+ "emphasis",
33
+ "strong"
34
+ ]);
35
+ /**
36
+ * Checks whether a phrasing node is one of the selected inline element targets.
37
+ */
38
+ function isInlineElement(node) {
39
+ return !!node && INLINE_ELEMENT_TYPES.has(node.type);
40
+ }
41
+ /**
42
+ * Checks whether the current inline element is nested inside another selected inline element.
43
+ */
44
+ function isNestedInlineElement(nodeContext) {
45
+ const { parent } = nodeContext;
46
+ return isInlineElement(parent);
47
+ }
48
+ /**
49
+ * Extracts the plain-text value of a phrasing node.
50
+ * If the node does not expose `value`, recursively concatenates the text from its children.
51
+ */
52
+ function getNodeValue(node) {
53
+ if (!node) return;
54
+ if ("value" in node) return node.value;
55
+ if (hasChildren(node)) return node.children.map(getNodeValue).join("") || void 0;
56
+ }
13
57
  /**
14
58
  * Gets the start and end offsets for a node.
15
59
  */
16
60
  function getNodePosition(node) {
17
61
  const start = node.position?.start.offset;
18
62
  const end = node.position?.end.offset;
63
+ /* v8 ignore if -- @preserve */
19
64
  if (start == null || end == null) return {
20
65
  position: false,
21
66
  start: 0,
@@ -27,18 +72,6 @@ function getNodePosition(node) {
27
72
  end
28
73
  };
29
74
  }
30
-
31
- //#endregion
32
- //#region src/utils/ast.ts
33
- /**
34
- * Checks whether an unknown value behaves like an mdast parent node.
35
- *
36
- * This intentionally accepts unknown values because ESLint's ancestor API does
37
- * not expose mdast-specific types.
38
- */
39
- function hasChildren(node) {
40
- return !!node && typeof node === "object" && "children" in node && Array.isArray(node.children);
41
- }
42
75
  function getNodeContext(context, node) {
43
76
  const parent = context.sourceCode.getAncestors(node).at(-1);
44
77
  if (!hasChildren(parent)) return {
@@ -47,6 +80,7 @@ function getNodeContext(context, node) {
47
80
  current: node
48
81
  };
49
82
  const currentIndex = parent.children.findIndex((child) => child === node);
83
+ /* v8 ignore if -- @preserve */
50
84
  if (currentIndex === -1) return {
51
85
  parent,
52
86
  prev: void 0,
@@ -60,9 +94,17 @@ function getNodeContext(context, node) {
60
94
  current: node
61
95
  };
62
96
  }
97
+ /**
98
+ * Gets the character adjacent to the start or end of a string.
99
+ */
100
+ function getAdjacentChar(str, position) {
101
+ if (!str) return void 0;
102
+ str = str.trim();
103
+ return position === "head" ? str[0] : str[str.length - 1];
104
+ }
63
105
 
64
106
  //#endregion
65
- //#region src/utils/rules/anchor.ts
107
+ //#region src/utils/anchor.ts
66
108
  /**
67
109
  * Match the trailing anchor-like fragment from a heading string.
68
110
  * @example `δΈ­ζ–‡ζ ‡ι’˜ {#Chinese-Title}` -> `{#Chinese-Title}`
@@ -128,16 +170,7 @@ function calcAnchorPositionCompensate(content) {
128
170
  }
129
171
 
130
172
  //#endregion
131
- //#region src/utils/rules/link.ts
132
- const LINK_SPACE_MESSAGE_IDS = {
133
- missingSpaceBeforeLink: "missingSpaceBeforeLink",
134
- missingSpaceAfterLink: "missingSpaceAfterLink",
135
- multipleSpacesBeforeLink: "multipleSpacesBeforeLink",
136
- multipleSpacesAfterLink: "multipleSpacesAfterLink",
137
- multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
138
- unexpectedSpaceBeforeLink: "unexpectedSpaceBeforeLink",
139
- unexpectedSpaceAfterLink: "unexpectedSpaceAfterLink"
140
- };
173
+ //#region src/utils/punctuation.ts
141
174
  const OPENING_PAIRED_PUNCTUATION = new Set([
142
175
  "(",
143
176
  "[",
@@ -149,6 +182,23 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
149
182
  "β€œ",
150
183
  "β€˜"
151
184
  ]);
185
+ const CLOSING_PAIRED_PUNCTUATION = new Set([
186
+ ")",
187
+ "]",
188
+ "}",
189
+ ">",
190
+ "οΌ‰",
191
+ "】",
192
+ "》",
193
+ "”",
194
+ "’"
195
+ ]);
196
+ /**
197
+ * Checks whether the character is a slash used as a path-like separator.
198
+ */
199
+ function isSlashPunctuation(str) {
200
+ return str === "/";
201
+ }
152
202
  /**
153
203
  * Checks whether the character is fullwidth punctuation.
154
204
  * @example `。` -> true
@@ -168,20 +218,6 @@ function isDashPunctuation(str) {
168
218
  if (!str || str.length !== 1) return false;
169
219
  return DASH_PUNCTUATION_RE.test(str);
170
220
  }
171
- /**
172
- * Checks whether adjacent text is a custom container marker on the next line.
173
- *
174
- * @deprecated Temporary workaround to prevent space-between-link from reporting
175
- * false positives on custom containers. Remove this and handle the case in a
176
- * dedicated custom container rule when one exists.
177
- * @see https://vitepress.dev/guide/markdown#custom-containers
178
- * @example `\n:::` -> true
179
- * @example `\n::::` -> true
180
- * @example `:::` -> false
181
- */
182
- function isCustomContainerMarker(str) {
183
- return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
184
- }
185
221
  const PUNCTUATION_RE = /^\p{P}$/u;
186
222
  /**
187
223
  * Checks whether the character is punctuation.
@@ -194,6 +230,20 @@ function isPunctuation(str) {
194
230
  return PUNCTUATION_RE.test(str);
195
231
  }
196
232
  /**
233
+ * Checks whether the start or end of a string is adjacent to punctuation.
234
+ * @example `。 hello`, `head` -> true
235
+ * @example `hello .`, `tail` -> true
236
+ */
237
+ function hasPunctuation(str, position = "head") {
238
+ if (!str) return false;
239
+ str = str.trim();
240
+ if (position === "head") return isPunctuation(str[0]);
241
+ else return isPunctuation(str[str.length - 1]);
242
+ }
243
+
244
+ //#endregion
245
+ //#region src/utils/space.ts
246
+ /**
197
247
  * Gets the count and range of consecutive whitespace at the start or end of a string.
198
248
  * @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
199
249
  * @example `text `, `tail` -> { count: 2, start: 4, end: 6 }
@@ -224,34 +274,6 @@ function getWhiteSpace(str, position = "head") {
224
274
  }
225
275
  }
226
276
  /**
227
- * Checks whether the start or end of a string is adjacent to punctuation.
228
- * @example `。 hello`, `head` -> true
229
- * @example `hello .`, `tail` -> true
230
- */
231
- function hasPunctuation(str, position = "head") {
232
- if (!str) return false;
233
- str = str.trim();
234
- if (position === "head") return isPunctuation(str[0]);
235
- else return isPunctuation(str[str.length - 1]);
236
- }
237
- /**
238
- * Gets the character adjacent to the start or end of a string.
239
- */
240
- function getAdjacentChar(str, position) {
241
- if (!str) return void 0;
242
- str = str.trim();
243
- return position === "head" ? str[0] : str[str.length - 1];
244
- }
245
- /**
246
- * Extracts the plain-text value of a phrasing node.
247
- * If the node does not expose `value`, recursively concatenates the text from its children.
248
- */
249
- function getNodeValue(node) {
250
- if (!node) return;
251
- if ("value" in node) return node.value;
252
- if (hasChildren(node)) return node.children.map(getNodeValue).join("") || void 0;
253
- }
254
- /**
255
277
  * Gets whitespace and punctuation information for text adjacent to a link or inline code node.
256
278
  */
257
279
  function getSpaceContext(nodeContext) {
@@ -273,6 +295,32 @@ function getSpaceContext(nodeContext) {
273
295
  }
274
296
  };
275
297
  }
298
+
299
+ //#endregion
300
+ //#region src/utils/inline-element.ts
301
+ const INLINE_SPACE_MESSAGE_IDS = {
302
+ missingSpaceBefore: "missingSpaceBefore",
303
+ missingSpaceAfter: "missingSpaceAfter",
304
+ multipleSpacesBefore: "multipleSpacesBefore",
305
+ multipleSpacesAfter: "multipleSpacesAfter",
306
+ multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
307
+ unexpectedSpaceBefore: "unexpectedSpaceBefore",
308
+ unexpectedSpaceAfter: "unexpectedSpaceAfter"
309
+ };
310
+ /**
311
+ * Checks whether adjacent text is a custom container marker on the next line.
312
+ *
313
+ * @deprecated Temporary workaround to prevent space-between-link from reporting
314
+ * false positives on custom containers. Remove this and handle the case in a
315
+ * dedicated custom container rule when one exists.
316
+ * @see https://vitepress.dev/guide/markdown#custom-containers
317
+ * @example `\n:::` -> true
318
+ * @example `\n::::` -> true
319
+ * @example `:::` -> false
320
+ */
321
+ function isCustomContainerMarker(str) {
322
+ return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
323
+ }
276
324
  /**
277
325
  * Validates whether a spacing run contains exactly one required space.
278
326
  */
@@ -281,121 +329,168 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
281
329
  if (count > 1) return multipleSpacesMessageId;
282
330
  }
283
331
  /**
284
- * Validates the spacing before a link when the previous character is punctuation.
332
+ * Validates spacing before an inline node when the previous character is punctuation.
285
333
  */
286
- function validateSpaceBeforeLinkAfterPunctuation(context) {
287
- if (OPENING_PAIRED_PUNCTUATION.has(getAdjacentChar(context.value, "tail") || "")) {
288
- if (context.whiteSpace.count > 0) return LINK_SPACE_MESSAGE_IDS.unexpectedSpaceBeforeLink;
334
+ function validateBeforePunctuation(context) {
335
+ const adjacentChar = getAdjacentChar(context.value, "tail");
336
+ if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
337
+ if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
289
338
  return;
290
339
  }
291
- if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, LINK_SPACE_MESSAGE_IDS.missingSpaceBeforeLink, LINK_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation);
292
- if (context.whiteSpace.count > 0) return LINK_SPACE_MESSAGE_IDS.unexpectedSpaceBeforeLink;
340
+ if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation);
341
+ if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
293
342
  }
294
343
  /**
295
- * Validates the spacing between the previous node and the current link.
344
+ * Validates the spacing between the previous node and the current inline node.
296
345
  */
297
- function validateSpaceBeforeLink(context) {
298
- if (context.hasPunctuation) return validateSpaceBeforeLinkAfterPunctuation(context);
299
- return validateSingleRequiredSpace(context.whiteSpace.count, LINK_SPACE_MESSAGE_IDS.missingSpaceBeforeLink, LINK_SPACE_MESSAGE_IDS.multipleSpacesBeforeLink);
346
+ function validateSpaceBeforeNode(context) {
347
+ if (context.hasPunctuation) return validateBeforePunctuation(context);
348
+ return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore, INLINE_SPACE_MESSAGE_IDS.multipleSpacesBefore);
300
349
  }
301
350
  /**
302
- * Validates the spacing after a link when the next character is punctuation.
351
+ * Validates spacing after an inline node when the next character is punctuation.
303
352
  */
304
- function validateSpaceAfterLinkBeforePunctuation(context) {
305
- if (isDashPunctuation(getAdjacentChar(context.value, "head"))) return validateSingleRequiredSpace(context.whiteSpace.count, LINK_SPACE_MESSAGE_IDS.missingSpaceAfterLink, LINK_SPACE_MESSAGE_IDS.multipleSpacesAfterLink);
353
+ function validateSpaceAfterPunctuation(context) {
354
+ const adjacentChar = getAdjacentChar(context.value, "head");
306
355
  if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
307
- if (context.whiteSpace.count > 0) return LINK_SPACE_MESSAGE_IDS.unexpectedSpaceAfterLink;
356
+ if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
357
+ if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceAfter, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfter);
358
+ if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
359
+ }
360
+ /**
361
+ * Validates the spacing between the current inline node and the next node.
362
+ */
363
+ function validateSpaceAfterNode(context) {
364
+ if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
365
+ return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceAfter, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfter);
308
366
  }
309
367
  /**
310
- * Validates the spacing between the current link and the next node.
368
+ * Validates spacing around an inline element inside a table cell.
369
+ * Table cells skip checks when the next sibling is another inline element.
311
370
  */
312
- function validateSpaceAfterLink(context) {
313
- if (context.hasPunctuation) return validateSpaceAfterLinkBeforePunctuation(context);
314
- return validateSingleRequiredSpace(context.whiteSpace.count, LINK_SPACE_MESSAGE_IDS.missingSpaceAfterLink, LINK_SPACE_MESSAGE_IDS.multipleSpacesAfterLink);
371
+ function validateTableCellSpace(nodeContext) {
372
+ const { prev, next } = getSpaceContext(nodeContext);
373
+ if (prev && prev.value) {
374
+ const beforeIssue = validateSpaceBeforeNode(prev);
375
+ if (beforeIssue) return beforeIssue;
376
+ }
377
+ if (!next || isInlineElement(nodeContext.next) || !next.value) return;
378
+ return validateSpaceAfterNode(next);
379
+ }
380
+ /**
381
+ * Validates spacing around an inline element in the default text flow.
382
+ */
383
+ function validateDefaultSpace(nodeContext) {
384
+ const { prev, next } = getSpaceContext(nodeContext);
385
+ if (prev && nodeContext.prev) {
386
+ const beforeIssue = validateSpaceBeforeNode(prev);
387
+ if (beforeIssue) return beforeIssue;
388
+ }
389
+ if (!next || isInlineElement(nodeContext.next) || !nodeContext.next) return;
390
+ return validateSpaceAfterNode(next);
315
391
  }
316
392
  /**
317
- * Validates whether the spacing around a link node follows the typography rules.
318
- * - Regular text and links should be separated by a single space.
319
- * - Fullwidth punctuation usually touches the link without spaces.
320
- * - Halfwidth punctuation, hyphens, and similar cases are handled by dedicated rules.
393
+ * Validates spacing around an inline element by delegating to the appropriate strategy
394
+ * for table cells or the default text flow.
395
+ * - Regular text and selected inline elements should be separated by one space.
396
+ * - Fullwidth punctuation and paired punctuation usually touch inline elements without spaces.
397
+ * - Adjacent selected inline elements are handled by the following element to avoid duplicate fixes.
321
398
  */
322
399
  function validateSpace(nodeContext) {
323
- const { prev, next } = nodeContext;
324
- const spaceContext = getSpaceContext(nodeContext);
325
- if (!prev || !spaceContext.prev) return;
326
- const beforeLinkIssue = validateSpaceBeforeLink(spaceContext.prev);
327
- if (beforeLinkIssue) return beforeLinkIssue;
328
- if (!next || !spaceContext.next) return;
329
- return validateSpaceAfterLink(spaceContext.next);
400
+ const { parent } = nodeContext;
401
+ if (parent && isTableCell(parent)) return validateTableCellSpace(nodeContext);
402
+ return validateDefaultSpace(nodeContext);
330
403
  }
331
404
 
332
405
  //#endregion
333
- //#region src/rules/space-between-link/index.ts
334
- const RULE_NAME$1 = "space-between-link";
335
- const BEFORE_LINK_MESSAGE_IDS = new Set([
336
- LINK_SPACE_MESSAGE_IDS.missingSpaceBeforeLink,
337
- LINK_SPACE_MESSAGE_IDS.multipleSpacesBeforeLink,
338
- LINK_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation,
339
- LINK_SPACE_MESSAGE_IDS.unexpectedSpaceBeforeLink
406
+ //#region src/rules/space-between-inline-element/index.ts
407
+ const RULE_NAME$1 = "space-between-inline-element";
408
+ const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set([
409
+ INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore,
410
+ INLINE_SPACE_MESSAGE_IDS.multipleSpacesBefore,
411
+ INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation,
412
+ INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore
340
413
  ]);
341
- var space_between_link_default = createRule({
414
+ /**
415
+ * Checks one selected inline element and reports the fix range around it.
416
+ */
417
+ function checkInlineElement(context, node) {
418
+ const { position, start, end } = getNodePosition(node);
419
+ /* v8 ignore if -- @preserve */
420
+ if (!position) return;
421
+ const nodeContext = getNodeContext(context, node);
422
+ if (isNestedInlineElement(nodeContext)) return;
423
+ const spaceContext = getSpaceContext(nodeContext);
424
+ const messageId = validateSpace(nodeContext);
425
+ if (!messageId) return;
426
+ if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
427
+ const { count } = spaceContext.prev.whiteSpace;
428
+ const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore ? "" : " ";
429
+ context.report({
430
+ node,
431
+ messageId,
432
+ fix(fixer) {
433
+ return fixer.replaceTextRange([start - count, start], replaceText);
434
+ }
435
+ });
436
+ return;
437
+ }
438
+ if (spaceContext.next) {
439
+ const { count } = spaceContext.next.whiteSpace;
440
+ const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter ? "" : " ";
441
+ context.report({
442
+ node,
443
+ messageId,
444
+ fix(fixer) {
445
+ return fixer.replaceTextRange([end, end + count], replaceText);
446
+ }
447
+ });
448
+ }
449
+ }
450
+ var space_between_inline_element_default = createRule({
342
451
  name: RULE_NAME$1,
343
452
  meta: {
344
453
  type: "layout",
345
- docs: { description: "Enforce spacing around Markdown links: one space next to text, no spaces next to punctuation." },
454
+ docs: { description: "Enforce spacing around Markdown inline elements." },
346
455
  messages: {
347
- missingSpaceBeforeLink: "A space is required before the link.",
348
- missingSpaceAfterLink: "A space is required after the link.",
349
- multipleSpacesBeforeLink: "Use exactly one space before the link.",
350
- multipleSpacesAfterLink: "Use exactly one space after the link.",
456
+ missingSpaceBefore: "A space is required before the inline element.",
457
+ missingSpaceAfter: "A space is required after the inline element.",
458
+ multipleSpacesBefore: "Use exactly one space before the inline element.",
459
+ multipleSpacesAfter: "Use exactly one space after the inline element.",
351
460
  multipleSpacesAfterPunctuation: "Use one space after punctuation.",
352
- unexpectedSpaceBeforeLink: "Do not add a space between punctuation and the link.",
353
- unexpectedSpaceAfterLink: "Do not add a space between the link and punctuation."
461
+ unexpectedSpaceBefore: "Do not add a space between punctuation and the inline element.",
462
+ unexpectedSpaceAfter: "Do not add a space between the inline element and punctuation."
354
463
  },
355
464
  fixable: "whitespace",
356
465
  schema: []
357
466
  },
358
467
  defaultOptions: [],
359
468
  create(context) {
360
- return { link(node) {
361
- const { position, start, end } = getNodePosition(node);
362
- if (!position) return;
363
- const nodeContext = getNodeContext(context, node);
364
- const spaceContext = getSpaceContext(nodeContext);
365
- const messageId = validateSpace(nodeContext);
366
- if (!messageId) return;
367
- if (BEFORE_LINK_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
368
- const { count } = spaceContext.prev.whiteSpace;
369
- const replaceText = messageId === LINK_SPACE_MESSAGE_IDS.unexpectedSpaceBeforeLink ? "" : " ";
370
- context.report({
371
- node,
372
- messageId,
373
- fix(fixer) {
374
- return fixer.replaceTextRange([start - count, start], replaceText);
375
- }
376
- });
377
- return;
378
- }
379
- if (spaceContext.next) {
380
- const { count } = spaceContext.next.whiteSpace;
381
- const replaceText = messageId === LINK_SPACE_MESSAGE_IDS.unexpectedSpaceAfterLink ? "" : " ";
382
- context.report({
383
- node,
384
- messageId,
385
- fix(fixer) {
386
- return fixer.replaceTextRange([end, end + count], replaceText);
387
- }
388
- });
469
+ return {
470
+ link(node) {
471
+ checkInlineElement(context, node);
472
+ },
473
+ image(node) {
474
+ checkInlineElement(context, node);
475
+ },
476
+ inlineCode(node) {
477
+ checkInlineElement(context, node);
478
+ },
479
+ emphasis(node) {
480
+ checkInlineElement(context, node);
481
+ },
482
+ strong(node) {
483
+ checkInlineElement(context, node);
389
484
  }
390
- } };
485
+ };
391
486
  }
392
487
  });
393
488
 
394
489
  //#endregion
395
490
  //#region src/utils/markdown.ts
396
- const language = new MarkdownLanguage({ mode: "commonmark" });
491
+ const language = new MarkdownLanguage({ mode: "gfm" });
397
492
  /**
398
- * Parses Markdown with the same CommonMark language implementation used by the
493
+ * Parses Markdown with the same GFM language implementation used by the
399
494
  * plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
400
495
  */
401
496
  function parseMarkdown(markdown$1) {
@@ -409,6 +504,7 @@ function parseMarkdown(markdown$1) {
409
504
  ...language.defaultLanguageOptions,
410
505
  frontmatter: "yaml"
411
506
  } });
507
+ /* v8 ignore if -- @preserve */
412
508
  if (!parseResult.ok) throw new Error(parseResult.errors[0]?.message ?? "Failed to parse markdown.");
413
509
  return {
414
510
  ast: parseResult.ast,
@@ -417,7 +513,7 @@ function parseMarkdown(markdown$1) {
417
513
  }
418
514
 
419
515
  //#endregion
420
- //#region src/utils/rules/heading.ts
516
+ //#region src/utils/heading.ts
421
517
  /**
422
518
  * Returns true when the Markdown document starts with YAML frontmatter.
423
519
  */
@@ -450,6 +546,7 @@ var valid_heading_anchor_default = createRule({
450
546
  create(context) {
451
547
  return { heading(node) {
452
548
  const { position, start, end } = getNodePosition(node);
549
+ /* v8 ignore if -- @preserve */
453
550
  if (!position) return;
454
551
  const source = context.sourceCode.text.slice(start, end);
455
552
  if (isStrictAnchor(source) || !hasChinese(source)) return;
@@ -481,7 +578,7 @@ var valid_heading_anchor_default = createRule({
481
578
  //#endregion
482
579
  //#region src/rules/index.ts
483
580
  const rules = {
484
- "space-between-link": space_between_link_default,
581
+ "space-between-inline-element": space_between_inline_element_default,
485
582
  "valid-heading-anchor": valid_heading_anchor_default
486
583
  };
487
584
 
@@ -490,10 +587,7 @@ const rules = {
490
587
  const plugin = {
491
588
  rules,
492
589
  processors: markdown.processors,
493
- languages: {
494
- commonmark: new MarkdownLanguage({ mode: "commonmark" }),
495
- gfm: new MarkdownLanguage({ mode: "gfm" })
496
- }
590
+ languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
497
591
  };
498
592
  const allRuleEntries = Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]);
499
593
  const recommendedRules = Object.fromEntries(allRuleEntries);
@@ -503,14 +597,14 @@ const configs = {
503
597
  name: "md-style/recommended",
504
598
  files: ["**/*.md"],
505
599
  plugins: { "md-style": plugin },
506
- language: "md-style/commonmark",
600
+ language: "md-style/gfm",
507
601
  rules: recommendedRules
508
602
  },
509
603
  all: {
510
604
  name: "md-style/all",
511
605
  files: ["**/*.md"],
512
606
  plugins: { "md-style": plugin },
513
- language: "md-style/commonmark",
607
+ language: "md-style/gfm",
514
608
  rules: allRules
515
609
  }
516
610
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-md-style",
3
3
  "type": "module",
4
- "version": "0.1.0-beta.2",
4
+ "version": "0.1.0",
5
5
  "packageManager": "pnpm@10.21.0",
6
6
  "description": "ESLint plugin for enforcing style rules in Markdown-based documentation",
7
7
  "author": "noisefan <noisefan@163.com>",
@@ -43,8 +43,14 @@
43
43
  "prepare": "simple-git-hooks"
44
44
  },
45
45
  "peerDependencies": {
46
+ "@antfu/eslint-config": "^7.5.0",
46
47
  "@eslint/markdown": "^7.5.1",
47
- "eslint": "^9.0.0 || ^10.0.0"
48
+ "eslint": "^9.30.0 || ^10.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@antfu/eslint-config": {
52
+ "optional": true
53
+ }
48
54
  },
49
55
  "dependencies": {},
50
56
  "devDependencies": {