eslint-plugin-markdown-preferences 0.3.3 → 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,8 @@ 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
+ | [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. | 🔧 | |
96
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. | 🔧 | |
97
98
 
98
99
  <!--RULES_TABLE_END-->
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.3.3";
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",
@@ -244,8 +244,167 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
244
244
  });
245
245
 
246
246
  //#endregion
247
- //#region src/rules/prefer-linked-words.ts
247
+ //#region src/utils/search-words.ts
248
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
+ /**
250
+ * Iterate through words in a text node that match the specified words.
251
+ */
252
+ function* iterateSearchWords({ sourceCode, node, words, ignores }) {
253
+ const text = sourceCode.getText(node);
254
+ for (const word of words) {
255
+ if (ignores.ignore(word)) continue;
256
+ let startPosition = 0;
257
+ while (true) {
258
+ const index = text.indexOf(word, startPosition);
259
+ if (index < 0) break;
260
+ startPosition = index + word.length;
261
+ if (!RE_BOUNDARY.test(text[index - 1] || "") || !RE_BOUNDARY.test(text[index + word.length] || "")) continue;
262
+ const loc = sourceCode.getLoc(node);
263
+ const beforeLines = text.slice(0, index).split(/\n/u);
264
+ const line = loc.start.line + beforeLines.length - 1;
265
+ const column = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
266
+ const range = sourceCode.getRange(node);
267
+ yield {
268
+ loc: {
269
+ start: {
270
+ line,
271
+ column
272
+ },
273
+ end: {
274
+ line,
275
+ column: column + word.length
276
+ }
277
+ },
278
+ range: [range[0] + index, range[0] + index + word.length],
279
+ word
280
+ };
281
+ }
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
+ }
335
+
336
+ //#endregion
337
+ //#region src/rules/prefer-inline-code-words.ts
338
+ var prefer_inline_code_words_default = createRule("prefer-inline-code-words", {
339
+ meta: {
340
+ type: "suggestion",
341
+ docs: {
342
+ description: "enforce the use of inline code for specific words.",
343
+ categories: []
344
+ },
345
+ fixable: "code",
346
+ hasSuggestions: false,
347
+ schema: [{
348
+ type: "object",
349
+ properties: {
350
+ words: {
351
+ type: "array",
352
+ items: { type: "string" }
353
+ },
354
+ ignores: IGNORES_SCHEMA
355
+ },
356
+ required: ["words"],
357
+ additionalProperties: true
358
+ }],
359
+ messages: { requireInlineCode: "The word \"{{name}}\" should be in inline code." }
360
+ },
361
+ create(context) {
362
+ const sourceCode = context.sourceCode;
363
+ const words = context.options[0]?.words || [];
364
+ const ignores = createSearchWordsIgnoreContext(context.options[0]?.ignores);
365
+ let shortcutLinkReference = null;
366
+ return {
367
+ "*"(node) {
368
+ ignores.enter(node);
369
+ },
370
+ "*:exit"(node) {
371
+ ignores.exit(node);
372
+ },
373
+ linkReference(node) {
374
+ if (node.referenceType !== "shortcut") return;
375
+ if (shortcutLinkReference) return;
376
+ shortcutLinkReference = node;
377
+ },
378
+ "linkReference:exit"(node) {
379
+ if (shortcutLinkReference === node) shortcutLinkReference = null;
380
+ },
381
+ text(node) {
382
+ for (const { word, loc, range } of iterateSearchWords({
383
+ sourceCode,
384
+ node,
385
+ words,
386
+ ignores
387
+ })) {
388
+ const shortcutLinkReferenceToReport = shortcutLinkReference;
389
+ context.report({
390
+ node,
391
+ loc,
392
+ messageId: "requireInlineCode",
393
+ data: { name: word },
394
+ *fix(fixer) {
395
+ yield fixer.insertTextBeforeRange(range, "`");
396
+ yield fixer.insertTextAfterRange(range, "`");
397
+ if (shortcutLinkReferenceToReport) yield fixer.insertTextAfter(shortcutLinkReferenceToReport, `[${shortcutLinkReferenceToReport.label}]`);
398
+ }
399
+ });
400
+ }
401
+ }
402
+ };
403
+ }
404
+ });
405
+
406
+ //#endregion
407
+ //#region src/rules/prefer-linked-words.ts
249
408
  var prefer_linked_words_default = createRule("prefer-linked-words", {
250
409
  meta: {
251
410
  type: "suggestion",
@@ -257,76 +416,87 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
257
416
  hasSuggestions: false,
258
417
  schema: [{
259
418
  type: "object",
260
- properties: { words: { anyOf: [{
261
- type: "object",
262
- patternProperties: { "^[\\s\\S]+$": { type: ["string", "null"] } }
263
- }, {
264
- type: "array",
265
- items: { type: "string" }
266
- }] } },
267
- 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
268
431
  }],
269
432
  messages: { requireLink: "The word \"{{name}}\" should be a link." }
270
433
  },
271
434
  create(context) {
272
435
  const sourceCode = context.sourceCode;
273
- const words = context.options[0]?.words || {};
274
- const wordEntries = (Array.isArray(words) ? words.map((word) => [word, void 0]) : Object.entries(words)).map(([word, link]) => [word, link ? adjustLink(link) : void 0]).filter(([, link]) => link !== `./${path.basename(context.filename)}`);
275
- let ignore = null;
436
+ const wordsOption = context.options[0]?.words || {};
437
+ const ignores = createSearchWordsIgnoreContext(context.options[0]?.ignores);
438
+ const links = Object.create(null);
439
+ const words = [];
440
+ if (Array.isArray(wordsOption)) words.push(...wordsOption);
441
+ else for (const [word, link] of Object.entries(wordsOption)) {
442
+ if (link) {
443
+ const adjustedLink = adjustLink(link);
444
+ if (adjustedLink === `./${path.basename(context.filename)}`) continue;
445
+ links[word] = adjustedLink;
446
+ }
447
+ words.push(word);
448
+ }
449
+ let linkedNode = null;
276
450
  return {
277
- "link, linkReference, heading, footnoteDefinition"(node) {
278
- if (ignore) return;
279
- ignore = node;
451
+ "*"(node) {
452
+ ignores.enter(node);
453
+ },
454
+ "*:exit"(node) {
455
+ ignores.exit(node);
456
+ },
457
+ "link, linkReference"(node) {
458
+ if (linkedNode) return;
459
+ linkedNode = node;
280
460
  },
281
- "link, linkReference, heading, footnoteDefinition:exit"(node) {
282
- if (ignore === node) ignore = null;
461
+ "link, linkReference:exit"(node) {
462
+ if (linkedNode === node) linkedNode = null;
283
463
  },
284
464
  text(node) {
285
- if (ignore) return;
286
- const text = sourceCode.getText(node);
287
- for (const [word, link] of wordEntries) {
288
- let startPosition = 0;
289
- while (true) {
290
- const index = text.indexOf(word, startPosition);
291
- if (index < 0) break;
292
- startPosition = index + word.length;
293
- if (!RE_BOUNDARY.test(text[index - 1] || "") || !RE_BOUNDARY.test(text[index + word.length] || "")) continue;
294
- const loc = sourceCode.getLoc(node);
295
- const beforeLines = text.slice(0, index).split(/\n/u);
296
- const line = loc.start.line + beforeLines.length - 1;
297
- const column = (beforeLines.length === 1 ? loc.start.column : 1) + (beforeLines.at(-1) || "").length;
465
+ if (linkedNode) return;
466
+ for (const { word, loc, range } of iterateSearchWords({
467
+ sourceCode,
468
+ node,
469
+ words,
470
+ ignores
471
+ })) {
472
+ const link = links[word];
473
+ context.report({
474
+ node,
475
+ loc,
476
+ messageId: "requireLink",
477
+ data: { name: word },
478
+ fix: link ? (fixer) => {
479
+ return fixer.replaceTextRange(range, `[${word}](${link})`);
480
+ } : null
481
+ });
482
+ }
483
+ },
484
+ inlineCode(node) {
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];
298
490
  context.report({
299
491
  node,
300
- loc: {
301
- start: {
302
- line,
303
- column
304
- },
305
- end: {
306
- line,
307
- column: column + word.length
308
- }
309
- },
310
492
  messageId: "requireLink",
311
493
  data: { name: word },
312
494
  fix: link ? (fixer) => {
313
- const [start] = sourceCode.getRange(node);
314
- return fixer.replaceTextRange([start + index, start + index + word.length], `[${word}](${link})`);
495
+ return fixer.replaceText(node, `[\`${word}\`](${link})`);
315
496
  } : null
316
497
  });
317
498
  }
318
499
  }
319
- },
320
- inlineCode(node) {
321
- if (ignore) return;
322
- for (const [word, link] of wordEntries) if (node.value === word) context.report({
323
- node,
324
- messageId: "requireLink",
325
- data: { name: word },
326
- fix: link ? (fixer) => {
327
- return fixer.replaceText(node, `[\`${word}\`](${link})`);
328
- } : null
329
- });
330
500
  }
331
501
  };
332
502
  /**
@@ -347,6 +517,7 @@ const rules$1 = [
347
517
  hard_linebreak_style_default,
348
518
  no_text_backslash_linebreak_default,
349
519
  no_trailing_spaces_default,
520
+ prefer_inline_code_words_default,
350
521
  prefer_linked_words_default
351
522
  ];
352
523
 
@@ -382,7 +553,7 @@ __export(meta_exports, {
382
553
  version: () => version
383
554
  });
384
555
  const name = "eslint-plugin-markdown-preferences";
385
- const version = "0.3.3";
556
+ const version = "0.5.0";
386
557
 
387
558
  //#endregion
388
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.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {