@sugarat/theme 0.3.6 → 0.4.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/README.md CHANGED
@@ -42,7 +42,7 @@ pnpm build
42
42
  ```sh
43
43
  pnpm serve
44
44
  ```
45
- ## Advanced Ssage
45
+ ## Advanced Usage
46
46
  详细配置见文档 https://theme.sugarat.top
47
47
 
48
48
  ## Thanks
package/node.d.ts CHANGED
@@ -137,6 +137,10 @@ declare namespace Theme {
137
137
  server: string;
138
138
  }
139
139
  interface HotArticle {
140
+ /**
141
+ * 自定义标题,支持SVG + 文字
142
+ * @default '🔥 精选文章'
143
+ */
140
144
  title?: string;
141
145
  pageSize?: number;
142
146
  nextText?: string;
@@ -275,6 +279,11 @@ declare namespace Theme {
275
279
  * @default "动态计算"
276
280
  */
277
281
  scrollSpeed?: number;
282
+ /**
283
+ * 自定义展示标题,支持SVG + 文字
284
+ * @default '🤝 友情链接'
285
+ */
286
+ title?: string;
278
287
  }
279
288
  interface UserWork {
280
289
  title: string;
@@ -375,7 +384,8 @@ declare namespace Theme {
375
384
  mermaid?: any;
376
385
  /**
377
386
  * 设置解析 frontmatter 里 date 的时区
378
- * @default 8 => 'UTC+8'
387
+ * @default new Date().getTimezoneOffset() / -60
388
+ * @example 8 => 'UTC+8'
379
389
  */
380
390
  timeZone?: number;
381
391
  /**
@@ -418,7 +428,7 @@ declare namespace Theme {
418
428
  * 详见 https://oml2d.com/options/Options.html
419
429
  */
420
430
  oml2d?: Options;
421
- homeTags?: boolean;
431
+ homeTags?: boolean | HomeTagsConfig;
422
432
  buttonAfterArticle?: ButtonAfterArticleConfig | false;
423
433
  /**
424
434
  * 是否开启深色模式过渡动画
@@ -545,6 +555,13 @@ declare namespace Theme {
545
555
  */
546
556
  coverPreview?: ReplaceRule | ReplaceRule[];
547
557
  }
558
+ interface HomeTagsConfig {
559
+ /**
560
+ * 自定义标题,支持SVG + 文字
561
+ * @default '🏷 标签'
562
+ */
563
+ title?: string;
564
+ }
548
565
  }
549
566
 
550
567
  /**
package/node.js CHANGED
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(node_exports);
40
40
  // src/utils/node/mdPlugins.ts
41
41
  var import_module = require("module");
42
42
 
43
- // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.2.2_@algolia+client-search@4.19.1_@types+node@20.6.3__yypdqxhxkwwat3gkog3hzzeasy/node_modules/vitepress-plugin-tabs/dist/index.js
43
+ // ../../node_modules/.pnpm/vitepress-plugin-tabs@0.2.0_vitepress@1.2.3_@algolia+client-search@4.19.1_@types+node@20.6.3__tasys46ln23ubdv2to2chczqqi/node_modules/vitepress-plugin-tabs/dist/index.js
44
44
  var tabsMarker = "=tabs";
45
45
  var tabsMarkerLen = tabsMarker.length;
46
46
  var ruleBlockTabs = (state, startLine, endLine, silent) => {
@@ -214,17 +214,25 @@ function getDefaultTitle(content) {
214
214
  const match = content.match(/^(#+)\s+(.+)/m);
215
215
  return match?.[2] || "";
216
216
  }
217
+ var cache = /* @__PURE__ */ new Map();
217
218
  function getFileBirthTime(url) {
218
- let date = /* @__PURE__ */ new Date();
219
- try {
220
- const infoStr = (0, import_node_child_process.spawnSync)("git", ["log", "-1", '--pretty="%ci"', url]).stdout?.toString().replace(/["']/g, "").trim();
221
- if (infoStr) {
222
- date = new Date(infoStr);
223
- }
224
- } catch (error) {
225
- return date;
226
- }
227
- return date;
219
+ const cached = cache.get(url);
220
+ if (cached) {
221
+ return cached;
222
+ }
223
+ return new Promise((resolve) => {
224
+ const child = (0, import_node_child_process.spawn)("git", ["log", "-1", '--pretty="%ai"', url]);
225
+ let output = "";
226
+ child.stdout.on("data", (d) => output += String(d));
227
+ child.on("close", () => {
228
+ const date = new Date(output);
229
+ cache.set(url, date);
230
+ resolve(date);
231
+ });
232
+ child.on("error", () => {
233
+ resolve(void 0);
234
+ });
235
+ });
228
236
  }
229
237
  function getTextSummary(text, count = 100) {
230
238
  return text?.replace(/^#+\s+.*/, "")?.replace(/#/g, "")?.replace(/!\[.*?\]\(.*?\)/g, "")?.replace(/\[(.*?)\]\(.*?\)/g, "$1")?.replace(/\*\*(.*?)\*\*/g, "$1")?.split("\n")?.filter((v) => !!v)?.join("\n")?.replace(/>(.*)/, "")?.replace(/</g, "&lt;").replace(/>/g, "&gt;")?.trim()?.slice(0, count);
@@ -353,9 +361,113 @@ function patchOptimizeDeps(config) {
353
361
  var import_node_fs = __toESM(require("fs"));
354
362
  var import_node_path2 = __toESM(require("path"));
355
363
  var import_node_process = __toESM(require("process"));
364
+ var import_node_os = __toESM(require("os"));
356
365
  var import_fast_glob = __toESM(require("fast-glob"));
357
366
  var import_gray_matter = __toESM(require("gray-matter"));
358
367
 
368
+ // ../../node_modules/.pnpm/yocto-queue@1.0.0/node_modules/yocto-queue/index.js
369
+ var Node = class {
370
+ value;
371
+ next;
372
+ constructor(value) {
373
+ this.value = value;
374
+ }
375
+ };
376
+ var Queue = class {
377
+ #head;
378
+ #tail;
379
+ #size;
380
+ constructor() {
381
+ this.clear();
382
+ }
383
+ enqueue(value) {
384
+ const node = new Node(value);
385
+ if (this.#head) {
386
+ this.#tail.next = node;
387
+ this.#tail = node;
388
+ } else {
389
+ this.#head = node;
390
+ this.#tail = node;
391
+ }
392
+ this.#size++;
393
+ }
394
+ dequeue() {
395
+ const current = this.#head;
396
+ if (!current) {
397
+ return;
398
+ }
399
+ this.#head = this.#head.next;
400
+ this.#size--;
401
+ return current.value;
402
+ }
403
+ clear() {
404
+ this.#head = void 0;
405
+ this.#tail = void 0;
406
+ this.#size = 0;
407
+ }
408
+ get size() {
409
+ return this.#size;
410
+ }
411
+ *[Symbol.iterator]() {
412
+ let current = this.#head;
413
+ while (current) {
414
+ yield current.value;
415
+ current = current.next;
416
+ }
417
+ }
418
+ };
419
+
420
+ // ../../node_modules/.pnpm/p-limit@4.0.0/node_modules/p-limit/index.js
421
+ function pLimit(concurrency) {
422
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
423
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
424
+ }
425
+ const queue = new Queue();
426
+ let activeCount = 0;
427
+ const next = () => {
428
+ activeCount--;
429
+ if (queue.size > 0) {
430
+ queue.dequeue()();
431
+ }
432
+ };
433
+ const run = async (fn, resolve, args) => {
434
+ activeCount++;
435
+ const result = (async () => fn(...args))();
436
+ resolve(result);
437
+ try {
438
+ await result;
439
+ } catch {
440
+ }
441
+ next();
442
+ };
443
+ const enqueue = (fn, resolve, args) => {
444
+ queue.enqueue(run.bind(void 0, fn, resolve, args));
445
+ (async () => {
446
+ await Promise.resolve();
447
+ if (activeCount < concurrency && queue.size > 0) {
448
+ queue.dequeue()();
449
+ }
450
+ })();
451
+ };
452
+ const generator = (fn, ...args) => new Promise((resolve) => {
453
+ enqueue(fn, resolve, args);
454
+ });
455
+ Object.defineProperties(generator, {
456
+ activeCount: {
457
+ get: () => activeCount
458
+ },
459
+ pendingCount: {
460
+ get: () => queue.size
461
+ },
462
+ clearQueue: {
463
+ value: () => {
464
+ queue.clear();
465
+ }
466
+ }
467
+ });
468
+ return generator;
469
+ }
470
+
359
471
  // src/utils/client/index.ts
360
472
  function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
361
473
  if (!(d instanceof Date)) {
@@ -424,8 +536,8 @@ function getPageRoute(filepath, srcDir) {
424
536
  return `/${route}`;
425
537
  }
426
538
  var defaultTimeZoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset() / -60;
427
- function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
428
- const fileContent = import_node_fs.default.readFileSync(filepath, "utf-8");
539
+ async function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
540
+ const fileContent = await import_node_fs.default.promises.readFile(filepath, "utf-8");
429
541
  const { data: frontmatter, excerpt, content } = (0, import_gray_matter.default)(fileContent, {
430
542
  excerpt: true
431
543
  });
@@ -435,12 +547,9 @@ function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
435
547
  if (!meta.title) {
436
548
  meta.title = getDefaultTitle(content);
437
549
  }
438
- if (!meta.date) {
439
- meta.date = formatDate(getFileBirthTime(filepath));
440
- } else {
441
- meta.date = formatDate(
442
- /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
443
- );
550
+ const date = await (meta.date && /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`) || getFileBirthTime(filepath));
551
+ if (date) {
552
+ meta.date = formatDate(date);
444
553
  }
445
554
  meta.categories = typeof meta.categories === "string" ? [meta.categories] : meta.categories;
446
555
  meta.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags;
@@ -455,17 +564,31 @@ function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
455
564
  }
456
565
  return meta;
457
566
  }
458
- function getArticles(cfg) {
567
+ async function getArticles(cfg) {
459
568
  const srcDir = cfg?.srcDir || import_node_process.default.argv.slice(2)?.[1] || ".";
460
569
  const files = import_fast_glob.default.sync(`${srcDir}/**/*.md`, { ignore: ["node_modules"] });
461
- const pageData = files.map((filepath) => {
462
- const route = getPageRoute(filepath, srcDir);
463
- const meta = getArticleMeta(filepath, route, cfg?.timeZone);
464
- return {
570
+ const limit = pLimit(+(import_node_process.default.env.P_LIMT_MAX || import_node_os.default.cpus().length));
571
+ const metaResults = files.reduce((prev, curr) => {
572
+ const route = getPageRoute(curr, srcDir);
573
+ const metaPromise = limit(() => getArticleMeta(curr, route, cfg?.timeZone));
574
+ prev[curr] = {
465
575
  route,
466
- meta
576
+ metaPromise
467
577
  };
468
- }).filter((v) => v.meta.layout !== "home");
578
+ return prev;
579
+ }, {});
580
+ const pageData = [];
581
+ for (const file of files) {
582
+ const { route, metaPromise } = metaResults[file];
583
+ const meta = await metaPromise;
584
+ if (meta.layout === "home") {
585
+ continue;
586
+ }
587
+ pageData.push({
588
+ route,
589
+ meta
590
+ });
591
+ }
469
592
  return pageData;
470
593
  }
471
594
  function patchVPConfig(vpConfig, cfg) {
@@ -487,8 +610,6 @@ function checkConfig(cfg) {
487
610
 
488
611
  // src/utils/node/vitePlugins.ts
489
612
  var import_node_path3 = __toESM(require("path"));
490
- var import_node_child_process2 = require("child_process");
491
- var import_node_process2 = __toESM(require("process"));
492
613
  var import_node_fs2 = require("fs");
493
614
  var import_node_buffer = require("buffer");
494
615
  var import_vitepress_plugin_pagefind = require("vitepress-plugin-pagefind");
@@ -509,18 +630,18 @@ function themeReloadPlugin() {
509
630
  const restart = debounce(() => {
510
631
  server.restart();
511
632
  }, 500);
512
- server.watcher.on("add", (path4) => {
633
+ server.watcher.on("add", async (path4) => {
513
634
  const route = generateRoute(path4);
514
- const meta = getArticleMeta(path4, route, blogConfig?.timeZone);
635
+ const meta = await getArticleMeta(path4, route, blogConfig?.timeZone);
515
636
  blogConfig.pagesData.push({
516
637
  route,
517
638
  meta
518
639
  });
519
640
  restart();
520
641
  });
521
- server.watcher.on("change", (path4) => {
642
+ server.watcher.on("change", async (path4) => {
522
643
  const route = generateRoute(path4);
523
- const meta = getArticleMeta(path4, route, blogConfig?.timeZone);
644
+ const meta = await getArticleMeta(path4, route, blogConfig?.timeZone);
524
645
  const matched = blogConfig.pagesData.find((v) => v.route === route);
525
646
  if (matched && !isEqual(matched.meta, meta, ["date", "description"])) {
526
647
  matched.meta = meta;
@@ -547,10 +668,9 @@ function themeReloadPlugin() {
547
668
  // src/utils/node/vitePlugins.ts
548
669
  function getVitePlugins(cfg) {
549
670
  const plugins = [];
550
- const buildEndFn = [];
551
- plugins.push(inlineBuildEndPlugin(buildEndFn));
552
671
  plugins.push(coverImgTransform());
553
672
  plugins.push(themeReloadPlugin());
673
+ plugins.push(providePageData(cfg));
554
674
  if (cfg && cfg.search !== false) {
555
675
  const ops = cfg.search instanceof Object ? cfg.search : {};
556
676
  plugins.push(
@@ -565,6 +685,7 @@ function getVitePlugins(cfg) {
565
685
  }
566
686
  if (cfg?.mermaid !== false) {
567
687
  const { MermaidPlugin } = _require("vitepress-plugin-mermaid");
688
+ plugins.push(inlineInjectMermaidClient());
568
689
  plugins.push(MermaidPlugin(cfg?.mermaid === true ? {} : cfg?.mermaid ?? {}));
569
690
  }
570
691
  if (cfg?.RSS) {
@@ -577,25 +698,15 @@ function registerVitePlugins(vpCfg, plugins) {
577
698
  plugins
578
699
  };
579
700
  }
580
- function inlineBuildEndPlugin(buildEndFn) {
581
- let rewrite = false;
701
+ function inlineInjectMermaidClient() {
582
702
  return {
583
- name: "@sugarar/theme-plugin-build-end",
703
+ name: "@sugarat/theme-plugin-inline-inject-mermaid-client",
584
704
  enforce: "pre",
585
- configResolved(config) {
586
- if (rewrite) {
587
- return;
705
+ transform(code, id) {
706
+ if (id.endsWith("src/index.ts") && code.startsWith("// @sugarat/theme index")) {
707
+ return code.replace("// replace-mermaid-import-code", "import Mermaid from 'vitepress-plugin-mermaid/Mermaid.vue'").replace("// replace-mermaid-mounted-code", "if (!ctx.app.component('Mermaid')) { ctx.app.component('Mermaid', Mermaid as any) }");
588
708
  }
589
- const vitepressConfig = config.vitepress;
590
- if (!vitepressConfig) {
591
- return;
592
- }
593
- rewrite = true;
594
- const selfBuildEnd = vitepressConfig.buildEnd;
595
- vitepressConfig.buildEnd = (siteCfg) => {
596
- selfBuildEnd?.(siteCfg);
597
- buildEndFn.filter((fn) => typeof fn === "function").forEach((fn) => fn(siteCfg));
598
- };
709
+ return code;
599
710
  }
600
711
  };
601
712
  }
@@ -640,17 +751,31 @@ function coverImgTransform() {
640
751
  }
641
752
  };
642
753
  }
754
+ function providePageData(cfg) {
755
+ return {
756
+ name: "@sugarat/theme-plugin-provide-page-data",
757
+ async config(config) {
758
+ const pagesData = await getArticles(cfg);
759
+ config.vitepress.site.themeConfig.blog.pagesData = pagesData;
760
+ }
761
+ };
762
+ }
643
763
 
644
764
  // src/node.ts
645
- function getThemeConfig(cfg) {
765
+ function getThemeConfig(cfg = {}) {
646
766
  checkConfig(cfg);
647
- const pagesData = getArticles(cfg);
648
- const extraVPConfig = {};
767
+ cfg.mermaid = cfg.mermaid ?? false;
768
+ const pagesData = [];
769
+ const extraVPConfig = {
770
+ vite: {}
771
+ };
649
772
  const vitePlugins = getVitePlugins(cfg);
650
773
  registerVitePlugins(extraVPConfig, vitePlugins);
651
774
  const markdownPlugin = getMarkdownPlugins(cfg);
652
775
  registerMdPlugins(extraVPConfig, markdownPlugin);
653
- patchMermaidPluginCfg(extraVPConfig);
776
+ if (cfg?.mermaid !== false) {
777
+ patchMermaidPluginCfg(extraVPConfig);
778
+ }
654
779
  patchOptimizeDeps(extraVPConfig);
655
780
  patchVPConfig(extraVPConfig, cfg);
656
781
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "author": "sugar",
6
6
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "@mermaid-js/mermaid-mindmap": "^9.3.0",
43
43
  "@vue/shared": "^3.4.26",
44
44
  "@vueuse/core": "^9.13.0",
45
+ "cross-spawn": "^7.0.3",
45
46
  "fast-glob": "^3.3.2",
46
47
  "gray-matter": "^4.0.3",
47
48
  "markdown-it-task-checkbox": "^1.0.6",
@@ -51,24 +52,25 @@
51
52
  "vitepress-markdown-timeline": "^1.2.1",
52
53
  "vitepress-plugin-mermaid": "2.0.13",
53
54
  "vitepress-plugin-tabs": "0.2.0",
54
- "vitepress-plugin-pagefind": "0.3.3",
55
+ "vitepress-plugin-pagefind": "0.4.1",
55
56
  "vitepress-plugin-rss": "0.2.6"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@element-plus/icons-vue": "^2.3.1",
59
60
  "artalk": "^2.8.5",
60
61
  "element-plus": "^2.7.2",
62
+ "p-limit": "4",
61
63
  "pagefind": "^1.1.0",
62
64
  "sass": "^1.76.0",
63
65
  "typescript": "^5.4.5",
64
66
  "vite": "^5.2.11",
65
- "vitepress": "1.2.2",
67
+ "vitepress": "1.2.3",
66
68
  "vue": "^3.4.26"
67
69
  },
68
70
  "scripts": {
69
71
  "dev": "npm run build:node && npm run dev:docs",
70
72
  "dev:docs": "vitepress dev docs",
71
- "dev:node": "npx tsup",
73
+ "dev:node": "npx tsup --watch",
72
74
  "build": "npm run build:node && npm run build:docs",
73
75
  "build:docs": "vitepress build docs",
74
76
  "build:node": "npx tsup",
@@ -6,6 +6,7 @@ import Swiper from 'swiper'
6
6
  import { useBlogConfig } from '../composables/config/blog'
7
7
  import { getImageUrl, shuffleArray } from '../utils/client'
8
8
  import type { Theme } from '../'
9
+ import { friendLinkSvgStr } from '../constants/svg'
9
10
 
10
11
  const isDark = useDark({
11
12
  storageKey: 'vitepress-theme-appearance'
@@ -16,6 +17,7 @@ const friendConfig = computed<Theme.FriendConfig>(() => ({
16
17
  list: [],
17
18
  random: false,
18
19
  limit: Number.MAX_SAFE_INTEGER,
20
+ title: `${friendLinkSvgStr}友情链接`,
19
21
  ...(Array.isArray(friend) ? { list: friend } : friend)
20
22
  }))
21
23
 
@@ -96,24 +98,7 @@ onUnmounted(() => {
96
98
  <div v-if="friendList?.length" class="card friend-wrapper">
97
99
  <!-- 头部 -->
98
100
  <div class="card-header">
99
- <span class="title svg-icon"><svg width="512" height="512" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
100
- <path
101
- fill="#EF9645"
102
- d="M16.428 30.331a2.31 2.31 0 0 0 3.217-.568a.798.798 0 0 0-.197-1.114l-1.85-1.949l4.222 2.955a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089l-3.596-3.305l5.375 3.763a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089l-4.766-4.073l5.864 4.105a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089L4.733 11.194l-3.467 5.521c-.389.6-.283 1.413.276 1.891l7.786 6.671c.355.304.724.591 1.107.859l5.993 4.195z"
103
- />
104
- <path
105
- fill="#FFDC5D"
106
- d="M29.802 21.752L18.5 13.601l-.059-.08l.053-.08l.053-.053l.854.469c.958.62 3.147 1.536 4.806 1.536c1.135 0 1.815-.425 2.018-1.257a1.409 1.409 0 0 0-1.152-1.622a6.788 6.788 0 0 1-2.801-1.091l-.555-.373c-.624-.421-1.331-.898-1.853-1.206c-.65-.394-1.357-.585-2.163-.585c-1.196 0-2.411.422-3.585.83l-1.266.436a5.18 5.18 0 0 1-1.696.271c-1.544 0-3.055-.586-4.516-1.152l-.147-.058a1.389 1.389 0 0 0-1.674.56L1.35 15.669a1.357 1.357 0 0 0 .257 1.761l7.785 6.672c.352.301.722.588 1.1.852l6.165 4.316a2 2 0 0 0 2.786-.491a.803.803 0 0 0-.196-1.115l-1.833-1.283a.424.424 0 0 1-.082-.618a.422.422 0 0 1 .567-.075l3.979 2.785a1.4 1.4 0 0 0 1.606-2.294l-3.724-2.606a.424.424 0 0 1-.082-.618a.423.423 0 0 1 .567-.075l5.132 3.593a1.4 1.4 0 0 0 1.606-2.294l-4.868-3.407a.42.42 0 0 1-.081-.618a.377.377 0 0 1 .506-.066l5.656 3.959a1.4 1.4 0 0 0 1.606-2.295z"
107
- />
108
- <path
109
- fill="#EF9645"
110
- d="M16.536 27.929c-.07.267-.207.498-.389.681l-1.004.996a1.494 1.494 0 0 1-1.437.396a1.5 1.5 0 0 1-.683-2.512l1.004-.996a1.494 1.494 0 0 1 1.437-.396a1.502 1.502 0 0 1 1.072 1.831zM5.992 23.008l1.503-1.497a1.5 1.5 0 0 0-.444-2.429a1.495 1.495 0 0 0-1.674.31l-1.503 1.497a1.5 1.5 0 0 0 .445 2.429a1.496 1.496 0 0 0 1.673-.31zm5.204.052a1.5 1.5 0 1 0-2.122-2.118L6.072 23.94a1.5 1.5 0 1 0 2.122 2.118l3.002-2.998zm2.25 3a1.5 1.5 0 0 0-.945-2.555a1.489 1.489 0 0 0-1.173.44L9.323 25.94a1.5 1.5 0 0 0 .945 2.556c.455.036.874-.141 1.173-.44l2.005-1.996zm16.555-4.137l.627-.542l-6.913-10.85l-12.27 1.985a1.507 1.507 0 0 0-1.235 1.737c.658 2.695 6.003.693 8.355-.601l11.436 8.271z"
111
- />
112
- <path
113
- fill="#FFCC4D"
114
- d="M16.536 26.929c-.07.267-.207.498-.389.681l-1.004.996a1.494 1.494 0 0 1-1.437.396a1.5 1.5 0 0 1-.683-2.512l1.004-.996a1.494 1.494 0 0 1 1.437-.396a1.502 1.502 0 0 1 1.072 1.831zM5.992 22.008l1.503-1.497a1.5 1.5 0 0 0-.444-2.429a1.497 1.497 0 0 0-1.674.31l-1.503 1.497a1.5 1.5 0 0 0 .445 2.429a1.496 1.496 0 0 0 1.673-.31zm5.204.052a1.5 1.5 0 1 0-2.122-2.118L6.072 22.94a1.5 1.5 0 1 0 2.122 2.118l3.002-2.998zm2.25 3a1.5 1.5 0 0 0-.945-2.555a1.489 1.489 0 0 0-1.173.44L9.323 24.94a1.5 1.5 0 0 0 .945 2.556c.455.036.874-.141 1.173-.44l2.005-1.996zm21.557-7.456a1.45 1.45 0 0 0 .269-1.885l-.003-.005l-3.467-6.521a1.488 1.488 0 0 0-1.794-.6c-1.992.771-4.174 1.657-6.292.937l-1.098-.377c-1.948-.675-4.066-1.466-6-.294c-.695.409-1.738 1.133-2.411 1.58a6.873 6.873 0 0 1-2.762 1.076a1.502 1.502 0 0 0-1.235 1.737c.613 2.512 5.3.908 7.838-.369a.968.968 0 0 1 1.002.081l11.584 8.416l4.369-3.776z"
115
- />
116
- </svg> 友情链接</span>
101
+ <span class="title svg-icon" v-html="friendConfig.title" />
117
102
  </div>
118
103
  <!-- 友链列表 -->
119
104
  <div
@@ -9,10 +9,16 @@ import {
9
9
  useConfig,
10
10
  useCurrentPageNum,
11
11
  } from '../composables/config/blog'
12
+ import { tagsSvgStr } from '../constants/svg'
12
13
 
13
14
  const route = useRoute()
14
15
  const docs = useArticles()
15
- const showTags = useConfig()?.config?.blog?.homeTags ?? true
16
+ const homeTagsConfig = useConfig()?.config?.blog?.homeTags
17
+ const showTags = computed(() => !!(homeTagsConfig ?? true))
18
+ const title = computed(() => (typeof homeTagsConfig === 'boolean' || !homeTagsConfig?.title)
19
+ ? `${tagsSvgStr}标签`
20
+ : homeTagsConfig?.title
21
+ )
16
22
  const tags = computed(() => {
17
23
  return [...new Set(docs.value.map(v => v.meta.tag || []).flat(3))]
18
24
  })
@@ -81,28 +87,11 @@ watch(
81
87
  <div v-if="showTags && tags.length" class="card tags" data-pagefind-ignore="all">
82
88
  <!-- 头部 -->
83
89
  <div class="card-header">
84
- <span class="title svg-icon"><svg
85
- t="1695048840129" class="icon" viewBox="0 0 1024 1024" version="1.1"
86
- xmlns="http://www.w3.org/2000/svg" p-id="4290" width="200" height="200"
90
+ <span class="title svg-icon" v-html="title" />
91
+ <ElTag
92
+ v-if="activeTag.label" :type="activeTag.type || 'primary'" :effect="colorMode" closable
93
+ @close="handleCloseTag"
87
94
  >
88
- <path
89
- d="M810.88 245.888a118.432 118.432 0 1 0 0 236.864 118.432 118.432 0 0 0 0-236.864z m-151.008 118.432a151.008 151.008 0 1 1 302.016 0 151.008 151.008 0 0 1-302.016 0z"
90
- fill="#D3D3D3" p-id="4291"
91
- />
92
- <path
93
- d="M774.08 565.6l61.76-160.64c6.4-16.64 2.56-35.84-10.24-48.64l-151.04-151.04c-12.8-12.8-31.68-16.64-48.64-10.24l-160.64 61.76c-12.16 4.8-23.36 11.84-32.64 21.12l-355.2 355.2c-17.92 17.92-17.92 46.72 0 64.32l256 256c17.92 17.92 46.72 17.92 64.32 0l355.2-355.2c9.28-9.28 16.32-20.16 21.12-32.64z m-159.36-149.12c-22.08-22.08-22.08-57.6 0-79.68 22.08-22.08 57.6-22.08 79.68 0 22.08 22.08 22.08 57.6 0 79.68-22.08 21.76-57.92 21.76-79.68 0z"
94
- fill="#FCD53F" p-id="4292"
95
- />
96
- <path
97
- d="M654.4 320.48c14.4 0 28.8 5.44 39.68 16.64 22.08 22.08 22.08 57.6 0 79.68-10.88 10.88-25.28 16.64-39.68 16.64-14.4 0-28.8-5.44-39.68-16.64-22.08-22.08-22.08-57.6 0-79.68 10.88-11.2 25.28-16.64 39.68-16.64z m0-30.08c-23.04 0-44.8 8.96-61.12 25.28a86.72 86.72 0 0 0 0 122.24c16.32 16.32 38.08 25.28 61.12 25.28s44.8-8.96 61.12-25.28a86.72 86.72 0 0 0 0-122.24c-16.32-16.32-38.08-25.28-61.12-25.28z"
98
- fill="#F8312F" p-id="4293"
99
- />
100
- <path
101
- d="M676.16 348.032c8.992 0 16.288 7.296 16.288 16.288a118.144 118.144 0 0 0 64.288 105.44h0.064c22.24 11.296 47.36 15.264 71.68 11.84a16.288 16.288 0 0 1 4.48 32.32 154.24 154.24 0 0 1-90.848-15.04 150.72 150.72 0 0 1-82.24-134.56c0-8.992 7.296-16.288 16.288-16.288z"
102
- fill="#D3D3D3" p-id="4294"
103
- />
104
- </svg> 标签</span>
105
- <ElTag v-if="activeTag.label" :type="activeTag.type || 'primary'" :effect="colorMode" closable @close="handleCloseTag">
106
95
  {{ activeTag.label }}
107
96
  </ElTag>
108
97
  </div>
@@ -12,7 +12,7 @@ const hotArticle = computed(() =>
12
12
  _hotArticle === false ? undefined : _hotArticle
13
13
  )
14
14
 
15
- const title = computed(() => hotArticle.value?.title || (`<span class="svg-icon">${fireSVG}</span>` + ' 精选文章'))
15
+ const title = computed(() => hotArticle.value?.title || `${fireSVG}精选文章`)
16
16
  const nextText = computed(() => hotArticle.value?.nextText || '换一组')
17
17
  const pageSize = computed(() => hotArticle.value?.pageSize || 9)
18
18
  const empty = computed(() => hotArticle.value?.empty ?? '暂无精选内容')
@@ -59,7 +59,7 @@ const showChangeBtn = computed(() => {
59
59
  >
60
60
  <!-- 头部 -->
61
61
  <div class="card-header">
62
- <span class="title" v-html="title" />
62
+ <span class="title svg-icon" v-html="title" />
63
63
  <ElButton v-if="showChangeBtn" size="small" type="primary" text @click="changePage">
64
64
  {{ nextText }}
65
65
  </ElButton>
@@ -92,7 +92,7 @@ const resultCover = computed(() => {
92
92
  <div class="badge-list pc-visible">
93
93
  <span v-show="author" class="split">{{ author }}</span>
94
94
  <span class="split">{{ showTime }}</span>
95
- <span v-show="tag?.length" class="split">{{ tag?.join(' · ') }}</span>
95
+ <span v-if="tag?.length" class="split">{{ tag?.join(' · ') }}</span>
96
96
  </div>
97
97
  </div>
98
98
  <!-- 右侧封面图 -->
@@ -146,6 +146,10 @@ export namespace Theme {
146
146
  }
147
147
 
148
148
  export interface HotArticle {
149
+ /**
150
+ * 自定义标题,支持SVG + 文字
151
+ * @default '🔥 精选文章'
152
+ */
149
153
  title?: string
150
154
  pageSize?: number
151
155
  nextText?: string
@@ -288,6 +292,11 @@ export namespace Theme {
288
292
  * @default "动态计算"
289
293
  */
290
294
  scrollSpeed?: number
295
+ /**
296
+ * 自定义展示标题,支持SVG + 文字
297
+ * @default '🤝 友情链接'
298
+ */
299
+ title?: string
291
300
  }
292
301
 
293
302
  export interface UserWork {
@@ -408,7 +417,8 @@ export namespace Theme {
408
417
  mermaid?: any
409
418
  /**
410
419
  * 设置解析 frontmatter 里 date 的时区
411
- * @default 8 => 'UTC+8'
420
+ * @default new Date().getTimezoneOffset() / -60
421
+ * @example 8 => 'UTC+8'
412
422
  */
413
423
  timeZone?: number
414
424
  /**
@@ -452,7 +462,7 @@ export namespace Theme {
452
462
  * 详见 https://oml2d.com/options/Options.html
453
463
  */
454
464
  oml2d?: Oml2dOptions
455
- homeTags?: boolean
465
+ homeTags?: boolean | HomeTagsConfig
456
466
  buttonAfterArticle?: ButtonAfterArticleConfig | false
457
467
  /**
458
468
  * 是否开启深色模式过渡动画
@@ -586,4 +596,12 @@ export namespace Theme {
586
596
  */
587
597
  coverPreview?: ReplaceRule | ReplaceRule[]
588
598
  }
599
+
600
+ export interface HomeTagsConfig {
601
+ /**
602
+ * 自定义标题,支持SVG + 文字
603
+ * @default '🏷 标签'
604
+ */
605
+ title?: string
606
+ }
589
607
  }
@@ -75,3 +75,44 @@ export const weChatPaySVG = `
75
75
  />
76
76
  </svg>
77
77
  `
78
+
79
+ export const friendLinkSvgStr = `<svg width="512" height="512" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
80
+ <path
81
+ fill="#EF9645"
82
+ d="M16.428 30.331a2.31 2.31 0 0 0 3.217-.568a.798.798 0 0 0-.197-1.114l-1.85-1.949l4.222 2.955a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089l-3.596-3.305l5.375 3.763a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089l-4.766-4.073l5.864 4.105a1.497 1.497 0 0 0 2.089-.369a1.5 1.5 0 0 0-.369-2.089L4.733 11.194l-3.467 5.521c-.389.6-.283 1.413.276 1.891l7.786 6.671c.355.304.724.591 1.107.859l5.993 4.195z"
83
+ />
84
+ <path
85
+ fill="#FFDC5D"
86
+ d="M29.802 21.752L18.5 13.601l-.059-.08l.053-.08l.053-.053l.854.469c.958.62 3.147 1.536 4.806 1.536c1.135 0 1.815-.425 2.018-1.257a1.409 1.409 0 0 0-1.152-1.622a6.788 6.788 0 0 1-2.801-1.091l-.555-.373c-.624-.421-1.331-.898-1.853-1.206c-.65-.394-1.357-.585-2.163-.585c-1.196 0-2.411.422-3.585.83l-1.266.436a5.18 5.18 0 0 1-1.696.271c-1.544 0-3.055-.586-4.516-1.152l-.147-.058a1.389 1.389 0 0 0-1.674.56L1.35 15.669a1.357 1.357 0 0 0 .257 1.761l7.785 6.672c.352.301.722.588 1.1.852l6.165 4.316a2 2 0 0 0 2.786-.491a.803.803 0 0 0-.196-1.115l-1.833-1.283a.424.424 0 0 1-.082-.618a.422.422 0 0 1 .567-.075l3.979 2.785a1.4 1.4 0 0 0 1.606-2.294l-3.724-2.606a.424.424 0 0 1-.082-.618a.423.423 0 0 1 .567-.075l5.132 3.593a1.4 1.4 0 0 0 1.606-2.294l-4.868-3.407a.42.42 0 0 1-.081-.618a.377.377 0 0 1 .506-.066l5.656 3.959a1.4 1.4 0 0 0 1.606-2.295z"
87
+ />
88
+ <path
89
+ fill="#EF9645"
90
+ d="M16.536 27.929c-.07.267-.207.498-.389.681l-1.004.996a1.494 1.494 0 0 1-1.437.396a1.5 1.5 0 0 1-.683-2.512l1.004-.996a1.494 1.494 0 0 1 1.437-.396a1.502 1.502 0 0 1 1.072 1.831zM5.992 23.008l1.503-1.497a1.5 1.5 0 0 0-.444-2.429a1.495 1.495 0 0 0-1.674.31l-1.503 1.497a1.5 1.5 0 0 0 .445 2.429a1.496 1.496 0 0 0 1.673-.31zm5.204.052a1.5 1.5 0 1 0-2.122-2.118L6.072 23.94a1.5 1.5 0 1 0 2.122 2.118l3.002-2.998zm2.25 3a1.5 1.5 0 0 0-.945-2.555a1.489 1.489 0 0 0-1.173.44L9.323 25.94a1.5 1.5 0 0 0 .945 2.556c.455.036.874-.141 1.173-.44l2.005-1.996zm16.555-4.137l.627-.542l-6.913-10.85l-12.27 1.985a1.507 1.507 0 0 0-1.235 1.737c.658 2.695 6.003.693 8.355-.601l11.436 8.271z"
91
+ />
92
+ <path
93
+ fill="#FFCC4D"
94
+ d="M16.536 26.929c-.07.267-.207.498-.389.681l-1.004.996a1.494 1.494 0 0 1-1.437.396a1.5 1.5 0 0 1-.683-2.512l1.004-.996a1.494 1.494 0 0 1 1.437-.396a1.502 1.502 0 0 1 1.072 1.831zM5.992 22.008l1.503-1.497a1.5 1.5 0 0 0-.444-2.429a1.497 1.497 0 0 0-1.674.31l-1.503 1.497a1.5 1.5 0 0 0 .445 2.429a1.496 1.496 0 0 0 1.673-.31zm5.204.052a1.5 1.5 0 1 0-2.122-2.118L6.072 22.94a1.5 1.5 0 1 0 2.122 2.118l3.002-2.998zm2.25 3a1.5 1.5 0 0 0-.945-2.555a1.489 1.489 0 0 0-1.173.44L9.323 24.94a1.5 1.5 0 0 0 .945 2.556c.455.036.874-.141 1.173-.44l2.005-1.996zm21.557-7.456a1.45 1.45 0 0 0 .269-1.885l-.003-.005l-3.467-6.521a1.488 1.488 0 0 0-1.794-.6c-1.992.771-4.174 1.657-6.292.937l-1.098-.377c-1.948-.675-4.066-1.466-6-.294c-.695.409-1.738 1.133-2.411 1.58a6.873 6.873 0 0 1-2.762 1.076a1.502 1.502 0 0 0-1.235 1.737c.613 2.512 5.3.908 7.838-.369a.968.968 0 0 1 1.002.081l11.584 8.416l4.369-3.776z"
95
+ />
96
+ </svg>`
97
+
98
+ export const tagsSvgStr = `<svg
99
+ t="1695048840129" class="icon" viewBox="0 0 1024 1024" version="1.1"
100
+ xmlns="http://www.w3.org/2000/svg" p-id="4290" width="200" height="200"
101
+ >
102
+ <path
103
+ d="M810.88 245.888a118.432 118.432 0 1 0 0 236.864 118.432 118.432 0 0 0 0-236.864z m-151.008 118.432a151.008 151.008 0 1 1 302.016 0 151.008 151.008 0 0 1-302.016 0z"
104
+ fill="#D3D3D3" p-id="4291"
105
+ />
106
+ <path
107
+ d="M774.08 565.6l61.76-160.64c6.4-16.64 2.56-35.84-10.24-48.64l-151.04-151.04c-12.8-12.8-31.68-16.64-48.64-10.24l-160.64 61.76c-12.16 4.8-23.36 11.84-32.64 21.12l-355.2 355.2c-17.92 17.92-17.92 46.72 0 64.32l256 256c17.92 17.92 46.72 17.92 64.32 0l355.2-355.2c9.28-9.28 16.32-20.16 21.12-32.64z m-159.36-149.12c-22.08-22.08-22.08-57.6 0-79.68 22.08-22.08 57.6-22.08 79.68 0 22.08 22.08 22.08 57.6 0 79.68-22.08 21.76-57.92 21.76-79.68 0z"
108
+ fill="#FCD53F" p-id="4292"
109
+ />
110
+ <path
111
+ d="M654.4 320.48c14.4 0 28.8 5.44 39.68 16.64 22.08 22.08 22.08 57.6 0 79.68-10.88 10.88-25.28 16.64-39.68 16.64-14.4 0-28.8-5.44-39.68-16.64-22.08-22.08-22.08-57.6 0-79.68 10.88-11.2 25.28-16.64 39.68-16.64z m0-30.08c-23.04 0-44.8 8.96-61.12 25.28a86.72 86.72 0 0 0 0 122.24c16.32 16.32 38.08 25.28 61.12 25.28s44.8-8.96 61.12-25.28a86.72 86.72 0 0 0 0-122.24c-16.32-16.32-38.08-25.28-61.12-25.28z"
112
+ fill="#F8312F" p-id="4293"
113
+ />
114
+ <path
115
+ d="M676.16 348.032c8.992 0 16.288 7.296 16.288 16.288a118.144 118.144 0 0 0 64.288 105.44h0.064c22.24 11.296 47.36 15.264 71.68 11.84a16.288 16.288 0 0 1 4.48 32.32 154.24 154.24 0 0 1-90.848-15.04 150.72 150.72 0 0 1-82.24-134.56c0-8.992 7.296-16.288 16.288-16.288z"
116
+ fill="#D3D3D3" p-id="4294"
117
+ />
118
+ </svg>`
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // @sugarat/theme index
1
2
  // override style
2
3
  import './styles/index.scss'
3
4
 
@@ -23,13 +24,12 @@ import DefaultTheme from 'vitepress/theme'
23
24
  import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client'
24
25
 
25
26
  // 图表渲染组件
26
- // @ts-expect-error
27
- import Mermaid from 'vitepress-plugin-mermaid/Mermaid.vue'
27
+ // replace-mermaid-import-code
28
+ // import Mermaid from 'vitepress-plugin-mermaid/Mermaid.vue'
28
29
  import BlogApp from './components/BlogApp.vue'
29
30
  import { withConfigProvider } from './composables/config/blog'
30
31
 
31
32
  // page
32
- import TimelinePage from './components/TimelinePage.vue'
33
33
  import UserWorksPage from './components/UserWorks.vue'
34
34
 
35
35
  // 内置一些特殊的主题色
@@ -41,11 +41,9 @@ export const BlogTheme: Theme = {
41
41
  enhanceApp(ctx) {
42
42
  enhanceAppWithTabs(ctx.app as any)
43
43
  DefaultTheme.enhanceApp(ctx)
44
- ctx.app.component('TimelinePage', TimelinePage)
45
- ctx.app.component('UserWorksPage', UserWorksPage)
46
- if (!ctx.app.component('Mermaid')) {
47
- ctx.app.component('Mermaid', Mermaid as any)
48
- }
44
+ ctx.app.component('UserWorksPage', UserWorksPage as any)
45
+ // replace-mermaid-mounted-code
46
+ // if (!ctx.app.component('Mermaid')) { ctx.app.component('Mermaid', Mermaid as any) }
49
47
  }
50
48
  }
51
49
 
package/src/node.ts CHANGED
@@ -6,20 +6,25 @@ import {
6
6
  patchOptimizeDeps,
7
7
  registerMdPlugins,
8
8
  } from './utils/node/mdPlugins'
9
- import { checkConfig, getArticles, patchVPConfig, patchVPThemeConfig } from './utils/node/theme'
9
+ import { checkConfig, patchVPConfig, patchVPThemeConfig } from './utils/node/theme'
10
10
  import { getVitePlugins, registerVitePlugins } from './utils/node/vitePlugins'
11
11
 
12
12
  /**
13
13
  * 获取主题的配置
14
14
  * @param cfg 主题配置
15
15
  */
16
- export function getThemeConfig(cfg?: Partial<Theme.BlogConfig>) {
16
+ export function getThemeConfig(cfg: Partial<Theme.BlogConfig> = {}) {
17
17
  // 配置校验
18
18
  checkConfig(cfg)
19
19
 
20
+ // 默认不开启 markdown 图表,会明显影响构建速度
21
+ cfg.mermaid = cfg.mermaid ?? false
22
+
20
23
  // 文章数据
21
- const pagesData = getArticles(cfg)
22
- const extraVPConfig: any = {}
24
+ const pagesData: Theme.PageData[] = []
25
+ const extraVPConfig: any = {
26
+ vite: {}
27
+ }
23
28
 
24
29
  // 获取要加载的vite插件
25
30
  const vitePlugins = getVitePlugins(cfg)
@@ -32,7 +37,9 @@ export function getThemeConfig(cfg?: Partial<Theme.BlogConfig>) {
32
37
  registerMdPlugins(extraVPConfig, markdownPlugin)
33
38
 
34
39
  // patch extraVPConfig
35
- patchMermaidPluginCfg(extraVPConfig)
40
+ if (cfg?.mermaid !== false) {
41
+ patchMermaidPluginCfg(extraVPConfig)
42
+ }
36
43
  patchOptimizeDeps(extraVPConfig)
37
44
 
38
45
  patchVPConfig(extraVPConfig, cfg)
@@ -20,9 +20,9 @@ export function themeReloadPlugin() {
20
20
  const restart = debounce(() => {
21
21
  server.restart()
22
22
  }, 500)
23
- server.watcher.on('add', (path) => {
23
+ server.watcher.on('add', async (path) => {
24
24
  const route = generateRoute(path)
25
- const meta = getArticleMeta(path, route, blogConfig?.timeZone)
25
+ const meta = await getArticleMeta(path, route, blogConfig?.timeZone)
26
26
  blogConfig.pagesData.push({
27
27
  route,
28
28
  meta
@@ -30,9 +30,9 @@ export function themeReloadPlugin() {
30
30
  restart()
31
31
  })
32
32
 
33
- server.watcher.on('change', (path: string) => {
33
+ server.watcher.on('change', async (path: string) => {
34
34
  const route = generateRoute(path)
35
- const meta = getArticleMeta(path, route, blogConfig?.timeZone)
35
+ const meta = await getArticleMeta(path, route, blogConfig?.timeZone)
36
36
  const matched = blogConfig.pagesData.find(v => v.route === route)
37
37
 
38
38
  if (matched && !isEqual(matched.meta, meta, ['date', 'description'])) {
@@ -36,24 +36,27 @@ export function getDefaultTitle(content: string) {
36
36
  return match?.[2] || ''
37
37
  }
38
38
 
39
- export function getFileBirthTime(url: string) {
40
- let date = new Date()
41
-
42
- try {
43
- // 参考 vitepress 中的 getGitTimestamp 实现
44
- const infoStr = spawnSync('git', ['log', '-1', '--pretty="%ci"', url])
45
- .stdout?.toString()
46
- .replace(/["']/g, '')
47
- .trim()
48
- if (infoStr) {
49
- date = new Date(infoStr)
50
- }
51
- }
52
- catch (error) {
53
- return date
39
+ const cache = new Map<string, Date>()
40
+ export function getFileBirthTime(url: string): Promise<Date | undefined> | Date {
41
+ const cached = cache.get(url)
42
+ if (cached) {
43
+ return cached
54
44
  }
55
45
 
56
- return date
46
+ return new Promise((resolve) => {
47
+ // 使用异步回调
48
+ const child = spawn('git', ['log', '-1', '--pretty="%ai"', url])
49
+ let output = ''
50
+ child.stdout.on('data', d => (output += String(d)))
51
+ child.on('close', () => {
52
+ const date = new Date(output)
53
+ cache.set(url, date)
54
+ resolve(date)
55
+ })
56
+ child.on('error', () => {
57
+ resolve(undefined)
58
+ })
59
+ })
57
60
  }
58
61
 
59
62
  export function getGitTimestamp(file: string) {
@@ -2,8 +2,10 @@
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import process from 'node:process'
5
+ import os from 'node:os'
5
6
  import glob from 'fast-glob'
6
7
  import matter from 'gray-matter'
8
+ import pLimit from 'p-limit'
7
9
  import type { Theme } from '../../composables/config/index'
8
10
  import { formatDate } from '../client'
9
11
  import { getDefaultTitle, getFileBirthTime, getFirstImagURLFromMD, getTextSummary } from './index'
@@ -49,8 +51,8 @@ export function getPageRoute(filepath: string, srcDir: string) {
49
51
  }
50
52
 
51
53
  const defaultTimeZoneOffset = new Date().getTimezoneOffset() / -60
52
- export function getArticleMeta(filepath: string, route: string, timeZone = defaultTimeZoneOffset) {
53
- const fileContent = fs.readFileSync(filepath, 'utf-8')
54
+ export async function getArticleMeta(filepath: string, route: string, timeZone = defaultTimeZoneOffset) {
55
+ const fileContent = await fs.promises.readFile(filepath, 'utf-8')
54
56
 
55
57
  const { data: frontmatter, excerpt, content } = matter(fileContent, {
56
58
  excerpt: true,
@@ -63,13 +65,13 @@ export function getArticleMeta(filepath: string, route: string, timeZone = defau
63
65
  if (!meta.title) {
64
66
  meta.title = getDefaultTitle(content)
65
67
  }
66
- if (!meta.date) {
67
- meta.date = formatDate(getFileBirthTime(filepath))
68
- }
69
- else {
70
- meta.date = formatDate(
71
- new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
72
- )
68
+ const date = await (
69
+ (meta.date
70
+ && new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`))
71
+ || getFileBirthTime(filepath)
72
+ )
73
+ if (date) {
74
+ meta.date = formatDate(date)
73
75
  }
74
76
 
75
77
  // 处理tags和categories,兼容历史文章
@@ -101,22 +103,40 @@ export function getArticleMeta(filepath: string, route: string, timeZone = defau
101
103
  }
102
104
  return meta as Theme.PageMeta
103
105
  }
104
- export function getArticles(cfg?: Partial<Theme.BlogConfig>) {
106
+ export async function getArticles(cfg?: Partial<Theme.BlogConfig>) {
105
107
  const srcDir = cfg?.srcDir || process.argv.slice(2)?.[1] || '.'
106
108
  const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'] })
109
+ const limit = pLimit(+(process.env.P_LIMT_MAX || os.cpus().length))
107
110
 
108
- // 文章数据
109
- const pageData = files
110
- .map((filepath) => {
111
- const route = getPageRoute(filepath, srcDir)
112
- const meta = getArticleMeta(filepath, route, cfg?.timeZone)
113
- return {
114
- route,
115
- meta
116
- }
111
+ const metaResults = files.reduce((prev, curr) => {
112
+ const route = getPageRoute(curr, srcDir)
113
+ const metaPromise = limit(() => getArticleMeta(curr, route, cfg?.timeZone))
114
+
115
+ // 提前获取,有缓存取缓存
116
+ prev[curr] = {
117
+ route,
118
+ metaPromise
119
+ }
120
+ return prev
121
+ }, {} as Record<string, {
122
+ route: string
123
+ metaPromise: Promise<Theme.PageMeta>
124
+ }>)
125
+
126
+ const pageData: Theme.PageData[] = []
127
+
128
+ for (const file of files) {
129
+ const { route, metaPromise } = metaResults[file]
130
+ const meta = await metaPromise
131
+ if (meta.layout === 'home') {
132
+ continue
133
+ }
134
+ pageData.push({
135
+ route,
136
+ meta
117
137
  })
118
- .filter(v => v.meta.layout !== 'home')
119
- return pageData as Theme.PageData[]
138
+ }
139
+ return pageData
120
140
  }
121
141
 
122
142
  export function patchVPConfig(vpConfig: any, cfg?: Partial<Theme.BlogConfig>) {
@@ -1,6 +1,4 @@
1
1
  import path from 'node:path'
2
- import { execSync } from 'node:child_process'
3
- import process from 'node:process'
4
2
  import { existsSync, readFileSync } from 'node:fs'
5
3
  import { Buffer } from 'node:buffer'
6
4
  import type { SiteConfig } from 'vitepress'
@@ -10,19 +8,19 @@ import {
10
8
  pagefindPlugin
11
9
  } from 'vitepress-plugin-pagefind'
12
10
  import { RssPlugin } from 'vitepress-plugin-rss'
11
+ import type { PluginOption } from 'vite'
13
12
  import type { Theme } from '../../composables/config/index'
14
13
  import { _require } from './mdPlugins'
15
14
  import { themeReloadPlugin } from './hot-reload-plugin'
15
+ import { getArticles } from './theme'
16
16
  import { joinPath } from './index'
17
17
 
18
18
  export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
19
19
  const plugins: any[] = []
20
20
 
21
- // Build完后运行的一系列列方法
22
- const buildEndFn: any[] = []
23
-
24
- // 执行自定义的 buildEnd 钩子
25
- plugins.push(inlineBuildEndPlugin(buildEndFn))
21
+ // const buildEndFn: any[] = []
22
+ // Build完后运行的一系列列方法,执行自定义的 buildEnd 钩子
23
+ // plugins.push(inlineBuildEndPlugin(buildEndFn))
26
24
 
27
25
  // 处理cover image的路径(暂只支持自动识别的文章首图)
28
26
  plugins.push(coverImgTransform())
@@ -30,6 +28,9 @@ export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
30
28
  // 自动重载首页
31
29
  plugins.push(themeReloadPlugin())
32
30
 
31
+ // 主题pageData生成
32
+ plugins.push(providePageData(cfg))
33
+
33
34
  // 内置简化版的pagefind
34
35
  if (cfg && cfg.search !== false) {
35
36
  const ops = cfg.search instanceof Object ? cfg.search : {}
@@ -47,6 +48,7 @@ export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
47
48
  // 内置支持Mermaid
48
49
  if (cfg?.mermaid !== false) {
49
50
  const { MermaidPlugin } = _require('vitepress-plugin-mermaid')
51
+ plugins.push(inlineInjectMermaidClient())
50
52
  plugins.push(MermaidPlugin(cfg?.mermaid === true ? {} : (cfg?.mermaid ?? {})))
51
53
  }
52
54
 
@@ -63,45 +65,19 @@ export function registerVitePlugins(vpCfg: any, plugins: any[]) {
63
65
  }
64
66
  }
65
67
 
66
- export function inlinePagefindPlugin(buildEndFn: any[]) {
67
- buildEndFn.push(() => {
68
- // 调用pagefind
69
- const ignore: string[] = [
70
- // 侧边栏内容
71
- 'div.aside',
72
- // 标题锚点
73
- 'a.header-anchor'
74
- ]
75
- const { log } = console
76
- log()
77
- log('=== pagefind: https://pagefind.app/ ===')
78
- const siteDir = path.join(
79
- process.argv.slice(2)?.[1] || '.',
80
- '.vitepress/dist'
81
- )
82
- let command = `npx pagefind --site ${siteDir} --output-subdir "_pagefind"`
83
-
84
- if (ignore.length) {
85
- command += ` --exclude-selectors "${ignore.join(', ')}"`
86
- }
87
-
88
- log(command)
89
- log()
90
- execSync(command, {
91
- stdio: 'inherit'
92
- })
93
- })
68
+ export function inlineInjectMermaidClient() {
94
69
  return {
95
- name: '@sugarar/theme-plugin-pagefind',
70
+ name: '@sugarat/theme-plugin-inline-inject-mermaid-client',
96
71
  enforce: 'pre',
97
- // 添加检索的内容标识
98
- transform(code: string, id: string) {
99
- if (id.endsWith('theme-default/Layout.vue')) {
100
- return code.replace('<VPContent>', '<VPContent data-pagefind-body>')
72
+ transform(code, id) {
73
+ if (id.endsWith('src/index.ts') && code.startsWith('// @sugarat/theme index')) {
74
+ return code
75
+ .replace('// replace-mermaid-import-code', 'import Mermaid from \'vitepress-plugin-mermaid/Mermaid.vue\'')
76
+ .replace('// replace-mermaid-mounted-code', 'if (!ctx.app.component(\'Mermaid\')) { ctx.app.component(\'Mermaid\', Mermaid as any) }')
101
77
  }
102
78
  return code
103
- }
104
- }
79
+ },
80
+ } as PluginOption
105
81
  }
106
82
 
107
83
  export function inlineBuildEndPlugin(buildEndFn: any[]) {
@@ -176,3 +152,13 @@ export function coverImgTransform() {
176
152
  }
177
153
  }
178
154
  }
155
+
156
+ export function providePageData(cfg?: Partial<Theme.BlogConfig>) {
157
+ return {
158
+ name: '@sugarat/theme-plugin-provide-page-data',
159
+ async config(config: any) {
160
+ const pagesData = await getArticles(cfg)
161
+ config.vitepress.site.themeConfig.blog.pagesData = pagesData
162
+ },
163
+ } as PluginOption
164
+ }