eslint-plugin-md-style 0.1.0 → 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 +4 -2
  2. package/dist/index.mjs +430 -59
  3. package/package.json +19 -18
package/README.md CHANGED
@@ -70,7 +70,7 @@ export default [
70
70
  },
71
71
  language: 'md-style/gfm',
72
72
  rules: {
73
- 'md-style/space-between-inline-element': 'error',
73
+ 'md-style/space-around-inline-element': 'error',
74
74
  'md-style/valid-heading-anchor': 'error',
75
75
  },
76
76
  },
@@ -120,7 +120,9 @@ export default antfu(
120
120
 
121
121
  | Rule | Included in `recommended` | Autofix |
122
122
  | --- | --- | --- |
123
- | `md-style/space-between-inline-element` | ✅ | 🔧 |
123
+ | `md-style/space-around-inline-element` | ✅ | 🔧 |
124
+ | `md-style/space-around-number` | | 🔧 |
125
+ | `md-style/space-around-word` | | 🔧 |
124
126
  | `md-style/valid-heading-anchor` | ✅ | 🔧 |
125
127
 
126
128
  ## Why `@eslint/markdown` Is Required
package/dist/index.mjs CHANGED
@@ -52,7 +52,7 @@ function isNestedInlineElement(nodeContext) {
52
52
  function getNodeValue(node) {
53
53
  if (!node) return;
54
54
  if ("value" in node) return node.value;
55
- if (hasChildren(node)) return node.children.map(getNodeValue).join("") || void 0;
55
+ if (hasChildren(node)) return node.children.map(getNodeValue).join("");
56
56
  }
57
57
  /**
58
58
  * Gets the start and end offsets for a node.
@@ -95,7 +95,19 @@ function getNodeContext(context, node) {
95
95
  };
96
96
  }
97
97
  /**
98
- * Gets the character adjacent to the start or end of a string.
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.
99
111
  */
100
112
  function getAdjacentChar(str, position) {
101
113
  if (!str) return void 0;
@@ -143,7 +155,7 @@ function isStrictAnchor(str) {
143
155
  * Check whether the string contains CJK Han characters.
144
156
  */
145
157
  function hasChinese(str) {
146
- return /[\u4E00-\u9FA5]/.test(str);
158
+ return /\p{Script=Han}/u.test(str);
147
159
  }
148
160
  /**
149
161
  * Normalize raw anchor text into the strict anchor format content.
@@ -153,7 +165,7 @@ function hasChinese(str) {
153
165
  * - trim leading/trailing `-`
154
166
  */
155
167
  function normalizeAnchor(anchor) {
156
- 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, "");
157
169
  }
158
170
  /**
159
171
  * Count wrapper characters contributed by the trailing like-anchor fragment.
@@ -208,6 +220,16 @@ function isFullwidthPunctuation(str) {
208
220
  if (!str || str.length !== 1) return false;
209
221
  return /^[\u3001-\u303F\uFE10-\uFE1F\uFE30-\uFE4F\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65“”‘’…]$/u.test(str);
210
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
+ }
211
233
  const DASH_PUNCTUATION_RE = /^[-\u2013\u2014\u2212]$/u;
212
234
  /**
213
235
  * Checks whether the character is hyphen-like punctuation.
@@ -243,6 +265,14 @@ function hasPunctuation(str, position = "head") {
243
265
 
244
266
  //#endregion
245
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
+ };
246
276
  /**
247
277
  * Gets the count and range of consecutive whitespace at the start or end of a string.
248
278
  * @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
@@ -298,15 +328,6 @@ function getSpaceContext(nodeContext) {
298
328
 
299
329
  //#endregion
300
330
  //#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
331
  /**
311
332
  * Checks whether adjacent text is a custom container marker on the next line.
312
333
  *
@@ -334,18 +355,18 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
334
355
  function validateBeforePunctuation(context) {
335
356
  const adjacentChar = getAdjacentChar(context.value, "tail");
336
357
  if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
337
- if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
358
+ if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
338
359
  return;
339
360
  }
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;
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;
342
363
  }
343
364
  /**
344
365
  * Validates the spacing between the previous node and the current inline node.
345
366
  */
346
367
  function validateSpaceBeforeNode(context) {
347
368
  if (context.hasPunctuation) return validateBeforePunctuation(context);
348
- return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore, INLINE_SPACE_MESSAGE_IDS.multipleSpacesBefore);
369
+ return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesBefore);
349
370
  }
350
371
  /**
351
372
  * Validates spacing after an inline node when the next character is punctuation.
@@ -353,16 +374,16 @@ function validateSpaceBeforeNode(context) {
353
374
  function validateSpaceAfterPunctuation(context) {
354
375
  const adjacentChar = getAdjacentChar(context.value, "head");
355
376
  if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
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;
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;
359
380
  }
360
381
  /**
361
382
  * Validates the spacing between the current inline node and the next node.
362
383
  */
363
384
  function validateSpaceAfterNode(context) {
364
385
  if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
365
- return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceAfter, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfter);
386
+ return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
366
387
  }
367
388
  /**
368
389
  * Validates spacing around an inline element inside a table cell.
@@ -403,14 +424,61 @@ function validateSpace(nodeContext) {
403
424
  }
404
425
 
405
426
  //#endregion
406
- //#region src/rules/space-between-inline-element/index.ts
407
- const RULE_NAME$1 = "space-between-inline-element";
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
+ };
408
438
  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
439
+ MESSAGE_IDS$1.missingSpaceBefore,
440
+ MESSAGE_IDS$1.multipleSpacesBefore,
441
+ MESSAGE_IDS$1.multipleSpacesAfterPunctuation,
442
+ MESSAGE_IDS$1.unexpectedSpaceBefore
413
443
  ]);
444
+ var space_around_inline_element_default = createRule({
445
+ name: RULE_NAME$3,
446
+ meta: {
447
+ type: "layout",
448
+ docs: { description: "Enforce spacing around Markdown inline elements." },
449
+ messages: {
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.",
454
+ multipleSpacesAfterPunctuation: "Use one space after 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."
457
+ },
458
+ fixable: "whitespace",
459
+ schema: []
460
+ },
461
+ defaultOptions: [],
462
+ create(context) {
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);
478
+ }
479
+ };
480
+ }
481
+ });
414
482
  /**
415
483
  * Checks one selected inline element and reports the fix range around it.
416
484
  */
@@ -425,7 +493,7 @@ function checkInlineElement(context, node) {
425
493
  if (!messageId) return;
426
494
  if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
427
495
  const { count } = spaceContext.prev.whiteSpace;
428
- const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore ? "" : " ";
496
+ const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceBefore ? "" : " ";
429
497
  context.report({
430
498
  node,
431
499
  messageId,
@@ -437,7 +505,7 @@ function checkInlineElement(context, node) {
437
505
  }
438
506
  if (spaceContext.next) {
439
507
  const { count } = spaceContext.next.whiteSpace;
440
- const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter ? "" : " ";
508
+ const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceAfter ? "" : " ";
441
509
  context.report({
442
510
  node,
443
511
  messageId,
@@ -447,42 +515,339 @@ function checkInlineElement(context, node) {
447
515
  });
448
516
  }
449
517
  }
450
- var space_between_inline_element_default = createRule({
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({
451
818
  name: RULE_NAME$1,
452
819
  meta: {
453
820
  type: "layout",
454
- docs: { description: "Enforce spacing around Markdown inline elements." },
821
+ docs: { description: "Enforce a single space between CJK characters and Latin words." },
455
822
  messages: {
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.",
460
- multipleSpacesAfterPunctuation: "Use one space after 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."
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."
463
829
  },
464
830
  fixable: "whitespace",
465
831
  schema: []
466
832
  },
467
833
  defaultOptions: [],
468
834
  create(context) {
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);
484
- }
485
- };
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
+ });
850
+ } };
486
851
  }
487
852
  });
488
853
 
@@ -578,7 +943,9 @@ var valid_heading_anchor_default = createRule({
578
943
  //#endregion
579
944
  //#region src/rules/index.ts
580
945
  const rules = {
581
- "space-between-inline-element": space_between_inline_element_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,
582
949
  "valid-heading-anchor": valid_heading_anchor_default
583
950
  };
584
951
 
@@ -589,15 +956,18 @@ const plugin = {
589
956
  processors: markdown.processors,
590
957
  languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
591
958
  };
592
- const allRuleEntries = Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]);
593
- const recommendedRules = Object.fromEntries(allRuleEntries);
594
- const allRules = Object.fromEntries(allRuleEntries);
959
+ const recommendedRules = {
960
+ "md-style/valid-heading-anchor": "error",
961
+ "md-style/space-around-inline-element": "error"
962
+ };
963
+ const allRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]));
595
964
  const configs = {
596
965
  recommended: {
597
966
  name: "md-style/recommended",
598
967
  files: ["**/*.md"],
599
968
  plugins: { "md-style": plugin },
600
969
  language: "md-style/gfm",
970
+ languageOptions: { frontmatter: "yaml" },
601
971
  rules: recommendedRules
602
972
  },
603
973
  all: {
@@ -605,6 +975,7 @@ const configs = {
605
975
  files: ["**/*.md"],
606
976
  plugins: { "md-style": plugin },
607
977
  language: "md-style/gfm",
978
+ languageOptions: { frontmatter: "yaml" },
608
979
  rules: allRules
609
980
  }
610
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",
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"
@@ -52,24 +50,25 @@
52
50
  "optional": true
53
51
  }
54
52
  },
55
- "dependencies": {},
56
53
  "devDependencies": {
57
- "@antfu/eslint-config": "^6.2.0",
58
- "@eslint/markdown": "^7.5.1",
54
+ "@antfu/eslint-config": "^9.0.0",
55
+ "@eslint/markdown": "^8.0.1",
59
56
  "@types/mdast": "^4.0.4",
60
- "@types/node": "^24.10.1",
61
- "@typescript-eslint/utils": "^8.46.4",
62
- "@vitest/coverage-v8": "^4.1.5",
63
- "bumpp": "^10.3.1",
64
- "eslint": "9.39.1",
65
- "eslint-plugin-format": "^1.0.2",
66
- "eslint-vitest-rule-tester": "^3.0.0",
67
- "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",
68
67
  "simple-git-hooks": "^2.13.1",
69
68
  "tinyglobby": "^0.2.16",
70
- "tsdown": "^0.16.4",
71
- "typescript": "^5.9.3",
72
- "vitest": "^4.0.9"
69
+ "tsdown": "^0.22.0",
70
+ "typescript": "^6.0.3",
71
+ "vitest": "^4.1.6"
73
72
  },
74
73
  "simple-git-hooks": {
75
74
  "pre-commit": "pnpx lint-staged"
@@ -78,5 +77,7 @@
78
77
  "*.{js,ts,md,yml,yaml,json}": [
79
78
  "eslint --cache --fix"
80
79
  ]
81
- }
80
+ },
81
+ "main": "./dist/index.mjs",
82
+ "module": "./dist/index.mjs"
82
83
  }