eslint-plugin-markdown-preferences 0.13.0 → 0.15.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 +14 -7
- package/lib/index.d.ts +26 -2
- package/lib/index.js +504 -138
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -13,10 +13,10 @@ A specialized ESLint plugin that helps enforce consistent writing style and form
|
|
|
13
13
|
|
|
14
14
|
## 📛 Features
|
|
15
15
|
|
|
16
|
-
- **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting,
|
|
17
|
-
- **📖 Professional documentation** - Enforce consistent
|
|
18
|
-
-
|
|
19
|
-
- **⚙️
|
|
16
|
+
- **⚡ Effortless automation** - Transform your Markdown with auto-fixing that handles formatting, casing, and style consistency automatically
|
|
17
|
+
- **📖 Professional documentation** - Enforce consistent headings, table headers, and organize link definitions for enterprise-ready documentation
|
|
18
|
+
- **🎨 Clean formatting** - Remove trailing spaces, control line breaks, standardize code blocks, and ensure consistent list numbering for polished output
|
|
19
|
+
- **⚙️ Flexible customization** - Configure casing styles (Title Case, Sentence case), code block languages, emoji notation, and more with extensive options
|
|
20
20
|
|
|
21
21
|
**Try it live:** Check out the [Online Demo](https://eslint-online-playground.netlify.app/#eslint-plugin-markdown-preferences) to see the plugin in action!
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ For detailed usage instructions, rule configurations, and examples, visit our co
|
|
|
29
29
|
## 💿 Installation
|
|
30
30
|
|
|
31
31
|
```sh
|
|
32
|
-
npm install --save-dev eslint eslint-plugin-markdown-preferences
|
|
32
|
+
npm install --save-dev eslint @eslint/markdown eslint-plugin-markdown-preferences
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
<!--DOCS_IGNORE_END-->
|
|
@@ -90,26 +90,33 @@ The rules with the following star ⭐ are included in the configs.
|
|
|
90
90
|
|
|
91
91
|
### Preference Rules
|
|
92
92
|
|
|
93
|
+
- Rules to unify the expression and description style of documents.
|
|
94
|
+
|
|
93
95
|
| Rule ID | Description | Fixable | RECOMMENDED |
|
|
94
96
|
|:--------|:------------|:-------:|:-----------:|
|
|
95
97
|
| [markdown-preferences/canonical-code-block-language](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html) | enforce canonical language names in code blocks | 🔧 | |
|
|
96
|
-
| [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) |
|
|
98
|
+
| [markdown-preferences/emoji-notation](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html) | enforce consistent emoji notation style in Markdown files. | 🔧 | |
|
|
97
99
|
| [markdown-preferences/heading-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html) | enforce consistent casing in headings. | 🔧 | |
|
|
98
|
-
| [markdown-preferences/no-text-backslash-linebreak](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html) | disallow text backslash at the end of a line. | | ⭐ |
|
|
99
100
|
| [markdown-preferences/ordered-list-marker-start](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-start.html) | enforce that ordered list markers start with 1 or 0 | 🔧 | |
|
|
100
101
|
| [markdown-preferences/prefer-inline-code-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-inline-code-words.html) | enforce the use of inline code for specific words. | 🔧 | |
|
|
101
102
|
| [markdown-preferences/prefer-linked-words](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-linked-words.html) | enforce the specified word to be a link. | 🔧 | |
|
|
103
|
+
| [markdown-preferences/table-header-casing](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html) | enforce consistent casing in table header cells. | 🔧 | |
|
|
102
104
|
|
|
103
105
|
### Stylistic Rules
|
|
104
106
|
|
|
107
|
+
- Rules related to the formatting and visual style of Markdown.
|
|
108
|
+
|
|
105
109
|
| Rule ID | Description | Fixable | RECOMMENDED |
|
|
106
110
|
|:--------|:------------|:-------:|:-----------:|
|
|
107
111
|
| [markdown-preferences/atx-headings-closing-sequence-length](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence-length.html) | enforce consistent length for the closing sequence (trailing #s) in ATX headings. | 🔧 | |
|
|
108
112
|
| [markdown-preferences/atx-headings-closing-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence.html) | enforce consistent use of closing sequence in ATX headings. | 🔧 | |
|
|
113
|
+
| [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 | 🔧 | ⭐ |
|
|
109
114
|
| [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 | 🔧 | |
|
|
110
115
|
| [markdown-preferences/hard-linebreak-style](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/hard-linebreak-style.html) | enforce consistent hard linebreak style. | 🔧 | ⭐ |
|
|
116
|
+
| [markdown-preferences/list-marker-alignment](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html) | enforce consistent alignment of list markers | 🔧 | ⭐ |
|
|
111
117
|
| [markdown-preferences/no-laziness-blockquotes](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html) | disallow laziness in blockquotes | | ⭐ |
|
|
112
118
|
| [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. | 🔧 | |
|
|
119
|
+
| [markdown-preferences/no-text-backslash-linebreak](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-text-backslash-linebreak.html) | disallow text backslash at the end of a line. | | ⭐ |
|
|
113
120
|
| [markdown-preferences/no-trailing-spaces](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-trailing-spaces.html) | disallow trailing whitespace at the end of lines in Markdown files. | 🔧 | |
|
|
114
121
|
| [markdown-preferences/ordered-list-marker-sequence](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/ordered-list-marker-sequence.html) | enforce that ordered list markers use sequential numbers | 🔧 | |
|
|
115
122
|
| [markdown-preferences/prefer-autolinks](https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/prefer-autolinks.html) | enforce the use of autolinks for URLs | 🔧 | ⭐ |
|
package/lib/index.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ interface RuleOptions {
|
|
|
20
20
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/atx-headings-closing-sequence-length.html
|
|
21
21
|
*/
|
|
22
22
|
'markdown-preferences/atx-headings-closing-sequence-length'?: Linter.RuleEntry<MarkdownPreferencesAtxHeadingsClosingSequenceLength>;
|
|
23
|
+
/**
|
|
24
|
+
* enforce consistent alignment of blockquote markers
|
|
25
|
+
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/blockquote-marker-alignment.html
|
|
26
|
+
*/
|
|
27
|
+
'markdown-preferences/blockquote-marker-alignment'?: Linter.RuleEntry<[]>;
|
|
23
28
|
/**
|
|
24
29
|
* enforce canonical language names in code blocks
|
|
25
30
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/canonical-code-block-language.html
|
|
@@ -31,7 +36,7 @@ interface RuleOptions {
|
|
|
31
36
|
*/
|
|
32
37
|
'markdown-preferences/definitions-last'?: Linter.RuleEntry<[]>;
|
|
33
38
|
/**
|
|
34
|
-
*
|
|
39
|
+
* enforce consistent emoji notation style in Markdown files.
|
|
35
40
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/emoji-notation.html
|
|
36
41
|
*/
|
|
37
42
|
'markdown-preferences/emoji-notation'?: Linter.RuleEntry<MarkdownPreferencesEmojiNotation>;
|
|
@@ -45,6 +50,11 @@ interface RuleOptions {
|
|
|
45
50
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/heading-casing.html
|
|
46
51
|
*/
|
|
47
52
|
'markdown-preferences/heading-casing'?: Linter.RuleEntry<MarkdownPreferencesHeadingCasing>;
|
|
53
|
+
/**
|
|
54
|
+
* enforce consistent alignment of list markers
|
|
55
|
+
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/list-marker-alignment.html
|
|
56
|
+
*/
|
|
57
|
+
'markdown-preferences/list-marker-alignment'?: Linter.RuleEntry<MarkdownPreferencesListMarkerAlignment>;
|
|
48
58
|
/**
|
|
49
59
|
* disallow laziness in blockquotes
|
|
50
60
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/no-laziness-blockquotes.html
|
|
@@ -105,6 +115,11 @@ interface RuleOptions {
|
|
|
105
115
|
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/sort-definitions.html
|
|
106
116
|
*/
|
|
107
117
|
'markdown-preferences/sort-definitions'?: Linter.RuleEntry<MarkdownPreferencesSortDefinitions>;
|
|
118
|
+
/**
|
|
119
|
+
* enforce consistent casing in table header cells.
|
|
120
|
+
* @see https://ota-meshi.github.io/eslint-plugin-markdown-preferences/rules/table-header-casing.html
|
|
121
|
+
*/
|
|
122
|
+
'markdown-preferences/table-header-casing'?: Linter.RuleEntry<MarkdownPreferencesTableHeaderCasing>;
|
|
108
123
|
}
|
|
109
124
|
type MarkdownPreferencesAtxHeadingsClosingSequence = [] | [{
|
|
110
125
|
closingSequence?: ("always" | "never");
|
|
@@ -132,6 +147,9 @@ type MarkdownPreferencesHeadingCasing = [] | [{
|
|
|
132
147
|
ignorePatterns?: string[];
|
|
133
148
|
minorWords?: string[];
|
|
134
149
|
}];
|
|
150
|
+
type MarkdownPreferencesListMarkerAlignment = [] | [{
|
|
151
|
+
align?: ("left" | "right");
|
|
152
|
+
}];
|
|
135
153
|
type MarkdownPreferencesNoMultipleEmptyLines = [] | [{
|
|
136
154
|
max?: number;
|
|
137
155
|
maxEOF?: number;
|
|
@@ -178,6 +196,12 @@ type MarkdownPreferencesSortDefinitions = [] | [{
|
|
|
178
196
|
})[];
|
|
179
197
|
alphabetical?: boolean;
|
|
180
198
|
}];
|
|
199
|
+
type MarkdownPreferencesTableHeaderCasing = [] | [{
|
|
200
|
+
style?: ("Title Case" | "Sentence case");
|
|
201
|
+
preserveWords?: string[];
|
|
202
|
+
ignorePatterns?: string[];
|
|
203
|
+
minorWords?: string[];
|
|
204
|
+
}];
|
|
181
205
|
declare namespace recommended_d_exports {
|
|
182
206
|
export { files, language, languageOptions, name$1 as name, plugins, rules$1 as rules };
|
|
183
207
|
}
|
|
@@ -196,7 +220,7 @@ declare namespace meta_d_exports {
|
|
|
196
220
|
export { name, version };
|
|
197
221
|
}
|
|
198
222
|
declare const name: "eslint-plugin-markdown-preferences";
|
|
199
|
-
declare const version: "0.
|
|
223
|
+
declare const version: "0.15.0";
|
|
200
224
|
//#endregion
|
|
201
225
|
//#region src/index.d.ts
|
|
202
226
|
declare const configs: {
|
package/lib/index.js
CHANGED
|
@@ -108,7 +108,7 @@ function parseATXHeadingClosingSequenceFromText(text) {
|
|
|
108
108
|
|
|
109
109
|
//#endregion
|
|
110
110
|
//#region src/utils/lines.ts
|
|
111
|
-
const cache = /* @__PURE__ */ new WeakMap();
|
|
111
|
+
const cache$1 = /* @__PURE__ */ new WeakMap();
|
|
112
112
|
var ParsedLines = class {
|
|
113
113
|
lines;
|
|
114
114
|
constructor(codeText) {
|
|
@@ -150,11 +150,11 @@ var ParsedLines = class {
|
|
|
150
150
|
* @param sourceCode source code to parse
|
|
151
151
|
* @returns parsed lines
|
|
152
152
|
*/
|
|
153
|
-
function
|
|
154
|
-
const cached = cache.get(sourceCode);
|
|
153
|
+
function getParsedLines(sourceCode) {
|
|
154
|
+
const cached = cache$1.get(sourceCode);
|
|
155
155
|
if (cached) return cached;
|
|
156
156
|
const parsedLines = new ParsedLines(sourceCode.text);
|
|
157
|
-
cache.set(sourceCode, parsedLines);
|
|
157
|
+
cache$1.set(sourceCode, parsedLines);
|
|
158
158
|
return parsedLines;
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -228,9 +228,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
228
228
|
} };
|
|
229
229
|
})() : option.mode === "fixed-line-length" ? (() => {
|
|
230
230
|
const totalLength = option.length || 80;
|
|
231
|
-
const lines = parseLines(sourceCode);
|
|
232
231
|
const getExpected = (_node, parsed) => {
|
|
233
|
-
return totalLength - getContentLength(
|
|
232
|
+
return totalLength - getContentLength(parsed);
|
|
234
233
|
};
|
|
235
234
|
return { heading(node) {
|
|
236
235
|
verifyATXHeadingClosingSequenceLength(node, getExpected);
|
|
@@ -246,7 +245,6 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
246
245
|
} else verifyATXHeadingClosingSequenceLength(node, getExpected);
|
|
247
246
|
} };
|
|
248
247
|
})() : option.mode === "consistent-line-length" ? (() => {
|
|
249
|
-
const lines = parseLines(sourceCode);
|
|
250
248
|
const headings = [];
|
|
251
249
|
return {
|
|
252
250
|
heading(node) {
|
|
@@ -260,9 +258,9 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
260
258
|
"root:exit"() {
|
|
261
259
|
let mostLongContentHeading = null;
|
|
262
260
|
for (const heading of headings) {
|
|
263
|
-
const contentLength = getContentLength(
|
|
261
|
+
const contentLength = getContentLength(heading.parsed);
|
|
264
262
|
if (mostLongContentHeading == null || contentLength > mostLongContentHeading.contentLength) {
|
|
265
|
-
const lineLength = getLineLength(
|
|
263
|
+
const lineLength = getLineLength(heading.parsed);
|
|
266
264
|
mostLongContentHeading = {
|
|
267
265
|
...heading,
|
|
268
266
|
contentLength,
|
|
@@ -273,11 +271,11 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
273
271
|
if (!mostLongContentHeading) return;
|
|
274
272
|
let minLineLength = mostLongContentHeading.lineLength;
|
|
275
273
|
for (const heading of headings) {
|
|
276
|
-
const lineLength = getLineLength(
|
|
274
|
+
const lineLength = getLineLength(heading.parsed);
|
|
277
275
|
if (mostLongContentHeading.contentLength < lineLength && lineLength < minLineLength) minLineLength = Math.min(minLineLength, lineLength);
|
|
278
276
|
}
|
|
279
277
|
const getExpected = (_node, parsed) => {
|
|
280
|
-
return minLineLength - getContentLength(
|
|
278
|
+
return minLineLength - getContentLength(parsed);
|
|
281
279
|
};
|
|
282
280
|
for (const { node } of headings) verifyATXHeadingClosingSequenceLength(node, getExpected);
|
|
283
281
|
}
|
|
@@ -286,7 +284,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
286
284
|
/**
|
|
287
285
|
* Get the content length of the heading.
|
|
288
286
|
*/
|
|
289
|
-
function getContentLength(
|
|
287
|
+
function getContentLength(parsed) {
|
|
288
|
+
const lines = getParsedLines(sourceCode);
|
|
290
289
|
const line = lines.get(parsed.closingSequence.loc.start.line);
|
|
291
290
|
const beforeClosing = sourceCode.text.slice(line.range[0], parsed.closingSequence.range[0]);
|
|
292
291
|
return getTextWidth(beforeClosing);
|
|
@@ -294,7 +293,8 @@ var atx_headings_closing_sequence_length_default = createRule("atx-headings-clos
|
|
|
294
293
|
/**
|
|
295
294
|
* Get the line length of the heading.
|
|
296
295
|
*/
|
|
297
|
-
function getLineLength(
|
|
296
|
+
function getLineLength(parsed) {
|
|
297
|
+
const lines = getParsedLines(sourceCode);
|
|
298
298
|
const line = lines.get(parsed.closingSequence.loc.start.line);
|
|
299
299
|
const lineText = sourceCode.text.slice(line.range[0], parsed.closingSequence.range[1]);
|
|
300
300
|
return getTextWidth(lineText);
|
|
@@ -380,6 +380,123 @@ var atx_headings_closing_sequence_default = createRule("atx-headings-closing-seq
|
|
|
380
380
|
}
|
|
381
381
|
});
|
|
382
382
|
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/utils/blockquotes.ts
|
|
385
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
386
|
+
/**
|
|
387
|
+
* Helper function to get blockquote level information.
|
|
388
|
+
*/
|
|
389
|
+
function getBlockquoteLevelFromLine(sourceCode, lineNumber) {
|
|
390
|
+
let map = cache.get(sourceCode);
|
|
391
|
+
if (!map) {
|
|
392
|
+
map = /* @__PURE__ */ new Map();
|
|
393
|
+
cache.set(sourceCode, map);
|
|
394
|
+
}
|
|
395
|
+
const cached = map.get(lineNumber);
|
|
396
|
+
if (cached) return cached;
|
|
397
|
+
const lineText = sourceCode.lines[lineNumber - 1];
|
|
398
|
+
let prefix = "";
|
|
399
|
+
let level = 0;
|
|
400
|
+
const blockquoteMarkers = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const c of lineText) {
|
|
402
|
+
if (c === ">") {
|
|
403
|
+
level++;
|
|
404
|
+
blockquoteMarkers.set(level, { index: prefix.length });
|
|
405
|
+
} else if (c.trim()) break;
|
|
406
|
+
prefix += c;
|
|
407
|
+
}
|
|
408
|
+
const result = {
|
|
409
|
+
line: lineNumber,
|
|
410
|
+
prefix,
|
|
411
|
+
level,
|
|
412
|
+
blockquoteMarkers
|
|
413
|
+
};
|
|
414
|
+
map.set(lineNumber, result);
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/rules/blockquote-marker-alignment.ts
|
|
420
|
+
var blockquote_marker_alignment_default = createRule("blockquote-marker-alignment", {
|
|
421
|
+
meta: {
|
|
422
|
+
type: "layout",
|
|
423
|
+
docs: {
|
|
424
|
+
description: "enforce consistent alignment of blockquote markers",
|
|
425
|
+
categories: ["recommended"],
|
|
426
|
+
listCategory: "Stylistic"
|
|
427
|
+
},
|
|
428
|
+
fixable: "whitespace",
|
|
429
|
+
hasSuggestions: false,
|
|
430
|
+
schema: [],
|
|
431
|
+
messages: { inconsistentAlignment: "Blockquote markers should be consistently aligned at the same nesting level." }
|
|
432
|
+
},
|
|
433
|
+
create(context) {
|
|
434
|
+
const sourceCode = context.sourceCode;
|
|
435
|
+
let blockquoteStack = {
|
|
436
|
+
node: sourceCode.ast,
|
|
437
|
+
level: 0,
|
|
438
|
+
upper: null
|
|
439
|
+
};
|
|
440
|
+
return {
|
|
441
|
+
blockquote(node) {
|
|
442
|
+
blockquoteStack = {
|
|
443
|
+
node,
|
|
444
|
+
level: blockquoteStack.level + 1,
|
|
445
|
+
upper: blockquoteStack,
|
|
446
|
+
reported: blockquoteStack.reported
|
|
447
|
+
};
|
|
448
|
+
if (blockquoteStack.reported) return;
|
|
449
|
+
const blockquoteLevel = blockquoteStack.level;
|
|
450
|
+
const loc = sourceCode.getLoc(node);
|
|
451
|
+
const startLine = loc.start.line;
|
|
452
|
+
const endLine = loc.end.line;
|
|
453
|
+
const base = getBlockquoteLevelFromLine(sourceCode, startLine).blockquoteMarkers.get(blockquoteLevel);
|
|
454
|
+
if (!base) return;
|
|
455
|
+
for (let lineNumber = startLine + 1; lineNumber <= endLine; lineNumber++) {
|
|
456
|
+
const marker = getBlockquoteLevelFromLine(sourceCode, lineNumber).blockquoteMarkers.get(blockquoteLevel);
|
|
457
|
+
if (!marker) continue;
|
|
458
|
+
if (base.index === marker.index) continue;
|
|
459
|
+
blockquoteStack.reported = true;
|
|
460
|
+
context.report({
|
|
461
|
+
node,
|
|
462
|
+
loc: {
|
|
463
|
+
start: {
|
|
464
|
+
line: lineNumber,
|
|
465
|
+
column: marker.index + 1
|
|
466
|
+
},
|
|
467
|
+
end: {
|
|
468
|
+
line: lineNumber,
|
|
469
|
+
column: marker.index + 2
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
messageId: "inconsistentAlignment",
|
|
473
|
+
fix(fixer) {
|
|
474
|
+
const lines = getParsedLines(sourceCode);
|
|
475
|
+
const line = lines.get(lineNumber);
|
|
476
|
+
if (marker.index < base.index) {
|
|
477
|
+
const addSpaces = " ".repeat(base.index - marker.index);
|
|
478
|
+
return fixer.insertTextBeforeRange([line.range[0] + marker.index, line.range[0] + marker.index], addSpaces);
|
|
479
|
+
}
|
|
480
|
+
if (blockquoteLevel === 1) {
|
|
481
|
+
const expectedSpaces = " ".repeat(base.index);
|
|
482
|
+
return fixer.replaceTextRange([line.range[0], line.range[0] + marker.index], expectedSpaces);
|
|
483
|
+
}
|
|
484
|
+
const itemBefore = line.text.slice(0, line.range[0] + marker.index);
|
|
485
|
+
if (itemBefore.includes(" ")) return null;
|
|
486
|
+
let removeStartIndex = marker.index;
|
|
487
|
+
for (; removeStartIndex > base.index; removeStartIndex--) if (line.text[removeStartIndex - 1] !== " ") break;
|
|
488
|
+
return fixer.removeRange([line.range[0] + removeStartIndex, line.range[0] + marker.index]);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
"blockquote:exit"() {
|
|
494
|
+
blockquoteStack = blockquoteStack.upper;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
383
500
|
//#endregion
|
|
384
501
|
//#region src/utils/ast.ts
|
|
385
502
|
/**
|
|
@@ -2546,7 +2663,7 @@ var emoji_notation_default = createRule("emoji-notation", {
|
|
|
2546
2663
|
meta: {
|
|
2547
2664
|
type: "suggestion",
|
|
2548
2665
|
docs: {
|
|
2549
|
-
description: "
|
|
2666
|
+
description: "enforce consistent emoji notation style in Markdown files.",
|
|
2550
2667
|
categories: [],
|
|
2551
2668
|
listCategory: "Preference"
|
|
2552
2669
|
},
|
|
@@ -2813,6 +2930,7 @@ const defaultPreserveWords = [
|
|
|
2813
2930
|
"Biome",
|
|
2814
2931
|
"oxc",
|
|
2815
2932
|
"swc",
|
|
2933
|
+
"markdownlint",
|
|
2816
2934
|
"Webpack",
|
|
2817
2935
|
"Vite",
|
|
2818
2936
|
"Babel",
|
|
@@ -3148,7 +3266,8 @@ const defaultPreserveWords = [
|
|
|
3148
3266
|
"Insomnia",
|
|
3149
3267
|
"Redoc",
|
|
3150
3268
|
"Stoplight",
|
|
3151
|
-
"FAQ"
|
|
3269
|
+
"FAQ",
|
|
3270
|
+
"YouTube"
|
|
3152
3271
|
];
|
|
3153
3272
|
|
|
3154
3273
|
//#endregion
|
|
@@ -3187,44 +3306,113 @@ const defaultMinorWords = [
|
|
|
3187
3306
|
];
|
|
3188
3307
|
|
|
3189
3308
|
//#endregion
|
|
3190
|
-
//#region src/
|
|
3309
|
+
//#region src/utils/word-casing.ts
|
|
3191
3310
|
/**
|
|
3192
|
-
*
|
|
3193
|
-
* - Single words are added to preserveWords
|
|
3194
|
-
* - Multi-word phrases are added to preservePhrases and added to preserveWords with no spaces
|
|
3311
|
+
* Converts the casing of a word based on the specified case style and whether it is the first or last word in a phrase.
|
|
3195
3312
|
*/
|
|
3196
|
-
function
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3313
|
+
function convertWordCasing({ word, first, last }, { caseStyle, minorWords }) {
|
|
3314
|
+
if (caseStyle === "Title Case") {
|
|
3315
|
+
if (first || last) return {
|
|
3316
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3317
|
+
isMinorWord: false
|
|
3318
|
+
};
|
|
3319
|
+
if (minorWords.some((minorWord) => minorWord.toLowerCase() === word.toLowerCase())) return {
|
|
3320
|
+
word: word.toLowerCase(),
|
|
3321
|
+
isMinorWord: true
|
|
3322
|
+
};
|
|
3323
|
+
return {
|
|
3324
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3325
|
+
isMinorWord: false
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
if (first) return {
|
|
3329
|
+
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3330
|
+
isMinorWord: false
|
|
3331
|
+
};
|
|
3332
|
+
return {
|
|
3333
|
+
word: word.toLowerCase(),
|
|
3334
|
+
isMinorWord: false
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
//#endregion
|
|
3339
|
+
//#region src/utils/preserve-words.ts
|
|
3340
|
+
var PreserveWordsContext = class {
|
|
3341
|
+
preserveWords;
|
|
3342
|
+
preservePhrases;
|
|
3343
|
+
constructor(preserveWordsOption) {
|
|
3344
|
+
const preserveWords = /* @__PURE__ */ new Map();
|
|
3345
|
+
/**
|
|
3346
|
+
* Add a single word to the preserveWords map
|
|
3347
|
+
*/
|
|
3348
|
+
function addPreserveWord(word) {
|
|
3349
|
+
const lowerWord = word.toLowerCase();
|
|
3350
|
+
let list = preserveWords.get(lowerWord);
|
|
3351
|
+
if (!list) {
|
|
3352
|
+
list = [];
|
|
3353
|
+
preserveWords.set(lowerWord, list);
|
|
3354
|
+
}
|
|
3355
|
+
list.push(word);
|
|
3207
3356
|
}
|
|
3208
|
-
|
|
3357
|
+
const preservePhrases = /* @__PURE__ */ new Map();
|
|
3358
|
+
for (const word of preserveWordsOption) {
|
|
3359
|
+
const splitted = word.split(/\s+/);
|
|
3360
|
+
if (splitted.length <= 1) addPreserveWord(word);
|
|
3361
|
+
else {
|
|
3362
|
+
preservePhrases.set(word, splitted);
|
|
3363
|
+
addPreserveWord(splitted.join(""));
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
this.preserveWords = preserveWords;
|
|
3367
|
+
this.preservePhrases = [...preservePhrases.values()].sort((a, b) => b.length - a.length);
|
|
3368
|
+
}
|
|
3369
|
+
findPreserveWord(word) {
|
|
3370
|
+
return this.preserveWords.get(word.toLowerCase()) ?? null;
|
|
3209
3371
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3372
|
+
findPreservePhrase(words) {
|
|
3373
|
+
const firstWord = words.next();
|
|
3374
|
+
if (firstWord.done) return null;
|
|
3375
|
+
const firstLowerWord = firstWord.value.toLowerCase();
|
|
3376
|
+
let returnCandidate = null;
|
|
3377
|
+
const subWords = [firstWord.value];
|
|
3378
|
+
for (const phrase of this.preservePhrases) {
|
|
3379
|
+
if (returnCandidate && returnCandidate.preservePhrase.length !== phrase.length) break;
|
|
3380
|
+
if (firstLowerWord !== phrase[0].toLowerCase()) continue;
|
|
3381
|
+
while (subWords.length < phrase.length) {
|
|
3382
|
+
const word = words.next();
|
|
3383
|
+
if (word.done) return null;
|
|
3384
|
+
subWords.push(word.value);
|
|
3385
|
+
}
|
|
3386
|
+
if (subWords.every((word, i) => word.toLowerCase() === phrase[i].toLowerCase())) {
|
|
3387
|
+
let matchCount = 0;
|
|
3388
|
+
for (let i = 0; i < subWords.length; i++) {
|
|
3389
|
+
const word = subWords[i];
|
|
3390
|
+
if (word === phrase[i]) matchCount++;
|
|
3391
|
+
}
|
|
3392
|
+
if (!returnCandidate || matchCount > returnCandidate.matchCount) returnCandidate = {
|
|
3393
|
+
preservePhrase: phrase,
|
|
3394
|
+
matchCount
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3217
3397
|
}
|
|
3398
|
+
return returnCandidate?.preservePhrase ?? null;
|
|
3218
3399
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3400
|
+
};
|
|
3401
|
+
/**
|
|
3402
|
+
* Parse preserve words and phrases from the options
|
|
3403
|
+
* - Single words are added to preserveWords
|
|
3404
|
+
* - Multi-word phrases are added to preservePhrases and added to preserveWords with no spaces
|
|
3405
|
+
*/
|
|
3406
|
+
function parsePreserveWordsOption(preserveWordsOption) {
|
|
3407
|
+
return new PreserveWordsContext(preserveWordsOption);
|
|
3223
3408
|
}
|
|
3409
|
+
|
|
3410
|
+
//#endregion
|
|
3411
|
+
//#region src/utils/words.ts
|
|
3224
3412
|
/**
|
|
3225
3413
|
* Parse text into words with offsets
|
|
3226
3414
|
*/
|
|
3227
|
-
function
|
|
3415
|
+
function parseWordsFromText(text, firstNode, lastNode) {
|
|
3228
3416
|
const words = [];
|
|
3229
3417
|
const pattern = /(\w+(?:[^\s\w]\w+)*|:\w+:|[^\s\w]+|\s+)/gu;
|
|
3230
3418
|
let match;
|
|
@@ -3255,33 +3443,9 @@ function parseText(text, firstNode, lastNode) {
|
|
|
3255
3443
|
}
|
|
3256
3444
|
return words;
|
|
3257
3445
|
}
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
function convertWord({ word, first, last }, caseStyle, minorWords) {
|
|
3262
|
-
if (caseStyle === "Title Case") {
|
|
3263
|
-
if (first || last) return {
|
|
3264
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3265
|
-
isMinorWord: false
|
|
3266
|
-
};
|
|
3267
|
-
if (minorWords.some((minorWord) => minorWord.toLowerCase() === word.toLowerCase())) return {
|
|
3268
|
-
word: word.toLowerCase(),
|
|
3269
|
-
isMinorWord: true
|
|
3270
|
-
};
|
|
3271
|
-
return {
|
|
3272
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3273
|
-
isMinorWord: false
|
|
3274
|
-
};
|
|
3275
|
-
}
|
|
3276
|
-
if (first) return {
|
|
3277
|
-
word: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
|
3278
|
-
isMinorWord: false
|
|
3279
|
-
};
|
|
3280
|
-
return {
|
|
3281
|
-
word: word.toLowerCase(),
|
|
3282
|
-
isMinorWord: false
|
|
3283
|
-
};
|
|
3284
|
-
}
|
|
3446
|
+
|
|
3447
|
+
//#endregion
|
|
3448
|
+
//#region src/rules/heading-casing.ts
|
|
3285
3449
|
var heading_casing_default = createRule("heading-casing", {
|
|
3286
3450
|
meta: {
|
|
3287
3451
|
type: "suggestion",
|
|
@@ -3324,14 +3488,14 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3324
3488
|
create(context) {
|
|
3325
3489
|
const sourceCode = context.sourceCode;
|
|
3326
3490
|
const caseStyle = context.options[0]?.style || "Title Case";
|
|
3327
|
-
const
|
|
3491
|
+
const preserveWordsOption = parsePreserveWordsOption(context.options[0]?.preserveWords || defaultPreserveWords);
|
|
3328
3492
|
const minorWords = context.options[0]?.minorWords || defaultMinorWords;
|
|
3329
3493
|
const ignorePatterns = (context.options[0]?.ignorePatterns || [
|
|
3330
3494
|
"/^v\\d+/u",
|
|
3331
3495
|
"/\\w+\\.[a-z\\d]+$/u",
|
|
3332
|
-
"/\\w
|
|
3333
|
-
"/\\w
|
|
3334
|
-
"/\\w
|
|
3496
|
+
"/\\w+(?:API|Api)$/u",
|
|
3497
|
+
"/\\w+(?:SDK|Sdk)$/u",
|
|
3498
|
+
"/\\w+(?:CLI|Cli)$/u"
|
|
3335
3499
|
]).map((pattern) => {
|
|
3336
3500
|
if (isRegExp(pattern)) return toRegExp(pattern);
|
|
3337
3501
|
try {
|
|
@@ -3347,7 +3511,7 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3347
3511
|
*/
|
|
3348
3512
|
function checkTextNode(node, firstNode, lastNode) {
|
|
3349
3513
|
const text = sourceCode.getText(node);
|
|
3350
|
-
const wordAndOffsets =
|
|
3514
|
+
const wordAndOffsets = parseWordsFromText(text, firstNode, lastNode);
|
|
3351
3515
|
const processed = /* @__PURE__ */ new Set();
|
|
3352
3516
|
for (let index = 0; index < wordAndOffsets.length; index++) {
|
|
3353
3517
|
if (processed.has(index)) continue;
|
|
@@ -3355,7 +3519,12 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3355
3519
|
const wordAndOffset = wordAndOffsets[index];
|
|
3356
3520
|
if (wordAndOffset.punctuation) continue;
|
|
3357
3521
|
if (ignorePatterns.some((pattern) => pattern.test(wordAndOffset.word))) continue;
|
|
3358
|
-
const preservePhrase = findPreservePhrase(
|
|
3522
|
+
const preservePhrase = preserveWordsOption.findPreservePhrase((function* () {
|
|
3523
|
+
const firstWord = wordAndOffsets[index];
|
|
3524
|
+
if (firstWord.punctuation) return;
|
|
3525
|
+
yield firstWord.word;
|
|
3526
|
+
for (let next = index + 1; next < wordAndOffsets.length; next++) yield wordAndOffsets[next].word;
|
|
3527
|
+
})());
|
|
3359
3528
|
if (preservePhrase) {
|
|
3360
3529
|
for (let wordIndex = 0; wordIndex < preservePhrase.length; wordIndex++) {
|
|
3361
3530
|
processed.add(index + wordIndex);
|
|
@@ -3363,12 +3532,15 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3363
3532
|
}
|
|
3364
3533
|
continue;
|
|
3365
3534
|
}
|
|
3366
|
-
const preserveWordList =
|
|
3535
|
+
const preserveWordList = preserveWordsOption.findPreserveWord(wordAndOffset.word);
|
|
3367
3536
|
if (preserveWordList) {
|
|
3368
3537
|
if (!preserveWordList.some((w) => w === wordAndOffset.word)) verifyWord(wordAndOffset, preserveWordList[0], "preserved");
|
|
3369
3538
|
continue;
|
|
3370
3539
|
}
|
|
3371
|
-
const expectedWordResult =
|
|
3540
|
+
const expectedWordResult = convertWordCasing(wordAndOffset, {
|
|
3541
|
+
caseStyle,
|
|
3542
|
+
minorWords
|
|
3543
|
+
});
|
|
3372
3544
|
verifyWord(wordAndOffset, expectedWordResult.word, expectedWordResult.isMinorWord ? "minor" : "normal");
|
|
3373
3545
|
}
|
|
3374
3546
|
/**
|
|
@@ -3393,58 +3565,109 @@ var heading_casing_default = createRule("heading-casing", {
|
|
|
3393
3565
|
});
|
|
3394
3566
|
}
|
|
3395
3567
|
}
|
|
3396
|
-
/**
|
|
3397
|
-
* Find a preserve phrase starting from the given index
|
|
3398
|
-
* Returns the longest matching phrase or null if no match is found
|
|
3399
|
-
*/
|
|
3400
|
-
function findPreservePhrase(wordAndOffsets, index) {
|
|
3401
|
-
const firstWord = wordAndOffsets[index];
|
|
3402
|
-
if (firstWord.punctuation) return null;
|
|
3403
|
-
const firstLowerWord = firstWord.word.toLowerCase();
|
|
3404
|
-
let returnCandidate = null;
|
|
3405
|
-
let subWords = null;
|
|
3406
|
-
for (const phrase of preservePhrases) {
|
|
3407
|
-
if (returnCandidate && returnCandidate.preservePhrase.length !== phrase.length) break;
|
|
3408
|
-
if (firstLowerWord !== phrase[0].toLowerCase()) continue;
|
|
3409
|
-
if (!subWords || subWords.length !== phrase.length) subWords = wordAndOffsets.slice(index, index + phrase.length).map((wo) => wo.word);
|
|
3410
|
-
if (subWords.length === phrase.length && subWords.every((word, i) => word.toLowerCase() === phrase[i].toLowerCase())) {
|
|
3411
|
-
let matchCount = 0;
|
|
3412
|
-
for (let i = 0; i < subWords.length; i++) {
|
|
3413
|
-
const word = subWords[i];
|
|
3414
|
-
if (word === phrase[i]) matchCount++;
|
|
3415
|
-
}
|
|
3416
|
-
if (!returnCandidate || matchCount > returnCandidate.matchCount) returnCandidate = {
|
|
3417
|
-
preservePhrase: phrase,
|
|
3418
|
-
matchCount
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
return returnCandidate?.preservePhrase ?? null;
|
|
3423
|
-
}
|
|
3424
3568
|
return { heading(node) {
|
|
3425
3569
|
if (!node.children.length) return;
|
|
3426
3570
|
const children = node.children.filter((child) => child.type !== "text" || child.value.trim());
|
|
3427
3571
|
children.forEach((child, i) => {
|
|
3428
|
-
if (child.type === "text") checkTextNode(child, i === 0, i ===
|
|
3572
|
+
if (child.type === "text") checkTextNode(child, i === 0, i === children.length - 1);
|
|
3429
3573
|
});
|
|
3430
3574
|
} };
|
|
3431
3575
|
}
|
|
3432
3576
|
});
|
|
3433
3577
|
|
|
3578
|
+
//#endregion
|
|
3579
|
+
//#region src/rules/list-marker-alignment.ts
|
|
3580
|
+
const ALIGN_TO_POSITION_NAME = {
|
|
3581
|
+
left: "start",
|
|
3582
|
+
right: "end"
|
|
3583
|
+
};
|
|
3584
|
+
var list_marker_alignment_default = createRule("list-marker-alignment", {
|
|
3585
|
+
meta: {
|
|
3586
|
+
type: "layout",
|
|
3587
|
+
docs: {
|
|
3588
|
+
description: "enforce consistent alignment of list markers",
|
|
3589
|
+
categories: ["recommended"],
|
|
3590
|
+
listCategory: "Stylistic"
|
|
3591
|
+
},
|
|
3592
|
+
fixable: "whitespace",
|
|
3593
|
+
hasSuggestions: false,
|
|
3594
|
+
schema: [{
|
|
3595
|
+
type: "object",
|
|
3596
|
+
properties: { align: { enum: ["left", "right"] } },
|
|
3597
|
+
additionalProperties: false
|
|
3598
|
+
}],
|
|
3599
|
+
messages: { incorrectAlignment: "List marker alignment is inconsistent. Expected {{expected}} characters of indentation, but got {{actual}}." }
|
|
3600
|
+
},
|
|
3601
|
+
create(context) {
|
|
3602
|
+
const sourceCode = context.sourceCode;
|
|
3603
|
+
const alignPositionName = ALIGN_TO_POSITION_NAME[context.options[0]?.align ?? "left"];
|
|
3604
|
+
/**
|
|
3605
|
+
* Get the marker location of a list item
|
|
3606
|
+
*/
|
|
3607
|
+
function getMarkerLocation(node) {
|
|
3608
|
+
const start = sourceCode.getLoc(node).start;
|
|
3609
|
+
const startColumnIndex = start.column - 1;
|
|
3610
|
+
const marker = getListItemMarker(sourceCode, node);
|
|
3611
|
+
return {
|
|
3612
|
+
line: start.line,
|
|
3613
|
+
start: startColumnIndex,
|
|
3614
|
+
end: startColumnIndex + marker.raw.length
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Check if list items have consistent alignment
|
|
3619
|
+
*/
|
|
3620
|
+
function checkListAlignment(listNode) {
|
|
3621
|
+
const items = listNode.children;
|
|
3622
|
+
if (items.length <= 1) return;
|
|
3623
|
+
const referenceMarkerLocation = getMarkerLocation(items[0]);
|
|
3624
|
+
for (const item of items.slice(1)) {
|
|
3625
|
+
const markerLocation = getMarkerLocation(item);
|
|
3626
|
+
const diff = markerLocation[alignPositionName] - referenceMarkerLocation[alignPositionName];
|
|
3627
|
+
if (diff === 0) continue;
|
|
3628
|
+
context.report({
|
|
3629
|
+
node: item,
|
|
3630
|
+
loc: {
|
|
3631
|
+
start: {
|
|
3632
|
+
line: markerLocation.line,
|
|
3633
|
+
column: markerLocation.start + 1
|
|
3634
|
+
},
|
|
3635
|
+
end: {
|
|
3636
|
+
line: markerLocation.line,
|
|
3637
|
+
column: markerLocation.end + 1
|
|
3638
|
+
}
|
|
3639
|
+
},
|
|
3640
|
+
messageId: "incorrectAlignment",
|
|
3641
|
+
data: {
|
|
3642
|
+
expected: String(markerLocation.start - diff),
|
|
3643
|
+
actual: String(markerLocation.start)
|
|
3644
|
+
},
|
|
3645
|
+
fix(fixer) {
|
|
3646
|
+
const lines = getParsedLines(sourceCode);
|
|
3647
|
+
const line = lines.get(markerLocation.line);
|
|
3648
|
+
if (diff < 0) {
|
|
3649
|
+
const addSpaces = " ".repeat(-diff);
|
|
3650
|
+
return fixer.insertTextBeforeRange([line.range[0] + markerLocation.start, line.range[0] + markerLocation.start], addSpaces);
|
|
3651
|
+
}
|
|
3652
|
+
const itemBefore = line.text.slice(0, markerLocation.start - diff);
|
|
3653
|
+
if (itemBefore.includes(" ")) return null;
|
|
3654
|
+
const referenceMarkerBefore = lines.get(referenceMarkerLocation.line).text.slice(0, referenceMarkerLocation.start);
|
|
3655
|
+
if (referenceMarkerBefore === itemBefore) {
|
|
3656
|
+
const removeEndIndex = line.range[0] + markerLocation.start;
|
|
3657
|
+
const removeStartIndex = removeEndIndex - diff;
|
|
3658
|
+
return fixer.removeRange([removeStartIndex, removeEndIndex]);
|
|
3659
|
+
}
|
|
3660
|
+
return null;
|
|
3661
|
+
}
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return { list: checkListAlignment };
|
|
3666
|
+
}
|
|
3667
|
+
});
|
|
3668
|
+
|
|
3434
3669
|
//#endregion
|
|
3435
3670
|
//#region src/rules/no-laziness-blockquotes.ts
|
|
3436
|
-
/**
|
|
3437
|
-
* Helper function to get blockquote line information.
|
|
3438
|
-
*/
|
|
3439
|
-
function getBlockquoteLineInfo(line) {
|
|
3440
|
-
const regex = /^\s*(?:>\s*)*/u;
|
|
3441
|
-
const match = regex.exec(line.text);
|
|
3442
|
-
return {
|
|
3443
|
-
line,
|
|
3444
|
-
prefix: match[0],
|
|
3445
|
-
level: (match[0].match(/>/gu) || []).length
|
|
3446
|
-
};
|
|
3447
|
-
}
|
|
3448
3671
|
var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
3449
3672
|
meta: {
|
|
3450
3673
|
type: "problem",
|
|
@@ -3465,7 +3688,6 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
|
3465
3688
|
create(context) {
|
|
3466
3689
|
const sourceCode = context.sourceCode;
|
|
3467
3690
|
const checkedLines = /* @__PURE__ */ new Set();
|
|
3468
|
-
const lines = parseLines(sourceCode);
|
|
3469
3691
|
/**
|
|
3470
3692
|
* Report invalid blockquote lines.
|
|
3471
3693
|
*/
|
|
@@ -3489,15 +3711,16 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
|
3489
3711
|
for (const group of invalidGroups) {
|
|
3490
3712
|
const first = group.lines[0];
|
|
3491
3713
|
const last = group.lines.at(-1);
|
|
3714
|
+
const lines = getParsedLines(sourceCode);
|
|
3492
3715
|
context.report({
|
|
3493
3716
|
loc: {
|
|
3494
3717
|
start: {
|
|
3495
|
-
line: first.line
|
|
3718
|
+
line: first.line,
|
|
3496
3719
|
column: 1
|
|
3497
3720
|
},
|
|
3498
3721
|
end: {
|
|
3499
|
-
line: last.line
|
|
3500
|
-
column: last.line.text.length + 1
|
|
3722
|
+
line: last.line,
|
|
3723
|
+
column: lines.get(last.line).text.length + 1
|
|
3501
3724
|
}
|
|
3502
3725
|
},
|
|
3503
3726
|
messageId: "lazyBlockquoteLine",
|
|
@@ -3509,12 +3732,16 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
|
3509
3732
|
messageId: "addMarker",
|
|
3510
3733
|
data: { missingMarkers: `${base.level - group.level}` },
|
|
3511
3734
|
*fix(fixer) {
|
|
3512
|
-
for (const invalidLine of group.lines)
|
|
3735
|
+
for (const invalidLine of group.lines) {
|
|
3736
|
+
const parsedLine = lines.get(invalidLine.line);
|
|
3737
|
+
yield fixer.replaceTextRange([parsedLine.range[0], parsedLine.range[0] + invalidLine.prefix.length], base.prefix);
|
|
3738
|
+
}
|
|
3513
3739
|
}
|
|
3514
3740
|
}, {
|
|
3515
3741
|
messageId: "addLineBreak",
|
|
3516
3742
|
fix: (fixer) => {
|
|
3517
|
-
|
|
3743
|
+
const parsedLine = lines.get(first.line);
|
|
3744
|
+
return fixer.insertTextBeforeRange([parsedLine.range[0], parsedLine.range[0]], `${first.prefix.trimEnd()}\n`);
|
|
3518
3745
|
}
|
|
3519
3746
|
}]
|
|
3520
3747
|
});
|
|
@@ -3524,7 +3751,7 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
|
3524
3751
|
const loc = sourceCode.getLoc(node);
|
|
3525
3752
|
const startLine = loc.start.line;
|
|
3526
3753
|
const endLine = loc.end.line;
|
|
3527
|
-
const base =
|
|
3754
|
+
const base = getBlockquoteLevelFromLine(sourceCode, startLine);
|
|
3528
3755
|
const invalidLines = [];
|
|
3529
3756
|
for (let lineNumber = startLine + 1; lineNumber <= endLine; lineNumber++) {
|
|
3530
3757
|
if (checkedLines.has(lineNumber)) {
|
|
@@ -3533,8 +3760,7 @@ var no_laziness_blockquotes_default = createRule("no-laziness-blockquotes", {
|
|
|
3533
3760
|
continue;
|
|
3534
3761
|
}
|
|
3535
3762
|
checkedLines.add(lineNumber);
|
|
3536
|
-
const
|
|
3537
|
-
const current = getBlockquoteLineInfo(line);
|
|
3763
|
+
const current = getBlockquoteLevelFromLine(sourceCode, lineNumber);
|
|
3538
3764
|
if (base.level <= current.level) {
|
|
3539
3765
|
reportInvalidLines(invalidLines, base);
|
|
3540
3766
|
invalidLines.length = 0;
|
|
@@ -3611,7 +3837,7 @@ var no_multiple_empty_lines_default = createRule("no-multiple-empty-lines", {
|
|
|
3611
3837
|
toml: addIgnoreLoc,
|
|
3612
3838
|
json: addIgnoreLoc,
|
|
3613
3839
|
"root:exit"() {
|
|
3614
|
-
const lines = [...
|
|
3840
|
+
const lines = [...getParsedLines(sourceCode)];
|
|
3615
3841
|
const bofEmptyLines = [];
|
|
3616
3842
|
while (lines.length) {
|
|
3617
3843
|
if (lines[0].text.trim()) break;
|
|
@@ -3710,11 +3936,11 @@ var no_multiple_empty_lines_default = createRule("no-multiple-empty-lines", {
|
|
|
3710
3936
|
//#region src/rules/no-text-backslash-linebreak.ts
|
|
3711
3937
|
var no_text_backslash_linebreak_default = createRule("no-text-backslash-linebreak", {
|
|
3712
3938
|
meta: {
|
|
3713
|
-
type: "
|
|
3939
|
+
type: "layout",
|
|
3714
3940
|
docs: {
|
|
3715
3941
|
description: "disallow text backslash at the end of a line.",
|
|
3716
3942
|
categories: ["recommended"],
|
|
3717
|
-
listCategory: "
|
|
3943
|
+
listCategory: "Stylistic"
|
|
3718
3944
|
},
|
|
3719
3945
|
fixable: void 0,
|
|
3720
3946
|
hasSuggestions: true,
|
|
@@ -3825,7 +4051,7 @@ var no_trailing_spaces_default = createRule("no-trailing-spaces", {
|
|
|
3825
4051
|
"root:exit"() {
|
|
3826
4052
|
const re = /[^\S\n\r]+$/u;
|
|
3827
4053
|
const skipMatch = /^[^\S\n\r]*$/u;
|
|
3828
|
-
const lines =
|
|
4054
|
+
const lines = getParsedLines(sourceCode);
|
|
3829
4055
|
const commentLineNumbers = getCommentLineNumbers();
|
|
3830
4056
|
for (const lineInfo of lines) {
|
|
3831
4057
|
const matches = re.exec(lineInfo.text);
|
|
@@ -4113,7 +4339,6 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
|
|
|
4113
4339
|
},
|
|
4114
4340
|
create(context) {
|
|
4115
4341
|
const sourceCode = context.sourceCode;
|
|
4116
|
-
const lines = parseLines(sourceCode);
|
|
4117
4342
|
return { code(node) {
|
|
4118
4343
|
const kind = getCodeBlockKind(sourceCode, node);
|
|
4119
4344
|
if (kind === "backtick-fenced" || kind === "tilde-fenced") return;
|
|
@@ -4123,6 +4348,7 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
|
|
|
4123
4348
|
messageId: "useFencedCodeBlock",
|
|
4124
4349
|
fix(fixer) {
|
|
4125
4350
|
if (!isFixableIndentedCodeBlock(node)) return null;
|
|
4351
|
+
const lines = getParsedLines(sourceCode);
|
|
4126
4352
|
const startColumnOffset = loc.start.column - 1;
|
|
4127
4353
|
const removeRanges = [];
|
|
4128
4354
|
let prefixText = null;
|
|
@@ -4158,6 +4384,7 @@ var prefer_fenced_code_blocks_default = createRule("prefer-fenced-code-blocks",
|
|
|
4158
4384
|
*/
|
|
4159
4385
|
function isFixableIndentedCodeBlock(node) {
|
|
4160
4386
|
if (!node.value.startsWith(" ")) return true;
|
|
4387
|
+
const lines = getParsedLines(sourceCode);
|
|
4161
4388
|
const loc = sourceCode.getLoc(node);
|
|
4162
4389
|
const firstLine = lines.get(loc.start.line);
|
|
4163
4390
|
const codeBlockFirstLine = normalizePrefix(node.value.split(/\r?\n/u)[0]);
|
|
@@ -4781,14 +5008,14 @@ var sort_definitions_default = createRule("sort-definitions", {
|
|
|
4781
5008
|
}
|
|
4782
5009
|
/** Compile order option */
|
|
4783
5010
|
function compileOption(orderOption) {
|
|
4784
|
-
const cache$
|
|
5011
|
+
const cache$2 = /* @__PURE__ */ new Map();
|
|
4785
5012
|
const compiled = compileOptionWithoutCache(orderOption);
|
|
4786
5013
|
return {
|
|
4787
5014
|
match: (node) => {
|
|
4788
|
-
const cached = cache$
|
|
5015
|
+
const cached = cache$2.get(node);
|
|
4789
5016
|
if (cached != null) return cached;
|
|
4790
5017
|
const result = compiled.match(node);
|
|
4791
|
-
cache$
|
|
5018
|
+
cache$2.set(node, result);
|
|
4792
5019
|
return result;
|
|
4793
5020
|
},
|
|
4794
5021
|
sort: compiled.sort
|
|
@@ -4891,16 +5118,152 @@ function normalizedURL(url) {
|
|
|
4891
5118
|
return urlObj.href.endsWith("/") ? urlObj.href : `${urlObj.href}/`;
|
|
4892
5119
|
}
|
|
4893
5120
|
|
|
5121
|
+
//#endregion
|
|
5122
|
+
//#region src/rules/table-header-casing.ts
|
|
5123
|
+
var table_header_casing_default = createRule("table-header-casing", {
|
|
5124
|
+
meta: {
|
|
5125
|
+
type: "suggestion",
|
|
5126
|
+
fixable: "code",
|
|
5127
|
+
docs: {
|
|
5128
|
+
description: "enforce consistent casing in table header cells.",
|
|
5129
|
+
categories: [],
|
|
5130
|
+
listCategory: "Preference"
|
|
5131
|
+
},
|
|
5132
|
+
schema: [{
|
|
5133
|
+
type: "object",
|
|
5134
|
+
properties: {
|
|
5135
|
+
style: { enum: ["Title Case", "Sentence case"] },
|
|
5136
|
+
preserveWords: {
|
|
5137
|
+
type: "array",
|
|
5138
|
+
items: { type: "string" },
|
|
5139
|
+
description: "Words that should be preserved as-is (case-insensitive matching)"
|
|
5140
|
+
},
|
|
5141
|
+
ignorePatterns: {
|
|
5142
|
+
type: "array",
|
|
5143
|
+
items: { type: "string" },
|
|
5144
|
+
description: "Regular expression patterns for words to ignore during casing checks"
|
|
5145
|
+
},
|
|
5146
|
+
minorWords: {
|
|
5147
|
+
type: "array",
|
|
5148
|
+
items: { type: "string" },
|
|
5149
|
+
description: "Words that should not be capitalized in Title Case (unless they're the first or last word)"
|
|
5150
|
+
}
|
|
5151
|
+
},
|
|
5152
|
+
additionalProperties: false
|
|
5153
|
+
}],
|
|
5154
|
+
messages: {
|
|
5155
|
+
expectedTitleCase: "Expected \"{{actual}}\" to be \"{{expected}}\" (Title Case).",
|
|
5156
|
+
expectedTitleCaseMinorWord: "Expected \"{{actual}}\" to be \"{{expected}}\" (Title Case - minor word).",
|
|
5157
|
+
expectedSentenceCase: "Expected \"{{actual}}\" to be \"{{expected}}\" (Sentence case).",
|
|
5158
|
+
expectedPreserveWord: "Expected \"{{actual}}\" to be \"{{expected}}\" (preserved word)."
|
|
5159
|
+
}
|
|
5160
|
+
},
|
|
5161
|
+
create(context) {
|
|
5162
|
+
const sourceCode = context.sourceCode;
|
|
5163
|
+
const caseStyle = context.options[0]?.style || "Title Case";
|
|
5164
|
+
const preserveWordsOption = parsePreserveWordsOption(context.options[0]?.preserveWords || defaultPreserveWords);
|
|
5165
|
+
const minorWords = context.options[0]?.minorWords || defaultMinorWords;
|
|
5166
|
+
const ignorePatterns = (context.options[0]?.ignorePatterns || [
|
|
5167
|
+
"/^v\\d+/u",
|
|
5168
|
+
"/\\w+\\.[a-z\\d]+$/u",
|
|
5169
|
+
"/\\w+(?:API|Api)$/u",
|
|
5170
|
+
"/\\w+(?:SDK|Sdk)$/u",
|
|
5171
|
+
"/\\w+(?:CLI|Cli)$/u"
|
|
5172
|
+
]).map((pattern) => {
|
|
5173
|
+
if (isRegExp(pattern)) return toRegExp(pattern);
|
|
5174
|
+
try {
|
|
5175
|
+
return new RegExp(pattern, "v");
|
|
5176
|
+
} catch {}
|
|
5177
|
+
try {
|
|
5178
|
+
return new RegExp(pattern, "u");
|
|
5179
|
+
} catch {}
|
|
5180
|
+
return new RegExp(pattern);
|
|
5181
|
+
});
|
|
5182
|
+
/**
|
|
5183
|
+
* Check text node and report word-level errors
|
|
5184
|
+
*/
|
|
5185
|
+
function checkTextNode(node, firstNode, lastNode) {
|
|
5186
|
+
const text = sourceCode.getText(node);
|
|
5187
|
+
const wordAndOffsets = parseWordsFromText(text, firstNode, lastNode);
|
|
5188
|
+
const processed = /* @__PURE__ */ new Set();
|
|
5189
|
+
for (let index = 0; index < wordAndOffsets.length; index++) {
|
|
5190
|
+
if (processed.has(index)) continue;
|
|
5191
|
+
processed.add(index);
|
|
5192
|
+
const wordAndOffset = wordAndOffsets[index];
|
|
5193
|
+
if (wordAndOffset.punctuation) continue;
|
|
5194
|
+
if (ignorePatterns.some((pattern) => pattern.test(wordAndOffset.word))) continue;
|
|
5195
|
+
const preservePhrase = preserveWordsOption.findPreservePhrase((function* () {
|
|
5196
|
+
const firstWord = wordAndOffsets[index];
|
|
5197
|
+
if (firstWord.punctuation) return;
|
|
5198
|
+
yield firstWord.word;
|
|
5199
|
+
for (let next = index + 1; next < wordAndOffsets.length; next++) yield wordAndOffsets[next].word;
|
|
5200
|
+
})());
|
|
5201
|
+
if (preservePhrase) {
|
|
5202
|
+
for (let wordIndex = 0; wordIndex < preservePhrase.length; wordIndex++) {
|
|
5203
|
+
processed.add(index + wordIndex);
|
|
5204
|
+
verifyWord(wordAndOffsets[index + wordIndex], preservePhrase[wordIndex], "preserved");
|
|
5205
|
+
}
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
const preserveWordList = preserveWordsOption.findPreserveWord(wordAndOffset.word);
|
|
5209
|
+
if (preserveWordList) {
|
|
5210
|
+
if (!preserveWordList.some((w) => w === wordAndOffset.word)) verifyWord(wordAndOffset, preserveWordList[0], "preserved");
|
|
5211
|
+
continue;
|
|
5212
|
+
}
|
|
5213
|
+
const expectedWordResult = convertWordCasing(wordAndOffset, {
|
|
5214
|
+
caseStyle,
|
|
5215
|
+
minorWords
|
|
5216
|
+
});
|
|
5217
|
+
verifyWord(wordAndOffset, expectedWordResult.word, expectedWordResult.isMinorWord ? "minor" : "normal");
|
|
5218
|
+
}
|
|
5219
|
+
/**
|
|
5220
|
+
* Verify a single word against the expected casing
|
|
5221
|
+
*/
|
|
5222
|
+
function verifyWord(wordAndOffset, expectedWord, wordType = "normal") {
|
|
5223
|
+
const { word, offset } = wordAndOffset;
|
|
5224
|
+
if (word === expectedWord) return;
|
|
5225
|
+
const [nodeStart] = sourceCode.getRange(node);
|
|
5226
|
+
const range = [nodeStart + offset, nodeStart + offset + word.length];
|
|
5227
|
+
context.report({
|
|
5228
|
+
node,
|
|
5229
|
+
messageId: wordType === "preserved" ? "expectedPreserveWord" : caseStyle === "Title Case" ? wordType === "minor" ? "expectedTitleCaseMinorWord" : "expectedTitleCase" : "expectedSentenceCase",
|
|
5230
|
+
data: {
|
|
5231
|
+
actual: word,
|
|
5232
|
+
expected: expectedWord
|
|
5233
|
+
},
|
|
5234
|
+
loc: getSourceLocationFromRange(sourceCode, node, range),
|
|
5235
|
+
fix(fixer) {
|
|
5236
|
+
return fixer.replaceTextRange(range, expectedWord);
|
|
5237
|
+
}
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
return { table(node) {
|
|
5242
|
+
if (!node.children.length) return;
|
|
5243
|
+
const headerRow = node.children[0];
|
|
5244
|
+
if (headerRow.type !== "tableRow") return;
|
|
5245
|
+
for (const cell of headerRow.children) {
|
|
5246
|
+
const children = cell.children.filter((child) => child.type !== "text" || child.value.trim());
|
|
5247
|
+
children.forEach((child, i) => {
|
|
5248
|
+
if (child.type === "text") checkTextNode(child, i === 0, i === children.length - 1);
|
|
5249
|
+
});
|
|
5250
|
+
}
|
|
5251
|
+
} };
|
|
5252
|
+
}
|
|
5253
|
+
});
|
|
5254
|
+
|
|
4894
5255
|
//#endregion
|
|
4895
5256
|
//#region src/utils/rules.ts
|
|
4896
5257
|
const rules$1 = [
|
|
4897
5258
|
atx_headings_closing_sequence_length_default,
|
|
4898
5259
|
atx_headings_closing_sequence_default,
|
|
5260
|
+
blockquote_marker_alignment_default,
|
|
4899
5261
|
canonical_code_block_language_default,
|
|
4900
5262
|
definitions_last_default,
|
|
4901
5263
|
emoji_notation_default,
|
|
4902
5264
|
hard_linebreak_style_default,
|
|
4903
5265
|
heading_casing_default,
|
|
5266
|
+
list_marker_alignment_default,
|
|
4904
5267
|
no_laziness_blockquotes_default,
|
|
4905
5268
|
no_multiple_empty_lines_default,
|
|
4906
5269
|
no_text_backslash_linebreak_default,
|
|
@@ -4912,7 +5275,8 @@ const rules$1 = [
|
|
|
4912
5275
|
prefer_inline_code_words_default,
|
|
4913
5276
|
prefer_link_reference_definitions_default,
|
|
4914
5277
|
prefer_linked_words_default,
|
|
4915
|
-
sort_definitions_default
|
|
5278
|
+
sort_definitions_default,
|
|
5279
|
+
table_header_casing_default
|
|
4916
5280
|
];
|
|
4917
5281
|
|
|
4918
5282
|
//#endregion
|
|
@@ -4937,7 +5301,9 @@ const plugins = {
|
|
|
4937
5301
|
}
|
|
4938
5302
|
};
|
|
4939
5303
|
const rules$2 = {
|
|
5304
|
+
"markdown-preferences/blockquote-marker-alignment": "error",
|
|
4940
5305
|
"markdown-preferences/hard-linebreak-style": "error",
|
|
5306
|
+
"markdown-preferences/list-marker-alignment": "error",
|
|
4941
5307
|
"markdown-preferences/no-laziness-blockquotes": "error",
|
|
4942
5308
|
"markdown-preferences/no-text-backslash-linebreak": "error",
|
|
4943
5309
|
"markdown-preferences/prefer-autolinks": "error",
|
|
@@ -4952,7 +5318,7 @@ __export(meta_exports, {
|
|
|
4952
5318
|
version: () => version
|
|
4953
5319
|
});
|
|
4954
5320
|
const name = "eslint-plugin-markdown-preferences";
|
|
4955
|
-
const version = "0.
|
|
5321
|
+
const version = "0.15.0";
|
|
4956
5322
|
|
|
4957
5323
|
//#endregion
|
|
4958
5324
|
//#region src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-markdown-preferences",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "ESLint plugin that enforces our markdown preferences",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"lint": "eslint .",
|
|
23
23
|
"tsc": "tsc --project tsconfig.build.json",
|
|
24
24
|
"eslint-fix": "eslint . --fix",
|
|
25
|
+
"markdownlint": "npx -y markdownlint-cli2 .",
|
|
25
26
|
"test": "npm run mocha -- \"tests/src/**/*.ts\" --reporter=dot --timeout=60000",
|
|
26
27
|
"cover": "c8 --reporter=lcov npm run test",
|
|
27
28
|
"test:update": "npm run mocha -- \"tests/src/**/*.ts\" --reporter=dot --update",
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
"@eslint/markdown": "^7.1.0",
|
|
60
61
|
"eslint": ">=9.0.0"
|
|
61
62
|
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"emoji-regex-xs": "^2.0.1",
|
|
65
|
+
"string-width": "^7.2.0"
|
|
66
|
+
},
|
|
62
67
|
"devDependencies": {
|
|
63
68
|
"@changesets/changelog-github": "^0.5.1",
|
|
64
69
|
"@changesets/cli": "^2.28.1",
|
|
@@ -121,9 +126,5 @@
|
|
|
121
126
|
},
|
|
122
127
|
"publishConfig": {
|
|
123
128
|
"access": "public"
|
|
124
|
-
},
|
|
125
|
-
"dependencies": {
|
|
126
|
-
"emoji-regex-xs": "^2.0.1",
|
|
127
|
-
"string-width": "^7.2.0"
|
|
128
129
|
}
|
|
129
130
|
}
|