@sugarat/theme 0.1.39 → 0.1.41

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.
@@ -0,0 +1,118 @@
1
+ /* eslint-disable global-require */
2
+ /* eslint-disable prefer-rest-params */
3
+ import { spawn, spawnSync } from 'child_process'
4
+ import { formatDate } from '../client'
5
+
6
+ export function clearMatterContent(content: string) {
7
+ let first___: unknown
8
+ let second___: unknown
9
+
10
+ const lines = content.split('\n').reduce<string[]>((pre, line) => {
11
+ // 移除开头的空白行
12
+ if (!line.trim() && pre.length === 0) {
13
+ return pre
14
+ }
15
+ if (line.trim() === '---') {
16
+ if (first___ === undefined) {
17
+ first___ = pre.length
18
+ } else if (second___ === undefined) {
19
+ second___ = pre.length
20
+ }
21
+ }
22
+ pre.push(line)
23
+ return pre
24
+ }, [])
25
+ return (
26
+ lines
27
+ // 剔除---之间的内容
28
+ .slice((second___ as number) || 0)
29
+ .join('\n')
30
+ )
31
+ }
32
+ export function getDefaultTitle(content: string) {
33
+ const title =
34
+ clearMatterContent(content)
35
+ .split('\n')
36
+ ?.find((str) => {
37
+ return str.startsWith('# ')
38
+ })
39
+ ?.slice(2)
40
+ .replace(/^\s+|\s+$/g, '') || ''
41
+ return title
42
+ }
43
+
44
+ export function getFileBirthTime(url: string) {
45
+ let date = new Date()
46
+
47
+ try {
48
+ // 参考 vitepress 中的 getGitTimestamp 实现
49
+ const infoStr = spawnSync('git', ['log', '-1', '--pretty="%ci"', url])
50
+ .stdout?.toString()
51
+ .replace(/["']/g, '')
52
+ .trim()
53
+ if (infoStr) {
54
+ date = new Date(infoStr)
55
+ }
56
+ } catch (error) {
57
+ return formatDate(date)
58
+ }
59
+
60
+ return formatDate(date)
61
+ }
62
+
63
+ export function getGitTimestamp(file: string) {
64
+ return new Promise((resolve, reject) => {
65
+ const child = spawn('git', ['log', '-1', '--pretty="%ci"', file])
66
+ let output = ''
67
+ child.stdout.on('data', (d) => {
68
+ output += String(d)
69
+ })
70
+ child.on('close', () => {
71
+ resolve(+new Date(output))
72
+ })
73
+ child.on('error', reject)
74
+ })
75
+ }
76
+
77
+ export function getTextSummary(text: string, count = 100) {
78
+ return (
79
+ clearMatterContent(text)
80
+ .match(/^# ([\s\S]+)/m)?.[1]
81
+ // 除去标题
82
+ ?.replace(/#/g, '')
83
+ // 除去图片
84
+ ?.replace(/!\[.*?\]\(.*?\)/g, '')
85
+ // 除去链接
86
+ ?.replace(/\[(.*?)\]\(.*?\)/g, '$1')
87
+ // 除去加粗
88
+ ?.replace(/\*\*(.*?)\*\*/g, '$1')
89
+ ?.split('\n')
90
+ ?.filter((v) => !!v)
91
+ ?.slice(1)
92
+ ?.join('\n')
93
+ ?.replace(/>(.*)/, '')
94
+ ?.slice(0, count)
95
+ )
96
+ }
97
+
98
+ export function aliasObjectToArray(obj: Record<string, string>) {
99
+ return Object.entries(obj).map(([find, replacement]) => ({
100
+ find,
101
+ replacement
102
+ }))
103
+ }
104
+
105
+ export const EXTERNAL_URL_RE = /^[a-z]+:/i
106
+
107
+ /**
108
+ * Join two paths by resolving the slash collision.
109
+ */
110
+ export function joinPath(base: string, path: string): string {
111
+ return `${base}${path}`.replace(/\/+/g, '/')
112
+ }
113
+
114
+ export function withBase(base: string, path: string) {
115
+ return EXTERNAL_URL_RE.test(path) || path.startsWith('.')
116
+ ? path
117
+ : joinPath(base, path)
118
+ }
@@ -0,0 +1,95 @@
1
+ /* eslint-disable global-require */
2
+ import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
3
+ import type { UserConfig } from 'vitepress'
4
+ import { aliasObjectToArray } from './index'
5
+ import type { Theme } from '../../composables/config/index'
6
+
7
+ export function getMarkdownPlugins(cfg?: Partial<Theme.BlogConfig>) {
8
+ const markdownPlugin: any[] = []
9
+ // tabs支持
10
+ if (cfg?.tabs) {
11
+ markdownPlugin.push(tabsMarkdownPlugin)
12
+ }
13
+
14
+ // 添加mermaid markdown 插件
15
+ if (cfg) {
16
+ cfg.mermaid = cfg?.mermaid ?? true
17
+ if (cfg?.mermaid !== false) {
18
+ const { MermaidMarkdown } = require('vitepress-plugin-mermaid')
19
+ markdownPlugin.push(MermaidMarkdown)
20
+ }
21
+ }
22
+ return markdownPlugin
23
+ }
24
+
25
+ export function registerMdPlugins(vpCfg: any, plugins: any[]) {
26
+ if (plugins.length) {
27
+ vpCfg.markdown = {
28
+ config(...rest: any[]) {
29
+ plugins.forEach((plugin) => {
30
+ plugin?.(...rest)
31
+ })
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 流程图支持,配置mermaid
39
+ */
40
+ export function assignMermaid(config: any) {
41
+ if (!config?.mermaid) return
42
+
43
+ if (!config.vite) config.vite = {}
44
+ if (!config.vite.plugins) config.vite.plugins = []
45
+ const { MermaidPlugin } = require('vitepress-plugin-mermaid')
46
+ config.vite.plugins.push(MermaidPlugin(config.mermaid))
47
+ if (!config.vite.resolve) config.vite.resolve = {}
48
+ if (!config.vite.resolve.alias) config.vite.resolve.alias = {}
49
+
50
+ config.vite.resolve.alias = [
51
+ ...aliasObjectToArray({
52
+ ...config.vite.resolve.alias,
53
+ 'cytoscape/dist/cytoscape.umd.js': 'cytoscape/dist/cytoscape.esm.js',
54
+ mermaid: 'mermaid/dist/mermaid.esm.mjs'
55
+ }),
56
+ { find: /^dayjs\/(.*).js/, replacement: 'dayjs/esm/$1' }
57
+ ]
58
+ }
59
+
60
+ export function wrapperCfgWithMermaid(config: UserConfig<Theme.Config>): any {
61
+ // @ts-ignore
62
+ const extendThemeConfig = (config.extends?.themeConfig?.blog ||
63
+ {}) as Theme.BlogConfig
64
+
65
+ // 开关支持Mermaid
66
+ const resultConfig =
67
+ extendThemeConfig.mermaid === false
68
+ ? config
69
+ : {
70
+ ...config,
71
+ mermaid:
72
+ extendThemeConfig.mermaid === true ? {} : extendThemeConfig.mermaid
73
+ }
74
+ assignMermaid(resultConfig)
75
+ return resultConfig
76
+ }
77
+
78
+ export function supportRunExtendsPlugin(config: UserConfig<Theme.Config>) {
79
+ // 处理markdown插件
80
+ if (!config.markdown) config.markdown = {}
81
+ // 支持运行继承的markdown插件
82
+ // @ts-ignore
83
+ if (config.extends?.markdown?.config) {
84
+ const markdownExtendsConfigOriginal =
85
+ // @ts-ignore
86
+ config.extends?.markdown?.config
87
+ const selfMarkdownConfig = config.markdown?.config
88
+
89
+ config.markdown.config = (...rest: any[]) => {
90
+ // @ts-ignore
91
+ selfMarkdownConfig?.(...rest)
92
+ markdownExtendsConfigOriginal?.(...rest)
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,147 @@
1
+ /* eslint-disable prefer-rest-params */
2
+ import glob from 'fast-glob'
3
+ import matter from 'gray-matter'
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+ import type { Theme } from '../../composables/config/index'
7
+ import { getDefaultTitle, getFileBirthTime, getTextSummary } from './index'
8
+ import { formatDate } from '../client'
9
+
10
+ export function patchDefaultThemeSideBar(cfg?: Partial<Theme.BlogConfig>) {
11
+ return cfg?.blog !== false && cfg?.recommend !== false
12
+ ? {
13
+ sidebar: [
14
+ {
15
+ text: '',
16
+ items: []
17
+ }
18
+ ]
19
+ }
20
+ : undefined
21
+ }
22
+
23
+ // hack:RSS用
24
+ export const pageMap = new Map<string, string>()
25
+
26
+ export function getArticles(cfg?: Partial<Theme.BlogConfig>) {
27
+ const srcDir = cfg?.srcDir || process.argv.slice(2)?.[1] || '.'
28
+ const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'] })
29
+
30
+ // 文章数据
31
+ const data = files
32
+ .map((v) => {
33
+ let route = v
34
+ // 处理文件后缀名
35
+ .replace('.md', '')
36
+
37
+ // 去除 srcDir 处理目录名
38
+ if (route.startsWith('./')) {
39
+ route = route.replace(
40
+ new RegExp(
41
+ `^\\.\\/${path
42
+ .join(srcDir, '/')
43
+ .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
44
+ ),
45
+ ''
46
+ )
47
+ } else {
48
+ route = route.replace(
49
+ new RegExp(
50
+ `^${path
51
+ .join(srcDir, '/')
52
+ .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
53
+ ),
54
+ ''
55
+ )
56
+ }
57
+ // hack:RSS使用
58
+ pageMap.set(`/${route}`, v)
59
+
60
+ const fileContent = fs.readFileSync(v, 'utf-8')
61
+ // TODO:摘要生成优化
62
+ const { data: frontmatter, excerpt } = matter(fileContent, {
63
+ excerpt: true
64
+ })
65
+
66
+ const meta: Partial<Theme.PageMeta> = {
67
+ ...frontmatter
68
+ }
69
+
70
+ if (!meta.title) {
71
+ meta.title = getDefaultTitle(fileContent)
72
+ }
73
+ if (!meta.date) {
74
+ // getGitTimestamp(v).then((v) => {
75
+ // meta.date = formatDate(v)
76
+ // })
77
+ meta.date = getFileBirthTime(v)
78
+ } else {
79
+ const timeZone = cfg?.timeZone ?? 8
80
+ meta.date = formatDate(
81
+ new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
82
+ )
83
+ }
84
+
85
+ // 处理tags和categories,兼容历史文章
86
+ meta.categories =
87
+ typeof meta.categories === 'string'
88
+ ? [meta.categories]
89
+ : meta.categories
90
+ meta.tags = typeof meta.tags === 'string' ? [meta.tags] : meta.tags
91
+ meta.tag = [meta.tag || []]
92
+ .flat()
93
+ .concat([
94
+ ...new Set([...(meta.categories || []), ...(meta.tags || [])])
95
+ ])
96
+
97
+ // 获取摘要信息
98
+ const wordCount = 100
99
+ meta.description =
100
+ meta.description || getTextSummary(fileContent, wordCount)
101
+
102
+ // 获取封面图
103
+ meta.cover =
104
+ meta.cover ??
105
+ (fileContent.match(/[!]\[.*?\]\((https:\/\/.+)\)/)?.[1] || '')
106
+
107
+ // 是否发布 默认发布
108
+ if (meta.publish === false) {
109
+ meta.hidden = true
110
+ meta.recommend = false
111
+ }
112
+
113
+ return {
114
+ route: `/${route}`,
115
+ meta
116
+ }
117
+ })
118
+ .filter((v) => v.meta.layout !== 'home')
119
+ return data as Theme.PageData[]
120
+ }
121
+
122
+ export function patchVPConfig(vpConfig: any, cfg?: Partial<Theme.BlogConfig>) {
123
+ // TODO: 待确定场景
124
+ }
125
+
126
+ export function patchVPThemeConfig(
127
+ cfg?: Partial<Theme.BlogConfig>,
128
+ vpThemeConfig: any = {}
129
+ ) {
130
+ // 添加 icon
131
+ const RSS = cfg?.RSS
132
+ if (RSS && RSS.showIcon !== false) {
133
+ vpThemeConfig.socialLinks = [
134
+ {
135
+ icon: {
136
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM112 416c-26.51 0-48-21.49-48-48s21.49-48 48-48s48 21.49 48 48s-21.49 48-48 48zm157.533 0h-34.335c-6.011 0-11.051-4.636-11.442-10.634c-5.214-80.05-69.243-143.92-149.123-149.123c-5.997-.39-10.633-5.431-10.633-11.441v-34.335c0-6.535 5.468-11.777 11.994-11.425c110.546 5.974 198.997 94.536 204.964 204.964c.352 6.526-4.89 11.994-11.425 11.994zm103.027 0h-34.334c-6.161 0-11.175-4.882-11.427-11.038c-5.598-136.535-115.204-246.161-251.76-251.76C68.882 152.949 64 147.935 64 141.774V107.44c0-6.454 5.338-11.664 11.787-11.432c167.83 6.025 302.21 141.191 308.205 308.205c.232 6.449-4.978 11.787-11.432 11.787z" fill="currentColor"></path></svg>'
137
+ },
138
+ link: RSS.url
139
+ }
140
+ ]
141
+ }
142
+
143
+ // 用于自定义sidebar卡片slot
144
+ vpThemeConfig.sidebar = patchDefaultThemeSideBar(cfg)?.sidebar
145
+
146
+ return vpThemeConfig
147
+ }
@@ -0,0 +1,98 @@
1
+ import type { SiteConfig } from 'vitepress'
2
+ import path from 'path'
3
+ import { execSync } from 'child_process'
4
+ import type { Theme } from '../../composables/config/index'
5
+ import { genFeed } from './genFeed'
6
+
7
+ export function getVitePlugins(cfg?: Partial<Theme.BlogConfig>) {
8
+ const plugins: any[] = []
9
+
10
+ // Build完后运行的一系列列方法
11
+ const buildEndFn: any[] = []
12
+ // 执行自定义的 buildEnd 钩子
13
+ plugins.push(inlineBuildEndPlugin(buildEndFn))
14
+
15
+ // 内置简化版的pagefind
16
+ if (
17
+ cfg?.search === 'pagefind' ||
18
+ (cfg?.search instanceof Object && cfg.search.mode === 'pagefind')
19
+ ) {
20
+ plugins.push(inlinePagefindPlugin(buildEndFn))
21
+ }
22
+
23
+ buildEndFn.push(genFeed)
24
+ return plugins
25
+ }
26
+
27
+ export function registerVitePlugins(vpCfg: any, plugins: any[]) {
28
+ vpCfg.vite = {
29
+ plugins
30
+ }
31
+ }
32
+
33
+ export function inlinePagefindPlugin(buildEndFn: any[]) {
34
+ buildEndFn.push(() => {
35
+ // 调用pagefind
36
+ const ignore: string[] = [
37
+ // 侧边栏内容
38
+ 'div.aside',
39
+ // 标题锚点
40
+ 'a.header-anchor'
41
+ ]
42
+ const { log } = console
43
+ log()
44
+ log('=== pagefind: https://pagefind.app/ ===')
45
+ let command = `npx pagefind --source ${path.join(
46
+ process.argv.slice(2)?.[1] || '.',
47
+ '.vitepress/dist'
48
+ )}`
49
+
50
+ if (ignore.length) {
51
+ command += ` --exclude-selectors "${ignore.join(', ')}"`
52
+ }
53
+
54
+ log(command)
55
+ log()
56
+ execSync(command, {
57
+ stdio: 'inherit'
58
+ })
59
+ })
60
+ return {
61
+ name: '@sugarar/theme-plugin-pagefind',
62
+ enforce: 'pre',
63
+ // 添加检索的内容标识
64
+ transform(code: string, id: string) {
65
+ if (id.endsWith('theme-default/Layout.vue')) {
66
+ return code.replace('<VPContent>', '<VPContent data-pagefind-body>')
67
+ }
68
+ return code
69
+ }
70
+ }
71
+ }
72
+
73
+ export function inlineBuildEndPlugin(buildEndFn: any[]) {
74
+ let rewrite = false
75
+ return {
76
+ name: '@sugarar/theme-plugin-build-end',
77
+ enforce: 'pre',
78
+ configResolved(config: any) {
79
+ // 避免重复定义
80
+ if (rewrite) {
81
+ return
82
+ }
83
+ const vitepressConfig: SiteConfig = config.vitepress
84
+ if (!vitepressConfig) {
85
+ return
86
+ }
87
+ rewrite = true
88
+ // 添加 自定义 vitepress build 的钩子
89
+ const selfBuildEnd = vitepressConfig.buildEnd
90
+ vitepressConfig.buildEnd = (siteCfg) => {
91
+ selfBuildEnd?.(siteCfg)
92
+ buildEndFn
93
+ .filter((fn) => typeof fn === 'function')
94
+ .forEach((fn) => fn(siteCfg))
95
+ }
96
+ }
97
+ }
98
+ }