eslint-plugin-md-style 0.2.0 → 0.3.1
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 +3 -0
- package/dist/index.d.mts +45 -3
- package/dist/index.mjs +856 -255
- package/package.json +21 -37
- package/LICENSE +0 -21
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import markdown, { MarkdownLanguage } from "@eslint/markdown";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
return {
|
|
6
|
-
create,
|
|
7
|
-
meta: {
|
|
8
|
-
defaultOptions,
|
|
9
|
-
...meta
|
|
10
|
-
}
|
|
11
|
-
};
|
|
2
|
+
//#region parser/ast.ts
|
|
3
|
+
function isObject(val) {
|
|
4
|
+
return !!val && typeof val === "object";
|
|
12
5
|
}
|
|
13
|
-
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region src/utils/ast.ts
|
|
16
6
|
/**
|
|
17
7
|
* Checks whether an unknown value behaves like an mdast parent node.
|
|
18
8
|
*
|
|
@@ -20,30 +10,13 @@ function createRule({ create, defaultOptions, meta }) {
|
|
|
20
10
|
* not expose mdast-specific types.
|
|
21
11
|
*/
|
|
22
12
|
function hasChildren(node) {
|
|
23
|
-
return
|
|
13
|
+
return isObject(node) && "children" in node && Array.isArray(node.children);
|
|
24
14
|
}
|
|
25
|
-
function
|
|
26
|
-
return node
|
|
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);
|
|
15
|
+
function isCodeNode(node) {
|
|
16
|
+
return node?.type === "code";
|
|
40
17
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*/
|
|
44
|
-
function isNestedInlineElement(nodeContext) {
|
|
45
|
-
const { parent } = nodeContext;
|
|
46
|
-
return isInlineElement(parent);
|
|
18
|
+
function isTableCell(node) {
|
|
19
|
+
return node?.type === "tableCell";
|
|
47
20
|
}
|
|
48
21
|
/**
|
|
49
22
|
* Extracts the plain-text value of a phrasing node.
|
|
@@ -80,7 +53,6 @@ function getNodeContext(context, node) {
|
|
|
80
53
|
current: node
|
|
81
54
|
};
|
|
82
55
|
const currentIndex = parent.children.findIndex((child) => child === node);
|
|
83
|
-
/* v8 ignore if -- @preserve */
|
|
84
56
|
if (currentIndex === -1) return {
|
|
85
57
|
parent,
|
|
86
58
|
prev: void 0,
|
|
@@ -114,11 +86,594 @@ function getAdjacentChar(str, position) {
|
|
|
114
86
|
str = str.trim();
|
|
115
87
|
return position === "head" ? str[0] : str[str.length - 1];
|
|
116
88
|
}
|
|
117
|
-
|
|
118
89
|
//#endregion
|
|
119
|
-
//#region
|
|
90
|
+
//#region parser/custom-container/parser.ts
|
|
91
|
+
/**
|
|
92
|
+
* Parses top-level text into container, blank-line, and text nodes.
|
|
93
|
+
*/
|
|
94
|
+
function parseCustomContainers(value) {
|
|
95
|
+
const state = {
|
|
96
|
+
splits: splitLines(value),
|
|
97
|
+
offset: 0
|
|
98
|
+
};
|
|
99
|
+
const nodes = [];
|
|
100
|
+
while (state.splits.length) {
|
|
101
|
+
const item = state.splits[0];
|
|
102
|
+
const previous = nodes.at(-1);
|
|
103
|
+
if (isOpenTag(item)) {
|
|
104
|
+
nodes.push(parseCustomContainer(state, 1, previous));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const consumed = consume(state);
|
|
108
|
+
if (isBlank(consumed.value)) {
|
|
109
|
+
const blankNode = parseBlankNode({
|
|
110
|
+
newline: consumed.value,
|
|
111
|
+
prevNode: previous
|
|
112
|
+
});
|
|
113
|
+
if (blankNode) nodes.push(blankNode);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
nodes.push({
|
|
117
|
+
type: "text",
|
|
118
|
+
value: consumed.value,
|
|
119
|
+
position: consumed.position
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return nodes;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parses one container by consuming its opening tag, nested containers, content,
|
|
126
|
+
* and a closing fence whose marker is at least as wide as the opening fence.
|
|
127
|
+
*/
|
|
128
|
+
function parseCustomContainer(state, depth, parentNode) {
|
|
129
|
+
const children = [];
|
|
130
|
+
const raw = state.splits[0];
|
|
131
|
+
const openTag = parseOpenTag(raw, parentNode);
|
|
132
|
+
consume(state);
|
|
133
|
+
children.push(openTag);
|
|
134
|
+
while (state.splits.length) {
|
|
135
|
+
const item = state.splits[0];
|
|
136
|
+
const previous = children.at(-1);
|
|
137
|
+
if (parseOpenTag(item, previous)) {
|
|
138
|
+
children.push(parseCustomContainer(state, depth + 1, previous));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const closeTag = parseCloseTag(item, previous);
|
|
142
|
+
const consumed = consume(state);
|
|
143
|
+
if (closeTag && closeTag.markerLength >= openTag.markerLength) {
|
|
144
|
+
children.push(closeTag);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (isBlank(consumed.value)) {
|
|
148
|
+
const blankNode = parseBlankNode({
|
|
149
|
+
newline: consumed.value,
|
|
150
|
+
prevNode: previous
|
|
151
|
+
});
|
|
152
|
+
if (blankNode) children.push(blankNode);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
children.push({
|
|
156
|
+
type: "text",
|
|
157
|
+
value: consumed.value,
|
|
158
|
+
position: consumed.position
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
type: "custom-container",
|
|
163
|
+
depth,
|
|
164
|
+
children,
|
|
165
|
+
position: {
|
|
166
|
+
start: openTag.position.start,
|
|
167
|
+
end: children.at(-1)?.position.end ?? openTag.position.end
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Consumes the next split and advances its source offset.
|
|
173
|
+
*/
|
|
174
|
+
function consume(state) {
|
|
175
|
+
const value = state.splits.shift();
|
|
176
|
+
const start = state.offset;
|
|
177
|
+
state.offset += value.length;
|
|
178
|
+
return {
|
|
179
|
+
value,
|
|
180
|
+
position: {
|
|
181
|
+
start,
|
|
182
|
+
end: state.offset
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Splits text and keeps line-break tokens so their exact source ranges survive parsing.
|
|
188
|
+
*/
|
|
189
|
+
function splitLines(value) {
|
|
190
|
+
return value.split(/(\r?\n)/).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Parses an opening custom-container tag and calculates its source ranges.
|
|
194
|
+
*/
|
|
195
|
+
function parseOpenTag(raw, prevNode) {
|
|
196
|
+
const match = raw.match(CUSTOM_CONTAINER_OPEN_MARKER_RE);
|
|
197
|
+
/* v8 ignore if -- @preserve */
|
|
198
|
+
if (!match) return null;
|
|
199
|
+
const [_, rawMarker, type, titleAndAttrs] = match;
|
|
200
|
+
const tag = rawMarker + type;
|
|
201
|
+
const marker = rawMarker.trim();
|
|
202
|
+
const offset = prevNode?.position.end || 0;
|
|
203
|
+
const valueStart = offset + tag.length - type.length;
|
|
204
|
+
const value = {
|
|
205
|
+
content: type,
|
|
206
|
+
start: valueStart,
|
|
207
|
+
end: valueStart + type.length
|
|
208
|
+
};
|
|
209
|
+
const titleAndAttrsValue = titleAndAttrs?.trim() || "";
|
|
210
|
+
const attributeMatch = titleAndAttrsValue.match(CUSTOM_CONTAINER_ATTRS_RE);
|
|
211
|
+
const attrs = attributeMatch ? {
|
|
212
|
+
content: attributeMatch[1].slice(1, -1),
|
|
213
|
+
raw: attributeMatch[1]
|
|
214
|
+
} : void 0;
|
|
215
|
+
const title = (attributeMatch ? titleAndAttrsValue.slice(0, attributeMatch.index).trim() : titleAndAttrsValue) || void 0;
|
|
216
|
+
return {
|
|
217
|
+
type: "open",
|
|
218
|
+
markerLength: marker.length,
|
|
219
|
+
raw,
|
|
220
|
+
value,
|
|
221
|
+
title,
|
|
222
|
+
...attrs ? { attribute: attrs } : {},
|
|
223
|
+
position: {
|
|
224
|
+
start: offset,
|
|
225
|
+
end: offset + raw.length
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Parses the exact closing marker,
|
|
231
|
+
* `:::` and calculates its source range.
|
|
232
|
+
*/
|
|
233
|
+
function parseCloseTag(raw, prevNode) {
|
|
234
|
+
const match = raw.match(CUSTOM_CONTAINER_CLOSE_MARKER_STRICT_RE);
|
|
235
|
+
/* v8 ignore if -- @preserve */
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
const marker = match[0].match(/:{3,}/)?.[0] ?? "";
|
|
238
|
+
const start = prevNode?.position.end || 0;
|
|
239
|
+
const end = start + raw.length;
|
|
240
|
+
return {
|
|
241
|
+
type: "close",
|
|
242
|
+
markerLength: marker.length,
|
|
243
|
+
raw,
|
|
244
|
+
value: {
|
|
245
|
+
content: raw,
|
|
246
|
+
start,
|
|
247
|
+
end
|
|
248
|
+
},
|
|
249
|
+
position: {
|
|
250
|
+
start,
|
|
251
|
+
end
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Adds a blank-line node
|
|
257
|
+
* merging it with the preceding blank line when possible.
|
|
258
|
+
*/
|
|
259
|
+
function parseBlankNode(opts) {
|
|
260
|
+
const { newline = "\n", prevNode } = opts || {};
|
|
261
|
+
if (prevNode?.type === "blank") {
|
|
262
|
+
prevNode.value += newline;
|
|
263
|
+
prevNode.position.end += newline.length;
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const start = prevNode?.position.end || 0;
|
|
267
|
+
return {
|
|
268
|
+
type: "blank",
|
|
269
|
+
value: newline,
|
|
270
|
+
position: {
|
|
271
|
+
start,
|
|
272
|
+
end: start + newline.length
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const CUSTOM_CONTAINER_TYPE_SET = /* @__PURE__ */ new Set([
|
|
277
|
+
"info",
|
|
278
|
+
"tip",
|
|
279
|
+
"warning",
|
|
280
|
+
"danger",
|
|
281
|
+
"details",
|
|
282
|
+
"raw",
|
|
283
|
+
"code-group",
|
|
284
|
+
"v-pre",
|
|
285
|
+
"tabs"
|
|
286
|
+
]);
|
|
287
|
+
const CUSTOM_CONTAINER_MARKER_PATTERN = String.raw`(^ {0,3}:{3,}[ \t]*)`;
|
|
288
|
+
const CUSTOM_CONTAINER_TYPE_PATTERN = String.raw`([\w-]+)(?=$|[ \t]|\r?\n)`;
|
|
289
|
+
const CUSTOM_CONTAINER_TITLE_AND_ATTRS_PATTERN = String.raw`([ \t][^\r\n]*)?`;
|
|
290
|
+
const CUSTOM_CONTAINER_CLOSE_MARKER_PATTERN = String.raw` {0,3}:{3,}[ \t]*`;
|
|
291
|
+
const CUSTOM_CONTAINER_OPEN_MARKER_RE = new RegExp(`${CUSTOM_CONTAINER_MARKER_PATTERN}${CUSTOM_CONTAINER_TYPE_PATTERN}${CUSTOM_CONTAINER_TITLE_AND_ATTRS_PATTERN}`);
|
|
292
|
+
const CUSTOM_CONTAINER_CLOSE_MARKER_RE = new RegExp(`^\r?\n${CUSTOM_CONTAINER_CLOSE_MARKER_PATTERN}$`);
|
|
293
|
+
const CUSTOM_CONTAINER_CLOSE_MARKER_STRICT_RE = new RegExp(`^${CUSTOM_CONTAINER_CLOSE_MARKER_PATTERN}$`);
|
|
294
|
+
const CUSTOM_CONTAINER_ATTRS_RE = /(\{[^{}]+\})\s*$/;
|
|
295
|
+
/**
|
|
296
|
+
* Checks whether a value is one of the custom-container types supported by VitePress.
|
|
297
|
+
*/
|
|
298
|
+
function isCustomContainerType(value) {
|
|
299
|
+
return CUSTOM_CONTAINER_TYPE_SET.has(value);
|
|
300
|
+
}
|
|
301
|
+
function isOpenTag(value) {
|
|
302
|
+
return CUSTOM_CONTAINER_OPEN_MARKER_RE.test(value);
|
|
303
|
+
}
|
|
304
|
+
function isBlank(value) {
|
|
305
|
+
return value === "\n" || value === "\r\n";
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Checks whether adjacent text is a custom container marker on the next line.
|
|
309
|
+
*
|
|
310
|
+
* @see https://vitepress.dev/guide/markdown#custom-containers
|
|
311
|
+
* @example `::: info` -> true (open tag)
|
|
312
|
+
* @example `:::: tip` -> true
|
|
313
|
+
* @example `:::` -> false (close tag)
|
|
314
|
+
*/
|
|
315
|
+
function isCustomContainerMarker(str) {
|
|
316
|
+
if (!str) return false;
|
|
317
|
+
return CUSTOM_CONTAINER_CLOSE_MARKER_RE.test(str) || CUSTOM_CONTAINER_OPEN_MARKER_RE.test(str);
|
|
318
|
+
}
|
|
319
|
+
const isCustomContainerNode = (node) => !!node && node.type === "custom-container";
|
|
320
|
+
const isBlankNode = (node) => !!node && node.type === "blank";
|
|
321
|
+
const isOpenNode = (node) => !!node && node.type === "open";
|
|
322
|
+
const isCloseNode = (node) => !!node && node.type === "close";
|
|
323
|
+
/**
|
|
324
|
+
* Ignore `Custom-containerNode` in `CodeNode` & `InlineCodeNode`.
|
|
325
|
+
* Collects source offsets for fenced code blocks and inline code nodes.
|
|
326
|
+
*/
|
|
327
|
+
function getCodeNodeRanges(node) {
|
|
328
|
+
const ranges = [];
|
|
329
|
+
function visit(current) {
|
|
330
|
+
if (!isObject(current)) return;
|
|
331
|
+
if (isCodeNode(current)) {
|
|
332
|
+
const position = current.position;
|
|
333
|
+
if (!position) return;
|
|
334
|
+
const start = position.start?.offset;
|
|
335
|
+
const end = position.end?.offset;
|
|
336
|
+
if (start !== void 0 && end !== void 0) ranges.push({
|
|
337
|
+
start,
|
|
338
|
+
end
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (hasChildren(current)) for (const child of current.children) visit(child);
|
|
342
|
+
}
|
|
343
|
+
visit(node);
|
|
344
|
+
return ranges;
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region utils/index.ts
|
|
348
|
+
function createRule({ name, create, defaultOptions, meta }) {
|
|
349
|
+
return {
|
|
350
|
+
create,
|
|
351
|
+
meta: {
|
|
352
|
+
defaultOptions,
|
|
353
|
+
...meta,
|
|
354
|
+
docs: {
|
|
355
|
+
...meta?.docs,
|
|
356
|
+
url: `https://github.com/NoiseFan/eslint-plugin-md-style/blob/main/docs/rules/en/${name}.md`
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region rules/padding-around-custom-container/analyzs.ts
|
|
363
|
+
/**
|
|
364
|
+
* Analyzes custom containers and returns de-duplicated padding issues.
|
|
365
|
+
*/
|
|
366
|
+
function getNodesIssues(nodes, opts) {
|
|
367
|
+
const { offset = 0, ignoreRanges = [], mode = "loose" } = opts || {};
|
|
368
|
+
const issues = [];
|
|
369
|
+
/**
|
|
370
|
+
* Recursively analyzes every custom-container level in a parsed document.
|
|
371
|
+
*/
|
|
372
|
+
function analyzeLevel(nodes, opts) {
|
|
373
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
374
|
+
const container = nodes[index];
|
|
375
|
+
if (!isCustomContainerNode(container)) continue;
|
|
376
|
+
const boundary = getContainerBoundary(container);
|
|
377
|
+
if (!boundary) continue;
|
|
378
|
+
analyzeInnerBoundary(container.children, boundary, opts);
|
|
379
|
+
analyzeOuterBoundary(nodes, index, opts);
|
|
380
|
+
analyzeLevel(container.children, opts);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
analyzeLevel(nodes, {
|
|
384
|
+
offset,
|
|
385
|
+
issues,
|
|
386
|
+
mode
|
|
387
|
+
});
|
|
388
|
+
return dedupeIssues(issues).filter((issue) => !ignoreRanges.some((range) => issue.start < range.end && issue.end > range.start));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Checks the blank lines immediately inside a container's opening and closing tags.
|
|
392
|
+
*/
|
|
393
|
+
function analyzeInnerBoundary(children, containerBoundary, opts) {
|
|
394
|
+
const { openIndex, closeIndex } = containerBoundary;
|
|
395
|
+
const first = children[openIndex + 1];
|
|
396
|
+
checkInnerSide(first, {
|
|
397
|
+
direction: 1,
|
|
398
|
+
children,
|
|
399
|
+
...opts
|
|
400
|
+
});
|
|
401
|
+
const last = children[closeIndex - 1];
|
|
402
|
+
if (last !== first) checkInnerSide(last, {
|
|
403
|
+
direction: -1,
|
|
404
|
+
children,
|
|
405
|
+
...opts
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Checks one side of a container's inner boundary for the selected mode.
|
|
410
|
+
*/
|
|
411
|
+
function checkInnerSide(node, opts) {
|
|
412
|
+
const { direction, children, offset } = opts;
|
|
413
|
+
if (isBlankNode(node)) {
|
|
414
|
+
const lineBreak = getLineBreak(node.value);
|
|
415
|
+
const expected = opts.mode === "loose" ? `${lineBreak}${lineBreak}` : lineBreak;
|
|
416
|
+
if (node.value !== expected) opts.issues.push({
|
|
417
|
+
start: offset + node.position.start,
|
|
418
|
+
end: offset + node.position.end,
|
|
419
|
+
replacement: expected,
|
|
420
|
+
messageId: node.value.length < expected.length ? MESSAGE_IDS$4.missing : MESSAGE_IDS$4.unexpected
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (opts.mode === "loose" && node && !isOpenNode(node) && !isCloseNode(node)) {
|
|
425
|
+
const insertion = direction === 1 ? node.position.start : node.position.end;
|
|
426
|
+
opts.issues.push({
|
|
427
|
+
start: offset + insertion,
|
|
428
|
+
end: offset + insertion,
|
|
429
|
+
replacement: getLineBreakFromChildren(children) + getLineBreakFromChildren(children),
|
|
430
|
+
messageId: MESSAGE_IDS$4.missing
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Finds the first opening tag and last closing tag for a custom container.
|
|
436
|
+
*/
|
|
437
|
+
function getContainerBoundary(container) {
|
|
438
|
+
const openIndex = container.children.findIndex(isOpenNode);
|
|
439
|
+
const closeIndex = container.children.findLastIndex(isCloseNode);
|
|
440
|
+
if (openIndex === -1 || closeIndex <= openIndex) return;
|
|
441
|
+
return {
|
|
442
|
+
openIndex,
|
|
443
|
+
closeIndex
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Checks both sides of a container for the required external blank line.
|
|
448
|
+
*/
|
|
449
|
+
function analyzeOuterBoundary(children, boundaryIndex, opts) {
|
|
450
|
+
checkOuterSide(children, {
|
|
451
|
+
direction: -1,
|
|
452
|
+
boundaryIndex,
|
|
453
|
+
...opts
|
|
454
|
+
});
|
|
455
|
+
checkOuterSide(children, {
|
|
456
|
+
direction: 1,
|
|
457
|
+
boundaryIndex,
|
|
458
|
+
...opts
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Checks one side of a container and records insertion or normalization issues.
|
|
463
|
+
*/
|
|
464
|
+
function checkOuterSide(children, opts) {
|
|
465
|
+
const { boundaryIndex, direction, offset, issues } = opts;
|
|
466
|
+
const blankIndex = boundaryIndex + direction;
|
|
467
|
+
const blank = children[blankIndex];
|
|
468
|
+
const contentIndex = isBlankNode(blank) ? blankIndex + direction : blankIndex;
|
|
469
|
+
const content = children[contentIndex];
|
|
470
|
+
if (!isExternalContent(content, {
|
|
471
|
+
index: contentIndex,
|
|
472
|
+
length: children.length
|
|
473
|
+
})) return;
|
|
474
|
+
if (!isBlankNode(blank)) {
|
|
475
|
+
const insertion = direction === -1 ? children[boundaryIndex].position.start : children[boundaryIndex].position.end;
|
|
476
|
+
issues.push({
|
|
477
|
+
start: offset + insertion,
|
|
478
|
+
end: offset + insertion,
|
|
479
|
+
replacement: getLineBreakFromChildren(children),
|
|
480
|
+
messageId: MESSAGE_IDS$4.missing
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const lineBreak = getLineBreak(blank.value);
|
|
485
|
+
const expected = `${lineBreak}${lineBreak}`;
|
|
486
|
+
if (blank.value === expected) return;
|
|
487
|
+
issues.push({
|
|
488
|
+
start: offset + blank.position.start,
|
|
489
|
+
end: offset + blank.position.end,
|
|
490
|
+
replacement: expected,
|
|
491
|
+
messageId: blank.value.length < expected.length ? MESSAGE_IDS$4.missing : MESSAGE_IDS$4.unexpected
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Returns whether a node is content outside the container's own boundary tags.
|
|
496
|
+
*/
|
|
497
|
+
function isExternalContent(node, opts) {
|
|
498
|
+
if (!node || isBlankNode(node)) return false;
|
|
499
|
+
const { index, length } = opts;
|
|
500
|
+
if (index === 0 && isOpenNode(node)) return false;
|
|
501
|
+
if (index === length - 1 && isCloseNode(node)) return false;
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Removes issues that target the same range with the same replacement.
|
|
506
|
+
*/
|
|
507
|
+
function dedupeIssues(issues) {
|
|
508
|
+
const seen = /* @__PURE__ */ new Set();
|
|
509
|
+
return issues.filter((edit) => {
|
|
510
|
+
const key = `${edit.start}:${edit.end}:${edit.replacement}`;
|
|
511
|
+
if (seen.has(key)) return false;
|
|
512
|
+
seen.add(key);
|
|
513
|
+
return true;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Returns the line-break sequence used by a string.
|
|
518
|
+
*/
|
|
519
|
+
function getLineBreak(value) {
|
|
520
|
+
return value.includes("\r\n") ? "\r\n" : "\n";
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Uses the first blank child to infer a container's line-break sequence.
|
|
524
|
+
*/
|
|
525
|
+
function getLineBreakFromChildren(children) {
|
|
526
|
+
const blank = children.find((child) => isBlankNode(child));
|
|
527
|
+
return isBlankNode(blank) ? getLineBreak(blank.value) : "\n";
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region rules/padding-around-custom-container/index.ts
|
|
531
|
+
const RULE_NAME$6 = "padding-around-custom-container";
|
|
532
|
+
const MESSAGE_IDS$4 = {
|
|
533
|
+
missing: "missing",
|
|
534
|
+
unexpected: "unexpected"
|
|
535
|
+
};
|
|
536
|
+
var padding_around_custom_container_default = createRule({
|
|
537
|
+
name: RULE_NAME$6,
|
|
538
|
+
meta: {
|
|
539
|
+
type: "layout",
|
|
540
|
+
docs: {
|
|
541
|
+
description: "Enforce padding around VitePress custom containers.",
|
|
542
|
+
recommended: true
|
|
543
|
+
},
|
|
544
|
+
messages: {
|
|
545
|
+
missing: "A custom container must be separated from surrounding content by one blank line.",
|
|
546
|
+
unexpected: "Unexpected blank lines around a custom container."
|
|
547
|
+
},
|
|
548
|
+
fixable: "whitespace",
|
|
549
|
+
schema: [{ enum: ["compact", "loose"] }]
|
|
550
|
+
},
|
|
551
|
+
defaultOptions: ["loose"],
|
|
552
|
+
create(context) {
|
|
553
|
+
return { root(node) {
|
|
554
|
+
const { position, start } = getNodePosition(node);
|
|
555
|
+
/* v8 ignore if -- @preserve */
|
|
556
|
+
if (!position) return;
|
|
557
|
+
const source = context.sourceCode.text;
|
|
558
|
+
const ignoreRanges = getCodeNodeRanges(node);
|
|
559
|
+
const nodes = parseCustomContainers(source.slice(start));
|
|
560
|
+
const [mode] = context.options;
|
|
561
|
+
const issues = getNodesIssues(nodes, {
|
|
562
|
+
offset: start,
|
|
563
|
+
ignoreRanges,
|
|
564
|
+
mode
|
|
565
|
+
});
|
|
566
|
+
for (const { start, end, messageId, replacement } of issues) context.report({
|
|
567
|
+
node,
|
|
568
|
+
messageId,
|
|
569
|
+
loc: {
|
|
570
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
571
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
572
|
+
},
|
|
573
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], replacement)
|
|
574
|
+
});
|
|
575
|
+
} };
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region rules/space-around-custom-container/analyzs.ts
|
|
580
|
+
/**
|
|
581
|
+
* Finds custom-container marker lines and normalizes their surrounding spaces.
|
|
582
|
+
*
|
|
583
|
+
* Opening markers use exactly one space between the fence and the type,
|
|
584
|
+
* while closing markers contain only the fence. Leading indentation is removed.
|
|
585
|
+
* Ranges in `ignoredRanges` (for example fenced code blocks) are skipped.
|
|
586
|
+
*/
|
|
587
|
+
function getSourceIssues(source, ignoredRanges = []) {
|
|
588
|
+
const issues = [];
|
|
589
|
+
function visit(nodes) {
|
|
590
|
+
for (const node of nodes) {
|
|
591
|
+
if (!isCustomContainerNode(node)) continue;
|
|
592
|
+
for (const child of node.children) if (isOpenNode(child) || isCloseNode(child)) analyzeMarker(child, ignoredRanges, issues);
|
|
593
|
+
else if (isCustomContainerNode(child)) visit([child]);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
visit(parseCustomContainers(source));
|
|
597
|
+
return issues;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Adds a spacing issue for one parsed opening or closing marker.
|
|
601
|
+
*/
|
|
602
|
+
function analyzeMarker(tag, ignoredRanges, issues) {
|
|
603
|
+
if (ignoredRanges.some((range) => tag.position.start < range.end && tag.position.end > range.start)) return;
|
|
604
|
+
const replacement = normalizedMarker(tag);
|
|
605
|
+
if (tag.raw === replacement) return;
|
|
606
|
+
issues.push({
|
|
607
|
+
...tag.position,
|
|
608
|
+
replacement,
|
|
609
|
+
messageId: getMarkerMessageId(tag)
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Returns the normalized source text for a parsed container marker.
|
|
614
|
+
*/
|
|
615
|
+
function normalizedMarker(tag) {
|
|
616
|
+
const fence = ":".repeat(tag.markerLength);
|
|
617
|
+
if (isCloseNode(tag)) return fence;
|
|
618
|
+
const valueStart = tag.value.start - tag.position.start;
|
|
619
|
+
return `${fence} ${tag.raw.slice(valueStart).trim()}`;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Classifies the spacing problem represented by a parsed marker.
|
|
623
|
+
*/
|
|
624
|
+
function getMarkerMessageId(tag) {
|
|
625
|
+
if (tag.raw.length !== tag.raw.trimStart().length) return MESSAGE_IDS$3.unexpectedIndentation;
|
|
626
|
+
if (isCloseNode(tag)) return MESSAGE_IDS$3.unexpectedTrailingSpace;
|
|
627
|
+
if (tag.raw.slice(tag.markerLength, tag.value.start - tag.position.start).length === 0) return MESSAGE_IDS$3.missingSeparator;
|
|
628
|
+
return MESSAGE_IDS$3.unexpectedSeparator;
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region rules/space-around-custom-container/index.ts
|
|
632
|
+
const RULE_NAME$5 = "space-around-custom-container";
|
|
633
|
+
const MESSAGE_IDS$3 = {
|
|
634
|
+
unexpectedIndentation: "unexpectedIndentation",
|
|
635
|
+
missingSeparator: "missingSeparator",
|
|
636
|
+
unexpectedSeparator: "unexpectedSeparator",
|
|
637
|
+
unexpectedTrailingSpace: "unexpectedTrailingSpace"
|
|
638
|
+
};
|
|
639
|
+
var space_around_custom_container_default = createRule({
|
|
640
|
+
name: RULE_NAME$5,
|
|
641
|
+
meta: {
|
|
642
|
+
type: "layout",
|
|
643
|
+
docs: { description: "Enforce spacing around VitePress custom-container markers." },
|
|
644
|
+
messages: {
|
|
645
|
+
unexpectedIndentation: "Do not indent a custom-container marker.",
|
|
646
|
+
missingSeparator: "Add one space between the opening fence and its type.",
|
|
647
|
+
unexpectedSeparator: "Use exactly one space between the opening fence and its type.",
|
|
648
|
+
unexpectedTrailingSpace: "Remove trailing spaces from a custom-container closing marker."
|
|
649
|
+
},
|
|
650
|
+
fixable: "whitespace",
|
|
651
|
+
schema: []
|
|
652
|
+
},
|
|
653
|
+
defaultOptions: [],
|
|
654
|
+
create(context) {
|
|
655
|
+
return { root(node) {
|
|
656
|
+
const { position } = getNodePosition(node);
|
|
657
|
+
/* v8 ignore if -- @preserve */
|
|
658
|
+
if (!position) return;
|
|
659
|
+
const source = context.sourceCode.text;
|
|
660
|
+
const issues = getSourceIssues(source, getCodeNodeRanges(node));
|
|
661
|
+
for (const { start, end, messageId, replacement } of issues) context.report({
|
|
662
|
+
node,
|
|
663
|
+
messageId,
|
|
664
|
+
loc: {
|
|
665
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
666
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
667
|
+
},
|
|
668
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], replacement)
|
|
669
|
+
});
|
|
670
|
+
} };
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region utils/anchor.ts
|
|
120
675
|
/**
|
|
121
|
-
* Match the trailing anchor-like fragment from a heading
|
|
676
|
+
* Match the trailing anchor-like fragment from a heading or adjacent text.
|
|
122
677
|
* @example `中文标题 {#Chinese-Title}` -> `{#Chinese-Title}`
|
|
123
678
|
* @example `使用 describe #Grouping Tests` -> `#Grouping Tests`
|
|
124
679
|
*/
|
|
@@ -138,52 +693,15 @@ function getLikeAnchor(str) {
|
|
|
138
693
|
if (str === void 0) return null;
|
|
139
694
|
const match = getLikeAnchorMatch(str);
|
|
140
695
|
if (!match) return null;
|
|
141
|
-
const rawLikeAnchor = match.replace(/
|
|
696
|
+
const rawLikeAnchor = match.replace(/[{}]/g, "").replace(/^#/, "").trimStart();
|
|
142
697
|
return {
|
|
143
698
|
isLikeAnchor: rawLikeAnchor.includes(" "),
|
|
144
699
|
rawLikeAnchor
|
|
145
700
|
};
|
|
146
701
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Check if the string has an anchor.
|
|
149
|
-
* @example: {#chinese-anchor}
|
|
150
|
-
*/
|
|
151
|
-
function isStrictAnchor(str) {
|
|
152
|
-
return /\s\{#[a-z0-9]+(?:-[a-z0-9]+)*\}/.test(str);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Check whether the string contains CJK Han characters.
|
|
156
|
-
*/
|
|
157
|
-
function hasChinese(str) {
|
|
158
|
-
return /\p{Script=Han}/u.test(str);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Normalize raw anchor text into the strict anchor format content.
|
|
162
|
-
* - lowercase all letters
|
|
163
|
-
* - convert spaces to `-`
|
|
164
|
-
* - remove unsupported characters
|
|
165
|
-
* - trim leading/trailing `-`
|
|
166
|
-
*/
|
|
167
|
-
function normalizeAnchor(anchor) {
|
|
168
|
-
return anchor.toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Count wrapper characters contributed by the trailing like-anchor fragment.
|
|
172
|
-
* The value is the length difference between the raw matched fragment and the
|
|
173
|
-
* cleaned anchor text returned by `getLikeAnchor`.
|
|
174
|
-
* @example `# 中文标题 {#Chinese-Title}` -> 3
|
|
175
|
-
* @example `## 使用 \`describe\` 编组测试 #Grouping Tests with \`describe\`` -> 1
|
|
176
|
-
*/
|
|
177
|
-
function calcAnchorPositionCompensate(content) {
|
|
178
|
-
const match = getLikeAnchorMatch(content);
|
|
179
|
-
const anchor = getLikeAnchor(content);
|
|
180
|
-
if (!match || !anchor) return 0;
|
|
181
|
-
return match.length - anchor.rawLikeAnchor.length;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
702
|
//#endregion
|
|
185
|
-
//#region
|
|
186
|
-
const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
703
|
+
//#region utils/punctuation.ts
|
|
704
|
+
const OPENING_PAIRED_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
187
705
|
"(",
|
|
188
706
|
"[",
|
|
189
707
|
"{",
|
|
@@ -194,7 +712,7 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
|
194
712
|
"“",
|
|
195
713
|
"‘"
|
|
196
714
|
]);
|
|
197
|
-
const CLOSING_PAIRED_PUNCTUATION = new Set([
|
|
715
|
+
const CLOSING_PAIRED_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
198
716
|
")",
|
|
199
717
|
"]",
|
|
200
718
|
"}",
|
|
@@ -262,17 +780,8 @@ function hasPunctuation(str, position = "head") {
|
|
|
262
780
|
if (position === "head") return isPunctuation(str[0]);
|
|
263
781
|
else return isPunctuation(str[str.length - 1]);
|
|
264
782
|
}
|
|
265
|
-
|
|
266
783
|
//#endregion
|
|
267
|
-
//#region
|
|
268
|
-
const SPACE_MESSAGE_IDS = {
|
|
269
|
-
missingSpaceBefore: "missingSpaceBefore",
|
|
270
|
-
missingSpaceAfter: "missingSpaceAfter",
|
|
271
|
-
missingSpacesAround: "missingSpacesAround",
|
|
272
|
-
unexpectedSpaceBefore: "unexpectedSpaceBefore",
|
|
273
|
-
unexpectedSpaceAfter: "unexpectedSpaceAfter",
|
|
274
|
-
unexpectedSpaceAround: "unexpectedSpaceAround"
|
|
275
|
-
};
|
|
784
|
+
//#region utils/space.ts
|
|
276
785
|
/**
|
|
277
786
|
* Gets the count and range of consecutive whitespace at the start or end of a string.
|
|
278
787
|
* @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
|
|
@@ -303,45 +812,8 @@ function getWhiteSpace(str, position = "head") {
|
|
|
303
812
|
};
|
|
304
813
|
}
|
|
305
814
|
}
|
|
306
|
-
/**
|
|
307
|
-
* Gets whitespace and punctuation information for text adjacent to a link or inline code node.
|
|
308
|
-
*/
|
|
309
|
-
function getSpaceContext(nodeContext) {
|
|
310
|
-
const { prev, next } = nodeContext;
|
|
311
|
-
const prevValue = getNodeValue(prev);
|
|
312
|
-
const nextValue = getNodeValue(next);
|
|
313
|
-
return {
|
|
314
|
-
prev: {
|
|
315
|
-
value: prevValue,
|
|
316
|
-
whiteSpace: getWhiteSpace(prevValue, "tail"),
|
|
317
|
-
hasPunctuation: hasPunctuation(prevValue, "tail"),
|
|
318
|
-
punctuationType: isFullwidthPunctuation(getAdjacentChar(prevValue, "tail")) ? "full" : "half"
|
|
319
|
-
},
|
|
320
|
-
next: {
|
|
321
|
-
value: nextValue,
|
|
322
|
-
whiteSpace: getWhiteSpace(nextValue),
|
|
323
|
-
hasPunctuation: hasPunctuation(nextValue),
|
|
324
|
-
punctuationType: isFullwidthPunctuation(getAdjacentChar(nextValue, "head")) ? "full" : "half"
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
815
|
//#endregion
|
|
330
|
-
//#region
|
|
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
|
-
}
|
|
816
|
+
//#region rules/space-around-inline-element/analyze.ts
|
|
345
817
|
/**
|
|
346
818
|
* Validates whether a spacing run contains exactly one required space.
|
|
347
819
|
*/
|
|
@@ -355,18 +827,18 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
|
|
|
355
827
|
function validateBeforePunctuation(context) {
|
|
356
828
|
const adjacentChar = getAdjacentChar(context.value, "tail");
|
|
357
829
|
if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
|
|
358
|
-
if (context.whiteSpace.count > 0) return
|
|
830
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-before";
|
|
359
831
|
return;
|
|
360
832
|
}
|
|
361
|
-
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
362
|
-
if (context.whiteSpace.count > 0) return
|
|
833
|
+
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-before", "multiple-spaces-after-punctuation");
|
|
834
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-before";
|
|
363
835
|
}
|
|
364
836
|
/**
|
|
365
837
|
* Validates the spacing between the previous node and the current inline node.
|
|
366
838
|
*/
|
|
367
839
|
function validateSpaceBeforeNode(context) {
|
|
368
840
|
if (context.hasPunctuation) return validateBeforePunctuation(context);
|
|
369
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
841
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-before", "multiple-spaces-before");
|
|
370
842
|
}
|
|
371
843
|
/**
|
|
372
844
|
* Validates spacing after an inline node when the next character is punctuation.
|
|
@@ -374,16 +846,48 @@ function validateSpaceBeforeNode(context) {
|
|
|
374
846
|
function validateSpaceAfterPunctuation(context) {
|
|
375
847
|
const adjacentChar = getAdjacentChar(context.value, "head");
|
|
376
848
|
if (getLikeAnchor(context.value) || isCustomContainerMarker(context.value)) return;
|
|
377
|
-
if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return
|
|
378
|
-
if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
379
|
-
if (context.whiteSpace.count > 0) return
|
|
849
|
+
if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return "unexpected-space-after";
|
|
850
|
+
if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-after", "multiple-spaces-after");
|
|
851
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-after";
|
|
380
852
|
}
|
|
381
853
|
/**
|
|
382
854
|
* Validates the spacing between the current inline node and the next node.
|
|
383
855
|
*/
|
|
384
856
|
function validateSpaceAfterNode(context) {
|
|
385
857
|
if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
|
|
386
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
858
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-after", "multiple-spaces-after");
|
|
859
|
+
}
|
|
860
|
+
const INLINE_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
861
|
+
"link",
|
|
862
|
+
"image",
|
|
863
|
+
"inlineCode",
|
|
864
|
+
"emphasis",
|
|
865
|
+
"strong"
|
|
866
|
+
]);
|
|
867
|
+
/**
|
|
868
|
+
* Checks whether a phrasing node is one of the selected inline element targets.
|
|
869
|
+
*/
|
|
870
|
+
function isInlineElement(node) {
|
|
871
|
+
return !!node && INLINE_ELEMENT_TYPES.has(node.type);
|
|
872
|
+
}
|
|
873
|
+
function getSpaceContext(nodeContext) {
|
|
874
|
+
const { prev, next } = nodeContext;
|
|
875
|
+
const prevValue = getNodeValue(prev);
|
|
876
|
+
const nextValue = getNodeValue(next);
|
|
877
|
+
return {
|
|
878
|
+
prev: {
|
|
879
|
+
value: prevValue,
|
|
880
|
+
whiteSpace: getWhiteSpace(prevValue, "tail"),
|
|
881
|
+
hasPunctuation: hasPunctuation(prevValue, "tail"),
|
|
882
|
+
punctuationType: isFullwidthPunctuation(getAdjacentChar(prevValue, "tail")) ? "full" : "half"
|
|
883
|
+
},
|
|
884
|
+
next: {
|
|
885
|
+
value: nextValue,
|
|
886
|
+
whiteSpace: getWhiteSpace(nextValue),
|
|
887
|
+
hasPunctuation: hasPunctuation(nextValue),
|
|
888
|
+
punctuationType: isFullwidthPunctuation(getAdjacentChar(nextValue, "head")) ? "full" : "half"
|
|
889
|
+
}
|
|
890
|
+
};
|
|
387
891
|
}
|
|
388
892
|
/**
|
|
389
893
|
* Validates spacing around an inline element inside a table cell.
|
|
@@ -422,11 +926,10 @@ function validateSpace(nodeContext) {
|
|
|
422
926
|
if (parent && isTableCell(parent)) return validateTableCellSpace(nodeContext);
|
|
423
927
|
return validateDefaultSpace(nodeContext);
|
|
424
928
|
}
|
|
425
|
-
|
|
426
929
|
//#endregion
|
|
427
|
-
//#region
|
|
428
|
-
const RULE_NAME$
|
|
429
|
-
const MESSAGE_IDS$
|
|
930
|
+
//#region rules/space-around-inline-element/index.ts
|
|
931
|
+
const RULE_NAME$4 = "space-around-inline-element";
|
|
932
|
+
const MESSAGE_IDS$2 = {
|
|
430
933
|
missingSpaceBefore: "missingSpaceBefore",
|
|
431
934
|
missingSpaceAfter: "missingSpaceAfter",
|
|
432
935
|
multipleSpacesBefore: "multipleSpacesBefore",
|
|
@@ -435,17 +938,29 @@ const MESSAGE_IDS$1 = {
|
|
|
435
938
|
unexpectedSpaceBefore: "unexpectedSpaceBefore",
|
|
436
939
|
unexpectedSpaceAfter: "unexpectedSpaceAfter"
|
|
437
940
|
};
|
|
438
|
-
const
|
|
439
|
-
MESSAGE_IDS$
|
|
440
|
-
MESSAGE_IDS$
|
|
441
|
-
MESSAGE_IDS$
|
|
442
|
-
MESSAGE_IDS$
|
|
941
|
+
const ISSUE_TO_MESSAGE_ID = {
|
|
942
|
+
"missing-space-before": MESSAGE_IDS$2.missingSpaceBefore,
|
|
943
|
+
"missing-space-after": MESSAGE_IDS$2.missingSpaceAfter,
|
|
944
|
+
"multiple-spaces-before": MESSAGE_IDS$2.multipleSpacesBefore,
|
|
945
|
+
"multiple-spaces-after": MESSAGE_IDS$2.multipleSpacesAfter,
|
|
946
|
+
"multiple-spaces-after-punctuation": MESSAGE_IDS$2.multipleSpacesAfterPunctuation,
|
|
947
|
+
"unexpected-space-before": MESSAGE_IDS$2.unexpectedSpaceBefore,
|
|
948
|
+
"unexpected-space-after": MESSAGE_IDS$2.unexpectedSpaceAfter
|
|
949
|
+
};
|
|
950
|
+
const BEFORE_INLINE_ELEMENT_MESSAGE_IDS = /* @__PURE__ */ new Set([
|
|
951
|
+
MESSAGE_IDS$2.missingSpaceBefore,
|
|
952
|
+
MESSAGE_IDS$2.multipleSpacesBefore,
|
|
953
|
+
MESSAGE_IDS$2.multipleSpacesAfterPunctuation,
|
|
954
|
+
MESSAGE_IDS$2.unexpectedSpaceBefore
|
|
443
955
|
]);
|
|
444
956
|
var space_around_inline_element_default = createRule({
|
|
445
|
-
name: RULE_NAME$
|
|
957
|
+
name: RULE_NAME$4,
|
|
446
958
|
meta: {
|
|
447
959
|
type: "layout",
|
|
448
|
-
docs: {
|
|
960
|
+
docs: {
|
|
961
|
+
description: "Enforce spacing around Markdown inline elements.",
|
|
962
|
+
recommended: true
|
|
963
|
+
},
|
|
449
964
|
messages: {
|
|
450
965
|
missingSpaceBefore: "A space is required before the inline element.",
|
|
451
966
|
missingSpaceAfter: "A space is required after the inline element.",
|
|
@@ -487,13 +1002,13 @@ function checkInlineElement(context, node) {
|
|
|
487
1002
|
/* v8 ignore if -- @preserve */
|
|
488
1003
|
if (!position) return;
|
|
489
1004
|
const nodeContext = getNodeContext(context, node);
|
|
490
|
-
if (isNestedInlineElement(nodeContext)) return;
|
|
491
1005
|
const spaceContext = getSpaceContext(nodeContext);
|
|
492
|
-
const
|
|
493
|
-
if (!
|
|
1006
|
+
const issue = validateSpace(nodeContext);
|
|
1007
|
+
if (!issue) return;
|
|
1008
|
+
const messageId = ISSUE_TO_MESSAGE_ID[issue];
|
|
494
1009
|
if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
|
|
495
1010
|
const { count } = spaceContext.prev.whiteSpace;
|
|
496
|
-
const replaceText = messageId === MESSAGE_IDS$
|
|
1011
|
+
const replaceText = messageId === MESSAGE_IDS$2.unexpectedSpaceBefore ? "" : " ";
|
|
497
1012
|
context.report({
|
|
498
1013
|
node,
|
|
499
1014
|
messageId,
|
|
@@ -505,7 +1020,7 @@ function checkInlineElement(context, node) {
|
|
|
505
1020
|
}
|
|
506
1021
|
if (spaceContext.next) {
|
|
507
1022
|
const { count } = spaceContext.next.whiteSpace;
|
|
508
|
-
const replaceText = messageId === MESSAGE_IDS$
|
|
1023
|
+
const replaceText = messageId === MESSAGE_IDS$2.unexpectedSpaceAfter ? "" : " ";
|
|
509
1024
|
context.report({
|
|
510
1025
|
node,
|
|
511
1026
|
messageId,
|
|
@@ -515,9 +1030,8 @@ function checkInlineElement(context, node) {
|
|
|
515
1030
|
});
|
|
516
1031
|
}
|
|
517
1032
|
}
|
|
518
|
-
|
|
519
1033
|
//#endregion
|
|
520
|
-
//#region
|
|
1034
|
+
//#region parser/text/parser.ts
|
|
521
1035
|
const TEXT_TYPE = {
|
|
522
1036
|
"cjk": "cjk",
|
|
523
1037
|
"latin": "latin",
|
|
@@ -539,7 +1053,7 @@ const SYMBOL_RE = /^\p{Symbol}$/u;
|
|
|
539
1053
|
const EMOJI_RE = /^\p{Extended_Pictographic}$/u;
|
|
540
1054
|
const SPACE_RE = /^[\t\v\f \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]$/u;
|
|
541
1055
|
const NEWLINE_RE = /^[\n\r\u2028\u2029]$/u;
|
|
542
|
-
const INVISIBLE_CODE_POINTS = new Set([
|
|
1056
|
+
const INVISIBLE_CODE_POINTS = /* @__PURE__ */ new Set([
|
|
543
1057
|
173,
|
|
544
1058
|
847,
|
|
545
1059
|
1564,
|
|
@@ -560,15 +1074,13 @@ function isInvisible(char) {
|
|
|
560
1074
|
const codePoint = char.codePointAt(0);
|
|
561
1075
|
return codePoint != null && (INVISIBLE_CODE_POINTS.has(codePoint) || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303);
|
|
562
1076
|
}
|
|
563
|
-
function isLatinWordType(type) {
|
|
564
|
-
return type === TEXT_TYPE.latin;
|
|
565
|
-
}
|
|
566
|
-
function isNumberType(type) {
|
|
567
|
-
return type === TEXT_TYPE.number;
|
|
568
|
-
}
|
|
569
1077
|
function isNumber(char, prev) {
|
|
570
1078
|
return NUMBER_RE.test(char) || prev?.type === TEXT_TYPE.number && (char === "." || char === "%");
|
|
571
1079
|
}
|
|
1080
|
+
function isLatinWordType(type) {
|
|
1081
|
+
if (!type) return false;
|
|
1082
|
+
return type === "latin";
|
|
1083
|
+
}
|
|
572
1084
|
const TEXT_TYPE_MATCHERS = [
|
|
573
1085
|
{
|
|
574
1086
|
type: TEXT_TYPE.newline,
|
|
@@ -621,15 +1133,16 @@ const DEFAULT_START_POINT = {
|
|
|
621
1133
|
offset: 0
|
|
622
1134
|
};
|
|
623
1135
|
function advancePoint(point, char) {
|
|
1136
|
+
const { line, column, offset = 0 } = point;
|
|
624
1137
|
if (NEWLINE_RE.test(char)) return {
|
|
625
|
-
line:
|
|
1138
|
+
line: line + 1,
|
|
626
1139
|
column: 1,
|
|
627
|
-
offset:
|
|
1140
|
+
offset: offset + char.length
|
|
628
1141
|
};
|
|
629
1142
|
return {
|
|
630
|
-
line
|
|
631
|
-
column:
|
|
632
|
-
offset:
|
|
1143
|
+
line,
|
|
1144
|
+
column: column + char.length,
|
|
1145
|
+
offset: offset + char.length
|
|
633
1146
|
};
|
|
634
1147
|
}
|
|
635
1148
|
/**
|
|
@@ -694,9 +1207,8 @@ function buildTextNodeAst(node) {
|
|
|
694
1207
|
node
|
|
695
1208
|
};
|
|
696
1209
|
}
|
|
697
|
-
|
|
698
1210
|
//#endregion
|
|
699
|
-
//#region
|
|
1211
|
+
//#region rules/shared/text-boundary-spacing.ts
|
|
700
1212
|
/**
|
|
701
1213
|
* Normalizes an existing space token around the target token type.
|
|
702
1214
|
* When multiple spaces are collapsed, the redundant side is recorded so the
|
|
@@ -740,12 +1252,12 @@ function processTargetToken(ctx, result) {
|
|
|
740
1252
|
* collected missing or redundant space flags.
|
|
741
1253
|
*/
|
|
742
1254
|
function getBoundarySpaceMessageId(boundary) {
|
|
743
|
-
if (boundary.missingBefore && boundary.missingAfter) return
|
|
744
|
-
if (boundary.unexpectedBefore && boundary.unexpectedAfter) return
|
|
745
|
-
if (boundary.missingBefore) return
|
|
746
|
-
if (boundary.missingAfter) return
|
|
747
|
-
if (boundary.unexpectedBefore) return
|
|
748
|
-
return
|
|
1255
|
+
if (boundary.missingBefore && boundary.missingAfter) return "missingSpacesAround";
|
|
1256
|
+
if (boundary.unexpectedBefore && boundary.unexpectedAfter) return "unexpectedSpaceAround";
|
|
1257
|
+
if (boundary.missingBefore) return "missingSpaceBefore";
|
|
1258
|
+
if (boundary.missingAfter) return "missingSpaceAfter";
|
|
1259
|
+
if (boundary.unexpectedBefore) return "unexpectedSpaceBefore";
|
|
1260
|
+
return "unexpectedSpaceAfter";
|
|
749
1261
|
}
|
|
750
1262
|
/**
|
|
751
1263
|
* Rebuilds a text node with normalized spacing between CJK text and a target
|
|
@@ -770,12 +1282,8 @@ function fixBoundarySpace(node, isTargetType) {
|
|
|
770
1282
|
}
|
|
771
1283
|
return result;
|
|
772
1284
|
}
|
|
773
|
-
|
|
774
|
-
//#endregion
|
|
775
|
-
//#region src/rules/space-around-number/index.ts
|
|
776
|
-
const RULE_NAME$2 = "space-around-number";
|
|
777
1285
|
var space_around_number_default = createRule({
|
|
778
|
-
name:
|
|
1286
|
+
name: "space-around-number",
|
|
779
1287
|
meta: {
|
|
780
1288
|
type: "layout",
|
|
781
1289
|
docs: { description: "Enforce a single space between CJK characters and numbers." },
|
|
@@ -793,7 +1301,7 @@ var space_around_number_default = createRule({
|
|
|
793
1301
|
defaultOptions: [],
|
|
794
1302
|
create(context) {
|
|
795
1303
|
return { text(node) {
|
|
796
|
-
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node,
|
|
1304
|
+
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, (type) => type === "number");
|
|
797
1305
|
if (fixed === node.value) return;
|
|
798
1306
|
context.report({
|
|
799
1307
|
node,
|
|
@@ -803,19 +1311,13 @@ var space_around_number_default = createRule({
|
|
|
803
1311
|
unexpectedBefore,
|
|
804
1312
|
unexpectedAfter
|
|
805
1313
|
}),
|
|
806
|
-
fix(fixer)
|
|
807
|
-
return fixer.replaceText(node, fixed);
|
|
808
|
-
}
|
|
1314
|
+
fix: (fixer) => fixer.replaceText(node, fixed)
|
|
809
1315
|
});
|
|
810
1316
|
} };
|
|
811
1317
|
}
|
|
812
1318
|
});
|
|
813
|
-
|
|
814
|
-
//#endregion
|
|
815
|
-
//#region src/rules/space-around-word/index.ts
|
|
816
|
-
const RULE_NAME$1 = "space-around-word";
|
|
817
1319
|
var space_around_word_default = createRule({
|
|
818
|
-
name:
|
|
1320
|
+
name: "space-around-word",
|
|
819
1321
|
meta: {
|
|
820
1322
|
type: "layout",
|
|
821
1323
|
docs: { description: "Enforce a single space between CJK characters and Latin words." },
|
|
@@ -850,20 +1352,124 @@ var space_around_word_default = createRule({
|
|
|
850
1352
|
} };
|
|
851
1353
|
}
|
|
852
1354
|
});
|
|
853
|
-
|
|
854
1355
|
//#endregion
|
|
855
|
-
//#region
|
|
1356
|
+
//#region rules/valid-custom-container-type/index.ts
|
|
1357
|
+
const RULE_NAME$1 = "valid-custom-container-type";
|
|
1358
|
+
const MESSAGE_IDS$1 = {
|
|
1359
|
+
invalidType: "invalidType",
|
|
1360
|
+
invalidTypeCase: "invalidTypeCase"
|
|
1361
|
+
};
|
|
1362
|
+
var valid_custom_container_type_default = createRule({
|
|
1363
|
+
name: RULE_NAME$1,
|
|
1364
|
+
meta: {
|
|
1365
|
+
type: "problem",
|
|
1366
|
+
docs: {
|
|
1367
|
+
description: "Require custom containers to use a supported type.",
|
|
1368
|
+
recommended: true
|
|
1369
|
+
},
|
|
1370
|
+
messages: {
|
|
1371
|
+
invalidType: "Invalid custom container type \"{{type}}\". Use info, tip, warning, danger, details, raw, code-group, v-pre, or tabs.",
|
|
1372
|
+
invalidTypeCase: "Custom container type \"{{type}}\" must be lowercase."
|
|
1373
|
+
},
|
|
1374
|
+
fixable: "code",
|
|
1375
|
+
schema: []
|
|
1376
|
+
},
|
|
1377
|
+
defaultOptions: [],
|
|
1378
|
+
create(context) {
|
|
1379
|
+
return { text(node) {
|
|
1380
|
+
const { position, start: startPoint } = getNodePosition(node);
|
|
1381
|
+
/* v8 ignore if -- @preserve */
|
|
1382
|
+
if (!position) return;
|
|
1383
|
+
for (const tag of getOpeningTags(parseCustomContainers(node.value))) {
|
|
1384
|
+
const issue = getTypeIssue(tag.value.content);
|
|
1385
|
+
if (!issue) continue;
|
|
1386
|
+
const normalizedType = "normalizedType" in issue ? issue.normalizedType : void 0;
|
|
1387
|
+
const start = startPoint + tag.value.start;
|
|
1388
|
+
const end = startPoint + tag.value.end;
|
|
1389
|
+
context.report({
|
|
1390
|
+
node,
|
|
1391
|
+
messageId: issue.messageId,
|
|
1392
|
+
data: { type: tag.value.content },
|
|
1393
|
+
loc: {
|
|
1394
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
1395
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
1396
|
+
},
|
|
1397
|
+
fix: normalizedType ? (fixer) => fixer.replaceTextRange([start, end], normalizedType) : void 0
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
} };
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
function* getOpeningTags(nodes) {
|
|
1404
|
+
for (const node of nodes) {
|
|
1405
|
+
if (node.type !== "custom-container") continue;
|
|
1406
|
+
yield* getOpeningTagsFromContainer(node);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
function* getOpeningTagsFromContainer(container) {
|
|
1410
|
+
for (const child of container.children) if (child.type === "custom-container") yield* getOpeningTagsFromContainer(child);
|
|
1411
|
+
else if (child.type === "open") yield child;
|
|
1412
|
+
}
|
|
1413
|
+
function getTypeIssue(type) {
|
|
1414
|
+
if (isCustomContainerType(type)) return null;
|
|
1415
|
+
const normalizedType = type.toLowerCase();
|
|
1416
|
+
if (isCustomContainerType(normalizedType)) return {
|
|
1417
|
+
messageId: MESSAGE_IDS$1.invalidTypeCase,
|
|
1418
|
+
normalizedType
|
|
1419
|
+
};
|
|
1420
|
+
return { messageId: MESSAGE_IDS$1.invalidType };
|
|
1421
|
+
}
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region rules/valid-heading-anchor/anchor.ts
|
|
1424
|
+
/**
|
|
1425
|
+
* Check if the string has an anchor.
|
|
1426
|
+
* @example: {#chinese-anchor}
|
|
1427
|
+
*/
|
|
1428
|
+
function isStrictAnchor(str) {
|
|
1429
|
+
return /\s\{#[a-z0-9]+(?:-[a-z0-9]+)*\}/.test(str);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Check whether the string contains CJK Han characters.
|
|
1433
|
+
*/
|
|
1434
|
+
function hasChinese(str) {
|
|
1435
|
+
return /\p{Script=Han}/u.test(str);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Normalize raw anchor text into the strict anchor format content.
|
|
1439
|
+
* - lowercase all letters
|
|
1440
|
+
* - convert spaces to `-`
|
|
1441
|
+
* - remove unsupported characters
|
|
1442
|
+
* - trim leading/trailing `-`
|
|
1443
|
+
*/
|
|
1444
|
+
function normalizeAnchor(anchor) {
|
|
1445
|
+
return anchor.toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Count wrapper characters contributed by the trailing like-anchor fragment.
|
|
1449
|
+
* The value is the length difference between the raw matched fragment and the
|
|
1450
|
+
* cleaned anchor text returned by `getLikeAnchor`.
|
|
1451
|
+
* @example `# 中文标题 {#Chinese-Title}` -> 3
|
|
1452
|
+
* @example `## 使用 \`describe\` 编组测试 #Grouping Tests with \`describe\`` -> 1
|
|
1453
|
+
*/
|
|
1454
|
+
function calcAnchorPositionCompensate(content) {
|
|
1455
|
+
const match = getLikeAnchorMatch(content);
|
|
1456
|
+
const anchor = getLikeAnchor(content);
|
|
1457
|
+
if (!match || !anchor) return 0;
|
|
1458
|
+
return match.length - anchor.rawLikeAnchor.length;
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region parser/markdown.ts
|
|
856
1462
|
const language = new MarkdownLanguage({ mode: "gfm" });
|
|
857
1463
|
/**
|
|
858
1464
|
* Parses Markdown with the same GFM language implementation used by the
|
|
859
1465
|
* plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
|
|
860
1466
|
*/
|
|
861
|
-
function parseMarkdown(markdown
|
|
1467
|
+
function parseMarkdown(markdown) {
|
|
862
1468
|
const file = {
|
|
863
1469
|
path: "test.md",
|
|
864
1470
|
physicalPath: "test.md",
|
|
865
1471
|
bom: false,
|
|
866
|
-
body: markdown
|
|
1472
|
+
body: markdown
|
|
867
1473
|
};
|
|
868
1474
|
const parseResult = language.parse(file, { languageOptions: {
|
|
869
1475
|
...language.defaultLanguageOptions,
|
|
@@ -876,90 +1482,87 @@ function parseMarkdown(markdown$1) {
|
|
|
876
1482
|
sourceCode: language.createSourceCode(file, parseResult)
|
|
877
1483
|
};
|
|
878
1484
|
}
|
|
879
|
-
|
|
880
1485
|
//#endregion
|
|
881
|
-
//#region
|
|
1486
|
+
//#region rules/valid-heading-anchor/frontmatter.ts
|
|
882
1487
|
/**
|
|
883
1488
|
* Returns true when the Markdown document starts with YAML frontmatter.
|
|
884
1489
|
*/
|
|
885
|
-
function hasFrontmatter(markdown
|
|
886
|
-
if (prevNode?.type === "thematicBreak") markdown
|
|
887
|
-
const { ast } = parseMarkdown(markdown
|
|
1490
|
+
function hasFrontmatter(markdown, prevNode) {
|
|
1491
|
+
if (prevNode?.type === "thematicBreak") markdown = `---\n${markdown}`;
|
|
1492
|
+
const { ast } = parseMarkdown(markdown);
|
|
888
1493
|
return ast.children[0]?.type === "yaml";
|
|
889
1494
|
}
|
|
890
|
-
|
|
891
1495
|
//#endregion
|
|
892
|
-
//#region
|
|
1496
|
+
//#region rules/valid-heading-anchor/index.ts
|
|
893
1497
|
const RULE_NAME = "valid-heading-anchor";
|
|
894
1498
|
const MESSAGE_IDS = {
|
|
895
1499
|
missingAnchor: "missingAnchor",
|
|
896
1500
|
invalidHeadingAnchor: "invalidHeadingAnchor"
|
|
897
1501
|
};
|
|
898
|
-
var valid_heading_anchor_default = createRule({
|
|
899
|
-
name: RULE_NAME,
|
|
900
|
-
meta: {
|
|
901
|
-
type: "layout",
|
|
902
|
-
docs: { description: "Require strict lowercase anchors for headings that contain CJK text." },
|
|
903
|
-
messages: {
|
|
904
|
-
missingAnchor: "Non-ASCII heading must have an anchor in the format \"{#lowercase-anchor}\".",
|
|
905
|
-
invalidHeadingAnchor: "Anchor must use lowercase letters and valid characters only."
|
|
906
|
-
},
|
|
907
|
-
fixable: "whitespace",
|
|
908
|
-
schema: []
|
|
909
|
-
},
|
|
910
|
-
defaultOptions: [],
|
|
911
|
-
create(context) {
|
|
912
|
-
return { heading(node) {
|
|
913
|
-
const { position, start, end } = getNodePosition(node);
|
|
914
|
-
/* v8 ignore if -- @preserve */
|
|
915
|
-
if (!position) return;
|
|
916
|
-
const source = context.sourceCode.text.slice(start, end);
|
|
917
|
-
if (isStrictAnchor(source) || !hasChinese(source)) return;
|
|
918
|
-
if (hasFrontmatter(source, getNodeContext(context, node).prev)) return;
|
|
919
|
-
const liked = getLikeAnchor(source);
|
|
920
|
-
if (!liked) {
|
|
921
|
-
context.report({
|
|
922
|
-
node,
|
|
923
|
-
messageId: MESSAGE_IDS.missingAnchor
|
|
924
|
-
});
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
const { rawLikeAnchor, isLikeAnchor } = liked;
|
|
928
|
-
const compensate = calcAnchorPositionCompensate(source);
|
|
929
|
-
const remainingContent = source.slice(0, -rawLikeAnchor.length - compensate).trim();
|
|
930
|
-
const anchor = normalizeAnchor(rawLikeAnchor);
|
|
931
|
-
if (rawLikeAnchor === anchor) return;
|
|
932
|
-
context.report({
|
|
933
|
-
node,
|
|
934
|
-
messageId: isLikeAnchor ? MESSAGE_IDS.missingAnchor : MESSAGE_IDS.invalidHeadingAnchor,
|
|
935
|
-
fix(fixer) {
|
|
936
|
-
return fixer.replaceTextRange([start, end], `${remainingContent} {#${anchor}}`);
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
} };
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
|
|
943
1502
|
//#endregion
|
|
944
|
-
//#region
|
|
1503
|
+
//#region rules/index.ts
|
|
945
1504
|
const rules = {
|
|
1505
|
+
"padding-around-custom-container": padding_around_custom_container_default,
|
|
946
1506
|
"space-around-inline-element": space_around_inline_element_default,
|
|
947
1507
|
"space-around-number": space_around_number_default,
|
|
948
1508
|
"space-around-word": space_around_word_default,
|
|
949
|
-
"
|
|
1509
|
+
"space-around-custom-container": space_around_custom_container_default,
|
|
1510
|
+
"valid-custom-container-type": valid_custom_container_type_default,
|
|
1511
|
+
"valid-heading-anchor": createRule({
|
|
1512
|
+
name: RULE_NAME,
|
|
1513
|
+
meta: {
|
|
1514
|
+
type: "layout",
|
|
1515
|
+
docs: {
|
|
1516
|
+
description: "Require strict lowercase anchors for headings that contain CJK text.",
|
|
1517
|
+
recommended: true
|
|
1518
|
+
},
|
|
1519
|
+
messages: {
|
|
1520
|
+
missingAnchor: "Non-ASCII heading must have an anchor in the format \"{#lowercase-anchor}\".",
|
|
1521
|
+
invalidHeadingAnchor: "Anchor must use lowercase letters and valid characters only."
|
|
1522
|
+
},
|
|
1523
|
+
fixable: "whitespace",
|
|
1524
|
+
schema: []
|
|
1525
|
+
},
|
|
1526
|
+
defaultOptions: [],
|
|
1527
|
+
create(context) {
|
|
1528
|
+
return { heading(node) {
|
|
1529
|
+
const { position, start, end } = getNodePosition(node);
|
|
1530
|
+
/* v8 ignore if -- @preserve */
|
|
1531
|
+
if (!position) return;
|
|
1532
|
+
const source = context.sourceCode.text.slice(start, end);
|
|
1533
|
+
if (isStrictAnchor(source) || !hasChinese(source)) return;
|
|
1534
|
+
if (hasFrontmatter(source, getNodeContext(context, node).prev)) return;
|
|
1535
|
+
const liked = getLikeAnchor(source);
|
|
1536
|
+
if (!liked) {
|
|
1537
|
+
context.report({
|
|
1538
|
+
node,
|
|
1539
|
+
messageId: MESSAGE_IDS.missingAnchor
|
|
1540
|
+
});
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const { rawLikeAnchor, isLikeAnchor } = liked;
|
|
1544
|
+
const compensate = calcAnchorPositionCompensate(source);
|
|
1545
|
+
const fixed = `${source.slice(0, -rawLikeAnchor.length - compensate).trim()} {#${normalizeAnchor(rawLikeAnchor)}}`;
|
|
1546
|
+
if (source === fixed) return;
|
|
1547
|
+
context.report({
|
|
1548
|
+
node,
|
|
1549
|
+
messageId: isLikeAnchor ? MESSAGE_IDS.missingAnchor : MESSAGE_IDS.invalidHeadingAnchor,
|
|
1550
|
+
fix(fixer) {
|
|
1551
|
+
return fixer.replaceTextRange([start, end], fixed);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
} };
|
|
1555
|
+
}
|
|
1556
|
+
})
|
|
950
1557
|
};
|
|
951
|
-
|
|
952
1558
|
//#endregion
|
|
953
|
-
//#region
|
|
1559
|
+
//#region index.ts
|
|
954
1560
|
const plugin = {
|
|
955
1561
|
rules,
|
|
956
1562
|
processors: markdown.processors,
|
|
957
1563
|
languages: { gfm: new MarkdownLanguage({ mode: "gfm" }) }
|
|
958
1564
|
};
|
|
959
|
-
const recommendedRules = {
|
|
960
|
-
"md-style/valid-heading-anchor": "error",
|
|
961
|
-
"md-style/space-around-inline-element": "error"
|
|
962
|
-
};
|
|
1565
|
+
const recommendedRules = Object.fromEntries(Object.entries(rules).filter(([, rule]) => rule.meta?.docs?.recommended).map(([ruleName]) => [`md-style/${ruleName}`, "error"]));
|
|
963
1566
|
const allRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]));
|
|
964
1567
|
const configs = {
|
|
965
1568
|
recommended: {
|
|
@@ -980,7 +1583,5 @@ const configs = {
|
|
|
980
1583
|
}
|
|
981
1584
|
};
|
|
982
1585
|
const mdStylePlugin = Object.assign(plugin, { configs });
|
|
983
|
-
var src_default = mdStylePlugin;
|
|
984
|
-
|
|
985
1586
|
//#endregion
|
|
986
|
-
export { configs,
|
|
1587
|
+
export { configs, mdStylePlugin as default, plugin };
|