eslint-plugin-markdown-preferences 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  A specialized ESLint plugin that helps enforce consistent writing style and formatting conventions in Markdown files. Perfect for documentation projects, blog posts, and any Markdown content where consistency matters.
4
4
 
5
- [![NPM license](https://img.shields.io/npm/l/eslint-plugin-markdown-preferences.svg)](https://www.npmjs.com/package/eslint-plugin-markdown-preferences)
6
- [![NPM version](https://img.shields.io/npm/v/eslint-plugin-markdown-preferences.svg)](https://www.npmjs.com/package/eslint-plugin-markdown-preferences)
7
- [![NPM downloads](https://img.shields.io/badge/dynamic/json.svg?label=downloads&colorB=green&suffix=/day&query=$.downloads&uri=https://api.npmjs.org//downloads/point/last-day/eslint-plugin-markdown-preferences&maxAge=3600)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
8
- [![NPM downloads](https://img.shields.io/npm/dw/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
9
- [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
10
- [![NPM downloads](https://img.shields.io/npm/dy/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
11
- [![NPM downloads](https://img.shields.io/npm/dt/eslint-plugin-markdown-preferences.svg)](http://www.npmtrends.com/eslint-plugin-markdown-preferences)
5
+ [![NPM license](https://img.shields.io/npm/l/eslint-plugin-markdown-preferences.svg)][npm-package]
6
+ [![NPM version](https://img.shields.io/npm/v/eslint-plugin-markdown-preferences.svg)][npm-package]
7
+ [![NPM downloads](https://img.shields.io/badge/dynamic/json.svg?label=downloads&colorB=green&suffix=/day&query=$.downloads&uri=https://api.npmjs.org//downloads/point/last-day/eslint-plugin-markdown-preferences&maxAge=3600)][npmtrends]
8
+ [![NPM downloads](https://img.shields.io/npm/dw/eslint-plugin-markdown-preferences.svg)][npmtrends]
9
+ [![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-markdown-preferences.svg)][npmtrends]
10
+ [![NPM downloads](https://img.shields.io/npm/dy/eslint-plugin-markdown-preferences.svg)][npmtrends]
11
+ [![NPM downloads](https://img.shields.io/npm/dt/eslint-plugin-markdown-preferences.svg)][npmtrends]
12
12
  [![Build Status](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/actions/workflows/NodeCI.yml/badge.svg?branch=main)](https://github.com/ota-meshi/eslint-plugin-markdown-preferences/actions/workflows/NodeCI.yml)
13
13
 
14
14
  ## 📛 Features
@@ -81,7 +81,7 @@ 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-->
@@ -90,10 +90,12 @@ The rules with the following star ⭐ are included in the configs.
90
90
 
91
91
  | Rule ID | Description | Fixable | RECOMMENDED |
92
92
  |:--------|:------------|:-------:|:-----------:|
93
+ | [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 | 🔧 | |
93
94
  | [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
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. | | ⭐ |
95
96
  | [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
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
+ | [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 | 🔧 | |
97
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. | 🔧 | |
98
100
 
99
101
  <!--RULES_TABLE_END-->
@@ -108,9 +110,12 @@ Please use GitHub's Issues/PRs.
108
110
 
109
111
  ### Development Tools
110
112
 
111
- - `npm test` runs tests and measures coverage.
112
- - `npm run update` runs in order to update readme and recommended configuration.
113
+ - `npm test` runs tests and measures coverage.
114
+ - `npm run update` runs in order to update readme and recommended configuration.
113
115
 
114
116
  ## 🔒 License
115
117
 
116
118
  See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
119
+
120
+ [npm-package]: https://www.npmjs.com/package/eslint-plugin-markdown-preferences
121
+ [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
@@ -30,6 +35,11 @@ interface RuleOptions {
30
35
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html
31
36
  */
32
37
  'markdown-preferences/prefer-inline-code-words'?: Linter.RuleEntry<MarkdownPreferencesPreferInlineCodeWords>;
38
+ /**
39
+ * enforce using link reference definitions instead of inline links
40
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html
41
+ */
42
+ 'markdown-preferences/prefer-link-reference-definitions'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkReferenceDefinitions>;
33
43
  /**
34
44
  * enforce the specified word to be a link.
35
45
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
@@ -54,6 +64,9 @@ type MarkdownPreferencesPreferInlineCodeWords = [] | [{
54
64
  }[];
55
65
  [k: string]: unknown | undefined;
56
66
  }];
67
+ type MarkdownPreferencesPreferLinkReferenceDefinitions = [] | [{
68
+ minLinks?: number;
69
+ }];
57
70
  type MarkdownPreferencesPreferLinkedWords = [] | [{
58
71
  words: ({
59
72
  [k: string]: (string | null);
@@ -82,7 +95,7 @@ declare namespace meta_d_exports {
82
95
  export { name, version };
83
96
  }
84
97
  declare const name: "eslint-plugin-markdown-preferences";
85
- declare const version: "0.5.0";
98
+ declare const version: "0.7.0";
86
99
  //#endregion
87
100
  //#region src/index.d.ts
88
101
  declare const configs: {
package/lib/index.js CHANGED
@@ -25,6 +25,46 @@ 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
+ },
37
+ fixable: "code",
38
+ hasSuggestions: false,
39
+ schema: [],
40
+ messages: {}
41
+ },
42
+ create(context) {
43
+ const sourceCode = context.sourceCode;
44
+ 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"))));
45
+ if (!lastNonDefinition) return {};
46
+ const lastNonDefinitionRange = sourceCode.getRange(lastNonDefinition);
47
+ return { "definition, footnoteDefinition"(node) {
48
+ const range = sourceCode.getRange(node);
49
+ if (lastNonDefinitionRange[1] <= range[0]) return;
50
+ context.report({
51
+ node,
52
+ message: "Definition or footnote definition should be placed at the end of the document.",
53
+ *fix(fixer) {
54
+ let rangeStart = range[0];
55
+ for (let index = range[0] - 1; index >= 0; index--) {
56
+ const c = sourceCode.text[index];
57
+ if (c.trim()) break;
58
+ rangeStart = index;
59
+ }
60
+ yield fixer.removeRange([rangeStart, range[1]]);
61
+ yield fixer.insertTextAfterRange(lastNonDefinitionRange, sourceCode.text.slice(rangeStart, range[1]));
62
+ }
63
+ });
64
+ } };
65
+ }
66
+ });
67
+
28
68
  //#endregion
29
69
  //#region src/rules/hard-linebreak-style.ts
30
70
  var hard_linebreak_style_default = createRule("hard-linebreak-style", {
@@ -245,7 +285,7 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
245
285
 
246
286
  //#endregion
247
287
  //#region src/utils/search-words.ts
248
- const RE_BOUNDARY = /^[\s\p{Letter_Number}\p{Modifier_Letter}\p{Modifier_Symbol}\p{Nonspacing_Mark}\p{Other_Letter}\p{Other_Symbol}\p{Script=Han}!"#$%&'(),./:;<=>?\\{|}~\u{2ffc}-\u{303d}\u{30a0}-\u{30fb}\u{3192}-\u{32bf}\u{fe10}-\u{fe1f}\u{fe30}-\u{fe6f}\u{ff00}-\u{ffef}\u{2ebf0}-\u{2ee5d}]*$/u;
288
+ const RE_BOUNDARY = /^[\s\p{Letter_Number}\p{Modifier_Letter}\p{Modifier_Symbol}\p{Nonspacing_Mark}\p{Other_Letter}\p{Other_Symbol}\p{Script=Han}!"#$%&'()*+,./:;<=>?\\{|}~\u{2ffc}-\u{303d}\u{30a0}-\u{30fb}\u{3192}-\u{32bf}\u{fe10}-\u{fe1f}\u{fe30}-\u{fe6f}\u{ff00}-\u{ffef}\u{2ebf0}-\u{2ee5d}]*$/u;
249
289
  /**
250
290
  * Iterate through words in a text node that match the specified words.
251
291
  */
@@ -403,6 +443,147 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
403
443
  }
404
444
  });
405
445
 
446
+ //#endregion
447
+ //#region src/rules/prefer-link-reference-definitions.ts
448
+ var prefer_link_reference_definitions_default = createRule("prefer-link-reference-definitions", {
449
+ meta: {
450
+ type: "layout",
451
+ docs: {
452
+ description: "enforce using link reference definitions instead of inline links",
453
+ categories: []
454
+ },
455
+ fixable: "code",
456
+ hasSuggestions: false,
457
+ schema: [{
458
+ type: "object",
459
+ properties: { minLinks: {
460
+ type: "number",
461
+ description: "minimum number of links to trigger the rule (default: 2)",
462
+ default: 2,
463
+ minimum: 1
464
+ } },
465
+ additionalProperties: false
466
+ }],
467
+ messages: { useLinkReferenceDefinitions: "Use link reference definitions instead of inline links." }
468
+ },
469
+ create(context) {
470
+ const sourceCode = context.sourceCode;
471
+ const options = context.options[0] || {};
472
+ const minLinks = options.minLinks ?? 2;
473
+ const definitions = [];
474
+ const links = [];
475
+ const linkReferences = [];
476
+ const headings = [];
477
+ /**
478
+ * Verify links.
479
+ */
480
+ function verify() {
481
+ const resourceToNodes = /* @__PURE__ */ new Map();
482
+ 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);
486
+ }
487
+ for (const definition of definitions) getResourceNodes(definition).definitions.push(definition);
488
+ 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;
490
+ for (const link of nodes.links) {
491
+ const linkInfo = getLinkInfo(link);
492
+ if (linkInfo.label === "") continue;
493
+ context.report({
494
+ node: link,
495
+ messageId: "useLinkReferenceDefinitions",
496
+ *fix(fixer) {
497
+ const definition = nodes.definitions[0];
498
+ let identifier;
499
+ 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}]`}`);
502
+ if (!definition) {
503
+ const linkRange = sourceCode.getRange(link);
504
+ const nextSectionHeading = headings.find((heading) => linkRange[1] < sourceCode.getRange(heading)[0]);
505
+ let insertIndex;
506
+ if (nextSectionHeading) {
507
+ const headingRange = sourceCode.getRange(nextSectionHeading);
508
+ const headingStartLoc = sourceCode.getLoc(nextSectionHeading).start;
509
+ insertIndex = headingRange[0] - headingStartLoc.column;
510
+ } else insertIndex = sourceCode.text.trimEnd().length;
511
+ 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" : ""}`);
512
+ }
513
+ }
514
+ });
515
+ }
516
+ }
517
+ /**
518
+ * Get the resource nodes for a link or definition.
519
+ */
520
+ function getResourceNodes(resource) {
521
+ const url = resource.url;
522
+ const title = resource.title ?? null;
523
+ let map = resourceToNodes.get(url);
524
+ if (!map) {
525
+ map = /* @__PURE__ */ new Map();
526
+ resourceToNodes.set(url, map);
527
+ }
528
+ let nodes = map.get(title);
529
+ if (!nodes) {
530
+ nodes = {
531
+ links: [],
532
+ linkReferences: [],
533
+ definitions: []
534
+ };
535
+ map.set(title, nodes);
536
+ }
537
+ return nodes;
538
+ }
539
+ }
540
+ return {
541
+ link(node) {
542
+ links.push(node);
543
+ },
544
+ linkReference(node) {
545
+ linkReferences.push(node);
546
+ },
547
+ definition(node) {
548
+ definitions.push(node);
549
+ },
550
+ heading(node) {
551
+ headings.push(node);
552
+ },
553
+ "root:exit"() {
554
+ verify();
555
+ }
556
+ };
557
+ /**
558
+ * Get the range of the link label.
559
+ */
560
+ function getLinkInfo(link) {
561
+ 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]);
566
+ return {
567
+ label: linkLabelText,
568
+ labelRange: linkLabelRange,
569
+ urlAndTitleRange: [urlStartIndex, range[1]]
570
+ };
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];
582
+ }
583
+ }
584
+ }
585
+ });
586
+
406
587
  //#endregion
407
588
  //#region src/rules/prefer-linked-words.ts
408
589
  var prefer_linked_words_default = createRule("prefer-linked-words", {
@@ -514,10 +695,12 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
514
695
  //#endregion
515
696
  //#region src/utils/rules.ts
516
697
  const rules$1 = [
698
+ definitions_last_default,
517
699
  hard_linebreak_style_default,
518
700
  no_text_backslash_linebreak_default,
519
701
  no_trailing_spaces_default,
520
702
  prefer_inline_code_words_default,
703
+ prefer_link_reference_definitions_default,
521
704
  prefer_linked_words_default
522
705
  ];
523
706
 
@@ -532,7 +715,7 @@ __export(recommended_exports, {
532
715
  rules: () => rules$2
533
716
  });
534
717
  const name$1 = "markdown-preferences/recommended";
535
- const files = ["**/*.md"];
718
+ const files = ["*.md", "**/*.md"];
536
719
  const language = "markdown/commonmark";
537
720
  const plugins = {
538
721
  markdown,
@@ -553,7 +736,7 @@ __export(meta_exports, {
553
736
  version: () => version
554
737
  });
555
738
  const name = "eslint-plugin-markdown-preferences";
556
- const version = "0.5.0";
739
+ const version = "0.7.0";
557
740
 
558
741
  //#endregion
559
742
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,13 +64,14 @@
64
64
  "@changesets/get-release-plan": "^4.0.8",
65
65
  "@eslint/core": "^0.15.0",
66
66
  "@eslint/markdown": "^7.1.0",
67
- "@ota-meshi/eslint-plugin": "^0.17.6",
67
+ "@ota-meshi/eslint-plugin": "^0.18.0",
68
68
  "@shikijs/vitepress-twoslash": "^3.0.0",
69
69
  "@types/eslint": "^9.6.1",
70
70
  "@types/eslint-scope": "^8.0.0",
71
71
  "@types/eslint-utils": "^3.0.5",
72
72
  "@types/estree": "^1.0.6",
73
73
  "@types/json-schema": "^7.0.15",
74
+ "@types/mdast": "^4.0.4",
74
75
  "@types/mocha": "^10.0.10",
75
76
  "@types/node": "^22.13.10",
76
77
  "@types/semver": "^7.5.8",
@@ -81,7 +82,7 @@
81
82
  "eslint-compat-utils": "^0.6.4",
82
83
  "eslint-config-prettier": "^10.1.1",
83
84
  "eslint-plugin-eslint-comments": "^3.2.0",
84
- "eslint-plugin-eslint-plugin": "^6.4.0",
85
+ "eslint-plugin-eslint-plugin": "^7.0.0",
85
86
  "eslint-plugin-jsdoc": "^52.0.0",
86
87
  "eslint-plugin-json-schema-validator": "^5.3.1",
87
88
  "eslint-plugin-jsonc": "^2.19.1",