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 +2 -1
- package/lib/index.d.ts +66 -3
- package/lib/index.js +226 -55
- package/package.json +1 -1
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/
|
|
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.
|
|
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/
|
|
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: {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
274
|
-
const
|
|
275
|
-
|
|
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
|
-
"
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
282
|
-
if (
|
|
461
|
+
"link, linkReference:exit"(node) {
|
|
462
|
+
if (linkedNode === node) linkedNode = null;
|
|
283
463
|
},
|
|
284
464
|
text(node) {
|
|
285
|
-
if (
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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.
|
|
556
|
+
const version = "0.5.0";
|
|
386
557
|
|
|
387
558
|
//#endregion
|
|
388
559
|
//#region src/index.ts
|