mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
initial commit
This commit is contained in:
commit
5c79656269
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
7
.eslintignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
.temp/
|
||||||
|
lib/
|
||||||
|
dist/
|
||||||
|
!.vuepress/
|
||||||
|
!.*.js
|
||||||
|
example/
|
||||||
68
.eslintrc.js
Normal file
68
.eslintrc.js
Normal 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
10
.gitattributes
vendored
Normal 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
25
.gitignore
vendored
Normal 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
8
.npmignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/example
|
||||||
|
/node_modules
|
||||||
|
/src
|
||||||
|
.editorconfig
|
||||||
|
.eslint*
|
||||||
|
.git*
|
||||||
|
**/*.tsbuildinfo
|
||||||
|
vuepress.config.js
|
||||||
14
example/1.前端/1.React/组件.md
Normal file
14
example/1.前端/1.React/组件.md
Normal 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
|
||||||
14
example/1.前端/2.Vue/组件.md
Normal file
14
example/1.前端/2.Vue/组件.md
Normal 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
|
||||||
|
---
|
||||||
|
|
||||||
|
# 组件
|
||||||
|
|
||||||
|
组件内容 组件 组件
|
||||||
265
example/1.前端/正则表达式.md
Normal file
265
example/1.前端/正则表达式.md
Normal 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/>当m,n值为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
|
||||||
|
```
|
||||||
12
example/2.学习笔记/typescript学习.md
Normal file
12
example/2.学习笔记/typescript学习.md
Normal 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 学习
|
||||||
10
example/2.学习笔记/呵呵.md
Normal file
10
example/2.学习笔记/呵呵.md
Normal 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
|
||||||
|
---
|
||||||
12
example/2.学习笔记/哈哈.md
Normal file
12
example/2.学习笔记/哈哈.md
Normal 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
|
||||||
|
---
|
||||||
|
|
||||||
17
example/3.杂谈/电影杂谈.md
Normal file
17
example/3.杂谈/电影杂谈.md
Normal 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
3
example/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
---
|
||||||
BIN
example/public/avatar.gif
Normal file
BIN
example/public/avatar.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
example/public/big-banner.jpg
Normal file
BIN
example/public/big-banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
82
package.json
Normal file
82
package.json
Normal 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
81
readme.md
Normal 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 和 本主题
|
||||||
7
src/client/clientAppEnhanceFiles.ts
Normal file
7
src/client/clientAppEnhanceFiles.ts
Normal 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
|
||||||
|
})
|
||||||
6
src/client/clientAppSetup.ts
Normal file
6
src/client/clientAppSetup.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { defineClientAppSetup } from '@vuepress/client'
|
||||||
|
// import { setupDarkMode } from './composables'
|
||||||
|
import './styles/index.css'
|
||||||
|
export default defineClientAppSetup(() => {
|
||||||
|
// setupDarkMode()
|
||||||
|
})
|
||||||
32
src/client/component/Archives.vue
Normal file
32
src/client/component/Archives.vue
Normal 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>
|
||||||
18
src/client/component/Category.vue
Normal file
18
src/client/component/Category.vue
Normal 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>
|
||||||
57
src/client/component/CategoryGroup.vue
Normal file
57
src/client/component/CategoryGroup.vue
Normal 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>
|
||||||
21
src/client/component/Home.vue
Normal file
21
src/client/component/Home.vue
Normal 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>
|
||||||
57
src/client/component/Icon.vue
Normal file
57
src/client/component/Icon.vue
Normal 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>
|
||||||
41
src/client/component/NavBar.vue
Normal file
41
src/client/component/NavBar.vue
Normal 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>
|
||||||
16
src/client/component/Post.vue
Normal file
16
src/client/component/Post.vue
Normal 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>
|
||||||
41
src/client/component/PostList.vue
Normal file
41
src/client/component/PostList.vue
Normal 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>
|
||||||
64
src/client/component/PostMeta.vue
Normal file
64
src/client/component/PostMeta.vue
Normal 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>
|
||||||
35
src/client/component/RightSideBar.vue
Normal file
35
src/client/component/RightSideBar.vue
Normal 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>
|
||||||
40
src/client/component/TageSidebar.vue
Normal file
40
src/client/component/TageSidebar.vue
Normal 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>
|
||||||
14
src/client/component/Tags.vue
Normal file
14
src/client/component/Tags.vue
Normal 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>
|
||||||
57
src/client/component/ToggleDarkModeButton.vue
Normal file
57
src/client/component/ToggleDarkModeButton.vue
Normal 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>
|
||||||
24
src/client/component/ToggleSidebarButton.vue
Normal file
24
src/client/component/ToggleSidebarButton.vue
Normal 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>
|
||||||
7
src/client/composables/index.ts
Normal file
7
src/client/composables/index.ts
Normal 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'
|
||||||
34
src/client/composables/useArchives.ts
Normal file
34
src/client/composables/useArchives.ts
Normal 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
|
||||||
61
src/client/composables/useCategoryList.ts
Normal file
61
src/client/composables/useCategoryList.ts
Normal 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
|
||||||
66
src/client/composables/useDarkMode.ts
Normal file
66
src/client/composables/useDarkMode.ts
Normal 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())
|
||||||
|
}
|
||||||
23
src/client/composables/useNavLink.ts
Normal file
23
src/client/composables/useNavLink.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/client/composables/usePostIndex.ts
Normal file
16
src/client/composables/usePostIndex.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/client/composables/useResolveRouteWithRedirect.ts
Normal file
28
src/client/composables/useResolveRouteWithRedirect.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
22
src/client/composables/useScrollPromise.ts
Normal file
22
src/client/composables/useScrollPromise.ts
Normal 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
|
||||||
179
src/client/composables/useSidebarItems.ts
Normal file
179
src/client/composables/useSidebarItems.ts
Normal 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)
|
||||||
|
}
|
||||||
34
src/client/composables/useTages.ts
Normal file
34
src/client/composables/useTages.ts
Normal 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
|
||||||
14
src/client/composables/useThemeData.ts
Normal file
14
src/client/composables/useThemeData.ts
Normal 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>()
|
||||||
8
src/client/layouts/404.vue
Normal file
8
src/client/layouts/404.vue
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useSiteData } from '@vuepress/client'
|
||||||
|
console.log(useSiteData())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>404</div>
|
||||||
|
</template>
|
||||||
31
src/client/layouts/Layout.vue
Normal file
31
src/client/layouts/Layout.vue
Normal 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
5
src/client/shim.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { ComponentOptions } from 'vue'
|
||||||
|
const comp: ComponentOptions
|
||||||
|
export default comp
|
||||||
|
}
|
||||||
34
src/client/styles/archives.css
Normal file
34
src/client/styles/archives.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/client/styles/arrow.css
Normal file
24
src/client/styles/arrow.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
52
src/client/styles/category.css
Normal file
52
src/client/styles/category.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/client/styles/code-group.css
Normal file
69
src/client/styles/code-group.css
Normal 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
265
src/client/styles/code.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/client/styles/home.css
Normal file
40
src/client/styles/home.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
14
src/client/styles/index.css
Normal file
14
src/client/styles/index.css
Normal 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';
|
||||||
7
src/client/styles/navBar.css
Normal file
7
src/client/styles/navBar.css
Normal 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
177
src/client/styles/normalize.css
vendored
Normal 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);
|
||||||
|
}
|
||||||
57
src/client/styles/post.css
Normal file
57
src/client/styles/post.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/client/styles/tags.css
Normal file
35
src/client/styles/tags.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/client/styles/tailwind.css
Normal file
3
src/client/styles/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
46
src/client/styles/vars-dark.css
Normal file
46
src/client/styles/vars-dark.css
Normal 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
134
src/client/styles/vars.css
Normal 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
3
src/client/utils/index.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './isActiveSidebarItem';
|
||||||
|
export * from './resolveEditLink';
|
||||||
|
export * from './resolveRepoType';
|
||||||
3
src/client/utils/index.js
Normal file
3
src/client/utils/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './isActiveSidebarItem';
|
||||||
|
export * from './resolveEditLink';
|
||||||
|
export * from './resolveRepoType';
|
||||||
3
src/client/utils/index.ts
Normal file
3
src/client/utils/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './isActiveSidebarItem'
|
||||||
|
export * from './resolveEditLink'
|
||||||
|
export * from './resolveRepoType'
|
||||||
34
src/client/utils/isActiveSidebarItem.ts
Normal file
34
src/client/utils/isActiveSidebarItem.ts
Normal 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
|
||||||
|
}
|
||||||
64
src/client/utils/resolveEditLink.ts
Normal file
64
src/client/utils/resolveEditLink.ts
Normal 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}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/client/utils/resolveRepoType.ts
Normal file
11
src/client/utils/resolveRepoType.ts
Normal 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
|
||||||
|
}
|
||||||
18
src/node/createBlogPage.ts
Normal file
18
src/node/createBlogPage.ts
Normal 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
23
src/node/extendsPage.ts
Normal 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+\./, '')
|
||||||
|
}
|
||||||
99
src/node/formatFrontmatter.ts
Normal file
99
src/node/formatFrontmatter.ts
Normal 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
5
src/node/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { blogTheme } from './theme'
|
||||||
|
|
||||||
|
export * from './theme'
|
||||||
|
|
||||||
|
export default blogTheme
|
||||||
78
src/node/preparedPostIndex.ts
Normal file
78
src/node/preparedPostIndex.ts
Normal 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
101
src/node/theme.ts
Normal 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
1
src/node/utils/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './resolveActiveHeaderLinksPluginOptions';
|
||||||
1
src/node/utils/index.js
Normal file
1
src/node/utils/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './resolveActiveHeaderLinksPluginOptions';
|
||||||
1
src/node/utils/index.ts
Normal file
1
src/node/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './resolveActiveHeaderLinksPluginOptions'
|
||||||
5
src/node/utils/pageFilter.ts
Normal file
5
src/node/utils/pageFilter.ts
Normal 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
|
||||||
|
}
|
||||||
37
src/node/utils/readFileList.ts
Normal file
37
src/node/utils/readFileList.ts
Normal 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
|
||||||
|
}
|
||||||
20
src/node/utils/resolveActiveHeaderLinksPluginOptions.ts
Normal file
20
src/node/utils/resolveActiveHeaderLinksPluginOptions.ts
Normal 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
5
src/shared/index.ts
Normal 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
57
src/shared/nav.ts
Normal 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
11
src/shared/navbar.ts
Normal 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
283
src/shared/options.ts
Normal 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
41
src/shared/page.ts
Normal 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
17
src/shared/post.ts
Normal 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[]
|
||||||
33
src/template/index.build.html
Normal file
33
src/template/index.build.html
Normal 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
10
tailwind.config.js
Normal 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
18
tsconfig.base.json
Normal 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
8
tsconfig.build.json
Normal 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
9
tsconfig.cjs.json
Normal 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
15
tsconfig.dev.json
Normal 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
14
tsconfig.esm.json
Normal 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
14
tsconfig.json
Normal 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
22
vuepress.config.ts
Normal 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: '学习,生活,娱乐,我全都要',
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user