eslint-plugin-markdown-preferences 0.13.0 → 0.14.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 +8 -3
- package/lib/index.d.ts +13 -2
- package/lib/index.js +260 -98
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ For detailed usage instructions, rule configurations, and examples, visit our co
|
|
|
29
29
|
## 💿 Installation
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
|
-
npm install --save-dev eslint eslint-plugin-markdown-preferences
|
|
32
|
+
npm install --save-dev eslint @eslint/markdown eslint-plugin-markdown-preferences
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
<!--DOCS_IGNORE_END-->
|
|
@@ -90,18 +90,22 @@ The rules with the following star ⭐ are included in the configs.
|
|
|
90
90
|
|
|
91
91
|
### Preference Rules
|
|
92
92
|
|
|
93
|
+
- Rules to unify the expression and description style of documents.
|
|
94
|
+
|
|
93
95
|
| Rule ID | Description | Fixable | RECOMMENDED |
|
|
94
96
|
|:--------|:------------|:-------:|:-----------:|
|
|
95
97
|
| [markdown-preferences/canonical-code-block-language](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html) | enforce canonical language names in code blocks | 🔧 | |
|
|
96
|
-
| [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) |
|
|
98
|
+
| [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) | enforce consistent emoji notation style in Markdown files. | 🔧 | |
|
|
97
99
|
| [markdown-preferences/heading-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html) | enforce consistent casing in headings. | 🔧 | |
|
|
98
|
-
| [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. | | ⭐ |
|
|
99
100
|
| [markdown-preferences/ordered-list-marker-start](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html) | enforce that ordered list markers start with 1 or 0 | 🔧 | |
|
|
100
101
|
| [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. | 🔧 | |
|
|
101
102
|
| [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. | 🔧 | |
|
|
103
|
+
| [markdown-preferences/table-header-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html) | enforce consistent casing in table header cells. | 🔧 | |
|
|
102
104
|
|
|
103
105
|
### Stylistic Rules
|
|
104
106
|
|
|
107
|
+
- Rules related to the formatting and visual style of Markdown.
|
|
108
|
+
|
|
105
109
|
| Rule ID | Description | Fixable | RECOMMENDED |
|
|
106
110
|
|:--------|:------------|:-------:|:-----------:|
|
|
107
111
|
| [markdown-preferences/atx-headings-closing-sequence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence-length.html) | enforce consistent length for the closing sequence (trailing #s) in ATX headings. | 🔧 | |
|
|
@@ -110,6 +114,7 @@ The rules with the following star ⭐ are included in the configs.
|
|
|
110
114
|
| [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
|
|
111
115
|
| [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐ |
|
|
112
116
|
| [markdown-preferences/no-multiple-empty-lines](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-multiple-empty-lines.html) | disallow multiple empty lines in Markdown files. | 🔧 | |
|
|
117
|
+
| [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. | | ⭐ |
|
|
113
118
|
| [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. | 🔧 | |
|
|
114
119
|
| [markdown-preferences/ordered-list-marker-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-sequence.html) | enforce that ordered list markers use sequential numbers | 🔧 | |
|
|
115
120
|
| [markdown-preferences/prefer-autolinks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html) | enforce the use of autolinks for URLs | 🔧 | ⭐ |
|
package/lib/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ interface RuleOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
'markdown-preferences/definitions-last'?: Linter.RuleEntry<[]>;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* enforce consistent emoji notation style in Markdown files.
|
|
35
35
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html
|
|
36
36
|
*/
|
|
37
37
|
'markdown-preferences/emoji-notation'?: Linter.RuleEntry<MarkdownPreferencesEmojiNotation>;
|
|
@@ -105,6 +105,11 @@ interface RuleOptions {
|
|
|
105
105
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
|
|
106
106
|
*/
|
|
107
107
|
'markdown-preferences/sort-definitions'?: Linter.RuleEntry<MarkdownPreferencesSortDefinitions>;
|
|
108
|
+
/**
|
|
109
|
+
* enforce consistent casing in table header cells.
|
|
110
|
+
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
|
|
111
|
+
*/
|
|
112
|
+
'markdown-preferences/table-header-casing'?: Linter.RuleEntry<MarkdownPreferencesTableHeaderCasing>;
|
|
108
113
|
}
|
|
109
114
|
type MarkdownPreferencesAtxHeadingsClosingSequence = [] | [{
|
|
110
115
|
closingSequence?: ("always" | "never");
|
|
@@ -178,6 +183,12 @@ type MarkdownPreferencesSortDefinitions = [] | [{
|
|
|
178
183
|
})[];
|
|
179
184
|
alphabetical?: boolean;
|
|
180
185
|
}];
|
|
186
|
+
type MarkdownPreferencesTableHeaderCasing = [] | [{
|
|
187
|
+
style?: ("Title Case" | "Sentence case");
|
|
188
|
+
preserveWords?: string[];
|
|
189
|
+
ignorePatterns?: string[];
|
|
190
|
+
minorWords?: string[];
|
|
191
|
+
}];
|
|
181
192
|
declare namespace recommended_d_exports {
|
|
182
193
|
export { files, language, languageOptions, name$1 as name, plugins, rules$1 as rules };
|
|
183
194
|
}
|
|
@@ -196,7 +207,7 @@ declare namespace meta_d_exports {
|
|
|
196
207
|
export { name, version };
|
|
197
208
|
}
|
|
198
209
|
declare const name: "eslint-plugin-markdown-preferences";
|
|
199
|
-
declare const version: "0.
|
|
210
|
+
declare const version: "0.14.0";
|
|
200
211
|
//#endregion
|
|
201
212
|
//#region src/index.d.ts
|
|
202
213
|
declare const configs: {
|
package/lib/index.js
CHANGED
|
@@ -2546,7 +2546,7 @@ var emoji_notation_default = createRule("emoji-notation", {
|
|
|
2546
2546
|
meta: {
|
|
2547
2547
|
type: "suggestion",
|
|
2548
2548
|
docs: {
|
|
2549
|
-
description: "
|
|
2549
|
+
description: "enforce consistent emoji notation style in Markdown files.",
|
|
2550
2550
|
categories: [],
|
|
2551
2551
|
listCategory: "Preference"
|
|
2552
2552
|
},
|
|
@@ -2813,6 +2813,7 @@ const defaultPreserveWords = [
|
|
|
2813
2813
|
"Biome",
|
|
2814
2814
|
"oxc",
|
|
2815
2815
|
"swc",
|
|
2816
|
+
"markdownlint",
|
|
2816
2817
|
"Webpack",
|
|
2817
2818
|
"Vite",
|
|
2818
2819
|
"Babel",
|
|
@@ -3148,7 +3149,8 @@ const defaultPreserveWords = [
|
|
|
3148
3149
|
"Insomnia",
|
|
3149
3150
|
"Redoc",
|
|
3150
3151
|
"Stoplight",
|
|
3151
|
-
"FAQ"
|
|
3152
|
+
"FAQ",
|
|
3153
|
+
"YouTube"
|
|
3152
3154
|
];
|
|
3153
3155
|
|
|
3154
3156
|
//#endregion
|
|
@@ -3187,44 +3189,113 @@ const defaultMinorWords = [
|
|
|
3187
3189
|
];
|
|
3188
3190
|
|
|
3189
3191
|
//#endregion
|
|
3190
|
-
//#region src/
|
|
3192
|
+
//#region src/utils/word-casing.ts
|
|
3191
3193
|
/**
|
|
3192
|
-
*
|
|
3193
|
-
* - Single words are added to preserveWords
|
|
3194
|
-
* - Multi-word phrases are added to preservePhrases and added to preserveWords with no spaces
|
|
3194
|
+
* Converts the casing of a word based on the specified case style and whether it is the first or last word in a phrase.
|
|
3195
3195
|
*/
|
|
3196
|
-
function
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3196
|
+
function convertWordCasing({ word, first, last }, { caseStyle, minorWords }) {
|
|
3197
|
+
if (caseStyle === "Title Case") {
|
|
3198
|
+
if (first || last) return {
|
|
3199
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3200
|
+
isMinorWord: false
|
|
3201
|
+
};
|
|
3202
|
+
if (minorWords.some((minorWord) => minorWord.toLowerCase() === word.toLowerCase())) return {
|
|
3203
|
+
word: word.toLowerCase(),
|
|
3204
|
+
isMinorWord: true
|
|
3205
|
+
};
|
|
3206
|
+
return {
|
|
3207
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3208
|
+
isMinorWord: false
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
if (first) return {
|
|
3212
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3213
|
+
isMinorWord: false
|
|
3214
|
+
};
|
|
3215
|
+
return {
|
|
3216
|
+
word: word.toLowerCase(),
|
|
3217
|
+
isMinorWord: false
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
//#endregion
|
|
3222
|
+
//#region src/utils/preserve-words.ts
|
|
3223
|
+
var PreserveWordsContext = class {
|
|
3224
|
+
preserveWords;
|
|
3225
|
+
preservePhrases;
|
|
3226
|
+
constructor(preserveWordsOption) {
|
|
3227
|
+
const preserveWords = /* @__PURE__ */ new Map();
|
|
3228
|
+
/**
|
|
3229
|
+
* Add a single word to the preserveWords map
|
|
3230
|
+
*/
|
|
3231
|
+
function addPreserveWord(word) {
|
|
3232
|
+
const lowerWord = word.toLowerCase();
|
|
3233
|
+
let list = preserveWords.get(lowerWord);
|
|
3234
|
+
if (!list) {
|
|
3235
|
+
list = [];
|
|
3236
|
+
preserveWords.set(lowerWord, list);
|
|
3237
|
+
}
|
|
3238
|
+
list.push(word);
|
|
3239
|
+
}
|
|
3240
|
+
const preservePhrases = /* @__PURE__ */ new Map();
|
|
3241
|
+
for (const word of preserveWordsOption) {
|
|
3242
|
+
const splitted = word.split(/\s+/);
|
|
3243
|
+
if (splitted.length <= 1) addPreserveWord(word);
|
|
3244
|
+
else {
|
|
3245
|
+
preservePhrases.set(word, splitted);
|
|
3246
|
+
addPreserveWord(splitted.join(""));
|
|
3247
|
+
}
|
|
3207
3248
|
}
|
|
3208
|
-
|
|
3249
|
+
this.preserveWords = preserveWords;
|
|
3250
|
+
this.preservePhrases = [...preservePhrases.values()].sort((a, b) => b.length - a.length);
|
|
3251
|
+
}
|
|
3252
|
+
findPreserveWord(word) {
|
|
3253
|
+
return this.preserveWords.get(word.toLowerCase()) ?? null;
|
|
3209
3254
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3255
|
+
findPreservePhrase(words) {
|
|
3256
|
+
const firstWord = words.next();
|
|
3257
|
+
if (firstWord.done) return null;
|
|
3258
|
+
const firstLowerWord = firstWord.value.toLowerCase();
|
|
3259
|
+
let returnCandidate = null;
|
|
3260
|
+
const subWords = [firstWord.value];
|
|
3261
|
+
for (const phrase of this.preservePhrases) {
|
|
3262
|
+
if (returnCandidate && returnCandidate.preservePhrase.length !== phrase.length) break;
|
|
3263
|
+
if (firstLowerWord !== phrase[0].toLowerCase()) continue;
|
|
3264
|
+
while (subWords.length < phrase.length) {
|
|
3265
|
+
const word = words.next();
|
|
3266
|
+
if (word.done) return null;
|
|
3267
|
+
subWords.push(word.value);
|
|
3268
|
+
}
|
|
3269
|
+
if (subWords.every((word, i) => word.toLowerCase() === phrase[i].toLowerCase())) {
|
|
3270
|
+
let matchCount = 0;
|
|
3271
|
+
for (let i = 0; i < subWords.length; i++) {
|
|
3272
|
+
const word = subWords[i];
|
|
3273
|
+
if (word === phrase[i]) matchCount++;
|
|
3274
|
+
}
|
|
3275
|
+
if (!returnCandidate || matchCount > returnCandidate.matchCount) returnCandidate = {
|
|
3276
|
+
preservePhrase: phrase,
|
|
3277
|
+
matchCount
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3217
3280
|
}
|
|
3281
|
+
return returnCandidate?.preservePhrase ?? null;
|
|
3218
3282
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3283
|
+
};
|
|
3284
|
+
/**
|
|
3285
|
+
* Parse preserve words and phrases from the options
|
|
3286
|
+
* - Single words are added to preserveWords
|
|
3287
|
+
* - Multi-word phrases are added to preservePhrases and added to preserveWords with no spaces
|
|
3288
|
+
*/
|
|
3289
|
+
function parsePreserveWordsOption(preserveWordsOption) {
|
|
3290
|
+
return new PreserveWordsContext(preserveWordsOption);
|
|
3223
3291
|
}
|
|
3292
|
+
|
|
3293
|
+
//#endregion
|
|
3294
|
+
//#region src/utils/words.ts
|
|
3224
3295
|
/**
|
|
3225
3296
|
* Parse text into words with offsets
|
|
3226
3297
|
*/
|
|
3227
|
-
function
|
|
3298
|
+
function parseWordsFromText(text, firstNode, lastNode) {
|
|
3228
3299
|
const words = [];
|
|
3229
3300
|
const pattern = /(\w+(?:[^\s\w]\w+)*|:\w+:|[^\s\w]+|\s+)/gu;
|
|
3230
3301
|
let match;
|
|
@@ -3255,33 +3326,9 @@ function parseText(text, firstNode, lastNode) {
|
|
|
3255
3326
|
}
|
|
3256
3327
|
return words;
|
|
3257
3328
|
}
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
function convertWord({ word, first, last }, caseStyle, minorWords) {
|
|
3262
|
-
if (caseStyle === "Title Case") {
|
|
3263
|
-
if (first || last) return {
|
|
3264
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3265
|
-
isMinorWord: false
|
|
3266
|
-
};
|
|
3267
|
-
if (minorWords.some((minorWord) => minorWord.toLowerCase() === word.toLowerCase())) return {
|
|
3268
|
-
word: word.toLowerCase(),
|
|
3269
|
-
isMinorWord: true
|
|
3270
|
-
};
|
|
3271
|
-
return {
|
|
3272
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3273
|
-
isMinorWord: false
|
|
3274
|
-
};
|
|
3275
|
-
}
|
|
3276
|
-
if (first) return {
|
|
3277
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3278
|
-
isMinorWord: false
|
|
3279
|
-
};
|
|
3280
|
-
return {
|
|
3281
|
-
word: word.toLowerCase(),
|
|
3282
|
-
isMinorWord: false
|
|
3283
|
-
};
|
|
3284
|
-
}
|
|
3329
|
+
|
|
3330
|
+
//#endregion
|
|
3331
|
+
//#region src/rules/heading-casing.ts
|
|
3285
3332
|
var heading_casing_default = createRule("heading-casing", {
|
|
3286
3333
|
meta: {
|
|
3287
3334
|
type: "suggestion",
|
|
@@ -3324,14 +3371,14 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3324
3371
|
create(context) {
|
|
3325
3372
|
const sourceCode = context.sourceCode;
|
|
3326
3373
|
const caseStyle = context.options[0]?.style || "Title Case";
|
|
3327
|
-
const
|
|
3374
|
+
const preserveWordsOption = parsePreserveWordsOption(context.options[0]?.preserveWords || defaultPreserveWords);
|
|
3328
3375
|
const minorWords = context.options[0]?.minorWords || defaultMinorWords;
|
|
3329
3376
|
const ignorePatterns = (context.options[0]?.ignorePatterns || [
|
|
3330
3377
|
"/^v\\d+/u",
|
|
3331
3378
|
"/\\w+\\.[a-z\\d]+$/u",
|
|
3332
|
-
"/\\w
|
|
3333
|
-
"/\\w
|
|
3334
|
-
"/\\w
|
|
3379
|
+
"/\\w+(?:API|Api)$/u",
|
|
3380
|
+
"/\\w+(?:SDK|Sdk)$/u",
|
|
3381
|
+
"/\\w+(?:CLI|Cli)$/u"
|
|
3335
3382
|
]).map((pattern) => {
|
|
3336
3383
|
if (isRegExp(pattern)) return toRegExp(pattern);
|
|
3337
3384
|
try {
|
|
@@ -3347,7 +3394,7 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3347
3394
|
*/
|
|
3348
3395
|
function checkTextNode(node, firstNode, lastNode) {
|
|
3349
3396
|
const text = sourceCode.getText(node);
|
|
3350
|
-
const wordAndOffsets =
|
|
3397
|
+
const wordAndOffsets = parseWordsFromText(text, firstNode, lastNode);
|
|
3351
3398
|
const processed = /* @__PURE__ */ new Set();
|
|
3352
3399
|
for (let index = 0; index < wordAndOffsets.length; index++) {
|
|
3353
3400
|
if (processed.has(index)) continue;
|
|
@@ -3355,7 +3402,12 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3355
3402
|
const wordAndOffset = wordAndOffsets[index];
|
|
3356
3403
|
if (wordAndOffset.punctuation) continue;
|
|
3357
3404
|
if (ignorePatterns.some((pattern) => pattern.test(wordAndOffset.word))) continue;
|
|
3358
|
-
const preservePhrase = findPreservePhrase(
|
|
3405
|
+
const preservePhrase = preserveWordsOption.findPreservePhrase((function* () {
|
|
3406
|
+
const firstWord = wordAndOffsets[index];
|
|
3407
|
+
if (firstWord.punctuation) return;
|
|
3408
|
+
yield firstWord.word;
|
|
3409
|
+
for (let next = index + 1; next < wordAndOffsets.length; next++) yield wordAndOffsets[next].word;
|
|
3410
|
+
})());
|
|
3359
3411
|
if (preservePhrase) {
|
|
3360
3412
|
for (let wordIndex = 0; wordIndex < preservePhrase.length; wordIndex++) {
|
|
3361
3413
|
processed.add(index + wordIndex);
|
|
@@ -3363,12 +3415,15 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3363
3415
|
}
|
|
3364
3416
|
continue;
|
|
3365
3417
|
}
|
|
3366
|
-
const preserveWordList =
|
|
3418
|
+
const preserveWordList = preserveWordsOption.findPreserveWord(wordAndOffset.word);
|
|
3367
3419
|
if (preserveWordList) {
|
|
3368
3420
|
if (!preserveWordList.some((w) => w === wordAndOffset.word)) verifyWord(wordAndOffset, preserveWordList[0], "preserved");
|
|
3369
3421
|
continue;
|
|
3370
3422
|
}
|
|
3371
|
-
const expectedWordResult =
|
|
3423
|
+
const expectedWordResult = convertWordCasing(wordAndOffset, {
|
|
3424
|
+
caseStyle,
|
|
3425
|
+
minorWords
|
|
3426
|
+
});
|
|
3372
3427
|
verifyWord(wordAndOffset, expectedWordResult.word, expectedWordResult.isMinorWord ? "minor" : "normal");
|
|
3373
3428
|
}
|
|
3374
3429
|
/**
|
|
@@ -3393,39 +3448,11 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3393
3448
|
});
|
|
3394
3449
|
}
|
|
3395
3450
|
}
|
|
3396
|
-
/**
|
|
3397
|
-
* Find a preserve phrase starting from the given index
|
|
3398
|
-
* Returns the longest matching phrase or null if no match is found
|
|
3399
|
-
*/
|
|
3400
|
-
function findPreservePhrase(wordAndOffsets, index) {
|
|
3401
|
-
const firstWord = wordAndOffsets[index];
|
|
3402
|
-
if (firstWord.punctuation) return null;
|
|
3403
|
-
const firstLowerWord = firstWord.word.toLowerCase();
|
|
3404
|
-
let returnCandidate = null;
|
|
3405
|
-
let subWords = null;
|
|
3406
|
-
for (const phrase of preservePhrases) {
|
|
3407
|
-
if (returnCandidate && returnCandidate.preservePhrase.length !== phrase.length) break;
|
|
3408
|
-
if (firstLowerWord !== phrase[0].toLowerCase()) continue;
|
|
3409
|
-
if (!subWords || subWords.length !== phrase.length) subWords = wordAndOffsets.slice(index, index + phrase.length).map((wo) => wo.word);
|
|
3410
|
-
if (subWords.length === phrase.length && subWords.every((word, i) => word.toLowerCase() === phrase[i].toLowerCase())) {
|
|
3411
|
-
let matchCount = 0;
|
|
3412
|
-
for (let i = 0; i < subWords.length; i++) {
|
|
3413
|
-
const word = subWords[i];
|
|
3414
|
-
if (word === phrase[i]) matchCount++;
|
|
3415
|
-
}
|
|
3416
|
-
if (!returnCandidate || matchCount > returnCandidate.matchCount) returnCandidate = {
|
|
3417
|
-
preservePhrase: phrase,
|
|
3418
|
-
matchCount
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
return returnCandidate?.preservePhrase ?? null;
|
|
3423
|
-
}
|
|
3424
3451
|
return { heading(node) {
|
|
3425
3452
|
if (!node.children.length) return;
|
|
3426
3453
|
const children = node.children.filter((child) => child.type !== "text" || child.value.trim());
|
|
3427
3454
|
children.forEach((child, i) => {
|
|
3428
|
-
if (child.type === "text") checkTextNode(child, i === 0, i ===
|
|
3455
|
+
if (child.type === "text") checkTextNode(child, i === 0, i === children.length - 1);
|
|
3429
3456
|
});
|
|
3430
3457
|
} };
|
|
3431
3458
|
}
|
|
@@ -3710,11 +3737,11 @@ var no_multiple_empty_lines_default = createRule("no-multiple-empty-lines", {
|
|
|
3710
3737
|
//#region src/rules/no-text-backslash-linebreak.ts
|
|
3711
3738
|
var no_text_backslash_linebreak_default = createRule("no-text-backslash-linebreak", {
|
|
3712
3739
|
meta: {
|
|
3713
|
-
type: "
|
|
3740
|
+
type: "layout",
|
|
3714
3741
|
docs: {
|
|
3715
3742
|
description: "disallow text backslash at the end of a line.",
|
|
3716
3743
|
categories: ["recommended"],
|
|
3717
|
-
listCategory: "
|
|
3744
|
+
listCategory: "Stylistic"
|
|
3718
3745
|
},
|
|
3719
3746
|
fixable: void 0,
|
|
3720
3747
|
hasSuggestions: true,
|
|
@@ -4891,6 +4918,140 @@ function normalizedURL(url) {
|
|
|
4891
4918
|
return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
|
|
4892
4919
|
}
|
|
4893
4920
|
|
|
4921
|
+
//#endregion
|
|
4922
|
+
//#region src/rules/table-header-casing.ts
|
|
4923
|
+
var table_header_casing_default = createRule("table-header-casing", {
|
|
4924
|
+
meta: {
|
|
4925
|
+
type: "suggestion",
|
|
4926
|
+
fixable: "code",
|
|
4927
|
+
docs: {
|
|
4928
|
+
description: "enforce consistent casing in table header cells.",
|
|
4929
|
+
categories: [],
|
|
4930
|
+
listCategory: "Preference"
|
|
4931
|
+
},
|
|
4932
|
+
schema: [{
|
|
4933
|
+
type: "object",
|
|
4934
|
+
properties: {
|
|
4935
|
+
style: { enum: ["Title Case", "Sentence case"] },
|
|
4936
|
+
preserveWords: {
|
|
4937
|
+
type: "array",
|
|
4938
|
+
items: { type: "string" },
|
|
4939
|
+
description: "Words that should be preserved as-is (case-insensitive matching)"
|
|
4940
|
+
},
|
|
4941
|
+
ignorePatterns: {
|
|
4942
|
+
type: "array",
|
|
4943
|
+
items: { type: "string" },
|
|
4944
|
+
description: "Regular expression patterns for words to ignore during casing checks"
|
|
4945
|
+
},
|
|
4946
|
+
minorWords: {
|
|
4947
|
+
type: "array",
|
|
4948
|
+
items: { type: "string" },
|
|
4949
|
+
description: "Words that should not be capitalized in Title Case (unless they're the first or last word)"
|
|
4950
|
+
}
|
|
4951
|
+
},
|
|
4952
|
+
additionalProperties: false
|
|
4953
|
+
}],
|
|
4954
|
+
messages: {
|
|
4955
|
+
expectedTitleCase: "Expected \"{{actual}}\" to be \"{{expected}}\" (Title Case).",
|
|
4956
|
+
expectedTitleCaseMinorWord: "Expected \"{{actual}}\" to be \"{{expected}}\" (Title Case - minor word).",
|
|
4957
|
+
expectedSentenceCase: "Expected \"{{actual}}\" to be \"{{expected}}\" (Sentence case).",
|
|
4958
|
+
expectedPreserveWord: "Expected \"{{actual}}\" to be \"{{expected}}\" (preserved word)."
|
|
4959
|
+
}
|
|
4960
|
+
},
|
|
4961
|
+
create(context) {
|
|
4962
|
+
const sourceCode = context.sourceCode;
|
|
4963
|
+
const caseStyle = context.options[0]?.style || "Title Case";
|
|
4964
|
+
const preserveWordsOption = parsePreserveWordsOption(context.options[0]?.preserveWords || defaultPreserveWords);
|
|
4965
|
+
const minorWords = context.options[0]?.minorWords || defaultMinorWords;
|
|
4966
|
+
const ignorePatterns = (context.options[0]?.ignorePatterns || [
|
|
4967
|
+
"/^v\\d+/u",
|
|
4968
|
+
"/\\w+\\.[a-z\\d]+$/u",
|
|
4969
|
+
"/\\w+(?:API|Api)$/u",
|
|
4970
|
+
"/\\w+(?:SDK|Sdk)$/u",
|
|
4971
|
+
"/\\w+(?:CLI|Cli)$/u"
|
|
4972
|
+
]).map((pattern) => {
|
|
4973
|
+
if (isRegExp(pattern)) return toRegExp(pattern);
|
|
4974
|
+
try {
|
|
4975
|
+
return new RegExp(pattern, "v");
|
|
4976
|
+
} catch {}
|
|
4977
|
+
try {
|
|
4978
|
+
return new RegExp(pattern, "u");
|
|
4979
|
+
} catch {}
|
|
4980
|
+
return new RegExp(pattern);
|
|
4981
|
+
});
|
|
4982
|
+
/**
|
|
4983
|
+
* Check text node and report word-level errors
|
|
4984
|
+
*/
|
|
4985
|
+
function checkTextNode(node, firstNode, lastNode) {
|
|
4986
|
+
const text = sourceCode.getText(node);
|
|
4987
|
+
const wordAndOffsets = parseWordsFromText(text, firstNode, lastNode);
|
|
4988
|
+
const processed = /* @__PURE__ */ new Set();
|
|
4989
|
+
for (let index = 0; index < wordAndOffsets.length; index++) {
|
|
4990
|
+
if (processed.has(index)) continue;
|
|
4991
|
+
processed.add(index);
|
|
4992
|
+
const wordAndOffset = wordAndOffsets[index];
|
|
4993
|
+
if (wordAndOffset.punctuation) continue;
|
|
4994
|
+
if (ignorePatterns.some((pattern) => pattern.test(wordAndOffset.word))) continue;
|
|
4995
|
+
const preservePhrase = preserveWordsOption.findPreservePhrase((function* () {
|
|
4996
|
+
const firstWord = wordAndOffsets[index];
|
|
4997
|
+
if (firstWord.punctuation) return;
|
|
4998
|
+
yield firstWord.word;
|
|
4999
|
+
for (let next = index + 1; next < wordAndOffsets.length; next++) yield wordAndOffsets[next].word;
|
|
5000
|
+
})());
|
|
5001
|
+
if (preservePhrase) {
|
|
5002
|
+
for (let wordIndex = 0; wordIndex < preservePhrase.length; wordIndex++) {
|
|
5003
|
+
processed.add(index + wordIndex);
|
|
5004
|
+
verifyWord(wordAndOffsets[index + wordIndex], preservePhrase[wordIndex], "preserved");
|
|
5005
|
+
}
|
|
5006
|
+
continue;
|
|
5007
|
+
}
|
|
5008
|
+
const preserveWordList = preserveWordsOption.findPreserveWord(wordAndOffset.word);
|
|
5009
|
+
if (preserveWordList) {
|
|
5010
|
+
if (!preserveWordList.some((w) => w === wordAndOffset.word)) verifyWord(wordAndOffset, preserveWordList[0], "preserved");
|
|
5011
|
+
continue;
|
|
5012
|
+
}
|
|
5013
|
+
const expectedWordResult = convertWordCasing(wordAndOffset, {
|
|
5014
|
+
caseStyle,
|
|
5015
|
+
minorWords
|
|
5016
|
+
});
|
|
5017
|
+
verifyWord(wordAndOffset, expectedWordResult.word, expectedWordResult.isMinorWord ? "minor" : "normal");
|
|
5018
|
+
}
|
|
5019
|
+
/**
|
|
5020
|
+
* Verify a single word against the expected casing
|
|
5021
|
+
*/
|
|
5022
|
+
function verifyWord(wordAndOffset, expectedWord, wordType = "normal") {
|
|
5023
|
+
const { word, offset } = wordAndOffset;
|
|
5024
|
+
if (word === expectedWord) return;
|
|
5025
|
+
const [nodeStart] = sourceCode.getRange(node);
|
|
5026
|
+
const range = [nodeStart + offset, nodeStart + offset + word.length];
|
|
5027
|
+
context.report({
|
|
5028
|
+
node,
|
|
5029
|
+
messageId: wordType === "preserved" ? "expectedPreserveWord" : caseStyle === "Title Case" ? wordType === "minor" ? "expectedTitleCaseMinorWord" : "expectedTitleCase" : "expectedSentenceCase",
|
|
5030
|
+
data: {
|
|
5031
|
+
actual: word,
|
|
5032
|
+
expected: expectedWord
|
|
5033
|
+
},
|
|
5034
|
+
loc: getSourceLocationFromRange(sourceCode, node, range),
|
|
5035
|
+
fix(fixer) {
|
|
5036
|
+
return fixer.replaceTextRange(range, expectedWord);
|
|
5037
|
+
}
|
|
5038
|
+
});
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
return { table(node) {
|
|
5042
|
+
if (!node.children.length) return;
|
|
5043
|
+
const headerRow = node.children[0];
|
|
5044
|
+
if (headerRow.type !== "tableRow") return;
|
|
5045
|
+
for (const cell of headerRow.children) {
|
|
5046
|
+
const children = cell.children.filter((child) => child.type !== "text" || child.value.trim());
|
|
5047
|
+
children.forEach((child, i) => {
|
|
5048
|
+
if (child.type === "text") checkTextNode(child, i === 0, i === children.length - 1);
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
} };
|
|
5052
|
+
}
|
|
5053
|
+
});
|
|
5054
|
+
|
|
4894
5055
|
//#endregion
|
|
4895
5056
|
//#region src/utils/rules.ts
|
|
4896
5057
|
const rules$1 = [
|
|
@@ -4912,7 +5073,8 @@ const rules$1 = [
|
|
|
4912
5073
|
prefer_inline_code_words_default,
|
|
4913
5074
|
prefer_link_reference_definitions_default,
|
|
4914
5075
|
prefer_linked_words_default,
|
|
4915
|
-
sort_definitions_default
|
|
5076
|
+
sort_definitions_default,
|
|
5077
|
+
table_header_casing_default
|
|
4916
5078
|
];
|
|
4917
5079
|
|
|
4918
5080
|
//#endregion
|
|
@@ -4952,7 +5114,7 @@ __export(meta_exports, {
|
|
|
4952
5114
|
version: () => version
|
|
4953
5115
|
});
|
|
4954
5116
|
const name = "eslint-plugin-markdown-preferences";
|
|
4955
|
-
const version = "0.
|
|
5117
|
+
const version = "0.14.0";
|
|
4956
5118
|
|
|
4957
5119
|
//#endregion
|
|
4958
5120
|
//#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
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "ESLint plugin that enforces our markdown preferences",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"lint": "eslint .",
|
|
23
23
|
"tsc": "tsc --project tsconfig.build.json",
|
|
24
24
|
"eslint-fix": "eslint . --fix",
|
|
25
|
+
"markdownlint": "npx -y markdownlint-cli2 .",
|
|
25
26
|
"test": "npm run mocha -- \"tests/src/**/*.ts\" --reporter=dot --timeout=60000",
|
|
26
27
|
"cover": "c8 --reporter=lcov npm run test",
|
|
27
28
|
"test:update": "npm run mocha -- \"tests/src/**/*.ts\" --reporter=dot --update",
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
"@eslint/markdown": "^7.1.0",
|
|
60
61
|
"eslint": ">=9.0.0"
|
|
61
62
|
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"emoji-regex-xs": "^2.0.1",
|
|
65
|
+
"string-width": "^7.2.0"
|
|
66
|
+
},
|
|
62
67
|
"devDependencies": {
|
|
63
68
|
"@changesets/changelog-github": "^0.5.1",
|
|
64
69
|
"@changesets/cli": "^2.28.1",
|
|
@@ -121,9 +126,5 @@
|
|
|
121
126
|
},
|
|
122
127
|
"publishConfig": {
|
|
123
128
|
"access": "public"
|
|
124
|
-
},
|
|
125
|
-
"dependencies": {
|
|
126
|
-
"emoji-regex-xs": "^2.0.1",
|
|
127
|
-
"string-width": "^7.2.0"
|
|
128
129
|
}
|
|
129
130
|
}
|