@sugarat/theme 0.4.3 → 0.4.5

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/node.d.ts CHANGED
@@ -4,6 +4,7 @@ import { RSSOptions } from 'vitepress-plugin-rss';
4
4
  import { Repo, Mapping } from '@giscus/vue';
5
5
  import { Options } from 'oh-my-live2d';
6
6
  import { Ref } from 'vue';
7
+ import { PagefindConfig } from 'vitepress-plugin-pagefind';
7
8
  export { tabsMarkdownPlugin } from 'vitepress-plugin-tabs';
8
9
 
9
10
  type RSSPluginOptions = RSSOptions;
@@ -324,17 +325,7 @@ declare namespace Theme {
324
325
  tags?: string[];
325
326
  top?: number;
326
327
  }
327
- type SearchConfig = boolean | 'pagefind' | {
328
- btnPlaceholder?: string;
329
- placeholder?: string;
330
- emptyText?: string;
331
- /**
332
- * @example
333
- * 'Total: {{searchResult}} search results.'
334
- */
335
- heading?: string;
336
- mode?: boolean | 'pagefind';
337
- };
328
+ type SearchConfig = false | PagefindConfig;
338
329
  interface UserWorks {
339
330
  title: string;
340
331
  description?: string;
@@ -347,7 +338,7 @@ declare namespace Theme {
347
338
  /**
348
339
  * 内置一些主题色
349
340
  * @default 'vp-default'
350
- * 也可以自定义颜色,详见 TODO:文档
341
+ * 也可以自定义颜色,详见 https://theme.sugarat.top/config/style.html#%E4%B8%BB%E9%A2%98%E8%89%B2
351
342
  */
352
343
  themeColor?: ThemeColor;
353
344
  pagesData: PageData[];
@@ -400,7 +391,7 @@ declare namespace Theme {
400
391
  /**
401
392
  * 启用RSS配置
402
393
  */
403
- RSS?: RSSOptions;
394
+ RSS?: RSSOptions | RSSOptions[];
404
395
  /**
405
396
  * 首页页脚
406
397
  */
package/node.js CHANGED
@@ -208,59 +208,14 @@ var tabsPlugin = (md) => {
208
208
  var import_vitepress_markdown_timeline = __toESM(require("vitepress-markdown-timeline"));
209
209
 
210
210
  // src/utils/node/index.ts
211
- var import_node_child_process = require("child_process");
212
211
  var import_node_path = __toESM(require("path"));
213
- var import_node_fs = __toESM(require("fs"));
214
- function getDefaultTitle(content) {
215
- const match = content.match(/^(#+)\s+(.+)/m);
216
- return match?.[2] || "";
217
- }
218
- var cache = /* @__PURE__ */ new Map();
219
- function getFileBirthTime(url) {
220
- const cached = cache.get(url);
221
- if (cached) {
222
- return cached;
223
- }
224
- return new Promise((resolve) => {
225
- const child = (0, import_node_child_process.spawn)("git", ["log", "-1", '--pretty="%ai"', url]);
226
- let output = "";
227
- child.stdout.on("data", (d) => output += String(d));
228
- child.on("close", async () => {
229
- let date;
230
- if (output.trim()) {
231
- date = new Date(output);
232
- } else {
233
- date = await getFileBirthTimeByFs(url);
234
- }
235
- cache.set(url, date);
236
- resolve(date);
237
- });
238
- child.on("error", async () => {
239
- const fsDate = await getFileBirthTimeByFs(url);
240
- resolve(fsDate);
241
- });
242
- });
243
- }
244
- async function getFileBirthTimeByFs(url) {
245
- try {
246
- const fsStat = await import_node_fs.default.promises.stat(url);
247
- return fsStat.birthtime;
248
- } catch {
249
- return void 0;
250
- }
251
- }
252
- function getTextSummary(text, count = 100) {
253
- 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);
254
- }
212
+ var import_theme_shared = require("@sugarat/theme-shared");
255
213
  function aliasObjectToArray(obj) {
256
214
  return Object.entries(obj).map(([find, replacement]) => ({
257
215
  find,
258
216
  replacement
259
217
  }));
260
218
  }
261
- function joinPath(base, path4) {
262
- return `${base}${path4}`.replace(/\/+/g, "/");
263
- }
264
219
  function isBase64ImageURL(url) {
265
220
  const regex = /^data:image\/[a-z]+;base64,/;
266
221
  return regex.test(url);
@@ -275,10 +230,10 @@ function getFirstImagURLFromMD(content, route) {
275
230
  if (isHTTPSource || isBase64ImageURL(url)) {
276
231
  return url;
277
232
  }
278
- const paths = joinPath("/", route).split("/");
233
+ const paths = (0, import_theme_shared.joinPath)("/", route).split("/");
279
234
  paths.splice(paths.length - 1, 1);
280
235
  const relativePath = url.startsWith("/") ? url : import_node_path.default.join(paths.join("/") || "", url);
281
- return joinPath("/", relativePath);
236
+ return (0, import_theme_shared.joinPath)("/", relativePath);
282
237
  }
283
238
  function debounce(func, delay = 1e3) {
284
239
  let timeoutId;
@@ -373,115 +328,11 @@ function patchOptimizeDeps(config) {
373
328
  }
374
329
 
375
330
  // src/utils/node/theme.ts
376
- var import_node_fs2 = __toESM(require("fs"));
331
+ var import_node_fs = __toESM(require("fs"));
377
332
  var import_node_path2 = __toESM(require("path"));
378
333
  var import_node_process = __toESM(require("process"));
379
- var import_node_os = __toESM(require("os"));
380
334
  var import_fast_glob = __toESM(require("fast-glob"));
381
- var import_gray_matter = __toESM(require("gray-matter"));
382
-
383
- // ../../node_modules/.pnpm/yocto-queue@1.0.0/node_modules/yocto-queue/index.js
384
- var Node = class {
385
- value;
386
- next;
387
- constructor(value) {
388
- this.value = value;
389
- }
390
- };
391
- var Queue = class {
392
- #head;
393
- #tail;
394
- #size;
395
- constructor() {
396
- this.clear();
397
- }
398
- enqueue(value) {
399
- const node = new Node(value);
400
- if (this.#head) {
401
- this.#tail.next = node;
402
- this.#tail = node;
403
- } else {
404
- this.#head = node;
405
- this.#tail = node;
406
- }
407
- this.#size++;
408
- }
409
- dequeue() {
410
- const current = this.#head;
411
- if (!current) {
412
- return;
413
- }
414
- this.#head = this.#head.next;
415
- this.#size--;
416
- return current.value;
417
- }
418
- clear() {
419
- this.#head = void 0;
420
- this.#tail = void 0;
421
- this.#size = 0;
422
- }
423
- get size() {
424
- return this.#size;
425
- }
426
- *[Symbol.iterator]() {
427
- let current = this.#head;
428
- while (current) {
429
- yield current.value;
430
- current = current.next;
431
- }
432
- }
433
- };
434
-
435
- // ../../node_modules/.pnpm/p-limit@4.0.0/node_modules/p-limit/index.js
436
- function pLimit(concurrency) {
437
- if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
438
- throw new TypeError("Expected `concurrency` to be a number from 1 and up");
439
- }
440
- const queue = new Queue();
441
- let activeCount = 0;
442
- const next = () => {
443
- activeCount--;
444
- if (queue.size > 0) {
445
- queue.dequeue()();
446
- }
447
- };
448
- const run = async (fn, resolve, args) => {
449
- activeCount++;
450
- const result = (async () => fn(...args))();
451
- resolve(result);
452
- try {
453
- await result;
454
- } catch {
455
- }
456
- next();
457
- };
458
- const enqueue = (fn, resolve, args) => {
459
- queue.enqueue(run.bind(void 0, fn, resolve, args));
460
- (async () => {
461
- await Promise.resolve();
462
- if (activeCount < concurrency && queue.size > 0) {
463
- queue.dequeue()();
464
- }
465
- })();
466
- };
467
- const generator = (fn, ...args) => new Promise((resolve) => {
468
- enqueue(fn, resolve, args);
469
- });
470
- Object.defineProperties(generator, {
471
- activeCount: {
472
- get: () => activeCount
473
- },
474
- pendingCount: {
475
- get: () => queue.size
476
- },
477
- clearQueue: {
478
- value: () => {
479
- queue.clear();
480
- }
481
- }
482
- });
483
- return generator;
484
- }
335
+ var import_theme_shared2 = require("@sugarat/theme-shared");
485
336
 
486
337
  // src/utils/client/index.ts
487
338
  function formatDate(d, fmt = "yyyy-MM-dd hh:mm:ss") {
@@ -532,44 +383,29 @@ function patchDefaultThemeSideBar(cfg) {
532
383
  } : void 0;
533
384
  }
534
385
  function getPageRoute(filepath, srcDir) {
535
- let route = filepath.replace(".md", "");
536
- if (route.startsWith("./")) {
537
- route = route.replace(
538
- new RegExp(
539
- `^\\.\\/${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
540
- ),
541
- ""
542
- );
543
- } else {
544
- route = route.replace(
545
- new RegExp(
546
- `^${import_node_path2.default.join(srcDir, "/").replace(new RegExp(`\\${import_node_path2.default.sep}`, "g"), "/")}`
547
- ),
548
- ""
549
- );
550
- }
386
+ const route = (0, import_theme_shared2.normalizePath)(import_node_path2.default.relative(srcDir, filepath)).replace(/\.md$/, "");
551
387
  return `/${route}`;
552
388
  }
553
389
  var defaultTimeZoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset() / -60;
554
390
  async function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset) {
555
- const fileContent = await import_node_fs2.default.promises.readFile(filepath, "utf-8");
556
- const { data: frontmatter, excerpt, content } = (0, import_gray_matter.default)(fileContent, {
391
+ const fileContent = await import_node_fs.default.promises.readFile(filepath, "utf-8");
392
+ const { data: frontmatter, excerpt, content } = (0, import_theme_shared2.grayMatter)(fileContent, {
557
393
  excerpt: true
558
394
  });
559
395
  const meta = {
560
396
  ...frontmatter
561
397
  };
562
398
  if (!meta.title) {
563
- meta.title = getDefaultTitle(content);
399
+ meta.title = (0, import_theme_shared2.getDefaultTitle)(content);
564
400
  }
565
- const date = await (meta.date && /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`) || getFileBirthTime(filepath));
401
+ const date = await (meta.date && /* @__PURE__ */ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`) || (0, import_theme_shared2.getFileLastModifyTime)(filepath));
566
402
  meta.date = formatDate(date || /* @__PURE__ */ new Date());
567
403
  meta.categories = typeof meta.categories === "string" ? [meta.categories] : meta.categories;
568
404
  meta.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags;
569
405
  meta.tag = [meta.tag || []].flat().concat([
570
406
  .../* @__PURE__ */ new Set([...meta.categories || [], ...meta.tags || []])
571
407
  ]);
572
- meta.description = meta.description || getTextSummary(content, 100) || excerpt;
408
+ meta.description = meta.description || (0, import_theme_shared2.getTextSummary)(content, 100) || excerpt;
573
409
  meta.cover = meta.cover ?? getFirstImagURLFromMD(fileContent, route);
574
410
  if (meta.publish === false) {
575
411
  meta.hidden = true;
@@ -577,13 +413,12 @@ async function getArticleMeta(filepath, route, timeZone = defaultTimeZoneOffset)
577
413
  }
578
414
  return meta;
579
415
  }
580
- async function getArticles(cfg) {
581
- const srcDir = cfg?.srcDir || import_node_process.default.argv.slice(2)?.[1] || ".";
582
- const files = import_fast_glob.default.sync(`${srcDir}/**/*.md`, { ignore: ["node_modules"] });
583
- const limit = pLimit(+(import_node_process.default.env.P_LIMT_MAX || import_node_os.default.cpus().length));
416
+ async function getArticles(cfg, vpConfig) {
417
+ const srcDir = cfg?.srcDir || vpConfig.srcDir.replace(vpConfig.root, "").replace(/^\//, "") || import_node_process.default.argv.slice(2)?.[1] || ".";
418
+ const files = import_fast_glob.default.sync(`${srcDir}/**/*.md`, { ignore: ["node_modules"], absolute: true });
584
419
  const metaResults = files.reduce((prev, curr) => {
585
- const route = getPageRoute(curr, srcDir);
586
- const metaPromise = limit(() => getArticleMeta(curr, route, cfg?.timeZone));
420
+ const route = getPageRoute(curr, vpConfig.srcDir);
421
+ const metaPromise = getArticleMeta(curr, route, cfg?.timeZone);
587
422
  prev[curr] = {
588
423
  route,
589
424
  metaPromise
@@ -623,10 +458,11 @@ function checkConfig(cfg) {
623
458
 
624
459
  // src/utils/node/vitePlugins.ts
625
460
  var import_node_path3 = __toESM(require("path"));
626
- var import_node_fs3 = require("fs");
461
+ var import_node_fs2 = require("fs");
627
462
  var import_node_buffer = require("buffer");
628
463
  var import_vitepress_plugin_pagefind = require("vitepress-plugin-pagefind");
629
464
  var import_vitepress_plugin_rss = require("vitepress-plugin-rss");
465
+ var import_theme_shared3 = require("@sugarat/theme-shared");
630
466
 
631
467
  // src/utils/node/hot-reload-plugin.ts
632
468
  function themeReloadPlugin() {
@@ -679,20 +515,19 @@ function themeReloadPlugin() {
679
515
  }
680
516
 
681
517
  // src/utils/node/vitePlugins.ts
682
- function getVitePlugins(cfg) {
518
+ function getVitePlugins(cfg = {}) {
683
519
  const plugins = [];
684
520
  plugins.push(coverImgTransform());
521
+ if (cfg.themeColor) {
522
+ plugins.push(setThemeScript(cfg.themeColor));
523
+ }
685
524
  plugins.push(themeReloadPlugin());
686
525
  plugins.push(providePageData(cfg));
687
526
  if (cfg && cfg.search !== false) {
688
527
  const ops = cfg.search instanceof Object ? cfg.search : {};
689
528
  plugins.push(
690
529
  (0, import_vitepress_plugin_pagefind.pagefindPlugin)({
691
- ...ops,
692
- customSearchQuery: import_vitepress_plugin_pagefind.chineseSearchOptimize,
693
- filter(searchItem) {
694
- return searchItem.meta.publish !== false;
695
- }
530
+ ...ops
696
531
  })
697
532
  );
698
533
  }
@@ -702,7 +537,8 @@ function getVitePlugins(cfg) {
702
537
  plugins.push(MermaidPlugin(cfg?.mermaid === true ? {} : cfg?.mermaid ?? {}));
703
538
  }
704
539
  if (cfg?.RSS) {
705
- plugins.push((0, import_vitepress_plugin_rss.RssPlugin)(cfg.RSS));
540
+ ;
541
+ [cfg?.RSS].flat().forEach((rssConfig) => plugins.push((0, import_vitepress_plugin_rss.RssPlugin)(rssConfig)));
706
542
  }
707
543
  return plugins;
708
544
  }
@@ -749,13 +585,13 @@ function coverImgTransform() {
749
585
  }
750
586
  try {
751
587
  const realPath = import_node_path3.default.join(vitepressConfig.root, cover);
752
- if (!(0, import_node_fs3.existsSync)(realPath)) {
588
+ if (!(0, import_node_fs2.existsSync)(realPath)) {
753
589
  continue;
754
590
  }
755
- const fileBuffer = (0, import_node_fs3.readFileSync)(realPath);
591
+ const fileBuffer = (0, import_node_fs2.readFileSync)(realPath);
756
592
  const matchAsset = assetsMap.find((v) => import_node_buffer.Buffer.compare(fileBuffer, v.source) === 0);
757
593
  if (matchAsset) {
758
- page.meta.cover = joinPath("/", matchAsset.fileName);
594
+ page.meta.cover = (0, import_theme_shared3.joinPath)("/", matchAsset.fileName);
759
595
  }
760
596
  } catch (e) {
761
597
  vitepressConfig.logger.warn(e?.message);
@@ -768,11 +604,38 @@ function providePageData(cfg) {
768
604
  return {
769
605
  name: "@sugarat/theme-plugin-provide-page-data",
770
606
  async config(config) {
771
- const pagesData = await getArticles(cfg);
607
+ const pagesData = await getArticles(cfg, config.vitepress);
772
608
  config.vitepress.site.themeConfig.blog.pagesData = pagesData;
773
609
  }
774
610
  };
775
611
  }
612
+ function setThemeScript(themeColor) {
613
+ let resolveConfig;
614
+ const pluginOps = {
615
+ name: "@sugarat/theme-plugin-theme-color-script",
616
+ enforce: "pre",
617
+ configResolved(config) {
618
+ if (resolveConfig) {
619
+ return;
620
+ }
621
+ resolveConfig = config;
622
+ const vitepressConfig = config.vitepress;
623
+ if (!vitepressConfig) {
624
+ return;
625
+ }
626
+ const selfTransformHead = vitepressConfig.transformHead;
627
+ vitepressConfig.transformHead = async (ctx) => {
628
+ const selfHead = await Promise.resolve(selfTransformHead?.(ctx)) || [];
629
+ return selfHead.concat([
630
+ ["script", { type: "text/javascript" }, `;(function() {
631
+ document.documentElement.setAttribute("theme", "${themeColor}");
632
+ })()`]
633
+ ]);
634
+ };
635
+ }
636
+ };
637
+ return pluginOps;
638
+ }
776
639
 
777
640
  // src/node.ts
778
641
  function getThemeConfig(cfg = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sugarat/theme",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "简约风的 Vitepress 博客主题,sugarat vitepress blog theme",
5
5
  "author": "sugar",
6
6
  "license": "MIT",
@@ -42,9 +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",
46
45
  "fast-glob": "^3.3.2",
47
- "gray-matter": "^4.0.3",
48
46
  "markdown-it-task-checkbox": "^1.0.6",
49
47
  "mermaid": "^10.9.0",
50
48
  "oh-my-live2d": "^0.19.3",
@@ -52,14 +50,14 @@
52
50
  "vitepress-markdown-timeline": "^1.2.1",
53
51
  "vitepress-plugin-mermaid": "2.0.13",
54
52
  "vitepress-plugin-tabs": "0.2.0",
55
- "vitepress-plugin-pagefind": "0.4.1",
56
- "vitepress-plugin-rss": "0.2.6"
53
+ "@sugarat/theme-shared": "0.0.1",
54
+ "vitepress-plugin-rss": "0.2.7",
55
+ "vitepress-plugin-pagefind": "0.4.2"
57
56
  },
58
57
  "devDependencies": {
59
58
  "@element-plus/icons-vue": "^2.3.1",
60
59
  "artalk": "^2.8.5",
61
60
  "element-plus": "^2.7.2",
62
- "p-limit": "4",
63
61
  "pagefind": "^1.1.0",
64
62
  "sass": "^1.76.0",
65
63
  "typescript": "^5.4.5",
@@ -102,7 +102,7 @@ const resultCover = computed(() => {
102
102
  <div class="badge-list mobile-visible">
103
103
  <span v-show="author" class="split">{{ author }}</span>
104
104
  <span class="split">{{ showTime }}</span>
105
- <span v-show="tag?.length" class="split">{{ tag?.join(' · ') }}</span>
105
+ <span v-if="tag?.length" class="split">{{ tag?.join(' · ') }}</span>
106
106
  </div>
107
107
  </a>
108
108
  </template>
@@ -5,6 +5,7 @@ import type { RSSOptions } from 'vitepress-plugin-rss'
5
5
  import type { Mapping, Repo } from '@giscus/vue'
6
6
  import type { Options as Oml2dOptions } from 'oh-my-live2d'
7
7
  import type { Ref } from 'vue'
8
+ import type { PagefindConfig } from 'vitepress-plugin-pagefind'
8
9
 
9
10
  type RSSPluginOptions = RSSOptions
10
11
 
@@ -347,19 +348,8 @@ export namespace Theme {
347
348
  top?: number
348
349
  }
349
350
  export type SearchConfig =
350
- | boolean
351
- | 'pagefind'
352
- | {
353
- btnPlaceholder?: string
354
- placeholder?: string
355
- emptyText?: string
356
- /**
357
- * @example
358
- * 'Total: {{searchResult}} search results.'
359
- */
360
- heading?: string
361
- mode?: boolean | 'pagefind'
362
- }
351
+ | false
352
+ | PagefindConfig
363
353
 
364
354
  export interface UserWorks {
365
355
  title: string
@@ -381,7 +371,7 @@ export namespace Theme {
381
371
  /**
382
372
  * 内置一些主题色
383
373
  * @default 'vp-default'
384
- * 也可以自定义颜色,详见 TODO:文档
374
+ * 也可以自定义颜色,详见 https://theme.sugarat.top/config/style.html#%E4%B8%BB%E9%A2%98%E8%89%B2
385
375
  */
386
376
  themeColor?: ThemeColor
387
377
  pagesData: PageData[]
@@ -434,7 +424,7 @@ export namespace Theme {
434
424
  /**
435
425
  * 启用RSS配置
436
426
  */
437
- RSS?: RSSOptions
427
+ RSS?: RSSOptions | RSSOptions[]
438
428
  /**
439
429
  * 首页页脚
440
430
  */
@@ -1,118 +1,5 @@
1
- /* eslint-disable global-require */
2
- /* eslint-disable prefer-rest-params */
3
- import { spawn } from 'node:child_process'
4
1
  import path from 'node:path'
5
- import fs from 'node:fs'
6
-
7
- export function clearMatterContent(content: string) {
8
- let first___: unknown
9
- let second___: unknown
10
-
11
- const lines = content.split('\n').reduce<string[]>((pre, line) => {
12
- // 移除开头的空白行
13
- if (!line.trim() && pre.length === 0) {
14
- return pre
15
- }
16
- if (line.trim() === '---') {
17
- if (first___ === undefined) {
18
- first___ = pre.length
19
- }
20
- else if (second___ === undefined) {
21
- second___ = pre.length
22
- }
23
- }
24
- pre.push(line)
25
- return pre
26
- }, [])
27
- return (
28
- lines
29
- // 剔除---之间的内容
30
- .slice((second___ as number) || 0)
31
- .join('\n')
32
- )
33
- }
34
-
35
- export function getDefaultTitle(content: string) {
36
- const match = content.match(/^(#+)\s+(.+)/m)
37
- return match?.[2] || ''
38
- }
39
-
40
- const cache = new Map<string, Date | undefined>()
41
- export function getFileBirthTime(url: string): Promise<Date | undefined> | Date {
42
- const cached = cache.get(url)
43
- if (cached) {
44
- return cached
45
- }
46
-
47
- return new Promise((resolve) => {
48
- // 使用异步回调
49
- const child = spawn('git', ['log', '-1', '--pretty="%ai"', url])
50
- let output = ''
51
- child.stdout.on('data', d => (output += String(d)))
52
- child.on('close', async () => {
53
- let date: Date | undefined
54
- if (output.trim()) {
55
- date = new Date(output)
56
- }
57
- else {
58
- date = await getFileBirthTimeByFs(url)
59
- }
60
- cache.set(url, date)
61
- resolve(date)
62
- })
63
- child.on('error', async () => {
64
- const fsDate = await getFileBirthTimeByFs(url)
65
- resolve(fsDate)
66
- })
67
- })
68
- }
69
-
70
- export async function getFileBirthTimeByFs(url: string) {
71
- try {
72
- const fsStat = await fs.promises.stat(url)
73
- return fsStat.birthtime
74
- }
75
- catch {
76
- return undefined
77
- }
78
- }
79
-
80
- export function getGitTimestamp(file: string) {
81
- return new Promise((resolve, reject) => {
82
- const child = spawn('git', ['log', '-1', '--pretty="%ci"', file])
83
- let output = ''
84
- child.stdout.on('data', (d) => {
85
- output += String(d)
86
- })
87
- child.on('close', () => {
88
- resolve(+new Date(output))
89
- })
90
- child.on('error', reject)
91
- })
92
- }
93
-
94
- export function getTextSummary(text: string, count = 100) {
95
- return (
96
- text
97
- // 首个标题
98
- ?.replace(/^#+\s+.*/, '')
99
- // 除去标题
100
- ?.replace(/#/g, '')
101
- // 除去图片
102
- ?.replace(/!\[.*?\]\(.*?\)/g, '')
103
- // 除去链接
104
- ?.replace(/\[(.*?)\]\(.*?\)/g, '$1')
105
- // 除去加粗
106
- ?.replace(/\*\*(.*?)\*\*/g, '$1')
107
- ?.split('\n')
108
- ?.filter(v => !!v)
109
- ?.join('\n')
110
- ?.replace(/>(.*)/, '')
111
- ?.replace(/</g, '&lt;').replace(/>/g, '&gt;')
112
- ?.trim()
113
- ?.slice(0, count)
114
- )
115
- }
2
+ import { joinPath } from '@sugarat/theme-shared'
116
3
 
117
4
  export function aliasObjectToArray(obj: Record<string, string>) {
118
5
  return Object.entries(obj).map(([find, replacement]) => ({
@@ -121,21 +8,6 @@ export function aliasObjectToArray(obj: Record<string, string>) {
121
8
  }))
122
9
  }
123
10
 
124
- export const EXTERNAL_URL_RE = /^[a-z]+:/i
125
-
126
- /**
127
- * Join two paths by resolving the slash collision.
128
- */
129
- export function joinPath(base: string, path: string): string {
130
- return `${base}${path}`.replace(/\/+/g, '/')
131
- }
132
-
133
- export function withBase(base: string, path: string) {
134
- return EXTERNAL_URL_RE.test(path) || path.startsWith('.')
135
- ? path
136
- : joinPath(base, path)
137
- }
138
-
139
11
  function isBase64ImageURL(url: string) {
140
12
  // Base64 图片链接的格式为 data:image/[image format];base64,[Base64 编码的数据]
141
13
  const regex = /^data:image\/[a-z]+;base64,/
@@ -2,13 +2,12 @@
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'
6
5
  import glob from 'fast-glob'
7
- import matter from 'gray-matter'
8
- import pLimit from 'p-limit'
6
+ import { getDefaultTitle, getFileLastModifyTime, getTextSummary, grayMatter, normalizePath } from '@sugarat/theme-shared'
7
+ import type { SiteConfig } from 'vitepress'
9
8
  import type { Theme } from '../../composables/config/index'
10
9
  import { formatDate } from '../client'
11
- import { getDefaultTitle, getFileBirthTime, getFirstImagURLFromMD, getTextSummary } from './index'
10
+ import { getFirstImagURLFromMD } from './index'
12
11
 
13
12
  export function patchDefaultThemeSideBar(cfg?: Partial<Theme.BlogConfig>) {
14
13
  return cfg?.blog !== false && cfg?.recommend !== false
@@ -24,29 +23,8 @@ export function patchDefaultThemeSideBar(cfg?: Partial<Theme.BlogConfig>) {
24
23
  }
25
24
 
26
25
  export function getPageRoute(filepath: string, srcDir: string) {
27
- let route = filepath.replace('.md', '')
28
- // 去除 srcDir 处理目录名
29
- // TODO:优化 路径处理,同VitePress 内部一致
30
- if (route.startsWith('./')) {
31
- route = route.replace(
32
- new RegExp(
33
- `^\\.\\/${path
34
- .join(srcDir, '/')
35
- .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
36
- ),
37
- ''
38
- )
39
- }
40
- else {
41
- route = route.replace(
42
- new RegExp(
43
- `^${path
44
- .join(srcDir, '/')
45
- .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
46
- ),
47
- ''
48
- )
49
- }
26
+ const route = normalizePath(path.relative(srcDir, filepath))
27
+ .replace(/\.md$/, '')
50
28
  return `/${route}`
51
29
  }
52
30
 
@@ -54,7 +32,7 @@ const defaultTimeZoneOffset = new Date().getTimezoneOffset() / -60
54
32
  export async function getArticleMeta(filepath: string, route: string, timeZone = defaultTimeZoneOffset) {
55
33
  const fileContent = await fs.promises.readFile(filepath, 'utf-8')
56
34
 
57
- const { data: frontmatter, excerpt, content } = matter(fileContent, {
35
+ const { data: frontmatter, excerpt, content } = grayMatter(fileContent, {
58
36
  excerpt: true,
59
37
  })
60
38
 
@@ -68,7 +46,7 @@ export async function getArticleMeta(filepath: string, route: string, timeZone =
68
46
  const date = await (
69
47
  (meta.date
70
48
  && new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`))
71
- || getFileBirthTime(filepath)
49
+ || getFileLastModifyTime(filepath)
72
50
  )
73
51
  // 无法获取时兜底当前时间
74
52
  meta.date = formatDate(date || new Date())
@@ -102,14 +80,16 @@ export async function getArticleMeta(filepath: string, route: string, timeZone =
102
80
  }
103
81
  return meta as Theme.PageMeta
104
82
  }
105
- export async function getArticles(cfg?: Partial<Theme.BlogConfig>) {
106
- const srcDir = cfg?.srcDir || process.argv.slice(2)?.[1] || '.'
107
- const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'] })
108
- const limit = pLimit(+(process.env.P_LIMT_MAX || os.cpus().length))
83
+ export async function getArticles(cfg: Partial<Theme.BlogConfig>, vpConfig: SiteConfig) {
84
+ const srcDir
85
+ = cfg?.srcDir || vpConfig.srcDir.replace(vpConfig.root, '').replace(/^\//, '')
86
+ || process.argv.slice(2)?.[1]
87
+ || '.'
88
+ const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'], absolute: true })
109
89
 
110
90
  const metaResults = files.reduce((prev, curr) => {
111
- const route = getPageRoute(curr, srcDir)
112
- const metaPromise = limit(() => getArticleMeta(curr, route, cfg?.timeZone))
91
+ const route = getPageRoute(curr, vpConfig.srcDir)
92
+ const metaPromise = getArticleMeta(curr, route, cfg?.timeZone)
113
93
 
114
94
  // 提前获取,有缓存取缓存
115
95
  prev[curr] = {
@@ -1,30 +1,28 @@
1
1
  import path from 'node:path'
2
2
  import { existsSync, readFileSync } from 'node:fs'
3
3
  import { Buffer } from 'node:buffer'
4
- import type { SiteConfig } from 'vitepress'
5
-
4
+ import type { HeadConfig, SiteConfig } from 'vitepress'
6
5
  import {
7
- chineseSearchOptimize,
8
6
  pagefindPlugin
9
7
  } from 'vitepress-plugin-pagefind'
10
8
  import { RssPlugin } from 'vitepress-plugin-rss'
11
9
  import type { PluginOption } from 'vite'
10
+ import { joinPath } from '@sugarat/theme-shared'
12
11
  import type { Theme } from '../../composables/config/index'
13
12
  import { _require } from './mdPlugins'
14
13
  import { themeReloadPlugin } from './hot-reload-plugin'
15
14
  import { getArticles } from './theme'
16
- import { joinPath } from './index'
17
15
 
18
- export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
16
+ export function getVitePlugins(cfg: Partial<Theme.BlogConfig> = {}) {
19
17
  const plugins: any[] = []
20
18
 
21
- // const buildEndFn: any[] = []
22
- // Build完后运行的一系列列方法,执行自定义的 buildEnd 钩子
23
- // plugins.push(inlineBuildEndPlugin(buildEndFn))
24
-
25
19
  // 处理cover image的路径(暂只支持自动识别的文章首图)
26
20
  plugins.push(coverImgTransform())
27
21
 
22
+ // 处理自定义主题色
23
+ if (cfg.themeColor) {
24
+ plugins.push(setThemeScript(cfg.themeColor))
25
+ }
28
26
  // 自动重载首页
29
27
  plugins.push(themeReloadPlugin())
30
28
 
@@ -37,10 +35,6 @@ export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
37
35
  plugins.push(
38
36
  pagefindPlugin({
39
37
  ...ops,
40
- customSearchQuery: chineseSearchOptimize,
41
- filter(searchItem) {
42
- return searchItem.meta.publish !== false
43
- }
44
38
  })
45
39
  )
46
40
  }
@@ -54,7 +48,7 @@ export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
54
48
 
55
49
  // 内置支持RSS
56
50
  if (cfg?.RSS) {
57
- plugins.push(RssPlugin(cfg.RSS))
51
+ ;[cfg?.RSS].flat().forEach(rssConfig => plugins.push(RssPlugin(rssConfig)))
58
52
  }
59
53
  return plugins
60
54
  }
@@ -153,12 +147,44 @@ export function coverImgTransform() {
153
147
  }
154
148
  }
155
149
 
156
- export function providePageData(cfg?: Partial<Theme.BlogConfig>) {
150
+ export function providePageData(cfg: Partial<Theme.BlogConfig>) {
157
151
  return {
158
152
  name: '@sugarat/theme-plugin-provide-page-data',
159
153
  async config(config: any) {
160
- const pagesData = await getArticles(cfg)
154
+ const pagesData = await getArticles(cfg, config.vitepress)
161
155
  config.vitepress.site.themeConfig.blog.pagesData = pagesData
162
156
  },
163
157
  } as PluginOption
164
158
  }
159
+
160
+ export function setThemeScript(
161
+ themeColor: Theme.ThemeColor
162
+ ) {
163
+ let resolveConfig: any
164
+ const pluginOps: PluginOption = {
165
+ name: '@sugarat/theme-plugin-theme-color-script',
166
+ enforce: 'pre',
167
+ configResolved(config: any) {
168
+ if (resolveConfig) {
169
+ return
170
+ }
171
+ resolveConfig = config
172
+
173
+ const vitepressConfig: SiteConfig = config.vitepress
174
+ if (!vitepressConfig) {
175
+ return
176
+ }
177
+ // 通过 head 添加额外的脚本注入
178
+ const selfTransformHead = vitepressConfig.transformHead
179
+ vitepressConfig.transformHead = async (ctx) => {
180
+ const selfHead = (await Promise.resolve(selfTransformHead?.(ctx))) || []
181
+ return selfHead.concat([
182
+ ['script', { type: 'text/javascript' }, `;(function() {
183
+ document.documentElement.setAttribute("theme", "${themeColor}");
184
+ })()`]
185
+ ] as HeadConfig[])
186
+ }
187
+ }
188
+ }
189
+ return pluginOps
190
+ }