@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.
@@ -56,7 +56,7 @@ import {
56
56
  CollectionTag
57
57
  } from '@element-plus/icons-vue'
58
58
  import { useBlogConfig, useCurrentArticle } from '../composables/config/blog'
59
- import countWord, { formatShowDate } from '../utils/index'
59
+ import countWord, { formatShowDate } from '../utils/client'
60
60
  import { Theme } from '../composables/config'
61
61
  import BlogDocCover from './BlogDocCover.vue'
62
62
 
@@ -24,7 +24,7 @@ import { ElAvatar } from 'element-plus'
24
24
  import { useDark } from '@vueuse/core'
25
25
  import { computed } from 'vue'
26
26
  import { useBlogConfig } from '../composables/config/blog'
27
- import { getImageUrl } from '../utils'
27
+ import { getImageUrl } from '../utils/client'
28
28
 
29
29
  const isDark = useDark({
30
30
  storageKey: 'vitepress-theme-appearance'
@@ -114,4 +114,4 @@ const friendList = computed(() => {
114
114
  }
115
115
  }
116
116
  }
117
- </style>
117
+ </style>
@@ -19,7 +19,7 @@
19
19
 
20
20
  <script lang="ts" setup>
21
21
  import { computed } from 'vue'
22
- import { isCurrentWeek } from '../utils'
22
+ import { isCurrentWeek } from '../utils/client'
23
23
  import { useArticles } from '../composables/config/blog'
24
24
 
25
25
  const docs = useArticles()
@@ -44,7 +44,7 @@ import { ref, computed } from 'vue'
44
44
  import { ElButton, ElLink } from 'element-plus'
45
45
  import { withBase } from 'vitepress'
46
46
  import { useArticles, useBlogConfig } from '../composables/config/blog'
47
- import { formatShowDate } from '../utils/index'
47
+ import { formatShowDate } from '../utils/client'
48
48
 
49
49
  const { hotArticle } = useBlogConfig()
50
50
  const title = computed(() => hotArticle?.title || '🔥 精选文章')
@@ -42,7 +42,7 @@
42
42
  import { withBase } from 'vitepress'
43
43
  import { computed } from 'vue'
44
44
  import { useWindowSize } from '@vueuse/core'
45
- import { formatShowDate } from '../utils/index'
45
+ import { formatShowDate } from '../utils/client'
46
46
 
47
47
  const { width } = useWindowSize()
48
48
  const inMobile = computed(() => width.value <= 500)
@@ -50,13 +50,13 @@
50
50
  import { ref, computed } from 'vue'
51
51
  import { useRoute, withBase } from 'vitepress'
52
52
  import { ElButton, ElLink } from 'element-plus'
53
- import { formatShowDate } from '../utils/index'
53
+ import { formatShowDate } from '../utils/client'
54
54
  import { useArticles, useBlogConfig } from '../composables/config/blog'
55
55
 
56
56
  const { recommend: _recommend } = useBlogConfig()
57
57
 
58
58
  const sidebarStyle = computed(() =>
59
- _recommend && _recommend?.style ? _recommend.style : 'card'
59
+ _recommend && _recommend?.style ? _recommend.style : 'sidebar'
60
60
  )
61
61
 
62
62
  const recommendPadding = computed(() =>
@@ -132,7 +132,7 @@ import { computed, nextTick, ref, watch, onBeforeMount, onMounted } from 'vue'
132
132
  import { Command } from 'vue-command-palette'
133
133
  import { useRoute, useRouter, withBase } from 'vitepress'
134
134
  import { useMagicKeys, useWindowSize } from '@vueuse/core'
135
- import { chineseSearchOptimize, formatDate } from '../utils'
135
+ import { chineseSearchOptimize, formatDate } from '../utils/client'
136
136
  import { useArticles, useBlogConfig } from '../composables/config/blog'
137
137
  import { Theme } from '../composables/config'
138
138
  import LogoPagefind from './LogoPagefind.vue'
@@ -198,7 +198,7 @@ import {
198
198
  getGithubUpdateTime,
199
199
  formatDate,
200
200
  getGithubDirUpdateTime
201
- } from '../utils'
201
+ } from '../utils/client'
202
202
  import {
203
203
  useUserWorks,
204
204
  useActiveAnchor,
@@ -1,5 +1,6 @@
1
1
  import type { ElButton } from 'element-plus'
2
2
  import type { DefaultTheme } from 'vitepress'
3
+ import type { FeedOptions } from 'feed'
3
4
 
4
5
  export namespace BlogPopover {
5
6
  export interface Title {
@@ -113,7 +114,7 @@ export namespace Theme {
113
114
  empty?: string | boolean
114
115
  /**
115
116
  * 设置推荐文章的展示风格
116
- * @default 'card'
117
+ * @default 'sidebar'
117
118
  */
118
119
  style?: 'card' | 'sidebar'
119
120
  }
@@ -266,7 +267,7 @@ export namespace Theme {
266
267
  works?: UserWorks
267
268
  /**
268
269
  * https://mermaid.js.org/config/setup/modules/mermaidAPI.html#mermaidapi-configuration-defaults for options
269
- * @default false
270
+ * @default true
270
271
  */
271
272
  mermaid?: any
272
273
  /**
@@ -274,8 +275,29 @@ export namespace Theme {
274
275
  * @default 8 => 'UTC+8'
275
276
  * */
276
277
  timeZone?: number
278
+ /**
279
+ * 启用RSS配置
280
+ */
281
+ RSS?: RSSOptions
277
282
  }
278
283
 
284
+ export type RSSOptions = FeedOptions & {
285
+ baseUrl: string
286
+ /**
287
+ * 线上访问的RSS地址
288
+ */
289
+ url: string
290
+ /**
291
+ * 输出的RSS文件名
292
+ * @default 'feed.rss'
293
+ */
294
+ filename?: string
295
+ /**
296
+ * 是否展示RSS的图标
297
+ * @default true
298
+ */
299
+ showIcon?: boolean
300
+ }
279
301
  export interface Config extends DefaultTheme.Config {
280
302
  blog?: BlogConfig
281
303
  }
package/src/node.ts CHANGED
@@ -1,387 +1,56 @@
1
1
  /* eslint-disable global-require */
2
2
  /* eslint-disable prefer-rest-params */
3
- import glob from 'fast-glob'
4
- import matter from 'gray-matter'
5
- import fs from 'fs'
6
- import { execSync, spawn, spawnSync } from 'child_process'
7
- import path from 'path'
8
- import type { SiteConfig, UserConfig } from 'vitepress'
9
- import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
10
- import { formatDate } from './utils/index'
3
+ import type { UserConfig } from 'vitepress'
11
4
  import type { Theme } from './composables/config/index'
12
-
13
- const checkKeys = ['themeConfig']
14
-
5
+ import {
6
+ getMarkdownPlugins,
7
+ registerMdPlugins,
8
+ wrapperCfgWithMermaid,
9
+ supportRunExtendsPlugin
10
+ } from './utils/node/mdPlugins'
11
+ import { getArticles, patchVPThemeConfig } from './utils/node/theme'
12
+ import { getVitePlugins, registerVitePlugins } from './utils/node/vitePlugins'
13
+
14
+ /**
15
+ * 获取主题的配置
16
+ * @param cfg 主题配置
17
+ */
15
18
  export function getThemeConfig(cfg?: Partial<Theme.BlogConfig>) {
16
- const srcDir = cfg?.srcDir || process.argv.slice(2)?.[1] || '.'
17
- const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'] })
18
-
19
- const data = files
20
- .map((v) => {
21
- let route = v
22
- // 处理文件后缀名
23
- .replace('.md', '')
24
-
25
- // 去除 srcDir 处理目录名
26
- if (route.startsWith('./')) {
27
- route = route.replace(
28
- new RegExp(
29
- `^\\.\\/${path
30
- .join(srcDir, '/')
31
- .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
32
- ),
33
- ''
34
- )
35
- } else {
36
- route = route.replace(
37
- new RegExp(
38
- `^${path
39
- .join(srcDir, '/')
40
- .replace(new RegExp(`\\${path.sep}`, 'g'), '/')}`
41
- ),
42
- ''
43
- )
44
- }
45
-
46
- const fileContent = fs.readFileSync(v, 'utf-8')
47
-
48
- const meta: Partial<Theme.PageMeta> = {
49
- ...matter(fileContent).data
50
- }
51
-
52
- if (!meta.title) {
53
- meta.title = getDefaultTitle(fileContent)
54
- }
55
- if (!meta.date) {
56
- // getGitTimestamp(v).then((v) => {
57
- // meta.date = formatDate(v)
58
- // })
59
- meta.date = getFileBirthTime(v)
60
- } else {
61
- const timeZone = cfg?.timeZone ?? 8
62
- meta.date = formatDate(
63
- new Date(`${new Date(meta.date).toUTCString()}+${timeZone}`)
64
- )
65
- }
66
-
67
- // 处理tags和categories,兼容历史文章
68
- meta.categories =
69
- typeof meta.categories === 'string'
70
- ? [meta.categories]
71
- : meta.categories
72
- meta.tags = typeof meta.tags === 'string' ? [meta.tags] : meta.tags
73
- meta.tag = [meta.tag || []]
74
- .flat()
75
- .concat([
76
- ...new Set([...(meta.categories || []), ...(meta.tags || [])])
77
- ])
78
-
79
- // 获取摘要信息
80
- const wordCount = 100
81
- meta.description =
82
- meta.description || getTextSummary(fileContent, wordCount)
83
-
84
- // 获取封面图
85
- meta.cover =
86
- meta.cover ??
87
- (fileContent.match(/[!]\[.*?\]\((https:\/\/.+)\)/)?.[1] || '')
88
-
89
- // 是否发布 默认发布
90
- if (meta.publish === false) {
91
- meta.hidden = true
92
- meta.recommend = false
93
- }
94
-
95
- return {
96
- route: `/${route}`,
97
- meta
98
- }
99
- })
100
- .filter((v) => v.meta.layout !== 'home')
101
-
102
- const extraConfig: any = {}
19
+ // 文章数据
20
+ const pagesData = getArticles(cfg)
21
+ const extraVPConfig: any = {}
103
22
 
104
- if (
105
- cfg?.search === 'pagefind' ||
106
- (cfg?.search instanceof Object && cfg.search.mode === 'pagefind')
107
- ) {
108
- checkKeys.push('vite')
109
-
110
- let resolveConfig: any
111
- extraConfig.vite = {
112
- plugins: [
113
- {
114
- name: '@sugarar/theme-plugin-pagefind',
115
- enforce: 'pre',
116
- configResolved(config: any) {
117
- if (resolveConfig) {
118
- return
119
- }
120
- resolveConfig = config
121
-
122
- const vitepressConfig: SiteConfig = config.vitepress
123
- if (!vitepressConfig) {
124
- return
125
- }
126
-
127
- // 添加 自定义 vitepress 的钩子
128
- const selfBuildEnd = vitepressConfig.buildEnd
129
- vitepressConfig.buildEnd = (siteConfig: any) => {
130
- // 调用自己的
131
- selfBuildEnd?.(siteConfig)
132
- // 调用pagefind
133
- const ignore: string[] = [
134
- // 侧边栏内容
135
- 'div.aside',
136
- // 标题锚点
137
- 'a.header-anchor'
138
- ]
139
- const { log } = console
140
- log()
141
- log('=== pagefind: https://pagefind.app/ ===')
142
- let command = `npx pagefind --source ${path.join(
143
- process.argv.slice(2)?.[1] || '.',
144
- '.vitepress/dist'
145
- )}`
146
-
147
- if (ignore.length) {
148
- command += ` --exclude-selectors "${ignore.join(', ')}"`
149
- }
150
-
151
- log(command)
152
- log()
153
- execSync(command, {
154
- stdio: 'inherit'
155
- })
156
- }
157
- },
158
- // 添加检索的内容标识
159
- transform(code: string, id: string) {
160
- if (id.endsWith('theme-default/Layout.vue')) {
161
- return code.replace(
162
- '<VPContent>',
163
- '<VPContent data-pagefind-body>'
164
- )
165
- }
166
- return code
167
- }
168
- }
169
- ]
170
- }
171
- }
172
- const markdownPlugin: any[] = []
173
- // tabs支持
174
- if (cfg?.tabs) {
175
- markdownPlugin.push(tabsMarkdownPlugin)
176
- }
177
-
178
- // 流程图支持
179
- if (cfg) {
180
- cfg.mermaid = cfg?.mermaid ?? true
181
- }
182
- if (cfg?.mermaid !== false) {
183
- const { MermaidMarkdown } = require('vitepress-plugin-mermaid')
184
- markdownPlugin.push(MermaidMarkdown)
185
- }
23
+ // 获取要加载的vite插件
24
+ const vitePlugins = getVitePlugins(cfg)
25
+ // 注册Vite插件
26
+ registerVitePlugins(extraVPConfig, vitePlugins)
186
27
 
28
+ // 获取要加载的markdown插件
29
+ const markdownPlugin = getMarkdownPlugins(cfg)
187
30
  // 注册markdown插件
188
- if (markdownPlugin.length) {
189
- extraConfig.markdown = {
190
- config(...rest: any[]) {
191
- markdownPlugin.forEach((plugin) => {
192
- plugin?.(...rest)
193
- })
194
- }
195
- }
196
- }
31
+ registerMdPlugins(extraVPConfig, markdownPlugin)
32
+
197
33
  return {
198
34
  themeConfig: {
199
35
  blog: {
200
- pagesData: data as Theme.PageData[],
36
+ pagesData,
201
37
  ...cfg
202
38
  },
203
- ...(cfg?.blog !== false && cfg?.recommend !== false
204
- ? {
205
- sidebar: [
206
- {
207
- text: '',
208
- items: []
209
- }
210
- ]
211
- }
212
- : undefined)
39
+ // 补充一些额外的配置用于继承
40
+ ...patchVPThemeConfig(cfg)
213
41
  },
214
- ...extraConfig
42
+ ...extraVPConfig
215
43
  }
216
44
  }
217
45
 
218
- export function getDefaultTitle(content: string) {
219
- const title =
220
- clearMatterContent(content)
221
- .split('\n')
222
- ?.find((str) => {
223
- return str.startsWith('# ')
224
- })
225
- ?.slice(2)
226
- .replace(/^\s+|\s+$/g, '') || ''
227
- return title
228
- }
229
-
230
- export function clearMatterContent(content: string) {
231
- let first___: unknown
232
- let second___: unknown
233
-
234
- const lines = content.split('\n').reduce<string[]>((pre, line) => {
235
- // 移除开头的空白行
236
- if (!line.trim() && pre.length === 0) {
237
- return pre
238
- }
239
- if (line.trim() === '---') {
240
- if (first___ === undefined) {
241
- first___ = pre.length
242
- } else if (second___ === undefined) {
243
- second___ = pre.length
244
- }
245
- }
246
- pre.push(line)
247
- return pre
248
- }, [])
249
- return (
250
- lines
251
- // 剔除---之间的内容
252
- .slice((second___ as number) || 0)
253
- .join('\n')
254
- )
255
- }
256
-
257
- export function getFileBirthTime(url: string) {
258
- let date = new Date()
259
-
260
- try {
261
- // 参考 vitepress 中的 getGitTimestamp 实现
262
- const infoStr = spawnSync('git', ['log', '-1', '--pretty="%ci"', url])
263
- .stdout?.toString()
264
- .replace(/["']/g, '')
265
- .trim()
266
- if (infoStr) {
267
- date = new Date(infoStr)
268
- }
269
- } catch (error) {
270
- return formatDate(date)
271
- }
272
-
273
- return formatDate(date)
274
- }
275
-
276
- export function getGitTimestamp(file: string) {
277
- return new Promise((resolve, reject) => {
278
- const child = spawn('git', ['log', '-1', '--pretty="%ci"', file])
279
- let output = ''
280
- child.stdout.on('data', (d) => {
281
- output += String(d)
282
- })
283
- child.on('close', () => {
284
- resolve(+new Date(output))
285
- })
286
- child.on('error', reject)
287
- })
288
- }
289
-
290
- function getTextSummary(text: string, count = 100) {
291
- return (
292
- clearMatterContent(text)
293
- .match(/^# ([\s\S]+)/m)?.[1]
294
- // 除去标题
295
- ?.replace(/#/g, '')
296
- // 除去图片
297
- ?.replace(/!\[.*?\]\(.*?\)/g, '')
298
- // 除去链接
299
- ?.replace(/\[(.*?)\]\(.*?\)/g, '$1')
300
- // 除去加粗
301
- ?.replace(/\*\*(.*?)\*\*/g, '$1')
302
- ?.split('\n')
303
- ?.filter((v) => !!v)
304
- ?.slice(1)
305
- ?.join('\n')
306
- ?.replace(/>(.*)/, '')
307
- ?.slice(0, count)
308
- )
309
- }
310
-
311
- export function assignMermaid(config: any) {
312
- if (!config?.mermaid) return
313
-
314
- if (!config.vite) config.vite = {}
315
- if (!config.vite.plugins) config.vite.plugins = []
316
- const { MermaidPlugin } = require('vitepress-plugin-mermaid')
317
- config.vite.plugins.push(MermaidPlugin(config.mermaid))
318
- if (!config.vite.resolve) config.vite.resolve = {}
319
- if (!config.vite.resolve.alias) config.vite.resolve.alias = {}
320
-
321
- config.vite.resolve.alias = [
322
- ...aliasObjectToArray({
323
- ...config.vite.resolve.alias,
324
- 'cytoscape/dist/cytoscape.umd.js': 'cytoscape/dist/cytoscape.esm.js',
325
- mermaid: 'mermaid/dist/mermaid.esm.mjs'
326
- }),
327
- { find: /^dayjs\/(.*).js/, replacement: 'dayjs/esm/$1' }
328
- ]
329
- }
330
- function aliasObjectToArray(obj: Record<string, string>) {
331
- return Object.entries(obj).map(([find, replacement]) => ({
332
- find,
333
- replacement
334
- }))
335
- }
46
+ /**
47
+ * defineConfig Helper
48
+ */
336
49
  export function defineConfig(config: UserConfig<Theme.Config>): any {
337
- // 兼容低版本主题配置
338
- // @ts-ignore
339
- if (config.themeConfig?.themeConfig) {
340
- config.extends = checkKeys.reduce((pre, key) => {
341
- // @ts-ignore
342
- pre[key] = config.themeConfig[key]
343
- // @ts-ignore
344
- delete config.themeConfig[key]
345
- return pre
346
- }, {})
347
-
348
- // 打印warn信息
349
- setTimeout(() => {
350
- console.warn('==↓ 主题配置方式过期,请尽快参照文档更新 ↓==')
351
- console.warn('https://theme.sugarat.top/config/global.html')
352
- }, 1200)
353
- }
354
- // @ts-ignore
355
- const extendThemeConfig = (config.extends?.themeConfig?.blog ||
356
- {}) as Theme.BlogConfig
357
-
358
- // 开关支持Mermaid
359
- const resultConfig =
360
- extendThemeConfig.mermaid === false
361
- ? config
362
- : {
363
- ...config,
364
- mermaid:
365
- extendThemeConfig.mermaid === true ? {} : extendThemeConfig.mermaid
366
- }
367
- assignMermaid(resultConfig)
368
-
369
- // 处理markdown插件
370
- if (!resultConfig.markdown) resultConfig.markdown = {}
371
- // @ts-ignore
372
- if (config.extends?.markdown?.config) {
373
- const markdownExtendsConfigOriginal =
374
- // @ts-ignore
375
- config.extends?.markdown?.config
376
- const selfMarkdownConfig = resultConfig.markdown?.config
377
-
378
- resultConfig.markdown.config = (...rest: any[]) => {
379
- // @ts-ignore
380
- selfMarkdownConfig?.(...rest)
381
- markdownExtendsConfigOriginal?.(...rest)
382
- }
383
- }
50
+ const resultConfig = wrapperCfgWithMermaid(config)
51
+ supportRunExtendsPlugin(resultConfig)
384
52
  return resultConfig
385
53
  }
386
54
 
55
+ // 重新导包 tabsMarkdownPlugin 导出CJS格式支持
387
56
  export { tabsMarkdownPlugin } from 'vitepress-plugin-tabs'
@@ -65,7 +65,7 @@ html[class='dark'] {
65
65
  @media screen and (min-width: 768px) and (max-width: 1200px) {
66
66
  .VPNavBarMenuGroup .button span.text,
67
67
  .VPNavBarMenuLink {
68
- font-size: 10px;
68
+ font-size: 12px !important;
69
69
  }
70
70
  .VPNavBar {
71
71
  height: auto !important;
@@ -1,4 +1,4 @@
1
- import type { ThemeableImage } from '../composables/config'
1
+ import type { ThemeableImage } from '../../composables/config'
2
2
 
3
3
  export function formatDate(d: any, fmt = 'yyyy-MM-dd hh:mm:ss') {
4
4
  if (!(d instanceof Date)) {
@@ -0,0 +1,75 @@
1
+ /* eslint-disable no-console */
2
+ import path from 'path'
3
+ import fs, { writeFileSync } from 'fs'
4
+ import { Feed } from 'feed'
5
+ import type { SiteConfig } from 'vitepress'
6
+ import type { Theme } from '../../composables/config/index'
7
+ import { withBase } from './index'
8
+ import { pageMap } from './theme'
9
+
10
+ export async function genFeed(config: SiteConfig) {
11
+ const blogCfg: Theme.BlogConfig = config.userConfig.themeConfig.blog
12
+ const posts: Theme.PageData[] = blogCfg.pagesData
13
+ const { RSS, authorList = [] } = blogCfg
14
+ if (!RSS) return
15
+ const { createMarkdownRenderer } = await import('vitepress')
16
+
17
+ const mdRender = await createMarkdownRenderer(
18
+ config.srcDir,
19
+ config.markdown,
20
+ config.site.base,
21
+ config.logger
22
+ )
23
+ console.log()
24
+ console.log('=== feed: https://github.com/jpmonette/feed ===')
25
+ const { base } = config.userConfig
26
+
27
+ const { baseUrl, filename } = RSS
28
+ const feed = new Feed(RSS)
29
+
30
+ posts.sort(
31
+ (a, b) =>
32
+ +new Date(b.meta.date as string) - +new Date(a.meta.date as string)
33
+ )
34
+
35
+ for (const { route, meta } of posts) {
36
+ const { title, description, date, hidden } = meta
37
+ if (hidden) continue
38
+ const author = meta.author ?? blogCfg.author
39
+ let link = `${baseUrl}${withBase(
40
+ base || '',
41
+ // 移除末尾的index
42
+ route.replace(/(^|\/)index$/, '$1')
43
+ )}`
44
+ // 补全后缀
45
+ link = link.endsWith('/')
46
+ ? link
47
+ : `${link}${config?.cleanUrls ? '' : '.html'}`
48
+ const authorLink = authorList.find((v) => v.nickname === author)?.url
49
+ let html
50
+ const filepath = pageMap.get(route)
51
+ if (filepath) {
52
+ const fileContent = fs.readFileSync(filepath, 'utf-8')
53
+ html = mdRender.render(fileContent)
54
+ }
55
+
56
+ feed.addItem({
57
+ title,
58
+ id: link,
59
+ link,
60
+ description,
61
+ content: html,
62
+ author: [
63
+ {
64
+ name: author,
65
+ link: authorLink
66
+ }
67
+ ],
68
+ date: new Date(date)
69
+ })
70
+ }
71
+ const RSSFile = path.join(config.outDir, filename || 'feed.rss')
72
+ writeFileSync(RSSFile, feed.rss2())
73
+ console.log('🎉 RSS generated', filename || 'feed.rss')
74
+ console.log()
75
+ }