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 +28 -18
- package/lib/index.d.ts +18 -1
- package/lib/index.js +455 -41
- package/package.json +3 -3
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
|
-
[]
|
|
6
|
-
[]
|
|
7
|
-
[]
|
|
8
|
-
[]
|
|
9
|
-
[]
|
|
10
|
-
[]
|
|
11
|
-
[]
|
|
5
|
+
[][npm-package]
|
|
6
|
+
[][npm-package]
|
|
7
|
+
[][npmtrends]
|
|
8
|
+
[][npmtrends]
|
|
9
|
+
[][npmtrends]
|
|
10
|
+
[][npmtrends]
|
|
11
|
+
[][npmtrends]
|
|
12
12
|
[](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/actions/workflows/NodeCI.yml)
|
|
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
|
|
|
@@ -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
|
-
###
|
|
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.
|
|
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
|
|
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
|
|
443
|
-
const definition = definitions.find((def) => def.identifier ===
|
|
444
|
-
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);
|
|
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.
|
|
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
|
|
460
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
596
|
+
bracketsRange,
|
|
522
597
|
urlAndTitleRange: [urlStartIndex, range[1]]
|
|
523
598
|
};
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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",
|