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/dist/index.mjs CHANGED
@@ -1,18 +1,8 @@
1
1
  import markdown, { MarkdownLanguage } from "@eslint/markdown";
2
-
3
- //#region src/utils/index.ts
4
- function createRule({ create, defaultOptions, meta }) {
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 !!node && typeof node === "object" && "children" in node && Array.isArray(node.children);
13
+ return isObject(node) && "children" in node && Array.isArray(node.children);
24
14
  }
25
- function isTableCell(node) {
26
- return node.type === "tableCell";
27
- }
28
- const INLINE_ELEMENT_TYPES = new Set([
29
- "link",
30
- "image",
31
- "inlineCode",
32
- "emphasis",
33
- "strong"
34
- ]);
35
- /**
36
- * Checks whether a phrasing node is one of the selected inline element targets.
37
- */
38
- function isInlineElement(node) {
39
- return !!node && INLINE_ELEMENT_TYPES.has(node.type);
15
+ function isCodeNode(node) {
16
+ return node?.type === "code";
40
17
  }
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,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 src/utils/anchor.ts
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 string.
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(/(\{|\})/g, "").replace(/^#/, "").trimStart();
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 src/utils/punctuation.ts
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 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
- };
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 src/utils/inline-element.ts
331
- /**
332
- * Checks whether adjacent text is a custom container marker on the next line.
333
- *
334
- * @deprecated Temporary workaround to prevent space-between-link from reporting
335
- * false positives on custom containers. Remove this and handle the case in a
336
- * dedicated custom container rule when one exists.
337
- * @see https://vitepress.dev/guide/markdown#custom-containers
338
- * @example `\n:::` -> true
339
- * @example `\n::::` -> true
340
- * @example `:::` -> false
341
- */
342
- function isCustomContainerMarker(str) {
343
- return /^[ \t]*\n[ \t]*:{3,}[ \t]*$/u.test(str || "");
344
- }
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 MESSAGE_IDS$1.unexpectedSpaceBefore;
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, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesAfterPunctuation);
362
- if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceBefore;
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, MESSAGE_IDS$1.missingSpaceBefore, MESSAGE_IDS$1.multipleSpacesBefore);
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 MESSAGE_IDS$1.unexpectedSpaceAfter;
378
- if (context.punctuationType === "half" && OPENING_PAIRED_PUNCTUATION.has(adjacentChar || "") || isDashPunctuation(adjacentChar)) return validateSingleRequiredSpace(context.whiteSpace.count, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
379
- if (context.whiteSpace.count > 0) return MESSAGE_IDS$1.unexpectedSpaceAfter;
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, MESSAGE_IDS$1.missingSpaceAfter, MESSAGE_IDS$1.multipleSpacesAfter);
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 src/rules/space-around-inline-element/index.ts
428
- const RULE_NAME$3 = "space-around-inline-element";
429
- const MESSAGE_IDS$1 = {
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 BEFORE_INLINE_ELEMENT_MESSAGE_IDS = new Set([
439
- MESSAGE_IDS$1.missingSpaceBefore,
440
- MESSAGE_IDS$1.multipleSpacesBefore,
441
- MESSAGE_IDS$1.multipleSpacesAfterPunctuation,
442
- MESSAGE_IDS$1.unexpectedSpaceBefore
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$3,
957
+ name: RULE_NAME$4,
446
958
  meta: {
447
959
  type: "layout",
448
- docs: { description: "Enforce spacing around Markdown inline elements." },
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 messageId = validateSpace(nodeContext);
493
- if (!messageId) return;
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$1.unexpectedSpaceBefore ? "" : " ";
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$1.unexpectedSpaceAfter ? "" : " ";
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 src/utils/text/tokenizer.ts
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: point.line + 1,
1138
+ line: line + 1,
626
1139
  column: 1,
627
- offset: point.offset + char.length
1140
+ offset: offset + char.length
628
1141
  };
629
1142
  return {
630
- line: point.line,
631
- column: point.column + char.length,
632
- offset: point.offset + char.length
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 src/utils/text/boundary-space.ts
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 SPACE_MESSAGE_IDS.missingSpacesAround;
744
- if (boundary.unexpectedBefore && boundary.unexpectedAfter) return SPACE_MESSAGE_IDS.unexpectedSpaceAround;
745
- if (boundary.missingBefore) return SPACE_MESSAGE_IDS.missingSpaceBefore;
746
- if (boundary.missingAfter) return SPACE_MESSAGE_IDS.missingSpaceAfter;
747
- if (boundary.unexpectedBefore) return SPACE_MESSAGE_IDS.unexpectedSpaceBefore;
748
- return SPACE_MESSAGE_IDS.unexpectedSpaceAfter;
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: RULE_NAME$2,
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, isNumberType);
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: RULE_NAME$1,
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 src/utils/markdown.ts
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$1) {
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$1
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 src/utils/heading.ts
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$1, prevNode) {
886
- if (prevNode?.type === "thematicBreak") markdown$1 = `---\n${markdown$1}`;
887
- const { ast } = parseMarkdown(markdown$1);
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 src/rules/valid-heading-anchor/index.ts
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 src/rules/index.ts
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
- "valid-heading-anchor": valid_heading_anchor_default
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 src/index.ts
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, src_default as default, plugin };
1587
+ export { configs, mdStylePlugin as default, plugin };