eslint-plugin-markdown-preferences 0.4.0 → 0.5.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
@@ -92,7 +92,7 @@ The rules with the following star ⭐ are included in the configs.
92
92
  |:--------|:------------|:-------:|:-----------:|
93
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
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
- | [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | trailing whitespace at the end of lines in Markdown files. | 🔧 | |
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
97
  | [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
98
 
package/lib/index.d.ts CHANGED
@@ -3,7 +3,70 @@ import * as _eslint_core0 from "@eslint/core";
3
3
  import { RuleDefinition } from "@eslint/core";
4
4
  import { ESLint, Linter } from "eslint";
5
5
 
6
- //#region src/configs/recommended.d.ts
6
+ //#region src/rule-types.d.ts
7
+ declare module 'eslint' {
8
+ namespace Linter {
9
+ interface RulesRecord extends RuleOptions {}
10
+ }
11
+ }
12
+ interface RuleOptions {
13
+ /**
14
+ * enforce consistent hard linebreak style.
15
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html
16
+ */
17
+ 'markdown-preferences/hard-linebreak-style'?: Linter.RuleEntry<MarkdownPreferencesHardLinebreakStyle>;
18
+ /**
19
+ * disallow text backslash at the end of a line.
20
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html
21
+ */
22
+ 'markdown-preferences/no-text-backslash-linebreak'?: Linter.RuleEntry<[]>;
23
+ /**
24
+ * disallow trailing whitespace at the end of lines in Markdown files.
25
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html
26
+ */
27
+ 'markdown-preferences/no-trailing-spaces'?: Linter.RuleEntry<MarkdownPreferencesNoTrailingSpaces>;
28
+ /**
29
+ * enforce the use of inline code for specific words.
30
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html
31
+ */
32
+ 'markdown-preferences/prefer-inline-code-words'?: Linter.RuleEntry<MarkdownPreferencesPreferInlineCodeWords>;
33
+ /**
34
+ * enforce the specified word to be a link.
35
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html
36
+ */
37
+ 'markdown-preferences/prefer-linked-words'?: Linter.RuleEntry<MarkdownPreferencesPreferLinkedWords>;
38
+ }
39
+ type MarkdownPreferencesHardLinebreakStyle = [] | [{
40
+ style?: ("backslash" | "spaces");
41
+ }];
42
+ type MarkdownPreferencesNoTrailingSpaces = [] | [{
43
+ skipBlankLines?: boolean;
44
+ ignoreComments?: boolean;
45
+ }];
46
+ type MarkdownPreferencesPreferInlineCodeWords = [] | [{
47
+ words: string[];
48
+ ignores?: {
49
+ words?: (string | string[]);
50
+ node?: {
51
+ [k: string]: unknown | undefined;
52
+ };
53
+ [k: string]: unknown | undefined;
54
+ }[];
55
+ [k: string]: unknown | undefined;
56
+ }];
57
+ type MarkdownPreferencesPreferLinkedWords = [] | [{
58
+ words: ({
59
+ [k: string]: (string | null);
60
+ } | string[]);
61
+ ignores?: {
62
+ words?: (string | string[]);
63
+ node?: {
64
+ [k: string]: unknown | undefined;
65
+ };
66
+ [k: string]: unknown | undefined;
67
+ }[];
68
+ [k: string]: unknown | undefined;
69
+ }];
7
70
  declare namespace recommended_d_exports {
8
71
  export { files, language, name$1 as name, plugins, rules$1 as rules };
9
72
  }
@@ -19,7 +82,7 @@ declare namespace meta_d_exports {
19
82
  export { name, version };
20
83
  }
21
84
  declare const name: "eslint-plugin-markdown-preferences";
22
- declare const version: "0.4.0";
85
+ declare const version: "0.5.0";
23
86
  //#endregion
24
87
  //#region src/index.d.ts
25
88
  declare const configs: {
@@ -34,4 +97,4 @@ declare const _default: {
34
97
  rules: Record<string, RuleDefinition<_eslint_core0.RuleDefinitionTypeOptions>>;
35
98
  };
36
99
  //#endregion
37
- export { configs, _default as default, meta_d_exports as meta, rules };
100
+ export { RuleOptions, configs, _default as default, meta_d_exports as meta, rules };
package/lib/index.js CHANGED
@@ -131,7 +131,7 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
131
131
  meta: {
132
132
  type: "layout",
133
133
  docs: {
134
- description: "trailing whitespace at the end of lines in Markdown files.",
134
+ description: "disallow trailing whitespace at the end of lines in Markdown files.",
135
135
  categories: []
136
136
  },
137
137
  fixable: "whitespace",
@@ -249,9 +249,10 @@ const RE_BOUNDARY = /^[\s\p{Letter_Number}\p{Modifier_Letter}\p{Modifier_Symbol}
249
249
  /**
250
250
  * Iterate through words in a text node that match the specified words.
251
251
  */
252
- function* iterateSearchWords(sourceCode, node, words) {
252
+ function* iterateSearchWords({ sourceCode, node, words, ignores }) {
253
253
  const text = sourceCode.getText(node);
254
254
  for (const word of words) {
255
+ if (ignores.ignore(word)) continue;
255
256
  let startPosition = 0;
256
257
  while (true) {
257
258
  const index = text.indexOf(word, startPosition);
@@ -280,6 +281,57 @@ function* iterateSearchWords(sourceCode, node, words) {
280
281
  }
281
282
  }
282
283
  }
284
+ const IGNORES_SCHEMA = {
285
+ type: "array",
286
+ items: {
287
+ type: "object",
288
+ properties: {
289
+ words: { anyOf: [{ type: "string" }, {
290
+ type: "array",
291
+ items: { type: "string" }
292
+ }] },
293
+ node: { type: "object" }
294
+ },
295
+ additionalProperties: true
296
+ }
297
+ };
298
+ /**
299
+ * Create a context for ignoring specific words or nodes.
300
+ */
301
+ function createSearchWordsIgnoreContext(ignores) {
302
+ if (!ignores || ignores.length === 0) return {
303
+ enter: () => void 0,
304
+ exit: () => void 0,
305
+ ignore: () => false
306
+ };
307
+ const conditions = ignores.map((ignore) => {
308
+ const isIgnoreWord = ignore.words == null ? () => true : Array.isArray(ignore.words) ? (word) => ignore.words.includes(word) : (word) => ignore.words === word;
309
+ const node = ignore.node || {};
310
+ const keys = Object.keys(node);
311
+ return {
312
+ isIgnoreWord,
313
+ isIgnoreNode: (nodeToCheck) => {
314
+ return keys.every((key) => nodeToCheck[key] === node[key]);
315
+ }
316
+ };
317
+ });
318
+ const currentIgnores = /* @__PURE__ */ new Set();
319
+ return {
320
+ enter(node) {
321
+ for (const ignore of conditions) if (ignore.isIgnoreNode(node)) currentIgnores.add({
322
+ node,
323
+ condition: ignore
324
+ });
325
+ },
326
+ exit(node) {
327
+ for (const element of [...currentIgnores]) if (element.node === node) currentIgnores.delete(element);
328
+ },
329
+ ignore(word) {
330
+ for (const { condition } of currentIgnores) if (condition.isIgnoreWord(word)) return true;
331
+ return false;
332
+ }
333
+ };
334
+ }
283
335
 
284
336
  //#endregion
285
337
  //#region src/rules/prefer-inline-code-words.ts
@@ -294,19 +346,30 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
294
346
  hasSuggestions: false,
295
347
  schema: [{
296
348
  type: "object",
297
- properties: { words: {
298
- type: "array",
299
- items: { type: "string" }
300
- } },
301
- required: ["words"]
349
+ properties: {
350
+ words: {
351
+ type: "array",
352
+ items: { type: "string" }
353
+ },
354
+ ignores: IGNORES_SCHEMA
355
+ },
356
+ required: ["words"],
357
+ additionalProperties: true
302
358
  }],
303
359
  messages: { requireInlineCode: "The word \"{{name}}\" should be in inline code." }
304
360
  },
305
361
  create(context) {
306
362
  const sourceCode = context.sourceCode;
307
363
  const words = context.options[0]?.words || [];
364
+ const ignores = createSearchWordsIgnoreContext(context.options[0]?.ignores);
308
365
  let shortcutLinkReference = null;
309
366
  return {
367
+ "*"(node) {
368
+ ignores.enter(node);
369
+ },
370
+ "*:exit"(node) {
371
+ ignores.exit(node);
372
+ },
310
373
  linkReference(node) {
311
374
  if (node.referenceType !== "shortcut") return;
312
375
  if (shortcutLinkReference) return;
@@ -316,7 +379,12 @@ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
316
379
  if (shortcutLinkReference === node) shortcutLinkReference = null;
317
380
  },
318
381
  text(node) {
319
- for (const { word, loc, range } of iterateSearchWords(sourceCode, node, words)) {
382
+ for (const { word, loc, range } of iterateSearchWords({
383
+ sourceCode,
384
+ node,
385
+ words,
386
+ ignores
387
+ })) {
320
388
  const shortcutLinkReferenceToReport = shortcutLinkReference;
321
389
  context.report({
322
390
  node,
@@ -348,20 +416,25 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
348
416
  hasSuggestions: false,
349
417
  schema: [{
350
418
  type: "object",
351
- properties: { words: { anyOf: [{
352
- type: "object",
353
- patternProperties: { "^[\\s\\S]+$": { type: ["string", "null"] } }
354
- }, {
355
- type: "array",
356
- items: { type: "string" }
357
- }] } },
358
- required: ["words"]
419
+ properties: {
420
+ words: { anyOf: [{
421
+ type: "object",
422
+ patternProperties: { "^[\\s\\S]+$": { type: ["string", "null"] } }
423
+ }, {
424
+ type: "array",
425
+ items: { type: "string" }
426
+ }] },
427
+ ignores: IGNORES_SCHEMA
428
+ },
429
+ required: ["words"],
430
+ additionalProperties: true
359
431
  }],
360
432
  messages: { requireLink: "The word \"{{name}}\" should be a link." }
361
433
  },
362
434
  create(context) {
363
435
  const sourceCode = context.sourceCode;
364
436
  const wordsOption = context.options[0]?.words || {};
437
+ const ignores = createSearchWordsIgnoreContext(context.options[0]?.ignores);
365
438
  const links = Object.create(null);
366
439
  const words = [];
367
440
  if (Array.isArray(wordsOption)) words.push(...wordsOption);
@@ -373,18 +446,29 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
373
446
  }
374
447
  words.push(word);
375
448
  }
376
- let ignore = null;
449
+ let linkedNode = null;
377
450
  return {
378
- "link, linkReference, heading, footnoteDefinition"(node) {
379
- if (ignore) return;
380
- ignore = node;
451
+ "*"(node) {
452
+ ignores.enter(node);
453
+ },
454
+ "*:exit"(node) {
455
+ ignores.exit(node);
381
456
  },
382
- "link, linkReference, heading, footnoteDefinition:exit"(node) {
383
- if (ignore === node) ignore = null;
457
+ "link, linkReference"(node) {
458
+ if (linkedNode) return;
459
+ linkedNode = node;
460
+ },
461
+ "link, linkReference:exit"(node) {
462
+ if (linkedNode === node) linkedNode = null;
384
463
  },
385
464
  text(node) {
386
- if (ignore) return;
387
- for (const { word, loc, range } of iterateSearchWords(sourceCode, node, words)) {
465
+ if (linkedNode) return;
466
+ for (const { word, loc, range } of iterateSearchWords({
467
+ sourceCode,
468
+ node,
469
+ words,
470
+ ignores
471
+ })) {
388
472
  const link = links[word];
389
473
  context.report({
390
474
  node,
@@ -398,17 +482,20 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
398
482
  }
399
483
  },
400
484
  inlineCode(node) {
401
- if (ignore) return;
402
- for (const word of words) if (node.value === word) {
403
- const link = links[word];
404
- context.report({
405
- node,
406
- messageId: "requireLink",
407
- data: { name: word },
408
- fix: link ? (fixer) => {
409
- return fixer.replaceText(node, `[\`${word}\`](${link})`);
410
- } : null
411
- });
485
+ if (linkedNode) return;
486
+ for (const word of words) {
487
+ if (ignores.ignore(word)) continue;
488
+ if (node.value === word) {
489
+ const link = links[word];
490
+ context.report({
491
+ node,
492
+ messageId: "requireLink",
493
+ data: { name: word },
494
+ fix: link ? (fixer) => {
495
+ return fixer.replaceText(node, `[\`${word}\`](${link})`);
496
+ } : null
497
+ });
498
+ }
412
499
  }
413
500
  }
414
501
  };
@@ -466,7 +553,7 @@ __export(meta_exports, {
466
553
  version: () => version
467
554
  });
468
555
  const name = "eslint-plugin-markdown-preferences";
469
- const version = "0.4.0";
556
+ const version = "0.5.0";
470
557
 
471
558
  //#endregion
472
559
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {