eslint-plugin-markdown-preferences 0.5.0 → 0.6.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
@@ -94,6 +94,7 @@ The rules with the following star ⭐ are included in the configs.
94
94
  | [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
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
96
  | [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 | 🔧 | |
97
98
  | [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
99
 
99
100
  <!--RULES_TABLE_END-->
package/lib/index.d.ts CHANGED
@@ -30,6 +30,11 @@ interface RuleOptions {
30
30
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html
31
31
  */
32
32
  'markdown-preferences/prefer-inline-code-words'?: Linter.RuleEntry<MarkdownPreferencesPreferInlineCodeWords>;
33
+ /**
34
+ * enforce using link reference definitions instead of inline links
35
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html
36
+ */
37
+ 'markdown-preferences/prefer-link-reference-definitions'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkReferenceDefinitions>;
33
38
  /**
34
39
  * enforce the specified word to be a link.
35
40
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
@@ -54,6 +59,9 @@ type MarkdownPreferencesPreferInlineCodeWords = [] | [{
54
59
  }[];
55
60
  [k: string]: unknown | undefined;
56
61
  }];
62
+ type MarkdownPreferencesPreferLinkReferenceDefinitions = [] | [{
63
+ minLinks?: number;
64
+ }];
57
65
  type MarkdownPreferencesPreferLinkedWords = [] | [{
58
66
  words: ({
59
67
  [k: string]: (string | null);
@@ -82,7 +90,7 @@ declare namespace meta_d_exports {
82
90
  export { name, version };
83
91
  }
84
92
  declare const name: "eslint-plugin-markdown-preferences";
85
- declare const version: "0.5.0";
93
+ declare const version: "0.6.0";
86
94
  //#endregion
87
95
  //#region src/index.d.ts
88
96
  declare const configs: {
package/lib/index.js CHANGED
@@ -245,7 +245,7 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
245
245
 
246
246
  //#endregion
247
247
  //#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;
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;
249
249
  /**
250
250
  * Iterate through words in a text node that match the specified words.
251
251
  */
@@ -403,6 +403,140 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
403
403
  }
404
404
  });
405
405
 
406
+ //#endregion
407
+ //#region src/rules/prefer-link-reference-definitions.ts
408
+ var prefer_link_reference_definitions_default = createRule("prefer-link-reference-definitions", {
409
+ meta: {
410
+ type: "layout",
411
+ docs: {
412
+ description: "enforce using link reference definitions instead of inline links",
413
+ categories: []
414
+ },
415
+ fixable: "code",
416
+ hasSuggestions: false,
417
+ schema: [{
418
+ type: "object",
419
+ properties: { minLinks: {
420
+ type: "number",
421
+ description: "minimum number of links to trigger the rule (default: 2)",
422
+ default: 2,
423
+ minimum: 1
424
+ } },
425
+ additionalProperties: false
426
+ }],
427
+ messages: { useLinkReferenceDefinitions: "Use link reference definitions instead of inline links." }
428
+ },
429
+ create(context) {
430
+ const sourceCode = context.sourceCode;
431
+ const options = context.options[0] || {};
432
+ const minLinks = options.minLinks ?? 2;
433
+ const definitions = [];
434
+ const links = [];
435
+ const linkReferences = [];
436
+ /**
437
+ * Verify links.
438
+ */
439
+ function verify() {
440
+ const resourceToNodes = /* @__PURE__ */ new Map();
441
+ for (const link of links) getResourceNodes(link).links.push(link);
442
+ for (const linkReference of linkReferences) {
443
+ const definition = definitions.find((def) => def.identifier === linkReference.identifier);
444
+ if (definition) getResourceNodes(definition).linkReferences.push(linkReference);
445
+ }
446
+ for (const definition of definitions) getResourceNodes(definition).definitions.push(definition);
447
+ for (const map of resourceToNodes.values()) for (const nodes of map.values()) {
448
+ if (nodes.links.length === 0 || nodes.links.length + nodes.linkReferences.length < minLinks) continue;
449
+ for (const link of nodes.links) {
450
+ const linkInfo = getLinkInfo(link);
451
+ if (linkInfo.label === "") continue;
452
+ context.report({
453
+ node: link,
454
+ messageId: "useLinkReferenceDefinitions",
455
+ *fix(fixer) {
456
+ const definition = nodes.definitions[0];
457
+ let identifier;
458
+ if (definition) identifier = definition.label ?? definition.identifier;
459
+ else identifier = linkInfo.label.replaceAll(/\]/g, "-");
460
+ yield fixer.replaceText(link, `${sourceCode.text.slice(...linkInfo.labelRange)}${identifier === linkInfo.label ? "" : `[${identifier}]`}`);
461
+ if (!definition) {
462
+ const linkRange = sourceCode.getRange(link);
463
+ const reLineBreaks = /\n#{1,6}\s/gu;
464
+ reLineBreaks.lastIndex = linkRange[1];
465
+ const nextSectionMatch = reLineBreaks.exec(sourceCode.text);
466
+ const insertIndex = !nextSectionMatch ? sourceCode.text.trimEnd().length : nextSectionMatch.index;
467
+ yield fixer.insertTextAfterRange([insertIndex, insertIndex], `${sourceCode.text[insertIndex - 1] === "\n" ? "" : "\n"}\n[${identifier}]: ${sourceCode.text.slice(linkInfo.urlAndTitleRange[0] + 1, linkInfo.urlAndTitleRange[1] - 1).trim()}${nextSectionMatch ? "\n" : ""}`);
468
+ }
469
+ }
470
+ });
471
+ }
472
+ }
473
+ /**
474
+ * Get the resource nodes for a link or definition.
475
+ */
476
+ function getResourceNodes(resource) {
477
+ const url = resource.url;
478
+ const title = resource.title ?? null;
479
+ let map = resourceToNodes.get(url);
480
+ if (!map) {
481
+ map = /* @__PURE__ */ new Map();
482
+ resourceToNodes.set(url, map);
483
+ }
484
+ let nodes = map.get(title);
485
+ if (!nodes) {
486
+ nodes = {
487
+ links: [],
488
+ linkReferences: [],
489
+ definitions: []
490
+ };
491
+ map.set(title, nodes);
492
+ }
493
+ return nodes;
494
+ }
495
+ }
496
+ return {
497
+ link(node) {
498
+ links.push(node);
499
+ },
500
+ linkReference(node) {
501
+ linkReferences.push(node);
502
+ },
503
+ definition(node) {
504
+ definitions.push(node);
505
+ },
506
+ "root:exit"() {
507
+ verify();
508
+ }
509
+ };
510
+ /**
511
+ * Get the range of the link label.
512
+ */
513
+ function getLinkInfo(link) {
514
+ const range = sourceCode.getRange(link);
515
+ const linkLabelRange = getLinkLabelRange();
516
+ const linkLabelWithBracketsText = sourceCode.text.slice(...linkLabelRange);
517
+ const linkLabelText = linkLabelWithBracketsText.slice(1, -1).trim();
518
+ const urlStartIndex = sourceCode.text.indexOf("(", linkLabelRange[1]);
519
+ return {
520
+ label: linkLabelText,
521
+ labelRange: linkLabelRange,
522
+ urlAndTitleRange: [urlStartIndex, range[1]]
523
+ };
524
+ /**
525
+ * Get the range of the link label.
526
+ */
527
+ function getLinkLabelRange() {
528
+ if (link.children.length === 0) {
529
+ const index$1 = sourceCode.text.indexOf("]", range[0] + 1);
530
+ return [range[0], index$1 + 1];
531
+ }
532
+ const lastRange = sourceCode.getRange(link.children[link.children.length - 1]);
533
+ const index = sourceCode.text.indexOf("]", lastRange[0] + 1);
534
+ return [range[0], index + 1];
535
+ }
536
+ }
537
+ }
538
+ });
539
+
406
540
  //#endregion
407
541
  //#region src/rules/prefer-linked-words.ts
408
542
  var prefer_linked_words_default = createRule("prefer-linked-words", {
@@ -518,6 +652,7 @@ const rules$1 = [
518
652
  no_text_backslash_linebreak_default,
519
653
  no_trailing_spaces_default,
520
654
  prefer_inline_code_words_default,
655
+ prefer_link_reference_definitions_default,
521
656
  prefer_linked_words_default
522
657
  ];
523
658
 
@@ -553,7 +688,7 @@ __export(meta_exports, {
553
688
  version: () => version
554
689
  });
555
690
  const name = "eslint-plugin-markdown-preferences";
556
- const version = "0.5.0";
691
+ const version = "0.6.0";
557
692
 
558
693
  //#endregion
559
694
  //#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.6.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {
@@ -71,6 +71,7 @@
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",