eslint-plugin-md-style 0.1.0-beta.2 β 0.2.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 +19 -5
- package/dist/index.mjs +618 -153
- package/package.json +26 -19
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-
|
|
73
|
+
'md-style/space-around-inline-element': 'error',
|
|
67
74
|
'md-style/valid-heading-anchor': 'error',
|
|
68
75
|
},
|
|
69
76
|
},
|
|
@@ -113,14 +120,21 @@ export default antfu(
|
|
|
113
120
|
|
|
114
121
|
| Rule | Included in `recommended` | Autofix |
|
|
115
122
|
| --- | --- | --- |
|
|
116
|
-
| `md-style/space-
|
|
123
|
+
| `md-style/space-around-inline-element` | β
| π§ |
|
|
124
|
+
| `md-style/space-around-number` | | π§ |
|
|
125
|
+
| `md-style/space-around-word` | | π§ |
|
|
117
126
|
| `md-style/valid-heading-anchor` | β
| π§ |
|
|
118
127
|
|
|
119
128
|
## Why `@eslint/markdown` Is Required
|
|
120
129
|
|
|
121
130
|
This plugin builds on top of `@eslint/markdown` rather than replacing it.
|
|
122
131
|
|
|
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/
|
|
132
|
+
`@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.
|
|
133
|
+
|
|
134
|
+
## References
|
|
135
|
+
|
|
136
|
+
- [W3C Manual of Style](https://www.w3.org/guide/manual-of-style/)
|
|
137
|
+
- [δΈζζηθ¦ζ±](https://w3c.github.io/clreq/)
|
|
124
138
|
|
|
125
139
|
## License
|
|
126
140
|
|
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("");
|
|
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,29 @@ function getNodeContext(context, node) {
|
|
|
60
94
|
current: node
|
|
61
95
|
};
|
|
62
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns the current node and adjacent siblings from a known children array.
|
|
99
|
+
* Useful when iterating a tokenized child list directly without an ESLint ancestor context.
|
|
100
|
+
*/
|
|
101
|
+
function getNodeContextByParent(childrenNodes, currentIndex) {
|
|
102
|
+
return {
|
|
103
|
+
prev: childrenNodes[currentIndex - 1],
|
|
104
|
+
current: childrenNodes[currentIndex],
|
|
105
|
+
next: childrenNodes[currentIndex + 1]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Gets the first or last visible character of a string after trimming
|
|
110
|
+
* surrounding whitespace.
|
|
111
|
+
*/
|
|
112
|
+
function getAdjacentChar(str, position) {
|
|
113
|
+
if (!str) return void 0;
|
|
114
|
+
str = str.trim();
|
|
115
|
+
return position === "head" ? str[0] : str[str.length - 1];
|
|
116
|
+
}
|
|
63
117
|
|
|
64
118
|
//#endregion
|
|
65
|
-
//#region src/utils/
|
|
119
|
+
//#region src/utils/anchor.ts
|
|
66
120
|
/**
|
|
67
121
|
* Match the trailing anchor-like fragment from a heading string.
|
|
68
122
|
* @example `δΈζζ ι’ {#Chinese-Title}` -> `{#Chinese-Title}`
|
|
@@ -101,7 +155,7 @@ function isStrictAnchor(str) {
|
|
|
101
155
|
* Check whether the string contains CJK Han characters.
|
|
102
156
|
*/
|
|
103
157
|
function hasChinese(str) {
|
|
104
|
-
return /
|
|
158
|
+
return /\p{Script=Han}/u.test(str);
|
|
105
159
|
}
|
|
106
160
|
/**
|
|
107
161
|
* Normalize raw anchor text into the strict anchor format content.
|
|
@@ -111,7 +165,7 @@ function hasChinese(str) {
|
|
|
111
165
|
* - trim leading/trailing `-`
|
|
112
166
|
*/
|
|
113
167
|
function normalizeAnchor(anchor) {
|
|
114
|
-
return anchor.toLowerCase().replace(/[\s.]
|
|
168
|
+
return anchor.toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
|
|
115
169
|
}
|
|
116
170
|
/**
|
|
117
171
|
* Count wrapper characters contributed by the trailing like-anchor fragment.
|
|
@@ -128,16 +182,7 @@ function calcAnchorPositionCompensate(content) {
|
|
|
128
182
|
}
|
|
129
183
|
|
|
130
184
|
//#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
|
-
};
|
|
185
|
+
//#region src/utils/punctuation.ts
|
|
141
186
|
const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
142
187
|
"(",
|
|
143
188
|
"[",
|
|
@@ -149,6 +194,23 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
|
149
194
|
"β",
|
|
150
195
|
"β"
|
|
151
196
|
]);
|
|
197
|
+
const CLOSING_PAIRED_PUNCTUATION = new Set([
|
|
198
|
+
")",
|
|
199
|
+
"]",
|
|
200
|
+
"}",
|
|
201
|
+
">",
|
|
202
|
+
"οΌ",
|
|
203
|
+
"γ",
|
|
204
|
+
"γ",
|
|
205
|
+
"β",
|
|
206
|
+
"β"
|
|
207
|
+
]);
|
|
208
|
+
/**
|
|
209
|
+
* Checks whether the character is a slash used as a path-like separator.
|
|
210
|
+
*/
|
|
211
|
+
function isSlashPunctuation(str) {
|
|
212
|
+
return str === "/";
|
|
213
|
+
}
|
|
152
214
|
/**
|
|
153
215
|
* Checks whether the character is fullwidth punctuation.
|
|
154
216
|
* @example `γ` -> true
|
|
@@ -158,6 +220,16 @@ function isFullwidthPunctuation(str) {
|
|
|
158
220
|
if (!str || str.length !== 1) return false;
|
|
159
221
|
return /^[\u3001-\u303F\uFE10-\uFE1F\uFE30-\uFE4F\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65βββββ¦]$/u.test(str);
|
|
160
222
|
}
|
|
223
|
+
const HALFWIDTH_PUNCTUATION_RE = /^\p{P}$/u;
|
|
224
|
+
/**
|
|
225
|
+
* Checks whether the character is halfwidth punctuation.
|
|
226
|
+
* @example `,` -> true
|
|
227
|
+
* @example `οΌ` -> false
|
|
228
|
+
*/
|
|
229
|
+
function isHalfwidthPunctuation(str) {
|
|
230
|
+
if (!str || str.length !== 1) return false;
|
|
231
|
+
return str.charCodeAt(0) <= 126 && HALFWIDTH_PUNCTUATION_RE.test(str);
|
|
232
|
+
}
|
|
161
233
|
const DASH_PUNCTUATION_RE = /^[-\u2013\u2014\u2212]$/u;
|
|
162
234
|
/**
|
|
163
235
|
* Checks whether the character is hyphen-like punctuation.
|
|
@@ -168,20 +240,6 @@ function isDashPunctuation(str) {
|
|
|
168
240
|
if (!str || str.length !== 1) return false;
|
|
169
241
|
return DASH_PUNCTUATION_RE.test(str);
|
|
170
242
|
}
|
|
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
243
|
const PUNCTUATION_RE = /^\p{P}$/u;
|
|
186
244
|
/**
|
|
187
245
|
* Checks whether the character is punctuation.
|
|
@@ -194,6 +252,28 @@ function isPunctuation(str) {
|
|
|
194
252
|
return PUNCTUATION_RE.test(str);
|
|
195
253
|
}
|
|
196
254
|
/**
|
|
255
|
+
* Checks whether the start or end of a string is adjacent to punctuation.
|
|
256
|
+
* @example `γ hello`, `head` -> true
|
|
257
|
+
* @example `hello .`, `tail` -> true
|
|
258
|
+
*/
|
|
259
|
+
function hasPunctuation(str, position = "head") {
|
|
260
|
+
if (!str) return false;
|
|
261
|
+
str = str.trim();
|
|
262
|
+
if (position === "head") return isPunctuation(str[0]);
|
|
263
|
+
else return isPunctuation(str[str.length - 1]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/utils/space.ts
|
|
268
|
+
const SPACE_MESSAGE_IDS = {
|
|
269
|
+
missingSpaceBefore: "missingSpaceBefore",
|
|
270
|
+
missingSpaceAfter: "missingSpaceAfter",
|
|
271
|
+
missingSpacesAround: "missingSpacesAround",
|
|
272
|
+
unexpectedSpaceBefore: "unexpectedSpaceBefore",
|
|
273
|
+
unexpectedSpaceAfter: "unexpectedSpaceAfter",
|
|
274
|
+
unexpectedSpaceAround: "unexpectedSpaceAround"
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
197
277
|
* Gets the count and range of consecutive whitespace at the start or end of a string.
|
|
198
278
|
* @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
|
|
199
279
|
* @example `text `, `tail` -> { count: 2, start: 4, end: 6 }
|
|
@@ -224,34 +304,6 @@ function getWhiteSpace(str, position = "head") {
|
|
|
224
304
|
}
|
|
225
305
|
}
|
|
226
306
|
/**
|
|
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
307
|
* Gets whitespace and punctuation information for text adjacent to a link or inline code node.
|
|
256
308
|
*/
|
|
257
309
|
function getSpaceContext(nodeContext) {
|
|
@@ -273,6 +325,23 @@ function getSpaceContext(nodeContext) {
|
|
|
273
325
|
}
|
|
274
326
|
};
|
|
275
327
|
}
|
|
328
|
+
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/utils/inline-element.ts
|
|
331
|
+
/**
|
|
332
|
+
* Checks whether adjacent text is a custom container marker on the next line.
|
|
333
|
+
*
|
|
334
|
+
* @deprecated Temporary workaround to prevent space-between-link from reporting
|
|
335
|
+
* false positives on custom containers. Remove this and handle the case in a
|
|
336
|
+
* dedicated custom container rule when one exists.
|
|
337
|
+
* @see https://vitepress.dev/guide/markdown#custom-containers
|
|
338
|
+
* @example `\n:::` -> true
|
|
339
|
+
* @example `\n::::` -> true
|
|
340
|
+
* @example `:::` -> false
|
|
341
|
+
*/
|
|
342
|
+
function isCustomContainerMarker(str) {
|
|
343
|
+
return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
|
|
344
|
+
}
|
|
276
345
|
/**
|
|
277
346
|
* Validates whether a spacing run contains exactly one required space.
|
|
278
347
|
*/
|
|
@@ -281,121 +350,512 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
|
|
|
281
350
|
if (count > 1) return multipleSpacesMessageId;
|
|
282
351
|
}
|
|
283
352
|
/**
|
|
284
|
-
* Validates
|
|
353
|
+
* Validates spacing before an inline node when the previous character is punctuation.
|
|
285
354
|
*/
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
355
|
+
function validateBeforePunctuation(context) {
|
|
356
|
+
const adjacentChar = getAdjacentChar(context.value, "tail");
|
|
357
|
+
if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
|
|
358
|
+
if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
|
|
289
359
|
return;
|
|
290
360
|
}
|
|
291
|
-
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
292
|
-
if (context.whiteSpace.count > 0) return
|
|
361
|
+
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesAfterPunctuation);
|
|
362
|
+
if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
|
|
293
363
|
}
|
|
294
364
|
/**
|
|
295
|
-
* Validates the spacing between the previous node and the current
|
|
365
|
+
* Validates the spacing between the previous node and the current inline node.
|
|
296
366
|
*/
|
|
297
|
-
function
|
|
298
|
-
if (context.hasPunctuation) return
|
|
299
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
367
|
+
function validateSpaceBeforeNode(context) {
|
|
368
|
+
if (context.hasPunctuation) return validateBeforePunctuation(context);
|
|
369
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesBefore);
|
|
300
370
|
}
|
|
301
371
|
/**
|
|
302
|
-
* Validates
|
|
372
|
+
* Validates spacing after an inline node when the next character is punctuation.
|
|
303
373
|
*/
|
|
304
|
-
function
|
|
305
|
-
|
|
374
|
+
function validateSpaceAfterPunctuation(context) {
|
|
375
|
+
const adjacentChar = getAdjacentChar(context.value, "head");
|
|
306
376
|
if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
|
|
307
|
-
if (context.whiteSpace.count > 0) return
|
|
377
|
+
if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceAfter;
|
|
378
|
+
if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
|
|
379
|
+
if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceAfter;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Validates the spacing between the current inline node and the next node.
|
|
383
|
+
*/
|
|
384
|
+
function validateSpaceAfterNode(context) {
|
|
385
|
+
if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
|
|
386
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
|
|
308
387
|
}
|
|
309
388
|
/**
|
|
310
|
-
* Validates
|
|
389
|
+
* Validates spacing around an inline element inside a table cell.
|
|
390
|
+
* Table cells skip checks when the next sibling is another inline element.
|
|
311
391
|
*/
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
392
|
+
function validateTableCellSpace(nodeContext) {
|
|
393
|
+
const { prev, next } = getSpaceContext(nodeContext);
|
|
394
|
+
if (prev && prev.value) {
|
|
395
|
+
const beforeIssue = validateSpaceBeforeNode(prev);
|
|
396
|
+
if (beforeIssue) return beforeIssue;
|
|
397
|
+
}
|
|
398
|
+
if (!next || isInlineElement(nodeContext.next) || !next.value) return;
|
|
399
|
+
return validateSpaceAfterNode(next);
|
|
315
400
|
}
|
|
316
401
|
/**
|
|
317
|
-
* Validates
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
402
|
+
* Validates spacing around an inline element in the default text flow.
|
|
403
|
+
*/
|
|
404
|
+
function validateDefaultSpace(nodeContext) {
|
|
405
|
+
const { prev, next } = getSpaceContext(nodeContext);
|
|
406
|
+
if (prev && nodeContext.prev) {
|
|
407
|
+
const beforeIssue = validateSpaceBeforeNode(prev);
|
|
408
|
+
if (beforeIssue) return beforeIssue;
|
|
409
|
+
}
|
|
410
|
+
if (!next || isInlineElement(nodeContext.next) || !nodeContext.next) return;
|
|
411
|
+
return validateSpaceAfterNode(next);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Validates spacing around an inline element by delegating to the appropriate strategy
|
|
415
|
+
* for table cells or the default text flow.
|
|
416
|
+
* - Regular text and selected inline elements should be separated by one space.
|
|
417
|
+
* - Fullwidth punctuation and paired punctuation usually touch inline elements without spaces.
|
|
418
|
+
* - Adjacent selected inline elements are handled by the following element to avoid duplicate fixes.
|
|
321
419
|
*/
|
|
322
420
|
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);
|
|
421
|
+
const { parent } = nodeContext;
|
|
422
|
+
if (parent && isTableCell(parent)) return validateTableCellSpace(nodeContext);
|
|
423
|
+
return validateDefaultSpace(nodeContext);
|
|
330
424
|
}
|
|
331
425
|
|
|
332
426
|
//#endregion
|
|
333
|
-
//#region src/rules/space-
|
|
334
|
-
const RULE_NAME$
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
427
|
+
//#region src/rules/space-around-inline-element/index.ts
|
|
428
|
+
const RULE_NAME$3 = "space-around-inline-element";
|
|
429
|
+
const MESSAGE_IDS$1 = {
|
|
430
|
+
missingSpaceBefore: "missingSpaceBefore",
|
|
431
|
+
missingSpaceAfter: "missingSpaceAfter",
|
|
432
|
+
multipleSpacesBefore: "multipleSpacesBefore",
|
|
433
|
+
multipleSpacesAfter: "multipleSpacesAfter",
|
|
434
|
+
multipleSpacesAfterPunctuation: "multipleSpacesAfterPunctuation",
|
|
435
|
+
unexpectedSpaceBefore: "unexpectedSpaceBefore",
|
|
436
|
+
unexpectedSpaceAfter: "unexpectedSpaceAfter"
|
|
437
|
+
};
|
|
438
|
+
const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set([
|
|
439
|
+
MESSAGE_IDS$1.missingSpaceBefore,
|
|
440
|
+
MESSAGE_IDS$1.multipleSpacesBefore,
|
|
441
|
+
MESSAGE_IDS$1.multipleSpacesAfterPunctuation,
|
|
442
|
+
MESSAGE_IDS$1.unexpectedSpaceBefore
|
|
340
443
|
]);
|
|
341
|
-
var
|
|
342
|
-
name: RULE_NAME$
|
|
444
|
+
var space_around_inline_element_default = createRule({
|
|
445
|
+
name: RULE_NAME$3,
|
|
343
446
|
meta: {
|
|
344
447
|
type: "layout",
|
|
345
|
-
docs: { description: "Enforce spacing around Markdown
|
|
448
|
+
docs: { description: "Enforce spacing around Markdown inline elements." },
|
|
346
449
|
messages: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
450
|
+
missingSpaceBefore: "A space is required before the inline element.",
|
|
451
|
+
missingSpaceAfter: "A space is required after the inline element.",
|
|
452
|
+
multipleSpacesBefore: "Use exactly one space before the inline element.",
|
|
453
|
+
multipleSpacesAfter: "Use exactly one space after the inline element.",
|
|
351
454
|
multipleSpacesAfterPunctuation: "Use one space after punctuation.",
|
|
352
|
-
|
|
353
|
-
|
|
455
|
+
unexpectedSpaceBefore: "Do not add a space between punctuation and the inline element.",
|
|
456
|
+
unexpectedSpaceAfter: "Do not add a space between the inline element and punctuation."
|
|
354
457
|
},
|
|
355
458
|
fixable: "whitespace",
|
|
356
459
|
schema: []
|
|
357
460
|
},
|
|
358
461
|
defaultOptions: [],
|
|
359
462
|
create(context) {
|
|
360
|
-
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
return;
|
|
463
|
+
return {
|
|
464
|
+
link(node) {
|
|
465
|
+
checkInlineElement(context, node);
|
|
466
|
+
},
|
|
467
|
+
image(node) {
|
|
468
|
+
checkInlineElement(context, node);
|
|
469
|
+
},
|
|
470
|
+
inlineCode(node) {
|
|
471
|
+
checkInlineElement(context, node);
|
|
472
|
+
},
|
|
473
|
+
emphasis(node) {
|
|
474
|
+
checkInlineElement(context, node);
|
|
475
|
+
},
|
|
476
|
+
strong(node) {
|
|
477
|
+
checkInlineElement(context, node);
|
|
378
478
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
/**
|
|
483
|
+
* Checks one selected inline element and reports the fix range around it.
|
|
484
|
+
*/
|
|
485
|
+
function checkInlineElement(context, node) {
|
|
486
|
+
const { position, start, end } = getNodePosition(node);
|
|
487
|
+
/* v8 ignore if -- @preserve */
|
|
488
|
+
if (!position) return;
|
|
489
|
+
const nodeContext = getNodeContext(context, node);
|
|
490
|
+
if (isNestedInlineElement(nodeContext)) return;
|
|
491
|
+
const spaceContext = getSpaceContext(nodeContext);
|
|
492
|
+
const messageId = validateSpace(nodeContext);
|
|
493
|
+
if (!messageId) return;
|
|
494
|
+
if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
|
|
495
|
+
const { count } = spaceContext.prev.whiteSpace;
|
|
496
|
+
const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceBefore ? "" : " ";
|
|
497
|
+
context.report({
|
|
498
|
+
node,
|
|
499
|
+
messageId,
|
|
500
|
+
fix(fixer) {
|
|
501
|
+
return fixer.replaceTextRange([start - count, start], replaceText);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (spaceContext.next) {
|
|
507
|
+
const { count } = spaceContext.next.whiteSpace;
|
|
508
|
+
const replaceText = messageId === MESSAGE_IDS$1.unexpectedSpaceAfter ? "" : " ";
|
|
509
|
+
context.report({
|
|
510
|
+
node,
|
|
511
|
+
messageId,
|
|
512
|
+
fix(fixer) {
|
|
513
|
+
return fixer.replaceTextRange([end, end + count], replaceText);
|
|
389
514
|
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/utils/text/tokenizer.ts
|
|
521
|
+
const TEXT_TYPE = {
|
|
522
|
+
"cjk": "cjk",
|
|
523
|
+
"latin": "latin",
|
|
524
|
+
"number": "number",
|
|
525
|
+
"space": "space",
|
|
526
|
+
"newline": "newline",
|
|
527
|
+
"fullwidth-punctuation": "fullwidth-punctuation",
|
|
528
|
+
"halfwidth-punctuation": "halfwidth-punctuation",
|
|
529
|
+
"dash": "dash",
|
|
530
|
+
"symbol": "symbol",
|
|
531
|
+
"emoji": "emoji",
|
|
532
|
+
"invisible": "invisible",
|
|
533
|
+
"other": "other"
|
|
534
|
+
};
|
|
535
|
+
const CJK_RE = /^\p{Script=Han}$|^\p{Script=Hiragana}$|^\p{Script=Katakana}$|^\p{Script=Hangul}$/u;
|
|
536
|
+
const LATIN_RE = /^\p{Script=Latin}$/u;
|
|
537
|
+
const NUMBER_RE = /^\p{Number}$/u;
|
|
538
|
+
const SYMBOL_RE = /^\p{Symbol}$/u;
|
|
539
|
+
const EMOJI_RE = /^\p{Extended_Pictographic}$/u;
|
|
540
|
+
const SPACE_RE = /^[\t\v\f \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]$/u;
|
|
541
|
+
const NEWLINE_RE = /^[\n\r\u2028\u2029]$/u;
|
|
542
|
+
const INVISIBLE_CODE_POINTS = new Set([
|
|
543
|
+
173,
|
|
544
|
+
847,
|
|
545
|
+
1564,
|
|
546
|
+
4447,
|
|
547
|
+
4448,
|
|
548
|
+
6068,
|
|
549
|
+
6069,
|
|
550
|
+
6158,
|
|
551
|
+
65279,
|
|
552
|
+
65440
|
|
553
|
+
]);
|
|
554
|
+
/**
|
|
555
|
+
* Checks whether the character is an invisible Unicode control or filler.
|
|
556
|
+
* These characters affect rendering, cursor movement, or text direction but
|
|
557
|
+
* should not be treated as visible spacing, punctuation, or symbols.
|
|
558
|
+
*/
|
|
559
|
+
function isInvisible(char) {
|
|
560
|
+
const codePoint = char.codePointAt(0);
|
|
561
|
+
return codePoint != null && (INVISIBLE_CODE_POINTS.has(codePoint) || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303);
|
|
562
|
+
}
|
|
563
|
+
function isLatinWordType(type) {
|
|
564
|
+
return type === TEXT_TYPE.latin;
|
|
565
|
+
}
|
|
566
|
+
function isNumberType(type) {
|
|
567
|
+
return type === TEXT_TYPE.number;
|
|
568
|
+
}
|
|
569
|
+
function isNumber(char, prev) {
|
|
570
|
+
return NUMBER_RE.test(char) || prev?.type === TEXT_TYPE.number && (char === "." || char === "%");
|
|
571
|
+
}
|
|
572
|
+
const TEXT_TYPE_MATCHERS = [
|
|
573
|
+
{
|
|
574
|
+
type: TEXT_TYPE.newline,
|
|
575
|
+
test: (char) => NEWLINE_RE.test(char)
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
type: TEXT_TYPE.space,
|
|
579
|
+
test: (char) => SPACE_RE.test(char)
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
type: TEXT_TYPE.invisible,
|
|
583
|
+
test: (char) => isInvisible(char)
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
type: TEXT_TYPE.cjk,
|
|
587
|
+
test: (char) => CJK_RE.test(char)
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
type: TEXT_TYPE.latin,
|
|
591
|
+
test: (char) => LATIN_RE.test(char)
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
type: TEXT_TYPE.number,
|
|
595
|
+
test: isNumber
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
type: TEXT_TYPE.dash,
|
|
599
|
+
test: (char) => isDashPunctuation(char)
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
type: TEXT_TYPE["fullwidth-punctuation"],
|
|
603
|
+
test: (char) => isFullwidthPunctuation(char)
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
type: TEXT_TYPE["halfwidth-punctuation"],
|
|
607
|
+
test: (char) => isHalfwidthPunctuation(char)
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
type: TEXT_TYPE.emoji,
|
|
611
|
+
test: (char) => EMOJI_RE.test(char)
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
type: TEXT_TYPE.symbol,
|
|
615
|
+
test: (char) => SYMBOL_RE.test(char)
|
|
616
|
+
}
|
|
617
|
+
];
|
|
618
|
+
const DEFAULT_START_POINT = {
|
|
619
|
+
line: 1,
|
|
620
|
+
column: 1,
|
|
621
|
+
offset: 0
|
|
622
|
+
};
|
|
623
|
+
function advancePoint(point, char) {
|
|
624
|
+
if (NEWLINE_RE.test(char)) return {
|
|
625
|
+
line: point.line + 1,
|
|
626
|
+
column: 1,
|
|
627
|
+
offset: point.offset + char.length
|
|
628
|
+
};
|
|
629
|
+
return {
|
|
630
|
+
line: point.line,
|
|
631
|
+
column: point.column + char.length,
|
|
632
|
+
offset: point.offset + char.length
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Classifies a single Unicode code point for Markdown text style rules.
|
|
637
|
+
*/
|
|
638
|
+
function getTextType(char, prev) {
|
|
639
|
+
for (const matcher of TEXT_TYPE_MATCHERS) if (matcher.test(char, prev)) return matcher.type;
|
|
640
|
+
return TEXT_TYPE.other;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Tokenizes text into consecutive typed runs while preserving source positions.
|
|
644
|
+
*/
|
|
645
|
+
function tokenizeText(value, start = DEFAULT_START_POINT) {
|
|
646
|
+
const tokens = [];
|
|
647
|
+
let point = start;
|
|
648
|
+
let prevToken;
|
|
649
|
+
for (const char of value) {
|
|
650
|
+
const type = getTextType(char, prevToken);
|
|
651
|
+
const tokenStart = point;
|
|
652
|
+
const tokenEnd = advancePoint(point, char);
|
|
653
|
+
if (prevToken?.type === type) {
|
|
654
|
+
prevToken.value += char;
|
|
655
|
+
prevToken.position.end = tokenEnd;
|
|
656
|
+
} else {
|
|
657
|
+
prevToken = {
|
|
658
|
+
type,
|
|
659
|
+
value: char,
|
|
660
|
+
position: {
|
|
661
|
+
start: tokenStart,
|
|
662
|
+
end: tokenEnd
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
tokens.push(prevToken);
|
|
666
|
+
}
|
|
667
|
+
point = tokenEnd;
|
|
668
|
+
}
|
|
669
|
+
return tokens;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Converts an mdast text node into a tokenized text AST with normalized source positions.
|
|
673
|
+
*/
|
|
674
|
+
function buildTextNodeAst(node) {
|
|
675
|
+
const start = node.position?.start;
|
|
676
|
+
const end = node.position?.end;
|
|
677
|
+
const position = {
|
|
678
|
+
start: {
|
|
679
|
+
line: start?.line ?? DEFAULT_START_POINT.line,
|
|
680
|
+
column: start?.column ?? DEFAULT_START_POINT.column,
|
|
681
|
+
offset: start?.offset ?? DEFAULT_START_POINT.offset
|
|
682
|
+
},
|
|
683
|
+
end: {
|
|
684
|
+
line: end?.line ?? DEFAULT_START_POINT.line,
|
|
685
|
+
column: end?.column ?? DEFAULT_START_POINT.column + node.value.length,
|
|
686
|
+
offset: end?.offset ?? DEFAULT_START_POINT.offset + node.value.length
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
return {
|
|
690
|
+
type: "text",
|
|
691
|
+
value: node.value,
|
|
692
|
+
position,
|
|
693
|
+
children: tokenizeText(node.value, position.start),
|
|
694
|
+
node
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/utils/text/boundary-space.ts
|
|
700
|
+
/**
|
|
701
|
+
* Normalizes an existing space token around the target token type.
|
|
702
|
+
* When multiple spaces are collapsed, the redundant side is recorded so the
|
|
703
|
+
* caller can choose the most specific lint message.
|
|
704
|
+
*/
|
|
705
|
+
function processSpaceToken(ctx, result, isTargetType) {
|
|
706
|
+
const { prev, current, next } = ctx;
|
|
707
|
+
/* v8 ignore if -- @preserve */
|
|
708
|
+
if (!current) return;
|
|
709
|
+
const cjkToTarget = prev?.type === TEXT_TYPE.cjk && isTargetType(next?.type);
|
|
710
|
+
const targetToCjk = isTargetType(prev?.type) && next?.type === TEXT_TYPE.cjk;
|
|
711
|
+
const targets = isTargetType(prev?.type) && isTargetType(next?.type);
|
|
712
|
+
const hasUnexpectedSpaces = current.value.length !== 1;
|
|
713
|
+
if (hasUnexpectedSpaces) {
|
|
714
|
+
if (cjkToTarget || targets) result.unexpectedBefore = true;
|
|
715
|
+
if (targetToCjk || targets) result.unexpectedAfter = true;
|
|
716
|
+
}
|
|
717
|
+
if (cjkToTarget || targetToCjk || hasUnexpectedSpaces) result.fixed += " ";
|
|
718
|
+
else result.fixed += current.value;
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Inserts missing spaces before or after the target token when it directly
|
|
722
|
+
* touches adjacent CJK text.
|
|
723
|
+
*/
|
|
724
|
+
function processTargetToken(ctx, result) {
|
|
725
|
+
const { prev, current, next } = ctx;
|
|
726
|
+
/* v8 ignore if -- @preserve */
|
|
727
|
+
if (!current) return;
|
|
728
|
+
if (prev?.type === TEXT_TYPE.cjk) {
|
|
729
|
+
result.fixed += " ";
|
|
730
|
+
result.missingBefore = true;
|
|
731
|
+
}
|
|
732
|
+
result.fixed += current.value;
|
|
733
|
+
if (next?.type === TEXT_TYPE.cjk) {
|
|
734
|
+
result.fixed += " ";
|
|
735
|
+
result.missingAfter = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Chooses the most specific shared boundary-space message id from the
|
|
740
|
+
* collected missing or redundant space flags.
|
|
741
|
+
*/
|
|
742
|
+
function getBoundarySpaceMessageId(boundary) {
|
|
743
|
+
if (boundary.missingBefore && boundary.missingAfter) return SPACE_MESSAGE_IDS.missingSpacesAround;
|
|
744
|
+
if (boundary.unexpectedBefore && boundary.unexpectedAfter) return SPACE_MESSAGE_IDS.unexpectedSpaceAround;
|
|
745
|
+
if (boundary.missingBefore) return SPACE_MESSAGE_IDS.missingSpaceBefore;
|
|
746
|
+
if (boundary.missingAfter) return SPACE_MESSAGE_IDS.missingSpaceAfter;
|
|
747
|
+
if (boundary.unexpectedBefore) return SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
|
|
748
|
+
return SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Rebuilds a text node with normalized spacing between CJK text and a target
|
|
752
|
+
* token class such as Latin words or numbers.
|
|
753
|
+
*/
|
|
754
|
+
function fixBoundarySpace(node, isTargetType) {
|
|
755
|
+
const { children } = buildTextNodeAst(node);
|
|
756
|
+
const result = {
|
|
757
|
+
fixed: "",
|
|
758
|
+
missingBefore: false,
|
|
759
|
+
missingAfter: false,
|
|
760
|
+
unexpectedBefore: false,
|
|
761
|
+
unexpectedAfter: false
|
|
762
|
+
};
|
|
763
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
764
|
+
const ctx = getNodeContextByParent(children, i);
|
|
765
|
+
/* v8 ignore if -- @preserve */
|
|
766
|
+
if (!ctx.current) continue;
|
|
767
|
+
if (ctx.current.type === TEXT_TYPE.space) processSpaceToken(ctx, result, isTargetType);
|
|
768
|
+
else if (isTargetType(ctx.current.type)) processTargetToken(ctx, result);
|
|
769
|
+
else result.fixed += ctx.current.value;
|
|
770
|
+
}
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/rules/space-around-number/index.ts
|
|
776
|
+
const RULE_NAME$2 = "space-around-number";
|
|
777
|
+
var space_around_number_default = createRule({
|
|
778
|
+
name: RULE_NAME$2,
|
|
779
|
+
meta: {
|
|
780
|
+
type: "layout",
|
|
781
|
+
docs: { description: "Enforce a single space between CJK characters and numbers." },
|
|
782
|
+
messages: {
|
|
783
|
+
missingSpaceBefore: "Add a space before the number.",
|
|
784
|
+
missingSpaceAfter: "Add a space after the number.",
|
|
785
|
+
missingSpacesAround: "Add spaces before and after the number.",
|
|
786
|
+
unexpectedSpaceBefore: "Remove the unexpected space before the number.",
|
|
787
|
+
unexpectedSpaceAfter: "Remove the unexpected space after the number.",
|
|
788
|
+
unexpectedSpaceAround: "Remove the unexpected spaces around the number."
|
|
789
|
+
},
|
|
790
|
+
fixable: "whitespace",
|
|
791
|
+
schema: []
|
|
792
|
+
},
|
|
793
|
+
defaultOptions: [],
|
|
794
|
+
create(context) {
|
|
795
|
+
return { text(node) {
|
|
796
|
+
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, isNumberType);
|
|
797
|
+
if (fixed === node.value) return;
|
|
798
|
+
context.report({
|
|
799
|
+
node,
|
|
800
|
+
messageId: getBoundarySpaceMessageId({
|
|
801
|
+
missingBefore,
|
|
802
|
+
missingAfter,
|
|
803
|
+
unexpectedBefore,
|
|
804
|
+
unexpectedAfter
|
|
805
|
+
}),
|
|
806
|
+
fix(fixer) {
|
|
807
|
+
return fixer.replaceText(node, fixed);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
} };
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/rules/space-around-word/index.ts
|
|
816
|
+
const RULE_NAME$1 = "space-around-word";
|
|
817
|
+
var space_around_word_default = createRule({
|
|
818
|
+
name: RULE_NAME$1,
|
|
819
|
+
meta: {
|
|
820
|
+
type: "layout",
|
|
821
|
+
docs: { description: "Enforce a single space between CJK characters and Latin words." },
|
|
822
|
+
messages: {
|
|
823
|
+
missingSpaceBefore: "Add a space before the word.",
|
|
824
|
+
missingSpaceAfter: "Add a space after the word.",
|
|
825
|
+
missingSpacesAround: "Add spaces before and after the word.",
|
|
826
|
+
unexpectedSpaceBefore: "Remove the unexpected space before the word.",
|
|
827
|
+
unexpectedSpaceAfter: "Remove the unexpected space after the word.",
|
|
828
|
+
unexpectedSpaceAround: "Remove the unexpected spaces around the word."
|
|
829
|
+
},
|
|
830
|
+
fixable: "whitespace",
|
|
831
|
+
schema: []
|
|
832
|
+
},
|
|
833
|
+
defaultOptions: [],
|
|
834
|
+
create(context) {
|
|
835
|
+
return { text(node) {
|
|
836
|
+
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, isLatinWordType);
|
|
837
|
+
if (fixed === node.value) return;
|
|
838
|
+
context.report({
|
|
839
|
+
node,
|
|
840
|
+
messageId: getBoundarySpaceMessageId({
|
|
841
|
+
missingBefore,
|
|
842
|
+
missingAfter,
|
|
843
|
+
unexpectedBefore,
|
|
844
|
+
unexpectedAfter
|
|
845
|
+
}),
|
|
846
|
+
fix(fixer) {
|
|
847
|
+
return fixer.replaceText(node, fixed);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
390
850
|
} };
|
|
391
851
|
}
|
|
392
852
|
});
|
|
393
853
|
|
|
394
854
|
//#endregion
|
|
395
855
|
//#region src/utils/markdown.ts
|
|
396
|
-
const language = new MarkdownLanguage({ mode: "
|
|
856
|
+
const language = new MarkdownLanguage({ mode: "gfm" });
|
|
397
857
|
/**
|
|
398
|
-
* Parses Markdown with the same
|
|
858
|
+
* Parses Markdown with the same GFM language implementation used by the
|
|
399
859
|
* plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
|
|
400
860
|
*/
|
|
401
861
|
function parseMarkdown(markdown$1) {
|
|
@@ -409,6 +869,7 @@ function parseMarkdown(markdown$1) {
|
|
|
409
869
|
...language.defaultLanguageOptions,
|
|
410
870
|
frontmatter: "yaml"
|
|
411
871
|
} });
|
|
872
|
+
/* v8 ignore if -- @preserve */
|
|
412
873
|
if (!parseResult.ok) throw new Error(parseResult.errors[0]?.message ?? "Failed to parse markdown.");
|
|
413
874
|
return {
|
|
414
875
|
ast: parseResult.ast,
|
|
@@ -417,7 +878,7 @@ function parseMarkdown(markdown$1) {
|
|
|
417
878
|
}
|
|
418
879
|
|
|
419
880
|
//#endregion
|
|
420
|
-
//#region src/utils/
|
|
881
|
+
//#region src/utils/heading.ts
|
|
421
882
|
/**
|
|
422
883
|
* Returns true when the Markdown document starts with YAML frontmatter.
|
|
423
884
|
*/
|
|
@@ -450,6 +911,7 @@ var valid_heading_anchor_default = createRule({
|
|
|
450
911
|
create(context) {
|
|
451
912
|
return { heading(node) {
|
|
452
913
|
const { position, start, end } = getNodePosition(node);
|
|
914
|
+
/* v8 ignore if -- @preserve */
|
|
453
915
|
if (!position) return;
|
|
454
916
|
const source = context.sourceCode.text.slice(start, end);
|
|
455
917
|
if (isStrictAnchor(source) || !hasChinese(source)) return;
|
|
@@ -481,7 +943,9 @@ var valid_heading_anchor_default = createRule({
|
|
|
481
943
|
//#endregion
|
|
482
944
|
//#region src/rules/index.ts
|
|
483
945
|
const rules = {
|
|
484
|
-
"space-
|
|
946
|
+
"space-around-inline-element": space_around_inline_element_default,
|
|
947
|
+
"space-around-number": space_around_number_default,
|
|
948
|
+
"space-around-word": space_around_word_default,
|
|
485
949
|
"valid-heading-anchor": valid_heading_anchor_default
|
|
486
950
|
};
|
|
487
951
|
|
|
@@ -490,27 +954,28 @@ const rules = {
|
|
|
490
954
|
const plugin = {
|
|
491
955
|
rules,
|
|
492
956
|
processors: markdown.processors,
|
|
493
|
-
languages: {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
957
|
+
languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
|
|
958
|
+
};
|
|
959
|
+
const recommendedRules = {
|
|
960
|
+
"md-style/valid-heading-anchor": "error",
|
|
961
|
+
"md-style/space-around-inline-element": "error"
|
|
497
962
|
};
|
|
498
|
-
const
|
|
499
|
-
const recommendedRules = Object.fromEntries(allRuleEntries);
|
|
500
|
-
const allRules = Object.fromEntries(allRuleEntries);
|
|
963
|
+
const allRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]));
|
|
501
964
|
const configs = {
|
|
502
965
|
recommended: {
|
|
503
966
|
name: "md-style/recommended",
|
|
504
967
|
files: ["**/*.md"],
|
|
505
968
|
plugins: { "md-style": plugin },
|
|
506
|
-
language: "md-style/
|
|
969
|
+
language: "md-style/gfm",
|
|
970
|
+
languageOptions: { frontmatter: "yaml" },
|
|
507
971
|
rules: recommendedRules
|
|
508
972
|
},
|
|
509
973
|
all: {
|
|
510
974
|
name: "md-style/all",
|
|
511
975
|
files: ["**/*.md"],
|
|
512
976
|
plugins: { "md-style": plugin },
|
|
513
|
-
language: "md-style/
|
|
977
|
+
language: "md-style/gfm",
|
|
978
|
+
languageOptions: { frontmatter: "yaml" },
|
|
514
979
|
rules: allRules
|
|
515
980
|
}
|
|
516
981
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-md-style",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.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>",
|
|
@@ -18,8 +18,6 @@
|
|
|
18
18
|
".": "./dist/index.mjs",
|
|
19
19
|
"./package.json": "./package.json"
|
|
20
20
|
},
|
|
21
|
-
"main": "./dist/index.mjs",
|
|
22
|
-
"module": "./dist/index.mjs",
|
|
23
21
|
"types": "./dist/index.d.mts",
|
|
24
22
|
"files": [
|
|
25
23
|
"dist"
|
|
@@ -43,27 +41,34 @@
|
|
|
43
41
|
"prepare": "simple-git-hooks"
|
|
44
42
|
},
|
|
45
43
|
"peerDependencies": {
|
|
44
|
+
"@antfu/eslint-config": "^7.5.0",
|
|
46
45
|
"@eslint/markdown": "^7.5.1",
|
|
47
|
-
"eslint": "^9.
|
|
46
|
+
"eslint": "^9.30.0 || ^10.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@antfu/eslint-config": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
48
52
|
},
|
|
49
|
-
"dependencies": {},
|
|
50
53
|
"devDependencies": {
|
|
51
|
-
"@antfu/eslint-config": "^
|
|
52
|
-
"@eslint/markdown": "^
|
|
54
|
+
"@antfu/eslint-config": "^9.0.0",
|
|
55
|
+
"@eslint/markdown": "^8.0.1",
|
|
53
56
|
"@types/mdast": "^4.0.4",
|
|
54
|
-
"@types/node": "^
|
|
55
|
-
"@typescript-eslint/utils": "^8.
|
|
56
|
-
"@vitest/coverage-v8": "^4.1.
|
|
57
|
-
"bumpp": "^
|
|
58
|
-
"eslint": "
|
|
59
|
-
"eslint-
|
|
60
|
-
"eslint-
|
|
61
|
-
"
|
|
57
|
+
"@types/node": "^25.7.0",
|
|
58
|
+
"@typescript-eslint/utils": "^8.59.3",
|
|
59
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
60
|
+
"bumpp": "^11.1.0",
|
|
61
|
+
"eslint": "10.3.0",
|
|
62
|
+
"eslint-factory": "^0.1.2",
|
|
63
|
+
"eslint-plugin-format": "^2.0.1",
|
|
64
|
+
"eslint-plugin-md-style": "^0.1.0",
|
|
65
|
+
"eslint-vitest-rule-tester": "^3.1.0",
|
|
66
|
+
"lint-staged": "^17.0.4",
|
|
62
67
|
"simple-git-hooks": "^2.13.1",
|
|
63
68
|
"tinyglobby": "^0.2.16",
|
|
64
|
-
"tsdown": "^0.
|
|
65
|
-
"typescript": "^
|
|
66
|
-
"vitest": "^4.
|
|
69
|
+
"tsdown": "^0.22.0",
|
|
70
|
+
"typescript": "^6.0.3",
|
|
71
|
+
"vitest": "^4.1.6"
|
|
67
72
|
},
|
|
68
73
|
"simple-git-hooks": {
|
|
69
74
|
"pre-commit": "pnpx lint-staged"
|
|
@@ -72,5 +77,7 @@
|
|
|
72
77
|
"*.{js,ts,md,yml,yaml,json}": [
|
|
73
78
|
"eslint --cache --fix"
|
|
74
79
|
]
|
|
75
|
-
}
|
|
80
|
+
},
|
|
81
|
+
"main": "./dist/index.mjs",
|
|
82
|
+
"module": "./dist/index.mjs"
|
|
76
83
|
}
|