eslint-plugin-markdown-preferences 0.18.0 → 0.19.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
@@ -119,6 +119,7 @@ The rules with the following star ⭐ are included in the configs.
119
119
  | [markdown-preferences/blockquote-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/blockquote-marker-alignment.html) | enforce consistent alignment of blockquote markers | 🔧 | ⭐ |
120
120
  | [markdown-preferences/bullet-list-marker-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/bullet-list-marker-style.html) | enforce consistent bullet list (unordered list) marker style | 🔧 | |
121
121
  | [markdown-preferences/definitions-last](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html) | require link definitions and footnote definitions to be placed at the end of the document | 🔧 | |
122
+ | [markdown-preferences/emphasis-delimiters-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emphasis-delimiters-style.html) | enforce a consistent delimiter style for emphasis and strong emphasis | 🔧 | |
122
123
  | [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
123
124
  | [markdown-preferences/level1-heading-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/level1-heading-style.html) | enforce consistent style for level 1 headings | 🔧 | |
124
125
  | [markdown-preferences/level2-heading-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/level2-heading-style.html) | enforce consistent style for level 2 headings | 🔧 | |
@@ -135,6 +136,7 @@ The rules with the following star ⭐ are included in the configs.
135
136
  | [markdown-preferences/prefer-link-reference-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-link-reference-definitions.html) | enforce using link reference definitions instead of inline links | 🔧 | |
136
137
  | [markdown-preferences/setext-heading-underline-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/setext-heading-underline-length.html) | enforce setext heading underline length | 🔧 | |
137
138
  | [markdown-preferences/sort-definitions](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html) | enforce a specific order for link definitions and footnote definitions | 🔧 | |
139
+ | [markdown-preferences/strikethrough-delimiters-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/strikethrough-delimiters-style.html) | enforce a consistent delimiter style for strikethrough | 🔧 | |
138
140
  | [markdown-preferences/thematic-break-character-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-character-style.html) | enforce consistent character style for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
139
141
  | [markdown-preferences/thematic-break-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-length.html) | enforce consistent length for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
140
142
  | [markdown-preferences/thematic-break-sequence-pattern](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/thematic-break-sequence-pattern.html) | enforce consistent repeating patterns for thematic breaks (horizontal rules) in Markdown. | 🔧 | |
package/lib/index.d.ts CHANGED
@@ -45,6 +45,11 @@ interface RuleOptions {
45
45
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html
46
46
  */
47
47
  'markdown-preferences/emoji-notation'?: Linter.RuleEntry<MarkdownPreferencesEmojiNotation>;
48
+ /**
49
+ * enforce a consistent delimiter style for emphasis and strong emphasis
50
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emphasis-delimiters-style.html
51
+ */
52
+ 'markdown-preferences/emphasis-delimiters-style'?: Linter.RuleEntry<MarkdownPreferencesEmphasisDelimitersStyle>;
48
53
  /**
49
54
  * enforce consistent hard linebreak style.
50
55
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html
@@ -145,6 +150,11 @@ interface RuleOptions {
145
150
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
146
151
  */
147
152
  'markdown-preferences/sort-definitions'?: Linter.RuleEntry<MarkdownPreferencesSortDefinitions>;
153
+ /**
154
+ * enforce a consistent delimiter style for strikethrough
155
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/strikethrough-delimiters-style.html
156
+ */
157
+ 'markdown-preferences/strikethrough-delimiters-style'?: Linter.RuleEntry<MarkdownPreferencesStrikethroughDelimitersStyle>;
148
158
  /**
149
159
  * enforce consistent casing in table header cells.
150
160
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
@@ -193,6 +203,23 @@ type MarkdownPreferencesEmojiNotation = [] | [{
193
203
  ignoreUnknown?: boolean;
194
204
  ignoreList?: string[];
195
205
  }];
206
+ type MarkdownPreferencesEmphasisDelimitersStyle = [] | [{
207
+ emphasis?: ("*" | "_");
208
+ strong?: ("**" | "__");
209
+ strongEmphasis?: (("***" | "___") | {
210
+ outer: "*";
211
+ inner: "__";
212
+ } | {
213
+ outer: "**";
214
+ inner: "_";
215
+ } | {
216
+ outer: "_";
217
+ inner: "**";
218
+ } | {
219
+ outer: "__";
220
+ inner: "*";
221
+ });
222
+ }];
196
223
  type MarkdownPreferencesHardLinebreakStyle = [] | [{
197
224
  style?: ("backslash" | "spaces");
198
225
  }];
@@ -283,6 +310,9 @@ type MarkdownPreferencesSortDefinitions = [] | [{
283
310
  })[];
284
311
  alphabetical?: boolean;
285
312
  }];
313
+ type MarkdownPreferencesStrikethroughDelimitersStyle = [] | [{
314
+ delimiter?: ("~" | "~~");
315
+ }];
286
316
  type MarkdownPreferencesTableHeaderCasing = [] | [{
287
317
  style?: ("Title Case" | "Sentence case");
288
318
  preserveWords?: string[];
@@ -316,7 +346,7 @@ declare namespace meta_d_exports {
316
346
  export { name, version };
317
347
  }
318
348
  declare const name: "eslint-plugin-markdown-preferences";
319
- declare const version: "0.18.0";
349
+ declare const version: "0.19.0";
320
350
  //#endregion
321
351
  //#region src/index.d.ts
322
352
  declare const configs: {
package/lib/index.js CHANGED
@@ -130,6 +130,27 @@ function getSourceLocationFromRange(sourceCode, node, range) {
130
130
  };
131
131
  }
132
132
 
133
+ //#endregion
134
+ //#region src/utils/unicode.ts
135
+ /**
136
+ * Check if the string is whitespace
137
+ */
138
+ function isWhitespace(string) {
139
+ return /^[\p{Zs}\t\n\f\r]+$/u.test(string);
140
+ }
141
+ /**
142
+ * Check if the string is a space or tab
143
+ */
144
+ function isSpaceOrTab(string) {
145
+ return /^[\t ]+$/u.test(string);
146
+ }
147
+ /**
148
+ * Check if the character is a punctuation character
149
+ */
150
+ function isPunctuation(char) {
151
+ return /^[\p{P}\p{S}]+$/u.test(char);
152
+ }
153
+
133
154
  //#endregion
134
155
  //#region src/utils/atx-heading.ts
135
156
  /**
@@ -262,7 +283,7 @@ function parseATXHeadingOpeningSequenceFromText(text) {
262
283
  function skipWhitespace(index) {
263
284
  let result = index;
264
285
  let c;
265
- while (result < text.length && (c = text[result]) && (c === " " || c === " ")) result++;
286
+ while (result < text.length && (c = text[result]) && isSpaceOrTab(c)) result++;
266
287
  return result;
267
288
  }
268
289
  }
@@ -287,7 +308,7 @@ function parseATXHeadingClosingSequenceFromText(text) {
287
308
  function skipEndWhitespace(index) {
288
309
  let result = index;
289
310
  let c;
290
- while (result >= 0 && (c = text[result]) && (c === " " || c === " ")) result--;
311
+ while (result >= 0 && (c = text[result]) && isSpaceOrTab(c)) result--;
291
312
  return result;
292
313
  }
293
314
  }
@@ -3043,6 +3064,199 @@ var emoji_notation_default = createRule("emoji-notation", {
3043
3064
  }
3044
3065
  });
3045
3066
 
3067
+ //#endregion
3068
+ //#region src/rules/emphasis-delimiters-style.ts
3069
+ /**
3070
+ * Check if the emphasis/strong node is intraword (inside a word)
3071
+ * CommonMark: Intraword emphasis with _ is not allowed
3072
+ */
3073
+ function isIntrawordForUnderline(text, range) {
3074
+ const before = text[range[0] - 1];
3075
+ if (!isWhitespace(before) && !isPunctuation(before)) return true;
3076
+ const after = text[range[1]];
3077
+ if (!isWhitespace(after) && !isPunctuation(after)) return true;
3078
+ return false;
3079
+ }
3080
+ var emphasis_delimiters_style_default = createRule("emphasis-delimiters-style", {
3081
+ meta: {
3082
+ type: "layout",
3083
+ docs: {
3084
+ description: "enforce a consistent delimiter style for emphasis and strong emphasis",
3085
+ categories: [],
3086
+ listCategory: "Stylistic"
3087
+ },
3088
+ fixable: "code",
3089
+ hasSuggestions: false,
3090
+ schema: [{
3091
+ type: "object",
3092
+ properties: {
3093
+ emphasis: { enum: ["*", "_"] },
3094
+ strong: { enum: ["**", "__"] },
3095
+ strongEmphasis: { anyOf: [
3096
+ { enum: ["***", "___"] },
3097
+ {
3098
+ type: "object",
3099
+ properties: {
3100
+ outer: { const: "*" },
3101
+ inner: { const: "__" }
3102
+ },
3103
+ required: ["outer", "inner"],
3104
+ additionalProperties: false
3105
+ },
3106
+ {
3107
+ type: "object",
3108
+ properties: {
3109
+ outer: { const: "**" },
3110
+ inner: { const: "_" }
3111
+ },
3112
+ required: ["outer", "inner"],
3113
+ additionalProperties: false
3114
+ },
3115
+ {
3116
+ type: "object",
3117
+ properties: {
3118
+ outer: { const: "_" },
3119
+ inner: { const: "**" }
3120
+ },
3121
+ required: ["outer", "inner"],
3122
+ additionalProperties: false
3123
+ },
3124
+ {
3125
+ type: "object",
3126
+ properties: {
3127
+ outer: { const: "__" },
3128
+ inner: { const: "*" }
3129
+ },
3130
+ required: ["outer", "inner"],
3131
+ additionalProperties: false
3132
+ }
3133
+ ] }
3134
+ },
3135
+ additionalProperties: false
3136
+ }],
3137
+ messages: {
3138
+ wrongEmphasis: "Emphasis delimiter should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3139
+ wrongStrong: "Strong emphasis delimiter should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3140
+ wrongStrongEmphasis: "Delimiter for strong+emphasis should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3141
+ wrongStrongEmphasisDelimiterPair: "Delimiters for strong+emphasis should be '{{expectedOpening}}' ... '{{expectedClosing}}' (was '{{actualOpening}}' ... '{{actualClosing}}')."
3142
+ }
3143
+ },
3144
+ create(context) {
3145
+ const sourceCode = context.sourceCode;
3146
+ const option = context.options[0] ?? {};
3147
+ const emphasisDelimiter = option.emphasis ?? "_";
3148
+ const emphasis = {
3149
+ opening: emphasisDelimiter,
3150
+ closing: emphasisDelimiter
3151
+ };
3152
+ const strongDelimiter = option.strong ?? "**";
3153
+ const strong = {
3154
+ opening: strongDelimiter,
3155
+ closing: strongDelimiter
3156
+ };
3157
+ const strongEmphasis = parseStrongEmphasis(emphasisDelimiter, strongDelimiter, option.strongEmphasis);
3158
+ const processed = /* @__PURE__ */ new Set();
3159
+ /**
3160
+ * Verify the delimiter of the node
3161
+ */
3162
+ function verifyDelimiter(node, expected, messageId) {
3163
+ const range = sourceCode.getRange(node);
3164
+ if (sourceCode.text.startsWith(expected.opening, range[0]) && sourceCode.text.endsWith(expected.closing, range[1])) return;
3165
+ if (sourceCode.text[range[0] - 1] === expected.opening[0] || sourceCode.text[range[1]] === expected.closing.at(-1) || sourceCode.text[range[0] + expected.opening.length] === expected.opening.at(-1) || sourceCode.text[range[1] - expected.closing.length - 1] === expected.closing[0]) return;
3166
+ if ((expected.opening.startsWith("_") || expected.closing.at(-1) === "_") && isIntrawordForUnderline(sourceCode.text, range)) return;
3167
+ const actual = {
3168
+ opening: sourceCode.text.slice(range[0], range[0] + expected.opening.length),
3169
+ closing: sourceCode.text.slice(range[1] - expected.closing.length, range[1])
3170
+ };
3171
+ context.report({
3172
+ node,
3173
+ messageId: expected.opening === expected.closing && actual.opening === actual.closing ? messageId : "wrongStrongEmphasisDelimiterPair",
3174
+ data: {
3175
+ expectedOpening: expected.opening,
3176
+ actualOpening: actual.opening,
3177
+ expectedClosing: expected.closing,
3178
+ actualClosing: actual.closing
3179
+ },
3180
+ fix(fixer) {
3181
+ return [fixer.replaceTextRange([range[0], range[0] + expected.opening.length], expected.opening), fixer.replaceTextRange([range[1] - expected.closing.length, range[1]], expected.closing)];
3182
+ }
3183
+ });
3184
+ }
3185
+ /**
3186
+ * Verify the emphasis node has the correct delimiter
3187
+ */
3188
+ function verifyEmphasis(node) {
3189
+ verifyDelimiter(node, emphasis, "wrongEmphasis");
3190
+ }
3191
+ /**
3192
+ * Verify the emphasis node has the correct delimiter
3193
+ */
3194
+ function verifyStrong(node) {
3195
+ verifyDelimiter(node, strong, "wrongStrong");
3196
+ }
3197
+ /**
3198
+ * Verify the strong emphasis node has the correct delimiter
3199
+ */
3200
+ function verifyStrongEmphasis(node) {
3201
+ verifyDelimiter(node, strongEmphasis, "wrongStrongEmphasis");
3202
+ }
3203
+ return {
3204
+ emphasis(node) {
3205
+ if (processed.has(node)) return;
3206
+ processed.add(node);
3207
+ if (node.children.length === 1 && node.children[0].type === "strong") {
3208
+ processed.add(node.children[0]);
3209
+ verifyStrongEmphasis(node);
3210
+ return;
3211
+ }
3212
+ verifyEmphasis(node);
3213
+ },
3214
+ strong(node) {
3215
+ if (processed.has(node)) return;
3216
+ processed.add(node);
3217
+ if (node.children.length === 1 && node.children[0].type === "emphasis") {
3218
+ processed.add(node.children[0]);
3219
+ verifyStrongEmphasis(node);
3220
+ return;
3221
+ }
3222
+ verifyStrong(node);
3223
+ }
3224
+ };
3225
+ }
3226
+ });
3227
+ /**
3228
+ * Parse strongEmphasis option to normalized object form
3229
+ */
3230
+ function parseStrongEmphasis(emphasis, strong, strongEmphasisOpt) {
3231
+ if (strongEmphasisOpt != null) {
3232
+ if (typeof strongEmphasisOpt === "string") return {
3233
+ opening: strongEmphasisOpt,
3234
+ closing: strongEmphasisOpt
3235
+ };
3236
+ return {
3237
+ opening: strongEmphasisOpt.outer + strongEmphasisOpt.inner,
3238
+ closing: strongEmphasisOpt.inner + strongEmphasisOpt.outer
3239
+ };
3240
+ }
3241
+ if (emphasis === "*") {
3242
+ if (strong === "**") return {
3243
+ opening: "***",
3244
+ closing: "***"
3245
+ };
3246
+ return {
3247
+ opening: "*__",
3248
+ closing: "__*"
3249
+ };
3250
+ } else if (strong === "**") return {
3251
+ opening: "**_",
3252
+ closing: "_**"
3253
+ };
3254
+ return {
3255
+ opening: "___",
3256
+ closing: "___"
3257
+ };
3258
+ }
3259
+
3046
3260
  //#endregion
3047
3261
  //#region src/rules/hard-linebreak-style.ts
3048
3262
  var hard_linebreak_style_default = createRule("hard-linebreak-style", {
@@ -6183,7 +6397,7 @@ var sort_definitions_default = createRule("sort-definitions", {
6183
6397
  const last = group.at(-1);
6184
6398
  if (last && (node.type !== "definition" && node.type !== "footnoteDefinition" || sourceCode.getParent(node) !== sourceCode.getParent(last))) {
6185
6399
  const range = sourceCode.getRange(node);
6186
- const lastDefinitionRange = sourceCode.getRange(node);
6400
+ const lastDefinitionRange = sourceCode.getRange(last);
6187
6401
  if (lastDefinitionRange[1] <= range[0]) {
6188
6402
  verify(group);
6189
6403
  group.length = 0;
@@ -6341,6 +6555,48 @@ function normalizedURL(url) {
6341
6555
  return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
6342
6556
  }
6343
6557
 
6558
+ //#endregion
6559
+ //#region src/rules/strikethrough-delimiters-style.ts
6560
+ var strikethrough_delimiters_style_default = createRule("strikethrough-delimiters-style", {
6561
+ meta: {
6562
+ type: "layout",
6563
+ docs: {
6564
+ description: "enforce a consistent delimiter style for strikethrough",
6565
+ categories: [],
6566
+ listCategory: "Stylistic"
6567
+ },
6568
+ fixable: "code",
6569
+ hasSuggestions: false,
6570
+ schema: [{
6571
+ type: "object",
6572
+ properties: { delimiter: { enum: ["~", "~~"] } },
6573
+ additionalProperties: false
6574
+ }],
6575
+ messages: { wrongDelimiter: "Strikethrough delimiter should be '{{expected}}' (was '{{actual}}')." }
6576
+ },
6577
+ create(context) {
6578
+ const sourceCode = context.sourceCode;
6579
+ const option = context.options[0] ?? {};
6580
+ const delimiter = option.delimiter ?? "~~";
6581
+ return { delete(node) {
6582
+ const range = sourceCode.getRange(node);
6583
+ const actualDelimiter = ["~~", "~"].find((d) => sourceCode.text.startsWith(d, range[0]) && sourceCode.text.endsWith(d, range[1]));
6584
+ if (!actualDelimiter || actualDelimiter === delimiter) return;
6585
+ context.report({
6586
+ node,
6587
+ messageId: "wrongDelimiter",
6588
+ data: {
6589
+ expected: delimiter,
6590
+ actual: actualDelimiter
6591
+ },
6592
+ fix(fixer) {
6593
+ return [fixer.replaceTextRange([range[0], range[0] + actualDelimiter.length], delimiter), fixer.replaceTextRange([range[1] - actualDelimiter.length, range[1]], delimiter)];
6594
+ }
6595
+ });
6596
+ } };
6597
+ }
6598
+ });
6599
+
6344
6600
  //#endregion
6345
6601
  //#region src/rules/table-header-casing.ts
6346
6602
  var table_header_casing_default = createRule("table-header-casing", {
@@ -6696,6 +6952,7 @@ const rules$1 = [
6696
6952
  canonical_code_block_language_default,
6697
6953
  definitions_last_default,
6698
6954
  emoji_notation_default,
6955
+ emphasis_delimiters_style_default,
6699
6956
  hard_linebreak_style_default,
6700
6957
  heading_casing_default,
6701
6958
  level1_heading_style_default,
@@ -6716,6 +6973,7 @@ const rules$1 = [
6716
6973
  prefer_linked_words_default,
6717
6974
  setext_heading_underline_length_default,
6718
6975
  sort_definitions_default,
6976
+ strikethrough_delimiters_style_default,
6719
6977
  table_header_casing_default,
6720
6978
  thematic_break_character_style_default,
6721
6979
  thematic_break_length_default,
@@ -6762,7 +7020,7 @@ __export(meta_exports, {
6762
7020
  version: () => version
6763
7021
  });
6764
7022
  const name = "eslint-plugin-markdown-preferences";
6765
- const version = "0.18.0";
7023
+ const version = "0.19.0";
6766
7024
 
6767
7025
  //#endregion
6768
7026
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {