@roidmc/vitepress-uni-icons 0.0.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 ADDED
@@ -0,0 +1,718 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { encodeSvgForCss, getIconData, iconToHTML, iconToSVG } from "@iconify/utils";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/matcher.ts
7
+ /**
8
+ * 图标匹配器 - 负责将标签匹配到图标
9
+ */
10
+ var IconMatcher = class {
11
+ constructor(iconMap) {
12
+ this.iconMap = iconMap;
13
+ this.sortedKeys = Object.keys(iconMap).sort((a, b) => b.length - a.length);
14
+ }
15
+ /**
16
+ * 从标签集合中匹配图标
17
+ */
18
+ match(labels) {
19
+ const matched = /* @__PURE__ */ new Map();
20
+ for (const label of labels) {
21
+ const icon = this.matchOne(label);
22
+ if (icon) this.addMatch(matched, icon, label);
23
+ }
24
+ return Array.from(matched.values());
25
+ }
26
+ /**
27
+ * 匹配单个标签
28
+ */
29
+ matchOne(label) {
30
+ const normalizedLabel = label.toLowerCase();
31
+ if (this.iconMap[label]) return this.iconMap[label];
32
+ for (const key of this.sortedKeys) if (key.startsWith(".")) {
33
+ if (normalizedLabel.endsWith(key) || normalizedLabel.includes(key.slice(1))) return this.iconMap[key];
34
+ }
35
+ for (const key of this.sortedKeys) {
36
+ const normalizedKey = key.toLowerCase();
37
+ if (new RegExp(`\\b${normalizedKey}\\b`, "i").test(normalizedLabel)) return this.iconMap[key];
38
+ }
39
+ for (const key of this.sortedKeys) {
40
+ const normalizedKey = key.toLowerCase();
41
+ if (normalizedLabel.startsWith(normalizedKey)) return this.iconMap[key];
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * 添加匹配结果
47
+ */
48
+ addMatch(matched, icon, label) {
49
+ const key = this.getIconKey(icon);
50
+ const existing = matched.get(key);
51
+ if (existing) existing.labels.push(label);
52
+ else matched.set(key, {
53
+ icon,
54
+ labels: [label]
55
+ });
56
+ }
57
+ /**
58
+ * 获取图标唯一键
59
+ */
60
+ getIconKey(icon) {
61
+ return typeof icon === "string" ? icon : `light:${icon.light},dark:${icon.dark}`;
62
+ }
63
+ };
64
+ //#endregion
65
+ //#region src/resolver.ts
66
+ /**
67
+ * 图标解析器 - 负责将图标标识符转换为SVG内容
68
+ */
69
+ var IconResolver = class {
70
+ constructor() {
71
+ this.cache = /* @__PURE__ */ new Map();
72
+ this.apiEndpoint = "https://api.iconify.design";
73
+ }
74
+ /**
75
+ * 设置API端点
76
+ */
77
+ setApiEndpoint(endpoint) {
78
+ this.apiEndpoint = endpoint;
79
+ }
80
+ /**
81
+ * 设置开发服务器
82
+ */
83
+ setDevServer(server) {
84
+ this.devServer = server;
85
+ this.publicDir = server.config.publicDir;
86
+ }
87
+ /**
88
+ * 解析图标值并返回SVG内容
89
+ */
90
+ async resolve(icon) {
91
+ if (typeof icon !== "string") return this.resolve(icon.light);
92
+ const cached = this.cache.get(icon);
93
+ if (cached) return {
94
+ svg: cached,
95
+ success: true
96
+ };
97
+ try {
98
+ let svg = "";
99
+ if (icon.startsWith("<svg")) svg = encodeSvgForCss(icon);
100
+ else if (icon.startsWith("http://") || icon.startsWith("https://")) svg = await this.fetchIcon(icon);
101
+ else if (icon.startsWith("/")) svg = await this.fetchPublicIcon(icon);
102
+ else if (icon.includes(":")) svg = await this.resolveIconify(icon);
103
+ else return {
104
+ svg: "",
105
+ success: false,
106
+ error: `Invalid icon format: ${icon}`
107
+ };
108
+ if (svg) this.cache.set(icon, svg);
109
+ return {
110
+ svg,
111
+ success: true
112
+ };
113
+ } catch (error) {
114
+ return {
115
+ svg: "",
116
+ success: false,
117
+ error: `Failed to resolve icon: ${icon}, ${error}`
118
+ };
119
+ }
120
+ }
121
+ /**
122
+ * 解析Iconify图标 - 支持本地包和API
123
+ */
124
+ async resolveIconify(icon) {
125
+ const [collection, iconName] = icon.split(":");
126
+ if (!collection || !iconName) throw new Error("Invalid iconify icon format");
127
+ try {
128
+ const { icons } = createRequire(import.meta.url)(`@iconify-json/${collection}`);
129
+ const iconData = getIconData(icons, iconName);
130
+ if (iconData) {
131
+ const { attributes, body } = iconToSVG(iconData);
132
+ return encodeSvgForCss(iconToHTML(body, attributes));
133
+ }
134
+ } catch {}
135
+ try {
136
+ return await this.fetchFromAPI(collection, iconName);
137
+ } catch (error) {
138
+ throw new Error(`Failed to load icon: ${icon}. Please install @iconify-json/${collection} or check API availability`);
139
+ }
140
+ }
141
+ /**
142
+ * 从Iconify API获取图标
143
+ */
144
+ async fetchFromAPI(collection, iconName) {
145
+ const url = `${this.apiEndpoint}/${collection}.json?icons=${iconName}`;
146
+ try {
147
+ const data = await (await fetch(url)).json();
148
+ if (!data || !data.icons || !data.icons[iconName]) throw new Error(`Icon not found in API: ${collection}:${iconName}`);
149
+ const iconData = data.icons[iconName];
150
+ const { attributes, body } = iconToSVG({
151
+ body: iconData.body,
152
+ width: iconData.width || data.width || 24,
153
+ height: iconData.height || data.height || 24
154
+ });
155
+ return encodeSvgForCss(iconToHTML(body, attributes));
156
+ } catch (error) {
157
+ throw new Error(`Failed to fetch from Iconify API: ${url}`);
158
+ }
159
+ }
160
+ /**
161
+ * 从URL获取图标
162
+ */
163
+ async fetchIcon(url) {
164
+ try {
165
+ return encodeSvgForCss(await (await fetch(url)).text());
166
+ } catch (error) {
167
+ throw new Error(`Failed to fetch icon from: ${url}`);
168
+ }
169
+ }
170
+ /**
171
+ * 从public目录获取图标
172
+ */
173
+ async fetchPublicIcon(publicPath) {
174
+ const isBrowser = typeof window !== "undefined";
175
+ try {
176
+ if (isBrowser) return encodeSvgForCss(await (await fetch(publicPath)).text());
177
+ if (this.publicDir) {
178
+ const filePath = join(this.publicDir, publicPath);
179
+ if (existsSync(filePath)) return encodeSvgForCss(readFileSync(filePath, "utf-8"));
180
+ }
181
+ if (this.devServer) {
182
+ const port = this.devServer.config.server.port || 5173;
183
+ const response = await fetch(`http://localhost:${port}${publicPath}`);
184
+ if (response.ok) return encodeSvgForCss(await response.text());
185
+ }
186
+ return `__PUBLIC_ICON__:${publicPath}`;
187
+ } catch (error) {
188
+ return `__PUBLIC_ICON__:${publicPath}`;
189
+ }
190
+ }
191
+ /**
192
+ * 清除缓存
193
+ */
194
+ clearCache() {
195
+ this.cache.clear();
196
+ }
197
+ };
198
+ //#endregion
199
+ //#region src/generator.ts
200
+ /**
201
+ * CSS生成器 - 负责生成图标样式
202
+ */
203
+ var CSSGenerator = class {
204
+ constructor(options = {}) {
205
+ this.resolver = new IconResolver();
206
+ this.options = options;
207
+ }
208
+ /**
209
+ * 设置开发服务器
210
+ */
211
+ setDevServer(server) {
212
+ this.resolver.setDevServer(server);
213
+ }
214
+ /**
215
+ * 生成完整的CSS
216
+ */
217
+ async generate(matchedIcons) {
218
+ return this.generateBaseCSS() + await this.generateIconCSS(matchedIcons);
219
+ }
220
+ /**
221
+ * 生成基础CSS样式
222
+ */
223
+ generateBaseCSS() {
224
+ const style = this.options.style || {};
225
+ const iconSize = style.iconSize || "1em";
226
+ const iconMarginRight = style.iconMarginRight || "0.5em";
227
+ const iconMarginBottom = style.iconMarginBottom || "-0.2em";
228
+ const titleBarFontSize = style.titleBarFontSize || "14px";
229
+ const titleBarLineHeight = style.titleBarLineHeight || "48px";
230
+ const titleBarPadding = style.titleBarPadding || "0 12px";
231
+ return `
232
+ /* VitePress Uni Icons - Base Styles */
233
+ .vp-code-block-title [data-title]::before,
234
+ .vp-code-group [data-title]::before {
235
+ display: inline-block;
236
+ width: ${iconSize};
237
+ height: ${iconSize};
238
+ margin-right: ${iconMarginRight};
239
+ margin-bottom: ${iconMarginBottom};
240
+ background: var(--icon) no-repeat center / contain;
241
+ }
242
+
243
+ .vp-code-block-title-bar {
244
+ position: relative;
245
+ margin: 16px -24px 0 -24px;
246
+ background-color: var(--vp-code-block-bg);
247
+ overflow-x: auto;
248
+ font-size: ${titleBarFontSize};
249
+ font-weight: 500;
250
+ color: var(--vp-code-tab-text-color);
251
+ white-space: nowrap;
252
+ transition: background-color 0.5s;
253
+ border-radius: 8px 8px 0 0;
254
+ padding: ${titleBarPadding};
255
+ box-shadow: inset 0 -1px var(--vp-code-tab-divider);
256
+ }
257
+
258
+ .custom-block .vp-code-block-title-bar {
259
+ margin: 16px 0 0 0;
260
+ }
261
+
262
+ @media (min-width: 640px) {
263
+ .vp-code-block-title-bar {
264
+ margin: 16px 0 0 0;
265
+ }
266
+ }
267
+
268
+ .vp-code-block-title-text {
269
+ padding: ${titleBarPadding};
270
+ line-height: ${titleBarLineHeight};
271
+ }
272
+
273
+ .vp-code-block-title div[class*=language-] {
274
+ margin-top: 0 !important;
275
+ border-top-left-radius: 0 !important;
276
+ border-top-right-radius: 0 !important;
277
+ }
278
+ `;
279
+ }
280
+ /**
281
+ * 生成图标CSS
282
+ */
283
+ async generateIconCSS(matchedIcons) {
284
+ return (await Promise.all(matchedIcons.map(({ icon, labels }) => this.generateOneIconCSS(icon, labels)))).filter(Boolean).join("\n");
285
+ }
286
+ /**
287
+ * 生成单个图标的CSS
288
+ */
289
+ async generateOneIconCSS(icon, labels) {
290
+ const selectors = labels.map((label) => `[data-title='${label}']::before`);
291
+ if (typeof icon !== "string") return this.generateThemedIconCSS(icon, selectors);
292
+ const result = await this.resolver.resolve(icon);
293
+ if (!result.success) {
294
+ if (this.options.debug) console.warn(`[vitepress-uni-icons] ${result.error}`);
295
+ return "";
296
+ }
297
+ if (result.svg.startsWith("__PUBLIC_ICON__:")) {
298
+ const publicPath = result.svg.replace("__PUBLIC_ICON__:", "");
299
+ return this.generatePublicIconRule(selectors, publicPath);
300
+ }
301
+ return this.generateIconRule(selectors, result.svg);
302
+ }
303
+ /**
304
+ * 生成public图标CSS规则(运行时加载)
305
+ */
306
+ generatePublicIconRule(selectors, publicPath) {
307
+ return `
308
+ ${selectors.join(",")} {
309
+ content: '';
310
+ --icon: url("${publicPath}");
311
+ }`;
312
+ }
313
+ /**
314
+ * 生成主题图标CSS
315
+ */
316
+ async generateThemedIconCSS(icon, selectors) {
317
+ const parts = [];
318
+ const lightResult = await this.resolver.resolve(icon.light);
319
+ if (lightResult.success) parts.push(this.generateIconRule(selectors, lightResult.svg));
320
+ const darkResult = await this.resolver.resolve(icon.dark);
321
+ if (darkResult.success) parts.push(this.generateIconRule(selectors, darkResult.svg, "html.dark"));
322
+ return parts.join("\n");
323
+ }
324
+ /**
325
+ * 生成CSS规则
326
+ */
327
+ generateIconRule(selectors, svg, prefix) {
328
+ return `
329
+ ${prefix ? selectors.map((s) => `${prefix} ${s}`).join(",") : selectors.join(",")} {
330
+ content: '';
331
+ --icon: url("data:image/svg+xml,${svg}");
332
+ }`;
333
+ }
334
+ };
335
+ //#endregion
336
+ //#region src/builtin.ts
337
+ /**
338
+ * 内置图标映射表
339
+ */
340
+ var builtinIcons = {
341
+ pnpm: {
342
+ light: "vscode-icons:file-type-light-pnpm",
343
+ dark: "vscode-icons:file-type-pnpm"
344
+ },
345
+ npm: "vscode-icons:file-type-npm",
346
+ yarn: "vscode-icons:file-type-yarn",
347
+ bun: "vscode-icons:file-type-bun",
348
+ deno: "vscode-icons:file-type-deno",
349
+ vitepress: "vscode-icons:file-type-vitepress",
350
+ vue: "vscode-icons:file-type-vue",
351
+ svelte: "vscode-icons:file-type-svelte",
352
+ angular: "vscode-icons:file-type-angular",
353
+ react: "vscode-icons:file-type-reactjs",
354
+ solid: "logos:solidjs-icon",
355
+ marko: "vscode-icons:file-type-marko",
356
+ next: {
357
+ light: "vscode-icons:file-type-light-next",
358
+ dark: "vscode-icons:file-type-next"
359
+ },
360
+ nuxt: "vscode-icons:file-type-nuxt",
361
+ astro: {
362
+ light: "vscode-icons:file-type-light-astro",
363
+ dark: "vscode-icons:file-type-astro"
364
+ },
365
+ qwik: "logos:qwik-icon",
366
+ ember: "vscode-icons:file-type-ember",
367
+ vitest: {
368
+ light: "vscode-icons:file-type-light-vitest",
369
+ dark: "vscode-icons:file-type-vitest"
370
+ },
371
+ rollup: "vscode-icons:file-type-rollup",
372
+ webpack: "vscode-icons:file-type-webpack",
373
+ vite: {
374
+ light: "vscode-icons:file-type-light-vite",
375
+ dark: "vscode-icons:file-type-vite"
376
+ },
377
+ esbuild: "vscode-icons:file-type-esbuild",
378
+ rolldown: {
379
+ light: "vscode-icons:file-type-light-rolldown",
380
+ dark: "vscode-icons:file-type-rolldown"
381
+ },
382
+ "package.json": "vscode-icons:file-type-node",
383
+ "tsconfig.json": "vscode-icons:file-type-tsconfig",
384
+ ".npmrc": "vscode-icons:file-type-npm",
385
+ ".editorconfig": "vscode-icons:file-type-editorconfig",
386
+ ".eslintrc": "vscode-icons:file-type-eslint",
387
+ ".eslintignore": "vscode-icons:file-type-eslint",
388
+ "eslint.config": "vscode-icons:file-type-eslint",
389
+ ".gitignore": "vscode-icons:file-type-git",
390
+ ".gitattributes": "vscode-icons:file-type-git",
391
+ ".env": "vscode-icons:file-type-dotenv",
392
+ ".env.example": "vscode-icons:file-type-dotenv",
393
+ ".vscode": "vscode-icons:file-type-vscode",
394
+ "tailwind.config": "vscode-icons:file-type-tailwind",
395
+ "uno.config": "vscode-icons:file-type-unocss",
396
+ "unocss.config": "vscode-icons:file-type-unocss",
397
+ "vue.config": "vscode-icons:file-type-vueconfig",
398
+ ".mts": "vscode-icons:file-type-typescript",
399
+ ".cts": "vscode-icons:file-type-typescript",
400
+ ".ts": "vscode-icons:file-type-typescript",
401
+ ".tsx": "vscode-icons:file-type-typescript",
402
+ ".mjs": "vscode-icons:file-type-js",
403
+ ".cjs": "vscode-icons:file-type-js",
404
+ ".json": "vscode-icons:file-type-json",
405
+ ".js": "vscode-icons:file-type-js",
406
+ ".jsx": "vscode-icons:file-type-js",
407
+ ".md": "vscode-icons:file-type-markdown",
408
+ ".py": "vscode-icons:file-type-python",
409
+ ".ico": "vscode-icons:file-type-favicon",
410
+ ".html": "vscode-icons:file-type-html",
411
+ ".css": "vscode-icons:file-type-css",
412
+ ".scss": "vscode-icons:file-type-scss",
413
+ ".yml": {
414
+ light: "vscode-icons:file-type-light-yaml",
415
+ dark: "vscode-icons:file-type-yaml"
416
+ },
417
+ ".yaml": {
418
+ light: "vscode-icons:file-type-light-yaml",
419
+ dark: "vscode-icons:file-type-yaml"
420
+ },
421
+ ".php": "vscode-icons:file-type-php",
422
+ ".gjs": "vscode-icons:file-type-glimmer",
423
+ ".gts": "vscode-icons:file-type-glimmer",
424
+ oxlint: {
425
+ light: "vscode-icons:file-type-light-oxc",
426
+ dark: "vscode-icons:file-type-oxc"
427
+ },
428
+ oxc: {
429
+ light: "vscode-icons:file-type-light-oxc",
430
+ dark: "vscode-icons:file-type-oxc"
431
+ },
432
+ oxfmt: {
433
+ light: "vscode-icons:file-type-light-oxc",
434
+ dark: "vscode-icons:file-type-oxc"
435
+ }
436
+ };
437
+ //#endregion
438
+ //#region src/scanner.ts
439
+ /**
440
+ * 图标扫描器 - 自动扫描已安装的iconify图标集
441
+ */
442
+ var IconScanner = class {
443
+ /**
444
+ * 扫描已安装的图标集并生成图标映射
445
+ * @param iconSets 用户配置的图标集
446
+ */
447
+ scan(iconSets) {
448
+ const result = {};
449
+ if (iconSets && iconSets.length > 0) for (const iconSet of iconSets) this.scanIconSet(result, iconSet);
450
+ this.scanVSCodeIcons(result);
451
+ this.scanLogos(result);
452
+ return result;
453
+ }
454
+ /**
455
+ * 扫描用户配置的图标集
456
+ */
457
+ scanIconSet(result, config) {
458
+ if (config.icons) {
459
+ const iconsData = config.icons.icons || config.icons;
460
+ this.scanIconData(result, config.name, iconsData, config.scan);
461
+ return;
462
+ }
463
+ try {
464
+ const module = createRequire(import.meta.url)(`@iconify-json/${config.name}`);
465
+ const icons = module.icons || module;
466
+ if (icons && typeof icons === "object") this.scanIconData(result, config.name, icons, config.scan);
467
+ } catch (e) {}
468
+ }
469
+ /**
470
+ * 扫描图标数据
471
+ */
472
+ scanIconData(result, collectionName, icons, scanConfig) {
473
+ if (!icons || typeof icons !== "object") return;
474
+ const prefix = scanConfig?.prefix;
475
+ const suffix = scanConfig?.suffix;
476
+ const theme = scanConfig?.theme;
477
+ const custom = scanConfig?.custom;
478
+ for (const iconName of Object.keys(icons)) {
479
+ if (custom) {
480
+ const mapped = custom(iconName, icons[iconName]);
481
+ if (mapped) {
482
+ const [key, value] = mapped;
483
+ if (!result[key]) result[key] = value;
484
+ }
485
+ continue;
486
+ }
487
+ let key = iconName;
488
+ if (prefix && iconName.startsWith(prefix)) key = iconName.slice(prefix.length);
489
+ if (suffix && iconName.endsWith(suffix)) key = iconName.slice(0, -suffix.length);
490
+ if (theme && key.startsWith("light-")) {
491
+ const baseName = key.slice(6);
492
+ if (baseName && !result[baseName]) {
493
+ const darkIcon = `${collectionName}:${prefix || ""}${baseName}`;
494
+ if (icons[`${prefix || ""}${baseName}`]) result[baseName] = {
495
+ light: `${collectionName}:${iconName}`,
496
+ dark: darkIcon
497
+ };
498
+ }
499
+ continue;
500
+ }
501
+ if (key && !result[key]) result[key] = `${collectionName}:${iconName}`;
502
+ }
503
+ }
504
+ /**
505
+ * 扫描 vscode-icons 图标集
506
+ */
507
+ scanVSCodeIcons(result) {
508
+ try {
509
+ const module = createRequire(import.meta.url)("@iconify-json/vscode-icons");
510
+ const icons = module.icons?.icons || module.icons;
511
+ if (icons && typeof icons === "object") {
512
+ for (const iconName of Object.keys(icons)) if (iconName.startsWith("file-type-")) {
513
+ const suffix = iconName.slice(10);
514
+ if (suffix) {
515
+ if (suffix.startsWith("light-")) {
516
+ const baseName = suffix.slice(6);
517
+ if (baseName && !result[baseName]) {
518
+ const darkIcon = `vscode-icons:file-type-${baseName}`;
519
+ if (icons[`file-type-${baseName}`]) result[baseName] = {
520
+ light: `vscode-icons:${iconName}`,
521
+ dark: darkIcon
522
+ };
523
+ }
524
+ } else if (!result[suffix]) result[suffix] = `vscode-icons:${iconName}`;
525
+ }
526
+ }
527
+ }
528
+ } catch (e) {}
529
+ }
530
+ /**
531
+ * 扫描 logos 图标集
532
+ */
533
+ scanLogos(result) {
534
+ try {
535
+ const module = createRequire(import.meta.url)("@iconify-json/logos");
536
+ const icons = module.icons?.icons || module.icons;
537
+ if (icons && typeof icons === "object") {
538
+ for (const iconName of Object.keys(icons)) if (iconName.endsWith("-icon")) {
539
+ const framework = iconName.slice(0, -5);
540
+ if (framework && !result[framework]) result[framework] = `logos:${iconName}`;
541
+ } else if (!result[iconName]) result[iconName] = `logos:${iconName}`;
542
+ }
543
+ } catch (e) {}
544
+ }
545
+ };
546
+ //#endregion
547
+ //#region src/plugin.ts
548
+ /**
549
+ * Vite插件 - 核心功能
550
+ */
551
+ function createPlugin(options = {}) {
552
+ const virtualCSSId = "virtual:uni-icons.css";
553
+ const resolvedCSSId = `\0${virtualCSSId}`;
554
+ const matcher = new IconMatcher({
555
+ ...new IconScanner().scan(options.iconSets),
556
+ ...options.disableBuiltin ? {} : builtinIcons,
557
+ ...options.customIcons
558
+ });
559
+ const generator = new CSSGenerator(options);
560
+ const matchedLabels = /* @__PURE__ */ new Set();
561
+ let previousLabels = null;
562
+ let server;
563
+ /**
564
+ * 检查标签是否变化
565
+ */
566
+ function hasLabelsChanged() {
567
+ if (!previousLabels) return true;
568
+ if (matchedLabels.size !== previousLabels.size) return true;
569
+ for (const label of matchedLabels) if (!previousLabels.has(label)) return true;
570
+ return false;
571
+ }
572
+ /**
573
+ * 触发CSS模块更新
574
+ */
575
+ function triggerUpdate() {
576
+ if (!server) return;
577
+ const mod = server.moduleGraph.getModuleById(resolvedCSSId);
578
+ if (mod) {
579
+ server.moduleGraph.invalidateModule(mod);
580
+ server.reloadModule(mod);
581
+ }
582
+ }
583
+ return {
584
+ name: "vitepress-uni-icons",
585
+ enforce: "post",
586
+ resolveId(id) {
587
+ if (id === virtualCSSId) return resolvedCSSId;
588
+ return null;
589
+ },
590
+ configureServer(_server) {
591
+ server = _server;
592
+ generator.setDevServer(_server);
593
+ },
594
+ async load(id) {
595
+ if (id !== resolvedCSSId) return null;
596
+ const allLabels = new Set([...matchedLabels, ...options.defaultLabels || []]);
597
+ const matchedIcons = matcher.match(allLabels);
598
+ const css = await generator.generate(matchedIcons);
599
+ previousLabels = new Set(matchedLabels);
600
+ return css;
601
+ },
602
+ transform: {
603
+ filter: { id: /\.(md|md\?vue|md\?v=)$/ },
604
+ handler(code) {
605
+ const regex = /\bdata-title=\\"([^"]*)\\"|\bdata-title="([^"]*)"|"data-title":\s*"([^"]*)"/g;
606
+ let match;
607
+ while ((match = regex.exec(code)) !== null) {
608
+ const label = match[1] || match[2] || match[3];
609
+ if (label) matchedLabels.add(label);
610
+ }
611
+ if (hasLabelsChanged()) triggerUpdate();
612
+ return null;
613
+ }
614
+ }
615
+ };
616
+ }
617
+ //#endregion
618
+ //#region src/markdown.ts
619
+ /**
620
+ * Markdown插件 - 处理代码组和代码块
621
+ */
622
+ function createMarkdownPlugin(md, options = {}) {
623
+ const titleBarOptions = options.titleBar || {};
624
+ processCodeGroup(md);
625
+ processCodeBlock(md, titleBarOptions);
626
+ }
627
+ /**
628
+ * 处理代码组
629
+ */
630
+ function processCodeGroup(md) {
631
+ const labelRE = /<label\b(?![^>]+\bdata-title\b)[^>]*>(.*?)<\/label>/g;
632
+ const originalRule = md.renderer.rules["container_code-group_open"];
633
+ if (!originalRule) return;
634
+ md.renderer.rules["container_code-group_open"] = (...args) => {
635
+ return originalRule(...args).replace(labelRE, (match, label) => `<label data-title="${md.utils.escapeHtml(label)}"${match.slice(6)}`);
636
+ };
637
+ }
638
+ /**
639
+ * 处理代码块标题
640
+ */
641
+ function processCodeBlock(md, options) {
642
+ const originalRule = md.renderer.rules.fence;
643
+ if (!originalRule) return;
644
+ md.renderer.rules.fence = (...args) => {
645
+ const [tokens, idx] = args;
646
+ const token = tokens[idx];
647
+ if (checkIfInCodeGroup(tokens, idx)) return originalRule(...args);
648
+ const titleMatch = token.info.match(/\[((?:[^[\]]|\[[^[\]]*\])*)\]/);
649
+ if (!titleMatch) return originalRule(...args);
650
+ if (token.src && !options.includeSnippet) return originalRule(...args);
651
+ const title = titleMatch[1];
652
+ return `
653
+ <div class="vp-code-block-title">
654
+ <div class="vp-code-block-title-bar">
655
+ <span class="vp-code-block-title-text" data-title="${md.utils.escapeHtml(title)}">${title}</span>
656
+ </div>
657
+ ${originalRule(...args)}
658
+ </div>
659
+ `;
660
+ };
661
+ }
662
+ /**
663
+ * 检查当前token是否在代码组内
664
+ */
665
+ function checkIfInCodeGroup(tokens, currentIndex) {
666
+ for (let i = currentIndex - 1; i >= 0; i--) {
667
+ if (tokens[i].type === "container_code-group_open") return true;
668
+ if (tokens[i].type === "container_code-group_close") return false;
669
+ }
670
+ return false;
671
+ }
672
+ //#endregion
673
+ //#region src/loader.ts
674
+ /**
675
+ * 本地图标加载器 - 从相对路径加载SVG文件
676
+ *
677
+ * @param url - import.meta.url (当前模块的URL)
678
+ * @param relativePath - 相对于当前模块的SVG文件路径
679
+ * @returns SVG字符串
680
+ *
681
+ * @example
682
+ * // 从assets目录加载
683
+ * 'vitepress': localIconLoader(import.meta.url, '../assets/vitepress.svg')
684
+ *
685
+ * // 从同目录加载
686
+ * 'my-icon': localIconLoader(import.meta.url, './my-icon.svg')
687
+ */
688
+ function localIconLoader(url, relativePath) {
689
+ try {
690
+ return readFileSync(resolve(dirname(fileURLToPath(url)), relativePath), "utf-8");
691
+ } catch (error) {
692
+ console.warn(`[vitepress-uni-icons] Failed to load icon from: ${relativePath}`, error);
693
+ return "";
694
+ }
695
+ }
696
+ /**
697
+ * Public图标加载器 - 从VitePress的public目录加载
698
+ * 注意: 这个函数返回的是public路径,需要在运行时通过fetch加载
699
+ *
700
+ * @param publicPath - public目录下的文件路径
701
+ * @returns Public路径标识(以 / 开头)
702
+ *
703
+ * @example
704
+ * // 从public目录加载
705
+ * 'my-framework': publicIconLoader('icons/git.svg')
706
+ *
707
+ * // 简写形式
708
+ * 'my-framework': publicIconLoader('git.svg') // 对应 public/git.svg
709
+ */
710
+ function publicIconLoader(publicPath) {
711
+ return publicPath.startsWith("/") ? publicPath : `/${publicPath}`;
712
+ }
713
+ //#endregion
714
+ //#region src/index.ts
715
+ var uniIconsPlugin = createPlugin;
716
+ var uniIconsMarkdown = createMarkdownPlugin;
717
+ //#endregion
718
+ export { CSSGenerator, IconMatcher, IconResolver, IconScanner, builtinIcons, localIconLoader, publicIconLoader, uniIconsMarkdown, uniIconsPlugin };