eslint-plugin-markdown-preferences 0.7.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
@@ -13,10 +13,10 @@ A specialized ESLint plugin that helps enforce consistent writing style and form
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
 
@@ -86,17 +86,23 @@ 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
+
91
+ | Rule ID | Description | Fixable | RECOMMENDED |
92
+ |:--------|:------------|:-------:|:-----------:|
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. | | ⭐ |
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. | 🔧 | |
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. | 🔧 | |
96
+
97
+ ### Stylistic Rules
90
98
 
91
99
  | Rule ID | Description | Fixable | RECOMMENDED |
92
100
  |:--------|:------------|:-------:|:-----------:|
93
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 | 🔧 | |
94
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. | 🔧 | ⭐ |
95
- | [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. | | ⭐ |
96
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. | 🔧 | |
97
- | [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. | 🔧 | |
98
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 | 🔧 | |
99
- | [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. | 🔧 | |
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 | 🔧 | |
100
106
 
101
107
  <!--RULES_TABLE_END-->
102
108
  <!--RULES_SECTION_END-->
package/lib/index.d.ts CHANGED
@@ -45,6 +45,11 @@ interface RuleOptions {
45
45
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
46
46
  */
47
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>;
48
53
  }
49
54
  type MarkdownPreferencesHardLinebreakStyle = [] | [{
50
55
  style?: ("backslash" | "spaces");
@@ -80,6 +85,13 @@ type MarkdownPreferencesPreferLinkedWords = [] | [{
80
85
  }[];
81
86
  [k: string]: unknown | undefined;
82
87
  }];
88
+ type MarkdownPreferencesSortDefinitions = [] | [{
89
+ order?: (string | [string, ...(string)[]] | {
90
+ match: (string | [string, ...(string)[]]);
91
+ sort: ("alphabetical" | "ignore");
92
+ })[];
93
+ alphabetical?: boolean;
94
+ }];
83
95
  declare namespace recommended_d_exports {
84
96
  export { files, language, name$1 as name, plugins, rules$1 as rules };
85
97
  }
@@ -95,7 +107,7 @@ declare namespace meta_d_exports {
95
107
  export { name, version };
96
108
  }
97
109
  declare const name: "eslint-plugin-markdown-preferences";
98
- declare const version: "0.7.0";
110
+ declare const version: "0.8.0";
99
111
  //#endregion
100
112
  //#region src/index.d.ts
101
113
  declare const configs: {
package/lib/index.js CHANGED
@@ -32,7 +32,8 @@ var definitions_last_default = createRule("definitions-last", {
32
32
  type: "layout",
33
33
  docs: {
34
34
  description: "require link definitions and footnote definitions to be placed at the end of the document",
35
- categories: []
35
+ categories: [],
36
+ listCategory: "Stylistic"
36
37
  },
37
38
  fixable: "code",
38
39
  hasSuggestions: false,
@@ -72,7 +73,8 @@ var hard_linebreak_style_default = createRule("hard-linebreak-style", {
72
73
  type: "layout",
73
74
  docs: {
74
75
  description: "enforce consistent hard linebreak style.",
75
- categories: ["recommended"]
76
+ categories: ["recommended"],
77
+ listCategory: "Stylistic"
76
78
  },
77
79
  fixable: "code",
78
80
  hasSuggestions: false,
@@ -116,7 +118,8 @@ var no_text_backslash_linebreak_default = createRule("no-text-backslash-linebrea
116
118
  type: "suggestion",
117
119
  docs: {
118
120
  description: "disallow text backslash at the end of a line.",
119
- categories: ["recommended"]
121
+ categories: ["recommended"],
122
+ listCategory: "Preference"
120
123
  },
121
124
  fixable: void 0,
122
125
  hasSuggestions: true,
@@ -172,7 +175,8 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
172
175
  type: "layout",
173
176
  docs: {
174
177
  description: "disallow trailing whitespace at the end of lines in Markdown files.",
175
- categories: []
178
+ categories: [],
179
+ listCategory: "Stylistic"
176
180
  },
177
181
  fixable: "whitespace",
178
182
  hasSuggestions: false,
@@ -380,7 +384,8 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
380
384
  type: "suggestion",
381
385
  docs: {
382
386
  description: "enforce the use of inline code for specific words.",
383
- categories: []
387
+ categories: [],
388
+ listCategory: "Preference"
384
389
  },
385
390
  fixable: "code",
386
391
  hasSuggestions: false,
@@ -450,7 +455,8 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
450
455
  type: "layout",
451
456
  docs: {
452
457
  description: "enforce using link reference definitions instead of inline links",
453
- categories: []
458
+ categories: [],
459
+ listCategory: "Stylistic"
454
460
  },
455
461
  fixable: "code",
456
462
  hasSuggestions: false,
@@ -472,7 +478,7 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
472
478
  const minLinks = options.minLinks ?? 2;
473
479
  const definitions = [];
474
480
  const links = [];
475
- const linkReferences = [];
481
+ const references = [];
476
482
  const headings = [];
477
483
  /**
478
484
  * Verify links.
@@ -480,13 +486,13 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
480
486
  function verify() {
481
487
  const resourceToNodes = /* @__PURE__ */ new Map();
482
488
  for (const link of links) getResourceNodes(link).links.push(link);
483
- for (const linkReference of linkReferences) {
484
- const definition = definitions.find((def) => def.identifier === linkReference.identifier);
485
- 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);
486
492
  }
487
493
  for (const definition of definitions) getResourceNodes(definition).definitions.push(definition);
488
494
  for (const map of resourceToNodes.values()) for (const nodes of map.values()) {
489
- 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;
490
496
  for (const link of nodes.links) {
491
497
  const linkInfo = getLinkInfo(link);
492
498
  if (linkInfo.label === "") continue;
@@ -497,8 +503,16 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
497
503
  const definition = nodes.definitions[0];
498
504
  let identifier;
499
505
  if (definition) identifier = definition.label ?? definition.identifier;
500
- else identifier = linkInfo.label.replaceAll(/[[\]]/gu, "-");
501
- 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}]`}`);
502
516
  if (!definition) {
503
517
  const linkRange = sourceCode.getRange(link);
504
518
  const nextSectionHeading = headings.find((heading) => linkRange[1] < sourceCode.getRange(heading)[0]);
@@ -529,7 +543,7 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
529
543
  if (!nodes) {
530
544
  nodes = {
531
545
  links: [],
532
- linkReferences: [],
546
+ references: [],
533
547
  definitions: []
534
548
  };
535
549
  map.set(title, nodes);
@@ -539,10 +553,13 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
539
553
  }
540
554
  return {
541
555
  link(node) {
556
+ if (sourceCode.getText(node).startsWith("[")) links.push(node);
557
+ },
558
+ image(node) {
542
559
  links.push(node);
543
560
  },
544
- linkReference(node) {
545
- linkReferences.push(node);
561
+ "linkReference, imageReference"(node) {
562
+ references.push(node);
546
563
  },
547
564
  definition(node) {
548
565
  definitions.push(node);
@@ -559,31 +576,70 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
559
576
  */
560
577
  function getLinkInfo(link) {
561
578
  const range = sourceCode.getRange(link);
562
- const linkLabelRange = getLinkLabelRange();
563
- const linkLabelWithBracketsText = sourceCode.text.slice(...linkLabelRange);
564
- const linkLabelText = linkLabelWithBracketsText.slice(1, -1).trim();
565
- 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]);
566
594
  return {
567
595
  label: linkLabelText,
568
- labelRange: linkLabelRange,
596
+ bracketsRange,
569
597
  urlAndTitleRange: [urlStartIndex, range[1]]
570
598
  };
571
- /**
572
- * Get the range of the link label.
573
- */
574
- function getLinkLabelRange() {
575
- if (link.children.length === 0) {
576
- const index$1 = sourceCode.text.indexOf("]", range[0] + 1);
577
- return [range[0], index$1 + 1];
578
- }
579
- const lastRange = sourceCode.getRange(link.children[link.children.length - 1]);
580
- const index = sourceCode.text.indexOf("]", lastRange[1]);
581
- 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];
582
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];
583
620
  }
584
621
  }
585
622
  });
586
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
+
587
643
  //#endregion
588
644
  //#region src/rules/prefer-linked-words.ts
589
645
  var prefer_linked_words_default = createRule("prefer-linked-words", {
@@ -591,7 +647,8 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
591
647
  type: "suggestion",
592
648
  docs: {
593
649
  description: "enforce the specified word to be a link.",
594
- categories: []
650
+ categories: [],
651
+ listCategory: "Preference"
595
652
  },
596
653
  fixable: "code",
597
654
  hasSuggestions: false,
@@ -684,7 +741,7 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
684
741
  * Adjust link to be relative to the file.
685
742
  */
686
743
  function adjustLink(link) {
687
- if (/^\w+:/.test(link)) return link;
744
+ if (isValidURL(link)) return link;
688
745
  if (link.startsWith("#")) return link;
689
746
  const absoluteLink = path.isAbsolute(link) || path.posix.isAbsolute(link) ? link : path.join(context.cwd, link);
690
747
  return `./${path.relative(path.dirname(context.filename), absoluteLink)}`;
@@ -692,6 +749,314 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
692
749
  }
693
750
  });
694
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
+
695
1060
  //#endregion
696
1061
  //#region src/utils/rules.ts
697
1062
  const rules$1 = [
@@ -701,7 +1066,8 @@ const rules$1 = [
701
1066
  no_trailing_spaces_default,
702
1067
  prefer_inline_code_words_default,
703
1068
  prefer_link_reference_definitions_default,
704
- prefer_linked_words_default
1069
+ prefer_linked_words_default,
1070
+ sort_definitions_default
705
1071
  ];
706
1072
 
707
1073
  //#endregion
@@ -736,7 +1102,7 @@ __export(meta_exports, {
736
1102
  version: () => version
737
1103
  });
738
1104
  const name = "eslint-plugin-markdown-preferences";
739
- const version = "0.7.0";
1105
+ const version = "0.8.0";
740
1106
 
741
1107
  //#endregion
742
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.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {