Merge pull request #83 from pengzhanbo/RC-57

upgrade to vuepress rc12
This commit is contained in:
pengzhanbo 2024-05-27 22:46:58 +08:00 committed by GitHub
commit aa65b124d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 4584 additions and 3387 deletions

View File

@ -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": [

View File

@ -21,6 +21,10 @@ export default defineUserConfig({
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules'],
markdown: {
code: false,
},
bundler: viteBundler(),
theme,

View File

@ -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'],
},
],

View File

@ -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',

View File

@ -7,6 +7,7 @@ readingTime: false
prev: false
next: false
article: false
externalLink: false
docs:
-
name: VuePress Plume

View File

@ -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: 即使慢,驰而不息,纵会落后,纵会失败,但必须能够到达他所向的目标。
---

View File

@ -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[]
}
```

View File

@ -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[]`

View File

@ -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
你知道吗, =|鲁迅|= 曾说过:“ =|我没说过这句话!|= ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,=|我在床上翻了个身|=
你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
力量!于是,!!我在床上翻了个身!!
:::
## 选项组

View File

@ -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` 表示要高亮的行号。

View File

@ -28,7 +28,7 @@ permalink: /guide/write/
由于文件夹名称将作为分类名称,且不在主题配置中进行排序配置,对于有排序需要的场景,使用以下规则进行命名
``` ts
const dir = /\d+\.[^]+/
const dir = /\d+\.[\s\S]+/
// 即 数字 + . + 分类名称
// 如: 1.前端
```

View File

@ -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:*"
},

View File

@ -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',

View File

@ -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"
},

View 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;

View File

@ -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",

View File

@ -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"

View File

@ -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",

View File

@ -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()

View File

@ -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"

View File

@ -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'),

View File

@ -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"

View File

@ -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:*",

View File

@ -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']

View File

@ -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",

View File

@ -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": {

View File

@ -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')
}

View File

@ -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',

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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"

View File

@ -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",

View File

@ -1,7 +1,7 @@
export interface NotesDataOptions {
/**
*
* @default '/notes'
* @default '/notes/'
*/
dir: string
/**

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -2,6 +2,10 @@
使用 [`shiki`](https://shiki.style/) 为 Markdown 代码块启用代码高亮。
> [!WARNING]
> 相比于 官方的 [@vuepress/plugin-shiki](https://ecosystem.vuejs.press/zh/plugins/shiki.html)
> 本插件做了很多各种各样的调整,你可以认为这是试验性的。
## Install
```sh

View File

@ -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"
},

View 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)
})
}
})
}

View 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,
},
},
})
}

View File

@ -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

View File

@ -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',
},
}

View File

@ -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`)
}
}

View File

@ -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>`
}
}

View File

@ -0,0 +1,3 @@
export * from './createCopyCodeButtonRender.js'
export * from './copyCodeButtonLocales.js'
export * from './copyCodeButtonPlugin.js'

View File

@ -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, '&#123;')
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 }),

View File

@ -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)
}
}

View File

@ -0,0 +1,3 @@
export * from './highlightLinesPlugin.js'
export * from './lineNumberPlugin.js'
export * from './preWrapperPlugin.js'

View 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
}
}

View 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>`
}
}

View 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__,
})
},`
: ''}
}
`,
)
}

View File

@ -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
}
}
},
}

View File

@ -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)

View File

@ -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
}

View 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'],
}))
}

View File

@ -0,0 +1,4 @@
export * from './attrsToLines.js'
export * from './resolveAttr.js'
export * from './resolveLanguage.js'
export * from './lru.js'

View 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
}

View File

@ -0,0 +1,8 @@
const VUE_RE = /-vue$/
export function resolveLanguage(info: string): string {
return info
.match(/^([^ :[{]+)/)?.[1]
?.replace(VUE_RE, '')
.toLowerCase() ?? ''
}

View File

@ -2,6 +2,7 @@
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["vuepress/client-types"],
"outDir": "./lib"
},
"include": ["./src"]

4002
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -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:*"
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -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"

View File

@ -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;
}

View 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>

View File

@ -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);
}

View File

@ -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>

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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" />

View File

@ -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`

View File

@ -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,

View File

@ -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
}

View 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 }
}

View File

@ -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)

View File

@ -9,3 +9,4 @@ export * from './blog.js'
export * from './locale.js'
export * from './useRouteQuery.js'
export * from './watermark.js'
export * from './data.js'

View File

@ -25,8 +25,7 @@ export function useEditNavLink(): ComputedRef<null | NavItemWithLink> {
return null
const {
repo,
docsRepo = repo,
docsRepo,
docsBranch = 'main',
docsDir = '',
editLinkText,

View File

@ -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 (

View File

@ -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: {

View File

@ -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));
}

View File

@ -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: "";
}

View File

@ -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";

View File

@ -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 * {

View File

@ -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);
}

View File

@ -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)$/

View File

@ -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,
}
}

View 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'

View File

@ -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 = ':'

View 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
}

View 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
}

View File

@ -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 ??= []

View 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,
}),
}
}

View 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
}

View File

@ -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