eslint-plugin-markdown-preferences 0.19.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,6 +118,8 @@ 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 | 🔧 | |
122
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 | 🔧 | |
123
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. | 🔧 | ⭐ |
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
@@ -198,6 +208,18 @@ type MarkdownPreferencesCanonicalCodeBlockLanguage = [] | [{
198
208
  [k: string]: string;
199
209
  };
200
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
+ }];
201
223
  type MarkdownPreferencesEmojiNotation = [] | [{
202
224
  prefer?: ("unicode" | "colon");
203
225
  ignoreUnknown?: boolean;
@@ -346,7 +368,7 @@ declare namespace meta_d_exports {
346
368
  export { name, version };
347
369
  }
348
370
  declare const name: "eslint-plugin-markdown-preferences";
349
- declare const version: "0.19.0";
371
+ declare const version: "0.20.0";
350
372
  //#endregion
351
373
  //#region src/index.d.ts
352
374
  declare const configs: {
package/lib/index.js CHANGED
@@ -873,6 +873,108 @@ var bullet_list_marker_style_default = createRule("bullet-list-marker-style", {
873
873
  }
874
874
  });
875
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
+
876
978
  //#endregion
877
979
  //#region src/rules/canonical-code-block-language.ts
878
980
  const DEFAULT_LANGUAGES = {
@@ -927,23 +1029,204 @@ var canonical_code_block_language_default = createRule("canonical-code-block-lan
927
1029
  const canonical = languages[node.lang];
928
1030
  const current = node.lang;
929
1031
  if (current === canonical) return;
930
- const nodeRange = sourceCode.getRange(node);
931
- const nodeText = sourceCode.text.slice(nodeRange[0], nodeRange[1]);
932
- const fenceRegex = /^(`{3,}|~{3,})(\w*)(?:\s.*)?$/mu;
933
- const fenceMatch = fenceRegex.exec(nodeText.split("\n")[0]);
934
- if (!fenceMatch) return;
935
- const [, fence, langInfo] = fenceMatch;
936
- const range = [nodeRange[0] + fence.length, nodeRange[0] + fence.length + langInfo.length];
1032
+ const parsed = parseFencedCodeBlock(sourceCode, node);
1033
+ if (!parsed || !parsed.language) return;
937
1034
  context.report({
938
1035
  node,
939
- loc: getSourceLocationFromRange(sourceCode, node, range),
1036
+ loc: parsed.language.loc,
940
1037
  messageId: "useCanonical",
941
1038
  data: {
942
1039
  canonical,
943
1040
  current
944
1041
  },
945
1042
  fix(fixer) {
946
- 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)))];
947
1230
  }
948
1231
  });
949
1232
  } };
@@ -6950,6 +7233,8 @@ const rules$1 = [
6950
7233
  blockquote_marker_alignment_default,
6951
7234
  bullet_list_marker_style_default,
6952
7235
  canonical_code_block_language_default,
7236
+ code_fence_length_default,
7237
+ code_fence_style_default,
6953
7238
  definitions_last_default,
6954
7239
  emoji_notation_default,
6955
7240
  emphasis_delimiters_style_default,
@@ -7020,7 +7305,7 @@ __export(meta_exports, {
7020
7305
  version: () => version
7021
7306
  });
7022
7307
  const name = "eslint-plugin-markdown-preferences";
7023
- const version = "0.19.0";
7308
+ const version = "0.20.0";
7024
7309
 
7025
7310
  //#endregion
7026
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.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {