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 +14 -8
- package/lib/index.d.ts +13 -1
- package/lib/index.js +402 -36
- package/package.json +1 -1
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
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
-
###
|
|
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/
|
|
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.
|
|
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
|
|
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
|
|
484
|
-
const definition = definitions.find((def) => def.identifier ===
|
|
485
|
-
if (definition) getResourceNodes(definition).
|
|
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.
|
|
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
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
596
|
+
bracketsRange,
|
|
569
597
|
urlAndTitleRange: [urlStartIndex, range[1]]
|
|
570
598
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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 (
|
|
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.
|
|
1105
|
+
const version = "0.8.0";
|
|
740
1106
|
|
|
741
1107
|
//#endregion
|
|
742
1108
|
//#region src/index.ts
|