vitepress-plugin-toolkit 0.2.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,13 +1,80 @@
1
1
  import container from "markdown-it-container";
2
- import { camelCase, deepMerge, isBoolean, isNull, isNumber, isString, isUndefined, kebabCase, objectEntries, objectKeys, 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
3
  import ansis from "ansis";
4
4
  import process from "node:process";
5
+ import picomatch from "picomatch";
6
+ import { createHash } from "node:crypto";
5
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
+ */
6
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
+ */
7
37
  function isExternal(path) {
8
38
  return EXTERNAL_URL_RE.test(path);
9
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
+ */
10
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
+ */
11
78
  function isLinkWithProtocol(link) {
12
79
  return URL_PROTOCOL_RE.test(link) || link.startsWith("//");
13
80
  }
@@ -27,6 +94,13 @@ const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w-]+)(?:=(?<quote>['"])(?<valueWithQuo
27
94
  * @param info - Attribute string / 属性字符串
28
95
  * @returns Object with attrs and rawAttrs / 包含 attrs 和 rawAttrs 的对象
29
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
+ * ```
30
104
  */
31
105
  function resolveAttrs(info) {
32
106
  info = info.trim();
@@ -53,6 +127,11 @@ function resolveAttrs(info) {
53
127
  * @param info - Info string / 信息字符串
54
128
  * @param key - Attribute key / 属性键
55
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
+ * ```
56
135
  */
57
136
  function resolveAttr(info, key) {
58
137
  const pattern = new RegExp(`(?:^|\\s+)${key}(?:=(?<quote>['"])(?<valueWithQuote>.+?)\\k<quote>|=(?<valueWithoutQuote>\\S+))?(?:\\s+|$)`);
@@ -71,6 +150,13 @@ function resolveAttr(info, key) {
71
150
  * @param options - Optional before/after render hooks / 可选的 before/after 渲染钩子
72
151
  * @param options.before - Callback for rendering container opening tag / 渲染容器起始标签时的回调函数
73
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
+ * ```
74
160
  */
75
161
  function createContainerPlugin(md, type, { before, after } = {}) {
76
162
  const render = (tokens, index, options, env) => {
@@ -152,6 +238,7 @@ function createContainerSyntaxPlugin(md, type, render) {
152
238
  }
153
239
  //#endregion
154
240
  //#region src/node/markdown/embed.ts
241
+ const EXISTS_TYPES = /* @__PURE__ */ new WeakMap();
155
242
  /**
156
243
  * Create embed rule block
157
244
  *
@@ -164,46 +251,82 @@ function createContainerSyntaxPlugin(md, type, render) {
164
251
  * @param md - Markdown instance / Markdown 实例
165
252
  * @param {EmbedRuleBlockOptions} options - Embed rule block options / 嵌入规则块选项
166
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
+ * ```
167
262
  */
168
- function createEmbedRuleBlock(md, { type, name = type, syntaxPattern, beforeName = "code", ruleOptions = { alt: [
169
- "paragraph",
170
- "reference",
171
- "blockquote",
172
- "list"
173
- ] }, 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);
174
279
  const MIN_LENGTH = type.length + 5;
175
280
  const START_CODES = [
176
281
  64,
177
282
  91,
178
283
  ...type.split("").map((c) => c.charCodeAt(0))
179
284
  ];
180
- 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) => {
181
288
  const pos = state.bMarks[startLine] + state.tShift[startLine];
182
289
  const max = state.eMarks[startLine];
183
290
  if (pos + MIN_LENGTH > max) return false;
184
291
  for (let i = 0; i < START_CODES.length; i += 1) if (state.src.charCodeAt(pos + i) !== START_CODES[i]) return false;
185
- const content = state.src.slice(pos, max);
292
+ const content = state.src.slice(pos, max).trim();
186
293
  const match = content.match(syntaxPattern);
187
294
  if (!match) return false;
188
295
  /* istanbul ignore if -- @preserve */
189
296
  if (silent) return true;
190
297
  const token = state.push(name, "", 0);
191
- token.meta = meta(match);
298
+ const [, info = "", source = ""] = match;
299
+ token.meta = meta(info.trim(), source.trim());
192
300
  token.content = content;
193
301
  token.map = [startLine, startLine + 1];
194
302
  state.line = startLine + 1;
195
303
  return true;
196
- }, ruleOptions);
197
- md.renderer.rules[name] = (tokens, index, _, env) => {
198
- const token = tokens[index];
199
- token.content = content(token.meta, token.content, env);
200
- return token.content;
201
- };
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;
202
311
  }
203
312
  //#endregion
204
313
  //#region src/node/utils/constants.ts
205
314
  /**
206
- * 支持的视频文件名扩展名
315
+ * Whether the current process is running in production build mode.
316
+ *
317
+ * 当前进程是否运行在生产构建模式下。
318
+ */
319
+ const isBuild = process.env.NODE_ENV === "production";
320
+ /**
321
+ * Whether the current process is running in development mode.
322
+ *
323
+ * 当前进程是否运行在开发模式下。
324
+ */
325
+ const isDev = process.env.NODE_ENV === "development";
326
+ /**
327
+ * Browser-supported video file name extensions.
328
+ *
329
+ * 浏览器支持的视频文件名扩展名。
207
330
  */
208
331
  const EXTENSION_VIDEOS = [
209
332
  "mp4",
@@ -221,7 +344,9 @@ const EXTENSION_VIDEOS = [
221
344
  "ogv"
222
345
  ];
223
346
  /**
224
- * 支持的图片文件名扩展名
347
+ * Browser-supported image file name extensions.
348
+ *
349
+ * 浏览器支持的图片文件名扩展名。
225
350
  */
226
351
  const EXTENSION_IMAGES = [
227
352
  "jpg",
@@ -241,7 +366,9 @@ const EXTENSION_IMAGES = [
241
366
  "xbm"
242
367
  ];
243
368
  /**
244
- * 支持的音频文件名扩展名
369
+ * Browser-supported audio file name extensions.
370
+ *
371
+ * 浏览器支持的音频文件名扩展名。
245
372
  */
246
373
  const EXTENSION_AUDIOS = [
247
374
  "mp3",
@@ -253,6 +380,82 @@ const EXTENSION_AUDIOS = [
253
380
  "acc"
254
381
  ];
255
382
  //#endregion
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
+ */
406
+ function genHash(data, length) {
407
+ const str = isPrimitive(data) ? String(data) : JSON.stringify(data);
408
+ const hash = createHash("sha256").update(str).digest("hex");
409
+ return length ? hash.slice(0, length) : hash;
410
+ }
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
256
459
  //#region src/node/utils/logger.ts
257
460
  /**
258
461
  * Log levels mapping
@@ -274,6 +477,13 @@ const logLevels = {
274
477
  * @param prefix - Log prefix / 日志前缀
275
478
  * @param defaultLevel - Default log level / 默认日志级别
276
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
+ * ```
277
487
  */
278
488
  function createLogger(prefix, defaultLevel = "info") {
279
489
  prefix = `[${prefix}]`;
@@ -316,6 +526,12 @@ function createLogger(prefix, defaultLevel = "info") {
316
526
  * @param str - Size string / 尺寸字符串
317
527
  * @param unit - Unit to append (default: 'px') / 要添加的单位(默认:'px')
318
528
  * @returns Size string with unit / 带单位的尺寸字符串
529
+ * @example
530
+ * ```ts
531
+ * parseRect('100') // '100px'
532
+ * parseRect('50%') // '50%'
533
+ * parseRect('200', 'rpx') // '200rpx'
534
+ * ```
319
535
  */
320
536
  function parseRect(str, unit = "px") {
321
537
  if (Number.parseFloat(str) === Number(str)) return `${str}${unit}`;
@@ -327,7 +543,25 @@ const rControl = /[\u0000-\u001F]/g;
327
543
  const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g;
328
544
  const rCombining = /[\u0300-\u036F]/g;
329
545
  /**
330
- * 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
+ * ```
331
565
  */
332
566
  function slugify(str) {
333
567
  return str.normalize("NFKD").replace(rCombining, "").replace(rControl, "").replace(rSpecial, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").replace(/^(\d)/, "_$1").toLowerCase();
@@ -344,6 +578,13 @@ function slugify(str) {
344
578
  * @param forceStringify - Keys to force stringify / 强制字符串化的键
345
579
  * @returns HTML attribute string / HTML 属性字符串
346
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
+ * ```
347
588
  */
348
589
  function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = []) {
349
590
  const result = objectEntries(attrs).map(([key, value]) => {
@@ -365,7 +606,41 @@ function stringifyAttrs(attrs, withUndefinedOrNull = false, forceStringify = [])
365
606
  }
366
607
  //#endregion
367
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
+ */
368
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
+ */
369
644
  function treatAsHtml(filename) {
370
645
  if (KNOWN_EXTENSIONS.size === 0) {
371
646
  const extraExts = typeof process === "object" && process.env?.VITE_EXTRA_EXTENSIONS || import.meta.env?.VITE_EXTRA_EXTENSIONS || "";
@@ -375,7 +650,104 @@ function treatAsHtml(filename) {
375
650
  return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase());
376
651
  }
377
652
  //#endregion
653
+ //#region src/node/vite-plugins/iconPlugin.ts
654
+ /**
655
+ * Vite plugin name for the icons virtual module.
656
+ *
657
+ * 图标虚拟模块的 Vite 插件名称。
658
+ */
659
+ const name = "vitepress:tuck-icons";
660
+ /**
661
+ * Global list of resolved icons accumulated across plugin invocations.
662
+ *
663
+ * 跨插件调用累积的全局已解析图标列表。
664
+ */
665
+ const iconList = [];
666
+ /**
667
+ * Whether the virtual module is currently being processed.
668
+ *
669
+ * 虚拟模块是否正在处理中。
670
+ */
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
+ */
696
+ function iconPlugin(icons) {
697
+ for (const icon of icons) {
698
+ const index = iconList.findIndex((item) => item.name === icon.name);
699
+ if (index === -1) iconList.push({
700
+ ...omit(icon, ["classname"]),
701
+ classname: /* @__PURE__ */ new Set([icon.classname || icon.name])
702
+ });
703
+ else icon.classname && iconList[index].classname.add(icon.classname);
704
+ }
705
+ const moduleId = "virtual:tuck-icons.css";
706
+ const resolveId = `\0${moduleId}`;
707
+ return {
708
+ name,
709
+ enforce: "post",
710
+ resolveId(id) {
711
+ if (id === moduleId) {
712
+ isProcessing = true;
713
+ return resolveId;
714
+ }
715
+ },
716
+ load(id) {
717
+ if (id !== resolveId) return null;
718
+ if (!isProcessing) return null;
719
+ let css = "";
720
+ for (const icon of iconList) {
721
+ const classname = Array.from(icon.classname).map((name) => `.vpi-${name}`).join(",");
722
+ css += `${classname} { --icon: ${icon.svg}; }\n`;
723
+ }
724
+ isProcessing = false;
725
+ return css;
726
+ }
727
+ };
728
+ }
729
+ //#endregion
378
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
+ */
379
751
  function getVitepressConfig() {
380
752
  const config = globalThis.VITEPRESS_CONFIG;
381
753
  if (!config) throw new Error("VITEPRESS_CONFIG is not initialized");
@@ -384,10 +756,30 @@ function getVitepressConfig() {
384
756
  //#endregion
385
757
  //#region src/node/vitepress/createLocales.ts
386
758
  /**
387
- * 创建 locales
388
- * @param builtinLocales 内置的 locales
389
- * @param userLocales 用户的 locales
390
- * @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
+ * ```
391
783
  */
392
784
  function createLocales(builtinLocales, userLocales = {}) {
393
785
  const locales = {};
@@ -402,6 +794,27 @@ function createLocales(builtinLocales, userLocales = {}) {
402
794
  }
403
795
  //#endregion
404
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
+ */
405
818
  function getLocaleWithPath(path) {
406
819
  const locales = getVitepressConfig().userConfig?.locales || {};
407
820
  const keys = objectKeys(locales);
@@ -417,7 +830,37 @@ function getLocaleWithPath(path) {
417
830
  }
418
831
  //#endregion
419
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
+ */
420
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
+ */
421
864
  function resolveRouteLink(url, env) {
422
865
  if (isExternal(url)) return url;
423
866
  const config = getVitepressConfig();
@@ -440,8 +883,16 @@ function resolveRouteLink(url, env) {
440
883
  }
441
884
  return url;
442
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
+ */
443
894
  function normalizeHash(str) {
444
895
  return str ? encodeURI(`#${slugify(decodeURI(str).slice(1))}`) : "";
445
896
  }
446
897
  //#endregion
447
- export { EXTENSION_AUDIOS, EXTENSION_IMAGES, EXTENSION_VIDEOS, EXTERNAL_URL_RE, URL_PROTOCOL_RE, createContainerPlugin, createContainerSyntaxPlugin, createEmbedRuleBlock, createLocales, createLogger, getLocaleWithPath, getVitepressConfig, 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.2.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",