initial commit

This commit is contained in:
pengzhanbo 2022-03-13 06:14:05 +08:00
commit 5c79656269
96 changed files with 7299 additions and 0 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

7
.eslintignore Normal file
View File

@ -0,0 +1,7 @@
node_modules/
.temp/
lib/
dist/
!.vuepress/
!.*.js
example/

68
.eslintrc.js Normal file
View File

@ -0,0 +1,68 @@
module.exports = {
root: true,
extends: 'vuepress',
globals: {
__VUEPRESS_VERSION__: 'readonly',
__VUEPRESS_DEV__: 'readonly',
__VUEPRESS_SSR__: 'readonly',
__VUE_HMR_RUNTIME__: 'readonly',
__VUE_OPTIONS_API__: 'readonly',
__VUE_PROD_DEVTOOLS__: 'readonly',
},
overrides: [
{
files: ['*.ts', '*.vue'],
extends: 'vuepress-typescript',
parserOptions: {
project: ['tsconfig.json'],
},
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-var-requires': 'off',
'vue/component-tags-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/multi-word-component-names': 'off',
},
},
{
files: ['*.vue'],
globals: {
defineEmits: 'readonly',
defineProps: 'readonly',
},
rules: {
// disable for setup script
'@typescript-eslint/no-unused-vars': 'off',
},
},
{
files: ['clientAppEnhance.ts'],
rules: {
'vue/match-component-file-name': 'off',
},
},
{
files: ['**/__tests__/**/*.ts'],
env: {
jest: true,
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'vue/one-component-per-file': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['docs/**'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
],
}

10
.gitattributes vendored Normal file
View File

@ -0,0 +1,10 @@
* text eol=lf
*.txt text eol=crlf
*.png binary
*.jpg binary
*.jpeg binary
*.ico binary
*.tff binary
*.woff binary
*.woff2 binary

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# VuePress files
example/.vuepress/.temp/
example/.vuepress/.cache/
example/.vuepress/dist/
# Dist files
lib/
# Test temp files
**/__fixtures__/.temp/
# Test coverage files
coverage/
# Node modules
node_modules/
# MacOS Desktop Services Store
.DS_Store
# Log files
*.log
# Typescript build info
*.tsbuildinfo

8
.npmignore Normal file
View File

@ -0,0 +1,8 @@
/example
/node_modules
/src
.editorconfig
.eslint*
.git*
**/*.tsbuildinfo
vuepress.config.js

View File

@ -0,0 +1,14 @@
---
title: 组件
createTime: 2022/03/06 09:41:52
permalink: /post/yiobrmp0
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- react
- 组件
---
# React 组件4

View File

@ -0,0 +1,14 @@
---
title: 组件
createTime: 2022/03/06 09:41:26
permalink: /post/zrd6axrp
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tag:
- Vue
---
# 组件
组件内容 组件 组件

View File

@ -0,0 +1,265 @@
---
title: 正则表达式
createTime: 2022/03/08 06:32:01
permalink: /post/tjya08e9
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- javascript
---
本文正则表达式基于`javascript`,不同的计算机语言对正则表达式的支持情况以及实现,语法不尽相同,不一定适用于其他语言。
<!-- more -->
### 简介
`正则表达式`是一种文本模式Regular Expression是对字符串的一种匹配查找规则。可以方便的在某一文本字符串中查找、定位、替换符合某种规则的字符串。
比如说,我们想要找出一段文本中的手机号码,文本内容如下:
``` html
name:Mark tel:13800138000
name:Jhon tel:13800138888
```
很明显,在这段文本中,手机号码是以 `tel:`开头,这符合一定的规则,这样我们可以通过正则表达式来书写这个规则,然后去查找匹配:
``` js
var text = `name:Mark tel:13800138000
name:Jhon tel:13800138888`;
var result = text.match(/tel:(1\d{10})/);
// ["tel:13800138000", "13800138000", index: 0, input: "tel:13800138000", groups: undefined]
var tel = result[1];
// 13800138000
```
`/tel:(1\d{10})/` 便是所说的正则表达式。
### `RegExp`与字面量
`javascript`中,我们可以使用构造函数`RegExp` 创建正则表达式。
`new RegExp(pattern[, flags])`
``` js
var regExp = new RegExp('\\d', 'g');
```
也可以通过 字面量的方式:
``` js
var regExp = /\d/g;
```
两种创建正则表达式适用的场景有些细微的不同,一般使用`new RegExp()`来创建动态的正则表达式,使用字面量创建静态的正则表达式。
正则表达式字面量是提供了对正则表达式的编译,当正则表达式保持不变时,用字面量的方式创建正则表达式可以获得更好的性能。
_以下讨论以正则表达式字面量来创建正则表达式_
`正则表达式`一般由`元字符`和普通字符组成。
### 元字符
元字符也叫特殊字符,是正则表达式规定的,对符合特定的单一的规则的字符的描述。
| 字符 | 含义 |
|:----:|-----|
| \\ |在非特殊字符的前面加反斜杠,表示这个字符是特殊的,不能从字面上解释。比如在`\d`描述的不是一个普通的字符`d`,而是正则表达式中的数值`0-9`<br/>如果在特殊字符前面加反斜杠,这表示将这个字符转义为普通字符,比如`?`在正则中有其特殊含义,前面加反斜杠\?,这可以将其转为普通的`?`。|
|^|匹配文本开始的位置,如果开启了多行标志,也会匹配换行符后紧跟的位置。<br/>比如`^a`会匹配`abc`,但不会匹配到`bac`。|
|$|匹配文本结束的位置,如果开启了多行标志,也会匹配换行符前紧跟的位置。<br/>比如`b$`会匹配`acb`,但不会匹配到`abc`。|
|*|匹配前一个表达式0次到多次。<br/>比如,`ab*`会匹配到`abbbbbbc`中的`abbbbbb`,以及`acbbbbb`中的`a`。|
|+|匹配前一个表达式1次到多次。<br/>比如,`ab+`会匹配到`abbbbbbc`中的`abbbbbb`,但不会匹配`acbbbbb`。|
|?|匹配前一个表达式0次到1次。<br/>比如,`ab*`会匹配到`abbbbbbc`中的`ab`,以及`acbbbbb`中的`a`。|
|.| 匹配除换行符之外的任何单个字符。|
|x\|y| 匹配 x或者y。|
|\[xyz\]| 表示一个字符的集合。匹配集合中的任意一个字符。可以使用破折号`-`来指定一个字符范围。<br/>比如,`[0-4]``[01234]`,都可以匹配`4567`中的`4`。|
|\[^xyz\]| 表示一个方向字符集合。匹配任意一个不包括在集合中的字符。可以使用破折号`-`来指定一个字符范围。<br/>比如,`[0-4]``[01234]`,都可以匹配`2345`中的`5`。|
|{n}|n为一个整数表示匹配前一个匹配项n次。<br/>比如`a{2}`不会匹配`abc`中的`a`,但会匹配`aaaabc`中的`aa`。|
|{m,n}| m,n都是一个整数匹配前一个匹配项至少发生了m次最多发生了n次。<br/>当mn值为0时这个值被忽略当n值不写`{1,}`表示1次到多次。当m值不写时`{,1}`表示0次到1次。|
|(x)|匹配`x`并且捕获该匹配项。称为捕获括号,括号中的匹配项也称作子表达式。|
|(?:x)| 匹配`x`但不捕获该匹配项。称为非捕获括号。|
|x(?=y)| 匹配`x`且当`x`后面跟着`y`。称为正向肯定查找(正向前瞻)。|
|x(?!y)| 匹配`x`且当`x`后面不跟着`y`。称为正向否定查找(负向前瞻)。|
|[\b]|匹配一个退格(U+0008)。|
|\b |匹配一个词的边界。匹配的值的边界并不包含在匹配的内容中。|
|\B|匹配一个非单词的边界。|
|\d|匹配一个数字。等价于`[0-9]`。|
|\D|匹配一个非数字。等价于`[^0-9]`。|
|\n|匹配一个换行符 (U+000A)。|
|\r|匹配一个回车符 (U+000D)。|
|\s|匹配一个空白字符,包括空格、制表符、换页符和换行符。<br/>等价于`[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]`|
|\S|匹配一个非空白字符。|
|\t|匹配一个水平制表符 (U+0009)。|
|\w|匹配一个单字字符(字母、数字或者下划线)。<br/> 等价于`[A-Za-z0-9_]`。|
|\W|匹配一个非单字字符。|
|\xhh|与代码 hh 匹配字符(两个十六进制数字)|
|\uhhhh|与代码 hhhh 匹配字符(四个十六进制数字)。|
上表在多数文章都会提及,但有一些注意的细节,下面我单独拎出来说说。
1. __`[xyz]` 匹配集合中的任意一个字符__ <br/>
这个字符集的元素,可以是普通字符,也可以是特殊字符,也可以用破折号`-`规定一个字符集范围。<br/>
以匹配数字为例,可以写成`[0123456789]` ,也可以写成`[\d]`,也可以写成`[0-9]`<br/>
类似于`()`等特殊字符,在`[]`中有其作用,都特殊字符的作用一致,不能直接当做普通字符来使用,所以我们需要使用反斜杠`\`将其转义为普通字符,值得注意的是,上表的特殊字符中,星号`*`、小数点`.``[]`中并没有特殊用途,所以不需要做转义处理,当然,即使做了转义,也不会出现问题;而破折号`-``[]`中有其特殊作用,所以作为普通字符使用时,需要转义。
2. __`?`匹配前一个表达式0次到1次。__<br/>
其实这里准确描述来说,匹配前一个表达式,且该表达式 __非任何量词 `*`、 `+`、`?` 或 `{}`__ 匹配前一个表达式0次到1次。<br/>
如果紧跟在 __非任何量词 `*`、 `+`、`?` 或 `{}`__ 的后面,将会使量词变为非贪婪的(匹配尽量少的字符)<br/>
_贪婪与非贪婪匹配我们在下文细说。_
### 等价字符
正则表达式中,有不少特殊字符的写法,是等价的,也可以说是简写形式,下表的左右两边,都是等价的。
|regExp|regExp|
|--|--|
|*|{0,}|
|+|{1,}|
|?|{0,1}|
|\d|\[0-9\]|
|\D|\[^0-9\]|
|\w|\[a-zA-Z_\]|
|\W|\[^a-zA-Z_\]|
|\s|\[ \f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\]|
### 贪婪模式与非贪婪
__什么是贪婪模式__
贪婪是指正则表达式匹配时,是贪心的,会尽可能的匹配多的字符,主要体现在`量词特殊字符`
``` js
// 匹配一个到多个数字
var r = /\d+/;
var t1 = '12a';
var t2 = '1234a';
var t3 = 'a12b345';
console.log(t1.match(r)[0]); // 12
console.log(t2.match(r)[0]); // 1234
console.log(t3.match(r)[0]); // 12
```
`非贪婪`,即是让正则表达式匹配尽量少的字符。那么如何改变正则表达式的贪婪模式?
__在量词特殊字符后面紧跟使用`?`__
我们说说的量词包括`*`, `+`, `?`, `{m,n}`。那么紧跟了`?`,会有什么不同的表现呢?
我们从例子来分析:
``` js
var r1 = /<div>.*<\/div>/;
var r2 = /<div>.*?<\/div>/;
var str = '<div>aaa</div>bbb<div></div>ccc';
```
变量`r1`是贪婪匹配,得到的结果会是什么呢?
``` js
console.log(str.match(r1)[0]);
// <div>aaa</div>bbb<div></div>
```
在这段字符串中,有两个`</div>`的匹配字符串,正则表达式在遇到第一个`</div>`匹配字符项时,同时满足了`/.*/``/<\/div>/`的匹配条件,优先作为`/.*/`的匹配值,在遇到第二个时,同样还是优先作为`/.*/`的匹配值,直到匹配的字符串`str`的结束,没有满足条件的匹配字符串,再把第二个`</div>`作为`/<\/div>/`的匹配值。最终得到了`<div>aaa</div>bbb<div></div>`的匹配结果。
变量`r2`这是非贪婪匹配,得到的结果又会有所不同:
``` js
console.log(str.match(r1)[0]);
// <div>aaa</div>
```
同样,两个`</div>`的匹配字符串,但实际非贪婪匹配模式,在匹配到第一个`</div>`,就不会再继续向下匹配字符串了。
__也就是说贪婪匹配是在满足规则下尽可能多的匹配更多的字符串直到字符串结束或没有满足规则的字符串了非贪婪匹配是在满足规则下尽可能少的匹配最少的字符串一旦得到满足规则的字符串就不再向下匹配。__
1. `x*?`:尽可能少的匹配`x`匹配的结果可以是0个`x`
2. `x+?`:尽可能少的匹配`x`但匹配的结果至少有1个`x`
3. `x??`:尽可能少的匹配`x`, 匹配的结果可以是0个`x`,但最多可以有一个`x`
4. `x{m,n}?`:尽可能少的匹配`x`但匹配的结果至少有m个`x`最多可以有n个`x`
可能从字面来说,不好理解 `x??`, `x{m,n}?` ,来看一个例子就可以明白了:
``` js
var s1 = '<div>aa</div>';
var s2 = '<div>a</div>';
var s3 = '<div></div>';
var r1 = /<div>a??<\/div>/;
console.log(r1.test(s1)); // false
console.log(r1.test(s2)); // true
console.log(r1.test(s3)); // true
```
``` js
var s1 = '<div>aaa</div>';
var s2 = '<div>aa</div>';
var s3 = '<div>aaaa</div>'
var r1 = /<div>a{2,3}?<\/div>/;
var r2 = /<div>a{2,3}?/;
console.log(r1.test(s1)); // true
console.log(r1.test(s2)); // true
console.log(r1.test(s3)); // false
console.log(s1.match(r2)[0]); // <div>aa
console.log(s2.match(r2)[0]); // <div>aa
console.log(s3.match(r2)[0]); // <div>aa
```
### 正则表达式标志
|标志|描述|
|:--:| -- |
|g|全局搜索|
|i|不区分大小写搜索|
|m|多行搜索|
|y|执行“粘性”搜索,匹配从目标字符串的当前位置开始|
|u|Unicode模式。用来正确处理大于 \uFFFF 的Unicode字符|
__`m`__
使用`m`标志时,会改变开始(`^`)和结束字符(`$`)的工作模式,变为在多行上匹配,分别匹配每一行的开始和结束,即`\n``\r` 分割。
__`y`__
使用`y`标志时,匹配是从`RegExp.lastIndex`指定的位置开始匹配,匹配为真时,会修改`lastIndex`的值到当前匹配字符串后的位置,下次匹配从这个位置开始匹配,如果匹配为假时,不会修改`lastIndex`的值。
``` js
let reg = /o/y;
let str= 'foo';
// lastIndex 为 0从字符 f 开始匹配
reg.test(str); // false
// 由于结果为 false lastIndex 还是为 0
reg.test(str); // false
let str2 = 'oof';
// lastIndex 为 0 ,从字符 o 开始匹配
reg.test(str2); // true
// lastIndex 此时修改为 1, 从第二个 o 开始匹配
reg.test(str2); // true
// lastIndex 此时修改为 2
reg.test(str2); // false 此时开始匹配的字符是 f
// lastIndex没有被修改
reg.test(str2); // false
```
### 正则表达式中的捕获—— \1,\2,\3... 以及 $1,$2,$3...
在上文中我们介绍了 `(x)` 是匹配 `x` 并捕获,那么有了捕获就必然可以去使用捕获到的结果, `\1,\2,\3...` 以及` $1,$2,$3...` 便是指捕获的结果。
`\1, \2, \3, \4, \5, \6, \7, \8, \9` 在正则表达式中使用,捕获结果为正则表达式的源模式.
在这个正则表达式中`(bc)`被捕获并标记为`\1`, `(ef)`被捕获并标记为`\2`
``` js
let reg = /a(bc)d(ef)/
```
也可以使用来简化正则表达式
``` js
let reg = /a(bc)dbc/
let reg2 = /a(bc)d\1/
let str = 'abcdbc';
reg.test(str); // true
reg2.test(str); // true
```

`$1, $2, $3, $4, $5, $6, $7, $8, $9``RegExp`的包含括号子表达式的正则表达式静态的只读属性。
``` js
let reg = /a(bc)d/;
let str = 'abcd';
reg.test(str);
console.log(RegExp.$1); // bc
```
`String.replace()` 中使用:
``` js
let reg = /(\w+)\s(\w+)/;
let str = 'apple pear';
str.replace(reg, '$2 $1'); // pear apple
RegExp.$1; // apple
RegExp.$2; // pear
```

View File

@ -0,0 +1,12 @@
---
title: typescript学习
createTime: 2022/03/06 09:47:02
permalink: /post/upiqrep3
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- typescript
---
# typescript 学习

View File

@ -0,0 +1,10 @@
---
title: 呵呵
createTime: 2022/03/07 05:16:12
permalink: /post/zhervgal
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- notes
---

View File

@ -0,0 +1,12 @@
---
title: 哈哈
createTime: 2022/03/07 05:14:23
permalink: /post/0ts9cam6
author: pengzhanbo
top: false
type: # original: 原创: reprint 转载 可为空不填
tags:
- Vue
- notes
---

View File

@ -0,0 +1,17 @@
---
title: 电影杂谈
author: pengzhanbo
tags:
- 杂谈
- 电影
permalink: /post/zozx35d9
createTime: 2022/03/06 09:47:13
top: false
type: # original: 原创: reprint 转载 可为空不填
---
呵呵呵呵
<!-- more -->
111

3
example/README.md Normal file
View File

@ -0,0 +1,3 @@
---
home: true
---

BIN
example/public/avatar.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

82
package.json Normal file
View File

@ -0,0 +1,82 @@
{
"name": "@pengzhanbo/vuepress-theme-plume",
"version": "1.0.0-beta.3",
"description": "blog theme by vuepress2.x",
"main": "lib/node/index.js",
"types": "lib/node/index.d.ts",
"files": [
"lib",
"templates",
"tailwind.config.js"
],
"keywords": [
"vuepress-next",
"vuepress",
"vuepress-theme"
],
"author": "pengzhanbo",
"license": "MIT",
"scripts": {
"dev-watch": "yarn clean && concurrently \"yarn copy-watch\" \"yarn tsc-watch\"",
"dev": "vuepress dev example --debug",
"tsc": "tsc -b tsconfig.build.json",
"tsc-watch": "tsc -b tsconfig.dev.json -w",
"clean": "rimraf lib *.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,scss,css,html}\" lib",
"copy-watch": "cpx \"src/**/*.{d.ts,vue,scss,css,html}\" lib -w",
"build": "yarn clean & yarn tsc & yarn copy"
},
"prettier": "prettier-config-vuepress",
"dependencies": {
"@tailwindcss/typography": "^0.5.2",
"@types/webpack-env": "^1.16.3",
"@vuepress/client": "^2.0.0-beta.35",
"@vuepress/core": "^2.0.0-beta.36",
"@vuepress/plugin-active-header-links": "^2.0.0-beta.36",
"@vuepress/plugin-back-to-top": "^2.0.0-beta.36",
"@vuepress/plugin-container": "^2.0.0-beta.36",
"@vuepress/plugin-git": "^2.0.0-beta.36",
"@vuepress/plugin-medium-zoom": "^2.0.0-beta.36",
"@vuepress/plugin-nprogress": "^2.0.0-beta.36",
"@vuepress/plugin-palette": "^2.0.0-beta.36",
"@vuepress/plugin-prismjs": "^2.0.0-beta.36",
"@vuepress/plugin-theme-data": "^2.0.0-beta.36",
"@vuepress/shared": "^2.0.0-beta.35",
"@vuepress/theme-default": "^2.0.0-beta.36",
"@vuepress/utils": "^2.0.0-beta.35",
"@vueuse/core": "^7.7.1",
"autoprefixer": "^10.4.2",
"chokidar": "^3.5.3",
"concurrently": "^7.0.0",
"cpx2": "^4.2.0",
"dayjs": "^1.10.8",
"eslint": "^8.10.0",
"eslint-config-vuepress": "^3.5.0",
"eslint-config-vuepress-typescript": "^2.5.0",
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0",
"nanoid": "^3.3.1",
"postcss": "^8.4.8",
"postcss-at-rules-variables": "^0.3.0",
"postcss-each": "^1.1.0",
"postcss-import": "^14.0.2",
"postcss-nested": "^5.0.6",
"postcss-preset-env": "^7.4.2",
"postcss-simple-vars": "^6.0.3",
"prettier": "^2.5.1",
"prettier-config-vuepress": "^1.3.0",
"rimraf": "^3.0.2",
"sass": "^1.49.9",
"tailwindcss": "^3.0.23",
"typescript": "^4.6.2",
"vue": "^3.2.31",
"vue-router": "^4.0.13",
"vuepress": "^2.0.0-beta.36"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"vuepress": ">=2.0.0-beta.36"
}
}

81
readme.md Normal file
View File

@ -0,0 +1,81 @@
# vuepress-theme-plume
一款基于 [vuepress@next](https://github.com/vuepress/vuepress-next) 制作的简单的博客皮肤。
## 安装
``` sh
npm i @pengzhanbo/vuepress-theme-plume
# or yarn
yarn add @pengzhanbo/vuepress-theme-plume
```
## 特性
- 低配置化仅需少量的vuepress配置即可使用
- 通过目录约定生成栏目
## 配置
``` js
// vuepress.config.{js, ts}
// or {sourceDir}/.vuepress/config.{js, ts}
export default {
// ...
theme: path.resolve(__dirname, './lib/node/index.js'),
themeConfig: {
// 首页头部大图
bannerImg: '/big-banner.jpg',
// 博主头像
avatarUrl: '/avatar.gif',
// 博主名称
avatar: '未闻花名',
github: 'https://github.com/',
email: '_@outlook.com',
description: '学习,生活,娱乐,我全都要',
},
}
```
## 编写文章
在你的文档目录下,首先创建一个 `README.md` 文件
```
---
home: true
---
```
然后,创建其他的目录,格式如下
``` sh
\d+\.[^]+
```
比如: `1.前端` `2.web` 的形式
主题会解析目录名称,分解为`{ type, name }` 的形式,并根据 type作为排序的依据。
再在目录中创建并编写文章即可。
建议首先启动 vuepress 本地开发环境后,再创建新文章
首次创建,将会在文档头部自动生成文档配置。
``` md
---
title: 组件
createTime: 2022/03/06 09:41:52
permalink: /post/yiobrmp0
author: pengzhanbo
tags:
- react
- 组件
---
```
- `title` 根据文档名称生成
- `createTime` 根据文件创建时间生成
- `author` 根据 package.json 中的字段生成
- `permalink` 作为文章的永久链接,通过 nanoid 生成唯一链接,可自定义
- `tags` 文章标签,自定义
## 说明
- 由于 vuepress@next 目前尚在 beta开发阶段随时可能发生变化本主题可能在未来的版本中无法正常使用。
- 在未来将会持续关注 vuepress@next 开发进度,持续更新维护本主题
- 本主题尚处于开发阶段目前仅实现了基础的blog功能未来会继续添加更多的功能
- 欢迎各位尝试使用 vuepress@next 和 本主题

View File

@ -0,0 +1,7 @@
import { defineClientAppEnhance } from '@vuepress/client'
import './styles/tailwind.css'
import './styles/index.css'
export default defineClientAppEnhance(({ app, router, siteData }) => {
// do
})

View File

@ -0,0 +1,6 @@
import { defineClientAppSetup } from '@vuepress/client'
// import { setupDarkMode } from './composables'
import './styles/index.css'
export default defineClientAppSetup(() => {
// setupDarkMode()
})

View File

@ -0,0 +1,32 @@
<script lang="ts" setup>
import * as dayjs from 'dayjs'
import { useArchivesList } from '../composables/useArchives'
const archivesList = useArchivesList()
const format = (date: string | number | undefined): string => {
return dayjs(date).format('MM-DD')
}
</script>
<template>
<div class="archives-list-wrapper">
<h2>归档</h2>
<div
v-for="archives in archivesList"
:key="archives.label"
class="archives-list-container"
>
<h3>{{ archives.label }}</h3>
<ul>
<li
v-for="post in archives.postList"
:key="post.path"
class="archives-item"
>
<span>[{{ format(post.frontmatter.createTime) }}]</span>
<RouterLink :to="post.path">{{ post.title }}</RouterLink>
</li>
</ul>
</div>
</div>
</template>

View File

@ -0,0 +1,18 @@
<script lang="ts" setup>
import CategoryGroup from '@theme/CategoryGroup.vue'
import { useCategoryList } from '../composables'
const categoryList = useCategoryList()
</script>
<template>
<div class="category-wrapper">
<div class="category-container">
<h2 class="category-header">栏目</h2>
<CategoryGroup
v-for="(category, index) in categoryList"
:key="index"
:category="category"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import * as dayjs from 'dayjs'
import type { Component } from 'vue'
import { h, withDefaults } from 'vue'
import type { CategoryItem } from '../composables'
interface Props {
category: CategoryItem
head: number
}
const props = withDefaults(defineProps<Props>(), {
head: 2,
})
const format = (date: string | undefined): string => {
return dayjs(date).format('YYYY-MM-DD')
}
const getCount = (category: CategoryItem, count = 0): number => {
count += category.postList.length
if (category.children.length > 0) {
category.children.forEach((child) => {
count += child.postList.length
})
}
return count
}
const TitleFnComponent = (): Component => {
const head = props.head > 4 ? 4 : props.head
return h(`h${head}`, { id: props.category.label }, [
props.category.label,
h('span', `(${getCount(props.category)})`),
])
}
</script>
<template>
<div class="category-group-wrapper">
<TitleFnComponent />
<ul>
<li
v-for="post in category.postList"
:key="post.path"
class="category-group-item"
>
<span class="mr-5">[{{ format(post.frontmatter.createTime) }}]</span>
<RouterLink :to="post.path">{{ post.title }}</RouterLink>
</li>
</ul>
<CategoryGroup
v-for="cate in category.children"
:key="'' + head + cate.type"
:category="cate"
:head="head + 1"
/>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import PostList from '@theme/PostList.vue'
import RightSideBar from '@theme/RightSideBar.vue'
import { withBase } from '@vuepress/client'
import { ref } from 'vue'
import { useThemeData } from '../composables/useThemeData'
const themeData = useThemeData()
const bannerStyle = ref({
'background-image': `url(${withBase(themeData.value.bannerImg)})`,
})
</script>
<template>
<div class="home-wrapper">
<div class="home-banner" :style="bannerStyle"></div>
<div class="home-container">
<PostList class="flex-1" />
<RightSideBar></RightSideBar>
</div>
</div>
</template>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import { computed, withDefaults } from 'vue'
interface Props {
type: 'user' | 'folder' | 'clock' | 'github' | 'email' | 'tag'
size: 'sm' | 'md' | 'xs'
}
const props = withDefaults(defineProps<Props>(), {
type: 'user',
size: 'md',
})
const sizeList = { sm: 'w-4 h-4', md: 'w-6 h-6', xs: 'w-8 h-8' }
const className = computed(() => {
return sizeList[props.size] || sizeList.sm
})
</script>
<template>
<span class="inline-block">
<svg
:class="className"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
v-if="type === 'user'"
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
></path>
<path
v-if="type === 'folder'"
d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
></path>
<path
v-if="type === 'clock'"
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clip-rule="evenodd"
></path>
<path
v-if="type === 'github'"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
/>
<path
v-if="type === 'email'"
d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414.05 3.555ZM0 4.697v7.104l5.803-3.558L0 4.697ZM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586l-1.239-.757Zm3.436-.586L16 11.801V4.697l-5.803 3.546Z"
/>
<path
v-if="type === 'tag'"
fill-rule="evenodd"
d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
></path>
</svg>
</span>
</template>

View File

@ -0,0 +1,41 @@
<script lang="ts" setup>
import { useSiteData, withBase } from '@vuepress/client'
import { ref } from 'vue'
const siteData = useSiteData()
const navListRaw = [
{ label: '首页', link: '/' },
{ label: '栏目', link: '/category/' },
{ label: '标签', link: '/tags/' },
{ label: '归档', link: '/archives/' },
]
const navList = ref(
navListRaw.map(({ label, link }) => ({
label,
link: withBase(link),
}))
)
</script>
<template>
<div class="navbar-wrapper">
<div class="flex items-center">
<!-- <div class="blog-logo">
<img src="" />
</div> -->
<div class="blog-title">
<h3>{{ siteData.title }}</h3>
</div>
</div>
<div class="flex items-center">
<RouterLink
v-for="nav in navList"
:key="nav.link"
:to="nav.link"
class="nav-link"
>
<span>{{ nav.label }}</span>
</RouterLink>
</div>
</div>
</template>

View File

@ -0,0 +1,16 @@
<script lang="ts" setup>
import { usePageData } from '@vuepress/client'
import type { PlumeThemePageData } from '../../shared'
import PostMeta from './PostMeta.vue'
const post = usePageData<PlumeThemePageData>()
</script>
<template>
<main class="post-wrapper">
<div class="post-container">
<PostMeta :post="post" type="post" />
<main class="post-content">
<Content />
</main>
</div>
</main>
</template>

View File

@ -0,0 +1,41 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { usePostIndex } from '../composables/usePostIndex'
import PostMeta from './PostMeta.vue'
const postIndex = usePostIndex()
const router = useRouter()
const route = useRoute()
const postList = computed(() => {
const tag = route.query.tag as string
if (route.path === '/tags/' && tag) {
return postIndex.value.filter((post) =>
post.frontmatter.tags?.includes(tag)
)
}
return postIndex.value
})
const linkTo = (path: string): void => {
router.push({ path })
}
</script>
<template>
<div class="post-list">
<section
v-for="post in postList"
:key="post.path"
class="post-list-item"
@click="linkTo(post.path)"
>
<PostMeta :post="post" type="postItem"></PostMeta>
<div v-if="post.excerpt" class="mt-3">
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="post-list-excerpt" v-html="post.excerpt"></div>
</div>
</section>
</div>
</template>

View File

@ -0,0 +1,64 @@
<script lang="ts" setup>
import * as dayjs from 'dayjs'
import { computed, withDefaults } from 'vue'
import type { PostItemIndex } from '../../shared'
import Icon from './Icon.vue'
const props = withDefaults(
defineProps<{
post: PostItemIndex
type: 'post' | 'postItem'
}>(),
{}
)
const tags = computed(() => {
return (props.post.frontmatter.tags || []).filter((_, i) => i < 4)
})
const formatDate = (date: number | Date | string | undefined): string => {
return dayjs(date).format('YYYY-MM-DD')
}
// const getTags = (post: PostItemIndex): string[] => {
// const tags = post.frontmatter.tags || []
// return tags.splice(0, 4)
// }
</script>
<template>
<div class="post-meta">
<h2 v-if="type === 'postItem'">
{{ post.title }}
</h2>
<h1 v-else>{{ post.title }}</h1>
</div>
<div
class="post-meta-profile"
:class="post.excerpt || type === 'post' ? 'post-meta-profile-border' : ''"
>
<div class="post-author">
<Icon type="user" size="sm" class="mr-1"></Icon>
<span>{{ post.frontmatter.author }}</span>
</div>
<div class="post-category">
<Icon type="folder" size="sm" class="mr-1"></Icon>
<template v-for="(cate, index) in post.category" :key="cate.type">
<span>
{{ cate.name }}
</span>
<span v-if="index < post.category.length - 1"> / </span>
</template>
</div>
<div v-if="tags && tags.length > 0" class="post-tags">
<Icon type="tag" size="sm" class="mr-1"></Icon>
<template v-for="(tag, index) in tags" :key="tag">
<span>{{ tag }}</span>
<span v-if="index < tags.length - 1"></span>
</template>
</div>
<div class="post-createtime">
<Icon type="clock" size="sm" class="mr-1"></Icon>
<span>{{ formatDate(post.frontmatter.createTime) }}</span>
</div>
</div>
</template>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import Icon from '@theme/Icon.vue'
import TagSidebar from '@theme/TageSidebar.vue'
import { withBase } from '@vuepress/client'
import { computed } from 'vue'
import { useThemeData } from '../composables/useThemeData'
const themeData = useThemeData()
const avatarUrl = computed(() => {
return withBase(themeData.value.avatarUrl)
})
</script>
<template>
<div class="right-sidebar-wrapper">
<div class="profile-wrapper">
<img :src="avatarUrl" alt="" />
<div class="profile-link">
<a v-if="themeData.github" :href="themeData.github" target="_blank"
><Icon type="github" size="xs" />
</a>
<a
v-if="themeData.email"
:href="'mailto:' + themeData.email"
target="_blank"
><Icon type="email" size="xs" />
</a>
</div>
<h3>{{ themeData.avatar }}</h3>
<p v-if="themeData.description">
{{ themeData.description }}
</p>
</div>
<TagSidebar></TagSidebar>
</div>
</template>

View File

@ -0,0 +1,40 @@
<script lang="ts" setup>
import { ref } from 'vue'
import type { TagsItem } from '../composables/useTages'
import { useTags } from '../composables/useTages'
const tagList = useTags()
const show = ref(false)
const maxCount = ref(10)
const triggerShow = (): void => {
show.value = !show.value
}
const getLink = (tag: TagsItem): string => {
return '/tags/?tag=' + encodeURIComponent(tag.tag)
}
</script>
<template>
<div class="tags-sidebar-wrapper">
<h3>标签</h3>
<div class="tags-sidebar-container">
<RouterLink
v-for="(tag, index) in tagList"
v-show="index < maxCount || show"
:key="tag.tag"
:to="getLink(tag)"
>
<span>{{ tag.tag }}</span>
<span>({{ tag.count }})</span>
</RouterLink>
<span
v-if="tagList.length >= maxCount"
class="tags-more"
@click="triggerShow"
>
{{ show ? '隐藏' : '更多' }}
</span>
</div>
</div>
</template>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import PostList from '@theme/PostList.vue'
import TagSidebar from '@theme/TageSidebar.vue'
</script>
<template>
<div class="tags-wrapper">
<div class="tags-container">
<PostList class="flex-1" />
<div class="right-sidebar-container">
<TagSidebar></TagSidebar>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { useDarkMode, useThemeLocaleData } from '../composables'
const themeLocale = useThemeLocaleData()
const isDarkMode = useDarkMode()
const toggleDarkMode = (): void => {
isDarkMode.value = !isDarkMode.value
}
</script>
<template>
<button
class="toggle-dark-button"
:title="themeLocale.toggleDarkMode"
@click="toggleDarkMode"
>
<svg
v-show="!isDarkMode"
class="icon"
focusable="false"
viewBox="0 0 32 32"
>
<path
d="M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6z"
fill="currentColor"
/>
<path
d="M5.394 6.813l1.414-1.415l3.506 3.506L8.9 10.318z"
fill="currentColor"
/>
<path d="M2 15.005h5v2H2z" fill="currentColor" />
<path
d="M5.394 25.197L8.9 21.691l1.414 1.415l-3.506 3.505z"
fill="currentColor"
/>
<path d="M15 25.005h2v5h-2z" fill="currentColor" />
<path
d="M21.687 23.106l1.414-1.415l3.506 3.506l-1.414 1.414z"
fill="currentColor"
/>
<path d="M25 15.005h5v2h-5z" fill="currentColor" />
<path
d="M21.687 8.904l3.506-3.506l1.414 1.415l-3.506 3.505z"
fill="currentColor"
/>
<path d="M15 2.005h2v5h-2z" fill="currentColor" />
</svg>
<svg v-show="isDarkMode" class="icon" focusable="false" viewBox="0 0 32 32">
<path
d="M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z"
fill="currentColor"
/>
</svg>
</button>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { useThemeLocaleData } from '../composables'
defineEmits(['toggle'])
const themeLocale = useThemeLocaleData()
</script>
<template>
<div
class="toggle-sidebar-button"
:title="themeLocale.toggleSidebar"
aria-expanded="false"
role="button"
tabindex="0"
@click="$emit('toggle')"
>
<div class="icon" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
</div>
</template>

View File

@ -0,0 +1,7 @@
export * from './useDarkMode'
export * from './useNavLink'
export * from './useResolveRouteWithRedirect'
export * from './useScrollPromise'
export * from './useSidebarItems'
export * from './useThemeData'
export * from './useCategoryList'

View File

@ -0,0 +1,34 @@
import * as dayjs from 'dayjs'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PostItemIndex } from '../../shared'
import { usePostIndex } from './usePostIndex'
export interface ArchivesItem {
label: string | number
postList: PostItemIndex[]
}
export type Archives = ArchivesItem[]
export type ArchivesRef = Ref<Archives>
const ArchivesListRaw: Archives = []
usePostIndex().value.forEach((post) => {
const createTime = dayjs(post.frontmatter.createTime)
const year = createTime.year()
let current = ArchivesListRaw.find((arch) => arch.label === year)
if (!current) {
current = {
label: year,
postList: [],
}
ArchivesListRaw.push(current)
}
current.postList.push(post)
})
export const archivesList: ArchivesRef = ref(ArchivesListRaw)
export const useArchivesList = (): ArchivesRef => archivesList

View File

@ -0,0 +1,61 @@
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PostItemIndex } from '../../shared'
import { usePostIndex } from './usePostIndex'
export interface CategoryItem {
label: string
type: string | number
children: CategoryList
postList: PostItemIndex[]
}
export type CategoryList = CategoryItem[]
export type CategoryListRef = Ref<CategoryList>
const categoriesRaw: CategoryList = []
usePostIndex().value.forEach((post) => {
const category = post.category
let index = 1
let cate = category[0]
let first = categoriesRaw.find((c) => c.type === cate.type)
if (!first) {
first = {
label: cate.name,
type: cate.type,
children: [],
postList: [],
}
categoriesRaw.push(first)
}
if (category.length === 1) {
first.postList.push(post)
}
let children = first.children
while ((cate = category[index])) {
let current = children.find((c) => c.type === cate.type)
if (!current) {
current = {
label: cate.name,
type: cate.type,
children: [],
postList: [],
}
}
children.push(current)
children = current.children
if (index === category.length - 1) {
current.postList.push(post)
}
index++
}
})
export const categoryList: CategoryListRef = ref(
categoriesRaw.sort((left, right) => {
return left.type > right.type ? 1 : -1
})
)
export const useCategoryList = (): CategoryListRef => categoryList

View File

@ -0,0 +1,66 @@
import { usePreferredDark, useStorage } from '@vueuse/core'
import { computed, inject, onMounted, onUnmounted, provide, watch } from 'vue'
import type { InjectionKey, WritableComputedRef } from 'vue'
import { useThemeLocaleData } from '.'
export type DarkModeRef = WritableComputedRef<boolean>
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol('')
/**
* Inject dark mode global computed
*/
export const useDarkMode = (): DarkModeRef => {
const isDarkMode = inject(darkModeSymbol)
if (!isDarkMode) {
throw new Error('useDarkMode() is called without provider.')
}
return isDarkMode
}
/**
* Create dark mode ref and provide as global computed in setup
*/
export const setupDarkMode = (): void => {
const themeLocale = useThemeLocaleData()
const isDarkPreferred = usePreferredDark()
const darkStorage = useStorage('vuepress-color-scheme', 'auto')
const isDarkMode = computed<boolean>({
get() {
// disable dark mode
if (!themeLocale.value.darkMode) {
return false
}
// auto detected from prefers-color-scheme
if (darkStorage.value === 'auto') {
return isDarkPreferred.value
}
// storage value
return darkStorage.value === 'dark'
},
set(val) {
if (val === isDarkPreferred.value) {
darkStorage.value = 'auto'
} else {
darkStorage.value = val ? 'dark' : 'light'
}
},
})
provide(darkModeSymbol, isDarkMode)
updateHtmlDarkClass(isDarkMode)
}
export const updateHtmlDarkClass = (isDarkMode: DarkModeRef): void => {
const update = (value = isDarkMode.value): void => {
// set `class="dark"` on `<html>` element
const htmlEl = window?.document.querySelector('html')
htmlEl?.classList.toggle('dark', value)
}
onMounted(() => {
watch(isDarkMode, update, { immediate: true })
})
onUnmounted(() => update())
}

View File

@ -0,0 +1,23 @@
import type { NavLink } from '../../shared'
import { useResolveRouteWithRedirect } from './useResolveRouteWithRedirect'
declare module 'vue-router' {
interface RouteMeta {
title?: string
}
}
/**
* Resolve NavLink props from string
*
* @example
* - Input: '/README.md'
* - Output: { text: 'Home', link: '/' }
*/
export const useNavLink = (item: string): NavLink => {
const resolved = useResolveRouteWithRedirect(item)
return {
text: resolved.meta.title || item,
link: resolved.name === '404' ? item : resolved.fullPath,
}
}

View File

@ -0,0 +1,16 @@
import { postIndex as postListRaw } from '@internal/postIndex.js'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { PostIndex } from '../../shared'
export type PostIndexRef = Ref<PostIndex>
export const postIndex: PostIndexRef = ref(postListRaw)
export const usePostIndex = (): PostIndexRef => postIndex
if ((import.meta as any).hot) {
__VUE_HMR_RUNTIME__.updatePostIndex = (data: PostIndex) => {
postIndex.value = data
}
}

View File

@ -0,0 +1,28 @@
import { isFunction, isString } from '@vuepress/shared'
import { useRouter } from 'vue-router'
import type { Router } from 'vue-router'
/**
* Resolve a route with redirection
*/
export const useResolveRouteWithRedirect = (
...args: Parameters<Router['resolve']>
): ReturnType<Router['resolve']> => {
const router = useRouter()
const route = router.resolve(...args)
const lastMatched = route.matched[route.matched.length - 1]
if (!lastMatched?.redirect) {
return route
}
const { redirect } = lastMatched
const resolvedRedirect = isFunction(redirect) ? redirect(route) : redirect
const resolvedRedirectObj = isString(resolvedRedirect)
? { path: resolvedRedirect }
: resolvedRedirect
return useResolveRouteWithRedirect({
hash: route.hash,
query: route.query,
params: route.params,
...resolvedRedirectObj,
})
}

View File

@ -0,0 +1,22 @@
export interface ScrollPromise {
wait(): Promise<void> | null
pending: () => void
resolve: () => void
}
let promise: Promise<void> | null = null
let promiseResolve: (() => void) | null = null
const scrollPromise: ScrollPromise = {
wait: () => promise,
pending: () => {
promise = new Promise((resolve) => (promiseResolve = resolve))
},
resolve: () => {
promiseResolve?.()
promise = null
promiseResolve = null
},
}
export const useScrollPromise = (): ScrollPromise => scrollPromise

View File

@ -0,0 +1,179 @@
import { usePageData, usePageFrontmatter } from '@vuepress/client'
import type { PageHeader } from '@vuepress/client'
import type { PageFrontmatter } from '@vuepress/shared'
import {
isArray,
isPlainObject,
isString,
resolveLocalePath,
} from '@vuepress/shared'
import { computed, inject, provide } from 'vue'
import type { ComputedRef, InjectionKey } from 'vue'
import { useRoute } from 'vue-router'
import type {
PlumeThemeData,
PlumeThemePostPageFrontmatter,
ResolvedSidebarItem,
SidebarConfigArray,
SidebarConfigObject,
SidebarItem,
} from '../../shared'
import { useNavLink, useThemeLocaleData } from '.'
export type SidebarItemsRef = ComputedRef<ResolvedSidebarItem[]>
export const sidebarItemsSymbol: InjectionKey<SidebarItemsRef> =
Symbol('sidebarItems')
/**
* Inject sidebar items global computed
*/
export const useSidebarItems = (): SidebarItemsRef => {
const sidebarItems = inject(sidebarItemsSymbol)
if (!sidebarItems) {
throw new Error('useSidebarItems() is called without provider.')
}
return sidebarItems
}
/**
* Create sidebar items ref and provide as global computed in setup
*/
export const setupSidebarItems = (): void => {
const themeLocale = useThemeLocaleData()
const frontmatter = usePageFrontmatter<PlumeThemePostPageFrontmatter>()
const sidebarItems = computed(() =>
resolveSidebarItems(frontmatter.value, themeLocale.value)
)
provide(sidebarItemsSymbol, sidebarItems)
}
/**
* Resolve sidebar items global computed
*
* It should only be resolved and provided once
*/
export const resolveSidebarItems = (
frontmatter: PageFrontmatter<PlumeThemePostPageFrontmatter>,
themeLocale: PlumeThemeData
): ResolvedSidebarItem[] => {
// get sidebar config from frontmatter > themeConfig
const sidebarConfig = frontmatter.sidebar ?? themeLocale.sidebar ?? 'auto'
const sidebarDepth = frontmatter.sidebarDepth ?? themeLocale.sidebarDepth ?? 2
// resolve sidebar items according to the config
if (frontmatter.home || sidebarConfig === false) {
return []
}
if (sidebarConfig === 'auto') {
return resolveAutoSidebarItems(sidebarDepth)
}
if (isArray(sidebarConfig)) {
return resolveArraySidebarItems(sidebarConfig, sidebarDepth)
}
if (isPlainObject(sidebarConfig)) {
return resolveMultiSidebarItems(sidebarConfig, sidebarDepth)
}
return []
}
/**
* Util to transform page header to sidebar item
*/
export const headerToSidebarItem = (
header: PageHeader,
sidebarDepth: number
): ResolvedSidebarItem => ({
text: header.title,
link: `#${header.slug}`,
children: headersToSidebarItemChildren(header.children, sidebarDepth),
})
export const headersToSidebarItemChildren = (
headers: PageHeader[],
sidebarDepth: number
): ResolvedSidebarItem[] =>
sidebarDepth > 0
? headers.map((header) => headerToSidebarItem(header, sidebarDepth - 1))
: []
/**
* Resolve sidebar items if the config is `auto`
*/
export const resolveAutoSidebarItems = (
sidebarDepth: number
): ResolvedSidebarItem[] => {
const page = usePageData()
return [
{
text: page.value.title,
children: headersToSidebarItemChildren(page.value.headers, sidebarDepth),
},
]
}
/**
* Resolve sidebar items if the config is an array
*/
export const resolveArraySidebarItems = (
sidebarConfig: SidebarConfigArray,
sidebarDepth: number
): ResolvedSidebarItem[] => {
const route = useRoute()
const page = usePageData()
const handleChildItem = (
item: ResolvedSidebarItem | SidebarItem | string
): ResolvedSidebarItem => {
let childItem: ResolvedSidebarItem
if (isString(item)) {
childItem = useNavLink(item)
} else {
childItem = item as ResolvedSidebarItem
}
if (childItem.children) {
return {
...childItem,
children: childItem.children.map((item) => handleChildItem(item)),
}
}
// if the sidebar item is current page and children is not set
// use headers of current page as children
if (childItem.link === route.path) {
// skip h1 header
const headers =
page.value.headers[0]?.level === 1
? page.value.headers[0].children
: page.value.headers
return {
...childItem,
children: headersToSidebarItemChildren(headers, sidebarDepth),
}
}
return childItem
}
return sidebarConfig.map((item) => handleChildItem(item))
}
/**
* Resolve sidebar items if the config is a key -> value (path-prefix -> array) object
*/
export const resolveMultiSidebarItems = (
sidebarConfig: SidebarConfigObject,
sidebarDepth: number
): ResolvedSidebarItem[] => {
const route = useRoute()
const sidebarPath = resolveLocalePath(sidebarConfig, route.path)
const matchedSidebarConfig = sidebarConfig[sidebarPath] ?? []
return resolveArraySidebarItems(matchedSidebarConfig, sidebarDepth)
}

View File

@ -0,0 +1,34 @@
import type { Ref } from 'vue'
import { ref } from 'vue'
import { usePostIndex } from './usePostIndex'
export type TagsItem = {
tag: string
count: number
}
export type Tags = TagsItem[]
export type TagsRef = Ref<Tags>
const tagsObj: Record<string, number> = {}
usePostIndex().value.forEach((post) => {
;(post.frontmatter.tags || []).forEach((t) => {
if (!tagsObj[t]) {
tagsObj[t] = 1
} else {
tagsObj[t] += 1
}
})
})
const tagsRaw: Tags = Object.keys(tagsObj)
.map((key) => ({
tag: key,
count: tagsObj[key],
}))
.sort((left, right) => (left.count <= right.count ? 1 : -1))
export const tags: TagsRef = ref(tagsRaw)
export const useTags = (): TagsRef => tags

View File

@ -0,0 +1,14 @@
import {
useThemeData as _useThemeData,
useThemeLocaleData as _useThemeLocaleData,
} from '@vuepress/plugin-theme-data/lib/client'
import type {
ThemeDataRef,
ThemeLocaleDataRef,
} from '@vuepress/plugin-theme-data/lib/client'
import type { PlumeThemeData } from '../../shared'
export const useThemeData = (): ThemeDataRef<PlumeThemeData> =>
_useThemeData<PlumeThemeData>()
export const useThemeLocaleData = (): ThemeLocaleDataRef<PlumeThemeData> =>
_useThemeLocaleData<PlumeThemeData>()

View File

@ -0,0 +1,8 @@
<script lang="ts" setup>
import { useSiteData } from '@vuepress/client'
console.log(useSiteData())
</script>
<template>
<div>404</div>
</template>

View File

@ -0,0 +1,31 @@
<script lang="ts" setup>
import Archives from '@theme/Archives.vue'
import Category from '@theme/Category.vue'
import Home from '@theme/Home.vue'
import NavBar from '@theme/NavBar.vue'
import Post from '@theme/Post.vue'
import Tags from '@theme/Tags.vue'
import { usePageData, usePageFrontmatter } from '@vuepress/client'
import type {
PlumeThemePageData,
PlumeThemePageFrontmatter,
} from '../../shared'
const pageData = usePageData<PlumeThemePageData>()
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
const layout = {
category: Category,
archives: Archives,
tags: Tags,
}
</script>
<template>
<div class="theme-container">
<NavBar />
<Home v-if="frontmatter.home" />
<Post v-if="pageData.isPost" />
<Component :is="layout[frontmatter.pageType]" v-if="frontmatter.pageType" />
</div>
</template>

5
src/client/shim.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { ComponentOptions } from 'vue'
const comp: ComponentOptions
export default comp
}

View File

@ -0,0 +1,34 @@
.archives-list-wrapper {
@apply w-full max-w-6xl m-auto pt-20;
& .archives-list-container {
@apply p-5 bg-white rounded-md mb-5 shadow-sm;
}
& h2 {
@apply text-slate-800 text-4xl py-5 pl-5 bg-white shadow-sm rounded-md mb-5;
}
& h3,h4 {
@apply text-2xl pb-4 text-slate-500;
& span {
@apply ml-3 ;
}
}
& .archives-item {
@apply flex justify-start items-center;
@apply h-9 text-xl text-slate-400;
& span {
@apply mr-5;
}
& a {
@apply text-slate-500 font-bold transition-all;
@apply hover:underline hover:text-slate-600;
}
}
}

View File

@ -0,0 +1,24 @@
@custom-media --min-viewprot (min-width: 719px);
@media (--min-viewprot) {
::-webkit-scrollbar{
width:6px;
height:5px;
}
::-webkit-scrollbar-track-piece{
background-color:rgba(0,0,0,.15);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:vertical{
height:5px;
background-color:rgba(0,0,0,.28);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:horizontal{
width:5px;
background-color:rgba(0,0,0,.28);
border-radius: 3px;
}
}

View File

@ -0,0 +1,52 @@
.category-wrapper {
@apply pt-20;
& .category-container {
@apply w-full max-w-6xl m-auto shadow-sm mb-10;
}
& .category-header {
@apply text-slate-800 text-4xl py-5 pl-5 bg-white shadow-sm rounded-md mb-5;
}
}
.category-group-wrapper {
@apply pt-5 pl-5 bg-white shadow-sm rounded-md mb-5 pr-5 pb-5;
& & {
@apply shadow-none mb-0;
}
& h2 {
@apply text-3xl pb-4 mb-3;
& span {
@apply ml-5;
}
}
& h3,h4 {
@apply text-2xl pb-4 text-slate-500;
& span {
@apply ml-3 ;
}
}
& h5, h6 {
@apply text-xl pb-3;
& span {
@apply ml-2;
}
}
& .category-group-item {
@apply flex justify-start items-center;
@apply h-9 text-xl text-slate-400;
& a {
@apply text-slate-500 font-bold transition-all;
@apply hover:underline hover:text-slate-600;
}
}
}

View File

@ -0,0 +1,69 @@
/* @import '_variables'; */
/**
* code-group
*/
.code-group__nav {
margin-top: 0.85rem;
/* // 2 * margin + border-radius of <pre> tag */
margin-bottom: calc(-1.7rem - 6px);
padding-bottom: calc(1.7rem - 6px);
padding-left: 10px;
padding-top: 10px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background-color: var(--code-bg-color);
}
.code-group__ul {
margin: auto 0;
padding-left: 0;
display: inline-flex;
list-style: none;
}
.code-group__nav-tab {
border: 0;
padding: 5px;
cursor: pointer;
background-color: transparent;
font-size: 0.85em;
line-height: 1.4;
color: rgba(255, 255, 255, 0.9);
font-weight: 600;
}
.code-group__nav-tab:focus {
outline: none;
}
.code-group__nav-tab:focus-visible {
outline: 1px solid rgba(255, 255, 255, 0.9);
}
.code-group__nav-tab-active {
border-bottom: var(--c-brand) 1px solid;
}
@media (max-width: $MQMobileNarrow) {
.code-group__nav {
margin-left: -1.5rem;
margin-right: -1.5rem;
border-radius: 0;
}
}
/**
* code-group-item
*/
.code-group-item {
display: none;
}
.code-group-item__active {
display: block;
}
.code-group-item > pre {
background-color: orange;
}

265
src/client/styles/code.css Normal file
View File

@ -0,0 +1,265 @@
/* @import '_variables'; */
/* =============================== */
/* Forked and modified from prismjs/themes/prism-tomorrow.css */
code[class*='language-'],
pre[class*='language-'] {
color: #ccc;
background: none;
font-family: var(--font-family-code);
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
background-color: none;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #2d2d2d;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #ec5975;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: #3eaf7c;
}
/* =============================== */
.post-content{
pre,
pre[class*='language-'] {
line-height: 1.4;
padding: 1.3rem 1.5rem;
margin: 0.85rem 0;
border-radius: 6px;
overflow: auto;
code {
color: #fff;
padding: 0;
background-color: transparent;
border-radius: 0;
overflow-wrap: unset;
line-height: 1.77;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
}
.line-number {
font-family: var(--font-family-code);
}
}
div[class*='language-'] {
position: relative;
background-color: var(--code-bg-color);
border-radius: 6px;
&::before {
position: absolute;
z-index: 3;
top: 0.8em;
right: 1em;
font-size: 0.75rem;
color: var(--code-ln-color);
}
pre,
pre[class*='language-'] {
background: transparent !important;
position: relative;
z-index: 1;
}
.highlight-lines {
user-select: none;
padding-top: 1.3rem;
position: absolute;
top: 0;
left: 0;
width: 100%;
line-height: 1.4;
.highlight-line {
background-color: var(--code-hl-bg-color);
}
}
&:not(.line-numbers-mode) {
.line-numbers {
display: none;
}
}
&.line-numbers-mode {
.highlight-lines .highlight-line {
position: relative;
&::before {
content: ' ';
position: absolute;
z-index: 2;
left: 0;
top: 0;
display: block;
width: var(--code-ln-wrapper-width);
height: 100%;
}
}
pre {
margin-left: var(--code-ln-wrapper-width);
padding-left: 1rem;
vertical-align: middle;
}
.line-numbers {
position: absolute;
top: 0;
width: var(--code-ln-wrapper-width);
text-align: center;
color: var(--code-ln-color);
padding-top: 1.25rem;
line-height: 1.4;
br {
user-select: none;
}
.line-number {
position: relative;
z-index: 3;
user-select: none;
font-size: 0.85em;
line-height: 0;
}
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: var(--code-ln-wrapper-width);
height: 100%;
border-radius: 6px 0 0 6px;
border-right: 1px solid var(--code-hl-bg-color);
}
}
}
@each $lang in c, cpp, cs, css, dart, docker, fs, go, html, java, js, json, kt, less, makefile, md, php, py, rb, rs, sass, scss, sh, styl, ts, toml, vue, yml {
div[class*='language-'].ext-$(lang) {
&:before {
content: '$(lang)';
}
}
}
/* narrow mobile */
@media (max-width: 419px) {
.post-content {
div[class*='language-'] {
margin: 0.85rem -1.5rem;
border-radius: 0;
}
}
}

View File

@ -0,0 +1,40 @@
.home-wrapper {
@apply w-full min-h-screen pt-14;
& .home-banner {
@apply flex justify-center items-center h-80 m-0 bg-gray-200 bg-no-repeat bg-cover bg-top;
}
}
.home-container {
@apply flex justify-start items-start w-full max-w-6xl m-auto pt-5;
}
.right-sidebar-wrapper {
@apply ml-5 w-80;
& .profile-wrapper {
@apply bg-white shadow-sm hover:shadow-md transition-shadow p-5 rounded-md mb-5;
& .profile-link {
@apply flex justify-center items-center pt-3;
}
& img {
@apply w-2/3 m-auto;
}
& a {
@apply mx-2 text-slate-500;
}
& h3 {
@apply text-center pt-3;
}
& p {
@apply pt-2 text-gray-500 text-center;
}
}
}

View File

@ -0,0 +1,14 @@
@import 'vars';
@import 'vars-dark';
@import './normalize';
@import './arrow';
@import './navBar';
@import './home';
@import './post';
@import './category';
@import './archives';
@import './tags';
@import './code';
@import './code-group';

View File

@ -0,0 +1,7 @@
.navbar-wrapper {
@apply fixed top-0 left-0 w-full h-14 bg-white shadow-md z-50 flex justify-between items-center px-10;
& .nav-link {
@apply ml-8 text-slate-600 font-bold;
}
}

177
src/client/styles/normalize.css vendored Normal file
View File

@ -0,0 +1,177 @@
html,
body {
padding: 0;
margin: 0;
@apply bg-gray-100;
transition: background-color var(--t-color);
}
html.dark {
color-scheme: dark;
}
body {
font-family: var(--font-family);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
color: var(--c-text);
}
*,
*::after,
*::before {
box-sizing: border-box;
}
a {
font-weight: 500;
color: var(--c-text-accent);
text-decoration: none;
overflow-wrap: break-word;
}
p a code {
font-weight: 400;
color: var(--c-text-accent);
}
kbd {
font-family: var(--font-family-code);
color: var(--c-text);
background: var(--c-bg-lighter);
border: solid 0.15rem var(--c-border-dark);
border-bottom: solid 0.25rem var(--c-border-dark);
border-radius: 0.15rem;
padding: 0 0.15em;
}
code {
font-family: var(--font-family-code);
color: var(--c-text-lighter);
padding: 0.25rem 0.5rem;
margin: 0;
font-size: 0.85em;
border-radius: 3px;
overflow-wrap: break-word;
transition: background-color var(--t-color);
}
blockquote {
font-size: 1rem;
color: var(--c-text-quote);
border-left: 0.2rem solid var(--c-border-dark);
margin: 1rem 0;
padding: 0.25rem 0 0.25rem 1rem;
& > p {
margin: 0;
}
}
ul,
ol {
padding-left: 1.2em;
}
strong {
font-weight: 600;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
line-height: 1.25;
&:focus-visible {
outline: none;
}
&:hover .header-anchor {
opacity: 1;
}
}
h1 {
font-size: 2.2rem;
}
h2 {
font-size: 1.65rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--c-border);
transition: border-color var(--t-color);
}
h3 {
font-size: 1.35rem;
}
h4 {
font-size: 1.15rem;
}
h5 {
font-size: 1.05rem;
}
h6 {
font-size: 1rem;
}
a.header-anchor {
font-size: 0.85em;
float: left;
margin-left: -0.87em;
padding-right: 0.23em;
margin-top: 0.125em;
opacity: 0;
&:hover {
text-decoration: none;
}
&:focus-visible {
opacity: 1;
}
}
p,
ul,
ol {
line-height: 1.7;
}
hr {
border: 0;
border-top: 1px solid var(--c-border);
}
table {
border-collapse: collapse;
margin: 1rem 0;
display: block;
overflow-x: auto;
transition: border-color var(--t-color);
}
tr {
border-top: 1px solid var(--c-border-dark);
transition: border-color var(--t-color);
&:nth-child(2n) {
background-color: var(--c-bg-light);
transition: background-color var(--t-color);
}
}
th,
td {
padding: 0.6em 1em;
border: 1px solid var(--c-border-dark);
transition: border-color var(--t-color);
}

View File

@ -0,0 +1,57 @@
.post-wrapper {
@apply pt-20;
& .post-container {
@apply w-full max-w-6xl p-12 pt-0 m-auto bg-white shadow-sm mb-10;
}
& .post-content {
@apply prose prose-lg prose-gray max-w-none mt-10;
}
}
.post-meta {
@apply flex pb-2;
& h2 {
@apply flex-1 pr-5 border-none;
}
& h1 {
@apply flex-1 pr-5 pt-9 mb-5;
}
&-profile {
@apply flex justify-start text-base text-gray-400;
&-border {
@apply border-b border-gray-200 pb-3;
}
& .post-author {
@apply flex items-center mr-5;
}
& .post-category {
@apply flex items-center mr-5;
}
& .post-createtime {
@apply flex items-center;
}
& .post-tags {
@apply flex items-center mr-5;
}
}
}
.post-list {
&-item {
@apply bg-white mb-5 shadow-sm hover:shadow-md transition-shadow p-5 rounded-md cursor-pointer;
}
&-excerpt {
@apply prose prose-lg max-w-none;
}
}

View File

@ -0,0 +1,35 @@
.tags-sidebar-wrapper {
@apply bg-white shadow-sm hover:shadow-md transition-shadow p-5 rounded-md mb-5;
& h3 {
@apply border-b border-gray-300 pb-3 text-slate-500 text-xl;
}
& .tags-sidebar-container {
@apply pt-3 -mx-3;
& a {
@apply inline-block px-3 py-2 bg-slate-400 text-white ml-3 mt-3 leading-4 rounded-sm;
& span:last-of-type {
@apply ml-1;
}
}
& .tags-more {
@apply inline-block px-3 py-2 text-slate-400 ml-3 mt-3 leading-4 rounded-sm cursor-pointer;
}
}
}
.tags-wrapper {
@apply w-full min-h-screen pt-14;
}
.tags-container {
@apply flex justify-start items-start w-full max-w-6xl m-auto pt-5;
.right-sidebar-container {
@apply ml-5 w-80;
}
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,46 @@
html.dark {
/* // brand colors */
--c-brand: #3aa675;
--c-brand-light: #349469;
/* // background colors */
--c-bg: #22272e;
--c-bg-light: #2b313a;
--c-bg-lighter: #262c34;
/* // text colors */
--c-text: #adbac7;
--c-text-light: #96a7b7;
--c-text-lighter: #8b9eb0;
--c-text-lightest: #8094a8;
/* // border colors */
--c-border: #3e4c5a;
--c-border-dark: #34404c;
/* // custom container colors */
--c-tip: #318a62;
--c-warning: #ceab00;
--c-warning-bg: #7e755b;
--c-warning-title: #ceac03;
--c-warning-text: #362e00;
--c-danger: #940000;
--c-danger-bg: #806161;
--c-danger-title: #610000;
--c-danger-text: #3a0000;
--c-details-bg: #323843;
/* // code blocks vars */
--code-hl-bg-color: #363b46;
}
/* // plugin-docsearch */
html.dark .DocSearch {
--docsearch-logo-color: var(--c-text);
--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;
--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d,
0 2px 2px 0 rgba(3, 4, 9, 0.3);
--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
}

134
src/client/styles/vars.css Normal file
View File

@ -0,0 +1,134 @@
:root {
/* // brand colors */
--c-brand: #0099CC;
--c-brand-light: #0099FF;
/* // background colors */
--c-bg: #ffffff;
--c-bg-light: #f3f4f5;
--c-bg-lighter: #eeeeee;
--c-bg-navbar: var(--c-bg);
--c-bg-sidebar: var(--c-bg);
--c-bg-arrow: #cccccc;
/* // text colors */
--c-text: #2c3e50;
--c-text-accent: var(--c-brand);
--c-text-light: #3a5169;
--c-text-lighter: #4e6e8e;
--c-text-lightest: #6a8bad;
--c-text-quote: #999999;
/* // border colors */
--c-border: #eaecef;
--c-border-dark: #dfe2e5;
/* // custom container colors */
--c-tip: #42b983;
--c-tip-bg: var(--c-bg-light);
--c-tip-title: var(--c-text);
--c-tip-text: var(--c-text);
--c-tip-text-accent: var(--c-text-accent);
--c-warning: #e7c000;
--c-warning-bg: #fffae3;
--c-warning-title: #ad9000;
--c-warning-text: #746000;
--c-warning-text-accent: var(--c-text);
--c-danger: #cc0000;
--c-danger-bg: #ffe0e0;
--c-danger-title: #990000;
--c-danger-text: #660000;
--c-danger-text-accent: var(--c-text);
--c-details-bg: #eeeeee;
/* // badge component colors */
--c-badge-tip: var(--c-tip);
--c-badge-warning: var(--c-warning);
--c-badge-danger: var(--c-danger);
/* // transition vars */
--t-color: 0.3s ease;
--t-transform: 0.3s ease;
/* // code blocks vars */
--code-bg-color: #282c34;
--code-hl-bg-color: rgba(0, 0, 0, 0.66);
--code-ln-color: #9e9e9e;
--code-ln-wrapper-width: 3.5rem;
/* // font vars */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
--font-family-code: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
/* // layout vars */
--navbar-height: 3.6rem;
--navbar-padding-v: 0.7rem;
--navbar-padding-h: 1.5rem;
--sidebar-width: 20rem;
--sidebar-width-mobile: calc(var(--sidebar-width) * 0.82);
--content-width: 740px;
--homepage-width: 960px;
}
/* // plugin-back-to-top */
.back-to-top {
--back-to-top-color: var(--c-brand);
--back-to-top-color-hover: var(--c-brand-light);
}
/* // plugin-docsearch */
.DocSearch {
--docsearch-primary-color: var(--c-brand);
--docsearch-text-color: var(--c-text);
--docsearch-highlight-color: var(--c-brand);
--docsearch-muted-color: var(--c-text-quote);
--docsearch-container-background: rgba(9, 10, 17, 0.8);
--docsearch-modal-background: var(--c-bg-light);
--docsearch-searchbox-background: var(--c-bg-lighter);
--docsearch-searchbox-focus-background: var(--c-bg);
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);
--docsearch-hit-color: var(--c-text-light);
--docsearch-hit-active-color: var(--c-bg);
--docsearch-hit-background: var(--c-bg);
--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);
--docsearch-footer-background: var(--c-bg);
}
/* // plugin-external-link-icon */
.external-link-icon {
--external-link-icon-color: var(--c-text-quote);
}
/* // plugin-medium-zoom */
.medium-zoom-overlay {
--medium-zoom-bg-color: var(--c-bg);
}
/* // plugin-nprogress */
#nprogress {
--nprogress-color: var(--c-brand);
}
/* // plugin-pwa-popup */
.pwa-popup {
--pwa-popup-text-color: var(--c-text);
--pwa-popup-bg-color: var(--c-bg);
--pwa-popup-border-color: var(--c-brand);
--pwa-popup-shadow: 0 4px 16px var(--c-brand);
--pwa-popup-btn-text-color: var(--c-bg);
--pwa-popup-btn-bg-color: var(--c-brand);
--pwa-popup-btn-hover-bg-color: var(--c-brand-light);
}
/* // plugin-search */
.search-box {
--search-bg-color: var(--c-bg);
--search-accent-color: var(--c-brand);
--search-text-color: var(--c-text);
--search-border-color: var(--c-border);
--search-item-text-color: var(--c-text-lighter);
--search-item-focus-bg-color: var(--c-bg-light);
}

3
src/client/utils/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export * from './isActiveSidebarItem';
export * from './resolveEditLink';
export * from './resolveRepoType';

View File

@ -0,0 +1,3 @@
export * from './isActiveSidebarItem';
export * from './resolveEditLink';
export * from './resolveRepoType';

View File

@ -0,0 +1,3 @@
export * from './isActiveSidebarItem'
export * from './resolveEditLink'
export * from './resolveRepoType'

View File

@ -0,0 +1,34 @@
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { ResolvedSidebarItem } from '../../shared'
const normalizePath = (path: string): string =>
decodeURI(path)
.replace(/#.*$/, '')
.replace(/(index)?\.(md|html)$/, '')
const isActiveLink = (
link: string,
route: RouteLocationNormalizedLoaded
): boolean => {
if (route.hash === link) {
return true
}
const currentPath = normalizePath(route.path)
const targetPath = normalizePath(link)
return currentPath === targetPath
}
export const isActiveSidebarItem = (
item: ResolvedSidebarItem,
route: RouteLocationNormalizedLoaded
): boolean => {
if (item.link && isActiveLink(item.link, route)) {
return true
}
if (item.children) {
return item.children.some((child) => isActiveSidebarItem(child, route))
}
return false
}

View File

@ -0,0 +1,64 @@
import {
isLinkHttp,
removeEndingSlash,
removeLeadingSlash,
} from '@vuepress/shared'
import { resolveRepoType } from './resolveRepoType'
import type { RepoType } from './resolveRepoType'
export const editLinkPatterns: Record<Exclude<RepoType, null>, string> = {
GitHub: ':repo/edit/:branch/:path',
GitLab: ':repo/-/edit/:branch/:path',
Gitee: ':repo/edit/:branch/:path',
Bitbucket:
':repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default',
}
const resolveEditLinkPatterns = ({
docsRepo,
editLinkPattern,
}: {
docsRepo: string
editLinkPattern?: string
}): string | null => {
if (editLinkPattern) {
return editLinkPattern
}
const repoType = resolveRepoType(docsRepo)
if (repoType !== null) {
return editLinkPatterns[repoType]
}
return null
}
export const resolveEditLink = ({
docsRepo,
docsBranch,
docsDir,
filePathRelative,
editLinkPattern,
}: {
docsRepo: string
docsBranch: string
docsDir: string
filePathRelative: string | null
editLinkPattern?: string
}): string | null => {
if (!filePathRelative) return null
const pattern = resolveEditLinkPatterns({ docsRepo, editLinkPattern })
if (!pattern) return null
return pattern
.replace(
/:repo/,
isLinkHttp(docsRepo) ? docsRepo : `https://github.com/${docsRepo}`
)
.replace(/:branch/, docsBranch)
.replace(
/:path/,
removeLeadingSlash(`${removeEndingSlash(docsDir)}/${filePathRelative}`)
)
}

View File

@ -0,0 +1,11 @@
import { isLinkHttp } from '@vuepress/shared'
export type RepoType = 'GitHub' | 'GitLab' | 'Gitee' | 'Bitbucket' | null
export const resolveRepoType = (repo: string): RepoType => {
if (!isLinkHttp(repo) || /github\.com/.test(repo)) return 'GitHub'
if (/bitbucket\.org/.test(repo)) return 'Bitbucket'
if (/gitlab\.com/.test(repo)) return 'GitLab'
if (/gitee\.com/.test(repo)) return 'Gitee'
return null
}

View File

@ -0,0 +1,18 @@
import { createPage } from '@vuepress/core'
import type { App } from 'vuepress'
import { navbarList } from '../shared'
export const createBlogPage = async (app: App): Promise<void> => {
const pagePromise = navbarList.map((nav) => {
return createPage(app, {
path: nav.link,
frontmatter: {
title: nav.label,
...nav.frontmatter,
},
content: '',
})
})
const pageList = await Promise.all(pagePromise)
pageList.forEach((page) => app.pages.push(page))
}

23
src/node/extendsPage.ts Normal file
View File

@ -0,0 +1,23 @@
import type { Page } from '@vuepress/core'
import type { PlumeThemePageData } from '../shared'
import { pageFilter } from './utils/pageFilter'
let uuid = 100
export const extendsPage = (page: Page<PlumeThemePageData>): void => {
if (!pageFilter(page)) return
const pagePath = page.filePathRelative
const category = pagePath
?.split('/')
.slice(0, -1)
.map((category) => {
const match = category.match(/^(\d+?)?(?:\.?)([^]+)$/) || []
return {
type: Number(match[1]) || uuid++,
name: match[2],
}
})
page.data.category = category || []
page.data.sort = parseInt(page.slug.split('.')[0]) || -1
page.data.isPost = true
page.slug = page.slug?.replace(/^\d+\./, '')
}

View File

@ -0,0 +1,99 @@
import * as os from 'os'
import type { App } from '@vuepress/core'
import { fs, path } from '@vuepress/utils'
import { isBoolean, isNumber } from '@vueuse/core'
import * as chokidar from 'chokidar'
import * as dayjs from 'dayjs'
import * as matter from 'gray-matter'
import * as json2yaml from 'json2yaml'
import { customAlphabet } from 'nanoid'
import type { MarkdownFile } from './utils/readFileList'
import { readFileList } from './utils/readFileList'
const prefix = '/post/'
const nanoid = customAlphabet('134567890abcdefghijklmnopqrstuvwxyz', 8)
const matterTask = {
title: ({ filePath }: MarkdownFile, title: string): string | undefined => {
if (title) return title
return path
.basename(filePath)
.replace(/^\d+\./, '')
.replace(path.extname(filePath), '')
},
createTime: ({ createTime }: MarkdownFile, formatTime: string) => {
if (formatTime) return formatTime
return dayjs(createTime).format('YYYY/MM/DD hh:mm:ss')
},
permalink: (_, permalink: string) => {
if (permalink) return permalink
return prefix + nanoid() + ' # 文章永久链接,自动生成,可自行配置'
},
author: (_, author: string) => {
if (author) return author
const pkg = require(path.join(process.cwd(), 'package.json'))
return pkg.author
},
top: (_, top: boolean) => {
if (isBoolean(top)) return top
return false
},
type: (_, type: string) => {
if (type) return type
return ' ' + ' # original: 原创: reprint 转载 可为空不填'
},
sort: (_, sort: string) => {
if (isNumber(parseInt(sort))) return sort
return 0
},
}
function formatMarkdown(file: MarkdownFile): string {
const { data, content } = matter(file.content)
Object.keys(matterTask).forEach((key) => {
const value = matterTask[key](file, data[key])
data[key] = value ?? data[key]
})
return (
json2yaml
.stringify(data)
.replace(/\n\s{2}/g, '\n')
.replace(/"/g, '') +
'---' +
os.EOL +
content
)
}
export const globFormatFrontmatter = (sourceDir: string): void => {
const files = readFileList(sourceDir)
files.forEach((file) => {
const content = formatMarkdown(file)
fs.writeFileSync(file.filePath, content, 'utf-8')
})
}
export const watchNewMarkdown = (app: App, watchers): void => {
const watcher = chokidar.watch(['**/*.md', '!README.md', '!readme.md'], {
cwd: app.options.source,
ignoreInitial: true,
})
watcher.on('add', (filePath, stat) => {
const basename = path.basename(filePath)
const extname = path.extname(basename)
const name = basename.replace(extname, '')
filePath = path.join(app.options.source, filePath)
if (extname !== '.md' && extname !== '.MD') return
if (/readme/i.test(name)) return
stat = stat || fs.statSync(filePath)
const file: MarkdownFile = {
filePath,
content: '',
createTime:
stat.birthtime.getFullYear() !== 1970 ? stat.birthtime : stat.atime,
}
const content = formatMarkdown(file)
fs.writeFileSync(filePath, content, 'utf-8')
})
watchers.push(watcher)
}

5
src/node/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { blogTheme } from './theme'
export * from './theme'
export default blogTheme

View File

@ -0,0 +1,78 @@
import type { App, Page } from '@vuepress/core'
import * as chokidar from 'chokidar'
import * as dayjs from 'dayjs'
import type {
PlumeThemePageData,
PlumeThemePostPageFrontmatter,
PostIndex,
} from '../shared'
import { pageFilter } from './utils/pageFilter'
const HMR_CODE = `
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
if (__VUE_HMR_RUNTIME__.updatePostIndex) {
__VUE_HMR_RUNTIME__.updatePostIndex(postIndex)
}
}
if (import.meta.hot) {
import.meta.hot.accept(({ postIndex, ...other }) => {
__VUE_HMR_RUNTIME__.updatePostIndex(postIndex)
console.log('other', other)
})
}
`
export const preparedPostIndex = async function (app: App): Promise<string> {
const postListIndex: PostIndex = (app.pages as Page<PlumeThemePageData>[])
.filter((page) => {
return pageFilter(page)
})
.map((page) => {
return {
title: page.title,
path: page.path,
frontmatter:
page.frontmatter as unknown as PlumeThemePostPageFrontmatter,
excerpt: page.excerpt,
category: page.data.category || [],
}
})
.sort((left, right) => {
const leftTime = dayjs(left.frontmatter.createTime).unix()
const rightTime = dayjs(right.frontmatter.createTime).unix()
return leftTime < rightTime ? 1 : -1
})
const topPostIndex = postListIndex.findIndex((post) => !!post.frontmatter.top)
if (topPostIndex !== -1) {
postListIndex.unshift(postListIndex.splice(topPostIndex, 1)[0])
}
let content = `
export const postIndex = ${JSON.stringify(postListIndex, null, 2)}
`
// inject HMR code
if (app.env.isDev) {
content += HMR_CODE
}
return app.writeTemp('internal/postIndex.js', content)
}
export const watchPostIndex = (app: App, watchers): void => {
const watcher = chokidar.watch('pages/**/*', {
cwd: app.dir.temp(),
ignoreInitial: true,
})
watcher.on('add', () => {
preparedPostIndex(app)
})
watcher.on('change', () => {
preparedPostIndex(app)
})
watcher.on('unlink', () => {
preparedPostIndex(app)
})
watchers.push(watcher)
}

101
src/node/theme.ts Normal file
View File

@ -0,0 +1,101 @@
import type { Theme, ThemeConfig } from '@vuepress/core'
import { fs, path } from '@vuepress/utils'
import { createBlogPage } from './createBlogPage'
import { extendsPage } from './extendsPage'
import { globFormatFrontmatter, watchNewMarkdown } from './formatFrontmatter'
import { preparedPostIndex, watchPostIndex } from './preparedPostIndex'
import { resolveActiveHeaderLinksPluginOptions } from './utils'
export interface BlogThemeOption extends ThemeConfig {
a?: string
}
export const blogTheme: Theme<BlogThemeOption> = (
{ themePlugins = {}, ...localeOptions },
app
) => {
if (app.options.bundler.endsWith('vite')) {
// eslint-disable-next-line import/no-extraneous-dependencies
app.options.bundlerConfig.viteOptions = require('vite').mergeConfig(
app.options.bundlerConfig.viteOptions,
{
css: {
postcss: {
plugins: [
// require('postcss-simple-vars'),
require('postcss-each'),
require('postcss-import')({
plugins: [
require('postcss-at-rules-variables'),
require('postcss-import'),
],
}),
require('tailwindcss/nesting'),
require('tailwindcss')(
path.resolve(__dirname, '../../tailwind.config.js')
),
require('postcss-preset-env')({
stage: 0,
features: {
'nesting-rules': false,
'custom-media-queries': true,
},
}),
require('autoprefixer'),
],
},
},
}
)
}
globFormatFrontmatter(app.options.source)
return {
name: '@pengzhanbo/vuepress-theme-blog',
templateBuild: path.resolve(__dirname, '../template/index.build.html'),
layouts: path.resolve(__dirname, '../client/layouts'),
clientAppEnhanceFiles: path.resolve(
__dirname,
'../client/clientAppEnhanceFiles.js'
),
clientAppSetupFiles: path.resolve(__dirname, '../client/clientAppSetup.js'),
// use alias to make all components replaceable
alias: Object.fromEntries(
fs
.readdirSync(path.resolve(__dirname, '../client/component'))
.filter((file) => file.endsWith('.vue'))
.map((file) => [
`@theme/${file}`,
path.resolve(__dirname, '../client/component', file),
])
),
onInitialized: async (app) => {
await createBlogPage(app)
},
onPrepared: (app) => {
preparedPostIndex(app)
},
onWatched: (app, watchers) => {
watchPostIndex(app, watchers)
watchNewMarkdown(app, watchers)
},
extendsPage,
plugins: [
[
'@vuepress/active-header-links',
resolveActiveHeaderLinksPluginOptions(themePlugins),
],
['@vuepress/prismjs', themePlugins.prismjs !== false],
['@vuepress/nprogress', themePlugins.nprogress !== false],
[
'@vuepress/medium-zoom',
{
selector: '.post-content > img, .post-content :not(a) > img',
zoomOptions: {},
// should greater than page transition duration
delay: 300,
},
],
['@vuepress/theme-data', { themeData: localeOptions }],
],
}
}

1
src/node/utils/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from './resolveActiveHeaderLinksPluginOptions';

1
src/node/utils/index.js Normal file
View File

@ -0,0 +1 @@
export * from './resolveActiveHeaderLinksPluginOptions';

1
src/node/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './resolveActiveHeaderLinksPluginOptions'

View File

@ -0,0 +1,5 @@
import type { Page } from '@vuepress/core'
export function pageFilter(page: Page): boolean {
return !!page.pathInferred && !!page.filePath && !page.frontmatter.home
}

View File

@ -0,0 +1,37 @@
import { fs, path } from '@vuepress/utils'
export interface MarkdownFile {
filePath: string
content: string
createTime: Date
}
export function readFileList(
sourceDir: string,
fileList: MarkdownFile[] = []
): MarkdownFile[] {
const files = fs.readdirSync(sourceDir)
files.forEach((file) => {
const filePath = path.join(sourceDir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
if (file !== '.vuepress') readFileList(filePath, fileList)
} else {
const extname = path.extname(file)
const basename = path.basename(file).replace(extname, '')
if (
(extname === '.md' || extname === '.MD') &&
basename !== 'README' &&
basename !== 'readme'
) {
fileList.push({
filePath,
content: fs.readFileSync(filePath, 'utf-8'),
createTime:
stat.birthtime.getFullYear() !== 1970 ? stat.birthtime : stat.atime,
})
}
}
})
return fileList
}

View File

@ -0,0 +1,20 @@
import type { ActiveHeaderLinksPluginOptions } from '@vuepress/plugin-active-header-links'
// import type { DefaultThemePluginsOptions } from '../../shared'
/**
* Resolve options for @vuepress/plugin-active-header-links
*/
export const resolveActiveHeaderLinksPluginOptions = (
themePlugins
): ActiveHeaderLinksPluginOptions | boolean => {
if (themePlugins?.activeHeaderLinks === false) {
return false
}
return {
headerLinkSelector: 'a.sidebar-item',
headerAnchorSelector: '.header-anchor',
// should greater than page transition duration
delay: 300,
}
}

5
src/shared/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './nav'
export * from './options'
export * from './page'
export * from './post'
export * from './navbar'

57
src/shared/nav.ts Normal file
View File

@ -0,0 +1,57 @@
/**
* Base nav item, displayed as text
*/
export interface NavItem {
text: string
ariaLabel?: string
}
/**
* Base nav group, has nav items children
*/
export interface NavGroup<T> extends NavItem {
children: T[]
}
/**
* Props for `<AutoLink>`
*/
export interface NavLink extends NavItem {
link: string
rel?: string
target?: string
activeMatch?: string
}
/**
* Navbar types
*/
// user config
export type NavbarItem = NavLink
export type NavbarGroup = NavGroup<NavbarGroup | NavbarItem | string>
export type NavbarConfig = (NavbarItem | NavbarGroup | string)[]
// resolved
export type ResolvedNavbarItem = NavbarItem | NavGroup<ResolvedNavbarItem>
/**
* Sidebar types
*/
// user config
export type SidebarItem = NavItem & Partial<NavLink>
export type SidebarGroup = SidebarItem &
NavGroup<SidebarItem | SidebarGroup | string>
export type SidebarGroupCollapsible = SidebarGroup & {
collapsible?: boolean
}
export type SidebarConfigArray = (
| SidebarItem
| SidebarGroupCollapsible
| string
)[]
export type SidebarConfigObject = Record<string, SidebarConfigArray>
export type SidebarConfig = SidebarConfigArray | SidebarConfigObject
// resolved
export type ResolvedSidebarItem = SidebarItem &
Partial<NavGroup<ResolvedSidebarItem>> & {
collapsible?: boolean
}

11
src/shared/navbar.ts Normal file
View File

@ -0,0 +1,11 @@
export interface NavbarItemOption {
label: string
link: string
frontmatter: Record<string, any>
}
export const navbarList: NavbarItemOption[] = [
{ label: '栏目', link: '/category/', frontmatter: { pageType: 'category' } },
{ label: '标签', link: '/tags/', frontmatter: { pageType: 'tags' } },
{ label: '归档', link: '/archives/', frontmatter: { pageType: 'archives' } },
]

283
src/shared/options.ts Normal file
View File

@ -0,0 +1,283 @@
import type { ThemeData } from '@vuepress/plugin-theme-data'
import type { LocaleData } from '@vuepress/shared'
import type { NavbarConfig, SidebarConfig } from './nav'
export interface PlumeThemePluginsOptions {
bannerImg: string
avatarUrl: string
avatar: string
github?: string
email?: string
description: string
/**
* Enable @vuepress/plugin-active-header-links or not
*/
// activeHeaderLinks?: boolean
/**
* Enable @vuepress/plugin-back-to-top or not
*/
// backToTop?: boolean
/**
* Enable @vuepress/plugin-container or not
*/
// container?: {
// tip?: boolean
// warning?: boolean
// danger?: boolean
// details?: boolean
// codeGroup?: boolean
// codeGroupItem?: boolean
// }
/**
* Enable @vuepress/plugin-external-link-icon or not
*/
// externalLinkIcon?: boolean
/**
* Enable @vuepress/plugin-git or not
*/
// git?: boolean
/**
* Enable @vuepress/plugin-medium-zoom or not
*/
// mediumZoom?: boolean
/**
* Enable @vuepress/plugin-nprogress or not
*/
// nprogress?: boolean
/**
* Enable @vuepress/plugin-prismjs or not
*/
// prismjs?: boolean
}
export type PlumeThemeLocaleOptions = PlumeThemeData
export type PlumeThemeData = ThemeData<PlumeThemeLocaleData>
export interface PlumeThemeLocaleData extends LocaleData {
/**
* Home path of current locale
*
* Used as the link of back-to-home and navbar logo
*/
home?: string
/**
* Navbar config
*
* Set to `false` to disable navbar in current locale
*/
navbar?: false | NavbarConfig
/**
* Navbar logo config
*
* Logo to display in navbar
*/
logo?: null | string
/**
* Navbar logo config for dark mode
*
* Logo to display in navbar in dark mode
*/
logoDark?: null | string
/**
* Navbar dark mode button config
*
* Enable dark mode switching and display a button in navbar or not
*/
darkMode?: boolean
/**
* Navbar repository config
*
* Used for the repository link of navbar
*/
repo?: null | string
/**
* Navbar repository config
*
* Used for the repository text of navbar
*/
repoLabel?: string
/**
* Navbar language selection config
*
* Text of the language selection dropdown
*/
selectLanguageText?: string
/**
* Navbar language selection config
*
* Aria label of of the language selection dropdown
*/
selectLanguageAriaLabel?: string
/**
* Navbar language selection config
*
* Language name of current locale
*
* Displayed inside the language selection dropdown
*/
selectLanguageName?: string
/**
* Sidebar config
*
* Set to `false` to disable sidebar in current locale
*/
sidebar?: 'auto' | false | SidebarConfig
/**
* Sidebar depth
*
* - Set to `0` to disable all levels
* - Set to `1` to include `<h2>`
* - Set to `2` to include `<h2>` and `<h3>`
* - ...
*
* The max value depends on which headers you have extracted
* via `markdown.extractHeaders.level`.
*
* The default value of `markdown.extractHeaders.level` is `[2, 3]`,
* so the default max value of `sidebarDepth` is `2`
*/
sidebarDepth?: number
/**
* Page meta - edit link config
*
* Whether to show "Edit this page" or not
*/
editLink?: boolean
/**
* Page meta - edit link config
*
* The text to replace the default "Edit this page"
*/
editLinkText?: string
/**
* Page meta - edit link config
*
* Pattern of edit link
*
* @example ':repo/edit/:branch/:path'
*/
editLinkPattern?: string
/**
* Page meta - edit link config
*
* Use `repo` config by default
*
* Set this config if your docs is placed in a different repo
*/
docsRepo?: string
/**
* Page meta - edit link config
*
* Set this config if the branch of your docs is not 'main'
*/
docsBranch?: string
/**
* Page meta - edit link config
*
* Set this config if your docs is placed in sub dir of your `docsRepo`
*/
docsDir?: string
/**
* Page meta - last updated config
*
* Whether to show "Last Updated" or not
*/
lastUpdated?: boolean
/**
* Page meta - last updated config
*
* The text to replace the default "Last Updated"
*/
lastUpdatedText?: string
/**
* Page meta - contributors config
*
* Whether to show "Contributors" or not
*/
contributors?: boolean
/**
* Page meta - contributors config
*
* The text to replace the default "Contributors"
*/
contributorsText?: string
/**
* Custom block config
*
* Default title of TIP custom block
*/
tip?: string
/**
* Custom block config
*
* Default title of WARNING custom block
*/
warning?: string
/**
* Custom block config
*
* Default title of DANGER custom block
*/
danger?: string
/**
* 404 page config
*
* Not Found messages for 404 page
*/
notFound?: string[]
/**
* 404 page config
*
* Text of back-to-home link in 404 page
*/
backToHome?: string
/**
* A11y text for external link icon
*/
openInNewWindow?: string
/**
* A11y text for dark mode toggle button
*/
toggleDarkMode?: string
/**
* A11y text for sidebar toggle button
*/
toggleSidebar?: string
}

41
src/shared/page.ts Normal file
View File

@ -0,0 +1,41 @@
import type { GitPluginPageData } from '@vuepress/plugin-git'
import type { NavLink, SidebarConfig } from './nav'
import type { CategoryItem } from './post'
export interface PlumeThemePageData extends GitPluginPageData {
filePathRelative: string | null
category: CategoryItem[]
sort: number
isPost: boolean
}
export interface PlumeThemePageFrontmatter {
home?: boolean
navbar?: boolean
pageClass?: string
pageType?: string
}
export interface PlumeThemeHomePageFrontmatter
extends PlumeThemePageFrontmatter {
home: true
}
export interface PlumeThemePostPageFrontmatter
extends PlumeThemePageFrontmatter {
home?: false
editLink?: boolean
editLinkPattern?: string
lastUpdated?: boolean
contributors?: boolean
sidebar?: 'auto' | false | SidebarConfig
sidebarDepth?: number
prev?: string | NavLink
next?: string | NavLink
createTime: string
author: string
top: boolean
type: string
sort: number | string
tags: string[]
}

17
src/shared/post.ts Normal file
View File

@ -0,0 +1,17 @@
import type { PageFrontmatter } from 'vuepress'
import type { PlumeThemePostPageFrontmatter } from './page'
export interface CategoryItem {
type: number | string
name: string
}
export interface PostItemIndex {
title: string
path: string
frontmatter: PageFrontmatter<PlumeThemePostPageFrontmatter>
excerpt: string
category: CategoryItem[]
}
export type PostIndex = PostItemIndex[]

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="generator" content="VuePress {{ version }}">
<style>
:root {
--c-bg: rgba(0, 0, 0, 0.15);
}
html.dark {
--c-bg: #22272e;
}
html, body {
background-color: var(--c-bg);
}
</style>
<!-- <script>
const userMode = localStorage.getItem('vuepress-color-scheme');
const systemDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
if (userMode === 'dark' || (userMode !== 'light' && systemDarkMode)) {
document.documentElement.classList.toggle('dark', true);
}
</script> -->
<!--vuepress-ssr-head-->
<!--vuepress-ssr-resources-->
<!--vuepress-ssr-styles-->
</head>
<body>
<div id="app"><!--vuepress-ssr-app--></div>
<!--vuepress-ssr-scripts-->
</body>
</html>

10
tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
const path = require('path')
module.exports = {
content: ['./lib/**/*.{vue,html,css,ts,tsx,js,jsx}'].map((_) =>
path.join(__dirname, _)
),
theme: {
extend: {},
},
plugins: [require('@tailwindcss/typography')],
}

18
tsconfig.base.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": false,
"lib": ["DOM", "ES2020"],
"moduleResolution": "node",
"newLine": "lf",
"noEmitOnError": true,
"noImplicitAny": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": false,
"strict": true,
"strictNullChecks": true,
"target": "ES2018"
}
}

8
tsconfig.build.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.base.json",
"references": [
{ "path": "./tsconfig.esm.json" },
{ "path": "./tsconfig.cjs.json" }
],
"files": []
}

9
tsconfig.cjs.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src/node", "./src/shared"]
}

15
tsconfig.dev.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.base.json",
"references": [
{ "path": "./tsconfig.esm.json" },
{ "path": "./tsconfig.cjs.json" }
],
"files": [],
"watchOptions": {
"watchFile": "useFsEvents",
"watchDirectory": "useFsEvents",
"fallbackPolling": "dynamicPriority",
"synchronousWatchDirectory": true,
"excludeDirectories": ["**/node_modules", "./lib", "./example"],
}
}

14
tsconfig.esm.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "ES2020",
"rootDir": "./src",
"outDir": "./lib",
"types": ["@vuepress/client/types", "webpack-env", "vite/client"],
"paths": {
"@theme/*": ["./src/client/component/*"],
"@internal/*": ["./example/.vuepress/.temp/internal/*"],
},
},
"include": ["./src/client", "./src/shared"]
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"module": "ES2020",
"types": ["@vuepress/client/types", "webpack-env", "vite/client"],
"paths": {
"@theme/*": ["./src/client/component/*"],
"@internal/*": ["./example/.vuepress/.temp/internal/*"],
},
},
"include": ["./src/**/*", "./vuepress.config.ts"],
"exclude": ["node_modules", ".temp", "lib", "dist"]
}

22
vuepress.config.ts Normal file
View File

@ -0,0 +1,22 @@
import * as path from 'path'
import { defineUserConfig } from 'vuepress'
import type { DefaultThemeOptions } from 'vuepress'
export default defineUserConfig<DefaultThemeOptions>({
lang: 'zh',
title: '示例博客',
description: '热爱生活',
dest: 'docs',
temp: 'example/.vuepress/.temp',
cache: 'example/.vuepress/.cache',
public: 'example/public',
theme: path.resolve(__dirname, './lib/node/index.js'),
themeConfig: {
bannerImg: '/big-banner.jpg', // 1200x300
avatarUrl: '/avatar.gif',
avatar: '未闻花名',
github: 'https://github.com/',
email: '_@outlook.com',
description: '学习,生活,娱乐,我全都要',
},
})

3579
yarn.lock Normal file

File diff suppressed because it is too large Load Diff