eslint-plugin-markdown-preferences 0.18.0 → 0.20.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
@@ -118,7 +118,10 @@ The rules with the following star ⭐ are included in the configs.
118
118
  | [markdown-preferences/atx-heading-closing-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-heading-closing-sequence.html) | enforce consistent use of closing sequence in ATX headings. | 🔧 | |
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
+ | [markdown-preferences/code-fence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/code-fence-length.html) | enforce consistent code fence length in fenced code blocks. | 🔧 | |
122
+ | [markdown-preferences/code-fence-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/code-fence-style.html) | enforce a consistent code fence style (backtick or tilde) in Markdown fenced code blocks. | 🔧 | |
121
123
  | [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 | 🔧 | |
124
+ | [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
125
  | [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
126
  | [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
127
  | [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 +138,7 @@ The rules with the following star ⭐ are included in the configs.
135
138
  | [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
139
  | [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
140
  | [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 | 🔧 | |
141
+ | [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
142
  | [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
143
  | [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
144
  | [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
@@ -35,6 +35,16 @@ interface RuleOptions {
35
35
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html
36
36
  */
37
37
  'markdown-preferences/canonical-code-block-language'?: Linter.RuleEntry<MarkdownPreferencesCanonicalCodeBlockLanguage>;
38
+ /**
39
+ * enforce consistent code fence length in fenced code blocks.
40
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/code-fence-length.html
41
+ */
42
+ 'markdown-preferences/code-fence-length'?: Linter.RuleEntry<MarkdownPreferencesCodeFenceLength>;
43
+ /**
44
+ * enforce a consistent code fence style (backtick or tilde) in Markdown fenced code blocks.
45
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/code-fence-style.html
46
+ */
47
+ 'markdown-preferences/code-fence-style'?: Linter.RuleEntry<MarkdownPreferencesCodeFenceStyle>;
38
48
  /**
39
49
  * require link definitions and footnote definitions to be placed at the end of the document
40
50
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/definitions-last.html
@@ -45,6 +55,11 @@ interface RuleOptions {
45
55
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html
46
56
  */
47
57
  'markdown-preferences/emoji-notation'?: Linter.RuleEntry<MarkdownPreferencesEmojiNotation>;
58
+ /**
59
+ * enforce a consistent delimiter style for emphasis and strong emphasis
60
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emphasis-delimiters-style.html
61
+ */
62
+ 'markdown-preferences/emphasis-delimiters-style'?: Linter.RuleEntry<MarkdownPreferencesEmphasisDelimitersStyle>;
48
63
  /**
49
64
  * enforce consistent hard linebreak style.
50
65
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html
@@ -145,6 +160,11 @@ interface RuleOptions {
145
160
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
146
161
  */
147
162
  'markdown-preferences/sort-definitions'?: Linter.RuleEntry<MarkdownPreferencesSortDefinitions>;
163
+ /**
164
+ * enforce a consistent delimiter style for strikethrough
165
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/strikethrough-delimiters-style.html
166
+ */
167
+ 'markdown-preferences/strikethrough-delimiters-style'?: Linter.RuleEntry<MarkdownPreferencesStrikethroughDelimitersStyle>;
148
168
  /**
149
169
  * enforce consistent casing in table header cells.
150
170
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
@@ -188,11 +208,40 @@ type MarkdownPreferencesCanonicalCodeBlockLanguage = [] | [{
188
208
  [k: string]: string;
189
209
  };
190
210
  }];
211
+ type MarkdownPreferencesCodeFenceLength = [] | [{
212
+ length?: number;
213
+ fallbackLength?: (number | ("minimum" | "as-is"));
214
+ overrides?: {
215
+ lang: string;
216
+ length?: number;
217
+ fallbackLength?: (number | ("minimum" | "as-is"));
218
+ }[];
219
+ }];
220
+ type MarkdownPreferencesCodeFenceStyle = [] | [{
221
+ style?: ("backtick" | "tilde");
222
+ }];
191
223
  type MarkdownPreferencesEmojiNotation = [] | [{
192
224
  prefer?: ("unicode" | "colon");
193
225
  ignoreUnknown?: boolean;
194
226
  ignoreList?: string[];
195
227
  }];
228
+ type MarkdownPreferencesEmphasisDelimitersStyle = [] | [{
229
+ emphasis?: ("*" | "_");
230
+ strong?: ("**" | "__");
231
+ strongEmphasis?: (("***" | "___") | {
232
+ outer: "*";
233
+ inner: "__";
234
+ } | {
235
+ outer: "**";
236
+ inner: "_";
237
+ } | {
238
+ outer: "_";
239
+ inner: "**";
240
+ } | {
241
+ outer: "__";
242
+ inner: "*";
243
+ });
244
+ }];
196
245
  type MarkdownPreferencesHardLinebreakStyle = [] | [{
197
246
  style?: ("backslash" | "spaces");
198
247
  }];
@@ -283,6 +332,9 @@ type MarkdownPreferencesSortDefinitions = [] | [{
283
332
  })[];
284
333
  alphabetical?: boolean;
285
334
  }];
335
+ type MarkdownPreferencesStrikethroughDelimitersStyle = [] | [{
336
+ delimiter?: ("~" | "~~");
337
+ }];
286
338
  type MarkdownPreferencesTableHeaderCasing = [] | [{
287
339
  style?: ("Title Case" | "Sentence case");
288
340
  preserveWords?: string[];
@@ -316,7 +368,7 @@ declare namespace meta_d_exports {
316
368
  export { name, version };
317
369
  }
318
370
  declare const name: "eslint-plugin-markdown-preferences";
319
- declare const version: "0.18.0";
371
+ declare const version: "0.20.0";
320
372
  //#endregion
321
373
  //#region src/index.d.ts
322
374
  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
  }
@@ -852,6 +873,108 @@ var bullet_list_marker_style_default = createRule("bullet-list-marker-style", {
852
873
  }
853
874
  });
854
875
 
876
+ //#endregion
877
+ //#region src/utils/fenced-code-block.ts
878
+ const RE_OPENING_FENCE = /^(`{3,}|~{3,})/u;
879
+ const RE_LANGUAGE = /^(\w*)/u;
880
+ /**
881
+ * Parse the fenced code block.
882
+ */
883
+ function parseFencedCodeBlock(sourceCode, node) {
884
+ const loc = sourceCode.getLoc(node);
885
+ const range = sourceCode.getRange(node);
886
+ const text = sourceCode.text.slice(...range);
887
+ const match = RE_OPENING_FENCE.exec(text);
888
+ if (!match) return null;
889
+ const [, fenceText] = match;
890
+ const fenceChar = fenceText[0];
891
+ let closingFenceText = "";
892
+ const trimmed = text.trimEnd();
893
+ const trailingSpacesLength = text.length - trimmed.length;
894
+ for (let index = trimmed.length - 1; index >= 0; index--) {
895
+ const c = trimmed[index];
896
+ if (c === fenceChar || isSpaceOrTab(c)) {
897
+ closingFenceText = c + closingFenceText;
898
+ continue;
899
+ }
900
+ if (c === "\n") break;
901
+ return null;
902
+ }
903
+ closingFenceText = closingFenceText.trimStart();
904
+ if (!closingFenceText || !closingFenceText.startsWith(fenceText)) return null;
905
+ const lines = getParsedLines(sourceCode);
906
+ const afterOpeningFence = lines.get(loc.start.line).text.slice(fenceText.length);
907
+ const trimmedAfterOpeningFence = afterOpeningFence.trimStart();
908
+ const spaceAfterOpeningFenceLength = afterOpeningFence.length - trimmedAfterOpeningFence.length;
909
+ let languageText = "";
910
+ if (trimmedAfterOpeningFence) {
911
+ const langMatch = RE_LANGUAGE.exec(trimmedAfterOpeningFence);
912
+ languageText = langMatch[1];
913
+ }
914
+ const afterLanguage = trimmedAfterOpeningFence.slice(languageText.length);
915
+ const trimmedAfterLanguage = afterLanguage.trimStart();
916
+ const spaceAfterLanguageLength = afterLanguage.length - trimmedAfterLanguage.length;
917
+ const metaText = trimmedAfterLanguage.trimEnd();
918
+ const openingFence = {
919
+ text: fenceText,
920
+ range: [range[0], range[0] + fenceText.length],
921
+ loc: {
922
+ start: loc.start,
923
+ end: {
924
+ line: loc.start.line,
925
+ column: loc.start.column + fenceText.length
926
+ }
927
+ }
928
+ };
929
+ const language$1 = languageText ? {
930
+ text: languageText,
931
+ range: [openingFence.range[1] + spaceAfterOpeningFenceLength, openingFence.range[1] + spaceAfterOpeningFenceLength + languageText.length],
932
+ loc: {
933
+ start: {
934
+ line: openingFence.loc.end.line,
935
+ column: openingFence.loc.end.column + spaceAfterOpeningFenceLength
936
+ },
937
+ end: {
938
+ line: openingFence.loc.end.line,
939
+ column: openingFence.loc.end.column + spaceAfterOpeningFenceLength + languageText.length
940
+ }
941
+ }
942
+ } : null;
943
+ const meta = language$1 && metaText ? {
944
+ text: metaText,
945
+ range: [language$1.range[1] + spaceAfterLanguageLength, language$1.range[1] + spaceAfterLanguageLength + metaText.length],
946
+ loc: {
947
+ start: {
948
+ line: language$1.loc.end.line,
949
+ column: language$1.loc.end.column + spaceAfterLanguageLength
950
+ },
951
+ end: {
952
+ line: language$1.loc.end.line,
953
+ column: language$1.loc.end.column + spaceAfterLanguageLength + metaText.length
954
+ }
955
+ }
956
+ } : null;
957
+ return {
958
+ openingFence,
959
+ language: language$1,
960
+ meta,
961
+ closingFence: {
962
+ text: closingFenceText,
963
+ range: [range[1] - trailingSpacesLength - closingFenceText.length, range[1] - trailingSpacesLength],
964
+ loc: {
965
+ start: {
966
+ line: loc.end.line,
967
+ column: loc.end.column - trailingSpacesLength - closingFenceText.length
968
+ },
969
+ end: {
970
+ line: loc.end.line,
971
+ column: loc.end.column - trailingSpacesLength
972
+ }
973
+ }
974
+ }
975
+ };
976
+ }
977
+
855
978
  //#endregion
856
979
  //#region src/rules/canonical-code-block-language.ts
857
980
  const DEFAULT_LANGUAGES = {
@@ -906,23 +1029,204 @@ var canonical_code_block_language_default = createRule("canonical-code-block-lan
906
1029
  const canonical = languages[node.lang];
907
1030
  const current = node.lang;
908
1031
  if (current === canonical) return;
909
- const nodeRange = sourceCode.getRange(node);
910
- const nodeText = sourceCode.text.slice(nodeRange[0], nodeRange[1]);
911
- const fenceRegex = /^(`{3,}|~{3,})(\w*)(?:\s.*)?$/mu;
912
- const fenceMatch = fenceRegex.exec(nodeText.split("\n")[0]);
913
- if (!fenceMatch) return;
914
- const [, fence, langInfo] = fenceMatch;
915
- const range = [nodeRange[0] + fence.length, nodeRange[0] + fence.length + langInfo.length];
1032
+ const parsed = parseFencedCodeBlock(sourceCode, node);
1033
+ if (!parsed || !parsed.language) return;
916
1034
  context.report({
917
1035
  node,
918
- loc: getSourceLocationFromRange(sourceCode, node, range),
1036
+ loc: parsed.language.loc,
919
1037
  messageId: "useCanonical",
920
1038
  data: {
921
1039
  canonical,
922
1040
  current
923
1041
  },
924
1042
  fix(fixer) {
925
- return fixer.replaceTextRange(range, canonical);
1043
+ return fixer.replaceTextRange(parsed.language.range, canonical);
1044
+ }
1045
+ });
1046
+ } };
1047
+ }
1048
+ });
1049
+
1050
+ //#endregion
1051
+ //#region src/rules/code-fence-length.ts
1052
+ var code_fence_length_default = createRule("code-fence-length", {
1053
+ meta: {
1054
+ type: "layout",
1055
+ docs: {
1056
+ description: "enforce consistent code fence length in fenced code blocks.",
1057
+ categories: [],
1058
+ listCategory: "Stylistic"
1059
+ },
1060
+ fixable: "code",
1061
+ hasSuggestions: false,
1062
+ schema: [{
1063
+ type: "object",
1064
+ properties: {
1065
+ length: {
1066
+ type: "integer",
1067
+ minimum: 3
1068
+ },
1069
+ fallbackLength: { anyOf: [{
1070
+ type: "integer",
1071
+ minimum: 3
1072
+ }, { enum: ["minimum", "as-is"] }] },
1073
+ overrides: {
1074
+ type: "array",
1075
+ items: {
1076
+ type: "object",
1077
+ properties: {
1078
+ lang: { type: "string" },
1079
+ length: {
1080
+ type: "integer",
1081
+ minimum: 3
1082
+ },
1083
+ fallbackLength: { anyOf: [{
1084
+ type: "integer",
1085
+ minimum: 3
1086
+ }, { enum: ["minimum", "as-is"] }] }
1087
+ },
1088
+ required: ["lang"],
1089
+ additionalProperties: false
1090
+ }
1091
+ }
1092
+ },
1093
+ additionalProperties: false
1094
+ }],
1095
+ messages: {
1096
+ notMatch: "The opening and closing code fence lengths must match.",
1097
+ notPreferred: "Code fence length should be {{expected}} (was {{actual}})."
1098
+ }
1099
+ },
1100
+ create(context) {
1101
+ const sourceCode = context.sourceCode;
1102
+ const options = context.options[0] ?? {};
1103
+ /**
1104
+ * Get the effective options for the given code block node.
1105
+ */
1106
+ function getOptionForCode(node) {
1107
+ const override = options.overrides?.find((o) => o.lang === node.lang);
1108
+ return {
1109
+ length: override?.length ?? options.length ?? 3,
1110
+ fallbackLength: override?.fallbackLength ?? options.fallbackLength ?? "minimum"
1111
+ };
1112
+ }
1113
+ /**
1114
+ * Report the given code block node for not preferred length.
1115
+ */
1116
+ function reportNotPreferred(node, parsed, length) {
1117
+ const expectedFence = getExpectedFence(parsed, length);
1118
+ context.report({
1119
+ node,
1120
+ loc: parsed.openingFence.loc,
1121
+ data: {
1122
+ expected: expectedFence,
1123
+ actual: parsed.openingFence.text
1124
+ },
1125
+ messageId: "notPreferred",
1126
+ fix(fixer) {
1127
+ return [fixer.replaceTextRange(parsed.openingFence.range, expectedFence), fixer.replaceTextRange(parsed.closingFence.range, expectedFence)];
1128
+ }
1129
+ });
1130
+ }
1131
+ /**
1132
+ * Verify the length of the given code block node.
1133
+ */
1134
+ function verifyFenceLength(node, parsed) {
1135
+ const { length, fallbackLength } = getOptionForCode(node);
1136
+ if (parsed.openingFence.text.length === length) return true;
1137
+ const expectedFence = getExpectedFence(parsed, length);
1138
+ if (!node.value.includes(expectedFence)) {
1139
+ reportNotPreferred(node, parsed, length);
1140
+ return false;
1141
+ }
1142
+ if (fallbackLength === "as-is") return true;
1143
+ if (fallbackLength === "minimum") {
1144
+ let fallback = length + 1;
1145
+ while (node.value.includes(getExpectedFence(parsed, fallback))) fallback++;
1146
+ if (parsed.openingFence.text.length === fallback) return true;
1147
+ reportNotPreferred(node, parsed, fallback);
1148
+ return false;
1149
+ }
1150
+ if (fallbackLength <= length) return true;
1151
+ if (parsed.openingFence.text.length === fallbackLength) return true;
1152
+ const fallbackExpectedFence = getExpectedFence(parsed, fallbackLength);
1153
+ if (node.value.includes(fallbackExpectedFence)) return true;
1154
+ reportNotPreferred(node, parsed, fallbackLength);
1155
+ return false;
1156
+ }
1157
+ /**
1158
+ * Get the expected fence string for the given length.
1159
+ */
1160
+ function getExpectedFence(parsed, length) {
1161
+ const fenceChar = parsed.openingFence.text[0];
1162
+ return fenceChar.repeat(Math.max(3, length));
1163
+ }
1164
+ /**
1165
+ * Verify that the opening and closing fence lengths match.
1166
+ */
1167
+ function verifyClosingFenceLength(node, parsed) {
1168
+ if (parsed.openingFence.text.length === parsed.closingFence.text.length) return true;
1169
+ context.report({
1170
+ node,
1171
+ loc: parsed.closingFence.loc,
1172
+ messageId: "notMatch",
1173
+ fix(fixer) {
1174
+ return [fixer.replaceTextRange(parsed.closingFence.range, parsed.openingFence.text)];
1175
+ }
1176
+ });
1177
+ return false;
1178
+ }
1179
+ return { code(node) {
1180
+ const parsed = parseFencedCodeBlock(sourceCode, node);
1181
+ if (!parsed) return;
1182
+ if (!verifyFenceLength(node, parsed)) return;
1183
+ verifyClosingFenceLength(node, parsed);
1184
+ } };
1185
+ }
1186
+ });
1187
+
1188
+ //#endregion
1189
+ //#region src/rules/code-fence-style.ts
1190
+ var code_fence_style_default = createRule("code-fence-style", {
1191
+ meta: {
1192
+ type: "layout",
1193
+ docs: {
1194
+ description: "enforce a consistent code fence style (backtick or tilde) in Markdown fenced code blocks.",
1195
+ categories: [],
1196
+ listCategory: "Stylistic"
1197
+ },
1198
+ fixable: "code",
1199
+ hasSuggestions: false,
1200
+ schema: [{
1201
+ type: "object",
1202
+ properties: { style: {
1203
+ type: "string",
1204
+ enum: ["backtick", "tilde"]
1205
+ } },
1206
+ additionalProperties: false
1207
+ }],
1208
+ messages: { useCodeFenceStyle: "Use {{expected}} code fence style instead of {{actual}}." }
1209
+ },
1210
+ create(context) {
1211
+ const sourceCode = context.sourceCode;
1212
+ const styleOption = context.options[0]?.style ?? "backtick";
1213
+ const expectedChar = styleOption === "tilde" ? "~" : "`";
1214
+ return { code(node) {
1215
+ const parsed = parseFencedCodeBlock(sourceCode, node);
1216
+ if (!parsed) return;
1217
+ if (parsed.openingFence.text.includes(expectedChar)) return;
1218
+ const expectedOpeningFence = expectedChar.repeat(Math.max(3, parsed.openingFence.text.length));
1219
+ if (node.value.includes(expectedOpeningFence)) return;
1220
+ context.report({
1221
+ node,
1222
+ loc: parsed.openingFence.loc,
1223
+ data: {
1224
+ expected: expectedOpeningFence,
1225
+ actual: parsed.openingFence.text
1226
+ },
1227
+ messageId: "useCodeFenceStyle",
1228
+ fix(fixer) {
1229
+ return [fixer.replaceTextRange(parsed.openingFence.range, expectedOpeningFence), fixer.replaceTextRange(parsed.closingFence.range, expectedChar.repeat(Math.max(3, parsed.closingFence.text.length)))];
926
1230
  }
927
1231
  });
928
1232
  } };
@@ -3043,6 +3347,199 @@ var emoji_notation_default = createRule("emoji-notation", {
3043
3347
  }
3044
3348
  });
3045
3349
 
3350
+ //#endregion
3351
+ //#region src/rules/emphasis-delimiters-style.ts
3352
+ /**
3353
+ * Check if the emphasis/strong node is intraword (inside a word)
3354
+ * CommonMark: Intraword emphasis with _ is not allowed
3355
+ */
3356
+ function isIntrawordForUnderline(text, range) {
3357
+ const before = text[range[0] - 1];
3358
+ if (!isWhitespace(before) && !isPunctuation(before)) return true;
3359
+ const after = text[range[1]];
3360
+ if (!isWhitespace(after) && !isPunctuation(after)) return true;
3361
+ return false;
3362
+ }
3363
+ var emphasis_delimiters_style_default = createRule("emphasis-delimiters-style", {
3364
+ meta: {
3365
+ type: "layout",
3366
+ docs: {
3367
+ description: "enforce a consistent delimiter style for emphasis and strong emphasis",
3368
+ categories: [],
3369
+ listCategory: "Stylistic"
3370
+ },
3371
+ fixable: "code",
3372
+ hasSuggestions: false,
3373
+ schema: [{
3374
+ type: "object",
3375
+ properties: {
3376
+ emphasis: { enum: ["*", "_"] },
3377
+ strong: { enum: ["**", "__"] },
3378
+ strongEmphasis: { anyOf: [
3379
+ { enum: ["***", "___"] },
3380
+ {
3381
+ type: "object",
3382
+ properties: {
3383
+ outer: { const: "*" },
3384
+ inner: { const: "__" }
3385
+ },
3386
+ required: ["outer", "inner"],
3387
+ additionalProperties: false
3388
+ },
3389
+ {
3390
+ type: "object",
3391
+ properties: {
3392
+ outer: { const: "**" },
3393
+ inner: { const: "_" }
3394
+ },
3395
+ required: ["outer", "inner"],
3396
+ additionalProperties: false
3397
+ },
3398
+ {
3399
+ type: "object",
3400
+ properties: {
3401
+ outer: { const: "_" },
3402
+ inner: { const: "**" }
3403
+ },
3404
+ required: ["outer", "inner"],
3405
+ additionalProperties: false
3406
+ },
3407
+ {
3408
+ type: "object",
3409
+ properties: {
3410
+ outer: { const: "__" },
3411
+ inner: { const: "*" }
3412
+ },
3413
+ required: ["outer", "inner"],
3414
+ additionalProperties: false
3415
+ }
3416
+ ] }
3417
+ },
3418
+ additionalProperties: false
3419
+ }],
3420
+ messages: {
3421
+ wrongEmphasis: "Emphasis delimiter should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3422
+ wrongStrong: "Strong emphasis delimiter should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3423
+ wrongStrongEmphasis: "Delimiter for strong+emphasis should be '{{expectedOpening}}' (was '{{actualOpening}}').",
3424
+ wrongStrongEmphasisDelimiterPair: "Delimiters for strong+emphasis should be '{{expectedOpening}}' ... '{{expectedClosing}}' (was '{{actualOpening}}' ... '{{actualClosing}}')."
3425
+ }
3426
+ },
3427
+ create(context) {
3428
+ const sourceCode = context.sourceCode;
3429
+ const option = context.options[0] ?? {};
3430
+ const emphasisDelimiter = option.emphasis ?? "_";
3431
+ const emphasis = {
3432
+ opening: emphasisDelimiter,
3433
+ closing: emphasisDelimiter
3434
+ };
3435
+ const strongDelimiter = option.strong ?? "**";
3436
+ const strong = {
3437
+ opening: strongDelimiter,
3438
+ closing: strongDelimiter
3439
+ };
3440
+ const strongEmphasis = parseStrongEmphasis(emphasisDelimiter, strongDelimiter, option.strongEmphasis);
3441
+ const processed = /* @__PURE__ */ new Set();
3442
+ /**
3443
+ * Verify the delimiter of the node
3444
+ */
3445
+ function verifyDelimiter(node, expected, messageId) {
3446
+ const range = sourceCode.getRange(node);
3447
+ if (sourceCode.text.startsWith(expected.opening, range[0]) && sourceCode.text.endsWith(expected.closing, range[1])) return;
3448
+ 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;
3449
+ if ((expected.opening.startsWith("_") || expected.closing.at(-1) === "_") && isIntrawordForUnderline(sourceCode.text, range)) return;
3450
+ const actual = {
3451
+ opening: sourceCode.text.slice(range[0], range[0] + expected.opening.length),
3452
+ closing: sourceCode.text.slice(range[1] - expected.closing.length, range[1])
3453
+ };
3454
+ context.report({
3455
+ node,
3456
+ messageId: expected.opening === expected.closing && actual.opening === actual.closing ? messageId : "wrongStrongEmphasisDelimiterPair",
3457
+ data: {
3458
+ expectedOpening: expected.opening,
3459
+ actualOpening: actual.opening,
3460
+ expectedClosing: expected.closing,
3461
+ actualClosing: actual.closing
3462
+ },
3463
+ fix(fixer) {
3464
+ return [fixer.replaceTextRange([range[0], range[0] + expected.opening.length], expected.opening), fixer.replaceTextRange([range[1] - expected.closing.length, range[1]], expected.closing)];
3465
+ }
3466
+ });
3467
+ }
3468
+ /**
3469
+ * Verify the emphasis node has the correct delimiter
3470
+ */
3471
+ function verifyEmphasis(node) {
3472
+ verifyDelimiter(node, emphasis, "wrongEmphasis");
3473
+ }
3474
+ /**
3475
+ * Verify the emphasis node has the correct delimiter
3476
+ */
3477
+ function verifyStrong(node) {
3478
+ verifyDelimiter(node, strong, "wrongStrong");
3479
+ }
3480
+ /**
3481
+ * Verify the strong emphasis node has the correct delimiter
3482
+ */
3483
+ function verifyStrongEmphasis(node) {
3484
+ verifyDelimiter(node, strongEmphasis, "wrongStrongEmphasis");
3485
+ }
3486
+ return {
3487
+ emphasis(node) {
3488
+ if (processed.has(node)) return;
3489
+ processed.add(node);
3490
+ if (node.children.length === 1 && node.children[0].type === "strong") {
3491
+ processed.add(node.children[0]);
3492
+ verifyStrongEmphasis(node);
3493
+ return;
3494
+ }
3495
+ verifyEmphasis(node);
3496
+ },
3497
+ strong(node) {
3498
+ if (processed.has(node)) return;
3499
+ processed.add(node);
3500
+ if (node.children.length === 1 && node.children[0].type === "emphasis") {
3501
+ processed.add(node.children[0]);
3502
+ verifyStrongEmphasis(node);
3503
+ return;
3504
+ }
3505
+ verifyStrong(node);
3506
+ }
3507
+ };
3508
+ }
3509
+ });
3510
+ /**
3511
+ * Parse strongEmphasis option to normalized object form
3512
+ */
3513
+ function parseStrongEmphasis(emphasis, strong, strongEmphasisOpt) {
3514
+ if (strongEmphasisOpt != null) {
3515
+ if (typeof strongEmphasisOpt === "string") return {
3516
+ opening: strongEmphasisOpt,
3517
+ closing: strongEmphasisOpt
3518
+ };
3519
+ return {
3520
+ opening: strongEmphasisOpt.outer + strongEmphasisOpt.inner,
3521
+ closing: strongEmphasisOpt.inner + strongEmphasisOpt.outer
3522
+ };
3523
+ }
3524
+ if (emphasis === "*") {
3525
+ if (strong === "**") return {
3526
+ opening: "***",
3527
+ closing: "***"
3528
+ };
3529
+ return {
3530
+ opening: "*__",
3531
+ closing: "__*"
3532
+ };
3533
+ } else if (strong === "**") return {
3534
+ opening: "**_",
3535
+ closing: "_**"
3536
+ };
3537
+ return {
3538
+ opening: "___",
3539
+ closing: "___"
3540
+ };
3541
+ }
3542
+
3046
3543
  //#endregion
3047
3544
  //#region src/rules/hard-linebreak-style.ts
3048
3545
  var hard_linebreak_style_default = createRule("hard-linebreak-style", {
@@ -6183,7 +6680,7 @@ var sort_definitions_default = createRule("sort-definitions", {
6183
6680
  const last = group.at(-1);
6184
6681
  if (last && (node.type !== "definition" && node.type !== "footnoteDefinition" || sourceCode.getParent(node) !== sourceCode.getParent(last))) {
6185
6682
  const range = sourceCode.getRange(node);
6186
- const lastDefinitionRange = sourceCode.getRange(node);
6683
+ const lastDefinitionRange = sourceCode.getRange(last);
6187
6684
  if (lastDefinitionRange[1] <= range[0]) {
6188
6685
  verify(group);
6189
6686
  group.length = 0;
@@ -6341,6 +6838,48 @@ function normalizedURL(url) {
6341
6838
  return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
6342
6839
  }
6343
6840
 
6841
+ //#endregion
6842
+ //#region src/rules/strikethrough-delimiters-style.ts
6843
+ var strikethrough_delimiters_style_default = createRule("strikethrough-delimiters-style", {
6844
+ meta: {
6845
+ type: "layout",
6846
+ docs: {
6847
+ description: "enforce a consistent delimiter style for strikethrough",
6848
+ categories: [],
6849
+ listCategory: "Stylistic"
6850
+ },
6851
+ fixable: "code",
6852
+ hasSuggestions: false,
6853
+ schema: [{
6854
+ type: "object",
6855
+ properties: { delimiter: { enum: ["~", "~~"] } },
6856
+ additionalProperties: false
6857
+ }],
6858
+ messages: { wrongDelimiter: "Strikethrough delimiter should be '{{expected}}' (was '{{actual}}')." }
6859
+ },
6860
+ create(context) {
6861
+ const sourceCode = context.sourceCode;
6862
+ const option = context.options[0] ?? {};
6863
+ const delimiter = option.delimiter ?? "~~";
6864
+ return { delete(node) {
6865
+ const range = sourceCode.getRange(node);
6866
+ const actualDelimiter = ["~~", "~"].find((d) => sourceCode.text.startsWith(d, range[0]) && sourceCode.text.endsWith(d, range[1]));
6867
+ if (!actualDelimiter || actualDelimiter === delimiter) return;
6868
+ context.report({
6869
+ node,
6870
+ messageId: "wrongDelimiter",
6871
+ data: {
6872
+ expected: delimiter,
6873
+ actual: actualDelimiter
6874
+ },
6875
+ fix(fixer) {
6876
+ return [fixer.replaceTextRange([range[0], range[0] + actualDelimiter.length], delimiter), fixer.replaceTextRange([range[1] - actualDelimiter.length, range[1]], delimiter)];
6877
+ }
6878
+ });
6879
+ } };
6880
+ }
6881
+ });
6882
+
6344
6883
  //#endregion
6345
6884
  //#region src/rules/table-header-casing.ts
6346
6885
  var table_header_casing_default = createRule("table-header-casing", {
@@ -6694,8 +7233,11 @@ const rules$1 = [
6694
7233
  blockquote_marker_alignment_default,
6695
7234
  bullet_list_marker_style_default,
6696
7235
  canonical_code_block_language_default,
7236
+ code_fence_length_default,
7237
+ code_fence_style_default,
6697
7238
  definitions_last_default,
6698
7239
  emoji_notation_default,
7240
+ emphasis_delimiters_style_default,
6699
7241
  hard_linebreak_style_default,
6700
7242
  heading_casing_default,
6701
7243
  level1_heading_style_default,
@@ -6716,6 +7258,7 @@ const rules$1 = [
6716
7258
  prefer_linked_words_default,
6717
7259
  setext_heading_underline_length_default,
6718
7260
  sort_definitions_default,
7261
+ strikethrough_delimiters_style_default,
6719
7262
  table_header_casing_default,
6720
7263
  thematic_break_character_style_default,
6721
7264
  thematic_break_length_default,
@@ -6762,7 +7305,7 @@ __export(meta_exports, {
6762
7305
  version: () => version
6763
7306
  });
6764
7307
  const name = "eslint-plugin-markdown-preferences";
6765
- const version = "0.18.0";
7308
+ const version = "0.20.0";
6766
7309
 
6767
7310
  //#endregion
6768
7311
  //#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.20.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {