@sugarat/theme 0.1.0

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,159 @@
1
+ import type { ElButton } from 'element-plus'
2
+ import { DefaultTheme } from 'vitepress'
3
+
4
+ export namespace BlogPopover {
5
+ export interface Title {
6
+ type: 'title'
7
+ content: string
8
+ style?: string
9
+ }
10
+
11
+ export interface Text {
12
+ type: 'text'
13
+ content: string
14
+ style?: string
15
+ }
16
+
17
+ export interface Image {
18
+ type: 'image'
19
+ src: string
20
+ style?: string
21
+ }
22
+
23
+ export interface Button {
24
+ type: 'button'
25
+ link: string
26
+ content: string
27
+ style?: string
28
+ props?: InstanceType<typeof ElButton>['$props']
29
+ }
30
+
31
+ export type Value = Title | Text | Image | Button
32
+ }
33
+
34
+ export namespace Theme {
35
+ export interface PageMeta {
36
+ title: string
37
+ date: string
38
+ tag?: string[]
39
+ description?: string
40
+ cover?: string
41
+ sticky?: number
42
+ author?: string
43
+ hidden?: boolean
44
+ layout?: string
45
+ // old
46
+ categories: string[]
47
+ tags: string[]
48
+ }
49
+ export interface PageData {
50
+ route: string
51
+ meta: PageMeta
52
+ }
53
+ export interface activeTag {
54
+ label: string
55
+ type: string
56
+ }
57
+
58
+ export interface GiscusConfig {
59
+ repo: string
60
+ repoId: string
61
+ category: string
62
+ categoryId: string
63
+ mapping?: string
64
+ inputPosition?: 'top' | 'bottom'
65
+ lang?: string
66
+ loading?: 'lazy' | ''
67
+ }
68
+
69
+ export interface HotArticle {
70
+ title?: string
71
+ pageSize?: number
72
+ nextText?: string
73
+ empty?: string | boolean
74
+ }
75
+ export interface RecommendArticle {
76
+ title?: string
77
+ pageSize?: number
78
+ nextText?: string
79
+ empty?: string | boolean
80
+ }
81
+
82
+ export interface HomeBlog {
83
+ name?: string
84
+ motto?: string
85
+ inspiring?: string
86
+ pageSize?: number
87
+ }
88
+
89
+ export interface ArticleConfig {
90
+ readingTime?: boolean
91
+ }
92
+ export interface Alert {
93
+ type: 'success' | 'warning' | 'info' | 'error'
94
+ /**
95
+ * 细粒度的时间控制
96
+ * 默认展示时间,-1 只展示1次,其它数字为每次都展示,一定时间后自动消失,0为不自动消失
97
+ * 配置改变时,会重新触发展示
98
+ */
99
+ duration: number
100
+ title?: string
101
+ description?: string
102
+ closable?: boolean
103
+ center?: boolean
104
+ closeText?: string
105
+ showIcon?: boolean
106
+ html?: string
107
+ }
108
+
109
+ export interface Popover {
110
+ title: string
111
+ /**
112
+ * 细粒度的时间控制
113
+ * 默认展示时间,-1 只展示1次,其它数字为每次都展示,一定时间后自动消失,0为不自动消失
114
+ * 配置改变时,会重新触发展示
115
+ */
116
+ duration: number
117
+ body?: BlogPopover.Value[]
118
+ footer?: BlogPopover.Value[]
119
+ /**
120
+ * 手动重新打开
121
+ */
122
+ reopen?: boolean
123
+ }
124
+ export interface FriendLink {
125
+ nickname: string
126
+ des: string
127
+ url: string
128
+ avatar: string
129
+ }
130
+ export interface BlogConfig {
131
+ pagesData: PageData[]
132
+ srcDir?: string
133
+ author?: string
134
+ hotArticle?: HotArticle
135
+ home?: HomeBlog
136
+ // TODO: 本地全文搜索定制 pagefind || minisearch
137
+ search?: boolean
138
+ /**
139
+ * 配置评论
140
+ * power by https://giscus.app/zh-CN
141
+ */
142
+ comment?: GiscusConfig | false
143
+ recommend?: RecommendArticle
144
+ article?: ArticleConfig
145
+ /**
146
+ * el-alert
147
+ */
148
+ alert?: Alert
149
+ popover?: Popover
150
+ friend?: FriendLink[]
151
+ }
152
+
153
+ export interface Config extends DefaultTheme.Config {
154
+ blog: BlogConfig
155
+ }
156
+ export interface HomeConfig {
157
+ handleChangeSlogan?: (oldSlogan: string) => string | Promise<string>
158
+ }
159
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ // override style
2
+ import './styles/index.scss'
3
+
4
+ // element-ui
5
+ import 'element-plus/dist/index.css'
6
+ import 'element-plus/theme-chalk/dark/css-vars.css'
7
+
8
+ import { Theme } from 'vitepress'
9
+ import DefaultTheme from 'vitepress/theme'
10
+ import BlogApp from './components/BlogApp.vue'
11
+ import { withConfigProvider } from './composables/config/blog'
12
+
13
+ export const BlogTheme: Theme = {
14
+ ...DefaultTheme,
15
+ Layout: withConfigProvider(BlogApp)
16
+ }
17
+
18
+ export * from './composables/config/index'
package/src/node.ts ADDED
@@ -0,0 +1,175 @@
1
+ import glob from 'fast-glob'
2
+ import matter from 'gray-matter'
3
+ import fs from 'fs'
4
+ import { execSync, spawn } from 'child_process'
5
+ import path from 'path'
6
+ import type { UserConfig } from 'vitepress'
7
+ import { formatDate } from './utils/index'
8
+ import type { Theme } from './composables/config/index'
9
+
10
+ export function getThemeConfig(cfg?: Partial<Theme.BlogConfig>) {
11
+ const srcDir = cfg?.srcDir || process.argv.slice(2)?.[1] || '.'
12
+ const files = glob.sync(`${srcDir}/**/*.md`, { ignore: ['node_modules'] })
13
+
14
+ const data = files
15
+ .map((v) => {
16
+ let route = v
17
+ // 处理文件后缀名
18
+ .replace('.md', '')
19
+
20
+ // 去除 srcDir 处理目录名
21
+ if (route.startsWith('./')) {
22
+ route = route.replace(
23
+ new RegExp(`^\\.\\/${path.join(srcDir, '/')}`),
24
+ ''
25
+ )
26
+ } else {
27
+ route = route.replace(new RegExp(`^${path.join(srcDir, '/')}`), '')
28
+ }
29
+
30
+ const fileContent = fs.readFileSync(v, 'utf-8')
31
+
32
+ // TODO: 支持JSON
33
+ const meta: Partial<Theme.PageMeta> = {
34
+ ...matter(fileContent).data
35
+ }
36
+ if (!meta.title) {
37
+ meta.title = getDefaultTitle(fileContent)
38
+ }
39
+ if (!meta.date) {
40
+ // getGitTimestamp(v).then((v) => {
41
+ // meta.date = formatDate(v)
42
+ // })
43
+ meta.date = getFileBirthTime(v)
44
+ } else {
45
+ // TODO: 开放配置,设置时区
46
+ meta.date = formatDate(
47
+ new Date(`${new Date(meta.date).toUTCString()}+8`)
48
+ )
49
+ }
50
+
51
+ // 处理tags和categories,兼容历史文章
52
+ meta.tag = (meta.tag || []).concat([
53
+ ...new Set([...(meta.categories || []), ...(meta.tags || [])])
54
+ ])
55
+
56
+ // 获取摘要信息
57
+ const wordCount = 100
58
+ meta.description =
59
+ meta.description || getTextSummary(fileContent, wordCount)
60
+
61
+ // 获取封面图
62
+ meta.cover =
63
+ meta.cover ||
64
+ fileContent.match(/[!]\[.+?\]\((https:\/\/.+)\)/)?.[1] ||
65
+ ''
66
+ return {
67
+ route: `/${route}`,
68
+ meta
69
+ }
70
+ })
71
+ .filter((v) => v.meta.layout !== 'home')
72
+
73
+ return {
74
+ blog: {
75
+ pagesData: data as Theme.PageData[],
76
+ ...cfg
77
+ },
78
+ sidebar: [
79
+ {
80
+ text: '',
81
+ items: []
82
+ }
83
+ ]
84
+ }
85
+ }
86
+
87
+ export function getDefaultTitle(content: string) {
88
+ const title =
89
+ clearMatterContent(content)
90
+ .split('\n')
91
+ ?.find((str) => {
92
+ return str.startsWith('# ')
93
+ })
94
+ ?.slice(2)
95
+ .replace(/[\s]/g, '') || ''
96
+ return title
97
+ }
98
+
99
+ export function clearMatterContent(content: string) {
100
+ let first___: unknown
101
+ let second___: unknown
102
+
103
+ const lines = content.split('\n').reduce<string[]>((pre, line) => {
104
+ // 移除开头的空白行
105
+ if (!line.trim() && pre.length === 0) {
106
+ return pre
107
+ }
108
+ if (line.trim() === '---') {
109
+ if (first___ === undefined) {
110
+ first___ = pre.length
111
+ } else if (second___ === undefined) {
112
+ second___ = pre.length
113
+ }
114
+ }
115
+ pre.push(line)
116
+ return pre
117
+ }, [])
118
+ return (
119
+ lines
120
+ // 剔除---之间的内容
121
+ .slice((second___ as number) || 0)
122
+ .join('\n')
123
+ )
124
+ }
125
+
126
+ export function getFileBirthTime(url: string) {
127
+ // 参考 vitepress 中的 getGitTimestamp 实现
128
+ const infoStr = execSync(`git log -1 --pretty="%ci" ${url}`)
129
+ .toString('utf-8')
130
+ .trim()
131
+ let date = new Date()
132
+ if (infoStr) {
133
+ date = new Date(infoStr)
134
+ }
135
+ return formatDate(date)
136
+ }
137
+
138
+ export function getGitTimestamp(file: string) {
139
+ return new Promise((resolve, reject) => {
140
+ const child = spawn('git', ['log', '-1', '--pretty="%ci"', file])
141
+ let output = ''
142
+ child.stdout.on('data', (d) => {
143
+ output += String(d)
144
+ })
145
+ child.on('close', () => {
146
+ resolve(+new Date(output))
147
+ })
148
+ child.on('error', reject)
149
+ })
150
+ }
151
+
152
+ function getTextSummary(text: string, count = 100) {
153
+ return (
154
+ clearMatterContent(text)
155
+ .match(/^# ([\s\S]+)/m)?.[1]
156
+ // 除去标题
157
+ ?.replace(/#/g, '')
158
+ // 除去图片
159
+ ?.replace(/!\[.*?\]\(.*?\)/g, '')
160
+ // 除去链接
161
+ ?.replace(/\[(.*?)\]\(.*?\)/g, '$1')
162
+ // 除去加粗
163
+ ?.replace(/\*\*(.*?)\*\*/g, '$1')
164
+ ?.split('\n')
165
+ ?.filter((v) => !!v)
166
+ ?.slice(1)
167
+ ?.join('\n')
168
+ ?.replace(/>(.*)/, '')
169
+ ?.slice(0, count)
170
+ )
171
+ }
172
+
173
+ export function defineConfig(config: UserConfig<Theme.Config>) {
174
+ return config
175
+ }
Binary file
@@ -0,0 +1,95 @@
1
+ html {
2
+ --bg-gradient: 255, 255, 255, 0.1;
3
+ --bg-gradient-home: 255, 255, 255;
4
+ --box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.1);
5
+
6
+ // blog-item
7
+ --box-shadow-hover: 0 2px 16px 0 rgba(0, 0, 0, 0.2);
8
+ --nav-bgc: rgba(255, 255, 255, 0.9);
9
+ --badge-font-color: #4e5969;
10
+ --description-font-color: #86909c;
11
+ }
12
+
13
+ html[class='dark'] {
14
+ --bg-gradient: 20, 20, 20;
15
+ --bg-gradient-home: 20, 20, 20;
16
+ --box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.6);
17
+ --nav-bgc: rgba(0, 0, 0, 0.8);
18
+ --box-shadow-hover: 0 2px 16px 0 rgba(0, 0, 0, 0.7);
19
+ --badge-font-color: #bdc3cc;
20
+ --description-font-color: #9facba;
21
+ }
22
+ .VPHome {
23
+ &::before {
24
+ content: '';
25
+ inset: 0;
26
+ position: fixed;
27
+ top: 0;
28
+ z-index: -1;
29
+ background-image: url(./bg.png);
30
+ background-repeat: repeat;
31
+ min-height: 100%;
32
+ }
33
+ min-height: calc(100vh - var(--vp-nav-height));
34
+ background: radial-gradient(
35
+ ellipse,
36
+ rgba(var(--bg-gradient-home), 1) 0%,
37
+ rgba(var(--bg-gradient-home), 0) 700%
38
+ );
39
+ }
40
+
41
+ @media screen and (max-width: 959px) {
42
+ .VPNav {
43
+ background-color: var(--nav-bgc);
44
+ }
45
+ }
46
+
47
+ .el-pagination {
48
+ flex-wrap: wrap;
49
+ justify-content: center;
50
+ }
51
+ .el-pagination > * {
52
+ margin-bottom: 10px !important;
53
+ }
54
+
55
+ @media screen and (min-width: 768px) and (max-width: 1200px) {
56
+ .VPNavBarMenuGroup .button span.text,
57
+ .VPNavBarMenuLink {
58
+ font-size: 10px;
59
+ }
60
+ .VPNavBar {
61
+ height: auto !important;
62
+ }
63
+ .VPNavBarMenu.menu {
64
+ flex-wrap: wrap;
65
+ }
66
+ }
67
+
68
+ @media screen and (min-width: 960px) and (max-width: 1120px) {
69
+ .VPContent.has-sidebar {
70
+ margin-top: 60px !important;
71
+ }
72
+ }
73
+
74
+ // sidebar
75
+ @media (min-width: 1440px) {
76
+ aside.VPSidebar {
77
+ padding-left: max(
78
+ 32px,
79
+ calc((100% - (var(--vp-layout-max-width) - 16px)) / 2)
80
+ ) !important;
81
+ padding-right: 10px !important;
82
+ }
83
+ }
84
+
85
+ .VPDoc .content main {
86
+ img {
87
+ max-height: 300px;
88
+ margin: 0 auto;
89
+ cursor: pointer;
90
+ }
91
+
92
+ .vp-doc a {
93
+ word-break: break-all;
94
+ }
95
+ }
@@ -0,0 +1,84 @@
1
+ export function formatDate(d: any, fmt = 'yyyy-MM-dd hh:mm:ss') {
2
+ if (!(d instanceof Date)) {
3
+ d = new Date(d)
4
+ }
5
+ const o: any = {
6
+ 'M+': d.getMonth() + 1, // 月份
7
+ 'd+': d.getDate(), // 日
8
+ 'h+': d.getHours(), // 小时
9
+ 'm+': d.getMinutes(), // 分
10
+ 's+': d.getSeconds(), // 秒
11
+ 'q+': Math.floor((d.getMonth() + 3) / 3), // 季度
12
+ S: d.getMilliseconds() // 毫秒
13
+ }
14
+ if (/(y+)/.test(fmt)) {
15
+ fmt = fmt.replace(
16
+ RegExp.$1,
17
+ `${d.getFullYear()}`.substr(4 - RegExp.$1.length)
18
+ )
19
+ }
20
+ // eslint-disable-next-line no-restricted-syntax
21
+ for (const k in o) {
22
+ if (new RegExp(`(${k})`).test(fmt))
23
+ fmt = fmt.replace(
24
+ RegExp.$1,
25
+ RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length)
26
+ )
27
+ }
28
+ return fmt
29
+ }
30
+
31
+ export function isCurrentWeek(date: Date, target?: Date) {
32
+ const now = target || new Date()
33
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
34
+ const oneDay = 1000 * 60 * 60 * 24
35
+ const nowWeek = today.getDay()
36
+ // 本周一的时间
37
+ const startWeek = today.getTime() - (nowWeek === 0 ? 6 : nowWeek - 1) * oneDay
38
+ return +date >= startWeek && +date <= startWeek + 7 * oneDay
39
+ }
40
+
41
+ export function formatShowDate(date: Date | string) {
42
+ const source = +new Date(date)
43
+ const now = +new Date()
44
+ const diff = now - source
45
+ const oneSeconds = 1000
46
+ const oneMinute = oneSeconds * 60
47
+ const oneHour = oneMinute * 60
48
+ const oneDay = oneHour * 24
49
+ const oneWeek = oneDay * 7
50
+ if (diff < oneMinute) {
51
+ return `${Math.floor(diff / oneSeconds)}秒前`
52
+ }
53
+ if (diff < oneHour) {
54
+ return `${Math.floor(diff / oneMinute)}分钟前`
55
+ }
56
+ if (diff < oneDay) {
57
+ return `${Math.floor(diff / oneHour)}小时前`
58
+ }
59
+ if (diff < oneWeek) {
60
+ return `${Math.floor(diff / oneDay)}天前`
61
+ }
62
+
63
+ return formatDate(new Date(date), 'yyyy-MM-dd')
64
+ }
65
+
66
+ const pattern =
67
+ /[a-zA-Z0-9_\u0392-\u03c9\u00c0-\u00ff\u0600-\u06ff\u0400-\u04ff]+|[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g
68
+
69
+ // copy from https://github.com/youngjuning/vscode-juejin-wordcount/blob/main/count-word.ts
70
+ export default function countWord(data: string) {
71
+ const m = data.match(pattern)
72
+ let count = 0
73
+ if (!m) {
74
+ return 0
75
+ }
76
+ for (let i = 0; i < m.length; i += 1) {
77
+ if (m[i].charCodeAt(0) >= 0x4e00) {
78
+ count += m[i].length
79
+ } else {
80
+ count += 1
81
+ }
82
+ }
83
+ return count
84
+ }
@@ -0,0 +1,6 @@
1
+ declare module '*.vue' {
2
+ import { ComponentOptions } from 'vue'
3
+
4
+ const comp: ComponentOptions
5
+ export default comp
6
+ }