eslint-plugin-md-style 0.1.0-beta.2 β†’ 0.2.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 +19 -5
  2. package/dist/index.mjs +618 -153
  3. package/package.json +26 -19
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-around-inline-element': 'error',
67
74
  'md-style/valid-heading-anchor': 'error',
68
75
  },
69
76
  },
@@ -113,14 +120,21 @@ export default antfu(
113
120
 
114
121
  | Rule | Included in `recommended` | Autofix |
115
122
  | --- | --- | --- |
116
- | `md-style/space-between-link` | βœ… | πŸ”§ |
123
+ | `md-style/space-around-inline-element` | βœ… | πŸ”§ |
124
+ | `md-style/space-around-number` | | πŸ”§ |
125
+ | `md-style/space-around-word` | | πŸ”§ |
117
126
  | `md-style/valid-heading-anchor` | βœ… | πŸ”§ |
118
127
 
119
128
  ## Why `@eslint/markdown` Is Required
120
129
 
121
130
  This plugin builds on top of `@eslint/markdown` rather than replacing it.
122
131
 
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.
132
+ `@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.
133
+
134
+ ## References
135
+
136
+ - [W3C Manual of Style](https://www.w3.org/guide/manual-of-style/)
137
+ - [δΈ­ζ–‡ζŽ’η‰ˆθ¦ζ±‚](https://w3c.github.io/clreq/)
124
138
 
125
139
  ## License
126
140
 
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("");
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,29 @@ function getNodeContext(context, node) {
60
94
  current: node
61
95
  };
62
96
  }
97
+ /**
98
+ * Returns the current node and adjacent siblings from a known children array.
99
+ * Useful when iterating a tokenized child list directly without an ESLint ancestor context.
100
+ */
101
+ function getNodeContextByParent(childrenNodes, currentIndex) {
102
+ return {
103
+ prev: childrenNodes[currentIndex - 1],
104
+ current: childrenNodes[currentIndex],
105
+ next: childrenNodes[currentIndex + 1]
106
+ };
107
+ }
108
+ /**
109
+ * Gets the first or last visible character of a string after trimming
110
+ * surrounding whitespace.
111
+ */
112
+ function getAdjacentChar(str, position) {
113
+ if (!str) return void 0;
114
+ str = str.trim();
115
+ return position === "head" ? str[0] : str[str.length - 1];
116
+ }
63
117
 
64
118
  //#endregion
65
- //#region src/utils/rules/anchor.ts
119
+ //#region src/utils/anchor.ts
66
120
  /**
67
121
  * Match the trailing anchor-like fragment from a heading string.
68
122
  * @example `δΈ­ζ–‡ζ ‡ι’˜ {#Chinese-Title}` -> `{#Chinese-Title}`
@@ -101,7 +155,7 @@ function isStrictAnchor(str) {
101
155
  * Check whether the string contains CJK Han characters.
102
156
  */
103
157
  function hasChinese(str) {
104
- return /[\u4E00-\u9FA5]/.test(str);
158
+ return /\p{Script=Han}/u.test(str);
105
159
  }
106
160
  /**
107
161
  * Normalize raw anchor text into the strict anchor format content.
@@ -111,7 +165,7 @@ function hasChinese(str) {
111
165
  * - trim leading/trailing `-`
112
166
  */
113
167
  function normalizeAnchor(anchor) {
114
- return anchor.toLowerCase().replace(/[\s.]/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
168
+ return anchor.toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
115
169
  }
116
170
  /**
117
171
  * Count wrapper characters contributed by the trailing like-anchor fragment.
@@ -128,16 +182,7 @@ function calcAnchorPositionCompensate(content) {
128
182
  }
129
183
 
130
184
  //#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
- };
185
+ //#region src/utils/punctuation.ts
141
186
  const OPENING_PAIRED_PUNCTUATION = new Set([
142
187
  "(",
143
188
  "[",
@@ -149,6 +194,23 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
149
194
  "β€œ",
150
195
  "β€˜"
151
196
  ]);
197
+ const CLOSING_PAIRED_PUNCTUATION = new Set([
198
+ ")",
199
+ "]",
200
+ "}",
201
+ ">",
202
+ "οΌ‰",
203
+ "】",
204
+ "》",
205
+ "”",
206
+ "’"
207
+ ]);
208
+ /**
209
+ * Checks whether the character is a slash used as a path-like separator.
210
+ */
211
+ function isSlashPunctuation(str) {
212
+ return str === "/";
213
+ }
152
214
  /**
153
215
  * Checks whether the character is fullwidth punctuation.
154
216
  * @example `。` -> true
@@ -158,6 +220,16 @@ function isFullwidthPunctuation(str) {
158
220
  if (!str || str.length !== 1) return false;
159
221
  return /^[\u3001-\u303F\uFE10-\uFE1F\uFE30-\uFE4F\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65β€œβ€β€˜β€™β€¦]$/u.test(str);
160
222
  }
223
+ const HALFWIDTH_PUNCTUATION_RE = /^\p{P}$/u;
224
+ /**
225
+ * Checks whether the character is halfwidth punctuation.
226
+ * @example `,` -> true
227
+ * @example `,` -> false
228
+ */
229
+ function isHalfwidthPunctuation(str) {
230
+ if (!str || str.length !== 1) return false;
231
+ return str.charCodeAt(0) <= 126 && HALFWIDTH_PUNCTUATION_RE.test(str);
232
+ }
161
233
  const DASH_PUNCTUATION_RE = /^[-\u2013\u2014\u2212]$/u;
162
234
  /**
163
235
  * Checks whether the character is hyphen-like punctuation.
@@ -168,20 +240,6 @@ function isDashPunctuation(str) {
168
240
  if (!str || str.length !== 1) return false;
169
241
  return DASH_PUNCTUATION_RE.test(str);
170
242
  }
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
243
  const PUNCTUATION_RE = /^\p{P}$/u;
186
244
  /**
187
245
  * Checks whether the character is punctuation.
@@ -194,6 +252,28 @@ function isPunctuation(str) {
194
252
  return PUNCTUATION_RE.test(str);
195
253
  }
196
254
  /**
255
+ * Checks whether the start or end of a string is adjacent to punctuation.
256
+ * @example `。 hello`, `head` -> true
257
+ * @example `hello .`, `tail` -> true
258
+ */
259
+ function hasPunctuation(str, position = "head") {
260
+ if (!str) return false;
261
+ str = str.trim();
262
+ if (position === "head") return isPunctuation(str[0]);
263
+ else return isPunctuation(str[str.length - 1]);
264
+ }
265
+
266
+ //#endregion
267
+ //#region src/utils/space.ts
268
+ const SPACE_MESSAGE_IDS = {
269
+ missingSpaceBefore: "missingSpaceBefore",
270
+ missingSpaceAfter: "missingSpaceAfter",
271
+ missingSpacesAround: "missingSpacesAround",
272
+ unexpectedSpaceBefore: "unexpectedSpaceBefore",
273
+ unexpectedSpaceAfter: "unexpectedSpaceAfter",
274
+ unexpectedSpaceAround: "unexpectedSpaceAround"
275
+ };
276
+ /**
197
277
  * Gets the count and range of consecutive whitespace at the start or end of a string.
198
278
  * @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
199
279
  * @example `text `, `tail` -> { count: 2, start: 4, end: 6 }
@@ -224,34 +304,6 @@ function getWhiteSpace(str, position = "head") {
224
304
  }
225
305
  }
226
306
  /**
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
307
  * Gets whitespace and punctuation information for text adjacent to a link or inline code node.
256
308
  */
257
309
  function getSpaceContext(nodeContext) {
@@ -273,6 +325,23 @@ function getSpaceContext(nodeContext) {
273
325
  }
274
326
  };
275
327
  }
328
+
329
+ //#endregion
330
+ //#region src/utils/inline-element.ts
331
+ /**
332
+ * Checks whether adjacent text is a custom container marker on the next line.
333
+ *
334
+ * @deprecated Temporary workaround to prevent space-between-link from reporting
335
+ * false positives on custom containers. Remove this and handle the case in a
336
+ * dedicated custom container rule when one exists.
337
+ * @see https://vitepress.dev/guide/markdown#custom-containers
338
+ * @example `\n:::` -> true
339
+ * @example `\n::::` -> true
340
+ * @example `:::` -> false
341
+ */
342
+ function isCustomContainerMarker(str) {
343
+ return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
344
+ }
276
345
  /**
277
346
  * Validates whether a spacing run contains exactly one required space.
278
347
  */
@@ -281,121 +350,512 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
281
350
  if (count > 1) return multipleSpacesMessageId;
282
351
  }
283
352
  /**
284
- * Validates the spacing before a link when the previous character is punctuation.
353
+ * Validates spacing before an inline node when the previous character is punctuation.
285
354
  */
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;
355
+ function validateBeforePunctuation(context) {
356
+ const adjacentChar = getAdjacentChar(context.value, "tail");
357
+ if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
358
+ if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
289
359
  return;
290
360
  }
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;
361
+ if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesAfterPunctuation);
362
+ if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
293
363
  }
294
364
  /**
295
- * Validates the spacing between the previous node and the current link.
365
+ * Validates the spacing between the previous node and the current inline node.
296
366
  */
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);
367
+ function validateSpaceBeforeNode(context) {
368
+ if (context.hasPunctuation) return validateBeforePunctuation(context);
369
+ return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesBefore);
300
370
  }
301
371
  /**
302
- * Validates the spacing after a link when the next character is punctuation.
372
+ * Validates spacing after an inline node when the next character is punctuation.
303
373
  */
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);
374
+ function validateSpaceAfterPunctuation(context) {
375
+ const adjacentChar = getAdjacentChar(context.value, "head");
306
376
  if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
307
- if (context.whiteSpace.count > 0) return LINK_SPACE_MESSAGE_IDS.unexpectedSpaceAfterLink;
377
+ if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceAfter;
378
+ if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
379
+ if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceAfter;
380
+ }
381
+ /**
382
+ * Validates the spacing between the current inline node and the next node.
383
+ */
384
+ function validateSpaceAfterNode(context) {
385
+ if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
386
+ return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
308
387
  }
309
388
  /**
310
- * Validates the spacing between the current link and the next node.
389
+ * Validates spacing around an inline element inside a table cell.
390
+ * Table cells skip checks when the next sibling is another inline element.
311
391
  */
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);
392
+ function validateTableCellSpace(nodeContext) {
393
+ const { prev, next } = getSpaceContext(nodeContext);
394
+ if (prev && prev.value) {
395
+ const beforeIssue = validateSpaceBeforeNode(prev);
396
+ if (beforeIssue) return beforeIssue;
397
+ }
398
+ if (!next || isInlineElement(nodeContext.next) || !next.value) return;
399
+ return validateSpaceAfterNode(next);
315
400
  }
316
401
  /**
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.
402
+ * Validates spacing around an inline element in the default text flow.
403
+ */
404
+ function validateDefaultSpace(nodeContext) {
405
+ const { prev, next } = getSpaceContext(nodeContext);
406
+ if (prev && nodeContext.prev) {
407
+ const beforeIssue = validateSpaceBeforeNode(prev);
408
+ if (beforeIssue) return beforeIssue;
409
+ }
410
+ if (!next || isInlineElement(nodeContext.next) || !nodeContext.next) return;
411
+ return validateSpaceAfterNode(next);
412
+ }
413
+ /**
414
+ * Validates spacing around an inline element by delegating to the appropriate strategy
415
+ * for table cells or the default text flow.
416
+ * - Regular text and selected inline elements should be separated by one space.
417
+ * - Fullwidth punctuation and paired punctuation usually touch inline elements without spaces.
418
+ * - Adjacent selected inline elements are handled by the following element to avoid duplicate fixes.
321
419
  */
322
420
  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);
421
+ const { parent } = nodeContext;
422
+ if (parent && isTableCell(parent)) return validateTableCellSpace(nodeContext);
423
+ return validateDefaultSpace(nodeContext);
330
424
  }
331
425
 
332
426
  //#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
427
+ //#region src/rules/space-around-inline-element/index.ts
428
+ const RULE_NAME$3 = "space-around-inline-element";
429
+ const MESSAGE_IDS$1 = {
430
+ missingSpaceBefore: "missingSpaceBefore",
431
+ missingSpaceAfter: "missingSpaceAfter",
432
+ multipleSpacesBefore: "multipleSpacesBefore",
433
+ multipleSpacesAfter: "multipleSpacesAfter",
434
+ multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
435
+ unexpectedSpaceBefore: "unexpectedSpaceBefore",
436
+ unexpectedSpaceAfter: "unexpectedSpaceAfter"
437
+ };
438
+ const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set([
439
+ MESSAGE_IDS$1.missingSpaceBefore,
440
+ MESSAGE_IDS$1.multipleSpacesBefore,
441
+ MESSAGE_IDS$1.multipleSpacesAfterPunctuation,
442
+ MESSAGE_IDS$1.unexpectedSpaceBefore
340
443
  ]);
341
- var space_between_link_default = createRule({
342
- name: RULE_NAME$1,
444
+ var space_around_inline_element_default = createRule({
445
+ name: RULE_NAME$3,
343
446
  meta: {
344
447
  type: "layout",
345
- docs: { description: "Enforce spacing around Markdown links: one space next to text, no spaces next to punctuation." },
448
+ docs: { description: "Enforce spacing around Markdown inline elements." },
346
449
  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.",
450
+ missingSpaceBefore: "A space is required before the inline element.",
451
+ missingSpaceAfter: "A space is required after the inline element.",
452
+ multipleSpacesBefore: "Use exactly one space before the inline element.",
453
+ multipleSpacesAfter: "Use exactly one space after the inline element.",
351
454
  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."
455
+ unexpectedSpaceBefore: "Do not add a space between punctuation and the inline element.",
456
+ unexpectedSpaceAfter: "Do not add a space between the inline element and punctuation."
354
457
  },
355
458
  fixable: "whitespace",
356
459
  schema: []
357
460
  },
358
461
  defaultOptions: [],
359
462
  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;
463
+ return {
464
+ link(node) {
465
+ checkInlineElement(context, node);
466
+ },
467
+ image(node) {
468
+ checkInlineElement(context, node);
469
+ },
470
+ inlineCode(node) {
471
+ checkInlineElement(context, node);
472
+ },
473
+ emphasis(node) {
474
+ checkInlineElement(context, node);
475
+ },
476
+ strong(node) {
477
+ checkInlineElement(context, node);
378
478
  }
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
- });
479
+ };
480
+ }
481
+ });
482
+ /**
483
+ * Checks one selected inline element and reports the fix range around it.
484
+ */
485
+ function checkInlineElement(context, node) {
486
+ const { position, start, end } = getNodePosition(node);
487
+ /* v8 ignore if -- @preserve */
488
+ if (!position) return;
489
+ const nodeContext = getNodeContext(context, node);
490
+ if (isNestedInlineElement(nodeContext)) return;
491
+ const spaceContext = getSpaceContext(nodeContext);
492
+ const messageId = validateSpace(nodeContext);
493
+ if (!messageId) return;
494
+ if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
495
+ const { count } = spaceContext.prev.whiteSpace;
496
+ const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceBefore ? "" : " ";
497
+ context.report({
498
+ node,
499
+ messageId,
500
+ fix(fixer) {
501
+ return fixer.replaceTextRange([start - count, start], replaceText);
502
+ }
503
+ });
504
+ return;
505
+ }
506
+ if (spaceContext.next) {
507
+ const { count } = spaceContext.next.whiteSpace;
508
+ const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceAfter ? "" : " ";
509
+ context.report({
510
+ node,
511
+ messageId,
512
+ fix(fixer) {
513
+ return fixer.replaceTextRange([end, end + count], replaceText);
389
514
  }
515
+ });
516
+ }
517
+ }
518
+
519
+ //#endregion
520
+ //#region src/utils/text/tokenizer.ts
521
+ const TEXT_TYPE = {
522
+ "cjk": "cjk",
523
+ "latin": "latin",
524
+ "number": "number",
525
+ "space": "space",
526
+ "newline": "newline",
527
+ "fullwidth-punctuation": "fullwidth-punctuation",
528
+ "halfwidth-punctuation": "halfwidth-punctuation",
529
+ "dash": "dash",
530
+ "symbol": "symbol",
531
+ "emoji": "emoji",
532
+ "invisible": "invisible",
533
+ "other": "other"
534
+ };
535
+ const CJK_RE = /^\p{Script=Han}$|^\p{Script=Hiragana}$|^\p{Script=Katakana}$|^\p{Script=Hangul}$/u;
536
+ const LATIN_RE = /^\p{Script=Latin}$/u;
537
+ const NUMBER_RE = /^\p{Number}$/u;
538
+ const SYMBOL_RE = /^\p{Symbol}$/u;
539
+ const EMOJI_RE = /^\p{Extended_Pictographic}$/u;
540
+ const SPACE_RE = /^[\t\v\f \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]$/u;
541
+ const NEWLINE_RE = /^[\n\r\u2028\u2029]$/u;
542
+ const INVISIBLE_CODE_POINTS = new Set([
543
+ 173,
544
+ 847,
545
+ 1564,
546
+ 4447,
547
+ 4448,
548
+ 6068,
549
+ 6069,
550
+ 6158,
551
+ 65279,
552
+ 65440
553
+ ]);
554
+ /**
555
+ * Checks whether the character is an invisible Unicode control or filler.
556
+ * These characters affect rendering, cursor movement, or text direction but
557
+ * should not be treated as visible spacing, punctuation, or symbols.
558
+ */
559
+ function isInvisible(char) {
560
+ const codePoint = char.codePointAt(0);
561
+ return codePoint != null && (INVISIBLE_CODE_POINTS.has(codePoint) || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303);
562
+ }
563
+ function isLatinWordType(type) {
564
+ return type === TEXT_TYPE.latin;
565
+ }
566
+ function isNumberType(type) {
567
+ return type === TEXT_TYPE.number;
568
+ }
569
+ function isNumber(char, prev) {
570
+ return NUMBER_RE.test(char) || prev?.type === TEXT_TYPE.number && (char === "." || char === "%");
571
+ }
572
+ const TEXT_TYPE_MATCHERS = [
573
+ {
574
+ type: TEXT_TYPE.newline,
575
+ test: (char) => NEWLINE_RE.test(char)
576
+ },
577
+ {
578
+ type: TEXT_TYPE.space,
579
+ test: (char) => SPACE_RE.test(char)
580
+ },
581
+ {
582
+ type: TEXT_TYPE.invisible,
583
+ test: (char) => isInvisible(char)
584
+ },
585
+ {
586
+ type: TEXT_TYPE.cjk,
587
+ test: (char) => CJK_RE.test(char)
588
+ },
589
+ {
590
+ type: TEXT_TYPE.latin,
591
+ test: (char) => LATIN_RE.test(char)
592
+ },
593
+ {
594
+ type: TEXT_TYPE.number,
595
+ test: isNumber
596
+ },
597
+ {
598
+ type: TEXT_TYPE.dash,
599
+ test: (char) => isDashPunctuation(char)
600
+ },
601
+ {
602
+ type: TEXT_TYPE["fullwidth-punctuation"],
603
+ test: (char) => isFullwidthPunctuation(char)
604
+ },
605
+ {
606
+ type: TEXT_TYPE["halfwidth-punctuation"],
607
+ test: (char) => isHalfwidthPunctuation(char)
608
+ },
609
+ {
610
+ type: TEXT_TYPE.emoji,
611
+ test: (char) => EMOJI_RE.test(char)
612
+ },
613
+ {
614
+ type: TEXT_TYPE.symbol,
615
+ test: (char) => SYMBOL_RE.test(char)
616
+ }
617
+ ];
618
+ const DEFAULT_START_POINT = {
619
+ line: 1,
620
+ column: 1,
621
+ offset: 0
622
+ };
623
+ function advancePoint(point, char) {
624
+ if (NEWLINE_RE.test(char)) return {
625
+ line: point.line + 1,
626
+ column: 1,
627
+ offset: point.offset + char.length
628
+ };
629
+ return {
630
+ line: point.line,
631
+ column: point.column + char.length,
632
+ offset: point.offset + char.length
633
+ };
634
+ }
635
+ /**
636
+ * Classifies a single Unicode code point for Markdown text style rules.
637
+ */
638
+ function getTextType(char, prev) {
639
+ for (const matcher of TEXT_TYPE_MATCHERS) if (matcher.test(char, prev)) return matcher.type;
640
+ return TEXT_TYPE.other;
641
+ }
642
+ /**
643
+ * Tokenizes text into consecutive typed runs while preserving source positions.
644
+ */
645
+ function tokenizeText(value, start = DEFAULT_START_POINT) {
646
+ const tokens = [];
647
+ let point = start;
648
+ let prevToken;
649
+ for (const char of value) {
650
+ const type = getTextType(char, prevToken);
651
+ const tokenStart = point;
652
+ const tokenEnd = advancePoint(point, char);
653
+ if (prevToken?.type === type) {
654
+ prevToken.value += char;
655
+ prevToken.position.end = tokenEnd;
656
+ } else {
657
+ prevToken = {
658
+ type,
659
+ value: char,
660
+ position: {
661
+ start: tokenStart,
662
+ end: tokenEnd
663
+ }
664
+ };
665
+ tokens.push(prevToken);
666
+ }
667
+ point = tokenEnd;
668
+ }
669
+ return tokens;
670
+ }
671
+ /**
672
+ * Converts an mdast text node into a tokenized text AST with normalized source positions.
673
+ */
674
+ function buildTextNodeAst(node) {
675
+ const start = node.position?.start;
676
+ const end = node.position?.end;
677
+ const position = {
678
+ start: {
679
+ line: start?.line ?? DEFAULT_START_POINT.line,
680
+ column: start?.column ?? DEFAULT_START_POINT.column,
681
+ offset: start?.offset ?? DEFAULT_START_POINT.offset
682
+ },
683
+ end: {
684
+ line: end?.line ?? DEFAULT_START_POINT.line,
685
+ column: end?.column ?? DEFAULT_START_POINT.column + node.value.length,
686
+ offset: end?.offset ?? DEFAULT_START_POINT.offset + node.value.length
687
+ }
688
+ };
689
+ return {
690
+ type: "text",
691
+ value: node.value,
692
+ position,
693
+ children: tokenizeText(node.value, position.start),
694
+ node
695
+ };
696
+ }
697
+
698
+ //#endregion
699
+ //#region src/utils/text/boundary-space.ts
700
+ /**
701
+ * Normalizes an existing space token around the target token type.
702
+ * When multiple spaces are collapsed, the redundant side is recorded so the
703
+ * caller can choose the most specific lint message.
704
+ */
705
+ function processSpaceToken(ctx, result, isTargetType) {
706
+ const { prev, current, next } = ctx;
707
+ /* v8 ignore if -- @preserve */
708
+ if (!current) return;
709
+ const cjkToTarget = prev?.type === TEXT_TYPE.cjk && isTargetType(next?.type);
710
+ const targetToCjk = isTargetType(prev?.type) && next?.type === TEXT_TYPE.cjk;
711
+ const targets = isTargetType(prev?.type) && isTargetType(next?.type);
712
+ const hasUnexpectedSpaces = current.value.length !== 1;
713
+ if (hasUnexpectedSpaces) {
714
+ if (cjkToTarget || targets) result.unexpectedBefore = true;
715
+ if (targetToCjk || targets) result.unexpectedAfter = true;
716
+ }
717
+ if (cjkToTarget || targetToCjk || hasUnexpectedSpaces) result.fixed += " ";
718
+ else result.fixed += current.value;
719
+ }
720
+ /**
721
+ * Inserts missing spaces before or after the target token when it directly
722
+ * touches adjacent CJK text.
723
+ */
724
+ function processTargetToken(ctx, result) {
725
+ const { prev, current, next } = ctx;
726
+ /* v8 ignore if -- @preserve */
727
+ if (!current) return;
728
+ if (prev?.type === TEXT_TYPE.cjk) {
729
+ result.fixed += " ";
730
+ result.missingBefore = true;
731
+ }
732
+ result.fixed += current.value;
733
+ if (next?.type === TEXT_TYPE.cjk) {
734
+ result.fixed += " ";
735
+ result.missingAfter = true;
736
+ }
737
+ }
738
+ /**
739
+ * Chooses the most specific shared boundary-space message id from the
740
+ * collected missing or redundant space flags.
741
+ */
742
+ function getBoundarySpaceMessageId(boundary) {
743
+ if (boundary.missingBefore && boundary.missingAfter) return SPACE_MESSAGE_IDS.missingSpacesAround;
744
+ if (boundary.unexpectedBefore && boundary.unexpectedAfter) return SPACE_MESSAGE_IDS.unexpectedSpaceAround;
745
+ if (boundary.missingBefore) return SPACE_MESSAGE_IDS.missingSpaceBefore;
746
+ if (boundary.missingAfter) return SPACE_MESSAGE_IDS.missingSpaceAfter;
747
+ if (boundary.unexpectedBefore) return SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
748
+ return SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
749
+ }
750
+ /**
751
+ * Rebuilds a text node with normalized spacing between CJK text and a target
752
+ * token class such as Latin words or numbers.
753
+ */
754
+ function fixBoundarySpace(node, isTargetType) {
755
+ const { children } = buildTextNodeAst(node);
756
+ const result = {
757
+ fixed: "",
758
+ missingBefore: false,
759
+ missingAfter: false,
760
+ unexpectedBefore: false,
761
+ unexpectedAfter: false
762
+ };
763
+ for (let i = 0; i < children.length; i += 1) {
764
+ const ctx = getNodeContextByParent(children, i);
765
+ /* v8 ignore if -- @preserve */
766
+ if (!ctx.current) continue;
767
+ if (ctx.current.type === TEXT_TYPE.space) processSpaceToken(ctx, result, isTargetType);
768
+ else if (isTargetType(ctx.current.type)) processTargetToken(ctx, result);
769
+ else result.fixed += ctx.current.value;
770
+ }
771
+ return result;
772
+ }
773
+
774
+ //#endregion
775
+ //#region src/rules/space-around-number/index.ts
776
+ const RULE_NAME$2 = "space-around-number";
777
+ var space_around_number_default = createRule({
778
+ name: RULE_NAME$2,
779
+ meta: {
780
+ type: "layout",
781
+ docs: { description: "Enforce a single space between CJK characters and numbers." },
782
+ messages: {
783
+ missingSpaceBefore: "Add a space before the number.",
784
+ missingSpaceAfter: "Add a space after the number.",
785
+ missingSpacesAround: "Add spaces before and after the number.",
786
+ unexpectedSpaceBefore: "Remove the unexpected space before the number.",
787
+ unexpectedSpaceAfter: "Remove the unexpected space after the number.",
788
+ unexpectedSpaceAround: "Remove the unexpected spaces around the number."
789
+ },
790
+ fixable: "whitespace",
791
+ schema: []
792
+ },
793
+ defaultOptions: [],
794
+ create(context) {
795
+ return { text(node) {
796
+ const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, isNumberType);
797
+ if (fixed === node.value) return;
798
+ context.report({
799
+ node,
800
+ messageId: getBoundarySpaceMessageId({
801
+ missingBefore,
802
+ missingAfter,
803
+ unexpectedBefore,
804
+ unexpectedAfter
805
+ }),
806
+ fix(fixer) {
807
+ return fixer.replaceText(node, fixed);
808
+ }
809
+ });
810
+ } };
811
+ }
812
+ });
813
+
814
+ //#endregion
815
+ //#region src/rules/space-around-word/index.ts
816
+ const RULE_NAME$1 = "space-around-word";
817
+ var space_around_word_default = createRule({
818
+ name: RULE_NAME$1,
819
+ meta: {
820
+ type: "layout",
821
+ docs: { description: "Enforce a single space between CJK characters and Latin words." },
822
+ messages: {
823
+ missingSpaceBefore: "Add a space before the word.",
824
+ missingSpaceAfter: "Add a space after the word.",
825
+ missingSpacesAround: "Add spaces before and after the word.",
826
+ unexpectedSpaceBefore: "Remove the unexpected space before the word.",
827
+ unexpectedSpaceAfter: "Remove the unexpected space after the word.",
828
+ unexpectedSpaceAround: "Remove the unexpected spaces around the word."
829
+ },
830
+ fixable: "whitespace",
831
+ schema: []
832
+ },
833
+ defaultOptions: [],
834
+ create(context) {
835
+ return { text(node) {
836
+ const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, isLatinWordType);
837
+ if (fixed === node.value) return;
838
+ context.report({
839
+ node,
840
+ messageId: getBoundarySpaceMessageId({
841
+ missingBefore,
842
+ missingAfter,
843
+ unexpectedBefore,
844
+ unexpectedAfter
845
+ }),
846
+ fix(fixer) {
847
+ return fixer.replaceText(node, fixed);
848
+ }
849
+ });
390
850
  } };
391
851
  }
392
852
  });
393
853
 
394
854
  //#endregion
395
855
  //#region src/utils/markdown.ts
396
- const language = new MarkdownLanguage({ mode: "commonmark" });
856
+ const language = new MarkdownLanguage({ mode: "gfm" });
397
857
  /**
398
- * Parses Markdown with the same CommonMark language implementation used by the
858
+ * Parses Markdown with the same GFM language implementation used by the
399
859
  * plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
400
860
  */
401
861
  function parseMarkdown(markdown$1) {
@@ -409,6 +869,7 @@ function parseMarkdown(markdown$1) {
409
869
  ...language.defaultLanguageOptions,
410
870
  frontmatter: "yaml"
411
871
  } });
872
+ /* v8 ignore if -- @preserve */
412
873
  if (!parseResult.ok) throw new Error(parseResult.errors[0]?.message ?? "Failed to parse markdown.");
413
874
  return {
414
875
  ast: parseResult.ast,
@@ -417,7 +878,7 @@ function parseMarkdown(markdown$1) {
417
878
  }
418
879
 
419
880
  //#endregion
420
- //#region src/utils/rules/heading.ts
881
+ //#region src/utils/heading.ts
421
882
  /**
422
883
  * Returns true when the Markdown document starts with YAML frontmatter.
423
884
  */
@@ -450,6 +911,7 @@ var valid_heading_anchor_default = createRule({
450
911
  create(context) {
451
912
  return { heading(node) {
452
913
  const { position, start, end } = getNodePosition(node);
914
+ /* v8 ignore if -- @preserve */
453
915
  if (!position) return;
454
916
  const source = context.sourceCode.text.slice(start, end);
455
917
  if (isStrictAnchor(source) || !hasChinese(source)) return;
@@ -481,7 +943,9 @@ var valid_heading_anchor_default = createRule({
481
943
  //#endregion
482
944
  //#region src/rules/index.ts
483
945
  const rules = {
484
- "space-between-link": space_between_link_default,
946
+ "space-around-inline-element": space_around_inline_element_default,
947
+ "space-around-number": space_around_number_default,
948
+ "space-around-word": space_around_word_default,
485
949
  "valid-heading-anchor": valid_heading_anchor_default
486
950
  };
487
951
 
@@ -490,27 +954,28 @@ const rules = {
490
954
  const plugin = {
491
955
  rules,
492
956
  processors: markdown.processors,
493
- languages: {
494
- commonmark: new MarkdownLanguage({ mode: "commonmark" }),
495
- gfm: new MarkdownLanguage({ mode: "gfm" })
496
- }
957
+ languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
958
+ };
959
+ const recommendedRules = {
960
+ "md-style/valid-heading-anchor": "error",
961
+ "md-style/space-around-inline-element": "error"
497
962
  };
498
- const allRuleEntries = Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]);
499
- const recommendedRules = Object.fromEntries(allRuleEntries);
500
- const allRules = Object.fromEntries(allRuleEntries);
963
+ const allRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]));
501
964
  const configs = {
502
965
  recommended: {
503
966
  name: "md-style/recommended",
504
967
  files: ["**/*.md"],
505
968
  plugins: { "md-style": plugin },
506
- language: "md-style/commonmark",
969
+ language: "md-style/gfm",
970
+ languageOptions: { frontmatter: "yaml" },
507
971
  rules: recommendedRules
508
972
  },
509
973
  all: {
510
974
  name: "md-style/all",
511
975
  files: ["**/*.md"],
512
976
  plugins: { "md-style": plugin },
513
- language: "md-style/commonmark",
977
+ language: "md-style/gfm",
978
+ languageOptions: { frontmatter: "yaml" },
514
979
  rules: allRules
515
980
  }
516
981
  };
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.2.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>",
@@ -18,8 +18,6 @@
18
18
  ".": "./dist/index.mjs",
19
19
  "./package.json": "./package.json"
20
20
  },
21
- "main": "./dist/index.mjs",
22
- "module": "./dist/index.mjs",
23
21
  "types": "./dist/index.d.mts",
24
22
  "files": [
25
23
  "dist"
@@ -43,27 +41,34 @@
43
41
  "prepare": "simple-git-hooks"
44
42
  },
45
43
  "peerDependencies": {
44
+ "@antfu/eslint-config": "^7.5.0",
46
45
  "@eslint/markdown": "^7.5.1",
47
- "eslint": "^9.0.0 || ^10.0.0"
46
+ "eslint": "^9.30.0 || ^10.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@antfu/eslint-config": {
50
+ "optional": true
51
+ }
48
52
  },
49
- "dependencies": {},
50
53
  "devDependencies": {
51
- "@antfu/eslint-config": "^6.2.0",
52
- "@eslint/markdown": "^7.5.1",
54
+ "@antfu/eslint-config": "^9.0.0",
55
+ "@eslint/markdown": "^8.0.1",
53
56
  "@types/mdast": "^4.0.4",
54
- "@types/node": "^24.10.1",
55
- "@typescript-eslint/utils": "^8.46.4",
56
- "@vitest/coverage-v8": "^4.1.5",
57
- "bumpp": "^10.3.1",
58
- "eslint": "9.39.1",
59
- "eslint-plugin-format": "^1.0.2",
60
- "eslint-vitest-rule-tester": "^3.0.0",
61
- "lint-staged": "^16.4.0",
57
+ "@types/node": "^25.7.0",
58
+ "@typescript-eslint/utils": "^8.59.3",
59
+ "@vitest/coverage-v8": "^4.1.6",
60
+ "bumpp": "^11.1.0",
61
+ "eslint": "10.3.0",
62
+ "eslint-factory": "^0.1.2",
63
+ "eslint-plugin-format": "^2.0.1",
64
+ "eslint-plugin-md-style": "^0.1.0",
65
+ "eslint-vitest-rule-tester": "^3.1.0",
66
+ "lint-staged": "^17.0.4",
62
67
  "simple-git-hooks": "^2.13.1",
63
68
  "tinyglobby": "^0.2.16",
64
- "tsdown": "^0.16.4",
65
- "typescript": "^5.9.3",
66
- "vitest": "^4.0.9"
69
+ "tsdown": "^0.22.0",
70
+ "typescript": "^6.0.3",
71
+ "vitest": "^4.1.6"
67
72
  },
68
73
  "simple-git-hooks": {
69
74
  "pre-commit": "pnpx lint-staged"
@@ -72,5 +77,7 @@
72
77
  "*.{js,ts,md,yml,yaml,json}": [
73
78
  "eslint --cache --fix"
74
79
  ]
75
- }
80
+ },
81
+ "main": "./dist/index.mjs",
82
+ "module": "./dist/index.mjs"
76
83
  }