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 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) | Enforce consistent emoji notation style in Markdown files. | 🔧 | |
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
- * Enforce consistent emoji notation style in Markdown files.
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.13.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: "Enforce consistent emoji notation style in Markdown files.",
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/rules/heading-casing.ts
3192
+ //#region src/utils/word-casing.ts
3191
3193
  /**
3192
- * Parse preserve words and phrases from the options
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 parsePreserveWords(preserveWordsOption) {
3197
- const preserveWords = /* @__PURE__ */ new Map();
3198
- /**
3199
- * Add a single word to the preserveWords map
3200
- */
3201
- function addPreserveWord(word) {
3202
- const lowerWord = word.toLowerCase();
3203
- let list = preserveWords.get(lowerWord);
3204
- if (!list) {
3205
- list = [];
3206
- preserveWords.set(lowerWord, list);
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
- list.push(word);
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
- const preservePhrases = /* @__PURE__ */ new Map();
3211
- for (const word of preserveWordsOption) {
3212
- const splitted = word.split(/\s+/);
3213
- if (splitted.length <= 1) addPreserveWord(word);
3214
- else {
3215
- preservePhrases.set(word, splitted);
3216
- addPreserveWord(splitted.join(""));
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
- return {
3220
- preserveWords,
3221
- preservePhrases: [...preservePhrases.values()].sort((a, b) => b.length - a.length)
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 parseText(text, firstNode, lastNode) {
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
- * Convert a single word based on case style and context
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 { preserveWords, preservePhrases } = parsePreserveWords(context.options[0]?.preserveWords || defaultPreserveWords);
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*(?:API|Api)$/u",
3333
- "/\\w*(?:SDK|Sdk)$/u",
3334
- "/\\w*(?:CLI|Cli)$/u"
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 = parseText(text, firstNode, lastNode);
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(wordAndOffsets, index);
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 = preserveWords.get(wordAndOffset.word.toLowerCase());
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 = convertWord(wordAndOffset, caseStyle, minorWords);
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 === node.children.length - 1);
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: "suggestion",
3740
+ type: "layout",
3714
3741
  docs: {
3715
3742
  description: "disallow text backslash at the end of a line.",
3716
3743
  categories: ["recommended"],
3717
- listCategory: "Preference"
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.13.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.13.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
  }