eslint-plugin-md-style 0.1.0-beta.2 β 0.1.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 +17 -5
- package/dist/index.mjs +243 -149
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/eslint-plugin-md-style)
|
|
4
4
|
[](https://www.npmjs.com/package/eslint-plugin-md-style)
|
|
5
|
-
[](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style)
|
|
5
|
+
[](https://codecov.io/gh/NoiseFan/eslint-plugin-md-style?branch=main)
|
|
6
6
|
|
|
7
7
|
ESLint plugin for enforcing style rules in Markdown-based documentation.
|
|
8
8
|
|
|
@@ -18,6 +18,13 @@ It currently ships:
|
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
|
+
### Version Requirements
|
|
22
|
+
|
|
23
|
+
- `eslint`: `^9.30.0` or `^10.0.0`
|
|
24
|
+
- `@antfu/eslint-config`: `^7.5.0` when used
|
|
25
|
+
|
|
26
|
+
This plugin is designed for ESLint flat config. If you use `@antfu/eslint-config`, make sure its version satisfies the requirement above.
|
|
27
|
+
|
|
21
28
|
Install the required packages:
|
|
22
29
|
|
|
23
30
|
```bash
|
|
@@ -61,9 +68,9 @@ export default [
|
|
|
61
68
|
plugins: {
|
|
62
69
|
'md-style': mdStyle,
|
|
63
70
|
},
|
|
64
|
-
language: 'md-style/
|
|
71
|
+
language: 'md-style/gfm',
|
|
65
72
|
rules: {
|
|
66
|
-
'md-style/space-between-
|
|
73
|
+
'md-style/space-between-inline-element': 'error',
|
|
67
74
|
'md-style/valid-heading-anchor': 'error',
|
|
68
75
|
},
|
|
69
76
|
},
|
|
@@ -113,14 +120,19 @@ export default antfu(
|
|
|
113
120
|
|
|
114
121
|
| Rule | Included in `recommended` | Autofix |
|
|
115
122
|
| --- | --- | --- |
|
|
116
|
-
| `md-style/space-between-
|
|
123
|
+
| `md-style/space-between-inline-element` | β
| π§ |
|
|
117
124
|
| `md-style/valid-heading-anchor` | β
| π§ |
|
|
118
125
|
|
|
119
126
|
## Why `@eslint/markdown` Is Required
|
|
120
127
|
|
|
121
128
|
This plugin builds on top of `@eslint/markdown` rather than replacing it.
|
|
122
129
|
|
|
123
|
-
`@eslint/markdown` provides the Markdown processor and language support. This plugin re-exports those capabilities through its own plugin entry and adds documentation style rules on top, including the `md-style/
|
|
130
|
+
`@eslint/markdown` provides the Markdown processor and language support. This plugin re-exports those capabilities through its own plugin entry and adds documentation style rules on top, including the `md-style/gfm` language used by the bundled configs.
|
|
131
|
+
|
|
132
|
+
## References
|
|
133
|
+
|
|
134
|
+
- [W3C Manual of Style](https://www.w3.org/guide/manual-of-style/)
|
|
135
|
+
- [δΈζζηθ¦ζ±](https://w3c.github.io/clreq/)
|
|
124
136
|
|
|
125
137
|
## License
|
|
126
138
|
|
package/dist/index.mjs
CHANGED
|
@@ -10,12 +10,57 @@ function createRule({ create, defaultOptions, meta }) {
|
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/utils/ast.ts
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether an unknown value behaves like an mdast parent node.
|
|
18
|
+
*
|
|
19
|
+
* This intentionally accepts unknown values because ESLint's ancestor API does
|
|
20
|
+
* not expose mdast-specific types.
|
|
21
|
+
*/
|
|
22
|
+
function hasChildren(node) {
|
|
23
|
+
return !!node && typeof node === "object" && "children" in node && Array.isArray(node.children);
|
|
24
|
+
}
|
|
25
|
+
function isTableCell(node) {
|
|
26
|
+
return node.type === "tableCell";
|
|
27
|
+
}
|
|
28
|
+
const INLINE_ELEMENT_TYPES = new Set([
|
|
29
|
+
"link",
|
|
30
|
+
"image",
|
|
31
|
+
"inlineCode",
|
|
32
|
+
"emphasis",
|
|
33
|
+
"strong"
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Checks whether a phrasing node is one of the selected inline element targets.
|
|
37
|
+
*/
|
|
38
|
+
function isInlineElement(node) {
|
|
39
|
+
return !!node && INLINE_ELEMENT_TYPES.has(node.type);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Checks whether the current inline element is nested inside another selected inline element.
|
|
43
|
+
*/
|
|
44
|
+
function isNestedInlineElement(nodeContext) {
|
|
45
|
+
const { parent } = nodeContext;
|
|
46
|
+
return isInlineElement(parent);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extracts the plain-text value of a phrasing node.
|
|
50
|
+
* If the node does not expose `value`, recursively concatenates the text from its children.
|
|
51
|
+
*/
|
|
52
|
+
function getNodeValue(node) {
|
|
53
|
+
if (!node) return;
|
|
54
|
+
if ("value" in node) return node.value;
|
|
55
|
+
if (hasChildren(node)) return node.children.map(getNodeValue).join("") || void 0;
|
|
56
|
+
}
|
|
13
57
|
/**
|
|
14
58
|
* Gets the start and end offsets for a node.
|
|
15
59
|
*/
|
|
16
60
|
function getNodePosition(node) {
|
|
17
61
|
const start = node.position?.start.offset;
|
|
18
62
|
const end = node.position?.end.offset;
|
|
63
|
+
/* v8 ignore if -- @preserve */
|
|
19
64
|
if (start == null || end == null) return {
|
|
20
65
|
position: false,
|
|
21
66
|
start: 0,
|
|
@@ -27,18 +72,6 @@ function getNodePosition(node) {
|
|
|
27
72
|
end
|
|
28
73
|
};
|
|
29
74
|
}
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/utils/ast.ts
|
|
33
|
-
/**
|
|
34
|
-
* Checks whether an unknown value behaves like an mdast parent node.
|
|
35
|
-
*
|
|
36
|
-
* This intentionally accepts unknown values because ESLint's ancestor API does
|
|
37
|
-
* not expose mdast-specific types.
|
|
38
|
-
*/
|
|
39
|
-
function hasChildren(node) {
|
|
40
|
-
return !!node && typeof node === "object" && "children" in node && Array.isArray(node.children);
|
|
41
|
-
}
|
|
42
75
|
function getNodeContext(context, node) {
|
|
43
76
|
const parent = context.sourceCode.getAncestors(node).at(-1);
|
|
44
77
|
if (!hasChildren(parent)) return {
|
|
@@ -47,6 +80,7 @@ function getNodeContext(context, node) {
|
|
|
47
80
|
current: node
|
|
48
81
|
};
|
|
49
82
|
const currentIndex = parent.children.findIndex((child) => child === node);
|
|
83
|
+
/* v8 ignore if -- @preserve */
|
|
50
84
|
if (currentIndex === -1) return {
|
|
51
85
|
parent,
|
|
52
86
|
prev: void 0,
|
|
@@ -60,9 +94,17 @@ function getNodeContext(context, node) {
|
|
|
60
94
|
current: node
|
|
61
95
|
};
|
|
62
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Gets the character adjacent to the start or end of a string.
|
|
99
|
+
*/
|
|
100
|
+
function getAdjacentChar(str, position) {
|
|
101
|
+
if (!str) return void 0;
|
|
102
|
+
str = str.trim();
|
|
103
|
+
return position === "head" ? str[0] : str[str.length - 1];
|
|
104
|
+
}
|
|
63
105
|
|
|
64
106
|
//#endregion
|
|
65
|
-
//#region src/utils/
|
|
107
|
+
//#region src/utils/anchor.ts
|
|
66
108
|
/**
|
|
67
109
|
* Match the trailing anchor-like fragment from a heading string.
|
|
68
110
|
* @example `δΈζζ ι’ {#Chinese-Title}` -> `{#Chinese-Title}`
|
|
@@ -128,16 +170,7 @@ function calcAnchorPositionCompensate(content) {
|
|
|
128
170
|
}
|
|
129
171
|
|
|
130
172
|
//#endregion
|
|
131
|
-
//#region src/utils/
|
|
132
|
-
const LINK_SPACE_MESSAGE_IDS = {
|
|
133
|
-
missingSpaceBeforeLink: "missingSpaceBeforeLink",
|
|
134
|
-
missingSpaceAfterLink: "missingSpaceAfterLink",
|
|
135
|
-
multipleSpacesBeforeLink: "multipleSpacesBeforeLink",
|
|
136
|
-
multipleSpacesAfterLink: "multipleSpacesAfterLink",
|
|
137
|
-
multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
|
|
138
|
-
unexpectedSpaceBeforeLink: "unexpectedSpaceBeforeLink",
|
|
139
|
-
unexpectedSpaceAfterLink: "unexpectedSpaceAfterLink"
|
|
140
|
-
};
|
|
173
|
+
//#region src/utils/punctuation.ts
|
|
141
174
|
const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
142
175
|
"(",
|
|
143
176
|
"[",
|
|
@@ -149,6 +182,23 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
|
149
182
|
"β",
|
|
150
183
|
"β"
|
|
151
184
|
]);
|
|
185
|
+
const CLOSING_PAIRED_PUNCTUATION = new Set([
|
|
186
|
+
")",
|
|
187
|
+
"]",
|
|
188
|
+
"}",
|
|
189
|
+
">",
|
|
190
|
+
"οΌ",
|
|
191
|
+
"γ",
|
|
192
|
+
"γ",
|
|
193
|
+
"β",
|
|
194
|
+
"β"
|
|
195
|
+
]);
|
|
196
|
+
/**
|
|
197
|
+
* Checks whether the character is a slash used as a path-like separator.
|
|
198
|
+
*/
|
|
199
|
+
function isSlashPunctuation(str) {
|
|
200
|
+
return str === "/";
|
|
201
|
+
}
|
|
152
202
|
/**
|
|
153
203
|
* Checks whether the character is fullwidth punctuation.
|
|
154
204
|
* @example `γ` -> true
|
|
@@ -168,20 +218,6 @@ function isDashPunctuation(str) {
|
|
|
168
218
|
if (!str || str.length !== 1) return false;
|
|
169
219
|
return DASH_PUNCTUATION_RE.test(str);
|
|
170
220
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Checks whether adjacent text is a custom container marker on the next line.
|
|
173
|
-
*
|
|
174
|
-
* @deprecated Temporary workaround to prevent space-between-link from reporting
|
|
175
|
-
* false positives on custom containers. Remove this and handle the case in a
|
|
176
|
-
* dedicated custom container rule when one exists.
|
|
177
|
-
* @see https://vitepress.dev/guide/markdown#custom-containers
|
|
178
|
-
* @example `\n:::` -> true
|
|
179
|
-
* @example `\n::::` -> true
|
|
180
|
-
* @example `:::` -> false
|
|
181
|
-
*/
|
|
182
|
-
function isCustomContainerMarker(str) {
|
|
183
|
-
return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
|
|
184
|
-
}
|
|
185
221
|
const PUNCTUATION_RE = /^\p{P}$/u;
|
|
186
222
|
/**
|
|
187
223
|
* Checks whether the character is punctuation.
|
|
@@ -194,6 +230,20 @@ function isPunctuation(str) {
|
|
|
194
230
|
return PUNCTUATION_RE.test(str);
|
|
195
231
|
}
|
|
196
232
|
/**
|
|
233
|
+
* Checks whether the start or end of a string is adjacent to punctuation.
|
|
234
|
+
* @example `γ hello`, `head` -> true
|
|
235
|
+
* @example `hello .`, `tail` -> true
|
|
236
|
+
*/
|
|
237
|
+
function hasPunctuation(str, position = "head") {
|
|
238
|
+
if (!str) return false;
|
|
239
|
+
str = str.trim();
|
|
240
|
+
if (position === "head") return isPunctuation(str[0]);
|
|
241
|
+
else return isPunctuation(str[str.length - 1]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/utils/space.ts
|
|
246
|
+
/**
|
|
197
247
|
* Gets the count and range of consecutive whitespace at the start or end of a string.
|
|
198
248
|
* @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
|
|
199
249
|
* @example `text `, `tail` -> { count: 2, start: 4, end: 6 }
|
|
@@ -224,34 +274,6 @@ function getWhiteSpace(str, position = "head") {
|
|
|
224
274
|
}
|
|
225
275
|
}
|
|
226
276
|
/**
|
|
227
|
-
* Checks whether the start or end of a string is adjacent to punctuation.
|
|
228
|
-
* @example `γ hello`, `head` -> true
|
|
229
|
-
* @example `hello .`, `tail` -> true
|
|
230
|
-
*/
|
|
231
|
-
function hasPunctuation(str, position = "head") {
|
|
232
|
-
if (!str) return false;
|
|
233
|
-
str = str.trim();
|
|
234
|
-
if (position === "head") return isPunctuation(str[0]);
|
|
235
|
-
else return isPunctuation(str[str.length - 1]);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Gets the character adjacent to the start or end of a string.
|
|
239
|
-
*/
|
|
240
|
-
function getAdjacentChar(str, position) {
|
|
241
|
-
if (!str) return void 0;
|
|
242
|
-
str = str.trim();
|
|
243
|
-
return position === "head" ? str[0] : str[str.length - 1];
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Extracts the plain-text value of a phrasing node.
|
|
247
|
-
* If the node does not expose `value`, recursively concatenates the text from its children.
|
|
248
|
-
*/
|
|
249
|
-
function getNodeValue(node) {
|
|
250
|
-
if (!node) return;
|
|
251
|
-
if ("value" in node) return node.value;
|
|
252
|
-
if (hasChildren(node)) return node.children.map(getNodeValue).join("") || void 0;
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
277
|
* Gets whitespace and punctuation information for text adjacent to a link or inline code node.
|
|
256
278
|
*/
|
|
257
279
|
function getSpaceContext(nodeContext) {
|
|
@@ -273,6 +295,32 @@ function getSpaceContext(nodeContext) {
|
|
|
273
295
|
}
|
|
274
296
|
};
|
|
275
297
|
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/utils/inline-element.ts
|
|
301
|
+
const INLINE_SPACE_MESSAGE_IDS = {
|
|
302
|
+
missingSpaceBefore: "missingSpaceBefore",
|
|
303
|
+
missingSpaceAfter: "missingSpaceAfter",
|
|
304
|
+
multipleSpacesBefore: "multipleSpacesBefore",
|
|
305
|
+
multipleSpacesAfter: "multipleSpacesAfter",
|
|
306
|
+
multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
|
|
307
|
+
unexpectedSpaceBefore: "unexpectedSpaceBefore",
|
|
308
|
+
unexpectedSpaceAfter: "unexpectedSpaceAfter"
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Checks whether adjacent text is a custom container marker on the next line.
|
|
312
|
+
*
|
|
313
|
+
* @deprecated Temporary workaround to prevent space-between-link from reporting
|
|
314
|
+
* false positives on custom containers. Remove this and handle the case in a
|
|
315
|
+
* dedicated custom container rule when one exists.
|
|
316
|
+
* @see https://vitepress.dev/guide/markdown#custom-containers
|
|
317
|
+
* @example `\n:::` -> true
|
|
318
|
+
* @example `\n::::` -> true
|
|
319
|
+
* @example `:::` -> false
|
|
320
|
+
*/
|
|
321
|
+
function isCustomContainerMarker(str) {
|
|
322
|
+
return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
|
|
323
|
+
}
|
|
276
324
|
/**
|
|
277
325
|
* Validates whether a spacing run contains exactly one required space.
|
|
278
326
|
*/
|
|
@@ -281,121 +329,168 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
|
|
|
281
329
|
if (count > 1) return multipleSpacesMessageId;
|
|
282
330
|
}
|
|
283
331
|
/**
|
|
284
|
-
* Validates
|
|
332
|
+
* Validates spacing before an inline node when the previous character is punctuation.
|
|
285
333
|
*/
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
334
|
+
function validateBeforePunctuation(context) {
|
|
335
|
+
const adjacentChar = getAdjacentChar(context.value, "tail");
|
|
336
|
+
if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
|
|
337
|
+
if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
|
|
289
338
|
return;
|
|
290
339
|
}
|
|
291
|
-
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
292
|
-
if (context.whiteSpace.count > 0) return
|
|
340
|
+
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation);
|
|
341
|
+
if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
|
|
293
342
|
}
|
|
294
343
|
/**
|
|
295
|
-
* Validates the spacing between the previous node and the current
|
|
344
|
+
* Validates the spacing between the previous node and the current inline node.
|
|
296
345
|
*/
|
|
297
|
-
function
|
|
298
|
-
if (context.hasPunctuation) return
|
|
299
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
346
|
+
function validateSpaceBeforeNode(context) {
|
|
347
|
+
if (context.hasPunctuation) return validateBeforePunctuation(context);
|
|
348
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore, INLINE_SPACE_MESSAGE_IDS.multipleSpacesBefore);
|
|
300
349
|
}
|
|
301
350
|
/**
|
|
302
|
-
* Validates
|
|
351
|
+
* Validates spacing after an inline node when the next character is punctuation.
|
|
303
352
|
*/
|
|
304
|
-
function
|
|
305
|
-
|
|
353
|
+
function validateSpaceAfterPunctuation(context) {
|
|
354
|
+
const adjacentChar = getAdjacentChar(context.value, "head");
|
|
306
355
|
if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
|
|
307
|
-
if (context.whiteSpace.count > 0) return
|
|
356
|
+
if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
|
|
357
|
+
if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceAfter, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfter);
|
|
358
|
+
if (context.whiteSpace.count > 0) return INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Validates the spacing between the current inline node and the next node.
|
|
362
|
+
*/
|
|
363
|
+
function validateSpaceAfterNode(context) {
|
|
364
|
+
if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
|
|
365
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, INLINE_SPACE_MESSAGE_IDS.missingSpaceAfter, INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfter);
|
|
308
366
|
}
|
|
309
367
|
/**
|
|
310
|
-
* Validates
|
|
368
|
+
* Validates spacing around an inline element inside a table cell.
|
|
369
|
+
* Table cells skip checks when the next sibling is another inline element.
|
|
311
370
|
*/
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
371
|
+
function validateTableCellSpace(nodeContext) {
|
|
372
|
+
const { prev, next } = getSpaceContext(nodeContext);
|
|
373
|
+
if (prev && prev.value) {
|
|
374
|
+
const beforeIssue = validateSpaceBeforeNode(prev);
|
|
375
|
+
if (beforeIssue) return beforeIssue;
|
|
376
|
+
}
|
|
377
|
+
if (!next || isInlineElement(nodeContext.next) || !next.value) return;
|
|
378
|
+
return validateSpaceAfterNode(next);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Validates spacing around an inline element in the default text flow.
|
|
382
|
+
*/
|
|
383
|
+
function validateDefaultSpace(nodeContext) {
|
|
384
|
+
const { prev, next } = getSpaceContext(nodeContext);
|
|
385
|
+
if (prev && nodeContext.prev) {
|
|
386
|
+
const beforeIssue = validateSpaceBeforeNode(prev);
|
|
387
|
+
if (beforeIssue) return beforeIssue;
|
|
388
|
+
}
|
|
389
|
+
if (!next || isInlineElement(nodeContext.next) || !nodeContext.next) return;
|
|
390
|
+
return validateSpaceAfterNode(next);
|
|
315
391
|
}
|
|
316
392
|
/**
|
|
317
|
-
* Validates
|
|
318
|
-
*
|
|
319
|
-
* -
|
|
320
|
-
* -
|
|
393
|
+
* Validates spacing around an inline element by delegating to the appropriate strategy
|
|
394
|
+
* for table cells or the default text flow.
|
|
395
|
+
* - Regular text and selected inline elements should be separated by one space.
|
|
396
|
+
* - Fullwidth punctuation and paired punctuation usually touch inline elements without spaces.
|
|
397
|
+
* - Adjacent selected inline elements are handled by the following element to avoid duplicate fixes.
|
|
321
398
|
*/
|
|
322
399
|
function validateSpace(nodeContext) {
|
|
323
|
-
const {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const beforeLinkIssue = validateSpaceBeforeLink(spaceContext.prev);
|
|
327
|
-
if (beforeLinkIssue) return beforeLinkIssue;
|
|
328
|
-
if (!next || !spaceContext.next) return;
|
|
329
|
-
return validateSpaceAfterLink(spaceContext.next);
|
|
400
|
+
const { parent } = nodeContext;
|
|
401
|
+
if (parent && isTableCell(parent)) return validateTableCellSpace(nodeContext);
|
|
402
|
+
return validateDefaultSpace(nodeContext);
|
|
330
403
|
}
|
|
331
404
|
|
|
332
405
|
//#endregion
|
|
333
|
-
//#region src/rules/space-between-
|
|
334
|
-
const RULE_NAME$1 = "space-between-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
406
|
+
//#region src/rules/space-between-inline-element/index.ts
|
|
407
|
+
const RULE_NAME$1 = "space-between-inline-element";
|
|
408
|
+
const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set([
|
|
409
|
+
INLINE_SPACE_MESSAGE_IDS.missingSpaceBefore,
|
|
410
|
+
INLINE_SPACE_MESSAGE_IDS.multipleSpacesBefore,
|
|
411
|
+
INLINE_SPACE_MESSAGE_IDS.multipleSpacesAfterPunctuation,
|
|
412
|
+
INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore
|
|
340
413
|
]);
|
|
341
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Checks one selected inline element and reports the fix range around it.
|
|
416
|
+
*/
|
|
417
|
+
function checkInlineElement(context, node) {
|
|
418
|
+
const { position, start, end } = getNodePosition(node);
|
|
419
|
+
/* v8 ignore if -- @preserve */
|
|
420
|
+
if (!position) return;
|
|
421
|
+
const nodeContext = getNodeContext(context, node);
|
|
422
|
+
if (isNestedInlineElement(nodeContext)) return;
|
|
423
|
+
const spaceContext = getSpaceContext(nodeContext);
|
|
424
|
+
const messageId = validateSpace(nodeContext);
|
|
425
|
+
if (!messageId) return;
|
|
426
|
+
if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
|
|
427
|
+
const { count } = spaceContext.prev.whiteSpace;
|
|
428
|
+
const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceBefore ? "" : " ";
|
|
429
|
+
context.report({
|
|
430
|
+
node,
|
|
431
|
+
messageId,
|
|
432
|
+
fix(fixer) {
|
|
433
|
+
return fixer.replaceTextRange([start - count, start], replaceText);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (spaceContext.next) {
|
|
439
|
+
const { count } = spaceContext.next.whiteSpace;
|
|
440
|
+
const replaceText = messageId === INLINE_SPACE_MESSAGE_IDS.unexpectedSpaceAfter ? "" : " ";
|
|
441
|
+
context.report({
|
|
442
|
+
node,
|
|
443
|
+
messageId,
|
|
444
|
+
fix(fixer) {
|
|
445
|
+
return fixer.replaceTextRange([end, end + count], replaceText);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
var space_between_inline_element_default = createRule({
|
|
342
451
|
name: RULE_NAME$1,
|
|
343
452
|
meta: {
|
|
344
453
|
type: "layout",
|
|
345
|
-
docs: { description: "Enforce spacing around Markdown
|
|
454
|
+
docs: { description: "Enforce spacing around Markdown inline elements." },
|
|
346
455
|
messages: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
456
|
+
missingSpaceBefore: "A space is required before the inline element.",
|
|
457
|
+
missingSpaceAfter: "A space is required after the inline element.",
|
|
458
|
+
multipleSpacesBefore: "Use exactly one space before the inline element.",
|
|
459
|
+
multipleSpacesAfter: "Use exactly one space after the inline element.",
|
|
351
460
|
multipleSpacesAfterPunctuation: "Use one space after punctuation.",
|
|
352
|
-
|
|
353
|
-
|
|
461
|
+
unexpectedSpaceBefore: "Do not add a space between punctuation and the inline element.",
|
|
462
|
+
unexpectedSpaceAfter: "Do not add a space between the inline element and punctuation."
|
|
354
463
|
},
|
|
355
464
|
fixable: "whitespace",
|
|
356
465
|
schema: []
|
|
357
466
|
},
|
|
358
467
|
defaultOptions: [],
|
|
359
468
|
create(context) {
|
|
360
|
-
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
if (spaceContext.next) {
|
|
380
|
-
const { count } = spaceContext.next.whiteSpace;
|
|
381
|
-
const replaceText = messageId === LINK_SPACE_MESSAGE_IDS.unexpectedSpaceAfterLink ? "" : " ";
|
|
382
|
-
context.report({
|
|
383
|
-
node,
|
|
384
|
-
messageId,
|
|
385
|
-
fix(fixer) {
|
|
386
|
-
return fixer.replaceTextRange([end, end + count], replaceText);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
469
|
+
return {
|
|
470
|
+
link(node) {
|
|
471
|
+
checkInlineElement(context, node);
|
|
472
|
+
},
|
|
473
|
+
image(node) {
|
|
474
|
+
checkInlineElement(context, node);
|
|
475
|
+
},
|
|
476
|
+
inlineCode(node) {
|
|
477
|
+
checkInlineElement(context, node);
|
|
478
|
+
},
|
|
479
|
+
emphasis(node) {
|
|
480
|
+
checkInlineElement(context, node);
|
|
481
|
+
},
|
|
482
|
+
strong(node) {
|
|
483
|
+
checkInlineElement(context, node);
|
|
389
484
|
}
|
|
390
|
-
}
|
|
485
|
+
};
|
|
391
486
|
}
|
|
392
487
|
});
|
|
393
488
|
|
|
394
489
|
//#endregion
|
|
395
490
|
//#region src/utils/markdown.ts
|
|
396
|
-
const language = new MarkdownLanguage({ mode: "
|
|
491
|
+
const language = new MarkdownLanguage({ mode: "gfm" });
|
|
397
492
|
/**
|
|
398
|
-
* Parses Markdown with the same
|
|
493
|
+
* Parses Markdown with the same GFM language implementation used by the
|
|
399
494
|
* plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
|
|
400
495
|
*/
|
|
401
496
|
function parseMarkdown(markdown$1) {
|
|
@@ -409,6 +504,7 @@ function parseMarkdown(markdown$1) {
|
|
|
409
504
|
...language.defaultLanguageOptions,
|
|
410
505
|
frontmatter: "yaml"
|
|
411
506
|
} });
|
|
507
|
+
/* v8 ignore if -- @preserve */
|
|
412
508
|
if (!parseResult.ok) throw new Error(parseResult.errors[0]?.message ?? "Failed to parse markdown.");
|
|
413
509
|
return {
|
|
414
510
|
ast: parseResult.ast,
|
|
@@ -417,7 +513,7 @@ function parseMarkdown(markdown$1) {
|
|
|
417
513
|
}
|
|
418
514
|
|
|
419
515
|
//#endregion
|
|
420
|
-
//#region src/utils/
|
|
516
|
+
//#region src/utils/heading.ts
|
|
421
517
|
/**
|
|
422
518
|
* Returns true when the Markdown document starts with YAML frontmatter.
|
|
423
519
|
*/
|
|
@@ -450,6 +546,7 @@ var valid_heading_anchor_default = createRule({
|
|
|
450
546
|
create(context) {
|
|
451
547
|
return { heading(node) {
|
|
452
548
|
const { position, start, end } = getNodePosition(node);
|
|
549
|
+
/* v8 ignore if -- @preserve */
|
|
453
550
|
if (!position) return;
|
|
454
551
|
const source = context.sourceCode.text.slice(start, end);
|
|
455
552
|
if (isStrictAnchor(source) || !hasChinese(source)) return;
|
|
@@ -481,7 +578,7 @@ var valid_heading_anchor_default = createRule({
|
|
|
481
578
|
//#endregion
|
|
482
579
|
//#region src/rules/index.ts
|
|
483
580
|
const rules = {
|
|
484
|
-
"space-between-
|
|
581
|
+
"space-between-inline-element": space_between_inline_element_default,
|
|
485
582
|
"valid-heading-anchor": valid_heading_anchor_default
|
|
486
583
|
};
|
|
487
584
|
|
|
@@ -490,10 +587,7 @@ const rules = {
|
|
|
490
587
|
const plugin = {
|
|
491
588
|
rules,
|
|
492
589
|
processors: markdown.processors,
|
|
493
|
-
languages: {
|
|
494
|
-
commonmark: new MarkdownLanguage({ mode: "commonmark" }),
|
|
495
|
-
gfm: new MarkdownLanguage({ mode: "gfm" })
|
|
496
|
-
}
|
|
590
|
+
languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
|
|
497
591
|
};
|
|
498
592
|
const allRuleEntries = Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]);
|
|
499
593
|
const recommendedRules = Object.fromEntries(allRuleEntries);
|
|
@@ -503,14 +597,14 @@ const configs = {
|
|
|
503
597
|
name: "md-style/recommended",
|
|
504
598
|
files: ["**/*.md"],
|
|
505
599
|
plugins: { "md-style": plugin },
|
|
506
|
-
language: "md-style/
|
|
600
|
+
language: "md-style/gfm",
|
|
507
601
|
rules: recommendedRules
|
|
508
602
|
},
|
|
509
603
|
all: {
|
|
510
604
|
name: "md-style/all",
|
|
511
605
|
files: ["**/*.md"],
|
|
512
606
|
plugins: { "md-style": plugin },
|
|
513
|
-
language: "md-style/
|
|
607
|
+
language: "md-style/gfm",
|
|
514
608
|
rules: allRules
|
|
515
609
|
}
|
|
516
610
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-md-style",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"packageManager": "pnpm@10.21.0",
|
|
6
6
|
"description": "ESLint plugin for enforcing style rules in Markdown-based documentation",
|
|
7
7
|
"author": "noisefan <noisefan@163.com>",
|
|
@@ -43,8 +43,14 @@
|
|
|
43
43
|
"prepare": "simple-git-hooks"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
+
"@antfu/eslint-config": "^7.5.0",
|
|
46
47
|
"@eslint/markdown": "^7.5.1",
|
|
47
|
-
"eslint": "^9.
|
|
48
|
+
"eslint": "^9.30.0 || ^10.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@antfu/eslint-config": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
48
54
|
},
|
|
49
55
|
"dependencies": {},
|
|
50
56
|
"devDependencies": {
|