commit
aa65b124d7
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -18,6 +18,7 @@
|
||||
"stylelint.packageManager": "pnpm",
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "format/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
@ -27,11 +28,12 @@
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
"editor.formatOnSave": false,
|
||||
"prettier.enable": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.stylelint": "explicit",
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
// "source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
"editor.formatOnPaste": true,
|
||||
"eslint.validate": [
|
||||
|
||||
@ -21,6 +21,10 @@ export default defineUserConfig({
|
||||
|
||||
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules'],
|
||||
|
||||
markdown: {
|
||||
code: false,
|
||||
},
|
||||
|
||||
bundler: viteBundler(),
|
||||
|
||||
theme,
|
||||
|
||||
@ -97,7 +97,7 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
text: '内置插件',
|
||||
dir: 'plugins',
|
||||
collapsed: false,
|
||||
items: ['', '代码复制', '代码高亮', '搜索', '阅读统计', 'markdown增强', 'markdownPower', '百度统计'],
|
||||
items: ['', '代码高亮', '搜索', '阅读统计', 'markdown增强', 'markdownPower', '百度统计'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -107,7 +107,6 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
sidebar: [
|
||||
{
|
||||
text: '插件',
|
||||
link: '/plugins/',
|
||||
items: [
|
||||
'caniuse',
|
||||
'iconify',
|
||||
@ -117,7 +116,7 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
{
|
||||
text: 'plugin-netlify-functions',
|
||||
dir: 'netlify-functions',
|
||||
link: '/plugins/plugin-netlify-functions/',
|
||||
link: '/plugin-netlify-functions/',
|
||||
items: [
|
||||
'介绍',
|
||||
'使用',
|
||||
@ -137,7 +136,6 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
{
|
||||
text: '工具',
|
||||
icon: 'tabler:tools',
|
||||
link: '/tools/',
|
||||
items: ['home-hero-tint-plate', 'caniuse'],
|
||||
},
|
||||
],
|
||||
|
||||
@ -7,7 +7,7 @@ import { enNavbar, zhNavbar } from './navbar.js'
|
||||
export const theme: Theme = themePlume({
|
||||
logo: '/plume.png',
|
||||
hostname: process.env.SITE_HOST || 'https://plume.pengzhanbo.cn',
|
||||
repo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
|
||||
docsRepo: 'https://github.com/pengzhanbo/vuepress-theme-plume',
|
||||
docsDir: 'docs',
|
||||
|
||||
avatar: {
|
||||
@ -93,7 +93,7 @@ export const theme: Theme = themePlume({
|
||||
repoId: 'R_kgDOG_ebNA',
|
||||
category: 'docs-comment',
|
||||
categoryId: 'DIC_kwDOG_ebNM4Cd0uF',
|
||||
mapping: 'url',
|
||||
mapping: 'pathname',
|
||||
reactionsEnabled: true,
|
||||
inputPosition: 'top',
|
||||
darkTheme: 'dark_protanopia',
|
||||
|
||||
@ -7,6 +7,7 @@ readingTime: false
|
||||
prev: false
|
||||
next: false
|
||||
article: false
|
||||
externalLink: false
|
||||
docs:
|
||||
-
|
||||
name: VuePress Plume
|
||||
|
||||
@ -34,9 +34,50 @@ list:
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
groups:
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
title: 分组 1
|
||||
desc: 自定义颜色
|
||||
list:
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
backgroundColor: rgb(255,153,0)
|
||||
color: rgb(255,255,153)
|
||||
nameColor: rgb(255,255,170)
|
||||
borderColor: rgb(255,255,204)
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
backgroundColor: rgb(255,102,102)
|
||||
color: rgb(255,204,204)
|
||||
nameColor: rgb(255,238,238)
|
||||
borderColor: rgb(255,255,238)
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
backgroundColor: rgb(0,153,204)
|
||||
color: rgb(153,238,255)
|
||||
nameColor: rgb(153,255,255)
|
||||
borderColor: rgb(153,238,255)
|
||||
-
|
||||
title: 分组 2
|
||||
desc: 这里是分组 2 的描述文字
|
||||
list:
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
-
|
||||
name: pengzhanbo
|
||||
link: https://github.com/pengzhanbo
|
||||
avatar: https://github.com/pengzhanbo.png
|
||||
desc: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
|
||||
---
|
||||
|
||||
@ -71,5 +71,46 @@ interface FriendsItem {
|
||||
* 友情链接描述
|
||||
*/
|
||||
desc?: string
|
||||
|
||||
/**
|
||||
* 背景色
|
||||
*/
|
||||
backgroundColor?: string | { light: string, dark: string }
|
||||
/**
|
||||
* 字体颜色
|
||||
*/
|
||||
color?: string | { light: string, dark: string }
|
||||
/**
|
||||
* 名字颜色
|
||||
*/
|
||||
nameColor?: string | { light: string, dark: string }
|
||||
/**
|
||||
* 边框颜色
|
||||
*/
|
||||
borderColor?: string | { light: string, dark: string }
|
||||
}
|
||||
```
|
||||
|
||||
### groups
|
||||
|
||||
- 类型: `FriendsGroup[]`
|
||||
- 默认值: `[]`
|
||||
|
||||
友情链接分组
|
||||
|
||||
```ts
|
||||
interface FriendsGroup {
|
||||
/**
|
||||
* 分组名
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* 分组描述
|
||||
*/
|
||||
desc?: string
|
||||
/**
|
||||
* 友情链接列表
|
||||
*/
|
||||
list?: FriendsItem[]
|
||||
}
|
||||
```
|
||||
|
||||
@ -67,11 +67,61 @@ export default defineUserConfig({
|
||||
|
||||
### defaultHighlightLang
|
||||
|
||||
- 类型: `string`
|
||||
- 默认值: `text`
|
||||
- 类型: `string`
|
||||
- 默认值: `text`
|
||||
|
||||
默认高亮的编程语言。当代码块未指定语言时使用。
|
||||
|
||||
### lineNumbers
|
||||
|
||||
- 类型:`boolean | number`
|
||||
- 默认值: `true`
|
||||
|
||||
是否显示行号。
|
||||
|
||||
`true`: 显示行号\
|
||||
`false`: 不显示行号\
|
||||
`number`: 指定需要显式代码行号的最小行数。
|
||||
|
||||
### copyCode
|
||||
|
||||
- 类型: `boolean | CopyCodeOptions`
|
||||
- 默认值: `true`
|
||||
|
||||
是否允许复制代码。启用时,会在代码块右侧显示复制按钮。
|
||||
|
||||
```ts
|
||||
interface CopyCodeOptions {
|
||||
/**
|
||||
* 复制成功后提示文本持续时间
|
||||
*
|
||||
* @default 2000
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* 多语言配置
|
||||
*/
|
||||
locales?: {
|
||||
[localePath: string]: {
|
||||
/**
|
||||
* 复制按钮标题
|
||||
*
|
||||
* @default 'Copy code'
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* 复制成功提示
|
||||
*
|
||||
* @default 'Copied'
|
||||
*/
|
||||
copied?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### codeTransformers
|
||||
|
||||
- 类型: `ShikiTransformer[]`
|
||||
|
||||
@ -116,8 +116,8 @@ github: :[tdesign:logo-github-filled]:
|
||||
为了满足这种小小的心思,主题提供了一个 **“隐秘”文本** 的有趣小功能。它看起来像这样:
|
||||
|
||||
:::demo-wrapper
|
||||
你知道吗, =|鲁迅|= 曾说过:“ =|我没说过这句话!|= ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,=|我在床上翻了个身|= !
|
||||
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,!!我在床上翻了个身!! !
|
||||
:::
|
||||
|
||||
读者不能直接阅读到完整的内容,部分的内容被 “遮住”,需要鼠标悬停到内容上,才能看到被遮住的内容。
|
||||
@ -148,7 +148,7 @@ export default defineUserConfig({
|
||||
```ts
|
||||
interface PlotOptions {
|
||||
/**
|
||||
* 是否启用 `=| |=` markdown (该标记为非标准标记,脱离插件将不生效)
|
||||
* 是否启用 `!! !!` markdown (该标记为非标准标记,脱离插件将不生效)
|
||||
* 如果设置为 false, 则表示不启用该标记,只能使用 <Plot /> 组件
|
||||
* @default true
|
||||
*/
|
||||
@ -176,10 +176,10 @@ interface PlotOptions {
|
||||
### 语法
|
||||
|
||||
```md
|
||||
=|需要隐秘的内容|=
|
||||
!!需要隐秘的内容!!
|
||||
```
|
||||
|
||||
如果不想使用 非标准的 `=||=` 标记语法,你可以将 `plot.tag` 设置为 `false` ,
|
||||
如果不想使用 非标准的 `!! !!` 标记语法,你可以将 `plot.tag` 设置为 `false` ,
|
||||
然后使用 [`<Plot />`](/guide/features/component/#plot) 组件替代。
|
||||
|
||||
### 示例
|
||||
@ -187,15 +187,15 @@ interface PlotOptions {
|
||||
输入:
|
||||
|
||||
```md
|
||||
你知道吗, =|鲁迅|= 曾说过:“ =|我没说过这句话!|= ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,=|我在床上翻了个身|= !
|
||||
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,!!我在床上翻了个身!! !
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
:::demo-wrapper
|
||||
你知道吗, =|鲁迅|= 曾说过:“ =|我没说过这句话!|= ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,=|我在床上翻了个身|= !
|
||||
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
|
||||
力量!于是,!!我在床上翻了个身!! !
|
||||
:::
|
||||
|
||||
## 选项组
|
||||
|
||||
@ -8,6 +8,52 @@ permalink: /guide/code/features/
|
||||
|
||||
主题在代码高亮功能上,进一步支持了更多的特性,它们能够帮助你的代码块更加具备表达力。
|
||||
|
||||
## 代码行号
|
||||
|
||||
主题默认显示代码行号,它通过 `plugins.shiki.line-numbers` 来控制。
|
||||
|
||||
```ts
|
||||
export default defineUserConfig({
|
||||
theme: plumeTheme({
|
||||
plugins: {
|
||||
shiki: { lineNumbers: true }
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
你还可以通过 `:line-numbers` / `:no-line-numbers` 来控制当前代码块是否显示代码行号。
|
||||
|
||||
**输入:**
|
||||
|
||||
````
|
||||
```ts:line-numbers
|
||||
// 启用行号
|
||||
const line2 = 'This is line 2'
|
||||
const line3 = 'This is line 3'
|
||||
```
|
||||
|
||||
```ts:no-line-numbers
|
||||
// 行号已禁用
|
||||
const line3 = 'This is line 3'
|
||||
const line4 = 'This is line 4'
|
||||
```
|
||||
````
|
||||
|
||||
**输出:**
|
||||
|
||||
```ts:line-numbers
|
||||
// 启用行号
|
||||
const line2 = 'This is line 2'
|
||||
const line3 = 'This is line 3'
|
||||
```
|
||||
|
||||
```ts:no-line-numbers
|
||||
// 行号已禁用
|
||||
const line3 = 'This is line 3'
|
||||
const line4 = 'This is line 4'
|
||||
```
|
||||
|
||||
## 在代码块中实现行高亮
|
||||
|
||||
在 `[lang]` 之后紧跟随 `{xxxx}` ,可以实现行高亮,其中 `xxx` 表示要高亮的行号。
|
||||
|
||||
@ -28,7 +28,7 @@ permalink: /guide/write/
|
||||
由于文件夹名称将作为分类名称,且不在主题配置中进行排序配置,对于有排序需要的场景,使用以下规则进行命名
|
||||
|
||||
``` ts
|
||||
const dir = /\d+\.[^]+/
|
||||
const dir = /\d+\.[\s\S]+/
|
||||
// 即 数字 + . + 分类名称
|
||||
// 如: 1.前端
|
||||
```
|
||||
|
||||
@ -9,16 +9,16 @@
|
||||
"docs:serve": "anywhere -s -h localhost -d .vuepress/dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/json": "^2.2.208",
|
||||
"@vuepress/bundler-vite": "2.0.0-rc.9",
|
||||
"@iconify/json": "^2.2.214",
|
||||
"@vuepress/bundler-vite": "2.0.0-rc.12",
|
||||
"anywhere": "^1.6.0",
|
||||
"chart.js": "^4.4.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"echarts": "^5.5.0",
|
||||
"flowchart.ts": "^3.0.0",
|
||||
"mermaid": "^10.9.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"vue": "^3.4.27",
|
||||
"vuepress-theme-plume": "workspace:*"
|
||||
},
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import config from '@pengzhanbo/eslint-config-vue'
|
||||
|
||||
export default config({
|
||||
// todo: 正则校验
|
||||
// 当前项目中的 正则 海冰不能完全通过 规则,存在 53 个问题
|
||||
// 但处理起来比较麻烦,因此将会作为一项比较长期的工作来完成。
|
||||
regexp: false,
|
||||
ignores: [
|
||||
'lib',
|
||||
'docs/notes/theme/snippet/code-block.snippet.md',
|
||||
|
||||
19
package.json
19
package.json
@ -3,7 +3,7 @@
|
||||
"type": "module",
|
||||
"version": "1.0.0-rc.56",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.1.0",
|
||||
"packageManager": "pnpm@9.1.2",
|
||||
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@ -41,8 +41,8 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@pengzhanbo/eslint-config-vue": "^1.9.1",
|
||||
"@pengzhanbo/stylelint-config": "^1.9.1",
|
||||
"@pengzhanbo/eslint-config-vue": "^1.10.0",
|
||||
"@pengzhanbo/stylelint-config": "^1.10.0",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "20.12.10",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
@ -52,20 +52,15 @@
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"cpx2": "^7.0.1",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^9.2.0",
|
||||
"eslint": "^9.3.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"stylelint": "^16.5.0",
|
||||
"lint-staged": "^15.2.5",
|
||||
"rimraf": "^5.0.7",
|
||||
"stylelint": "^16.6.0",
|
||||
"tsconfig-vuepress": "^4.5.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@vuepress/markdown@2.0.0-rc.9": "patches/@vuepress__markdown@2.0.0-rc.9.patch"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
|
||||
13
patches/@vuepress__markdown@2.0.0-rc.11.patch
Normal file
13
patches/@vuepress__markdown@2.0.0-rc.11.patch
Normal file
@ -0,0 +1,13 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 7bad660ca70fe8c5873cebd8d237cd2b4257deb9..2c2540df2f278f781e081ac56f038b1e332ec803 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -267,7 +267,7 @@ var codePlugin = (md, {
|
||||
const info = token.info ? md.utils.unescapeAll(token.info).trim() : "";
|
||||
const language = resolveLanguage(info);
|
||||
const languageClass = `${options.langPrefix}${language.name}`;
|
||||
- const code = options.highlight?.(token.content, language.name, "") || md.utils.escapeHtml(token.content);
|
||||
+ const code = options.highlight?.(token.content, language.name, info || "") || md.utils.escapeHtml(token.content);
|
||||
token.attrJoin("class", languageClass);
|
||||
let result = code.startsWith("<pre") ? code : `<pre${slf.renderAttrs(token)}><code>${code}</code></pre>`;
|
||||
const useVPre = resolveVPre(info) ?? vPreBlock;
|
||||
@ -33,7 +33,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pengzhanbo/utils": "^1.1.2",
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "6.6.1",
|
||||
|
||||
@ -22,7 +22,7 @@ if (import.meta.hot) {
|
||||
}
|
||||
`
|
||||
|
||||
const headingRe = /<h(\d*).*?>.*?<\/h\1>/gi
|
||||
const headingRe = /<h(\d)[^>]*>.*?<\/h\1>/gi
|
||||
|
||||
function getTimestamp(time: Date): number {
|
||||
return new Date(time).getTime()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "@vuepress-plume/plugin-caniuse",
|
||||
"type": "module",
|
||||
"version": "1.0.0-rc.56",
|
||||
"private": "true",
|
||||
"description": "The Plugin for VuePres 2, Support Can-I-Use feature",
|
||||
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
||||
"license": "MIT",
|
||||
@ -43,7 +44,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it-container": "^4.0.0"
|
||||
|
||||
@ -14,7 +14,7 @@ export function caniusePlugin({
|
||||
}: CanIUsePluginOptions): Plugin {
|
||||
mode = isMode(mode) ? mode : modeMap[0]
|
||||
const type = 'caniuse'
|
||||
const validateReg = new RegExp(`^${type}\\s+(.*)$`)
|
||||
const validateReg = new RegExp(`^${type}(?:$|\s)`)
|
||||
const pluginObj: PluginObject = {
|
||||
name: '@vuepress-plume/plugin-caniuse',
|
||||
clientConfigFile: path.resolve(__dirname, '../client/clientConfig.js'),
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.27"
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress-plume/plugin-content-update": "workspace:*",
|
||||
|
||||
@ -5,7 +5,7 @@ import type { CopyCodeOptions } from '../shared/index.js'
|
||||
declare const __COPY_CODE_OPTIONS__: CopyCodeOptions
|
||||
|
||||
const options = __COPY_CODE_OPTIONS__
|
||||
const RE_LANGUAGE = /language-([\w]+)/
|
||||
const RE_LANGUAGE = /language-(\w+)/
|
||||
const RE_START_CODE = /^ *(\$|>)/gm
|
||||
const shells = ['shellscript', 'shell', 'bash', 'sh', 'zsh']
|
||||
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@iconify/json": "^2",
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@iconify/json": {
|
||||
@ -47,18 +47,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/utils": "^2.1.23",
|
||||
"@vuepress/helper": "2.0.0-rc.28",
|
||||
"@vuepress/helper": "2.0.0-rc.31",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"local-pkg": "^0.5.0",
|
||||
"markdown-it-container": "^4.0.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"shiki": "^1.5.1",
|
||||
"tm-grammars": "^1.11.1",
|
||||
"tm-themes": "^1.4.1",
|
||||
"shiki": "^1.6.0",
|
||||
"tm-grammars": "^1.12.4",
|
||||
"tm-themes": "^1.4.3",
|
||||
"vue": "^3.4.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.208",
|
||||
"@iconify/json": "^2.2.214",
|
||||
"@types/markdown-it": "^14.1.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@ -34,7 +34,7 @@ function highlight() {
|
||||
})
|
||||
if (container) {
|
||||
container.innerHTML = output
|
||||
.replace(/^<pre[^]+?>/, '')
|
||||
.replace(/^<pre[^>]*>/, '')
|
||||
.replace(/<\/pre>$/, '')
|
||||
.replace(/(<span class="line">)(<\/span>)/g, '$1<wbr>$2')
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { sleep } from '../utils/sleep.js'
|
||||
import { rustExecute } from './rustRepl.js'
|
||||
|
||||
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
|
||||
const RE_LANGUAGE = /language-([\w]+)/
|
||||
const RE_LANGUAGE = /language-(\w+)/
|
||||
const api = {
|
||||
go: 'https://api.pengzhanbo.cn/repl/golang/run',
|
||||
kotlin: 'https://api.pengzhanbo.cn/repl/kotlin/run',
|
||||
|
||||
@ -3,7 +3,7 @@ export function checkIsMobile(ua: string): boolean {
|
||||
}
|
||||
|
||||
export function checkIsSafari(ua: string): boolean {
|
||||
return /version\/([\w.]+) .*(mobile ?safari|safari)/i.test(ua)
|
||||
return /version\/[\w.]+ .*(?:mobile ?safari|safari)/i.test(ua)
|
||||
}
|
||||
|
||||
export function checkIsiPad(ua: string): boolean {
|
||||
|
||||
@ -29,18 +29,24 @@ export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
|
||||
const isInstalled = isPackageExists('@iconify/json')
|
||||
|
||||
const write = (content: string) => app.writeTemp('internal/md-power/icons.css', content)
|
||||
let timer: NodeJS.Timeout | null = null
|
||||
|
||||
const options = resolveOption(opt)
|
||||
const prefix = options.prefix
|
||||
const defaultContent = getDefaultContent(options)
|
||||
|
||||
async function writeCss() {
|
||||
let css = defaultContent
|
||||
if (timer)
|
||||
clearTimeout(timer)
|
||||
|
||||
for (const [, { content, className }] of cache)
|
||||
css += `.${className} {\n --svg: ${content};\n}\n`
|
||||
timer = setTimeout(async () => {
|
||||
let css = defaultContent
|
||||
|
||||
await write(css)
|
||||
for (const [, { content, className }] of cache)
|
||||
css += `.${className} {\n --svg: ${content};\n}\n`
|
||||
|
||||
await write(css)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function addIcon(iconName: string) {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import type { PluginWithOptions } from 'markdown-it'
|
||||
import type { RuleInline } from 'markdown-it/lib/parser_inline.mjs'
|
||||
|
||||
const [openTag, endTag] = ['=|', '|=']
|
||||
const [openTag, endTag] = ['!!', '!!']
|
||||
|
||||
function createTokenizer(): RuleInline {
|
||||
return (state, silent) => {
|
||||
@ -18,7 +18,7 @@ function createTokenizer(): RuleInline {
|
||||
if (silent)
|
||||
return false
|
||||
|
||||
// =||=
|
||||
// - !!!!
|
||||
if (max - start < 5)
|
||||
return false
|
||||
|
||||
|
||||
@ -41,22 +41,22 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@netlify/functions": "^2.6.3",
|
||||
"@netlify/functions": "^2.7.0",
|
||||
"chalk": "^5.3.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"cpx2": "^7.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.21.1",
|
||||
"execa": "^9.0.1",
|
||||
"netlify-cli": "^17.23.2",
|
||||
"esbuild": "^0.21.4",
|
||||
"execa": "^9.1.0",
|
||||
"netlify-cli": "^17.23.8",
|
||||
"portfinder": "^1.0.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.11"
|
||||
"@types/node": "^20.12.12"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "6.6.1",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export interface NotesDataOptions {
|
||||
/**
|
||||
* 保存所有笔记的目录
|
||||
* @default '/notes'
|
||||
* @default '/notes/'
|
||||
*/
|
||||
dir: string
|
||||
/**
|
||||
|
||||
@ -31,10 +31,10 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@netlify/functions": "^2.6.3",
|
||||
"@netlify/functions": "^2.7.0",
|
||||
"leancloud-storage": "^4.15.2",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "4.3.2",
|
||||
|
||||
@ -37,10 +37,10 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vuepress/helper": "2.0.0-rc.28",
|
||||
"@vuepress/helper": "2.0.0-rc.31",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/integrations": "^10.9.0",
|
||||
"chokidar": "^3.6.0",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { LocaleConfig, Page } from 'vuepress/core'
|
||||
import type { Options as MiniSearchOptions } from 'minisearch'
|
||||
|
||||
export type SearchBoxLocales = LocaleConfig<{
|
||||
export interface SearchLocaleOptions {
|
||||
placeholder: string
|
||||
buttonText: string
|
||||
resetButtonTitle: string
|
||||
@ -16,7 +16,9 @@ export type SearchBoxLocales = LocaleConfig<{
|
||||
closeText: string
|
||||
closeKeyAriaLabel: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>
|
||||
|
||||
export interface SearchPluginOptions extends SearchOptions {
|
||||
locales?: SearchBoxLocales
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
|
||||
使用 [`shiki`](https://shiki.style/) 为 Markdown 代码块启用代码高亮。
|
||||
|
||||
> [!WARNING]
|
||||
> 相比于 官方的 [@vuepress/plugin-shiki](https://ecosystem.vuejs.press/zh/plugins/shiki.html),
|
||||
> 本插件做了很多各种各样的调整,你可以认为这是试验性的。
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
|
||||
@ -33,18 +33,19 @@
|
||||
"ts": "tsc -b tsconfig.build.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shikijs/transformers": "^1.5.1",
|
||||
"@shikijs/twoslash": "^1.5.1",
|
||||
"@shikijs/transformers": "^1.6.0",
|
||||
"@shikijs/twoslash": "^1.6.0",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@vuepress/helper": "2.0.0-rc.31",
|
||||
"floating-vue": "^5.2.2",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.1.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"shiki": "^1.5.1",
|
||||
"shiki": "^1.6.0",
|
||||
"twoslash": "^0.2.6",
|
||||
"twoslash-vue": "^0.2.6"
|
||||
},
|
||||
|
||||
57
plugins/plugin-shikiji/src/client/composables/copy-code.ts
Normal file
57
plugins/plugin-shikiji/src/client/composables/copy-code.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { useClipboard, useEventListener } from '@vueuse/core'
|
||||
|
||||
const SHELL_RE = /language-(shellscript|shell|bash|sh|zsh)/
|
||||
const IGNORE_NODES = ['.vp-copy-ignore', '.diff.remove']
|
||||
|
||||
interface CopyCodeOptions {
|
||||
selector?: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export function useCopyCode({
|
||||
selector = 'div[class*="language-"] > button.copy',
|
||||
duration = 2000,
|
||||
}: CopyCodeOptions = {}): void {
|
||||
if (__VUEPRESS_SSR__)
|
||||
return
|
||||
|
||||
const timeoutIdMap = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>()
|
||||
const { copy } = useClipboard({ legacy: true })
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
const el = e.target as HTMLElement
|
||||
if (el.matches(selector)) {
|
||||
const parent = el.parentElement
|
||||
const sibling = el.nextElementSibling
|
||||
if (!parent || !sibling)
|
||||
return
|
||||
|
||||
const isShell = SHELL_RE.test(parent.className)
|
||||
|
||||
// Clone the node and remove the ignored nodes
|
||||
const clone = sibling.cloneNode(true) as HTMLElement
|
||||
clone
|
||||
.querySelectorAll(IGNORE_NODES.join(','))
|
||||
.forEach(node => node.remove())
|
||||
|
||||
let text = clone.textContent || ''
|
||||
|
||||
if (isShell)
|
||||
text = text.replace(/^ *(\$|>) /gm, '').trim()
|
||||
|
||||
copy(text).then(() => {
|
||||
if (duration <= 0)
|
||||
return
|
||||
|
||||
el.classList.add('copied')
|
||||
clearTimeout(timeoutIdMap.get(el))
|
||||
const timeoutId = setTimeout(() => {
|
||||
el.classList.remove('copied')
|
||||
el.blur()
|
||||
timeoutIdMap.delete(el)
|
||||
}, duration)
|
||||
timeoutIdMap.set(el, timeoutId)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
50
plugins/plugin-shikiji/src/client/composables/twoslash.ts
Normal file
50
plugins/plugin-shikiji/src/client/composables/twoslash.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { App } from 'vue'
|
||||
import FloatingVue, { recomputeAllPoppers } from 'floating-vue'
|
||||
import 'floating-vue/dist/style.css'
|
||||
|
||||
const isMobile = typeof navigator !== 'undefined' && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
|
||||
export type FloatingVueConfig = Parameters<(typeof FloatingVue)['install']>[1]
|
||||
|
||||
export function enhanceTwoslash(app: App) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Recompute poppers when clicking on a tab
|
||||
window.addEventListener('click', (e) => {
|
||||
const path = e.composedPath()
|
||||
if (path.some((el: any) => el?.classList?.contains?.('vp-code-group') || el?.classList?.contains?.('tabs')))
|
||||
recomputeAllPoppers()
|
||||
}, { passive: true })
|
||||
}
|
||||
app.use(FloatingVue, {
|
||||
themes: {
|
||||
'twoslash': {
|
||||
$extend: 'dropdown',
|
||||
triggers: isMobile ? ['touch'] : ['hover', 'touch'],
|
||||
popperTriggers: isMobile ? ['touch'] : ['hover', 'touch'],
|
||||
placement: 'bottom-start',
|
||||
overflowPadding: 10,
|
||||
delay: 0,
|
||||
handleResize: false,
|
||||
autoHide: true,
|
||||
instantMove: true,
|
||||
flip: false,
|
||||
arrowPadding: 8,
|
||||
autoBoundaryMaxSize: true,
|
||||
},
|
||||
'twoslash-query': {
|
||||
$extend: 'twoslash',
|
||||
triggers: ['click'],
|
||||
popperTriggers: ['click'],
|
||||
autoHide: false,
|
||||
},
|
||||
'twoslash-completion': {
|
||||
$extend: 'twoslash-query',
|
||||
triggers: ['click'],
|
||||
popperTriggers: ['click'],
|
||||
autoHide: false,
|
||||
distance: 0,
|
||||
arrowOverflow: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
import { type ClientConfig, defineClientConfig } from 'vuepress/client'
|
||||
import FloatingVue, { recomputeAllPoppers } from 'floating-vue'
|
||||
import 'floating-vue/dist/style.css'
|
||||
|
||||
const isMobile = typeof navigator !== 'undefined' && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
|
||||
export type FloatingVueConfig = Parameters<(typeof FloatingVue)['install']>[1]
|
||||
|
||||
export default defineClientConfig({
|
||||
enhance({ app }) {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Recompute poppers when clicking on a tab
|
||||
window.addEventListener('click', (e) => {
|
||||
const path = e.composedPath()
|
||||
if (path.some((el: any) => el?.classList?.contains?.('vp-code-group') || el?.classList?.contains?.('tabs')))
|
||||
recomputeAllPoppers()
|
||||
}, { passive: true })
|
||||
}
|
||||
app.use(FloatingVue, {
|
||||
themes: {
|
||||
'twoslash': {
|
||||
$extend: 'dropdown',
|
||||
triggers: isMobile ? ['touch'] : ['hover', 'touch'],
|
||||
popperTriggers: isMobile ? ['touch'] : ['hover', 'touch'],
|
||||
placement: 'bottom-start',
|
||||
overflowPadding: 10,
|
||||
delay: 0,
|
||||
handleResize: false,
|
||||
autoHide: true,
|
||||
instantMove: true,
|
||||
flip: false,
|
||||
arrowPadding: 8,
|
||||
autoBoundaryMaxSize: true,
|
||||
},
|
||||
'twoslash-query': {
|
||||
$extend: 'twoslash',
|
||||
triggers: ['click'],
|
||||
popperTriggers: ['click'],
|
||||
autoHide: false,
|
||||
},
|
||||
'twoslash-completion': {
|
||||
$extend: 'twoslash-query',
|
||||
triggers: ['click'],
|
||||
popperTriggers: ['click'],
|
||||
autoHide: false,
|
||||
distance: 0,
|
||||
arrowOverflow: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}) as ClientConfig
|
||||
@ -0,0 +1,106 @@
|
||||
import type { LocaleConfig } from 'vuepress'
|
||||
import type { CopyCodeLocaleOptions } from '../types.js'
|
||||
|
||||
/** Multi language config for copy code button */
|
||||
export const copyCodeButtonLocales: LocaleConfig<CopyCodeLocaleOptions>
|
||||
= {
|
||||
'/en/': {
|
||||
title: 'Copy code',
|
||||
copied: 'Copied',
|
||||
},
|
||||
|
||||
'/zh/': {
|
||||
title: '复制代码',
|
||||
copied: '已复制',
|
||||
},
|
||||
|
||||
'/zh-tw/': {
|
||||
title: '複製代碼',
|
||||
copied: '已複製',
|
||||
},
|
||||
|
||||
'/de/': {
|
||||
title: 'Kopiere den Code.',
|
||||
copied: 'Kopiert',
|
||||
},
|
||||
|
||||
'/de-at/': {
|
||||
title: 'Kopiere den Code.',
|
||||
copied: 'Kopierter',
|
||||
},
|
||||
|
||||
'/vi/': {
|
||||
title: 'Sao chép code',
|
||||
copied: 'Đã sao chép',
|
||||
},
|
||||
|
||||
'/uk/': {
|
||||
title: 'Скопіюйте код',
|
||||
copied: 'Скопійовано',
|
||||
},
|
||||
|
||||
'/ru/': {
|
||||
title: 'Скопировать код',
|
||||
copied: 'Скопировано',
|
||||
},
|
||||
|
||||
'/br/': {
|
||||
title: 'Copiar o código',
|
||||
copied: 'Código',
|
||||
},
|
||||
|
||||
'/pl/': {
|
||||
title: 'Skopiuj kod',
|
||||
copied: 'Skopiowane',
|
||||
},
|
||||
|
||||
'/sk/': {
|
||||
title: 'Skopíruj kód',
|
||||
copied: 'Skopírované',
|
||||
},
|
||||
|
||||
'/fr/': {
|
||||
title: 'Copier le code',
|
||||
copied: 'Copié',
|
||||
},
|
||||
|
||||
'/es/': {
|
||||
title: 'Copiar código',
|
||||
copied: 'Copiado',
|
||||
},
|
||||
|
||||
'/ja/': {
|
||||
title: 'コードをコピー',
|
||||
copied: 'コピーしました',
|
||||
},
|
||||
|
||||
'/tr/': {
|
||||
title: 'Kodu kopyala',
|
||||
copied: 'Kopyalandı',
|
||||
},
|
||||
|
||||
'/ko/': {
|
||||
title: '코드 복사',
|
||||
copied: '복사됨',
|
||||
},
|
||||
|
||||
'/fi/': {
|
||||
title: 'Kopioi koodi',
|
||||
copied: 'Kopioitu',
|
||||
},
|
||||
|
||||
'/hu/': {
|
||||
title: 'Kód másolása',
|
||||
copied: 'Másolva',
|
||||
},
|
||||
|
||||
'/id/': {
|
||||
title: 'Salin kode',
|
||||
copied: 'Disalin',
|
||||
},
|
||||
|
||||
'/nl/': {
|
||||
title: 'Kopieer code',
|
||||
copied: 'Gekopieerd',
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import type { App } from 'vuepress'
|
||||
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
|
||||
import type { CopyCodeOptions } from '../types.js'
|
||||
import { createCopyCodeButtonRender } from './createCopyCodeButtonRender.js'
|
||||
|
||||
/**
|
||||
* This plugin should work after `preWrapperPlugin`,
|
||||
* and if `preWrapper` is disabled, this plugin should not be called either.
|
||||
*/
|
||||
export function copyCodeButtonPlugin(md: Markdown, app: App, options?: boolean | CopyCodeOptions): void {
|
||||
const render = createCopyCodeButtonRender(app, options)
|
||||
|
||||
if (!render)
|
||||
return
|
||||
|
||||
const fence = md.renderer.rules.fence!
|
||||
|
||||
md.renderer.rules.fence = (...args) => {
|
||||
const [, , , env] = args
|
||||
|
||||
const result = fence(...args)
|
||||
const { filePathRelative } = env as MarkdownEnv
|
||||
// resolve copy code button
|
||||
const copyCodeButton = render(filePathRelative ?? '')
|
||||
|
||||
return result.replace('><pre', `>${copyCodeButton}<pre`)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import {
|
||||
getLocalePaths,
|
||||
getRootLangPath,
|
||||
isPlainObject,
|
||||
} from '@vuepress/helper'
|
||||
import type { App, LocaleConfig } from 'vuepress'
|
||||
import { ensureLeadingSlash, resolveLocalePath } from 'vuepress/shared'
|
||||
import type {
|
||||
CopyCodeLocaleOptions,
|
||||
CopyCodeOptions,
|
||||
} from '../types.js'
|
||||
import { copyCodeButtonLocales } from './copyCodeButtonLocales.js'
|
||||
|
||||
export function createCopyCodeButtonRender(app: App, options?: boolean | CopyCodeOptions): ((filePathRelative: string) => string) | null {
|
||||
if (options === false)
|
||||
return null
|
||||
|
||||
const { className = 'copy', locales: userLocales = {} }
|
||||
= isPlainObject(options) ? options : {}
|
||||
|
||||
const root = getRootLangPath(app)
|
||||
const locales: LocaleConfig<CopyCodeLocaleOptions> = {
|
||||
// fallback locale
|
||||
'/': userLocales['/'] || copyCodeButtonLocales[root],
|
||||
}
|
||||
|
||||
getLocalePaths(app).forEach((path) => {
|
||||
locales[path]
|
||||
= userLocales[path] || copyCodeButtonLocales[path === '/' ? root : path]
|
||||
})
|
||||
|
||||
return (filePathRelative) => {
|
||||
const relativePath = ensureLeadingSlash(filePathRelative ?? '')
|
||||
const localePath = resolveLocalePath(locales, relativePath)
|
||||
|
||||
const { title, copied } = locales[localePath] ?? {}
|
||||
|
||||
return `<button class="${className}" title="${title ?? 'Copy code'}" data-copied="${copied ?? 'Copied'}"></button>`
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export * from './createCopyCodeButtonRender.js'
|
||||
export * from './copyCodeButtonLocales.js'
|
||||
export * from './copyCodeButtonPlugin.js'
|
||||
@ -9,6 +9,7 @@ import {
|
||||
isSpecialLang,
|
||||
} from 'shiki'
|
||||
import {
|
||||
transformerCompactLineOptions,
|
||||
transformerNotationDiff,
|
||||
transformerNotationErrorLevel,
|
||||
transformerNotationFocus,
|
||||
@ -17,9 +18,8 @@ import {
|
||||
transformerRenderWhitespace,
|
||||
} from '@shikijs/transformers'
|
||||
import type { HighlighterOptions, ThemeOptions } from './types.js'
|
||||
import { resolveAttrs } from './resolveAttrs.js'
|
||||
import { LRUCache } from './lru.js'
|
||||
import { defaultHoverInfoProcessor, transformerTwoslash } from './rendererTransformer.js'
|
||||
import { LRUCache, attrsToLines, resolveLanguage } from './utils/index.js'
|
||||
import { defaultHoverInfoProcessor, transformerTwoslash } from './twoslash/rendererTransformer.js'
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
|
||||
const cache = new LRUCache<string, string>(64)
|
||||
@ -85,16 +85,17 @@ export async function highlight(
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'vuepress-shikiji:remove-escape',
|
||||
name: 'vuepress:remove-escape',
|
||||
postprocess: code => code.replace(RE_ESCAPE, '[!code'),
|
||||
},
|
||||
]
|
||||
|
||||
return (str: string, lang: string, attrs: string) => {
|
||||
lang = lang || defaultLang
|
||||
return (str: string, language: string, attrs: string) => {
|
||||
attrs = attrs || ''
|
||||
let lang = resolveLanguage(language) || defaultLang
|
||||
const vPre = vueRE.test(lang) ? '' : 'v-pre'
|
||||
|
||||
const key = str + lang + attrs
|
||||
const key = str + language + attrs
|
||||
|
||||
if (isDev) {
|
||||
const rendered = cache.get(key)
|
||||
@ -114,7 +115,8 @@ export async function highlight(
|
||||
lang = defaultLang
|
||||
}
|
||||
}
|
||||
const { attrs: attributes, rawAttrs } = resolveAttrs(attrs || '')
|
||||
// const { attrs: attributes, rawAttrs } = resolveAttrs(attrs || '')
|
||||
const enabledTwoslash = attrs.includes('twoslash')
|
||||
const mustaches = new Map<string, string>()
|
||||
|
||||
const removeMustache = (s: string) => {
|
||||
@ -133,7 +135,7 @@ export async function highlight(
|
||||
s = s.replaceAll(marker, match)
|
||||
})
|
||||
|
||||
if (attributes.twoslash && options.twoslash)
|
||||
if (enabledTwoslash && options.twoslash)
|
||||
s = s.replace(/{/g, '{')
|
||||
|
||||
return `${s}\n`
|
||||
@ -141,14 +143,14 @@ export async function highlight(
|
||||
|
||||
str = removeMustache(str).trimEnd()
|
||||
|
||||
const inlineTransformers: ShikiTransformer[] = []
|
||||
const inlineTransformers: ShikiTransformer[] = [
|
||||
transformerCompactLineOptions(attrsToLines(attrs)),
|
||||
]
|
||||
|
||||
if (attributes.twoslash && options.twoslash) {
|
||||
if (enabledTwoslash && options.twoslash) {
|
||||
inlineTransformers.push(transformerTwoslash({
|
||||
processHoverInfo(info) {
|
||||
return defaultHoverInfoProcessor(info)
|
||||
// Remove shiki_core namespace
|
||||
.replace(/_shikijs_core[\w_]*\./g, '')
|
||||
},
|
||||
}))
|
||||
}
|
||||
@ -162,10 +164,7 @@ export async function highlight(
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(whitespace && attributes.whitespace !== false)
|
||||
|| (!whitespace && attributes.whitespace)
|
||||
)
|
||||
if (attrs.includes('whitespace') || whitespace)
|
||||
inlineTransformers.push(transformerRenderWhitespace({ position: 'boundary' }))
|
||||
|
||||
try {
|
||||
@ -176,7 +175,7 @@ export async function highlight(
|
||||
...inlineTransformers,
|
||||
...userTransformers,
|
||||
],
|
||||
meta: { __raw: rawAttrs },
|
||||
meta: { __raw: attrs },
|
||||
...(typeof theme === 'object' && 'light' in theme && 'dark' in theme
|
||||
? { themes: theme, defaultColor: false }
|
||||
: { theme }),
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
// Modified from https://github.com/egoist/markdown-it-highlight-lines
|
||||
// Now this plugin is only used to normalize line attrs.
|
||||
// The else part of line highlights logic is in '../highlight.ts'.
|
||||
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
|
||||
const HIGHLIGHT_LINES_REGEXP = /{([\d,-]+)}/
|
||||
|
||||
export function highlightLinesPlugin(md: Markdown): void {
|
||||
const rawFence = md.renderer.rules.fence!
|
||||
|
||||
md.renderer.rules.fence = (...args) => {
|
||||
const [tokens, idx] = args
|
||||
const token = tokens[idx]
|
||||
|
||||
let lines: string | null = null
|
||||
|
||||
const rawInfo = token.info
|
||||
const result = rawInfo?.match(HIGHLIGHT_LINES_REGEXP)
|
||||
|
||||
if (!result)
|
||||
return rawFence(...args)
|
||||
|
||||
// ensure the next plugin get the correct lang
|
||||
token.info = rawInfo.replace(HIGHLIGHT_LINES_REGEXP, '').trim()
|
||||
|
||||
lines = result[1]
|
||||
|
||||
token.info += ` ${lines}`
|
||||
return rawFence(...args)
|
||||
}
|
||||
}
|
||||
3
plugins/plugin-shikiji/src/node/markdown/index.ts
Normal file
3
plugins/plugin-shikiji/src/node/markdown/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './highlightLinesPlugin.js'
|
||||
export * from './lineNumberPlugin.js'
|
||||
export * from './preWrapperPlugin.js'
|
||||
56
plugins/plugin-shikiji/src/node/markdown/lineNumberPlugin.ts
Normal file
56
plugins/plugin-shikiji/src/node/markdown/lineNumberPlugin.ts
Normal file
@ -0,0 +1,56 @@
|
||||
// markdown-it plugin for generating line numbers.
|
||||
// It depends on preWrapper plugin.
|
||||
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import type { LineNumberOptions } from '../types.js'
|
||||
|
||||
const LINE_NUMBERS_REGEXP = /:line-numbers\b/
|
||||
const NO_LINE_NUMBERS_REGEXP = /:no-line-numbers\b/
|
||||
|
||||
export function lineNumberPlugin(md: Markdown, { lineNumbers = true }: LineNumberOptions = {}): void {
|
||||
const rawFence = md.renderer.rules.fence!
|
||||
|
||||
md.renderer.rules.fence = (...args) => {
|
||||
const rawCode = rawFence(...args)
|
||||
|
||||
const [tokens, idx] = args
|
||||
const info = tokens[idx].info
|
||||
const enableLineNumbers = LINE_NUMBERS_REGEXP.test(info)
|
||||
const disableLineNumbers = NO_LINE_NUMBERS_REGEXP.test(info)
|
||||
|
||||
if (info.includes('twoslash'))
|
||||
return rawCode
|
||||
|
||||
if (
|
||||
(!lineNumbers && !enableLineNumbers)
|
||||
|| (lineNumbers && disableLineNumbers)
|
||||
)
|
||||
return rawCode
|
||||
|
||||
const code = rawCode.slice(
|
||||
rawCode.indexOf('<code>'),
|
||||
rawCode.indexOf('</code>'),
|
||||
)
|
||||
|
||||
const lines = code.split('\n')
|
||||
|
||||
if (
|
||||
typeof lineNumbers === 'number'
|
||||
&& lines.length < lineNumbers
|
||||
&& !enableLineNumbers
|
||||
)
|
||||
return rawCode
|
||||
|
||||
const lineNumbersCode = [...Array(lines.length)]
|
||||
.map(() => `<div class="line-number"></div>`)
|
||||
.join('')
|
||||
|
||||
const lineNumbersWrapperCode = `<div class="line-numbers" aria-hidden="true">${lineNumbersCode}</div>`
|
||||
|
||||
const finalCode = rawCode
|
||||
.replace(/<\/div>$/, `${lineNumbersWrapperCode}</div>`)
|
||||
.replace(/"(language-[^"]*?)"/, '"$1 line-numbers-mode"')
|
||||
|
||||
return finalCode
|
||||
}
|
||||
}
|
||||
32
plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts
Normal file
32
plugins/plugin-shikiji/src/node/markdown/preWrapperPlugin.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// markdown-it plugin for generating line numbers.
|
||||
// v-pre block logic is in `../highlight.ts`
|
||||
import type { Markdown } from 'vuepress/markdown'
|
||||
import type { PreWrapperOptions } from '../types.js'
|
||||
import { resolveAttr, resolveLanguage } from '../utils/index.js'
|
||||
|
||||
export function preWrapperPlugin(md: Markdown, { preWrapper = true }: PreWrapperOptions = {}): void {
|
||||
const rawFence = md.renderer.rules.fence!
|
||||
|
||||
md.renderer.rules.fence = (...args) => {
|
||||
const [tokens, idx, options] = args
|
||||
const token = tokens[idx]
|
||||
|
||||
// get token info
|
||||
const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
|
||||
|
||||
const lang = resolveLanguage(info)
|
||||
const title = resolveAttr(info, 'title') || lang
|
||||
const languageClass = `${options.langPrefix}${lang}`
|
||||
|
||||
let result = rawFence(...args)
|
||||
|
||||
if (!preWrapper) {
|
||||
// remove `<code>` attributes
|
||||
result = result.replace(/<code[^]*?>/, '<code>')
|
||||
result = `<pre class="${languageClass}"${result.slice('<pre'.length)}`
|
||||
return result
|
||||
}
|
||||
|
||||
return `<div class="${languageClass}" data-ext="${lang}" data-title="${title}">${result}</div>`
|
||||
}
|
||||
}
|
||||
38
plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts
Normal file
38
plugins/plugin-shikiji/src/node/prepareClientConfigFile.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { ensureEndingSlash } from '@vuepress/helper'
|
||||
import type { App } from 'vuepress'
|
||||
import { getDirname, path } from 'vuepress/utils'
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
|
||||
const CLIENT_FOLDER = ensureEndingSlash(
|
||||
path.resolve(__dirname, '../client'),
|
||||
)
|
||||
|
||||
export async function prepareClientConfigFile(app: App, {
|
||||
copyCode,
|
||||
twoslash,
|
||||
}: { copyCode: boolean, twoslash: boolean }): Promise<string> {
|
||||
return await app.writeTemp(
|
||||
'internal/plugin-shiki/client.js',
|
||||
`\
|
||||
${twoslash ? `import { enhanceTwoslash } from '${CLIENT_FOLDER}composables/twoslash.js'` : ''}
|
||||
${copyCode ? `import { useCopyCode } from '${CLIENT_FOLDER}composables/copy-code.js'` : ''}
|
||||
|
||||
export default {
|
||||
${twoslash
|
||||
? `enhance({ app }) {
|
||||
enhanceTwoslash(app)
|
||||
},`
|
||||
: ''}
|
||||
${copyCode
|
||||
? `setup() {
|
||||
useCopyCode({
|
||||
selector: __CC_SELECTOR__,
|
||||
duration: __CC_DURATION__,
|
||||
})
|
||||
},`
|
||||
: ''}
|
||||
}
|
||||
`,
|
||||
)
|
||||
}
|
||||
@ -1,44 +1,76 @@
|
||||
import type { Plugin, PluginObject } from 'vuepress/core'
|
||||
import { getDirname, path } from 'vuepress/utils'
|
||||
import type { Plugin } from 'vuepress/core'
|
||||
import { getDirname } from 'vuepress/utils'
|
||||
import { isPlainObject } from 'vuepress/shared'
|
||||
import { highlight } from './highlight.js'
|
||||
import type { HighlighterOptions } from './types.js'
|
||||
import type {
|
||||
CopyCodeOptions,
|
||||
HighlighterOptions,
|
||||
LineNumberOptions,
|
||||
PreWrapperOptions,
|
||||
} from './types.js'
|
||||
import {
|
||||
highlightLinesPlugin,
|
||||
lineNumberPlugin,
|
||||
preWrapperPlugin,
|
||||
} from './markdown/index.js'
|
||||
import { copyCodeButtonPlugin } from './copy-code-button/index.js'
|
||||
import { prepareClientConfigFile } from './prepareClientConfigFile.js'
|
||||
|
||||
export type ShikiPluginOptions = HighlighterOptions
|
||||
export interface ShikiPluginOptions extends HighlighterOptions, LineNumberOptions, PreWrapperOptions {
|
||||
/**
|
||||
* Add copy code button
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
copyCode?: boolean | CopyCodeOptions
|
||||
}
|
||||
|
||||
const __dirname = getDirname(import.meta.url)
|
||||
|
||||
export function shikiPlugin(options: ShikiPluginOptions = {}): Plugin {
|
||||
const plugin: PluginObject = {
|
||||
export function shikiPlugin({
|
||||
preWrapper = true,
|
||||
lineNumbers = true,
|
||||
copyCode = true,
|
||||
...options
|
||||
}: ShikiPluginOptions = {}): Plugin {
|
||||
const copyCodeOptions: CopyCodeOptions = isPlainObject(copyCode) ? copyCode : {}
|
||||
|
||||
return {
|
||||
name: '@vuepress-plume/plugin-shikiji',
|
||||
|
||||
define: {
|
||||
__CC_DURATION__: copyCodeOptions.duration ?? 2000,
|
||||
__CC_SELECTOR__: `div[class*="language-"] > button.${copyCodeOptions.className || 'copy'}`,
|
||||
},
|
||||
|
||||
clientConfigFile: app => prepareClientConfigFile(app, {
|
||||
copyCode: copyCode !== false,
|
||||
twoslash: options.twoslash ?? false,
|
||||
}),
|
||||
|
||||
extendsMarkdown: async (md, app) => {
|
||||
const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' }
|
||||
const highlighter = await highlight(theme, options, app.env.isDev)
|
||||
|
||||
md.options.highlight = highlighter
|
||||
md.options.highlight = await highlight(theme, options, app.env.isDev)
|
||||
|
||||
md.use(highlightLinesPlugin)
|
||||
md.use<PreWrapperOptions>(preWrapperPlugin, {
|
||||
preWrapper,
|
||||
})
|
||||
if (preWrapper) {
|
||||
copyCodeButtonPlugin(md, app, copyCode)
|
||||
md.use<LineNumberOptions>(lineNumberPlugin, { lineNumbers })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if (!options.twoslash)
|
||||
return plugin
|
||||
|
||||
return {
|
||||
|
||||
...plugin,
|
||||
|
||||
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
|
||||
|
||||
extendsMarkdownOptions: (options) => {
|
||||
if (options.code === false)
|
||||
return
|
||||
|
||||
// 注入 floating-vue 后,需要关闭 代码块 的 v-pre 配置
|
||||
if (options.code?.vPre) {
|
||||
options.code.vPre.block = false
|
||||
}
|
||||
else {
|
||||
options.code ??= {}
|
||||
options.code.vPre = { block: false }
|
||||
if ((options as any).vPre !== false) {
|
||||
const vPre = isPlainObject((options as any).vPre) ? (options as any).vPre : { block: true }
|
||||
if (vPre.block) {
|
||||
(options as any).vPre ??= {}
|
||||
;(options as any).vPre.block = false
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -19,8 +19,6 @@ export interface VitePressPluginTwoslashOptions extends TransformerTwoslashOptio
|
||||
|
||||
/**
|
||||
* Create a Shiki transformer for VitePress to enable twoslash integration
|
||||
*
|
||||
* Add this to `markdown.codeTransformers` in `.vitepress/config.ts`
|
||||
*/
|
||||
export function transformerTwoslash(options: VitePressPluginTwoslashOptions = {}): ShikiTransformer {
|
||||
const {
|
||||
@ -52,7 +50,7 @@ export function transformerTwoslash(options: VitePressPluginTwoslashOptions = {}
|
||||
|
||||
return {
|
||||
...twoslash,
|
||||
name: '@shikijs/vuepress-twoslash',
|
||||
name: '@shiki/vuepress-twoslash',
|
||||
preprocess(code, options) {
|
||||
const cleanup = options.transformers?.find(i => i.name === 'vuepress:clean-up')
|
||||
if (cleanup)
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
ShikiTransformer,
|
||||
ThemeRegistration,
|
||||
} from 'shiki'
|
||||
import type { LocaleConfig } from 'vuepress/shared'
|
||||
|
||||
export type ThemeOptions =
|
||||
| ThemeRegistration
|
||||
@ -72,3 +73,64 @@ export interface HighlighterOptions {
|
||||
*/
|
||||
whitespace?: boolean
|
||||
}
|
||||
|
||||
export interface LineNumberOptions {
|
||||
/**
|
||||
* Show line numbers in code blocks
|
||||
* @default true
|
||||
*/
|
||||
lineNumbers?: boolean | number
|
||||
}
|
||||
|
||||
export interface PreWrapperOptions {
|
||||
/**
|
||||
* Wrap the `<pre>` tag with an extra `<div>` or not. Do not disable it unless you
|
||||
* understand what's it for
|
||||
*
|
||||
* - Required for `lineNumbers`
|
||||
* - Required for title display of default theme
|
||||
*/
|
||||
preWrapper?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for copy code button
|
||||
*
|
||||
* `<button title="{title}" class="{className}"></button>`
|
||||
*/
|
||||
export interface CopyCodeOptions {
|
||||
/**
|
||||
* Class name of the button
|
||||
*
|
||||
* @default 'copy'
|
||||
*/
|
||||
className?: string
|
||||
|
||||
/**
|
||||
* Duration of the copied text
|
||||
*
|
||||
* @default 2000
|
||||
*/
|
||||
duration?: number
|
||||
|
||||
/**
|
||||
* Locale config for copy code button
|
||||
*/
|
||||
locales?: LocaleConfig<CopyCodeLocaleOptions>
|
||||
}
|
||||
|
||||
export interface CopyCodeLocaleOptions {
|
||||
/**
|
||||
* Title of the button
|
||||
*
|
||||
* @default 'Copy code'
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* Copied text
|
||||
*
|
||||
* @default 'Copied!'
|
||||
*/
|
||||
copied?: string
|
||||
}
|
||||
|
||||
37
plugins/plugin-shikiji/src/node/utils/attrsToLines.ts
Normal file
37
plugins/plugin-shikiji/src/node/utils/attrsToLines.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { TransformerCompactLineOption } from '@shikijs/transformers'
|
||||
|
||||
/**
|
||||
* 2 steps:
|
||||
*
|
||||
* 1. convert attrs into line numbers:
|
||||
* {4,7-13,16,23-27,40} -> [4,7,8,9,10,11,12,13,16,23,24,25,26,27,40]
|
||||
* 2. convert line numbers into line options:
|
||||
* [{ line: number, classes: string[] }]
|
||||
*/
|
||||
export function attrsToLines(attrs: string): TransformerCompactLineOption[] {
|
||||
attrs = attrs.replace(/^(?:\[.*?\])?.*?([\d,-]+).*/, '$1').trim()
|
||||
|
||||
const result: number[] = []
|
||||
|
||||
if (!attrs)
|
||||
return []
|
||||
|
||||
attrs
|
||||
.split(',')
|
||||
.map(v => v.split('-').map(v => Number.parseInt(v, 10)))
|
||||
.forEach(([start, end]) => {
|
||||
if (start && end) {
|
||||
result.push(
|
||||
...Array.from({ length: end - start + 1 }, (_, i) => start + i),
|
||||
)
|
||||
}
|
||||
else {
|
||||
result.push(start)
|
||||
}
|
||||
})
|
||||
|
||||
return result.map(line => ({
|
||||
line,
|
||||
classes: ['highlighted'],
|
||||
}))
|
||||
}
|
||||
4
plugins/plugin-shikiji/src/node/utils/index.ts
Normal file
4
plugins/plugin-shikiji/src/node/utils/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './attrsToLines.js'
|
||||
export * from './resolveAttr.js'
|
||||
export * from './resolveLanguage.js'
|
||||
export * from './lru.js'
|
||||
9
plugins/plugin-shikiji/src/node/utils/resolveAttr.ts
Normal file
9
plugins/plugin-shikiji/src/node/utils/resolveAttr.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function resolveAttr(info: string, attr: string): string | null {
|
||||
// try to match specified attr mark
|
||||
const pattern = `\\b${attr}\\s*=\\s*(?<quote>['"])(?<content>.+?)\\k<quote>(\\s|$)`
|
||||
const regex = new RegExp(pattern, 'i')
|
||||
const match = info.match(regex)
|
||||
|
||||
// return content if matched, null if not specified
|
||||
return match?.groups?.content ?? null
|
||||
}
|
||||
8
plugins/plugin-shikiji/src/node/utils/resolveLanguage.ts
Normal file
8
plugins/plugin-shikiji/src/node/utils/resolveLanguage.ts
Normal file
@ -0,0 +1,8 @@
|
||||
const VUE_RE = /-vue$/
|
||||
|
||||
export function resolveLanguage(info: string): string {
|
||||
return info
|
||||
.match(/^([^ :[{]+)/)?.[1]
|
||||
?.replace(VUE_RE, '')
|
||||
.toLowerCase() ?? ''
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
"extends": "../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"types": ["vuepress/client-types"],
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"include": ["./src"]
|
||||
|
||||
4002
pnpm-lock.yaml
generated
4002
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -63,7 +63,7 @@
|
||||
"ts:watch": "tsc -b tsconfig.build.json --watch"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vuepress": "2.0.0-rc.9"
|
||||
"vuepress": "2.0.0-rc.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pengzhanbo/utils": "^1.1.2",
|
||||
@ -76,22 +76,19 @@
|
||||
"@vuepress-plume/plugin-notes-data": "workspace:*",
|
||||
"@vuepress-plume/plugin-search": "workspace:*",
|
||||
"@vuepress-plume/plugin-shikiji": "workspace:*",
|
||||
"@vuepress/helper": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-active-header-links": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-comment": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-container": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-docsearch": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-external-link-icon": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-git": "2.0.0-rc.22",
|
||||
"@vuepress/plugin-medium-zoom": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-nprogress": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-palette": "2.0.0-rc.21",
|
||||
"@vuepress/plugin-reading-time": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-seo": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-sitemap": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-theme-data": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-toc": "2.0.0-rc.28",
|
||||
"@vuepress/plugin-watermark": "2.0.0-rc.28",
|
||||
"@vuepress/helper": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-active-header-links": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-comment": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-docsearch": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-git": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-markdown-container": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-medium-zoom": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-nprogress": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-reading-time": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-seo": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-sitemap": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-theme-data": "2.0.0-rc.31",
|
||||
"@vuepress/plugin-watermark": "2.0.0-rc.31",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"bcrypt-ts": "^5.0.2",
|
||||
"date-fns": "^3.6.0",
|
||||
@ -100,7 +97,7 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"vuepress-plugin-md-enhance": "^2.0.0-rc.39",
|
||||
"vuepress-plugin-md-enhance": "^2.0.0-rc.44",
|
||||
"vuepress-plugin-md-power": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { resolveRoutePath, useRouter } from 'vuepress/client'
|
||||
import { EXTERNAL_URL_RE } from '../utils/index.js'
|
||||
import { resolveRouteFullPath, useRoute, useRouter } from 'vuepress/client'
|
||||
import { isLinkExternal } from '@vuepress/helper/client'
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string
|
||||
@ -11,18 +11,21 @@ const props = defineProps<{
|
||||
rel?: string
|
||||
}>()
|
||||
|
||||
declare const __VUEPRESS_BASE__: string
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
|
||||
const isExternal = computed(
|
||||
() => props.href && EXTERNAL_URL_RE.test(props.href),
|
||||
() => props.href && isLinkExternal(props.href, __VUEPRESS_BASE__),
|
||||
)
|
||||
const link = computed(() => {
|
||||
if (!props.href)
|
||||
return undefined
|
||||
if (isExternal.value)
|
||||
return props.href
|
||||
return resolveRoutePath(props.href)
|
||||
return resolveRouteFullPath(props.href, route.path)
|
||||
})
|
||||
|
||||
function linkTo(e: Event) {
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { PlumeThemeBlog } from '../../../shared/index.js'
|
||||
|
||||
type NonFalseAndNullable<T> = T extends false | null | undefined ? never : T
|
||||
import { useData } from '../../composables/index.js'
|
||||
|
||||
defineProps<{
|
||||
pagination: NonFalseAndNullable<PlumeThemeBlog['pagination']>
|
||||
page: number
|
||||
totalPage: number
|
||||
isFirstPage: boolean
|
||||
@ -12,6 +9,8 @@ defineProps<{
|
||||
pageRange: { value: number | string, more?: true }[]
|
||||
}>()
|
||||
const emit = defineEmits<{ change: [value: number] }>()
|
||||
|
||||
const { theme } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -22,7 +21,7 @@ const emit = defineEmits<{ change: [value: number] }>()
|
||||
:disabled="isFirstPage"
|
||||
@click="() => emit('change', page - 1)"
|
||||
>
|
||||
{{ pagination?.prevPageText || 'Prev' }}
|
||||
{{ theme.prevPageLabel || 'Prev' }}
|
||||
</button>
|
||||
<div class="page-range">
|
||||
<button
|
||||
@ -43,7 +42,7 @@ const emit = defineEmits<{ change: [value: number] }>()
|
||||
:disabled="isLastPage"
|
||||
@click="() => emit('change', page + 1)"
|
||||
>
|
||||
{{ pagination?.nextPageText || 'Next' }}
|
||||
{{ theme.nextPageLabel || 'Next' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -5,7 +5,6 @@ import PostItem from './PostItem.vue'
|
||||
import Pagination from './Pagination.vue'
|
||||
|
||||
const {
|
||||
pagination,
|
||||
postList,
|
||||
page,
|
||||
totalPage,
|
||||
@ -29,7 +28,6 @@ const {
|
||||
</template>
|
||||
<Pagination
|
||||
v-if="isPaginationEnabled"
|
||||
:pagination="pagination"
|
||||
:page="page"
|
||||
:total-page="totalPage"
|
||||
:page-range="pageRange"
|
||||
|
||||
@ -5,11 +5,13 @@ import type { PlumeThemeFriendsFrontmatter } from '../../shared/index.js'
|
||||
import { useEditNavLink } from '../composables/index.js'
|
||||
import AutoLink from './AutoLink.vue'
|
||||
import FriendsItem from './FriendsItem.vue'
|
||||
import FriendsGroup from './FriendsGroup.vue'
|
||||
|
||||
const matter = usePageFrontmatter<PlumeThemeFriendsFrontmatter>()
|
||||
const editNavLink = useEditNavLink()
|
||||
|
||||
const list = computed(() => matter.value.list || [])
|
||||
const groups = computed(() => matter.value.groups || [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -28,6 +30,8 @@ const list = computed(() => matter.value.list || [])
|
||||
/>
|
||||
</section>
|
||||
|
||||
<FriendsGroup v-for="(group, index) in groups" :key="index" :group="group" />
|
||||
|
||||
<div v-if="editNavLink" class="edit-link">
|
||||
<AutoLink
|
||||
class="edit-link-button"
|
||||
@ -62,7 +66,7 @@ const list = computed(() => matter.value.list || [])
|
||||
margin-bottom: 1rem;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand-1);
|
||||
color: var(--vp-c-text-1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -87,9 +91,8 @@ const list = computed(() => matter.value.list || [])
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.friends-wrapper .title,
|
||||
.friends-wrapper .description,
|
||||
.edit-link {
|
||||
padding-left: 0;
|
||||
.friends-wrapper .description {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
@ -104,6 +107,12 @@ const list = computed(() => matter.value.list || [])
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.friends-wrapper .title,
|
||||
.friends-wrapper .description,
|
||||
.edit-link {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
81
theme/src/client/components/FriendsGroup.vue
Normal file
81
theme/src/client/components/FriendsGroup.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FriendGroup } from '../../shared/index.js'
|
||||
import FriendsItem from './FriendsItem.vue'
|
||||
|
||||
defineProps<{
|
||||
group: FriendGroup
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="friends-group">
|
||||
<h3 class="title">
|
||||
{{ group.title || 'My Friends' }}
|
||||
</h3>
|
||||
<p v-if="group.desc" class="description">
|
||||
{{ group.desc }}
|
||||
</p>
|
||||
<section v-if="group.list?.length" class="friends-list">
|
||||
<FriendsItem v-for="(friend, index) in group.list" :key="friend.name + index" :friend="friend" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.friends-group {
|
||||
width: 100%;
|
||||
padding: 64px 16px 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.friends-group .title {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
border-top: solid 1px var(--vp-c-divider);
|
||||
outline: none;
|
||||
transition: color var(--t-color), border-color var(--t-color);
|
||||
}
|
||||
|
||||
.friends-group .description {
|
||||
margin-bottom: 16px;
|
||||
line-height: 28px;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color var(--t-color);
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.friends-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.friends-group {
|
||||
padding: 64px 0 0;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.friends-group {
|
||||
max-width: 1104px;
|
||||
}
|
||||
|
||||
.friends-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,14 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { isPlainObject } from '@vuepress/helper/client'
|
||||
import { computed } from 'vue'
|
||||
import type { FriendsItem } from '../../shared/index'
|
||||
import { useDarkMode } from '../composables/index.js'
|
||||
import AutoLink from './AutoLink.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
friend: FriendsItem
|
||||
}>()
|
||||
|
||||
const isDark = useDarkMode()
|
||||
|
||||
function getStyle(name: string, color?: string | { light: string, dark: string }) {
|
||||
if (!color)
|
||||
return {}
|
||||
const value = isPlainObject(color) ? isDark.value ? color.dark : color.light : color
|
||||
return value ? { [name]: value } : {}
|
||||
}
|
||||
|
||||
const friendStyle = computed(() => {
|
||||
return {
|
||||
...getStyle('--vp-friends-text-color', props.friend.color),
|
||||
...getStyle('--vp-friends-bg-color', props.friend.backgroundColor),
|
||||
...getStyle('--vp-friends-border-color', props.friend.borderColor),
|
||||
...getStyle('--vp-friends-name-color', props.friend.nameColor),
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="friend">
|
||||
<div class="friend" :style="friendStyle">
|
||||
<AutoLink
|
||||
class="avatar-link"
|
||||
:href="friend.link"
|
||||
@ -49,7 +70,7 @@ defineProps<{
|
||||
}
|
||||
|
||||
.friend:hover {
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
@ -77,7 +98,7 @@ defineProps<{
|
||||
margin-left: -16px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-friends-link-color);
|
||||
color: var(--vp-friends-name-color);
|
||||
border-bottom: 1px dashed var(--vp-friends-border-color);
|
||||
transition: color var(--t-color), border-bottom var(--t-color);
|
||||
}
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSidebar } from '../composables/index.js'
|
||||
import { computed } from 'vue'
|
||||
import { useData, useSidebar } from '../composables/index.js'
|
||||
|
||||
const props = defineProps<{
|
||||
isNotFound?: boolean
|
||||
}>()
|
||||
|
||||
const { hasSidebar } = useSidebar()
|
||||
const { theme, frontmatter } = useData()
|
||||
|
||||
const enabledExternalIcon = computed(() => {
|
||||
return frontmatter.value.externalLink ?? theme.value.externalLinkIcon ?? true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="LayoutContent"
|
||||
class="layout-content"
|
||||
:class="{ 'has-sidebar': hasSidebar && !props.isNotFound }"
|
||||
id="LayoutContent" class="layout-content" :class="{
|
||||
'has-sidebar': hasSidebar && !props.isNotFound,
|
||||
'external-link-icon-enabled': enabledExternalIcon,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageData } from 'vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import { useMediumZoom } from '@vuepress/plugin-medium-zoom/client'
|
||||
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import { useDarkMode, useSidebar } from '../composables/index.js'
|
||||
import { useData, useSidebar } from '../composables/index.js'
|
||||
import { usePageEncrypt } from '../composables/encrypt.js'
|
||||
import PageAside from './PageAside.vue'
|
||||
import PageFooter from './PageFooter.vue'
|
||||
@ -13,8 +11,7 @@ import EncryptPage from './EncryptPage.vue'
|
||||
import TransitionFadeSlideY from './TransitionFadeSlideY.vue'
|
||||
|
||||
const { hasSidebar, hasAside } = useSidebar()
|
||||
const isDark = useDarkMode()
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const { page, isDark } = useData()
|
||||
|
||||
const { isPageDecrypted } = usePageEncrypt()
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageData } from 'vuepress/client'
|
||||
import { computed, ref } from 'vue'
|
||||
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
|
||||
import { useActiveAnchor, useThemeLocaleData } from '../composables/index.js'
|
||||
import { useActiveAnchor, useData } from '../composables/index.js'
|
||||
import PageAsideItem from './PageAsideItem.vue'
|
||||
|
||||
const page = usePageData()
|
||||
const theme = useThemeLocaleData()
|
||||
const { page, theme } = useData()
|
||||
|
||||
const headers = ref(page.value.headers)
|
||||
const hasOutline = computed(() => headers.value.length > 0)
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { usePageData, usePageFrontmatter } from 'vuepress/client'
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
useReadingTimeLocale,
|
||||
} from '@vuepress/plugin-reading-time/client'
|
||||
import type {
|
||||
PlumeThemePageData,
|
||||
PlumeThemePostFrontmatter,
|
||||
} from '../../shared/index.js'
|
||||
import { useExtraBlogData } from '../composables/index.js'
|
||||
import { useReadingTimeLocale } from '@vuepress/plugin-reading-time/client'
|
||||
import { useData, useExtraBlogData } from '../composables/index.js'
|
||||
|
||||
const { page, frontmatter: matter } = useData<'post'>()
|
||||
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const matter = usePageFrontmatter<PlumeThemePostFrontmatter>()
|
||||
const extraData = useExtraBlogData()
|
||||
const readingTime = useReadingTimeLocale()
|
||||
|
||||
|
||||
@ -1,72 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useDarkMode } from '../composables/index.js'
|
||||
import { useThemeLocaleData } from '../composables/themeData.js'
|
||||
import { APPEARANCE_KEY } from '../utils/index.js'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { useData } from '../composables/index.js'
|
||||
import Switch from './Switch.vue'
|
||||
|
||||
const theme = useThemeLocaleData()
|
||||
const checked = ref(false)
|
||||
const isDark = useDarkMode()
|
||||
const { theme, isDark } = useData()
|
||||
|
||||
const toggle = typeof document !== 'undefined' ? useAppearance() : () => {}
|
||||
|
||||
onMounted(() => {
|
||||
checked.value = document.documentElement.classList.contains('dark')
|
||||
const toggleAppearance = inject('toggle-appearance', () => {
|
||||
isDark.value = !isDark.value
|
||||
})
|
||||
|
||||
function useAppearance() {
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const classList = document.documentElement.classList
|
||||
|
||||
let userPreference = localStorage.getItem(APPEARANCE_KEY)
|
||||
|
||||
let isDark
|
||||
= (theme.value.appearance === 'dark' && userPreference == null)
|
||||
|| (userPreference === 'auto' || userPreference == null
|
||||
? query.matches
|
||||
: userPreference === 'dark')
|
||||
|
||||
query.onchange = (e) => {
|
||||
if (userPreference === 'auto')
|
||||
setClass((isDark = e.matches))
|
||||
}
|
||||
|
||||
setClass(isDark)
|
||||
|
||||
function toggle() {
|
||||
setClass((isDark = !isDark))
|
||||
|
||||
userPreference = isDark
|
||||
? query.matches
|
||||
? 'auto'
|
||||
: 'dark'
|
||||
: query.matches
|
||||
? 'light'
|
||||
: 'auto'
|
||||
|
||||
localStorage.setItem(APPEARANCE_KEY, userPreference)
|
||||
}
|
||||
|
||||
function setClass(dark: boolean): void {
|
||||
checked.value = dark
|
||||
classList[dark ? 'add' : 'remove']('dark')
|
||||
}
|
||||
|
||||
return toggle
|
||||
}
|
||||
|
||||
watch(checked, (newIsDark) => {
|
||||
isDark.value = newIsDark
|
||||
const switchTitle = computed(() => {
|
||||
return isDark.value
|
||||
? theme.value.lightModeSwitchTitle || 'Switch to light theme'
|
||||
: theme.value.darkModeSwitchTitle || 'Switch to dark theme'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch
|
||||
class="switch-appearance"
|
||||
aria-label="toggle dark mode"
|
||||
:title="switchTitle"
|
||||
:aria-checked="checked"
|
||||
@click="toggle"
|
||||
@click="toggleAppearance"
|
||||
>
|
||||
<span class="vpi-sun sun" />
|
||||
<span class="vpi-moon moon" />
|
||||
|
||||
@ -20,8 +20,8 @@ function setStyle(item: Element) {
|
||||
const el = item as HTMLElement
|
||||
|
||||
if (!_transition) {
|
||||
const value = typeof window !== 'undefined' && window.getComputedStyle ? window.getComputedStyle(el).transition : ''
|
||||
_transition = value && !value.includes('all') ? `${value}, ` : ' '
|
||||
const value = typeof window !== 'undefined' ? window.getComputedStyle?.(el).transition : ''
|
||||
_transition = value && !value.includes('all') ? `${value || ''}, ` : ' '
|
||||
}
|
||||
|
||||
el.style.transition = `${_transition}transform ${props.duration}s ease-in-out ${props.delay}s, opacity ${props.duration}s ease-in-out ${props.delay}s`
|
||||
|
||||
@ -3,7 +3,7 @@ import { useExtraBlogData as _useExtraBlogData, useBlogPostData } from '@vuepres
|
||||
import { type Ref, computed } from 'vue'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
|
||||
import { useLocaleLink, useRouteQuery, useThemeLocaleData } from '../composables/index.js'
|
||||
import { useData, useLocaleLink, useRouteQuery } from '../composables/index.js'
|
||||
import { toArray } from '../utils/index.js'
|
||||
|
||||
export const useExtraBlogData = _useExtraBlogData as () => Ref<{
|
||||
@ -20,11 +20,10 @@ export function useLocalePostList() {
|
||||
}
|
||||
|
||||
export function usePostListControl() {
|
||||
const themeData = useThemeLocaleData()
|
||||
const { theme } = useData()
|
||||
|
||||
const list = useLocalePostList()
|
||||
const blog = computed(() => themeData.value.blog || {})
|
||||
const pagination = computed(() => blog.value.pagination || {})
|
||||
const blog = computed(() => theme.value.blog || {})
|
||||
const is960 = useMediaQuery('(max-width: 960px)')
|
||||
|
||||
const postList = computed(() => {
|
||||
@ -128,7 +127,6 @@ export function usePostListControl() {
|
||||
}
|
||||
|
||||
return {
|
||||
pagination,
|
||||
postList: finalList,
|
||||
page,
|
||||
totalPage,
|
||||
@ -147,14 +145,15 @@ const extractLocales: Record<string, { tags: string, archives: string }> = {
|
||||
}
|
||||
|
||||
export function useBlogExtract() {
|
||||
const theme = useThemeLocaleData()
|
||||
const { theme } = useData()
|
||||
const locale = usePageLang()
|
||||
const postList = useLocalePostList()
|
||||
const { tags: tagsList } = useTags()
|
||||
const blog = computed(() => theme.value.blog || {})
|
||||
|
||||
const hasBlogExtract = computed(() => theme.value.blog?.archives !== false || theme.value.blog?.tags !== false)
|
||||
const tagsLink = useLocaleLink('blog/tags/')
|
||||
const archiveLink = useLocaleLink('blog/archives/')
|
||||
const hasBlogExtract = computed(() => blog.value.archives !== false || blog.value.tags !== false)
|
||||
const tagsLink = useLocaleLink(blog.value.tagsLink || 'blog/tags/')
|
||||
const archiveLink = useLocaleLink(blog.value.archivesLink || 'blog/archives/')
|
||||
|
||||
const tags = computed(() => ({
|
||||
link: tagsLink.value,
|
||||
|
||||
@ -1,39 +1,45 @@
|
||||
import { inject, onMounted, ref } from 'vue'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { inject, ref } from 'vue'
|
||||
import type { App, InjectionKey, Ref } from 'vue'
|
||||
import { useThemeData } from './themeData.js'
|
||||
|
||||
export type DarkModeRef = Ref<boolean>
|
||||
type DarkModeRef = Ref<boolean>
|
||||
|
||||
export const darkModeSymbol: InjectionKey<DarkModeRef> = Symbol(
|
||||
__VUEPRESS_DEV__ ? 'darkMode' : '',
|
||||
)
|
||||
|
||||
/**
|
||||
* Inject dark mode global computed
|
||||
*/
|
||||
export function useDarkMode(): DarkModeRef {
|
||||
const isDark = inject(darkModeSymbol)
|
||||
if (isDark === undefined)
|
||||
throw new Error('useDarkMode() is called without provider.')
|
||||
export function setupDarkMode(app: App): void {
|
||||
const themeLocale = useThemeData()
|
||||
|
||||
return isDark
|
||||
}
|
||||
const appearance = themeLocale.value.appearance
|
||||
const isDark
|
||||
= appearance === 'force-dark'
|
||||
? ref(true)
|
||||
: appearance
|
||||
? useDark({
|
||||
storageKey: 'vuepress-theme-appearance',
|
||||
disableTransition: false,
|
||||
initialValue: () =>
|
||||
typeof appearance === 'string' ? appearance : 'auto',
|
||||
...(typeof appearance === 'object' ? appearance : {}),
|
||||
})
|
||||
: ref(false)
|
||||
|
||||
/**
|
||||
* Create dark mode ref and provide as global computed in setup
|
||||
*/
|
||||
export function setupDarkMode(): void {
|
||||
const isDark = useDarkMode()
|
||||
onMounted(() => {
|
||||
if (document.documentElement.classList.contains('dark'))
|
||||
isDark.value = true
|
||||
})
|
||||
}
|
||||
|
||||
export function injectDarkMode(app: App): void {
|
||||
const isDark = ref<boolean>(false)
|
||||
app.provide(darkModeSymbol, isDark)
|
||||
|
||||
Object.defineProperty(app.config.globalProperties, '$isDark', {
|
||||
get: () => isDark,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject dark mode global computed
|
||||
*/
|
||||
export function useDarkMode(): DarkModeRef {
|
||||
const isDarkMode = inject(darkModeSymbol)
|
||||
if (!isDarkMode)
|
||||
throw new Error('useDarkMode() is called without provider.')
|
||||
|
||||
return isDarkMode
|
||||
}
|
||||
|
||||
46
theme/src/client/composables/data.ts
Normal file
46
theme/src/client/composables/data.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { ThemeLocaleDataRef } from '@vuepress/plugin-theme-data/client'
|
||||
import {
|
||||
usePageData,
|
||||
usePageFrontmatter,
|
||||
useSiteLocaleData,
|
||||
} from 'vuepress/client'
|
||||
import type {
|
||||
PageDataRef,
|
||||
PageFrontmatterRef,
|
||||
SiteLocaleDataRef,
|
||||
} from 'vuepress/client'
|
||||
import type {
|
||||
PlumeThemeLocaleData,
|
||||
PlumeThemePageData,
|
||||
PlumeThemePageFrontmatter,
|
||||
PlumeThemePostFrontmatter,
|
||||
} from '../../shared/index.js'
|
||||
import { useThemeLocaleData } from './themeData.js'
|
||||
import { hashRef } from './hash.js'
|
||||
import { useDarkMode } from './darkMode.js'
|
||||
|
||||
type FrontmatterType = 'post' | 'page'
|
||||
|
||||
type Frontmatter<T extends FrontmatterType = 'page'> = T extends 'post'
|
||||
? PlumeThemePostFrontmatter
|
||||
: PlumeThemePageFrontmatter
|
||||
|
||||
export interface Data<T extends FrontmatterType = 'page'> {
|
||||
theme: ThemeLocaleDataRef<PlumeThemeLocaleData>
|
||||
page: PageDataRef<PlumeThemePageData>
|
||||
frontmatter: PageFrontmatterRef<Frontmatter<T>>
|
||||
hash: Ref<string>
|
||||
site: SiteLocaleDataRef
|
||||
isDark: Ref<boolean>
|
||||
}
|
||||
|
||||
export function useData<T extends FrontmatterType = 'page'>(): Data<T> {
|
||||
const theme = useThemeLocaleData()
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const frontmatter = usePageFrontmatter<Frontmatter<T>>()
|
||||
const site = useSiteLocaleData()
|
||||
const isDark = useDarkMode()
|
||||
|
||||
return { theme, page, frontmatter, hash: hashRef, site, isDark }
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import { compareSync, genSaltSync } from 'bcrypt-ts/browser'
|
||||
import { type Ref, computed } from 'vue'
|
||||
import { hasOwn, useSessionStorage } from '@vueuse/core'
|
||||
import { usePageData, useRoute } from 'vuepress/client'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import { useRoute } from 'vuepress/client'
|
||||
import { useData } from './data.js'
|
||||
|
||||
declare const __PLUME_ENCRYPT_GLOBAL__: boolean
|
||||
declare const __PLUME_ENCRYPT_SEPARATOR__: string
|
||||
@ -88,7 +88,7 @@ export function useGlobalEncrypt(): {
|
||||
}
|
||||
|
||||
export function usePageEncrypt() {
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
const { page } = useData()
|
||||
const route = useRoute()
|
||||
|
||||
const hasPageEncrypt = computed(() => ruleList.length ? matches.some(toMatch) : false)
|
||||
|
||||
@ -9,3 +9,4 @@ export * from './blog.js'
|
||||
export * from './locale.js'
|
||||
export * from './useRouteQuery.js'
|
||||
export * from './watermark.js'
|
||||
export * from './data.js'
|
||||
|
||||
@ -25,8 +25,7 @@ export function useEditNavLink(): ComputedRef<null | NavItemWithLink> {
|
||||
return null
|
||||
|
||||
const {
|
||||
repo,
|
||||
docsRepo = repo,
|
||||
docsRepo,
|
||||
docsBranch = 'main',
|
||||
docsDir = '',
|
||||
editLinkText,
|
||||
|
||||
@ -10,7 +10,6 @@ import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
import type { PlumeThemePageData } from '../../shared/index.js'
|
||||
import { isActive } from '../utils/index.js'
|
||||
import { useThemeLocaleData } from './themeData.js'
|
||||
import { hashRef } from './hash.js'
|
||||
|
||||
export { useNotesData }
|
||||
@ -58,7 +57,6 @@ export function getSidebarFirstLink(sidebar: NotesSidebarItem[]) {
|
||||
export function useSidebar() {
|
||||
const route = useRoute()
|
||||
const notesData = useNotesData()
|
||||
const theme = useThemeLocaleData()
|
||||
const frontmatter = usePageFrontmatter()
|
||||
const page = usePageData<PlumeThemePageData>()
|
||||
|
||||
@ -74,7 +72,7 @@ export function useSidebar() {
|
||||
})
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return theme.value.notes ? getSidebarList(route.path, notesData.value) : []
|
||||
return getSidebarList(route.path, notesData.value)
|
||||
})
|
||||
const hasSidebar = computed(() => {
|
||||
return (
|
||||
|
||||
@ -4,23 +4,17 @@ import { defineClientConfig } from 'vuepress/client'
|
||||
import type { ClientConfig } from 'vuepress/client'
|
||||
import { h } from 'vue'
|
||||
import Badge from './components/global/Badge.vue'
|
||||
import ExternalLinkIcon from './components/global/ExternalLinkIcon.vue'
|
||||
import { injectDarkMode, setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
|
||||
import { setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
|
||||
import Layout from './layouts/Layout.vue'
|
||||
import NotFound from './layouts/NotFound.vue'
|
||||
import HomeBox from './components/Home/HomeBox.vue'
|
||||
|
||||
export default defineClientConfig({
|
||||
enhance({ app, router }) {
|
||||
injectDarkMode(app)
|
||||
|
||||
setupDarkMode(app)
|
||||
// global component
|
||||
app.component('Badge', Badge)
|
||||
|
||||
if (app._context.components.ExternalLinkIcon)
|
||||
delete app._context.components.ExternalLinkIcon
|
||||
app.component('ExternalLinkIcon', ExternalLinkIcon)
|
||||
|
||||
app.component('DocSearch', () => {
|
||||
const SearchComponent
|
||||
= app.component('Docsearch') || app.component('SearchBox')
|
||||
@ -56,7 +50,6 @@ export default defineClientConfig({
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
setupDarkMode()
|
||||
setupWatermark()
|
||||
},
|
||||
layouts: {
|
||||
|
||||
@ -242,6 +242,7 @@ div[class*="language-"] {
|
||||
margin: 0 -24px;
|
||||
transition: background-color 0.5s;
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
@ -281,3 +282,78 @@ div[class*="language-"] {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[class*="language-"] button.copy {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
|
||||
/* rtl:ignore */
|
||||
right: 12px;
|
||||
z-index: 3;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
background-color: var(--vp-code-copy-code-bg);
|
||||
background-image: var(--vp-icon-copy);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: 20px;
|
||||
border: 1px solid var(--vp-code-copy-code-border-color);
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
border-color 0.25s,
|
||||
background-color 0.25s,
|
||||
opacity 0.25s;
|
||||
|
||||
/* rtl:ignore */
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[class*="language-"]:hover > button.copy,
|
||||
[class*="language-"] > button.copy:focus,
|
||||
[class*="language-"] > button.copy.copied {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[class*="language-"] > button.copy:hover,
|
||||
[class*="language-"] > button.copy.copied {
|
||||
background-color: var(--vp-code-copy-code-hover-bg);
|
||||
border-color: var(--vp-code-copy-code-hover-border-color);
|
||||
}
|
||||
|
||||
[class*="language-"] > button.copy.copied,
|
||||
[class*="language-"] > button.copy:hover.copied {
|
||||
background-color: var(--vp-code-copy-code-hover-bg);
|
||||
background-image: var(--vp-icon-copied);
|
||||
|
||||
/* rtl:ignore */
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
[class*="language-"] > button.copy.copied::before,
|
||||
[class*="language-"] > button.copy:hover.copied::before {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-code-copy-code-active-text);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
content: attr(data-copied);
|
||||
background-color: var(--vp-code-copy-code-hover-bg);
|
||||
border: 1px solid var(--vp-code-copy-code-hover-border-color);
|
||||
|
||||
/* rtl:ignore */
|
||||
border-right: 0;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
/* rtl:ignore */
|
||||
transform: translateX(calc(-100% - 1px));
|
||||
}
|
||||
|
||||
@ -268,3 +268,31 @@
|
||||
.plume-content a:hover > code {
|
||||
color: var(--vp-code-link-hover-color);
|
||||
}
|
||||
|
||||
/**
|
||||
* External links
|
||||
* -------------------------------------------------------------------------- */
|
||||
:is(.vp-external-link-icon, .plume-content a[href*='://'], .plume-content a[target='_blank']):not(.no-icon)::after {
|
||||
--icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");
|
||||
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-top: -1px;
|
||||
margin-left: 4px;
|
||||
color: var(--vp-c-text-3);
|
||||
background: currentcolor;
|
||||
-webkit-mask-image: var(--icon);
|
||||
mask-image: var(--icon);
|
||||
}
|
||||
|
||||
.vp-external-link-icon::after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
.external-link-icon-enabled :is(.plume-content a[href*='://'], .plume-content a[target='_blank'])::after {
|
||||
color: currentcolor;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
@import "@vuepress/plugin-palette/palette";
|
||||
|
||||
@import "vars";
|
||||
@import "fonts";
|
||||
@import "normalize";
|
||||
@ -13,5 +11,3 @@
|
||||
@import "twoslash";
|
||||
@import "md-enhance";
|
||||
@import "search";
|
||||
|
||||
@import "@vuepress/plugin-palette/style";
|
||||
|
||||
@ -20,28 +20,6 @@
|
||||
--twoslash-tag-annotate-bg: var(--vp-c-green-soft);
|
||||
}
|
||||
|
||||
div[class*="language-"].line-numbers-mode:has(> .twoslash) {
|
||||
.line-numbers {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding-left: 1.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not selector(:has(a)) {
|
||||
div[class*="language-"] .twoslash + .line-numbers {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*="language-"] pre.twoslash {
|
||||
padding-left: 1.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect people's wishes to not have animations */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.twoslash * {
|
||||
|
||||
@ -355,12 +355,14 @@
|
||||
--vp-code-line-warning-color: var(--vp-c-yellow-soft);
|
||||
--vp-code-line-error-color: var(--vp-c-red-soft);
|
||||
|
||||
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");
|
||||
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E");
|
||||
|
||||
--vp-code-copy-code-border-color: var(--vp-c-divider);
|
||||
--vp-code-copy-code-bg: var(--vp-c-bg-soft);
|
||||
--vp-code-copy-code-hover-border-color: var(--vp-c-divider);
|
||||
--vp-code-copy-code-hover-bg: var(--vp-c-bg);
|
||||
--vp-code-copy-code-active-text: var(--vp-c-text-2);
|
||||
--vp-code-copy-copied-text-content: "Copied";
|
||||
|
||||
--vp-code-tab-divider: var(--vp-code-block-divider-color);
|
||||
--vp-code-tab-text-color: var(--vp-c-text-2);
|
||||
@ -370,10 +372,6 @@
|
||||
--vp-code-tab-active-bar-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
html[lang="zh-CN"] {
|
||||
--vp-code-copy-copied-text-content: "已复制";
|
||||
}
|
||||
|
||||
/**
|
||||
* Component: Button
|
||||
* -------------------------------------------------------------------------- */
|
||||
@ -527,7 +525,7 @@ html[lang="zh-CN"] {
|
||||
:root {
|
||||
--vp-friends-text-color: var(--vp-c-text-1);
|
||||
--vp-friends-bg-color: var(--vp-c-bg);
|
||||
--vp-friends-link-color: var(--vp-c-brand-1);
|
||||
--vp-friends-name-color: var(--vp-c-brand-1);
|
||||
--vp-friends-border-color: var(--vp-c-border);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export const EXTERNAL_URL_RE = /^[a-z]+:/i
|
||||
export const PATHNAME_PROTOCOL_RE = /^pathname:\/\//
|
||||
export const APPEARANCE_KEY = 'vuepress-theme-appearance'
|
||||
export const HASH_RE = /#.*$/
|
||||
export const EXT_RE = /(index)?\.(md|html)$/
|
||||
|
||||
|
||||
@ -1,190 +0,0 @@
|
||||
import { path } from 'vuepress/utils'
|
||||
import type { App } from 'vuepress/core'
|
||||
import { resolveLocalePath } from 'vuepress/shared'
|
||||
import type {
|
||||
AutoFrontmatterOptions,
|
||||
FrontmatterArray,
|
||||
FrontmatterObject,
|
||||
} from '@vuepress-plume/plugin-auto-frontmatter'
|
||||
import { format } from 'date-fns'
|
||||
import { uniq } from '@pengzhanbo/utils'
|
||||
import type {
|
||||
PlumeThemeLocaleOptions,
|
||||
PlumeThemePluginOptions,
|
||||
} from '../shared/index.js'
|
||||
import { getCurrentDirname, getPackage, nanoid, pathJoin } from './utils.js'
|
||||
import { resolveLinkBySidebar, resolveNotesList } from './resolveNotesList.js'
|
||||
import { resolveLocaleOptions } from './resolveLocaleOptions.js'
|
||||
|
||||
export default function autoFrontmatter(
|
||||
app: App,
|
||||
options: PlumeThemePluginOptions,
|
||||
localeOption: PlumeThemeLocaleOptions,
|
||||
): AutoFrontmatterOptions {
|
||||
const sourceDir = app.dir.source()
|
||||
const pkg = getPackage()
|
||||
const { locales = {}, article: articlePrefix = '/article/' } = localeOption
|
||||
const { frontmatter } = options
|
||||
const avatar = resolveLocaleOptions(localeOption, 'avatar')
|
||||
const notesList = resolveNotesList(localeOption)
|
||||
const localesNotesDirs = notesList
|
||||
.map(({ notes, dir }) => {
|
||||
const _dir = dir?.replace(/^\//, '')
|
||||
return notes.map(note => pathJoin(_dir, note.dir || ''))
|
||||
})
|
||||
.flat()
|
||||
.filter(Boolean)
|
||||
|
||||
const baseFrontmatter: FrontmatterObject = {
|
||||
author(author: string, _, data: any) {
|
||||
if (author)
|
||||
return author
|
||||
if (data.friends)
|
||||
return
|
||||
return avatar?.name || pkg.author || ''
|
||||
},
|
||||
createTime(formatTime: string, { createTime }, data: any) {
|
||||
if (formatTime)
|
||||
return formatTime
|
||||
if (data.friends)
|
||||
return
|
||||
return format(new Date(createTime), 'yyyy/MM/dd HH:mm:ss')
|
||||
},
|
||||
}
|
||||
|
||||
const resolveLocale = (filepath: string) => {
|
||||
const file = pathJoin('/', path.relative(sourceDir, filepath))
|
||||
|
||||
return resolveLocalePath(localeOption.locales!, file)
|
||||
}
|
||||
const notesByLocale = (locale: string) => {
|
||||
const notes = resolveLocaleOptions(localeOption, 'notes', locale)
|
||||
if (notes === false)
|
||||
return undefined
|
||||
return notes
|
||||
}
|
||||
|
||||
const findNote = (filepath: string) => {
|
||||
const file = pathJoin('/', path.relative(sourceDir, filepath))
|
||||
const locale = resolveLocalePath(locales, file)
|
||||
const notes = notesByLocale(locale)
|
||||
if (!notes)
|
||||
return undefined
|
||||
const notesList = notes?.notes || []
|
||||
const notesDir = notes?.dir || ''
|
||||
return notesList.find(note =>
|
||||
file.startsWith(path.join(locale, notesDir, note.dir)),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
include: frontmatter?.include ?? ['**/*.md'],
|
||||
exclude: uniq(['.vuepress/**/*', 'node_modules', ...(frontmatter?.exclude ?? [])]),
|
||||
|
||||
frontmatter: [
|
||||
localesNotesDirs.length
|
||||
? {
|
||||
// note 首页链接
|
||||
include: localesNotesDirs.map(dir => pathJoin(dir, '/{readme,README,index}.md')),
|
||||
frontmatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title)
|
||||
return title
|
||||
const note = findNote(filepath)
|
||||
if (note?.text)
|
||||
return note.text
|
||||
return getCurrentDirname(note?.dir, filepath) || ''
|
||||
},
|
||||
...baseFrontmatter,
|
||||
permalink(permalink: string, { filepath }, data: any) {
|
||||
if (permalink)
|
||||
return permalink
|
||||
if (data.friends)
|
||||
return
|
||||
const locale = resolveLocale(filepath)
|
||||
const notes = notesByLocale(locale)
|
||||
const note = findNote(filepath)
|
||||
return pathJoin(
|
||||
locale,
|
||||
notes?.link || '',
|
||||
note?.link || getCurrentDirname(note?.dir, filepath),
|
||||
'/',
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
: '',
|
||||
localesNotesDirs.length
|
||||
? {
|
||||
include: localesNotesDirs.map(dir => pathJoin(dir, '**/**.md')),
|
||||
frontmatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title)
|
||||
return title
|
||||
|
||||
const note = findNote(filepath)
|
||||
|
||||
let basename = path.basename(filepath, '.md')
|
||||
if (note?.sidebar === 'auto')
|
||||
basename = basename.replace(/^\d+\./, '')
|
||||
|
||||
return basename
|
||||
},
|
||||
...baseFrontmatter,
|
||||
permalink(permalink: string, { filepath }, data: any) {
|
||||
if (permalink)
|
||||
return permalink
|
||||
if (data.friends)
|
||||
return
|
||||
const locale = resolveLocale(filepath)
|
||||
const notes = notesByLocale(locale)
|
||||
const note = findNote(filepath)
|
||||
const args: string[] = [
|
||||
locale,
|
||||
notes?.link || '',
|
||||
note?.link || getCurrentDirname(note?.dir, filepath),
|
||||
]
|
||||
const sidebar = note?.sidebar
|
||||
|
||||
if (sidebar && sidebar !== 'auto') {
|
||||
const res = resolveLinkBySidebar(sidebar, pathJoin(notes?.dir || '', note?.dir || ''))
|
||||
const file = pathJoin('/', path.relative(sourceDir, filepath))
|
||||
res[file] && args.push(res[file])
|
||||
}
|
||||
|
||||
return pathJoin(...args, nanoid(), '/')
|
||||
},
|
||||
},
|
||||
}
|
||||
: '',
|
||||
{
|
||||
include: '**/{readme,README,index}.md',
|
||||
frontmatter: {},
|
||||
},
|
||||
{
|
||||
include: '*',
|
||||
frontmatter: {
|
||||
title(title: string, { filepath }) {
|
||||
if (title)
|
||||
return title
|
||||
const basename = path.basename(filepath, '.md')
|
||||
return basename
|
||||
},
|
||||
...baseFrontmatter,
|
||||
permalink(permalink: string, { filepath }) {
|
||||
if (permalink)
|
||||
return permalink
|
||||
const locale = resolveLocale(filepath)
|
||||
const prefix = resolveLocaleOptions(localeOption, 'article', locale, false)
|
||||
const args: string[] = []
|
||||
prefix
|
||||
? args.push(prefix)
|
||||
: args.push(locale, articlePrefix)
|
||||
|
||||
return pathJoin(...args, nanoid(), '/')
|
||||
},
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as FrontmatterArray,
|
||||
}
|
||||
}
|
||||
6
theme/src/node/config/index.ts
Normal file
6
theme/src/node/config/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from './resolveLocaleOptions.js'
|
||||
export * from './resolveThemeData.js'
|
||||
export * from './resolveSearchOptions.js'
|
||||
export * from './resolvePageHead.js'
|
||||
export * from './resolveEncrypt.js'
|
||||
export * from './resolveNotesOptions.js'
|
||||
@ -1,7 +1,7 @@
|
||||
import { genSaltSync, hashSync } from 'bcrypt-ts'
|
||||
import { isNumber, isString, random, toArray } from '@pengzhanbo/utils'
|
||||
import type { Page } from 'vuepress/core'
|
||||
import type { PlumeThemeEncrypt, PlumeThemePageData } from '../shared/index.js'
|
||||
import type { PlumeThemeEncrypt, PlumeThemePageData } from '../../shared/index.js'
|
||||
|
||||
const isStringLike = (value: unknown): boolean => isString(value) || isNumber(value)
|
||||
const separator = ':'
|
||||
41
theme/src/node/config/resolveLocaleOptions.ts
Normal file
41
theme/src/node/config/resolveLocaleOptions.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { entries, fromEntries, getLocaleConfig } from '@vuepress/helper'
|
||||
import type { App } from 'vuepress'
|
||||
import { LOCALE_OPTIONS } from '../locales/index.js'
|
||||
import type { PlumeThemeLocaleData, PlumeThemeLocaleOptions } from '../../shared/index.js'
|
||||
import { THEME_NAME } from '../utils.js'
|
||||
|
||||
const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
|
||||
appearance: true,
|
||||
|
||||
blog: { link: '/blog/', pagination: { perPage: 15 }, tags: true, archives: true, tagsLink: '/blog/tags/', archivesLink: '/blog/archives/' },
|
||||
article: '/article/',
|
||||
notes: { link: '/', dir: '/notes/', notes: [] },
|
||||
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],
|
||||
|
||||
// page meta
|
||||
editLink: true,
|
||||
contributors: true,
|
||||
|
||||
}
|
||||
|
||||
export function resolveLocaleOptions(app: App, { locales, ...options }: PlumeThemeLocaleOptions): PlumeThemeLocaleOptions {
|
||||
const resolvedOptions: PlumeThemeLocaleOptions = {
|
||||
...FALLBACK_OPTIONS,
|
||||
...options,
|
||||
locales: getLocaleConfig({
|
||||
app,
|
||||
name: THEME_NAME,
|
||||
default: LOCALE_OPTIONS,
|
||||
config: fromEntries(
|
||||
entries<PlumeThemeLocaleOptions>({
|
||||
'/': {},
|
||||
...locales,
|
||||
}).map(([locale, opt]) => [
|
||||
locale,
|
||||
{ ...options, ...opt },
|
||||
]),
|
||||
),
|
||||
}),
|
||||
}
|
||||
return resolvedOptions
|
||||
}
|
||||
37
theme/src/node/config/resolveNotesOptions.ts
Normal file
37
theme/src/node/config/resolveNotesOptions.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { NotesDataOptions, NotesSidebar } from '@vuepress-plume/plugin-notes-data'
|
||||
import { entries } from '@vuepress/helper'
|
||||
import { uniq } from '@pengzhanbo/utils'
|
||||
import type { PlumeThemeLocaleOptions } from '../..//shared/index.js'
|
||||
import { withBase } from '../utils.js'
|
||||
|
||||
export function resolveNotesLinkList(localeOptions: PlumeThemeLocaleOptions) {
|
||||
const locales = localeOptions.locales || {}
|
||||
const notesLinks: string[] = []
|
||||
for (const [locale, opt] of entries(locales)) {
|
||||
const config = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes
|
||||
if (config && config.notes?.length) {
|
||||
const prefix = config.link || ''
|
||||
notesLinks.push(
|
||||
...config.notes.map(
|
||||
note => withBase(`${prefix}/${note.link || ''}`, locale),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return uniq(notesLinks)
|
||||
}
|
||||
|
||||
export function resolveNotesOptions(localeOptions: PlumeThemeLocaleOptions): NotesDataOptions[] {
|
||||
const locales = localeOptions.locales || {}
|
||||
const notesOptionsList: NotesDataOptions[] = []
|
||||
for (const [locale, opt] of entries(locales)) {
|
||||
const options = locale === '/' ? (opt.notes || localeOptions.notes) : opt.notes
|
||||
if (options) {
|
||||
options.dir = withBase(options.dir, locale)
|
||||
notesOptionsList.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
return notesOptionsList
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Page } from 'vuepress'
|
||||
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
|
||||
import type { PlumeThemeLocaleOptions } from '../../shared/index.js'
|
||||
|
||||
export function resolvePageHead(page: Page, localeOptions: PlumeThemeLocaleOptions) {
|
||||
page.frontmatter.head ??= []
|
||||
27
theme/src/node/config/resolveSearchOptions.ts
Normal file
27
theme/src/node/config/resolveSearchOptions.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { getLocaleConfig } from '@vuepress/helper'
|
||||
import type { App } from 'vuepress'
|
||||
import type { DocsearchPluginOptions } from '@vuepress/plugin-docsearch'
|
||||
import type { SearchPluginOptions } from '@vuepress-plume/plugin-search'
|
||||
import { DOCSEARCH_LOCALES, SEARCH_LOCALES } from '../locales/index.js'
|
||||
|
||||
export function resolveSearchOptions(app: App, { locales, ...options }: SearchPluginOptions = {}): SearchPluginOptions {
|
||||
return {
|
||||
...options,
|
||||
locales: getLocaleConfig({
|
||||
app,
|
||||
default: SEARCH_LOCALES,
|
||||
config: locales,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDocsearchOptions(app: App, { locales, ...options }: DocsearchPluginOptions = {}): DocsearchPluginOptions {
|
||||
return {
|
||||
...options,
|
||||
locales: getLocaleConfig({
|
||||
app,
|
||||
default: DOCSEARCH_LOCALES,
|
||||
config: locales,
|
||||
}),
|
||||
}
|
||||
}
|
||||
61
theme/src/node/config/resolveThemeData.ts
Normal file
61
theme/src/node/config/resolveThemeData.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { entries, getRootLangPath } from '@vuepress/helper'
|
||||
import type { App } from 'vuepress'
|
||||
import type { NavItem, PlumeThemeLocaleOptions } from '../../shared/index.js'
|
||||
import { PRESET_LOCALES } from '../locales/index.js'
|
||||
import { withBase } from '../utils.js'
|
||||
|
||||
const EXCLUDE_LIST = ['locales', 'sidebar', 'navbar', 'notes', 'article']
|
||||
// 过滤不需要出现在多语言配置中的字段
|
||||
const EXCLUDE_LOCALE_LIST = [...EXCLUDE_LIST, 'blog', 'appearance']
|
||||
|
||||
export function resolveThemeData(app: App, options: PlumeThemeLocaleOptions): PlumeThemeLocaleOptions {
|
||||
const themeData: PlumeThemeLocaleOptions = { locales: {} }
|
||||
const root = getRootLangPath(app)
|
||||
|
||||
entries(options).forEach(([key, value]) => {
|
||||
if (!EXCLUDE_LIST.includes(key))
|
||||
themeData[key] = value
|
||||
})
|
||||
|
||||
entries(options.locales || {}).forEach(([locale, opt]) => {
|
||||
themeData.locales![locale] = {}
|
||||
entries(opt).forEach(([key, value]) => {
|
||||
if (!EXCLUDE_LOCALE_LIST.includes(key))
|
||||
themeData.locales![locale][key] = value
|
||||
})
|
||||
})
|
||||
|
||||
const blog = options.blog || {}
|
||||
const blogLink = blog.link || '/blog/'
|
||||
entries(options.locales || {}).forEach(([locale, opt]) => {
|
||||
// 注入预设 导航栏
|
||||
// home | blog | tags | archives
|
||||
if (opt.navbar !== false && opt.navbar?.length === 0) {
|
||||
// fallback navbar option
|
||||
const localePath = locale === '/' ? root : locale
|
||||
const navbar: NavItem[] = [{
|
||||
text: PRESET_LOCALES[localePath].home,
|
||||
link: locale,
|
||||
}]
|
||||
navbar.push({
|
||||
text: PRESET_LOCALES[localePath].blog,
|
||||
link: withBase(blogLink, locale),
|
||||
})
|
||||
blog.tags !== false && navbar.push({
|
||||
text: PRESET_LOCALES[locale].tag,
|
||||
link: withBase(blog.tagsLink || `${blogLink}/tags/`, locale),
|
||||
})
|
||||
blog.archives !== false && navbar.push({
|
||||
text: PRESET_LOCALES[locale].archive,
|
||||
link: withBase(blog.archivesLink || `${blogLink}/archives/`, locale),
|
||||
})
|
||||
|
||||
themeData.locales![locale].navbar = navbar
|
||||
}
|
||||
else {
|
||||
themeData.locales![locale].navbar = opt.navbar
|
||||
}
|
||||
})
|
||||
|
||||
return themeData
|
||||
}
|
||||
@ -1,180 +0,0 @@
|
||||
import type { App } from 'vuepress/core'
|
||||
import { deepClone, deepMerge } from '@pengzhanbo/utils'
|
||||
import type { PlumeThemeLocaleOptions } from '../shared/index.js'
|
||||
import { pathJoin } from './utils.js'
|
||||
import { resolveLocaleOptions, resolvedAppLocales } from './resolveLocaleOptions.js'
|
||||
|
||||
const defaultLocales: NonNullable<PlumeThemeLocaleOptions['locales']> = {
|
||||
'en-US': {
|
||||
selectLanguageName: 'English',
|
||||
selectLanguageText: 'Languages',
|
||||
editLinkText: 'Edit this page',
|
||||
contributorsText: 'Contributors',
|
||||
appearanceText: 'Appearance',
|
||||
|
||||
lastUpdated: {
|
||||
text: 'Last Updated',
|
||||
},
|
||||
|
||||
encryptButtonText: 'Confirm',
|
||||
encryptPlaceholder: 'Enter password',
|
||||
encryptGlobalText: 'Only password can access this site',
|
||||
encryptPageText: 'Only password can access this page',
|
||||
},
|
||||
'zh-CN': {
|
||||
selectLanguageName: '简体中文',
|
||||
selectLanguageText: '选择语言',
|
||||
|
||||
blog: { pagination: { prevPageText: '上一页', nextPageText: '下一页' } },
|
||||
|
||||
outlineLabel: '此页内容',
|
||||
returnToTopLabel: '返回顶部',
|
||||
editLinkText: '编辑此页',
|
||||
contributorsText: '贡献者',
|
||||
appearanceText: '外观',
|
||||
prevPageLabel: '上一页',
|
||||
nextPageLabel: '下一页',
|
||||
|
||||
lastUpdated: {
|
||||
text: '最后更新于',
|
||||
},
|
||||
|
||||
notFound: {
|
||||
code: '404',
|
||||
title: '页面未找到',
|
||||
quote: '但是,如果你不改变方向,并且一直寻找,最终可能会到达你要去的地方。',
|
||||
linkText: '返回首页',
|
||||
},
|
||||
|
||||
encryptButtonText: '确认',
|
||||
encryptPlaceholder: '请输入密码',
|
||||
encryptGlobalText: '本站只允许密码访问',
|
||||
encryptPageText: '本页面只允许密码访问',
|
||||
},
|
||||
}
|
||||
|
||||
export const fallbackLocaleOption: Partial<PlumeThemeLocaleOptions> = {
|
||||
article: '/article/',
|
||||
notes: { link: '/', dir: 'notes', notes: [] },
|
||||
appearance: true,
|
||||
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],
|
||||
// page meta
|
||||
editLink: true,
|
||||
contributors: true,
|
||||
footer: {
|
||||
message:
|
||||
'Power by <a target="_blank" href="https://v2.vuepress.vuejs.org/">VuePress</a> & <a target="_blank" href="https://github.com/pengzhanbo/vuepress-theme-plume">vuepress-theme-plume</a>',
|
||||
},
|
||||
}
|
||||
|
||||
interface PresetLocale {
|
||||
home: string
|
||||
blog: string
|
||||
tag: string
|
||||
archive: string
|
||||
}
|
||||
|
||||
const presetLocales: Record<string, PresetLocale> = {
|
||||
'zh-CN': {
|
||||
home: '首页',
|
||||
blog: '博客',
|
||||
tag: '标签',
|
||||
archive: '归档',
|
||||
},
|
||||
'en-US': {
|
||||
home: 'Home',
|
||||
blog: 'Blog',
|
||||
tag: 'Tags',
|
||||
archive: 'Archives',
|
||||
},
|
||||
}
|
||||
|
||||
export function mergeLocaleOptions(app: App, options: PlumeThemeLocaleOptions) {
|
||||
options.locales ??= {}
|
||||
|
||||
if (options.notes) {
|
||||
options.notes = {
|
||||
...fallbackLocaleOption.notes,
|
||||
...(options.notes ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (options.footer) {
|
||||
options.footer = {
|
||||
...fallbackLocaleOption.footer,
|
||||
...options.footer,
|
||||
}
|
||||
}
|
||||
|
||||
const { locales, ...otherOptions } = options
|
||||
locales['/'] ??= {}
|
||||
|
||||
Object.assign(options, {
|
||||
...fallbackLocaleOption,
|
||||
...options,
|
||||
})
|
||||
|
||||
Object.assign(locales['/'], {
|
||||
...deepClone(otherOptions),
|
||||
...locales['/'],
|
||||
})
|
||||
|
||||
const langs: Record<string, string> = {}
|
||||
const siteLocales = resolvedAppLocales(app)
|
||||
Object.keys(siteLocales).forEach((locale) => {
|
||||
const lang = siteLocales[locale]?.lang || 'en-US'
|
||||
langs[locale] = lang
|
||||
if (defaultLocales[lang]) {
|
||||
locales[locale] = deepMerge(
|
||||
{},
|
||||
defaultLocales[lang],
|
||||
locales[locale] || {},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const base = app.siteData.base || '/'
|
||||
const defaultLang = app.siteData.lang || 'en-US'
|
||||
const defaultBlog = resolveLocaleOptions(options, 'blog')
|
||||
Object.keys(locales).forEach((locale) => {
|
||||
const option = locales[locale]
|
||||
const lang = langs[locale] || defaultLang
|
||||
|
||||
// 当用户未配置导航栏时,生成默认导航栏
|
||||
if (!option.navbar || !option.navbar.length) {
|
||||
option.navbar = [{
|
||||
link: pathJoin(base, locale),
|
||||
text: presetLocales[lang]?.home || presetLocales[defaultLang].home,
|
||||
icon: 'material-symbols:home-outline',
|
||||
}]
|
||||
const blog = option.blog
|
||||
const link = blog?.link
|
||||
? blog.link
|
||||
: pathJoin(base, locale, defaultBlog?.link || '/blog/')
|
||||
link && option.navbar.push({
|
||||
link,
|
||||
text: presetLocales[lang]?.blog || presetLocales[defaultLang].blog,
|
||||
icon: 'material-symbols:article-outline',
|
||||
})
|
||||
const avatar = resolveLocaleOptions(options, 'avatar')
|
||||
if (!avatar) {
|
||||
if (blog?.tags !== false || defaultBlog?.tags !== false) {
|
||||
option.navbar.push({
|
||||
link: pathJoin(link, 'tags/'),
|
||||
text: presetLocales[lang]?.tag || presetLocales[defaultLang].tag,
|
||||
icon: 'tabler:tag',
|
||||
})
|
||||
}
|
||||
if (blog?.archives !== false || defaultBlog?.archives !== false) {
|
||||
option.navbar.push({
|
||||
link: pathJoin(link, 'archives/'),
|
||||
text: presetLocales[lang]?.archive || presetLocales[defaultLang].archive,
|
||||
icon: 'ph:file-archive',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return options
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user