eslint-plugin-markdown-preferences 0.9.0 → 0.11.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
@@ -50,7 +50,7 @@ Example **eslint.config.js**:
50
50
  ```js
51
51
  import { defineConfig } from "eslint/config";
52
52
  // import markdown from "@eslint/markdown";
53
- import markdownPreferences from 'eslint-plugin-markdown-preferences';
53
+ import markdownPreferences from "eslint-plugin-markdown-preferences";
54
54
  export default [
55
55
  // add more generic rule sets here, such as:
56
56
  // markdown.configs.recommended,
@@ -59,8 +59,8 @@ export default [
59
59
  rules: {
60
60
  // override/add rules settings here, such as:
61
61
  // 'markdown-preferences/prefer-linked-words': 'error'
62
- }
63
- }
62
+ },
63
+ },
64
64
  ];
65
65
  ```
66
66
 
@@ -86,10 +86,13 @@ The rules with the following star ⭐ are included in the configs.
86
86
 
87
87
  <!--RULES_TABLE_START-->
88
88
 
89
+ <!-- prettier-ignore-start -->
90
+
89
91
  ### Preference Rules
90
92
 
91
93
  | Rule ID | Description | Fixable | RECOMMENDED |
92
94
  |:--------|:------------|:-------:|:-----------:|
95
+ | [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 | 🔧 | |
93
96
  | [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. | | ⭐ |
94
97
  | [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. | 🔧 | |
95
98
  | [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. | 🔧 | |
@@ -98,14 +101,19 @@ The rules with the following star ⭐ are included in the configs.
98
101
 
99
102
  | Rule ID | Description | Fixable | RECOMMENDED |
100
103
  |:--------|:------------|:-------:|:-----------:|
101
- | [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 | 🔧 | |
102
104
  | [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 | 🔧 | |
103
105
  | [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
104
106
  | [markdown-preferences/heading-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html) | enforce consistent casing in headings. | 🔧 | |
107
+ | [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐ |
108
+ | [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. | 🔧 | |
105
109
  | [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. | 🔧 | |
110
+ | [markdown-preferences/prefer-autolinks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html) | enforce the use of autolinks for URLs | 🔧 | ⭐ |
111
+ | [markdown-preferences/prefer-fenced-code-blocks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-fenced-code-blocks.html) | enforce the use of fenced code blocks over indented code blocks | 🔧 | ⭐ |
106
112
  | [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 | 🔧 | |
107
113
  | [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 | 🔧 | |
108
114
 
115
+ <!-- prettier-ignore-end -->
116
+
109
117
  <!--RULES_TABLE_END-->
110
118
  <!--RULES_SECTION_END-->
111
119
  <!--DOCS_IGNORE_START-->
@@ -123,7 +131,7 @@ Please use GitHub's Issues/PRs.
123
131
 
124
132
  ## 🔒 License
125
133
 
126
- See the [LICENSE](LICENSE) file for license rights and limitations (MIT).
134
+ See the [LICENSE](./LICENSE) file for license rights and limitations (MIT).
127
135
 
128
136
  [npm-package]: https://www.npmjs.com/package/eslint-plugin-markdown-preferences
129
137
  [npmtrends]: http://www.npmtrends.com/eslint-plugin-markdown-preferences
package/lib/index.d.ts CHANGED
@@ -30,6 +30,16 @@ interface RuleOptions {
30
30
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html
31
31
  */
32
32
  'markdown-preferences/heading-casing'?: Linter.RuleEntry<MarkdownPreferencesHeadingCasing>;
33
+ /**
34
+ * disallow laziness in blockquotes
35
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html
36
+ */
37
+ 'markdown-preferences/no-laziness-blockquotes'?: Linter.RuleEntry<[]>;
38
+ /**
39
+ * disallow multiple empty lines in Markdown files.
40
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-multiple-empty-lines.html
41
+ */
42
+ 'markdown-preferences/no-multiple-empty-lines'?: Linter.RuleEntry<MarkdownPreferencesNoMultipleEmptyLines>;
33
43
  /**
34
44
  * disallow text backslash at the end of a line.
35
45
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html
@@ -40,6 +50,16 @@ interface RuleOptions {
40
50
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html
41
51
  */
42
52
  'markdown-preferences/no-trailing-spaces'?: Linter.RuleEntry<MarkdownPreferencesNoTrailingSpaces>;
53
+ /**
54
+ * enforce the use of autolinks for URLs
55
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html
56
+ */
57
+ 'markdown-preferences/prefer-autolinks'?: Linter.RuleEntry<[]>;
58
+ /**
59
+ * enforce the use of fenced code blocks over indented code blocks
60
+ * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-fenced-code-blocks.html
61
+ */
62
+ 'markdown-preferences/prefer-fenced-code-blocks'?: Linter.RuleEntry<[]>;
43
63
  /**
44
64
  * enforce the use of inline code for specific words.
45
65
  * @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html
@@ -75,6 +95,11 @@ type MarkdownPreferencesHeadingCasing = [] | [{
75
95
  ignorePatterns?: string[];
76
96
  minorWords?: string[];
77
97
  }];
98
+ type MarkdownPreferencesNoMultipleEmptyLines = [] | [{
99
+ max?: number;
100
+ maxEOF?: number;
101
+ maxBOF?: number;
102
+ }];
78
103
  type MarkdownPreferencesNoTrailingSpaces = [] | [{
79
104
  skipBlankLines?: boolean;
80
105
  ignoreComments?: boolean;
@@ -114,11 +139,14 @@ type MarkdownPreferencesSortDefinitions = [] | [{
114
139
  alphabetical?: boolean;
115
140
  }];
116
141
  declare namespace recommended_d_exports {
117
- export { files, language, name$1 as name, plugins, rules$1 as rules };
142
+ export { files, language, languageOptions, name$1 as name, plugins, rules$1 as rules };
118
143
  }
119
144
  declare const name$1 = "markdown-preferences/recommended";
120
145
  declare const files: string[];
121
- declare const language = "markdown/commonmark";
146
+ declare const language = "markdown/gfm";
147
+ declare const languageOptions: {
148
+ frontmatter: string;
149
+ };
122
150
  declare const plugins: {
123
151
  markdown: typeof markdown;
124
152
  readonly "markdown-preferences": ESLint.Plugin;
@@ -128,7 +156,7 @@ declare namespace meta_d_exports {
128
156
  export { name, version };
129
157
  }
130
158
  declare const name: "eslint-plugin-markdown-preferences";
131
- declare const version: "0.9.0";
159
+ declare const version: "0.11.0";
132
160
  //#endregion
133
161
  //#region src/index.d.ts
134
162
  declare const configs: {
package/lib/index.js CHANGED
@@ -56,7 +56,7 @@ var canonical_code_block_language_default = createRule("canonical-code-block-lan
56
56
  docs: {
57
57
  description: "enforce canonical language names in code blocks",
58
58
  categories: [],
59
- listCategory: "Stylistic"
59
+ listCategory: "Preference"
60
60
  },
61
61
  fixable: "code",
62
62
  hasSuggestions: false,
@@ -978,6 +978,333 @@ var heading_casing_default = createRule("heading-casing", {
978
978
  }
979
979
  });
980
980
 
981
+ //#endregion
982
+ //#region src/utils/lines.ts
983
+ const cache = /* @__PURE__ */ new WeakMap();
984
+ var ParsedLines = class {
985
+ lines;
986
+ constructor(codeText) {
987
+ let offset = 0;
988
+ this.lines = codeText.split(/(?<=\n)/u).map((lineText, index) => {
989
+ const start = offset;
990
+ offset += lineText.length;
991
+ const range = [start, offset];
992
+ let text = lineText;
993
+ let linebreak = "";
994
+ if (text.at(-1) === "\n") {
995
+ text = text.slice(0, -1);
996
+ linebreak = "\n";
997
+ }
998
+ if (text.at(-1) === "\r") {
999
+ text = text.slice(0, -1);
1000
+ linebreak = `\r${linebreak}`;
1001
+ }
1002
+ return {
1003
+ text,
1004
+ range,
1005
+ line: index + 1,
1006
+ linebreak
1007
+ };
1008
+ });
1009
+ }
1010
+ [Symbol.iterator]() {
1011
+ return this.lines[Symbol.iterator]();
1012
+ }
1013
+ get length() {
1014
+ return this.lines.length;
1015
+ }
1016
+ get(lineNumber) {
1017
+ return this.lines[lineNumber - 1];
1018
+ }
1019
+ };
1020
+ /**
1021
+ * Parse the lines of the source code.
1022
+ * @param sourceCode source code to parse
1023
+ * @returns parsed lines
1024
+ */
1025
+ function parseLines(sourceCode) {
1026
+ const cached = cache.get(sourceCode);
1027
+ if (cached) return cached;
1028
+ const parsedLines = new ParsedLines(sourceCode.text);
1029
+ cache.set(sourceCode, parsedLines);
1030
+ return parsedLines;
1031
+ }
1032
+
1033
+ //#endregion
1034
+ //#region src/rules/no-laziness-blockquotes.ts
1035
+ /**
1036
+ * Helper function to get blockquote line information.
1037
+ */
1038
+ function getBlockquoteLineInfo(line) {
1039
+ const regex = /^\s*(?:>\s*)*/u;
1040
+ const match = regex.exec(line.text);
1041
+ return {
1042
+ line,
1043
+ prefix: match[0],
1044
+ level: (match[0].match(/>/gu) || []).length
1045
+ };
1046
+ }
1047
+ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
1048
+ meta: {
1049
+ type: "problem",
1050
+ docs: {
1051
+ description: "disallow laziness in blockquotes",
1052
+ categories: ["recommended"],
1053
+ listCategory: "Stylistic"
1054
+ },
1055
+ fixable: void 0,
1056
+ hasSuggestions: true,
1057
+ schema: [],
1058
+ messages: {
1059
+ lazyBlockquoteLine: "Expected {{level}} '>' marker(s), but found {{actualLevel}}. This line will be interpreted as part of level {{level}} blockquote.",
1060
+ addMarker: "Add {{missingMarkers}} '>' marker(s).",
1061
+ addLineBreak: "Add line break to separate from blockquote."
1062
+ }
1063
+ },
1064
+ create(context) {
1065
+ const sourceCode = context.sourceCode;
1066
+ const checkedLines = /* @__PURE__ */ new Set();
1067
+ const lines = parseLines(sourceCode);
1068
+ /**
1069
+ * Report invalid blockquote lines.
1070
+ */
1071
+ function reportInvalidLines(invalidLines, base) {
1072
+ const invalidGroups = [];
1073
+ for (const invalidLine of invalidLines) {
1074
+ const currentGroup = invalidGroups.at(-1);
1075
+ if (!currentGroup || currentGroup.level < invalidLine.level) {
1076
+ invalidGroups.push({
1077
+ level: invalidLine.level,
1078
+ lines: [invalidLine]
1079
+ });
1080
+ continue;
1081
+ }
1082
+ if (currentGroup.level === invalidLine.level) {
1083
+ currentGroup.lines.push(invalidLine);
1084
+ continue;
1085
+ }
1086
+ if (invalidLine.level < currentGroup.level) break;
1087
+ }
1088
+ for (const group of invalidGroups) {
1089
+ const first = group.lines[0];
1090
+ const last = group.lines.at(-1);
1091
+ context.report({
1092
+ loc: {
1093
+ start: {
1094
+ line: first.line.line,
1095
+ column: 1
1096
+ },
1097
+ end: {
1098
+ line: last.line.line,
1099
+ column: last.line.text.length + 1
1100
+ }
1101
+ },
1102
+ messageId: "lazyBlockquoteLine",
1103
+ data: {
1104
+ level: `${base.level}`,
1105
+ actualLevel: `${group.level}`
1106
+ },
1107
+ suggest: [{
1108
+ messageId: "addMarker",
1109
+ data: { missingMarkers: `${base.level - group.level}` },
1110
+ *fix(fixer) {
1111
+ for (const invalidLine of group.lines) yield fixer.replaceTextRange([invalidLine.line.range[0], invalidLine.line.range[0] + invalidLine.prefix.length], base.prefix);
1112
+ }
1113
+ }, {
1114
+ messageId: "addLineBreak",
1115
+ fix: (fixer) => {
1116
+ return fixer.insertTextBeforeRange([first.line.range[0], first.line.range[0]], `${first.prefix.trimEnd()}\n`);
1117
+ }
1118
+ }]
1119
+ });
1120
+ }
1121
+ }
1122
+ return { "blockquote:exit"(node) {
1123
+ const loc = sourceCode.getLoc(node);
1124
+ const startLine = loc.start.line;
1125
+ const endLine = loc.end.line;
1126
+ const base = getBlockquoteLineInfo(lines.get(startLine));
1127
+ const invalidLines = [];
1128
+ for (let lineNumber = startLine + 1; lineNumber <= endLine; lineNumber++) {
1129
+ if (checkedLines.has(lineNumber)) {
1130
+ reportInvalidLines(invalidLines, base);
1131
+ invalidLines.length = 0;
1132
+ continue;
1133
+ }
1134
+ checkedLines.add(lineNumber);
1135
+ const line = lines.get(lineNumber);
1136
+ const current = getBlockquoteLineInfo(line);
1137
+ if (base.level <= current.level) {
1138
+ reportInvalidLines(invalidLines, base);
1139
+ invalidLines.length = 0;
1140
+ continue;
1141
+ }
1142
+ invalidLines.push(current);
1143
+ }
1144
+ reportInvalidLines(invalidLines, base);
1145
+ } };
1146
+ }
1147
+ });
1148
+
1149
+ //#endregion
1150
+ //#region src/rules/no-multiple-empty-lines.ts
1151
+ var no_multiple_empty_lines_default = createRule("no-multiple-empty-lines", {
1152
+ meta: {
1153
+ type: "layout",
1154
+ docs: {
1155
+ description: "disallow multiple empty lines in Markdown files.",
1156
+ categories: null,
1157
+ listCategory: "Stylistic"
1158
+ },
1159
+ fixable: "whitespace",
1160
+ hasSuggestions: false,
1161
+ schema: [{
1162
+ type: "object",
1163
+ properties: {
1164
+ max: {
1165
+ type: "integer",
1166
+ minimum: 0,
1167
+ default: 1
1168
+ },
1169
+ maxEOF: {
1170
+ type: "integer",
1171
+ minimum: 0,
1172
+ default: 0
1173
+ },
1174
+ maxBOF: {
1175
+ type: "integer",
1176
+ minimum: 0,
1177
+ default: 0
1178
+ }
1179
+ },
1180
+ additionalProperties: false
1181
+ }],
1182
+ messages: {
1183
+ blankBeginningOfFile: "Too many blank lines at the beginning of file. Max of {{max}} allowed.",
1184
+ blankEndOfFile: "Too many blank lines at the end of file. Max of {{max}} allowed.",
1185
+ consecutiveBlank: "More than {{max}} blank {{pluralizedLines}} not allowed."
1186
+ }
1187
+ },
1188
+ create(context) {
1189
+ const sourceCode = context.sourceCode;
1190
+ const option = context.options[0] || {};
1191
+ const max = typeof option.max === "number" ? option.max : 1;
1192
+ const maxEOF = typeof option.maxEOF === "number" ? option.maxEOF : 0;
1193
+ const maxBOF = typeof option.maxBOF === "number" ? option.maxBOF : 0;
1194
+ const ignoreLocs = [];
1195
+ /**
1196
+ * Register the range of nodes to ignore (code, html, yaml, toml, json)
1197
+ * @param node mdast node
1198
+ */
1199
+ function addIgnoreLoc(node) {
1200
+ const loc = sourceCode.getLoc(node);
1201
+ ignoreLocs.push({
1202
+ startLine: loc.start.line,
1203
+ endLine: loc.end.line
1204
+ });
1205
+ }
1206
+ return {
1207
+ code: addIgnoreLoc,
1208
+ html: addIgnoreLoc,
1209
+ yaml: addIgnoreLoc,
1210
+ toml: addIgnoreLoc,
1211
+ json: addIgnoreLoc,
1212
+ "root:exit"() {
1213
+ const lines = [...parseLines(sourceCode)];
1214
+ const bofEmptyLines = [];
1215
+ while (lines.length) {
1216
+ if (lines[0].text.trim()) break;
1217
+ bofEmptyLines.push(lines.shift());
1218
+ }
1219
+ const invalidBOFEmptyLines = bofEmptyLines.slice(maxBOF);
1220
+ if (invalidBOFEmptyLines.length > 0) {
1221
+ const first = invalidBOFEmptyLines[0];
1222
+ const last = invalidBOFEmptyLines[invalidBOFEmptyLines.length - 1];
1223
+ context.report({
1224
+ loc: {
1225
+ start: {
1226
+ line: first.line,
1227
+ column: 1
1228
+ },
1229
+ end: {
1230
+ line: last.line,
1231
+ column: last.text.length + 1
1232
+ }
1233
+ },
1234
+ messageId: "blankBeginningOfFile",
1235
+ data: { max: maxBOF },
1236
+ fix(fixer) {
1237
+ return fixer.removeRange([first.range[0], last.range[1]]);
1238
+ }
1239
+ });
1240
+ }
1241
+ const eofEmptyLines = [];
1242
+ while (lines.length) {
1243
+ if (lines[lines.length - 1].text.trim()) break;
1244
+ eofEmptyLines.unshift(lines.pop());
1245
+ }
1246
+ const invalidEOFEmptyLines = eofEmptyLines.slice(maxEOF);
1247
+ if (invalidEOFEmptyLines.length > 0) {
1248
+ const first = invalidEOFEmptyLines[0];
1249
+ const last = invalidEOFEmptyLines[invalidEOFEmptyLines.length - 1];
1250
+ context.report({
1251
+ loc: {
1252
+ start: {
1253
+ line: first.line,
1254
+ column: 1
1255
+ },
1256
+ end: {
1257
+ line: last.line,
1258
+ column: last.text.length + 1
1259
+ }
1260
+ },
1261
+ messageId: "blankEndOfFile",
1262
+ data: { max: maxEOF },
1263
+ fix(fixer) {
1264
+ return fixer.removeRange([first.range[0], last.range[1]]);
1265
+ }
1266
+ });
1267
+ }
1268
+ const emptyLines = [];
1269
+ for (const lineInfo of lines) {
1270
+ if (!lineInfo.text.trim() && !ignoreLocs.some(({ startLine, endLine }) => {
1271
+ return lineInfo.line > startLine && lineInfo.line < endLine;
1272
+ })) {
1273
+ emptyLines.push(lineInfo);
1274
+ continue;
1275
+ }
1276
+ const invalidEmptyLines = emptyLines.slice(max);
1277
+ emptyLines.length = 0;
1278
+ if (invalidEmptyLines.length) {
1279
+ const first = invalidEmptyLines[0];
1280
+ const last = invalidEmptyLines[invalidEmptyLines.length - 1];
1281
+ context.report({
1282
+ loc: {
1283
+ start: {
1284
+ line: first.line,
1285
+ column: 1
1286
+ },
1287
+ end: {
1288
+ line: last.line,
1289
+ column: last.text.length + 1
1290
+ }
1291
+ },
1292
+ messageId: "consecutiveBlank",
1293
+ data: {
1294
+ max,
1295
+ pluralizedLines: max === 1 ? "line" : "lines"
1296
+ },
1297
+ fix(fixer) {
1298
+ return fixer.removeRange([first.range[0], last.range[1]]);
1299
+ }
1300
+ });
1301
+ }
1302
+ }
1303
+ }
1304
+ };
1305
+ }
1306
+ });
1307
+
981
1308
  //#endregion
982
1309
  //#region src/rules/no-text-backslash-linebreak.ts
983
1310
  var no_text_backslash_linebreak_default = createRule("no-text-backslash-linebreak", {
@@ -1109,51 +1436,170 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
1109
1436
  "root:exit"() {
1110
1437
  const re = /[^\S\n\r]+$/u;
1111
1438
  const skipMatch = /^[^\S\n\r]*$/u;
1112
- const lines = sourceCode.lines;
1113
- const linebreaks = sourceCode.text.match(/\r?\n/gu);
1439
+ const lines = parseLines(sourceCode);
1114
1440
  const commentLineNumbers = getCommentLineNumbers();
1115
- let totalLength = 0;
1116
- for (let i = 0, ii = lines.length; i < ii; i++) {
1117
- const lineNumber = i + 1;
1118
- const linebreakLength = linebreaks && linebreaks[i] ? linebreaks[i].length : 1;
1119
- const lineLength = lines[i].length + linebreakLength;
1120
- const matches = re.exec(lines[i]);
1121
- if (!matches) {
1122
- totalLength += lineLength;
1123
- continue;
1124
- }
1441
+ for (const lineInfo of lines) {
1442
+ const matches = re.exec(lineInfo.text);
1443
+ if (!matches) continue;
1125
1444
  const location = {
1126
1445
  start: {
1127
- line: lineNumber,
1446
+ line: lineInfo.line,
1128
1447
  column: matches.index + 1
1129
1448
  },
1130
1449
  end: {
1131
- line: lineNumber,
1132
- column: lineLength + 1 - linebreakLength
1450
+ line: lineInfo.line,
1451
+ column: matches.index + 1 + matches[0].length
1133
1452
  }
1134
1453
  };
1135
- const rangeStart = totalLength + location.start.column - 1;
1136
- const rangeEnd = totalLength + location.end.column - 1;
1454
+ const range = [lineInfo.range[0] + location.start.column - 1, lineInfo.range[0] + location.end.column - 1];
1137
1455
  if (ignoreNodes.some((node) => {
1138
- const range = sourceCode.getRange(node);
1139
- return range[0] <= rangeStart && rangeEnd <= range[1];
1140
- })) {
1141
- totalLength += lineLength;
1142
- continue;
1143
- }
1144
- if (skipBlankLines && skipMatch.test(lines[i])) {
1145
- totalLength += lineLength;
1146
- continue;
1147
- }
1148
- const fixRange = [rangeStart, rangeEnd];
1149
- if (!ignoreComments || !commentLineNumbers.has(lineNumber)) report(location, fixRange);
1150
- totalLength += lineLength;
1456
+ const nodeRange = sourceCode.getRange(node);
1457
+ return nodeRange[0] <= range[0] && range[1] <= nodeRange[1];
1458
+ })) continue;
1459
+ if (skipBlankLines && skipMatch.test(lineInfo.text)) continue;
1460
+ if (!ignoreComments || !commentLineNumbers.has(lineInfo.line)) report(location, range);
1151
1461
  }
1152
1462
  }
1153
1463
  };
1154
1464
  }
1155
1465
  });
1156
1466
 
1467
+ //#endregion
1468
+ //#region src/utils/ast.ts
1469
+ /**
1470
+ * Get the kind of code block.
1471
+ */
1472
+ function getCodeBlockKind(sourceCode, node) {
1473
+ const text = sourceCode.getText(node);
1474
+ return text.startsWith("```") ? "backtick-fenced" : text.startsWith("~~~") ? "tilde-fenced" : "indented";
1475
+ }
1476
+ /**
1477
+ * Get the kind of link.
1478
+ */
1479
+ function getLinkKind(sourceCode, node) {
1480
+ const text = sourceCode.getText(node);
1481
+ return text.startsWith("[") ? "inline" : text.startsWith("<") && text.endsWith(">") ? "autolink" : "gfm-autolink";
1482
+ }
1483
+
1484
+ //#endregion
1485
+ //#region src/rules/prefer-autolinks.ts
1486
+ var prefer_autolinks_default = createRule("prefer-autolinks", {
1487
+ meta: {
1488
+ type: "layout",
1489
+ docs: {
1490
+ description: "enforce the use of autolinks for URLs",
1491
+ categories: ["recommended"],
1492
+ listCategory: "Stylistic"
1493
+ },
1494
+ fixable: "code",
1495
+ hasSuggestions: false,
1496
+ schema: [],
1497
+ messages: {
1498
+ useAutolinkInsteadGFMAutolink: "Use an autolink instead of a GFM autolink (bare url).",
1499
+ useAutolinkInsteadInlineLink: "Use an autolink instead of an inline link with the same URL."
1500
+ }
1501
+ },
1502
+ create(context) {
1503
+ const sourceCode = context.sourceCode;
1504
+ return { link(node) {
1505
+ const kind = getLinkKind(sourceCode, node);
1506
+ if (kind === "autolink") return;
1507
+ if (node.title) return;
1508
+ if (node.children.some((child) => child.type !== "text")) return;
1509
+ const label = node.children.map((child) => child.value).join("");
1510
+ if (node.url !== label) return;
1511
+ context.report({
1512
+ node,
1513
+ messageId: kind === "inline" ? "useAutolinkInsteadInlineLink" : "useAutolinkInsteadGFMAutolink",
1514
+ fix(fixer) {
1515
+ return fixer.replaceText(node, `<${node.url}>`);
1516
+ }
1517
+ });
1518
+ } };
1519
+ }
1520
+ });
1521
+
1522
+ //#endregion
1523
+ //#region src/rules/prefer-fenced-code-blocks.ts
1524
+ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks", {
1525
+ meta: {
1526
+ type: "layout",
1527
+ docs: {
1528
+ description: "enforce the use of fenced code blocks over indented code blocks",
1529
+ categories: ["recommended"],
1530
+ listCategory: "Stylistic"
1531
+ },
1532
+ fixable: "code",
1533
+ hasSuggestions: false,
1534
+ schema: [],
1535
+ messages: { useFencedCodeBlock: "Use a fenced code block instead of an indented code block." }
1536
+ },
1537
+ create(context) {
1538
+ const sourceCode = context.sourceCode;
1539
+ const lines = parseLines(sourceCode);
1540
+ return { code(node) {
1541
+ const kind = getCodeBlockKind(sourceCode, node);
1542
+ if (kind === "backtick-fenced" || kind === "tilde-fenced") return;
1543
+ const loc = sourceCode.getLoc(node);
1544
+ context.report({
1545
+ node,
1546
+ messageId: "useFencedCodeBlock",
1547
+ fix(fixer) {
1548
+ if (!isFixableIndentedCodeBlock(node)) return null;
1549
+ const startColumnOffset = loc.start.column - 1;
1550
+ const removeRanges = [];
1551
+ let prefixText = null;
1552
+ for (let line = loc.start.line; line <= loc.end.line; line++) {
1553
+ const parsedLine = lines.get(line);
1554
+ const currentPrefix = normalizePrefix(parsedLine.text.slice(0, startColumnOffset));
1555
+ if (!prefixText) prefixText = currentPrefix;
1556
+ else if (currentPrefix !== prefixText) return null;
1557
+ let removeRange = [parsedLine.range[0] + startColumnOffset, parsedLine.range[0] + startColumnOffset + 4];
1558
+ for (let index = removeRange[0]; index < removeRange[1]; index++) {
1559
+ const c = sourceCode.text[index];
1560
+ if (c === " ") continue;
1561
+ if (c === " ") {
1562
+ removeRange = [removeRange[0], index + 1];
1563
+ break;
1564
+ }
1565
+ return null;
1566
+ }
1567
+ removeRanges.push(removeRange);
1568
+ }
1569
+ const beginFenceInsertOffset = lines.get(loc.start.line).range[0];
1570
+ const endFenceInsertOffset = lines.get(loc.end.line).range[1];
1571
+ return [
1572
+ fixer.insertTextBeforeRange([beginFenceInsertOffset, beginFenceInsertOffset], `${prefixText}\`\`\`\n`),
1573
+ ...removeRanges.map((removeRange) => fixer.removeRange(removeRange)),
1574
+ fixer.insertTextAfterRange([endFenceInsertOffset, endFenceInsertOffset], `${prefixText}\`\`\`\n`)
1575
+ ];
1576
+ }
1577
+ });
1578
+ } };
1579
+ /**
1580
+ * Determines whether the given indented code block is fixable or not.
1581
+ */
1582
+ function isFixableIndentedCodeBlock(node) {
1583
+ if (!node.value.startsWith(" ")) return true;
1584
+ const loc = sourceCode.getLoc(node);
1585
+ const firstLine = lines.get(loc.start.line);
1586
+ const codeBlockFirstLine = normalizePrefix(node.value.split(/\r?\n/u)[0]);
1587
+ const startColumnOffset = loc.start.column - 1;
1588
+ const normalizedFirstLine = normalizePrefix(firstLine.text.slice(startColumnOffset));
1589
+ return normalizedFirstLine.startsWith(codeBlockFirstLine);
1590
+ }
1591
+ }
1592
+ });
1593
+ /**
1594
+ * Normalize the prefix by removing tabs and replacing them with spaces.
1595
+ */
1596
+ function normalizePrefix(prefix) {
1597
+ let result = "";
1598
+ for (const c of prefix) if (c !== " ") result += c;
1599
+ else result += " ".repeat(4 - result.length % 4);
1600
+ return result;
1601
+ }
1602
+
1157
1603
  //#endregion
1158
1604
  //#region src/utils/search-words.ts
1159
1605
  const RE_BOUNDARY = /^[\s\p{Letter_Number}\p{Modifier_Letter}\p{Modifier_Symbol}\p{Nonspacing_Mark}\p{Other_Letter}\p{Other_Symbol}\p{Script=Han}!"#$%&'()*+,./:;<=>?\\{|}~\u{2ffc}-\u{303d}\u{30a0}-\u{30fb}\u{3192}-\u{32bf}\u{fe10}-\u{fe1f}\u{fe30}-\u{fe6f}\u{ff00}-\u{ffef}\u{2ebf0}-\u{2ee5d}]*$/u;
@@ -1488,25 +1934,6 @@ var prefer_link_reference_definitions_default = createRule("prefer-link-referenc
1488
1934
  }
1489
1935
  });
1490
1936
 
1491
- //#endregion
1492
- //#region src/utils/url.ts
1493
- /**
1494
- * Utility function to check if a string is a valid URL.
1495
- */
1496
- function isValidURL(url) {
1497
- return Boolean(createURLSafe(url));
1498
- }
1499
- /**
1500
- * Utility function to create a URL object safely.
1501
- */
1502
- function createURLSafe(url) {
1503
- try {
1504
- return new URL(url);
1505
- } catch {
1506
- return null;
1507
- }
1508
- }
1509
-
1510
1937
  //#endregion
1511
1938
  //#region src/rules/prefer-linked-words.ts
1512
1939
  var prefer_linked_words_default = createRule("prefer-linked-words", {
@@ -1608,7 +2035,7 @@ var prefer_linked_words_default = createRule("prefer-linked-words", {
1608
2035
  * Adjust link to be relative to the file.
1609
2036
  */
1610
2037
  function adjustLink(link) {
1611
- if (isValidURL(link)) return link;
2038
+ if (URL.canParse(link)) return link;
1612
2039
  if (link.startsWith("#")) return link;
1613
2040
  const absoluteLink = path.isAbsolute(link) || path.posix.isAbsolute(link) ? link : path.join(context.cwd, link);
1614
2041
  return `./${path.relative(path.dirname(context.filename), absoluteLink)}`;
@@ -1789,14 +2216,14 @@ var sort_definitions_default = createRule("sort-definitions", {
1789
2216
  }
1790
2217
  /** Compile order option */
1791
2218
  function compileOption(orderOption) {
1792
- const cache = /* @__PURE__ */ new Map();
2219
+ const cache$1 = /* @__PURE__ */ new Map();
1793
2220
  const compiled = compileOptionWithoutCache(orderOption);
1794
2221
  return {
1795
2222
  match: (node) => {
1796
- const cached = cache.get(node);
2223
+ const cached = cache$1.get(node);
1797
2224
  if (cached != null) return cached;
1798
2225
  const result = compiled.match(node);
1799
- cache.set(node, result);
2226
+ cache$1.set(node, result);
1800
2227
  return result;
1801
2228
  },
1802
2229
  sort: compiled.sort
@@ -1847,7 +2274,7 @@ var sort_definitions_default = createRule("sort-definitions", {
1847
2274
  if (node.label === patternStr || node.identifier === patternStr) return true;
1848
2275
  if (node.type === "definition") {
1849
2276
  if (node.url === patternStr) return true;
1850
- if (isValidURL(patternStr)) {
2277
+ if (URL.canParse(patternStr) && URL.canParse(node.url)) {
1851
2278
  const normalizedPattern = normalizedURL(patternStr);
1852
2279
  const normalizedUrl = normalizedURL(node.url);
1853
2280
  if (normalizedUrl.startsWith(normalizedPattern)) return true;
@@ -1894,7 +2321,7 @@ function getQuote(text) {
1894
2321
  * Normalize a URL by ensuring it ends with a slash.
1895
2322
  */
1896
2323
  function normalizedURL(url) {
1897
- const urlObj = createURLSafe(url);
2324
+ const urlObj = new URL(url);
1898
2325
  if (!urlObj) return url;
1899
2326
  return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
1900
2327
  }
@@ -1906,8 +2333,12 @@ const rules$1 = [
1906
2333
  definitions_last_default,
1907
2334
  hard_linebreak_style_default,
1908
2335
  heading_casing_default,
2336
+ no_laziness_blockquotes_default,
2337
+ no_multiple_empty_lines_default,
1909
2338
  no_text_backslash_linebreak_default,
1910
2339
  no_trailing_spaces_default,
2340
+ prefer_autolinks_default,
2341
+ prefer_fenced_code_blocks_default,
1911
2342
  prefer_inline_code_words_default,
1912
2343
  prefer_link_reference_definitions_default,
1913
2344
  prefer_linked_words_default,
@@ -1920,13 +2351,15 @@ var recommended_exports = {};
1920
2351
  __export(recommended_exports, {
1921
2352
  files: () => files,
1922
2353
  language: () => language,
2354
+ languageOptions: () => languageOptions,
1923
2355
  name: () => name$1,
1924
2356
  plugins: () => plugins,
1925
2357
  rules: () => rules$2
1926
2358
  });
1927
2359
  const name$1 = "markdown-preferences/recommended";
1928
2360
  const files = ["*.md", "**/*.md"];
1929
- const language = "markdown/commonmark";
2361
+ const language = "markdown/gfm";
2362
+ const languageOptions = { frontmatter: "yaml" };
1930
2363
  const plugins = {
1931
2364
  markdown,
1932
2365
  get "markdown-preferences"() {
@@ -1935,7 +2368,10 @@ const plugins = {
1935
2368
  };
1936
2369
  const rules$2 = {
1937
2370
  "markdown-preferences/hard-linebreak-style": "error",
1938
- "markdown-preferences/no-text-backslash-linebreak": "error"
2371
+ "markdown-preferences/no-laziness-blockquotes": "error",
2372
+ "markdown-preferences/no-text-backslash-linebreak": "error",
2373
+ "markdown-preferences/prefer-autolinks": "error",
2374
+ "markdown-preferences/prefer-fenced-code-blocks": "error"
1939
2375
  };
1940
2376
 
1941
2377
  //#endregion
@@ -1946,7 +2382,7 @@ __export(meta_exports, {
1946
2382
  version: () => version
1947
2383
  });
1948
2384
  const name = "eslint-plugin-markdown-preferences";
1949
- const version = "0.9.0";
2385
+ const version = "0.11.0";
1950
2386
 
1951
2387
  //#endregion
1952
2388
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-markdown-preferences",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "ESLint plugin that enforces our markdown preferences",
5
5
  "type": "module",
6
6
  "exports": {