eslint-plugin-markdown-preferences 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,21 +2,21 @@
2
2
 
3
3
  A specialized ESLint plugin that helps enforce consistent writing style and formatting conventions in Markdown files. Perfect for documentation projects, blog posts, and any Markdown content where consistency matters.
4
4
 
5
- [![NPM license](https://img.shields.io/npm/l/eslint-plugin-markdown-preferences.svg)](https://www.npmjs.com/package/eslint-plugin-markdown-preferences)
6
- [![NPM version](https://img.shields.io/npm/v/eslint-plugin-markdown-preferences.svg)](https://www.npmjs.com/package/eslint-plugin-markdown-preferences)
7
- [![NPM downloads](https://img.shields.io/badge/dynamic/json.svg?label=downloads&colorB=green&suffix=/day&query=$.downloads&uri=https://api.npmjs.org//downloads/point/last-day/eslint-plugin-markdown-preferences&maxAge=3600)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
8
- [![NPM downloads](https://img.shields.io/npm/dw/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
9
- [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
10
- [![NPM downloads](https://img.shields.io/npm/dy/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
11
- [![NPM downloads](https://img.shields.io/npm/dt/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
5
+ [![NPM license](https://img.shields.io/npm/l/eslint-plugin-markdown-preferences.svg)][npm-package]
6
+ [![NPM version](https://img.shields.io/npm/v/eslint-plugin-markdown-preferences.svg)][npm-package]
7
+ [![NPM downloads](https://img.shields.io/badge/dynamic/json.svg?label=downloads&colorB=green&suffix=/day&query=$.downloads&uri=https://api.npmjs.org//downloads/point/last-day/eslint-plugin-markdown-preferences&maxAge=3600)][npmtrends]
8
+ [![NPM downloads](https://img.shields.io/npm/dw/eslint-plugin-markdown-preferences.svg)][npmtrends]
9
+ [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown-preferences.svg)][npmtrends]
10
+ [![NPM downloads](https://img.shields.io/npm/dy/eslint-plugin-markdown-preferences.svg)][npmtrends]
11
+ [![NPM downloads](https://img.shields.io/npm/dt/eslint-plugin-markdown-preferences.svg)][npmtrends]
12
12
  [![Build Status](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/actions/workflows/NodeCI.yml/badge.svg?branch=main)](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/actions/workflows/NodeCI.yml)
13
13
 
14
14
  ## 📛 Features
15
15
 
16
- - **🔧 Auto-fixable rules** - Automatically format your Markdown files to match your style preferences
17
- - **📝 Line break consistency** - Enforce consistent hard line break styles (backslash `\` vs trailing spaces)
18
- - **🔗 Link enforcement** - Ensure specific words or terms are properly linked to their documentation
19
- - **🎯 Customizable** - Configure rules to match your team's specific requirements
16
+ - **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting, linking, and style consistency automatically
17
+ - **📖 Professional documentation** - Enforce consistent line breaks, clean up trailing spaces, and organize link definitions for enterprise-ready documentation
18
+ - **🎯 Smart terminology management** - Automatically convert specified words into inline code or clickable links based on your configuration
19
+ - **⚙️ Highly customizable configuration** - Fine-tune every aspect with granular rule options, word lists, ignore patterns, and flexible thresholds to match your exact requirements
20
20
 
21
21
  **Try it live:** Check out the [Online Demo](https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences) to see the plugin in action!
22
22
 
@@ -81,22 +81,29 @@ Is not supported.
81
81
 
82
82
  <!--RULES_SECTION_START-->
83
83
 
84
- The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) automatically fixes problems reported by rules which have a wrench 🔧 below.
84
+ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) automatically fixes problems reported by rules which have a wrench 🔧 below.\
85
85
  The rules with the following star ⭐ are included in the configs.
86
86
 
87
87
  <!--RULES_TABLE_START-->
88
88
 
89
- ### Markdown Rules
89
+ ### Preference Rules
90
90
 
91
91
  | Rule ID | Description | Fixable | RECOMMENDED |
92
92
  |:--------|:------------|:-------:|:-----------:|
93
- | [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
94
93
  | [markdown-preferences/no-text-backslash-linebreak](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html) | disallow text backslash at the end of a line. | | ⭐ |
95
- | [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | disallow trailing whitespace at the end of lines in Markdown files. | 🔧 | |
96
94
  | [markdown-preferences/prefer-inline-code-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html) | enforce the use of inline code for specific words. | 🔧 | |
97
- | [markdown-preferences/prefer-link-reference-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html) | enforce using link reference definitions instead of inline links | 🔧 | |
98
95
  | [markdown-preferences/prefer-linked-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html) | enforce the specified word to be a link. | 🔧 | |
99
96
 
97
+ ### Stylistic Rules
98
+
99
+ | Rule ID | Description | Fixable | RECOMMENDED |
100
+ |:--------|:------------|:-------:|:-----------:|
101
+ | [markdown-preferences/definitions-last](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html) | require link definitions and footnote definitions to be placed at the end of the document | 🔧 | |
102
+ | [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
103
+ | [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | disallow trailing whitespace at the end of lines in Markdown files. | 🔧 | |
104
+ | [markdown-preferences/prefer-link-reference-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html) | enforce using link reference definitions instead of inline links | 🔧 | |
105
+ | [markdown-preferences/sort-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html) | enforce a specific order for link definitions and footnote definitions | 🔧 | |
106
+
100
107
  <!--RULES_TABLE_END-->
101
108
  <!--RULES_SECTION_END-->
102
109
  <!--DOCS_IGNORE_START-->
@@ -109,9 +116,12 @@ Please use GitHub's Issues/PRs.
109
116
 
110
117
  ### Development Tools
111
118
 
112
- - `npm test` runs tests and measures coverage.
113
- - `npm run update` runs in order to update readme and recommended configuration.
119
+ - `npm test` runs tests and measures coverage.
120
+ - `npm run update` runs in order to update readme and recommended configuration.
114
121
 
115
122
  ## 🔒 License
116
123
 
117
124
  See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
125
+
126
+ [npm-package]: https://www.npmjs.com/package/eslint-plugin-markdown-preferences
127
+ [npmtrends]: http://www.npmtrends.com/eslint-plugin-markdown-preferences
package/lib/index.d.ts CHANGED
@@ -10,6 +10,11 @@ declare module 'eslint' {
10
10
  }
11
11
  }
12
12
  interface RuleOptions {
13
+ /**
14
+ * require link definitions and footnote definitions to be placed at the end of the document
15
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html
16
+ */
17
+ 'markdown-preferences/definitions-last'?: Linter.RuleEntry<[]>;
13
18
  /**
14
19
  * enforce consistent hard linebreak style.
15
20
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html
@@ -40,6 +45,11 @@ interface RuleOptions {
40
45
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
41
46
  */
42
47
  'markdown-preferences/prefer-linked-words'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkedWords>;
48
+ /**
49
+ * enforce a specific order for link definitions and footnote definitions
50
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
51
+ */
52
+ 'markdown-preferences/sort-definitions'?: Linter.RuleEntry<MarkdownPreferencesSortDefinitions>;
43
53
  }
44
54
  type MarkdownPreferencesHardLinebreakStyle = [] | [{
45
55
  style?: ("backslash" | "spaces");
@@ -75,6 +85,13 @@ type MarkdownPreferencesPreferLinkedWords = [] | [{
75
85
  }[];
76
86
  [k: string]: unknown | undefined;
77
87
  }];
88
+ type MarkdownPreferencesSortDefinitions = [] | [{
89
+ order?: (string | [string, ...(string)[]] | {
90
+ match: (string | [string, ...(string)[]]);
91
+ sort: ("alphabetical" | "ignore");
92
+ })[];
93
+ alphabetical?: boolean;
94
+ }];
78
95
  declare namespace recommended_d_exports {
79
96
  export { files, language, name$1 as name, plugins, rules$1 as rules };
80
97
  }
@@ -90,7 +107,7 @@ declare namespace meta_d_exports {
90
107
  export { name, version };
91
108
  }
92
109
  declare const name: "eslint-plugin-markdown-preferences";
93
- declare const version: "0.6.0";
110
+ declare const version: "0.8.0";
94
111
  //#endregion
95
112
  //#region src/index.d.ts
96
113
  declare const configs: {
package/lib/index.js CHANGED
@@ -25,6 +25,47 @@ function createRule(ruleName, rule) {
25
25
  };
26
26
  }
27
27
 
28
+ //#endregion
29
+ //#region src/rules/definitions-last.ts
30
+ var definitions_last_default = createRule("definitions-last", {
31
+ meta: {
32
+ type: "layout",
33
+ docs: {
34
+ description: "require link definitions and footnote definitions to be placed at the end of the document",
35
+ categories: [],
36
+ listCategory: "Stylistic"
37
+ },
38
+ fixable: "code",
39
+ hasSuggestions: false,
40
+ schema: [],
41
+ messages: {}
42
+ },
43
+ create(context) {
44
+ const sourceCode = context.sourceCode;
45
+ const lastNonDefinition = sourceCode.ast.children.findLast((node) => node.type !== "definition" && node.type !== "footnoteDefinition" && !(node.type === "html" && (node.value.startsWith("<!--") || node.value.startsWith("<script") || node.value.startsWith("<style"))));
46
+ if (!lastNonDefinition) return {};
47
+ const lastNonDefinitionRange = sourceCode.getRange(lastNonDefinition);
48
+ return { "definition, footnoteDefinition"(node) {
49
+ const range = sourceCode.getRange(node);
50
+ if (lastNonDefinitionRange[1] <= range[0]) return;
51
+ context.report({
52
+ node,
53
+ message: "Definition or footnote definition should be placed at the end of the document.",
54
+ *fix(fixer) {
55
+ let rangeStart = range[0];
56
+ for (let index = range[0] - 1; index >= 0; index--) {
57
+ const c = sourceCode.text[index];
58
+ if (c.trim()) break;
59
+ rangeStart = index;
60
+ }
61
+ yield fixer.removeRange([rangeStart, range[1]]);
62
+ yield fixer.insertTextAfterRange(lastNonDefinitionRange, sourceCode.text.slice(rangeStart, range[1]));
63
+ }
64
+ });
65
+ } };
66
+ }
67
+ });
68
+
28
69
  //#endregion
29
70
  //#region src/rules/hard-linebreak-style.ts
30
71
  var hard_linebreak_style_default = createRule("hard-linebreak-style", {
@@ -32,7 +73,8 @@ var hard_linebreak_style_default = createRule("hard-linebreak-style", {
32
73
  type: "layout",
33
74
  docs: {
34
75
  description: "enforce consistent hard linebreak style.",
35
- categories: ["recommended"]
76
+ categories: ["recommended"],
77
+ listCategory: "Stylistic"
36
78
  },
37
79
  fixable: "code",
38
80
  hasSuggestions: false,
@@ -76,7 +118,8 @@ var no_text_backslash_linebreak_default = createRule("no-text-backslash-linebrea
76
118
  type: "suggestion",
77
119
  docs: {
78
120
  description: "disallow text backslash at the end of a line.",
79
- categories: ["recommended"]
121
+ categories: ["recommended"],
122
+ listCategory: "Preference"
80
123
  },
81
124
  fixable: void 0,
82
125
  hasSuggestions: true,
@@ -132,7 +175,8 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
132
175
  type: "layout",
133
176
  docs: {
134
177
  description: "disallow trailing whitespace at the end of lines in Markdown files.",
135
- categories: []
178
+ categories: [],
179
+ listCategory: "Stylistic"
136
180
  },
137
181
  fixable: "whitespace",
138
182
  hasSuggestions: false,
@@ -340,7 +384,8 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
340
384
  type: "suggestion",
341
385
  docs: {
342
386
  description: "enforce the use of inline code for specific words.",
343
- categories: []
387
+ categories: [],
388
+ listCategory: "Preference"
344
389
  },
345
390
  fixable: "code",
346
391
  hasSuggestions: false,
@@ -410,7 +455,8 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
410
455
  type: "layout",
411
456
  docs: {
412
457
  description: "enforce using link reference definitions instead of inline links",
413
- categories: []
458
+ categories: [],
459
+ listCategory: "Stylistic"
414
460
  },
415
461
  fixable: "code",
416
462
  hasSuggestions: false,
@@ -432,20 +478,21 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
432
478
  const minLinks = options.minLinks ?? 2;
433
479
  const definitions = [];
434
480
  const links = [];
435
- const linkReferences = [];
481
+ const references = [];
482
+ const headings = [];
436
483
  /**
437
484
  * Verify links.
438
485
  */
439
486
  function verify() {
440
487
  const resourceToNodes = /* @__PURE__ */ new Map();
441
488
  for (const link of links) getResourceNodes(link).links.push(link);
442
- for (const linkReference of linkReferences) {
443
- const definition = definitions.find((def) => def.identifier === linkReference.identifier);
444
- if (definition) getResourceNodes(definition).linkReferences.push(linkReference);
489
+ for (const reference of references) {
490
+ const definition = definitions.find((def) => def.identifier === reference.identifier);
491
+ if (definition) getResourceNodes(definition).references.push(reference);
445
492
  }
446
493
  for (const definition of definitions) getResourceNodes(definition).definitions.push(definition);
447
494
  for (const map of resourceToNodes.values()) for (const nodes of map.values()) {
448
- if (nodes.links.length === 0 || nodes.links.length + nodes.linkReferences.length < minLinks) continue;
495
+ if (nodes.links.length === 0 || nodes.links.length + nodes.references.length < minLinks) continue;
449
496
  for (const link of nodes.links) {
450
497
  const linkInfo = getLinkInfo(link);
451
498
  if (linkInfo.label === "") continue;
@@ -456,15 +503,26 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
456
503
  const definition = nodes.definitions[0];
457
504
  let identifier;
458
505
  if (definition) identifier = definition.label ?? definition.identifier;
459
- else identifier = linkInfo.label.replaceAll(/\]/g, "-");
460
- yield fixer.replaceText(link, `${sourceCode.text.slice(...linkInfo.labelRange)}${identifier === linkInfo.label ? "" : `[${identifier}]`}`);
506
+ else {
507
+ identifier = linkInfo.label.replaceAll(/[[\]]/gu, "-");
508
+ if (definitions.some((def) => def.identifier === identifier)) {
509
+ let seq = 1;
510
+ const original = identifier;
511
+ identifier = `${original}-${seq}`;
512
+ while (definitions.some((def) => def.identifier === identifier)) identifier = `${original}-${++seq}`;
513
+ }
514
+ }
515
+ yield fixer.replaceText(link, `${sourceCode.text.slice(...linkInfo.bracketsRange)}${identifier === linkInfo.label ? "" : `[${identifier}]`}`);
461
516
  if (!definition) {
462
517
  const linkRange = sourceCode.getRange(link);
463
- const reLineBreaks = /\n#{1,6}\s/gu;
464
- reLineBreaks.lastIndex = linkRange[1];
465
- const nextSectionMatch = reLineBreaks.exec(sourceCode.text);
466
- const insertIndex = !nextSectionMatch ? sourceCode.text.trimEnd().length : nextSectionMatch.index;
467
- yield fixer.insertTextAfterRange([insertIndex, insertIndex], `${sourceCode.text[insertIndex - 1] === "\n" ? "" : "\n"}\n[${identifier}]: ${sourceCode.text.slice(linkInfo.urlAndTitleRange[0] + 1, linkInfo.urlAndTitleRange[1] - 1).trim()}${nextSectionMatch ? "\n" : ""}`);
518
+ const nextSectionHeading = headings.find((heading) => linkRange[1] < sourceCode.getRange(heading)[0]);
519
+ let insertIndex;
520
+ if (nextSectionHeading) {
521
+ const headingRange = sourceCode.getRange(nextSectionHeading);
522
+ const headingStartLoc = sourceCode.getLoc(nextSectionHeading).start;
523
+ insertIndex = headingRange[0] - headingStartLoc.column;
524
+ } else insertIndex = sourceCode.text.trimEnd().length;
525
+ yield fixer.insertTextAfterRange([insertIndex, insertIndex], `${sourceCode.text[insertIndex - 1] === "\n" ? "" : "\n"}\n[${identifier}]: ${sourceCode.text.slice(linkInfo.urlAndTitleRange[0] + 1, linkInfo.urlAndTitleRange[1] - 1).trim()}${nextSectionHeading ? "\n" : ""}`);
468
526
  }
469
527
  }
470
528
  });
@@ -485,7 +543,7 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
485
543
  if (!nodes) {
486
544
  nodes = {
487
545
  links: [],
488
- linkReferences: [],
546
+ references: [],
489
547
  definitions: []
490
548
  };
491
549
  map.set(title, nodes);
@@ -495,14 +553,20 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
495
553
  }
496
554
  return {
497
555
  link(node) {
556
+ if (sourceCode.getText(node).startsWith("[")) links.push(node);
557
+ },
558
+ image(node) {
498
559
  links.push(node);
499
560
  },
500
- linkReference(node) {
501
- linkReferences.push(node);
561
+ "linkReference, imageReference"(node) {
562
+ references.push(node);
502
563
  },
503
564
  definition(node) {
504
565
  definitions.push(node);
505
566
  },
567
+ heading(node) {
568
+ headings.push(node);
569
+ },
506
570
  "root:exit"() {
507
571
  verify();
508
572
  }
@@ -512,31 +576,70 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
512
576
  */
513
577
  function getLinkInfo(link) {
514
578
  const range = sourceCode.getRange(link);
515
- const linkLabelRange = getLinkLabelRange();
516
- const linkLabelWithBracketsText = sourceCode.text.slice(...linkLabelRange);
517
- const linkLabelText = linkLabelWithBracketsText.slice(1, -1).trim();
518
- const urlStartIndex = sourceCode.text.indexOf("(", linkLabelRange[1]);
579
+ if (link.type === "link") {
580
+ const bracketsRange$1 = getLinkBracketsRange(link);
581
+ const linkBracketsText$1 = sourceCode.text.slice(...bracketsRange$1);
582
+ const linkLabelText$1 = linkBracketsText$1.slice(1, -1).trim();
583
+ const urlStartIndex$1 = sourceCode.text.indexOf("(", bracketsRange$1[1]);
584
+ return {
585
+ label: linkLabelText$1,
586
+ bracketsRange: bracketsRange$1,
587
+ urlAndTitleRange: [urlStartIndex$1, range[1]]
588
+ };
589
+ }
590
+ const bracketsRange = getImageBracketsRange(link);
591
+ const linkBracketsText = sourceCode.text.slice(...bracketsRange);
592
+ const linkLabelText = linkBracketsText.slice(1, -1).trim();
593
+ const urlStartIndex = sourceCode.text.indexOf("(", bracketsRange[1]);
519
594
  return {
520
595
  label: linkLabelText,
521
- labelRange: linkLabelRange,
596
+ bracketsRange,
522
597
  urlAndTitleRange: [urlStartIndex, range[1]]
523
598
  };
524
- /**
525
- * Get the range of the link label.
526
- */
527
- function getLinkLabelRange() {
528
- if (link.children.length === 0) {
529
- const index$1 = sourceCode.text.indexOf("]", range[0] + 1);
530
- return [range[0], index$1 + 1];
531
- }
532
- const lastRange = sourceCode.getRange(link.children[link.children.length - 1]);
533
- const index = sourceCode.text.indexOf("]", lastRange[0] + 1);
534
- return [range[0], index + 1];
599
+ }
600
+ /**
601
+ * Get the range of the link label.
602
+ */
603
+ function getLinkBracketsRange(link) {
604
+ const range = sourceCode.getRange(link);
605
+ if (link.children.length === 0) {
606
+ const index$1 = sourceCode.text.indexOf("]", range[0] + 1);
607
+ return [range[0], index$1 + 1];
535
608
  }
609
+ const lastRange = sourceCode.getRange(link.children[link.children.length - 1]);
610
+ const index = sourceCode.text.indexOf("]", lastRange[1]);
611
+ return [range[0], index + 1];
612
+ }
613
+ /**
614
+ * Get the range of the image label.
615
+ */
616
+ function getImageBracketsRange(image) {
617
+ const range = sourceCode.getRange(image);
618
+ const index = sourceCode.text.indexOf("]", range[0] + 2);
619
+ return [range[0] + 1, index + 1];
536
620
  }
537
621
  }
538
622
  });
539
623
 
624
+ //#endregion
625
+ //#region src/utils/url.ts
626
+ /**
627
+ * Utility function to check if a string is a valid URL.
628
+ */
629
+ function isValidURL(url) {
630
+ return Boolean(createURLSafe(url));
631
+ }
632
+ /**
633
+ * Utility function to create a URL object safely.
634
+ */
635
+ function createURLSafe(url) {
636
+ try {
637
+ return new URL(url);
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+
540
643
  //#endregion
541
644
  //#region src/rules/prefer-linked-words.ts
542
645
  var prefer_linked_words_default = createRule("prefer-linked-words", {
@@ -544,7 +647,8 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
544
647
  type: "suggestion",
545
648
  docs: {
546
649
  description: "enforce the specified word to be a link.",
547
- categories: []
650
+ categories: [],
651
+ listCategory: "Preference"
548
652
  },
549
653
  fixable: "code",
550
654
  hasSuggestions: false,
@@ -637,7 +741,7 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
637
741
  * Adjust link to be relative to the file.
638
742
  */
639
743
  function adjustLink(link) {
640
- if (/^\w+:/.test(link)) return link;
744
+ if (isValidURL(link)) return link;
641
745
  if (link.startsWith("#")) return link;
642
746
  const absoluteLink = path.isAbsolute(link) || path.posix.isAbsolute(link) ? link : path.join(context.cwd, link);
643
747
  return `./${path.relative(path.dirname(context.filename), absoluteLink)}`;
@@ -645,15 +749,325 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
645
749
  }
646
750
  });
647
751
 
752
+ //#endregion
753
+ //#region src/utils/regexp.ts
754
+ const RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u;
755
+ /**
756
+ * Convert a string to the `RegExp`.
757
+ * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
758
+ * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
759
+ *
760
+ * @param {string} string The string to convert.
761
+ * @returns {RegExp} Returns the `RegExp`.
762
+ */
763
+ function toRegExp(string) {
764
+ const parts = RE_REGEXP_STR.exec(string);
765
+ if (parts) return new RegExp(parts[1], parts[2]);
766
+ return { test: (s) => s === string };
767
+ }
768
+ /**
769
+ * Checks whether given string is regexp string
770
+ * @param {string} string
771
+ * @returns {boolean}
772
+ */
773
+ function isRegExp(string) {
774
+ return Boolean(RE_REGEXP_STR.test(string));
775
+ }
776
+
777
+ //#endregion
778
+ //#region src/rules/sort-definitions.ts
779
+ var sort_definitions_default = createRule("sort-definitions", {
780
+ meta: {
781
+ type: "layout",
782
+ docs: {
783
+ description: "enforce a specific order for link definitions and footnote definitions",
784
+ categories: [],
785
+ listCategory: "Stylistic"
786
+ },
787
+ fixable: "code",
788
+ hasSuggestions: false,
789
+ schema: [{
790
+ type: "object",
791
+ properties: {
792
+ order: {
793
+ type: "array",
794
+ items: { anyOf: [
795
+ { type: "string" },
796
+ {
797
+ type: "array",
798
+ items: { type: "string" },
799
+ uniqueItems: true,
800
+ minItems: 1
801
+ },
802
+ {
803
+ type: "object",
804
+ properties: {
805
+ match: { anyOf: [{ type: "string" }, {
806
+ type: "array",
807
+ items: { type: "string" },
808
+ uniqueItems: true,
809
+ minItems: 1
810
+ }] },
811
+ sort: { enum: ["alphabetical", "ignore"] }
812
+ },
813
+ required: ["match", "sort"],
814
+ additionalProperties: false
815
+ }
816
+ ] },
817
+ uniqueItems: true,
818
+ additionalItems: false
819
+ },
820
+ alphabetical: { type: "boolean" }
821
+ },
822
+ additionalProperties: false
823
+ }],
824
+ messages: { shouldBefore: "The definition '{{currentKey}}' should be before '{{prevKey}}'." }
825
+ },
826
+ create(context) {
827
+ const sourceCode = context.sourceCode;
828
+ const option = parseOption(context.options[0]);
829
+ const group = [];
830
+ const cacheText = /* @__PURE__ */ new Map();
831
+ /** Get normalized text */
832
+ function getDefinitionText(node) {
833
+ const k = cacheText.get(node);
834
+ if (k != null) return k;
835
+ if (node.type === "definition") return `[${node.label || node.identifier}]: ${node.url}${printTitle(node.title)}`;
836
+ let childrenText = "";
837
+ if (node.children.length) {
838
+ const [start] = sourceCode.getRange(node.children[0]);
839
+ const [, end] = sourceCode.getRange(node.children.at(-1));
840
+ childrenText = sourceCode.text.slice(start, end);
841
+ }
842
+ return `[^${node.identifier}]: ${childrenText}`;
843
+ }
844
+ /** Report */
845
+ function report(node, previousNode, definitions) {
846
+ const currentKey = getDefinitionText(node);
847
+ const prevKey = getDefinitionText(previousNode);
848
+ context.report({
849
+ node,
850
+ messageId: "shouldBefore",
851
+ data: {
852
+ currentKey,
853
+ prevKey
854
+ },
855
+ fix(fixer) {
856
+ const previousNodeIndex = definitions.indexOf(previousNode);
857
+ const targetNodeIndex = definitions.indexOf(node);
858
+ const previousNodes = definitions.slice(previousNodeIndex, targetNodeIndex);
859
+ const before = definitions.slice(0, previousNodeIndex);
860
+ const after = definitions.slice(targetNodeIndex + 1);
861
+ const movedNodes = [
862
+ ...before,
863
+ node,
864
+ ...previousNodes,
865
+ ...after
866
+ ];
867
+ return movedNodes.map((moveNode, index) => {
868
+ let text = sourceCode.getText(moveNode);
869
+ if (moveNode.type === "definition" && index > 0) {
870
+ if (movedNodes[index - 1].type === "footnoteDefinition") {
871
+ const footnoteLoc = sourceCode.getLoc(definitions[index - 1]);
872
+ const linkLoc = sourceCode.getLoc(definitions[index]);
873
+ if (linkLoc.start.line - footnoteLoc.end.line <= 1) text = `\n${text}`;
874
+ }
875
+ }
876
+ return fixer.replaceText(definitions[index], text);
877
+ });
878
+ }
879
+ });
880
+ }
881
+ /**
882
+ * Verify definitions and footnote definitions.
883
+ */
884
+ function verify(definitions) {
885
+ if (definitions.length === 0) return;
886
+ const validPreviousNodes = [];
887
+ for (const definition of definitions) {
888
+ if (option.ignore(definition)) continue;
889
+ const invalidPreviousNode = validPreviousNodes.find((previousNode) => option.compare(previousNode, definition) > 0);
890
+ if (invalidPreviousNode) {
891
+ report(definition, invalidPreviousNode, definitions);
892
+ continue;
893
+ }
894
+ validPreviousNodes.push(definition);
895
+ }
896
+ }
897
+ return {
898
+ "*"(node) {
899
+ const last = group.at(-1);
900
+ if (last && (node.type !== "definition" && node.type !== "footnoteDefinition" || sourceCode.getParent(node) !== sourceCode.getParent(last))) {
901
+ const range = sourceCode.getRange(node);
902
+ const lastDefinitionRange = sourceCode.getRange(node);
903
+ if (lastDefinitionRange[1] <= range[0]) {
904
+ verify(group);
905
+ group.length = 0;
906
+ }
907
+ }
908
+ if (node.type === "definition" || node.type === "footnoteDefinition") group.push(node);
909
+ },
910
+ "root:exit"() {
911
+ verify(group);
912
+ }
913
+ };
914
+ /** Parse options */
915
+ function parseOption(userOption) {
916
+ const order = userOption?.order ?? [{
917
+ match: String.raw`!/^\[\\^/u`,
918
+ sort: "alphabetical"
919
+ }, {
920
+ match: String.raw`/./u`,
921
+ sort: "alphabetical"
922
+ }];
923
+ const compiled = order.map(compileOption);
924
+ return {
925
+ ignore: (node) => {
926
+ return !compiled.some((c) => c.match(node));
927
+ },
928
+ compare: (a, b) => {
929
+ for (const c of compiled) {
930
+ const matchA = c.match(a);
931
+ const matchB = c.match(b);
932
+ if (matchA && matchB) {
933
+ if (c.sort === "alphabetical") {
934
+ const textA = getDefinitionText(a);
935
+ const textB = getDefinitionText(b);
936
+ if (textA === textB) return 0;
937
+ return textA < textB ? -1 : 1;
938
+ }
939
+ return 0;
940
+ }
941
+ if (matchA) return -1;
942
+ if (matchB) return 1;
943
+ }
944
+ throw new Error("Illegal state");
945
+ }
946
+ };
947
+ }
948
+ /** Compile order option */
949
+ function compileOption(orderOption) {
950
+ const cache = /* @__PURE__ */ new Map();
951
+ const compiled = compileOptionWithoutCache(orderOption);
952
+ return {
953
+ match: (node) => {
954
+ const cached = cache.get(node);
955
+ if (cached != null) return cached;
956
+ const result = compiled.match(node);
957
+ cache.set(node, result);
958
+ return result;
959
+ },
960
+ sort: compiled.sort
961
+ };
962
+ }
963
+ /** Compile order option without cache */
964
+ function compileOptionWithoutCache(orderOption) {
965
+ if (typeof orderOption === "string") {
966
+ const match$1 = compileMatcher([orderOption]);
967
+ return {
968
+ match: match$1,
969
+ sort: "ignore"
970
+ };
971
+ }
972
+ if (Array.isArray(orderOption)) {
973
+ const match$1 = compileMatcher(orderOption);
974
+ return {
975
+ match: match$1,
976
+ sort: "ignore"
977
+ };
978
+ }
979
+ const { match } = compileOptionWithoutCache(orderOption.match);
980
+ return {
981
+ match,
982
+ sort: orderOption.sort || "ignore"
983
+ };
984
+ }
985
+ /** Compile matcher */
986
+ function compileMatcher(pattern) {
987
+ const rules$3 = [];
988
+ for (const p of pattern) {
989
+ let negative, patternStr;
990
+ if (p.startsWith("!")) {
991
+ negative = true;
992
+ patternStr = p.substring(1);
993
+ } else {
994
+ negative = false;
995
+ patternStr = p;
996
+ }
997
+ const regex = toRegExp(patternStr);
998
+ if (isRegExp(patternStr)) rules$3.push({
999
+ negative,
1000
+ match: (node) => regex.test(getDefinitionText(node))
1001
+ });
1002
+ else rules$3.push({
1003
+ negative,
1004
+ match: (node) => {
1005
+ if (node.label === patternStr || node.identifier === patternStr) return true;
1006
+ if (node.type === "definition") {
1007
+ if (node.url === patternStr) return true;
1008
+ if (isValidURL(patternStr)) {
1009
+ const normalizedPattern = normalizedURL(patternStr);
1010
+ const normalizedUrl = normalizedURL(node.url);
1011
+ if (normalizedUrl.startsWith(normalizedPattern)) return true;
1012
+ }
1013
+ }
1014
+ return regex.test(getDefinitionText(node));
1015
+ }
1016
+ });
1017
+ }
1018
+ return (node) => {
1019
+ let result = Boolean(rules$3[0]?.negative);
1020
+ for (const { negative, match } of rules$3) {
1021
+ if (result === !negative) continue;
1022
+ if (match(node)) result = !negative;
1023
+ }
1024
+ return result;
1025
+ };
1026
+ }
1027
+ }
1028
+ });
1029
+ /**
1030
+ * Print the title with quotes.
1031
+ */
1032
+ function printTitle(title) {
1033
+ if (!title) return "";
1034
+ let titleToPrint = title.replaceAll(/\\(?=["')])/gu, "");
1035
+ if (titleToPrint.includes("\"") && titleToPrint.includes("'") && !titleToPrint.includes(")")) return ` (${titleToPrint})`;
1036
+ const quote = getQuote(titleToPrint);
1037
+ titleToPrint = titleToPrint.replaceAll("\\", "\\\\");
1038
+ titleToPrint = titleToPrint.replaceAll(quote, `\\${quote}`);
1039
+ return ` ${quote}${titleToPrint}${quote}`;
1040
+ }
1041
+ /**
1042
+ * Get the preferred quote for a string.
1043
+ */
1044
+ function getQuote(text) {
1045
+ let doubleQuoteCount = 0;
1046
+ let singleQuoteCount = 0;
1047
+ for (const character of text) if (character === "\"") doubleQuoteCount++;
1048
+ else if (character === "'") singleQuoteCount++;
1049
+ return doubleQuoteCount > singleQuoteCount ? "'" : "\"";
1050
+ }
1051
+ /**
1052
+ * Normalize a URL by ensuring it ends with a slash.
1053
+ */
1054
+ function normalizedURL(url) {
1055
+ const urlObj = createURLSafe(url);
1056
+ if (!urlObj) return url;
1057
+ return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
1058
+ }
1059
+
648
1060
  //#endregion
649
1061
  //#region src/utils/rules.ts
650
1062
  const rules$1 = [
1063
+ definitions_last_default,
651
1064
  hard_linebreak_style_default,
652
1065
  no_text_backslash_linebreak_default,
653
1066
  no_trailing_spaces_default,
654
1067
  prefer_inline_code_words_default,
655
1068
  prefer_link_reference_definitions_default,
656
- prefer_linked_words_default
1069
+ prefer_linked_words_default,
1070
+ sort_definitions_default
657
1071
  ];
658
1072
 
659
1073
  //#endregion
@@ -667,7 +1081,7 @@ __export(recommended_exports, {
667
1081
  rules: () => rules$2
668
1082
  });
669
1083
  const name$1 = "markdown-preferences/recommended";
670
- const files = ["**/*.md"];
1084
+ const files = ["*.md", "**/*.md"];
671
1085
  const language = "markdown/commonmark";
672
1086
  const plugins = {
673
1087
  markdown,
@@ -688,7 +1102,7 @@ __export(meta_exports, {
688
1102
  version: () => version
689
1103
  });
690
1104
  const name = "eslint-plugin-markdown-preferences";
691
- const version = "0.6.0";
1105
+ const version = "0.8.0";
692
1106
 
693
1107
  //#endregion
694
1108
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,7 +64,7 @@
64
64
  "@changesets/get-release-plan": "^4.0.8",
65
65
  "@eslint/core": "^0.15.0",
66
66
  "@eslint/markdown": "^7.1.0",
67
- "@ota-meshi/eslint-plugin": "^0.17.6",
67
+ "@ota-meshi/eslint-plugin": "^0.18.0",
68
68
  "@shikijs/vitepress-twoslash": "^3.0.0",
69
69
  "@types/eslint": "^9.6.1",
70
70
  "@types/eslint-scope": "^8.0.0",
@@ -82,7 +82,7 @@
82
82
  "eslint-compat-utils": "^0.6.4",
83
83
  "eslint-config-prettier": "^10.1.1",
84
84
  "eslint-plugin-eslint-comments": "^3.2.0",
85
- "eslint-plugin-eslint-plugin": "^6.4.0",
85
+ "eslint-plugin-eslint-plugin": "^7.0.0",
86
86
  "eslint-plugin-jsdoc": "^52.0.0",
87
87
  "eslint-plugin-json-schema-validator": "^5.3.1",
88
88
  "eslint-plugin-jsonc": "^2.19.1",