eslint-plugin-md-style 0.2.0 → 0.3.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 +3 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +841 -242
- package/package.json +16 -16
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 src/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
|
|
15
|
+
function isCodeNode(node) {
|
|
16
|
+
return node?.type === "code";
|
|
27
17
|
}
|
|
28
|
-
|
|
29
|
-
"
|
|
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);
|
|
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,587 @@ function getAdjacentChar(str, position) {
|
|
|
114
86
|
str = str.trim();
|
|
115
87
|
return position === "head" ? str[0] : str[str.length - 1];
|
|
116
88
|
}
|
|
117
|
-
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/parser/custom-container/parse.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 src/utils/index.ts
|
|
348
|
+
function createRule({ create, defaultOptions, meta }) {
|
|
349
|
+
return {
|
|
350
|
+
create,
|
|
351
|
+
meta: {
|
|
352
|
+
defaultOptions,
|
|
353
|
+
...meta
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/rules/padding-around-custom-container/analyzs.ts
|
|
359
|
+
/**
|
|
360
|
+
* Analyzes custom containers and returns de-duplicated padding issues.
|
|
361
|
+
*/
|
|
362
|
+
function getNodesIssues(nodes, opts) {
|
|
363
|
+
const { offset = 0, ignoreRanges = [], mode = "loose" } = opts || {};
|
|
364
|
+
const issues = [];
|
|
365
|
+
/**
|
|
366
|
+
* Recursively analyzes every custom-container level in a parsed document.
|
|
367
|
+
*/
|
|
368
|
+
function analyzeLevel(nodes, opts) {
|
|
369
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
370
|
+
const container = nodes[index];
|
|
371
|
+
if (!isCustomContainerNode(container)) continue;
|
|
372
|
+
const boundary = getContainerBoundary(container);
|
|
373
|
+
if (!boundary) continue;
|
|
374
|
+
analyzeInnerBoundary(container.children, boundary, opts);
|
|
375
|
+
analyzeOuterBoundary(nodes, index, opts);
|
|
376
|
+
analyzeLevel(container.children, opts);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
analyzeLevel(nodes, {
|
|
380
|
+
offset,
|
|
381
|
+
issues,
|
|
382
|
+
mode
|
|
383
|
+
});
|
|
384
|
+
return dedupeIssues(issues).filter((issue) => !ignoreRanges.some((range) => issue.start < range.end && issue.end > range.start));
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Checks the blank lines immediately inside a container's opening and closing tags.
|
|
388
|
+
*/
|
|
389
|
+
function analyzeInnerBoundary(children, containerBoundary, opts) {
|
|
390
|
+
const { openIndex, closeIndex } = containerBoundary;
|
|
391
|
+
const first = children[openIndex + 1];
|
|
392
|
+
checkInnerSide(first, {
|
|
393
|
+
direction: 1,
|
|
394
|
+
children,
|
|
395
|
+
...opts
|
|
396
|
+
});
|
|
397
|
+
const last = children[closeIndex - 1];
|
|
398
|
+
if (last !== first) checkInnerSide(last, {
|
|
399
|
+
direction: -1,
|
|
400
|
+
children,
|
|
401
|
+
...opts
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Checks one side of a container's inner boundary for the selected mode.
|
|
406
|
+
*/
|
|
407
|
+
function checkInnerSide(node, opts) {
|
|
408
|
+
const { direction, children, offset } = opts;
|
|
409
|
+
if (isBlankNode(node)) {
|
|
410
|
+
const lineBreak = getLineBreak(node.value);
|
|
411
|
+
const expected = opts.mode === "loose" ? `${lineBreak}${lineBreak}` : lineBreak;
|
|
412
|
+
if (node.value !== expected) opts.issues.push({
|
|
413
|
+
start: offset + node.position.start,
|
|
414
|
+
end: offset + node.position.end,
|
|
415
|
+
replacement: expected,
|
|
416
|
+
messageId: node.value.length < expected.length ? MESSAGE_IDS$4.missing : MESSAGE_IDS$4.unexpected
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (opts.mode === "loose" && node && !isOpenNode(node) && !isCloseNode(node)) {
|
|
421
|
+
const insertion = direction === 1 ? node.position.start : node.position.end;
|
|
422
|
+
opts.issues.push({
|
|
423
|
+
start: offset + insertion,
|
|
424
|
+
end: offset + insertion,
|
|
425
|
+
replacement: getLineBreakFromChildren(children) + getLineBreakFromChildren(children),
|
|
426
|
+
messageId: MESSAGE_IDS$4.missing
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Finds the first opening tag and last closing tag for a custom container.
|
|
432
|
+
*/
|
|
433
|
+
function getContainerBoundary(container) {
|
|
434
|
+
const openIndex = container.children.findIndex(isOpenNode);
|
|
435
|
+
const closeIndex = container.children.findLastIndex(isCloseNode);
|
|
436
|
+
if (openIndex === -1 || closeIndex <= openIndex) return;
|
|
437
|
+
return {
|
|
438
|
+
openIndex,
|
|
439
|
+
closeIndex
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Checks both sides of a container for the required external blank line.
|
|
444
|
+
*/
|
|
445
|
+
function analyzeOuterBoundary(children, boundaryIndex, opts) {
|
|
446
|
+
checkOuterSide(children, {
|
|
447
|
+
direction: -1,
|
|
448
|
+
boundaryIndex,
|
|
449
|
+
...opts
|
|
450
|
+
});
|
|
451
|
+
checkOuterSide(children, {
|
|
452
|
+
direction: 1,
|
|
453
|
+
boundaryIndex,
|
|
454
|
+
...opts
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Checks one side of a container and records insertion or normalization issues.
|
|
459
|
+
*/
|
|
460
|
+
function checkOuterSide(children, opts) {
|
|
461
|
+
const { boundaryIndex, direction, offset, issues } = opts;
|
|
462
|
+
const blankIndex = boundaryIndex + direction;
|
|
463
|
+
const blank = children[blankIndex];
|
|
464
|
+
const contentIndex = isBlankNode(blank) ? blankIndex + direction : blankIndex;
|
|
465
|
+
const content = children[contentIndex];
|
|
466
|
+
if (!isExternalContent(content, {
|
|
467
|
+
index: contentIndex,
|
|
468
|
+
length: children.length
|
|
469
|
+
})) return;
|
|
470
|
+
if (!isBlankNode(blank)) {
|
|
471
|
+
const insertion = direction === -1 ? children[boundaryIndex].position.start : children[boundaryIndex].position.end;
|
|
472
|
+
issues.push({
|
|
473
|
+
start: offset + insertion,
|
|
474
|
+
end: offset + insertion,
|
|
475
|
+
replacement: getLineBreakFromChildren(children),
|
|
476
|
+
messageId: MESSAGE_IDS$4.missing
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const lineBreak = getLineBreak(blank.value);
|
|
481
|
+
const expected = `${lineBreak}${lineBreak}`;
|
|
482
|
+
if (blank.value === expected) return;
|
|
483
|
+
issues.push({
|
|
484
|
+
start: offset + blank.position.start,
|
|
485
|
+
end: offset + blank.position.end,
|
|
486
|
+
replacement: expected,
|
|
487
|
+
messageId: blank.value.length < expected.length ? MESSAGE_IDS$4.missing : MESSAGE_IDS$4.unexpected
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Returns whether a node is content outside the container's own boundary tags.
|
|
492
|
+
*/
|
|
493
|
+
function isExternalContent(node, opts) {
|
|
494
|
+
if (!node || isBlankNode(node)) return false;
|
|
495
|
+
const { index, length } = opts;
|
|
496
|
+
if (index === 0 && isOpenNode(node)) return false;
|
|
497
|
+
if (index === length - 1 && isCloseNode(node)) return false;
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Removes issues that target the same range with the same replacement.
|
|
502
|
+
*/
|
|
503
|
+
function dedupeIssues(issues) {
|
|
504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
505
|
+
return issues.filter((edit) => {
|
|
506
|
+
const key = `${edit.start}:${edit.end}:${edit.replacement}`;
|
|
507
|
+
if (seen.has(key)) return false;
|
|
508
|
+
seen.add(key);
|
|
509
|
+
return true;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Returns the line-break sequence used by a string.
|
|
514
|
+
*/
|
|
515
|
+
function getLineBreak(value) {
|
|
516
|
+
return value.includes("\r\n") ? "\r\n" : "\n";
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Uses the first blank child to infer a container's line-break sequence.
|
|
520
|
+
*/
|
|
521
|
+
function getLineBreakFromChildren(children) {
|
|
522
|
+
const blank = children.find((child) => isBlankNode(child));
|
|
523
|
+
return isBlankNode(blank) ? getLineBreak(blank.value) : "\n";
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/rules/padding-around-custom-container/index.ts
|
|
527
|
+
const RULE_NAME$6 = "padding-around-custom-container";
|
|
528
|
+
const MESSAGE_IDS$4 = {
|
|
529
|
+
missing: "missing",
|
|
530
|
+
unexpected: "unexpected"
|
|
531
|
+
};
|
|
532
|
+
var padding_around_custom_container_default = createRule({
|
|
533
|
+
name: RULE_NAME$6,
|
|
534
|
+
meta: {
|
|
535
|
+
type: "layout",
|
|
536
|
+
docs: { description: "Enforce padding around VitePress custom containers." },
|
|
537
|
+
messages: {
|
|
538
|
+
missing: "A custom container must be separated from surrounding content by one blank line.",
|
|
539
|
+
unexpected: "Unexpected blank lines around a custom container."
|
|
540
|
+
},
|
|
541
|
+
fixable: "whitespace",
|
|
542
|
+
schema: [{ enum: ["compact", "loose"] }]
|
|
543
|
+
},
|
|
544
|
+
defaultOptions: ["loose"],
|
|
545
|
+
create(context) {
|
|
546
|
+
return { root(node) {
|
|
547
|
+
const { position, start } = getNodePosition(node);
|
|
548
|
+
/* v8 ignore if -- @preserve */
|
|
549
|
+
if (!position) return;
|
|
550
|
+
const source = context.sourceCode.text;
|
|
551
|
+
const ignoreRanges = getCodeNodeRanges(node);
|
|
552
|
+
const nodes = parseCustomContainers(source.slice(start));
|
|
553
|
+
const [mode] = context.options;
|
|
554
|
+
const issues = getNodesIssues(nodes, {
|
|
555
|
+
offset: start,
|
|
556
|
+
ignoreRanges,
|
|
557
|
+
mode
|
|
558
|
+
});
|
|
559
|
+
for (const { start, end, messageId, replacement } of issues) context.report({
|
|
560
|
+
node,
|
|
561
|
+
messageId,
|
|
562
|
+
loc: {
|
|
563
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
564
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
565
|
+
},
|
|
566
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], replacement)
|
|
567
|
+
});
|
|
568
|
+
} };
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/rules/space-around-custom-container/analyzs.ts
|
|
573
|
+
/**
|
|
574
|
+
* Finds custom-container marker lines and normalizes their surrounding spaces.
|
|
575
|
+
*
|
|
576
|
+
* Opening markers use exactly one space between the fence and the type,
|
|
577
|
+
* while closing markers contain only the fence. Leading indentation is removed.
|
|
578
|
+
* Ranges in `ignoredRanges` (for example fenced code blocks) are skipped.
|
|
579
|
+
*/
|
|
580
|
+
function getSourceIssues(source, ignoredRanges = []) {
|
|
581
|
+
const issues = [];
|
|
582
|
+
function visit(nodes) {
|
|
583
|
+
for (const node of nodes) {
|
|
584
|
+
if (!isCustomContainerNode(node)) continue;
|
|
585
|
+
for (const child of node.children) if (isOpenNode(child) || isCloseNode(child)) analyzeMarker(child, ignoredRanges, issues);
|
|
586
|
+
else if (isCustomContainerNode(child)) visit([child]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
visit(parseCustomContainers(source));
|
|
590
|
+
return issues;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Adds a spacing issue for one parsed opening or closing marker.
|
|
594
|
+
*/
|
|
595
|
+
function analyzeMarker(tag, ignoredRanges, issues) {
|
|
596
|
+
if (ignoredRanges.some((range) => tag.position.start < range.end && tag.position.end > range.start)) return;
|
|
597
|
+
const replacement = normalizedMarker(tag);
|
|
598
|
+
if (tag.raw === replacement) return;
|
|
599
|
+
issues.push({
|
|
600
|
+
...tag.position,
|
|
601
|
+
replacement,
|
|
602
|
+
messageId: getMarkerMessageId(tag)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Returns the normalized source text for a parsed container marker.
|
|
607
|
+
*/
|
|
608
|
+
function normalizedMarker(tag) {
|
|
609
|
+
const fence = ":".repeat(tag.markerLength);
|
|
610
|
+
if (isCloseNode(tag)) return fence;
|
|
611
|
+
const valueStart = tag.value.start - tag.position.start;
|
|
612
|
+
return `${fence} ${tag.raw.slice(valueStart).trim()}`;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Classifies the spacing problem represented by a parsed marker.
|
|
616
|
+
*/
|
|
617
|
+
function getMarkerMessageId(tag) {
|
|
618
|
+
if (tag.raw.length !== tag.raw.trimStart().length) return MESSAGE_IDS$3.unexpectedIndentation;
|
|
619
|
+
if (isCloseNode(tag)) return MESSAGE_IDS$3.unexpectedTrailingSpace;
|
|
620
|
+
if (tag.raw.slice(tag.markerLength, tag.value.start - tag.position.start).length === 0) return MESSAGE_IDS$3.missingSeparator;
|
|
621
|
+
return MESSAGE_IDS$3.unexpectedSeparator;
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/rules/space-around-custom-container/index.ts
|
|
625
|
+
const RULE_NAME$5 = "space-around-custom-container";
|
|
626
|
+
const MESSAGE_IDS$3 = {
|
|
627
|
+
unexpectedIndentation: "unexpectedIndentation",
|
|
628
|
+
missingSeparator: "missingSeparator",
|
|
629
|
+
unexpectedSeparator: "unexpectedSeparator",
|
|
630
|
+
unexpectedTrailingSpace: "unexpectedTrailingSpace"
|
|
631
|
+
};
|
|
632
|
+
var space_around_custom_container_default = createRule({
|
|
633
|
+
name: RULE_NAME$5,
|
|
634
|
+
meta: {
|
|
635
|
+
type: "layout",
|
|
636
|
+
docs: { description: "Enforce spacing around VitePress custom-container markers." },
|
|
637
|
+
messages: {
|
|
638
|
+
unexpectedIndentation: "Do not indent a custom-container marker.",
|
|
639
|
+
missingSeparator: "Add one space between the opening fence and its type.",
|
|
640
|
+
unexpectedSeparator: "Use exactly one space between the opening fence and its type.",
|
|
641
|
+
unexpectedTrailingSpace: "Remove trailing spaces from a custom-container closing marker."
|
|
642
|
+
},
|
|
643
|
+
fixable: "whitespace",
|
|
644
|
+
schema: []
|
|
645
|
+
},
|
|
646
|
+
defaultOptions: [],
|
|
647
|
+
create(context) {
|
|
648
|
+
return { root(node) {
|
|
649
|
+
const { position } = getNodePosition(node);
|
|
650
|
+
/* v8 ignore if -- @preserve */
|
|
651
|
+
if (!position) return;
|
|
652
|
+
const source = context.sourceCode.text;
|
|
653
|
+
const issues = getSourceIssues(source, getCodeNodeRanges(node));
|
|
654
|
+
for (const { start, end, messageId, replacement } of issues) context.report({
|
|
655
|
+
node,
|
|
656
|
+
messageId,
|
|
657
|
+
loc: {
|
|
658
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
659
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
660
|
+
},
|
|
661
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], replacement)
|
|
662
|
+
});
|
|
663
|
+
} };
|
|
664
|
+
}
|
|
665
|
+
});
|
|
118
666
|
//#endregion
|
|
119
667
|
//#region src/utils/anchor.ts
|
|
120
668
|
/**
|
|
121
|
-
* Match the trailing anchor-like fragment from a heading
|
|
669
|
+
* Match the trailing anchor-like fragment from a heading or adjacent text.
|
|
122
670
|
* @example `中文标题 {#Chinese-Title}` -> `{#Chinese-Title}`
|
|
123
671
|
* @example `使用 describe #Grouping Tests` -> `#Grouping Tests`
|
|
124
672
|
*/
|
|
@@ -138,52 +686,15 @@ function getLikeAnchor(str) {
|
|
|
138
686
|
if (str === void 0) return null;
|
|
139
687
|
const match = getLikeAnchorMatch(str);
|
|
140
688
|
if (!match) return null;
|
|
141
|
-
const rawLikeAnchor = match.replace(/
|
|
689
|
+
const rawLikeAnchor = match.replace(/[{}]/g, "").replace(/^#/, "").trimStart();
|
|
142
690
|
return {
|
|
143
691
|
isLikeAnchor: rawLikeAnchor.includes(" "),
|
|
144
692
|
rawLikeAnchor
|
|
145
693
|
};
|
|
146
694
|
}
|
|
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
695
|
//#endregion
|
|
185
696
|
//#region src/utils/punctuation.ts
|
|
186
|
-
const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
697
|
+
const OPENING_PAIRED_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
187
698
|
"(",
|
|
188
699
|
"[",
|
|
189
700
|
"{",
|
|
@@ -194,7 +705,7 @@ const OPENING_PAIRED_PUNCTUATION = new Set([
|
|
|
194
705
|
"“",
|
|
195
706
|
"‘"
|
|
196
707
|
]);
|
|
197
|
-
const CLOSING_PAIRED_PUNCTUATION = new Set([
|
|
708
|
+
const CLOSING_PAIRED_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
198
709
|
")",
|
|
199
710
|
"]",
|
|
200
711
|
"}",
|
|
@@ -262,17 +773,8 @@ function hasPunctuation(str, position = "head") {
|
|
|
262
773
|
if (position === "head") return isPunctuation(str[0]);
|
|
263
774
|
else return isPunctuation(str[str.length - 1]);
|
|
264
775
|
}
|
|
265
|
-
|
|
266
776
|
//#endregion
|
|
267
777
|
//#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
778
|
/**
|
|
277
779
|
* Gets the count and range of consecutive whitespace at the start or end of a string.
|
|
278
780
|
* @example ` text`, `head` -> { count: 2, start: 0, end: 2 }
|
|
@@ -303,45 +805,8 @@ function getWhiteSpace(str, position = "head") {
|
|
|
303
805
|
};
|
|
304
806
|
}
|
|
305
807
|
}
|
|
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
808
|
//#endregion
|
|
330
|
-
//#region src/
|
|
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
|
-
}
|
|
809
|
+
//#region src/rules/space-around-inline-element/analyze.ts
|
|
345
810
|
/**
|
|
346
811
|
* Validates whether a spacing run contains exactly one required space.
|
|
347
812
|
*/
|
|
@@ -355,18 +820,18 @@ function validateSingleRequiredSpace(count, missingSpaceMessageId, multipleSpace
|
|
|
355
820
|
function validateBeforePunctuation(context) {
|
|
356
821
|
const adjacentChar = getAdjacentChar(context.value, "tail");
|
|
357
822
|
if (OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isSlashPunctuation(adjacentChar)) {
|
|
358
|
-
if (context.whiteSpace.count > 0) return
|
|
823
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-before";
|
|
359
824
|
return;
|
|
360
825
|
}
|
|
361
|
-
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
362
|
-
if (context.whiteSpace.count > 0) return
|
|
826
|
+
if (context.punctuationType === "half") return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-before", "multiple-spaces-after-punctuation");
|
|
827
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-before";
|
|
363
828
|
}
|
|
364
829
|
/**
|
|
365
830
|
* Validates the spacing between the previous node and the current inline node.
|
|
366
831
|
*/
|
|
367
832
|
function validateSpaceBeforeNode(context) {
|
|
368
833
|
if (context.hasPunctuation) return validateBeforePunctuation(context);
|
|
369
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
834
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-before", "multiple-spaces-before");
|
|
370
835
|
}
|
|
371
836
|
/**
|
|
372
837
|
* Validates spacing after an inline node when the next character is punctuation.
|
|
@@ -374,16 +839,55 @@ function validateSpaceBeforeNode(context) {
|
|
|
374
839
|
function validateSpaceAfterPunctuation(context) {
|
|
375
840
|
const adjacentChar = getAdjacentChar(context.value, "head");
|
|
376
841
|
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
|
|
842
|
+
if (CLOSING_PAIRED_PUNCTUATION.has(adjacentChar || "") && context.whiteSpace.count > 0) return "unexpected-space-after";
|
|
843
|
+
if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-after", "multiple-spaces-after");
|
|
844
|
+
if (context.whiteSpace.count > 0) return "unexpected-space-after";
|
|
380
845
|
}
|
|
381
846
|
/**
|
|
382
847
|
* Validates the spacing between the current inline node and the next node.
|
|
383
848
|
*/
|
|
384
849
|
function validateSpaceAfterNode(context) {
|
|
385
850
|
if (context.hasPunctuation) return validateSpaceAfterPunctuation(context);
|
|
386
|
-
return validateSingleRequiredSpace(context.whiteSpace.count,
|
|
851
|
+
return validateSingleRequiredSpace(context.whiteSpace.count, "missing-space-after", "multiple-spaces-after");
|
|
852
|
+
}
|
|
853
|
+
const INLINE_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
854
|
+
"link",
|
|
855
|
+
"image",
|
|
856
|
+
"inlineCode",
|
|
857
|
+
"emphasis",
|
|
858
|
+
"strong"
|
|
859
|
+
]);
|
|
860
|
+
/**
|
|
861
|
+
* Checks whether a phrasing node is one of the selected inline element targets.
|
|
862
|
+
*/
|
|
863
|
+
function isInlineElement(node) {
|
|
864
|
+
return !!node && INLINE_ELEMENT_TYPES.has(node.type);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Checks whether the current inline element is nested inside another selected inline element.
|
|
868
|
+
*/
|
|
869
|
+
function isNestedInlineElement(nodeContext) {
|
|
870
|
+
const { parent } = nodeContext;
|
|
871
|
+
return isInlineElement(parent);
|
|
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
930
|
//#region src/rules/space-around-inline-element/index.ts
|
|
428
|
-
const RULE_NAME$
|
|
429
|
-
const MESSAGE_IDS$
|
|
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,14 +938,23 @@ 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
960
|
docs: { description: "Enforce spacing around Markdown inline elements." },
|
|
@@ -489,11 +1001,12 @@ function checkInlineElement(context, node) {
|
|
|
489
1001
|
const nodeContext = getNodeContext(context, node);
|
|
490
1002
|
if (isNestedInlineElement(nodeContext)) return;
|
|
491
1003
|
const spaceContext = getSpaceContext(nodeContext);
|
|
492
|
-
const
|
|
493
|
-
if (!
|
|
1004
|
+
const issue = validateSpace(nodeContext);
|
|
1005
|
+
if (!issue) return;
|
|
1006
|
+
const messageId = ISSUE_TO_MESSAGE_ID[issue];
|
|
494
1007
|
if (BEFORE_INLINE_ELEMENT_MESSAGE_IDS.has(messageId) && spaceContext.prev) {
|
|
495
1008
|
const { count } = spaceContext.prev.whiteSpace;
|
|
496
|
-
const replaceText = messageId === MESSAGE_IDS$
|
|
1009
|
+
const replaceText = messageId === MESSAGE_IDS$2.unexpectedSpaceBefore ? "" : " ";
|
|
497
1010
|
context.report({
|
|
498
1011
|
node,
|
|
499
1012
|
messageId,
|
|
@@ -505,7 +1018,7 @@ function checkInlineElement(context, node) {
|
|
|
505
1018
|
}
|
|
506
1019
|
if (spaceContext.next) {
|
|
507
1020
|
const { count } = spaceContext.next.whiteSpace;
|
|
508
|
-
const replaceText = messageId === MESSAGE_IDS$
|
|
1021
|
+
const replaceText = messageId === MESSAGE_IDS$2.unexpectedSpaceAfter ? "" : " ";
|
|
509
1022
|
context.report({
|
|
510
1023
|
node,
|
|
511
1024
|
messageId,
|
|
@@ -515,7 +1028,6 @@ function checkInlineElement(context, node) {
|
|
|
515
1028
|
});
|
|
516
1029
|
}
|
|
517
1030
|
}
|
|
518
|
-
|
|
519
1031
|
//#endregion
|
|
520
1032
|
//#region src/utils/text/tokenizer.ts
|
|
521
1033
|
const TEXT_TYPE = {
|
|
@@ -539,7 +1051,7 @@ const SYMBOL_RE = /^\p{Symbol}$/u;
|
|
|
539
1051
|
const EMOJI_RE = /^\p{Extended_Pictographic}$/u;
|
|
540
1052
|
const SPACE_RE = /^[\t\v\f \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]$/u;
|
|
541
1053
|
const NEWLINE_RE = /^[\n\r\u2028\u2029]$/u;
|
|
542
|
-
const INVISIBLE_CODE_POINTS = new Set([
|
|
1054
|
+
const INVISIBLE_CODE_POINTS = /* @__PURE__ */ new Set([
|
|
543
1055
|
173,
|
|
544
1056
|
847,
|
|
545
1057
|
1564,
|
|
@@ -560,15 +1072,13 @@ function isInvisible(char) {
|
|
|
560
1072
|
const codePoint = char.codePointAt(0);
|
|
561
1073
|
return codePoint != null && (INVISIBLE_CODE_POINTS.has(codePoint) || codePoint >= 8203 && codePoint <= 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8288 && codePoint <= 8303);
|
|
562
1074
|
}
|
|
563
|
-
function isLatinWordType(type) {
|
|
564
|
-
return type === TEXT_TYPE.latin;
|
|
565
|
-
}
|
|
566
|
-
function isNumberType(type) {
|
|
567
|
-
return type === TEXT_TYPE.number;
|
|
568
|
-
}
|
|
569
1075
|
function isNumber(char, prev) {
|
|
570
1076
|
return NUMBER_RE.test(char) || prev?.type === TEXT_TYPE.number && (char === "." || char === "%");
|
|
571
1077
|
}
|
|
1078
|
+
function isLatinWordType(type) {
|
|
1079
|
+
if (!type) return false;
|
|
1080
|
+
return type === "latin";
|
|
1081
|
+
}
|
|
572
1082
|
const TEXT_TYPE_MATCHERS = [
|
|
573
1083
|
{
|
|
574
1084
|
type: TEXT_TYPE.newline,
|
|
@@ -621,15 +1131,16 @@ const DEFAULT_START_POINT = {
|
|
|
621
1131
|
offset: 0
|
|
622
1132
|
};
|
|
623
1133
|
function advancePoint(point, char) {
|
|
1134
|
+
const { line, column, offset = 0 } = point;
|
|
624
1135
|
if (NEWLINE_RE.test(char)) return {
|
|
625
|
-
line:
|
|
1136
|
+
line: line + 1,
|
|
626
1137
|
column: 1,
|
|
627
|
-
offset:
|
|
1138
|
+
offset: offset + char.length
|
|
628
1139
|
};
|
|
629
1140
|
return {
|
|
630
|
-
line
|
|
631
|
-
column:
|
|
632
|
-
offset:
|
|
1141
|
+
line,
|
|
1142
|
+
column: column + char.length,
|
|
1143
|
+
offset: offset + char.length
|
|
633
1144
|
};
|
|
634
1145
|
}
|
|
635
1146
|
/**
|
|
@@ -694,9 +1205,8 @@ function buildTextNodeAst(node) {
|
|
|
694
1205
|
node
|
|
695
1206
|
};
|
|
696
1207
|
}
|
|
697
|
-
|
|
698
1208
|
//#endregion
|
|
699
|
-
//#region src/
|
|
1209
|
+
//#region src/rules/shared/text-boundary-spacing.ts
|
|
700
1210
|
/**
|
|
701
1211
|
* Normalizes an existing space token around the target token type.
|
|
702
1212
|
* When multiple spaces are collapsed, the redundant side is recorded so the
|
|
@@ -740,12 +1250,12 @@ function processTargetToken(ctx, result) {
|
|
|
740
1250
|
* collected missing or redundant space flags.
|
|
741
1251
|
*/
|
|
742
1252
|
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
|
|
1253
|
+
if (boundary.missingBefore && boundary.missingAfter) return "missingSpacesAround";
|
|
1254
|
+
if (boundary.unexpectedBefore && boundary.unexpectedAfter) return "unexpectedSpaceAround";
|
|
1255
|
+
if (boundary.missingBefore) return "missingSpaceBefore";
|
|
1256
|
+
if (boundary.missingAfter) return "missingSpaceAfter";
|
|
1257
|
+
if (boundary.unexpectedBefore) return "unexpectedSpaceBefore";
|
|
1258
|
+
return "unexpectedSpaceAfter";
|
|
749
1259
|
}
|
|
750
1260
|
/**
|
|
751
1261
|
* Rebuilds a text node with normalized spacing between CJK text and a target
|
|
@@ -770,12 +1280,8 @@ function fixBoundarySpace(node, isTargetType) {
|
|
|
770
1280
|
}
|
|
771
1281
|
return result;
|
|
772
1282
|
}
|
|
773
|
-
|
|
774
|
-
//#endregion
|
|
775
|
-
//#region src/rules/space-around-number/index.ts
|
|
776
|
-
const RULE_NAME$2 = "space-around-number";
|
|
777
1283
|
var space_around_number_default = createRule({
|
|
778
|
-
name:
|
|
1284
|
+
name: "space-around-number",
|
|
779
1285
|
meta: {
|
|
780
1286
|
type: "layout",
|
|
781
1287
|
docs: { description: "Enforce a single space between CJK characters and numbers." },
|
|
@@ -793,7 +1299,7 @@ var space_around_number_default = createRule({
|
|
|
793
1299
|
defaultOptions: [],
|
|
794
1300
|
create(context) {
|
|
795
1301
|
return { text(node) {
|
|
796
|
-
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node,
|
|
1302
|
+
const { fixed, missingBefore, missingAfter, unexpectedBefore, unexpectedAfter } = fixBoundarySpace(node, (type) => type === "number");
|
|
797
1303
|
if (fixed === node.value) return;
|
|
798
1304
|
context.report({
|
|
799
1305
|
node,
|
|
@@ -803,19 +1309,13 @@ var space_around_number_default = createRule({
|
|
|
803
1309
|
unexpectedBefore,
|
|
804
1310
|
unexpectedAfter
|
|
805
1311
|
}),
|
|
806
|
-
fix(fixer)
|
|
807
|
-
return fixer.replaceText(node, fixed);
|
|
808
|
-
}
|
|
1312
|
+
fix: (fixer) => fixer.replaceText(node, fixed)
|
|
809
1313
|
});
|
|
810
1314
|
} };
|
|
811
1315
|
}
|
|
812
1316
|
});
|
|
813
|
-
|
|
814
|
-
//#endregion
|
|
815
|
-
//#region src/rules/space-around-word/index.ts
|
|
816
|
-
const RULE_NAME$1 = "space-around-word";
|
|
817
1317
|
var space_around_word_default = createRule({
|
|
818
|
-
name:
|
|
1318
|
+
name: "space-around-word",
|
|
819
1319
|
meta: {
|
|
820
1320
|
type: "layout",
|
|
821
1321
|
docs: { description: "Enforce a single space between CJK characters and Latin words." },
|
|
@@ -850,20 +1350,121 @@ var space_around_word_default = createRule({
|
|
|
850
1350
|
} };
|
|
851
1351
|
}
|
|
852
1352
|
});
|
|
853
|
-
|
|
854
1353
|
//#endregion
|
|
855
|
-
//#region src/
|
|
1354
|
+
//#region src/rules/valid-custom-container-type/index.ts
|
|
1355
|
+
const RULE_NAME$1 = "valid-custom-container-type";
|
|
1356
|
+
const MESSAGE_IDS$1 = {
|
|
1357
|
+
invalidType: "invalidType",
|
|
1358
|
+
invalidTypeCase: "invalidTypeCase"
|
|
1359
|
+
};
|
|
1360
|
+
var valid_custom_container_type_default = createRule({
|
|
1361
|
+
name: RULE_NAME$1,
|
|
1362
|
+
meta: {
|
|
1363
|
+
type: "problem",
|
|
1364
|
+
docs: { description: "Require custom containers to use a supported type." },
|
|
1365
|
+
messages: {
|
|
1366
|
+
invalidType: "Invalid custom container type \"{{type}}\". Use info, tip, warning, danger, details, raw, code-group, v-pre, or tabs.",
|
|
1367
|
+
invalidTypeCase: "Custom container type \"{{type}}\" must be lowercase."
|
|
1368
|
+
},
|
|
1369
|
+
fixable: "code",
|
|
1370
|
+
schema: []
|
|
1371
|
+
},
|
|
1372
|
+
defaultOptions: [],
|
|
1373
|
+
create(context) {
|
|
1374
|
+
return { text(node) {
|
|
1375
|
+
const { position, start: startPoint } = getNodePosition(node);
|
|
1376
|
+
/* v8 ignore if -- @preserve */
|
|
1377
|
+
if (!position) return;
|
|
1378
|
+
for (const tag of getOpeningTags(parseCustomContainers(node.value))) {
|
|
1379
|
+
const issue = getTypeIssue(tag.value.content);
|
|
1380
|
+
if (!issue) continue;
|
|
1381
|
+
const normalizedType = "normalizedType" in issue ? issue.normalizedType : void 0;
|
|
1382
|
+
const start = startPoint + tag.value.start;
|
|
1383
|
+
const end = startPoint + tag.value.end;
|
|
1384
|
+
context.report({
|
|
1385
|
+
node,
|
|
1386
|
+
messageId: issue.messageId,
|
|
1387
|
+
data: { type: tag.value.content },
|
|
1388
|
+
loc: {
|
|
1389
|
+
start: context.sourceCode.getLocFromIndex(start),
|
|
1390
|
+
end: context.sourceCode.getLocFromIndex(end)
|
|
1391
|
+
},
|
|
1392
|
+
fix: normalizedType ? (fixer) => fixer.replaceTextRange([start, end], normalizedType) : void 0
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
} };
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
function* getOpeningTags(nodes) {
|
|
1399
|
+
for (const node of nodes) {
|
|
1400
|
+
if (node.type !== "custom-container") continue;
|
|
1401
|
+
yield* getOpeningTagsFromContainer(node);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function* getOpeningTagsFromContainer(container) {
|
|
1405
|
+
for (const child of container.children) if (child.type === "custom-container") yield* getOpeningTagsFromContainer(child);
|
|
1406
|
+
else if (child.type === "open") yield child;
|
|
1407
|
+
}
|
|
1408
|
+
function getTypeIssue(type) {
|
|
1409
|
+
if (isCustomContainerType(type)) return null;
|
|
1410
|
+
const normalizedType = type.toLowerCase();
|
|
1411
|
+
if (isCustomContainerType(normalizedType)) return {
|
|
1412
|
+
messageId: MESSAGE_IDS$1.invalidTypeCase,
|
|
1413
|
+
normalizedType
|
|
1414
|
+
};
|
|
1415
|
+
return { messageId: MESSAGE_IDS$1.invalidType };
|
|
1416
|
+
}
|
|
1417
|
+
//#endregion
|
|
1418
|
+
//#region src/rules/valid-heading-anchor/anchor.ts
|
|
1419
|
+
/**
|
|
1420
|
+
* Check if the string has an anchor.
|
|
1421
|
+
* @example: {#chinese-anchor}
|
|
1422
|
+
*/
|
|
1423
|
+
function isStrictAnchor(str) {
|
|
1424
|
+
return /\s\{#[a-z0-9]+(?:-[a-z0-9]+)*\}/.test(str);
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Check whether the string contains CJK Han characters.
|
|
1428
|
+
*/
|
|
1429
|
+
function hasChinese(str) {
|
|
1430
|
+
return /\p{Script=Han}/u.test(str);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Normalize raw anchor text into the strict anchor format content.
|
|
1434
|
+
* - lowercase all letters
|
|
1435
|
+
* - convert spaces to `-`
|
|
1436
|
+
* - remove unsupported characters
|
|
1437
|
+
* - trim leading/trailing `-`
|
|
1438
|
+
*/
|
|
1439
|
+
function normalizeAnchor(anchor) {
|
|
1440
|
+
return anchor.toLowerCase().replace(/[\s.]+/g, "-").replace(/[^a-z0-9_-]/g, "").replace(/^-+|-+$/g, "");
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Count wrapper characters contributed by the trailing like-anchor fragment.
|
|
1444
|
+
* The value is the length difference between the raw matched fragment and the
|
|
1445
|
+
* cleaned anchor text returned by `getLikeAnchor`.
|
|
1446
|
+
* @example `# 中文标题 {#Chinese-Title}` -> 3
|
|
1447
|
+
* @example `## 使用 \`describe\` 编组测试 #Grouping Tests with \`describe\`` -> 1
|
|
1448
|
+
*/
|
|
1449
|
+
function calcAnchorPositionCompensate(content) {
|
|
1450
|
+
const match = getLikeAnchorMatch(content);
|
|
1451
|
+
const anchor = getLikeAnchor(content);
|
|
1452
|
+
if (!match || !anchor) return 0;
|
|
1453
|
+
return match.length - anchor.rawLikeAnchor.length;
|
|
1454
|
+
}
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region src/parser/markdown.ts
|
|
856
1457
|
const language = new MarkdownLanguage({ mode: "gfm" });
|
|
857
1458
|
/**
|
|
858
1459
|
* Parses Markdown with the same GFM language implementation used by the
|
|
859
1460
|
* plugin tests and returns both the mdast tree and ESLint SourceCode wrapper.
|
|
860
1461
|
*/
|
|
861
|
-
function parseMarkdown(markdown
|
|
1462
|
+
function parseMarkdown(markdown) {
|
|
862
1463
|
const file = {
|
|
863
1464
|
path: "test.md",
|
|
864
1465
|
physicalPath: "test.md",
|
|
865
1466
|
bom: false,
|
|
866
|
-
body: markdown
|
|
1467
|
+
body: markdown
|
|
867
1468
|
};
|
|
868
1469
|
const parseResult = language.parse(file, { languageOptions: {
|
|
869
1470
|
...language.defaultLanguageOptions,
|
|
@@ -876,18 +1477,16 @@ function parseMarkdown(markdown$1) {
|
|
|
876
1477
|
sourceCode: language.createSourceCode(file, parseResult)
|
|
877
1478
|
};
|
|
878
1479
|
}
|
|
879
|
-
|
|
880
1480
|
//#endregion
|
|
881
|
-
//#region src/
|
|
1481
|
+
//#region src/rules/valid-heading-anchor/frontmatter.ts
|
|
882
1482
|
/**
|
|
883
1483
|
* Returns true when the Markdown document starts with YAML frontmatter.
|
|
884
1484
|
*/
|
|
885
|
-
function hasFrontmatter(markdown
|
|
886
|
-
if (prevNode?.type === "thematicBreak") markdown
|
|
887
|
-
const { ast } = parseMarkdown(markdown
|
|
1485
|
+
function hasFrontmatter(markdown, prevNode) {
|
|
1486
|
+
if (prevNode?.type === "thematicBreak") markdown = `---\n${markdown}`;
|
|
1487
|
+
const { ast } = parseMarkdown(markdown);
|
|
888
1488
|
return ast.children[0]?.type === "yaml";
|
|
889
1489
|
}
|
|
890
|
-
|
|
891
1490
|
//#endregion
|
|
892
1491
|
//#region src/rules/valid-heading-anchor/index.ts
|
|
893
1492
|
const RULE_NAME = "valid-heading-anchor";
|
|
@@ -895,60 +1494,60 @@ const MESSAGE_IDS = {
|
|
|
895
1494
|
missingAnchor: "missingAnchor",
|
|
896
1495
|
invalidHeadingAnchor: "invalidHeadingAnchor"
|
|
897
1496
|
};
|
|
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
1497
|
//#endregion
|
|
944
1498
|
//#region src/rules/index.ts
|
|
945
1499
|
const rules = {
|
|
1500
|
+
"padding-around-custom-container": padding_around_custom_container_default,
|
|
946
1501
|
"space-around-inline-element": space_around_inline_element_default,
|
|
947
1502
|
"space-around-number": space_around_number_default,
|
|
948
1503
|
"space-around-word": space_around_word_default,
|
|
949
|
-
"
|
|
1504
|
+
"space-around-custom-container": space_around_custom_container_default,
|
|
1505
|
+
"valid-custom-container-type": valid_custom_container_type_default,
|
|
1506
|
+
"valid-heading-anchor": createRule({
|
|
1507
|
+
name: RULE_NAME,
|
|
1508
|
+
meta: {
|
|
1509
|
+
type: "layout",
|
|
1510
|
+
docs: { description: "Require strict lowercase anchors for headings that contain CJK text." },
|
|
1511
|
+
messages: {
|
|
1512
|
+
missingAnchor: "Non-ASCII heading must have an anchor in the format \"{#lowercase-anchor}\".",
|
|
1513
|
+
invalidHeadingAnchor: "Anchor must use lowercase letters and valid characters only."
|
|
1514
|
+
},
|
|
1515
|
+
fixable: "whitespace",
|
|
1516
|
+
schema: []
|
|
1517
|
+
},
|
|
1518
|
+
defaultOptions: [],
|
|
1519
|
+
create(context) {
|
|
1520
|
+
return { heading(node) {
|
|
1521
|
+
const { position, start, end } = getNodePosition(node);
|
|
1522
|
+
/* v8 ignore if -- @preserve */
|
|
1523
|
+
if (!position) return;
|
|
1524
|
+
const source = context.sourceCode.text.slice(start, end);
|
|
1525
|
+
if (isStrictAnchor(source) || !hasChinese(source)) return;
|
|
1526
|
+
if (hasFrontmatter(source, getNodeContext(context, node).prev)) return;
|
|
1527
|
+
const liked = getLikeAnchor(source);
|
|
1528
|
+
if (!liked) {
|
|
1529
|
+
context.report({
|
|
1530
|
+
node,
|
|
1531
|
+
messageId: MESSAGE_IDS.missingAnchor
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const { rawLikeAnchor, isLikeAnchor } = liked;
|
|
1536
|
+
const compensate = calcAnchorPositionCompensate(source);
|
|
1537
|
+
const remainingContent = source.slice(0, -rawLikeAnchor.length - compensate).trim();
|
|
1538
|
+
const anchor = normalizeAnchor(rawLikeAnchor);
|
|
1539
|
+
if (rawLikeAnchor === anchor) return;
|
|
1540
|
+
context.report({
|
|
1541
|
+
node,
|
|
1542
|
+
messageId: isLikeAnchor ? MESSAGE_IDS.missingAnchor : MESSAGE_IDS.invalidHeadingAnchor,
|
|
1543
|
+
fix(fixer) {
|
|
1544
|
+
return fixer.replaceTextRange([start, end], `${remainingContent} {#${anchor}}`);
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
} };
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
950
1550
|
};
|
|
951
|
-
|
|
952
1551
|
//#endregion
|
|
953
1552
|
//#region src/index.ts
|
|
954
1553
|
const plugin = {
|
|
@@ -958,7 +1557,9 @@ const plugin = {
|
|
|
958
1557
|
};
|
|
959
1558
|
const recommendedRules = {
|
|
960
1559
|
"md-style/valid-heading-anchor": "error",
|
|
961
|
-
"md-style/space-around-inline-element": "error"
|
|
1560
|
+
"md-style/space-around-inline-element": "error",
|
|
1561
|
+
"md-style/padding-around-custom-container": "error",
|
|
1562
|
+
"md-style/valid-custom-container-type": "error"
|
|
962
1563
|
};
|
|
963
1564
|
const allRules = Object.fromEntries(Object.keys(rules).map((ruleName) => [`md-style/${ruleName}`, "error"]));
|
|
964
1565
|
const configs = {
|
|
@@ -980,7 +1581,5 @@ const configs = {
|
|
|
980
1581
|
}
|
|
981
1582
|
};
|
|
982
1583
|
const mdStylePlugin = Object.assign(plugin, { configs });
|
|
983
|
-
var src_default = mdStylePlugin;
|
|
984
|
-
|
|
985
1584
|
//#endregion
|
|
986
|
-
export { configs,
|
|
1585
|
+
export { configs, mdStylePlugin as default, plugin };
|