vitepress-plugin-toolkit 0.3.0 → 0.4.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.
@@ -1,14 +1,80 @@
1
1
  import container from "markdown-it-container";
2
- import { camelCase, deepMerge, isBoolean, isNull, isNumber, isPrimitive, isString, isUndefined, kebabCase, objectEntries, objectKeys, omit, slash } from "@pengzhanbo/utils";
2
+ import { LRUCache, camelCase, deepMerge, isArray, isBoolean, isNull, isNumber, isPrimitive, isString, isUndefined, kebabCase, objectEntries, objectKeys, omit, slash, toArray, uniq } from "@pengzhanbo/utils";
3
+ import ansis from "ansis";
3
4
  import process from "node:process";
5
+ import picomatch from "picomatch";
4
6
  import { createHash } from "node:crypto";
5
- import ansis from "ansis";
6
7
  //#region src/shared/link.ts
8
+ /**
9
+ * Regular expression that matches external URLs.
10
+ *
11
+ * Matches URLs that start with a protocol (such as `http:` or `mailto:`) or
12
+ * with `//` (protocol-relative URLs).
13
+ *
14
+ * 匹配外部链接的正则表达式。
15
+ *
16
+ * 匹配以协议(如 `http:` 或 `mailto:`)或 `//`(协议相对链接)开头的 URL。
17
+ *
18
+ * @example
19
+ * EXTERNAL_URL_RE.test('https://example.com') // true
20
+ * EXTERNAL_URL_RE.test('//cdn.example.com/lib.js') // true
21
+ * EXTERNAL_URL_RE.test('/about') // false
22
+ */
7
23
  const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
24
+ /**
25
+ * Checks whether the given path is an external URL.
26
+ *
27
+ * 判断给定路径是否为外部链接。
28
+ *
29
+ * @param path - The path to check / 要检查的路径
30
+ * @returns `true` if the path is an external URL, otherwise `false` / 若为外部链接返回 `true`,否则返回 `false`
31
+ * @example
32
+ * isExternal('https://example.com') // true
33
+ * isExternal('//cdn.example.com/lib.js') // true
34
+ * isExternal('/about') // false
35
+ * isExternal('mailto:foo@example.com') // true
36
+ */
8
37
  function isExternal(path) {
9
38
  return EXTERNAL_URL_RE.test(path);
10
39
  }
40
+ /**
41
+ * Regular expression that matches the protocol scheme of a URL.
42
+ *
43
+ * Matches the leading protocol portion such as `http:`, `https:`, or
44
+ * `mailto:`. Does not match protocol-relative URLs (`//`).
45
+ *
46
+ * 匹配 URL 协议部分的正则表达式。
47
+ *
48
+ * 匹配前导协议部分,如 `http:`、`https:` 或 `mailto:`。
49
+ * 不匹配协议相对链接(`//`)。
50
+ *
51
+ * @example
52
+ * URL_PROTOCOL_RE.test('https://example.com') // true
53
+ * URL_PROTOCOL_RE.test('mailto:foo@example.com') // true
54
+ * URL_PROTOCOL_RE.test('//cdn.example.com/lib.js') // false
55
+ */
11
56
  const URL_PROTOCOL_RE = /^[a-z][a-z0-9+.-]*:/;
57
+ /**
58
+ * Checks whether the given link contains a URL protocol scheme or is a
59
+ * protocol-relative URL.
60
+ *
61
+ * Unlike {@link isExternal}, this function also matches links that start with
62
+ * `//` via an additional check, in addition to those matched by
63
+ * {@link URL_PROTOCOL_RE}.
64
+ *
65
+ * 判断给定链接是否包含 URL 协议部分或为协议相对链接。
66
+ *
67
+ * 与 {@link isExternal} 不同,此函数除了匹配 {@link URL_PROTOCOL_RE} 之外,
68
+ * 还会通过额外检查匹配以 `//` 开头的链接。
69
+ *
70
+ * @param link - The link to check / 要检查的链接
71
+ * @returns `true` if the link has a protocol or starts with `//` / 若链接包含协议或以 `//` 开头则返回 `true`
72
+ * @example
73
+ * isLinkWithProtocol('https://example.com') // true
74
+ * isLinkWithProtocol('mailto:foo@example.com') // true
75
+ * isLinkWithProtocol('//cdn.example.com/lib.js') // true
76
+ * isLinkWithProtocol('/about') // false
77
+ */
12
78
  function isLinkWithProtocol(link) {
13
79
  return URL_PROTOCOL_RE.test(link) || link.startsWith("//");
14
80
  }
@@ -28,6 +94,13 @@ const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuo
28
94
  * @param info - Attribute string / 属性字符串
29
95
  * @returns Object with attrs and rawAttrs / 包含 attrs 和 rawAttrs 的对象
30
96
  * @typeParam T - Attribute type / 属性类型
97
+ * @example
98
+ * ```ts
99
+ * resolveAttrs('width="100" height="50"')
100
+ * // { width: '100', height: '50' }
101
+ * resolveAttrs('disabled title="Hello"')
102
+ * // { disabled: true, title: 'Hello' }
103
+ * ```
31
104
  */
32
105
  function resolveAttrs(info) {
33
106
  info = info.trim();
@@ -54,6 +127,11 @@ function resolveAttrs(info) {
54
127
  * @param info - Info string / 信息字符串
55
128
  * @param key - Attribute key / 属性键
56
129
  * @returns Attribute value or undefined / 属性值或 undefined
130
+ * @example
131
+ * ```ts
132
+ * resolveAttr('width="100" height="50"', 'width') // '100'
133
+ * resolveAttr('width="100" height="50"', 'color') // undefined
134
+ * ```
57
135
  */
58
136
  function resolveAttr(info, key) {
59
137
  const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`);
@@ -72,6 +150,13 @@ function resolveAttr(info, key) {
72
150
  * @param options - Optional before/after render hooks / 可选的 before/after 渲染钩子
73
151
  * @param options.before - Callback for rendering container opening tag / 渲染容器起始标签时的回调函数
74
152
  * @param options.after - Callback for rendering container closing tag / 渲染容器结束标签时的回调函数
153
+ * @example
154
+ * ```ts
155
+ * md.use(createContainerPlugin, 'tip', {
156
+ * before: info => `<div class="tip">${info}`,
157
+ * after: () => '</div>',
158
+ * })
159
+ * ```
75
160
  */
76
161
  function createContainerPlugin(md, type, { before, after } = {}) {
77
162
  const render = (tokens, index, options, env) => {
@@ -153,6 +238,7 @@ function createContainerSyntaxPlugin(md, type, render) {
153
238
  }
154
239
  //#endregion
155
240
  //#region src/node/markdown/embed.ts
241
+ const EXISTS_TYPES = /* @__PURE__ */ new WeakMap();
156
242
  /**
157
243
  * Create embed rule block
158
244
  *
@@ -165,48 +251,82 @@ function createContainerSyntaxPlugin(md, type, render) {
165
251
  * @param md - Markdown instance / Markdown 实例
166
252
  * @param {EmbedRuleBlockOptions} options - Embed rule block options / 嵌入规则块选项
167
253
  * @typeParam Meta - Metadata type / 元数据类型
254
+ * @example
255
+ * ```ts
256
+ * createEmbedRuleBlock(md, {
257
+ * type: 'video',
258
+ * meta: (info, source) => ({ src: source, title: info }),
259
+ * content: meta => `<video src="${meta.src}" title="${meta.title}"></video>`,
260
+ * })
261
+ * ```
168
262
  */
169
- function createEmbedRuleBlock(md, { type, name = type, syntaxPattern, beforeName = "code", ruleOptions = { alt: [
170
- "paragraph",
171
- "reference",
172
- "blockquote",
173
- "list"
174
- ] }, meta, content }) {
263
+ function createEmbedRuleBlock(md, { type, name = `embed_${type}`, meta, content }) {
264
+ if (!type) {
265
+ console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block type is empty`);
266
+ return;
267
+ }
268
+ let exists = EXISTS_TYPES.get(md);
269
+ !exists && EXISTS_TYPES.set(md, exists = /* @__PURE__ */ new Set());
270
+ if (exists.has(type)) {
271
+ console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block type ${ansis.green(type)} already exists`);
272
+ return;
273
+ }
274
+ if (md.renderer.rules[name]) {
275
+ console.warn(`${ansis.yellow("[markdown-it]")} Embed rule block ${type} (${name}) already exists`);
276
+ return;
277
+ }
278
+ exists.add(type);
175
279
  const MIN_LENGTH = type.length + 5;
176
280
  const START_CODES = [
177
281
  64,
178
282
  91,
179
283
  ...type.split("").map((c) => c.charCodeAt(0))
180
284
  ];
181
- md.block.ruler.before(beforeName, name, (state, startLine, endLine, silent) => {
285
+ const escapedType = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
+ const syntaxPattern = new RegExp(`^@\\[${escapedType}(?:\\s+([^\\]]*))?\\]\\(([^)]*)\\)$`);
287
+ md.block.ruler.before("code", name, (state, startLine, endLine, silent) => {
182
288
  const pos = state.bMarks[startLine] + state.tShift[startLine];
183
289
  const max = state.eMarks[startLine];
184
290
  if (pos + MIN_LENGTH > max) return false;
185
291
  for (let i = 0; i < START_CODES.length; i += 1) if (state.src.charCodeAt(pos + i) !== START_CODES[i]) return false;
186
- const content = state.src.slice(pos, max);
292
+ const content = state.src.slice(pos, max).trim();
187
293
  const match = content.match(syntaxPattern);
188
294
  if (!match) return false;
189
295
  /* istanbul ignore if -- @preserve */
190
296
  if (silent) return true;
191
297
  const token = state.push(name, "", 0);
192
- token.meta = meta(match);
298
+ const [, info = "", source = ""] = match;
299
+ token.meta = meta(info.trim(), source.trim());
193
300
  token.content = content;
194
301
  token.map = [startLine, startLine + 1];
195
302
  state.line = startLine + 1;
196
303
  return true;
197
- }, ruleOptions);
198
- md.renderer.rules[name] = (tokens, index, _, env) => {
199
- const token = tokens[index];
200
- token.content = content(token.meta, token.content, env);
201
- return token.content;
202
- };
304
+ }, { alt: [
305
+ "paragraph",
306
+ "reference",
307
+ "blockquote",
308
+ "list"
309
+ ] });
310
+ md.renderer.rules[name] = (tokens, index, _, env) => content?.(tokens[index].meta, env) ?? tokens[index].content;
203
311
  }
204
312
  //#endregion
205
313
  //#region src/node/utils/constants.ts
314
+ /**
315
+ * Whether the current process is running in production build mode.
316
+ *
317
+ * 当前进程是否运行在生产构建模式下。
318
+ */
206
319
  const isBuild = process.env.NODE_ENV === "production";
320
+ /**
321
+ * Whether the current process is running in development mode.
322
+ *
323
+ * 当前进程是否运行在开发模式下。
324
+ */
207
325
  const isDev = process.env.NODE_ENV === "development";
208
326
  /**
209
- * 浏览器支持的视频文件名扩展名
327
+ * Browser-supported video file name extensions.
328
+ *
329
+ * 浏览器支持的视频文件名扩展名。
210
330
  */
211
331
  const EXTENSION_VIDEOS = [
212
332
  "mp4",
@@ -224,7 +344,9 @@ const EXTENSION_VIDEOS = [
224
344
  "ogv"
225
345
  ];
226
346
  /**
227
- * 浏览器支持的图片文件名扩展名
347
+ * Browser-supported image file name extensions.
348
+ *
349
+ * 浏览器支持的图片文件名扩展名。
228
350
  */
229
351
  const EXTENSION_IMAGES = [
230
352
  "jpg",
@@ -244,7 +366,9 @@ const EXTENSION_IMAGES = [
244
366
  "xbm"
245
367
  ];
246
368
  /**
247
- * 浏览器支持的音频文件名扩展名
369
+ * Browser-supported audio file name extensions.
370
+ *
371
+ * 浏览器支持的音频文件名扩展名。
248
372
  */
249
373
  const EXTENSION_AUDIOS = [
250
374
  "mp3",
@@ -257,12 +381,81 @@ const EXTENSION_AUDIOS = [
257
381
  ];
258
382
  //#endregion
259
383
  //#region src/node/utils/hash.ts
384
+ /**
385
+ * Generate a SHA-256 hash from the given data.
386
+ *
387
+ * 根据给定数据生成 SHA-256 哈希值。
388
+ *
389
+ * Primitive values are converted to strings directly, while objects are serialized
390
+ * to JSON before hashing. When `length` is provided, the hash is truncated to the
391
+ * specified length.
392
+ *
393
+ * 基本类型直接转为字符串,对象则序列化为 JSON 后再哈希。提供 `length` 时,
394
+ * 哈希值会被截断到指定长度。
395
+ *
396
+ * @param data - The data to hash, can be any value / 要哈希的数据,可以是任意值
397
+ * @param length - Optional length to truncate the hash / 可选的哈希值截断长度
398
+ * @returns The SHA-256 hash string (truncated if length is given)
399
+ * / SHA-256 哈希字符串(指定长度时会被截断)
400
+ * @example
401
+ * ```ts
402
+ * genHash('hello') // full sha256 hash
403
+ * genHash({ a: 1 }, 8) // 8-character hash
404
+ * ```
405
+ */
260
406
  function genHash(data, length) {
261
407
  const str = isPrimitive(data) ? String(data) : JSON.stringify(data);
262
408
  const hash = createHash("sha256").update(str).digest("hex");
263
409
  return length ? hash.slice(0, length) : hash;
264
410
  }
265
411
  //#endregion
412
+ //#region src/node/utils/createMatcher.ts
413
+ const cache = new LRUCache({ maxSize: 100 });
414
+ /**
415
+ * Create a matcher for the given include and exclude patterns.
416
+ *
417
+ * 创建一个用于给定 include 和 exclude 模式的匹配器。
418
+ *
419
+ * @param include - Patterns to include, can be string or array / 要包含的模式,可以是字符串或数组
420
+ * @param exclude - Patterns to exclude, can be string or array / 要排除的模式,可以是字符串或数组
421
+ * @returns Matcher instance / 匹配器实例
422
+ */
423
+ function createMatcher(include, exclude) {
424
+ const key = genHash([normalize(include), normalize(exclude)]);
425
+ if (cache.has(key)) return cache.get(key);
426
+ const { pattern, ignore } = resolveMatcherPattern(include, exclude);
427
+ const matcher = picomatch(pattern, { ignore });
428
+ cache.set(key, matcher);
429
+ return matcher;
430
+ }
431
+ /**
432
+ * Resolve include and exclude patterns into pattern and ignore arrays.
433
+ * Converts various pattern formats into a standardized format for matching.
434
+ *
435
+ * 将 include 和 exclude 模式解析为 pattern 和 ignore 数组。
436
+ * 将各种模式格式转换为用于匹配的标准化格式。
437
+ *
438
+ * @param include - Patterns to include, can be string or array / 要包含的模式,可以是字符串或数组
439
+ * @param exclude - Patterns to exclude, can be string or array / 要排除的模式,可以是字符串或数组
440
+ * @returns Object containing pattern and ignore arrays / 包含 pattern 和 ignore 数组的对象
441
+ */
442
+ function resolveMatcherPattern(include, exclude) {
443
+ const pattern = [];
444
+ const ignore = uniq(toArray(exclude));
445
+ toArray(include).forEach((item) => {
446
+ if (item.startsWith("!")) ignore.push(item.slice(1));
447
+ else pattern.push(item);
448
+ });
449
+ if (pattern.length === 0) pattern.push("*");
450
+ return {
451
+ pattern,
452
+ ignore
453
+ };
454
+ }
455
+ function normalize(arr) {
456
+ return isArray(arr) ? arr.sort((a, b) => a.localeCompare(b)) : arr;
457
+ }
458
+ //#endregion
266
459
  //#region src/node/utils/logger.ts
267
460
  /**
268
461
  * Log levels mapping
@@ -284,6 +477,13 @@ const logLevels = {
284
477
  * @param prefix - Log prefix / 日志前缀
285
478
  * @param defaultLevel - Default log level / 默认日志级别
286
479
  * @returns Logger instance / 日志实例
480
+ * @example
481
+ * ```ts
482
+ * const logger = createLogger('my-plugin', 'info')
483
+ * logger.info('Starting build')
484
+ * logger.warn('Deprecated feature used')
485
+ * logger.debug('Verbose details', 'debug')
486
+ * ```
287
487
  */
288
488
  function createLogger(prefix, defaultLevel = "info") {
289
489
  prefix = `[${prefix}]`;
@@ -326,6 +526,12 @@ function createLogger(prefix, defaultLevel = "info") {
326
526
  * @param str - Size string / 尺寸字符串
327
527
  * @param unit - Unit to append (default: 'px') / 要添加的单位(默认:'px')
328
528
  * @returns Size string with unit / 带单位的尺寸字符串
529
+ * @example
530
+ * ```ts
531
+ * parseRect('100') // '100px'
532
+ * parseRect('50%') // '50%'
533
+ * parseRect('200', 'rpx') // '200rpx'
534
+ * ```
329
535
  */
330
536
  function parseRect(str, unit = "px") {
331
537
  if (Number.parseFloat(str) === Number(str)) return `${str}${unit}`;
@@ -337,7 +543,25 @@ const rControl = /[\u0000-\u001F]/g;
337
543
  const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
338
544
  const rCombining = /[\u0300-\u036F]/g;
339
545
  /**
340
- * Default slugification function
546
+ * Default slugification function that normalizes a string into a URL-safe slug.
547
+ *
548
+ * 默认的 slug 化函数,将字符串规范化为 URL 安全的 slug。
549
+ *
550
+ * The function removes accents, control characters, and special characters,
551
+ * replaces separators with hyphens, and ensures the result does not start
552
+ * with a number.
553
+ *
554
+ * 该函数会移除重音符号、控制字符和特殊字符,用连字符替换分隔符,
555
+ * 并确保结果不以数字开头。
556
+ *
557
+ * @param str - The string to slugify / 要 slug 化的字符串
558
+ * @returns The slugified string / slug 化后的字符串
559
+ * @example
560
+ * ```ts
561
+ * slugify('Hello World!') // 'hello-world'
562
+ * slugify('你好 World') // '你好-world'
563
+ * slugify('1st Section') // '_1st-section'
564
+ * ```
341
565
  */
342
566
  function slugify(str) {
343
567
  return str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
@@ -354,6 +578,13 @@ function slugify(str) {
354
578
  * @param forceStringify - Keys to force stringify / 强制字符串化的键
355
579
  * @returns HTML attribute string / HTML 属性字符串
356
580
  * @typeParam T - Attribute type / 属性类型
581
+ * @example
582
+ * ```ts
583
+ * stringifyAttrs({ width: 100, disabled: true })
584
+ * // ' :width="100" disabled'
585
+ * stringifyAttrs({ title: 'hello', visible: false })
586
+ * // ' title="hello"'
587
+ * ```
357
588
  */
358
589
  function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = []) {
359
590
  const result = objectEntries(attrs).map(([key, value]) => {
@@ -375,7 +606,41 @@ function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = [])
375
606
  }
376
607
  //#endregion
377
608
  //#region src/node/utils/treat-as-html.ts
609
+ /**
610
+ * Set of file extensions that should be treated as static assets rather than HTML.
611
+ *
612
+ * 应作为静态资源而非 HTML 处理的文件扩展名集合。
613
+ *
614
+ * Populated lazily on first use, combining a built-in list of known extensions
615
+ * with any extra extensions provided via the `VITE_EXTRA_EXTENSIONS` environment
616
+ * variable.
617
+ *
618
+ * 在首次使用时延迟填充,将内置已知扩展名列表与通过 `VITE_EXTRA_EXTENSIONS`
619
+ * 环境变量提供的额外扩展名合并。
620
+ */
378
621
  const KNOWN_EXTENSIONS = /* @__PURE__ */ new Set();
622
+ /**
623
+ * Determine whether a file should be treated as an HTML route based on its extension.
624
+ *
625
+ * 根据文件扩展名判断该文件是否应作为 HTML 路由处理。
626
+ *
627
+ * Returns `true` when the file has no extension or its extension is not in the
628
+ * known extensions list (meaning it should be served as an HTML page). Returns
629
+ * `false` for known static asset extensions.
630
+ *
631
+ * 当文件没有扩展名或其扩展名不在已知扩展名列表中时返回 `true`(表示应作为
632
+ * HTML 页面提供)。对于已知的静态资源扩展名返回 `false`。
633
+ *
634
+ * @param filename - The filename to check / 要检查的文件名
635
+ * @returns `true` if the file should be treated as HTML, `false` otherwise
636
+ * / 如果文件应作为 HTML 处理则返回 `true`,否则返回 `false`
637
+ * @example
638
+ * ```ts
639
+ * treatAsHtml('about') // true (no extension)
640
+ * treatAsHtml('logo.png') // false (known image extension)
641
+ * treatAsHtml('custom.xyz') // true (unknown extension)
642
+ * ```
643
+ */
379
644
  function treatAsHtml(filename) {
380
645
  if (KNOWN_EXTENSIONS.size === 0) {
381
646
  const extraExts = typeof process === "object" && process.env?.VITE_EXTRA_EXTENSIONS || import.meta.env?.VITE_EXTRA_EXTENSIONS || "";
@@ -386,15 +651,54 @@ function treatAsHtml(filename) {
386
651
  }
387
652
  //#endregion
388
653
  //#region src/node/vite-plugins/iconPlugin.ts
654
+ /**
655
+ * Vite plugin name for the icons virtual module.
656
+ *
657
+ * 图标虚拟模块的 Vite 插件名称。
658
+ */
389
659
  const name = "vitepress:tuck-icons";
660
+ /**
661
+ * Global list of resolved icons accumulated across plugin invocations.
662
+ *
663
+ * 跨插件调用累积的全局已解析图标列表。
664
+ */
390
665
  const iconList = [];
666
+ /**
667
+ * Whether the virtual module is currently being processed.
668
+ *
669
+ * 虚拟模块是否正在处理中。
670
+ */
391
671
  let isProcessing = false;
672
+ /**
673
+ * Create a Vite plugin that registers SVG icons as CSS custom properties.
674
+ *
675
+ * 创建一个 Vite 插件,将 SVG 图标注册为 CSS 自定义属性。
676
+ *
677
+ * The plugin collects icons into a global list and exposes them through a
678
+ * virtual module `virtual:tuck-icons.css`. Each icon is rendered as a CSS
679
+ * rule that sets the `--icon` custom property on `.vpi-<name>` selectors.
680
+ *
681
+ * 该插件将图标收集到全局列表中,并通过虚拟模块 `virtual:tuck-icons.css`
682
+ * 暴露它们。每个图标被渲染为一条 CSS 规则,在 `.vpi-<name>` 选择器上
683
+ * 设置 `--icon` 自定义属性。
684
+ *
685
+ * @param icons - Array of icon definitions to register / 要注册的图标定义数组
686
+ * @returns A Vite plugin instance / Vite 插件实例
687
+ * @example
688
+ * ```ts
689
+ * import { iconPlugin } from 'vitepress-plugin-toolkit/node'
690
+ * icons: iconPlugin([
691
+ * { name: 'github', svg: '<svg>...</svg>' },
692
+ * { name: 'twitter', svg: '<svg>...</svg>' },
693
+ * ])
694
+ * ```
695
+ */
392
696
  function iconPlugin(icons) {
393
697
  for (const icon of icons) {
394
698
  const index = iconList.findIndex((item) => item.name === icon.name);
395
699
  if (index === -1) iconList.push({
396
700
  ...omit(icon, ["classname"]),
397
- classname: new Set([icon.classname || icon.name])
701
+ classname: /* @__PURE__ */ new Set([icon.classname || icon.name])
398
702
  });
399
703
  else icon.classname && iconList[index].classname.add(icon.classname);
400
704
  }
@@ -414,8 +718,8 @@ function iconPlugin(icons) {
414
718
  if (!isProcessing) return null;
415
719
  let css = "";
416
720
  for (const icon of iconList) {
417
- const classname = Array.from(icon.classname).map((name) => `.vpi-${name}`).join(",\n");
418
- css += `${classname} {\n --icon: ${icon.svg};\n}\n`;
721
+ const classname = Array.from(icon.classname).map((name) => `.vpi-${name}`).join(",");
722
+ css += `${classname} { --icon: ${icon.svg}; }\n`;
419
723
  }
420
724
  isProcessing = false;
421
725
  return css;
@@ -424,6 +728,26 @@ function iconPlugin(icons) {
424
728
  }
425
729
  //#endregion
426
730
  //#region src/node/vitepress/get-vitepress-config.ts
731
+ /**
732
+ * Get the current VitePress site configuration from the global context.
733
+ *
734
+ * 从全局上下文获取当前的 VitePress 站点配置。
735
+ *
736
+ * VitePress stores its resolved `SiteConfig` on `globalThis.VITEPRESS_CONFIG`
737
+ * during the build and dev lifecycle. This helper reads that value and throws
738
+ * when it has not been initialized yet.
739
+ *
740
+ * VitePress 在构建和开发生命周期中将解析后的 `SiteConfig` 存储在
741
+ * `globalThis.VITEPRESS_CONFIG` 上。该辅助函数读取该值,未初始化时抛出错误。
742
+ *
743
+ * @returns The current VitePress site config / 当前的 VitePress 站点配置
744
+ * @throws {Error} When VITEPRESS_CONFIG is not initialized / VITEPRESS_CONFIG 未初始化时
745
+ * @example
746
+ * ```ts
747
+ * const config = getVitepressConfig()
748
+ * console.log(config.userConfig.title)
749
+ * ```
750
+ */
427
751
  function getVitepressConfig() {
428
752
  const config = globalThis.VITEPRESS_CONFIG;
429
753
  if (!config) throw new Error("VITEPRESS_CONFIG is not initialized");
@@ -432,10 +756,30 @@ function getVitepressConfig() {
432
756
  //#endregion
433
757
  //#region src/node/vitepress/createLocales.ts
434
758
  /**
435
- * 创建 locales
436
- * @param builtinLocales 内置的 locales
437
- * @param userLocales 用户的 locales
438
- * @returns locales
759
+ * Create a locales configuration by merging builtin locales with user locales.
760
+ *
761
+ * 通过合并内置语言环境与用户语言环境来创建语言环境配置。
762
+ *
763
+ * The function reads the VitePress locales config, matches each locale key
764
+ * (or its `lang` field) against the builtin locales, and falls back to the
765
+ * first builtin locale for the `root` entry when no match is found. User
766
+ * locales are deep-merged on top of the result.
767
+ *
768
+ * 该函数读取 VitePress 的语言环境配置,将每个语言环境键(或其 `lang` 字段)
769
+ * 与内置语言环境匹配,未匹配到时 `root` 条目回退到第一个内置语言环境。
770
+ * 用户语言环境会深度合并到结果之上。
771
+ *
772
+ * @param builtinLocales - Builtin locale entries / 内置语言环境条目
773
+ * @param userLocales - User-provided locale overrides / 用户提供的语言环境覆盖
774
+ * @returns Merged locales configuration / 合并后的语言环境配置
775
+ * @typeParam LocaleData - Locale data type / 语言环境数据类型
776
+ * @example
777
+ * ```ts
778
+ * const locales = createLocales(
779
+ * [[['en', 'en-US'], { title: 'English' }], [['zh', 'zh-CN'], { title: '中文' }]],
780
+ * { zh: { title: '我的站点' } },
781
+ * )
782
+ * ```
439
783
  */
440
784
  function createLocales(builtinLocales, userLocales = {}) {
441
785
  const locales = {};
@@ -450,6 +794,27 @@ function createLocales(builtinLocales, userLocales = {}) {
450
794
  }
451
795
  //#endregion
452
796
  //#region src/node/vitepress/get-locale-with-path.ts
797
+ /**
798
+ * Resolve the locale and language code for a given path.
799
+ *
800
+ * 解析给定路径对应的语言环境和语言代码。
801
+ *
802
+ * The function inspects the VitePress locales config and finds the locale key
803
+ * that the provided path starts with. When the matched key is `root`, the
804
+ * returned locale is an empty string. Returns empty strings for both fields
805
+ * when no locale matches.
806
+ *
807
+ * 该函数检查 VitePress 的语言环境配置,找到所提供路径以其开头的语言环境键。
808
+ * 当匹配的键为 `root` 时,返回的语言环境为空字符串。无匹配时两个字段均返回空字符串。
809
+ *
810
+ * @param path - The path to resolve / 要解析的路径
811
+ * @returns An object with `lang` and `locale` fields / 包含 `lang` 和 `locale` 字段的对象
812
+ * @example
813
+ * ```ts
814
+ * getLocaleWithPath('/zh/guide/') // { lang: 'zh-CN', locale: '/zh/' }
815
+ * getLocaleWithPath('/guide/') // { lang: 'en', locale: '' } (root locale)
816
+ * ```
817
+ */
453
818
  function getLocaleWithPath(path) {
454
819
  const locales = getVitepressConfig().userConfig?.locales || {};
455
820
  const keys = objectKeys(locales);
@@ -465,7 +830,37 @@ function getLocaleWithPath(path) {
465
830
  }
466
831
  //#endregion
467
832
  //#region src/node/vitepress/resolve-route-link.ts
833
+ /**
834
+ * Regular expression matching `index.md` paths with an optional hash.
835
+ *
836
+ * 匹配带可选哈希的 `index.md` 路径的正则表达式。
837
+ */
468
838
  const indexRE = /(^|.*\/)index.md(.*)$/i;
839
+ /**
840
+ * Resolve a markdown link to its final URL form based on VitePress routing rules.
841
+ *
842
+ * 根据 VitePress 路由规则将 markdown 链接解析为最终 URL 形式。
843
+ *
844
+ * External links are returned as-is. Absolute paths are prefixed with the
845
+ * site base. Hash links are slugified. Relative links to markdown files are
846
+ * transformed according to the `cleanUrls` setting (`.md` becomes `''` or
847
+ * `.html`), and `index.md` is collapsed to its directory.
848
+ *
849
+ * 外部链接原样返回。绝对路径添加站点 base 前缀。哈希链接进行 slug 化。
850
+ * 指向 markdown 文件的相对链接根据 `cleanUrls` 设置转换(`.md` 变为 `''`
851
+ * 或 `.html`),`index.md` 折叠为其所在目录。
852
+ *
853
+ * @param url - The raw URL from markdown content / markdown 内容中的原始 URL
854
+ * @param env - The markdown environment with routing context / 带路由上下文的 markdown 环境
855
+ * @returns The resolved URL / 解析后的 URL
856
+ * @example
857
+ * ```ts
858
+ * resolveRouteLink('./guide.md', { cleanUrls: false } as MarkdownEnv)
859
+ * // './guide.html'
860
+ * resolveRouteLink('/about/', {} as MarkdownEnv)
861
+ * // '/base/about/'
862
+ * ```
863
+ */
469
864
  function resolveRouteLink(url, env) {
470
865
  if (isExternal(url)) return url;
471
866
  const config = getVitepressConfig();
@@ -488,8 +883,16 @@ function resolveRouteLink(url, env) {
488
883
  }
489
884
  return url;
490
885
  }
886
+ /**
887
+ * Normalize a hash fragment by slugifying its content.
888
+ *
889
+ * 通过对内容进行 slug 化来规范化哈希片段。
890
+ *
891
+ * @param str - The hash string (with leading `#`) or empty / 哈希字符串(带前导 `#`)或空字符串
892
+ * @returns The encoded hash string or empty / 编码后的哈希字符串或空字符串
893
+ */
491
894
  function normalizeHash(str) {
492
895
  return str ? encodeURI(`#${slugify(decodeURI(str).slice(1))}`) : "";
493
896
  }
494
897
  //#endregion
495
- export { EXTENSION_AUDIOS, EXTENSION_IMAGES, EXTENSION_VIDEOS, EXTERNAL_URL_RE, URL_PROTOCOL_RE, createContainerPlugin, createContainerSyntaxPlugin, createEmbedRuleBlock, createLocales, createLogger, genHash, getLocaleWithPath, getVitepressConfig, iconPlugin, isBuild, isDev, isExternal, isLinkWithProtocol, logLevels, parseRect, resolveAttr, resolveAttrs, resolveRouteLink, slugify, stringifyAttrs, treatAsHtml };
898
+ export { EXTENSION_AUDIOS, EXTENSION_IMAGES, EXTENSION_VIDEOS, EXTERNAL_URL_RE, URL_PROTOCOL_RE, createContainerPlugin, createContainerSyntaxPlugin, createEmbedRuleBlock, createLocales, createLogger, createMatcher, genHash, getLocaleWithPath, getVitepressConfig, iconPlugin, isBuild, isDev, isExternal, isLinkWithProtocol, logLevels, parseRect, resolveAttr, resolveAttrs, resolveMatcherPattern, resolveRouteLink, slugify, stringifyAttrs, treatAsHtml };
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "vitepress-plugin-toolkit",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "Development toolkit for vitepress plugins",
6
6
  "author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
7
7
  "license": "MIT",
8
+ "homepage": "https://tuck.pengzhanbo.cn/guide/toolkit",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "git+https://github.com/pengzhanbo/vitepress-tuck.git",
11
12
  "directory": "packages/plugin-toolkit"
12
13
  },
14
+ "bugs": {
15
+ "url": "https://github.com/pengzhanbo/vitepress-tuck/issues"
16
+ },
13
17
  "keywords": [
14
18
  "vitepress",
15
19
  "vitepress-plugin",
@@ -40,7 +44,8 @@
40
44
  "@pengzhanbo/utils": "^3.7.3",
41
45
  "@vueuse/core": "^14.3.0",
42
46
  "ansis": "^4.3.1",
43
- "markdown-it-container": "^4.0.0"
47
+ "markdown-it-container": "^4.0.0",
48
+ "picomatch": "^4.0.4"
44
49
  },
45
50
  "publishConfig": {
46
51
  "access": "public",