diff --git a/.vscode/settings.json b/.vscode/settings.json
index c406b560..91ebc9bc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -33,9 +33,10 @@
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": "explicit",
"source.fixAll.eslint": "explicit",
+ "source.fixAll.markdownlint": "explicit",
"source.organizeImports": "never"
},
- "editor.formatOnPaste": true,
+ "editor.formatOnPaste": false,
"eslint.validate": [
"javascript",
"javascriptreact",
diff --git a/docs/notes/theme/config/主题配置.md b/docs/notes/theme/config/主题配置.md
index ef07513e..c5a9d818 100644
--- a/docs/notes/theme/config/主题配置.md
+++ b/docs/notes/theme/config/主题配置.md
@@ -334,6 +334,20 @@ type NavItem = string | {
详细配置请查看 [此文档](/config/notes/)
+### aside
+
+- 类型: `boolean | 'left'`
+- 默认值: `true`
+- 详情:
+
+ 是否显示侧边栏
+
+ - `false` 表示禁用 右侧边栏
+ - `true` 表示启用 右侧边栏
+ - `'left` 表示将有侧边栏移动到文章内容左侧,sidebar 右侧
+
+ 每个页面可以通过 [frontmatter aside](./frontmatter/basic.md#aside) 覆盖层级配置。
+
### outline
- 类型: `false | number | [number, number] | 'deep'`
@@ -348,6 +362,8 @@ type NavItem = string | {
`'deep'` 与 `[2, 6]` 相同,将显示从 `
` 到 `` 的所有标题。
+ 当 [aside](#aside) 被禁用时,`outline` 也会被禁用
+
每个页面可以通过 [frontmatter outline](./frontmatter/basic.md#outline) 覆盖层级配置。
### selectLanguageName
@@ -366,7 +382,7 @@ type NavItem = string | {
- 默认值: `''`
- 详情:
- _选择语言菜单_ 的文字。
+ _选择语言菜单_ 的文本。
如果你在站点配置中设置了多个 [locales](#locales) ,那么 _选择语言菜单_ 就会显示在导航栏中仓库按钮的旁边。
@@ -386,7 +402,7 @@ type NavItem = string | {
- 默认值: `'Menu'`
- 详情:
- 移动设备下的导航栏中 菜单选项的文字。
+ 移动设备下的导航栏中 菜单选项的文本。
### returnToTopLabel
@@ -394,7 +410,7 @@ type NavItem = string | {
- 默认值: `'return to top'`
- 详情:
- 移动设备下的导航栏中返回顶部的文字。
+ 移动设备下的导航栏中返回顶部的文本。
### outlineLabel
@@ -402,7 +418,7 @@ type NavItem = string | {
- 默认值: `'On this page'`
- 详情:
- 移动设备下的导航栏中大纲标题的文字
+ 移动设备下的导航栏中大纲标题的文本
### editLink
@@ -414,7 +430,7 @@ type NavItem = string | {
- 类型: `string`
- 默认值: `'Edit this page'`
-- 详情: 编辑链接文字
+- 详情: 编辑链接文本
### editLinkPattern
@@ -428,7 +444,7 @@ type NavItem = string | {
- 类型: `string`
- 默认值: `''`
-- 详情: 文档仓库配置, 用于生成 `Edit this page` 链接,如果为空,默认使用 `repo` 配置的值
+- 详情: 文档仓库配置, 用于生成 `Edit this page` 链接。
### docsBranch
@@ -478,19 +494,19 @@ interface LastUpdatedOptions {
- 类型: `string`
- 默认值: `'Contributors'`
-- 详情: 贡献者的文字
+- 详情: 贡献者的文本
### prevPageLabel
- 类型: `string`
- 默认值: `'Previous Page'`
-- 详情: 上一页的文字
+- 详情: 上一页的文本
### nextPageLabel
- 类型: `string`
- 默认值: `'Next Page'`
-- 详情: 下一页的文字
+- 详情: 下一页的文本
### notFound
diff --git a/docs/notes/theme/guide/markdown/基础.md b/docs/notes/theme/guide/markdown/基础.md
index 21b1a2f3..e6ccc712 100644
--- a/docs/notes/theme/guide/markdown/基础.md
+++ b/docs/notes/theme/guide/markdown/基础.md
@@ -4,6 +4,9 @@ author: pengzhanbo
icon: fluent:markdown-20-filled
createTime: 2024/03/05 11:10:39
permalink: /guide/markdown/basic/
+tags:
+ - 指南
+ - markdown
---
::: note
diff --git a/docs/notes/theme/guide/markdown/扩展.md b/docs/notes/theme/guide/markdown/扩展.md
index 447035f1..ff92f4a2 100644
--- a/docs/notes/theme/guide/markdown/扩展.md
+++ b/docs/notes/theme/guide/markdown/扩展.md
@@ -4,6 +4,9 @@ author: pengzhanbo
icon: fluent-mdl2:auto-enhance-on
createTime: 2024/03/05 23:29:07
permalink: /guide/markdown/extensions/
+tags:
+ - 指南
+ - markdown
---
## 标题锚点
diff --git a/docs/notes/theme/guide/markdown/进阶.md b/docs/notes/theme/guide/markdown/进阶.md
index c68ee3dd..0f2a7366 100644
--- a/docs/notes/theme/guide/markdown/进阶.md
+++ b/docs/notes/theme/guide/markdown/进阶.md
@@ -2,10 +2,448 @@
title: 更多
author: pengzhanbo
icon: ic:outline-auto-fix-high
+outline: 2
createTime: 2024/03/05 16:27:59
permalink: /guide/markdown/advance/
+tags:
+ - 指南
+ - markdown
---
+## 卡片
+
+对于想要突出显示的内容,可以将其放在 卡片容器 `::: card` 中。
+
+如果需要在空间足够时并排显示多个卡片,可以使用 `card-grid` 容器,包裹 多个卡片。
+
+### 语法
+
+```md
+
+::: card title="标题" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+
+:::: card-grid
+
+::: card title="卡片标题 1" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+::: card title="卡片标题 2" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+::::
+```
+
+卡片 支持 可选的 `title` 和 `icon`。 其中, icon 支持传入 图片链接,也可以传入 iconify 图标名。
+
+### 示例
+
+**输入:**
+
+```md
+::: card title="卡片标题" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+```
+
+**输出:**
+
+::: card title="卡片标题" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+**输入:**
+
+```md
+:::: card-grid
+::: card title="卡片标题 1" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+::: card title="卡片标题 2" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+::::
+```
+
+**输出:**
+
+:::: card-grid
+::: card title="卡片标题 1" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+
+::: card title="卡片标题 2" icon="twemoji:astonished-face"
+
+这里是卡片内容。
+:::
+::::
+
+::: info
+如果你更喜欢通过组件的方式使用 卡片,你可以查看 [卡片组件](/guide/features/component/#card) 。
+:::
+
+## 步骤
+
+有时候,你需要将内容 划分为递进的步骤展示,你可以使用 `steps` 容器 实现。
+
+### 语法
+
+在 `steps` 容器内,使用 有序列表 (或无序列表) 来表示步骤。你可以在 容器内使用 任意 markdown 语法。
+
+````md
+::: steps
+
+1. 步骤 1
+
+ 相关内容
+
+2. 步骤 2
+
+ 相关内容
+
+:::
+````
+
+### 示例
+
+输入:
+
+````md
+:::: steps
+1. 步骤 1
+
+ ```ts
+ console.log('Hello World!')
+ ```
+
+2. 步骤 2
+
+ 这里是步骤 2 的相关内容
+
+3. 步骤 3
+
+ ::: tip
+ 提示容器
+ :::
+
+4. 结束
+::::
+````
+
+输出:
+
+:::: steps
+
+1. 步骤 1
+
+ ```ts
+ console.log('Hello World!')
+ ```
+
+2. 步骤 2
+
+ 这里是步骤 2 的相关内容
+
+3. 步骤 3
+
+ ::: tip
+ 提示容器
+ :::
+
+4. 结束
+::::
+
+## 选项组
+
+在 Markdown 中支持选项卡。
+
+你需要将选项卡包装在 `tabs` 容器中。
+
+你可以在 `tabs` 容器中添加一个 id 后缀,该后缀将用作选项卡 id。
+所有具有相同 id 的选项卡将共享相同的切换事件。
+
+```md
+::: tabs#fruit
+
+
+
+
+
+:::
+```
+
+在这个容器内,你应该使用 `@tab` 标记来标记和分隔选项卡内容。
+
+在 `@tab` 标记后,你可以添加文本 `:active` 默认激活选项卡,之后的文本将被解析为选项卡标题。
+
+```md
+::: tabs
+
+@tab 标题 1
+
+
+
+@tab 标题 2
+
+
+
+@tab:active 标题 3
+
+
+
+
+
+:::
+```
+
+默认情况下,标题将用作选项卡的值,但你可以使用 id 后缀覆盖它。
+
+```md
+::: tabs
+
+@tab 标题 1
+
+
+
+
+
+@tab 标题 2#值 2
+
+
+
+
+
+:::
+```
+
+你可以在每个选项卡中使用 Vue 语法和组件,并且你可以访问 value 和 isActive,
+表示选项卡的绑定值和选项卡是否处于激活状态。
+
+**输入:**
+
+````
+::: tabs
+@tab npm
+
+npm 应该与 Node.js 被一同安装。
+
+@tab pnpm
+
+```sh
+corepack enable
+corepack use pnpm@8
+```
+
+:::
+````
+
+**输出:**
+
+::: tabs
+@tab npm
+
+npm 应该与 Node.js 被一同安装。
+
+@tab pnpm
+
+```sh
+corepack enable
+corepack use pnpm@8
+```
+
+:::
+
+## “隐秘”文本
+
+有时候,你不想直接把文本内容毫无保留的展示出来,想要保留一些 隐秘性,
+可能是为了引起读者的好奇心,也可能纯粹是故意增加点阅读障碍,让文章变得更加有趣。
+
+为了满足这种小小的心思,主题提供了一个 **“隐秘”文本** 的有趣小功能。它看起来像这样:
+
+:::demo-wrapper
+你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
+力量!于是,!!我在床上翻了个身!! !
+:::
+
+读者不能直接阅读到完整的内容,部分的内容被 “遮住”,需要鼠标悬停到内容上,才能看到被遮住的内容。
+
+### 配置
+
+该功能默认不启用,你需要在 `theme` 配置中启用。
+
+::: code-tabs
+@tab .vuepress/config.ts
+
+```ts
+export default defineUserConfig({
+ theme: plumeTheme({
+ plugins: {
+ markdownPower: {
+ plot: true,
+ },
+ }
+ })
+})
+```
+
+:::
+
+`markdownPower.plot` 支持传入 `boolean | PlotOptions` 类型
+
+```ts
+interface PlotOptions {
+ /**
+ * 是否启用 `!! !!` markdown (该标记为非标准标记,脱离插件将不生效)
+ * 如果设置为 false, 则表示不启用该标记,只能使用 组件
+ * @default true
+ */
+ tag?: boolean
+
+ /**
+ * 遮罩层颜色
+ */
+ mask?: string | { light: string, dark: string }
+
+ /**
+ * 文本颜色
+ */
+ color?: string | { light: string, dark: string }
+
+ /**
+ * 触发方式
+ *
+ * @default 'hover'
+ */
+ trigger?: 'hover' | 'click'
+}
+```
+
+### 语法
+
+```md
+!!需要隐秘的内容!!
+```
+
+如果不想使用 非标准的 `!! !!` 标记语法,你可以将 `plot.tag` 设置为 `false` ,
+然后使用 [``](/guide/features/component/#plot) 组件替代。
+
+### 示例
+
+输入:
+
+```md
+你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
+力量!于是,!!我在床上翻了个身!! !
+```
+
+输出:
+
+:::demo-wrapper
+你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
+力量!于是,!!我在床上翻了个身!! !
+:::
+
+## 示例容器
+
+有时候,你可能需要在 内容中补充一些 示例,但期望能与 其它内容 分隔开来呈现。
+主题支持在 Markdown 文件中添加示例容器。
+
+### 语法
+
+````md
+::: demo-wrapper
+添加你的示例
+:::
+````
+
+### 选项
+
+- `title="xxx"`:标题
+- `no-padding`:不添加内边距
+- `img`: 仅包含图片时使用
+- `height="xxx"`: 高度
+
+### 示例
+
+仅包含图片:
+
+```md
+::: demo-wrapper img no-padding
+
+:::
+```
+
+**输出:**
+::: demo-wrapper img no-padding
+
+:::
+
+包含 markdown 语法:
+
+```md
+::: demo-wrapper title="标题"
+### 三级标题
+
+这是示例容器中的内容。
+:::
+```
+
+**输出:**
+::: demo-wrapper title="标题"
+
+### 三级标题
+
+这是示例容器中的内容。
+:::
+
+包含 html /vue 代码:
+
+```md
+::: demo-wrapper
+这是标题
+
这是段落
+
+
+:::
+```
+
+**输出:**
+::: demo-wrapper
+
+这是标题
+这是段落
+
+
+
+:::
+
## iconify 图标
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题虽然提供了
@@ -108,284 +546,6 @@ github: :[tdesign:logo-github-filled]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
-## “隐秘”文本
-
-有时候,你不想直接把文本内容毫无保留的展示出来,想要保留一些 隐秘性,
-可能是为了引起读者的好奇心,也可能纯粹是故意增加点阅读障碍,让文章变得更加有趣。
-
-为了满足这种小小的心思,主题提供了一个 **“隐秘”文本** 的有趣小功能。它看起来像这样:
-
-:::demo-wrapper
-你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
-力量!于是,!!我在床上翻了个身!! !
-:::
-
-读者不能直接阅读到完整的内容,部分的内容被 “遮住”,需要鼠标悬停到内容上,才能看到被遮住的内容。
-
-### 配置
-
-该功能默认不启用,你需要在 `theme` 配置中启用。
-
-::: code-tabs
-@tab .vuepress/config.ts
-
-```ts
-export default defineUserConfig({
- theme: plumeTheme({
- plugins: {
- markdownPower: {
- plot: true,
- },
- }
- })
-})
-```
-
-:::
-
-`markdownPower.plot` 支持传入 `boolean | PlotOptions` 类型
-
-```ts
-interface PlotOptions {
- /**
- * 是否启用 `!! !!` markdown (该标记为非标准标记,脱离插件将不生效)
- * 如果设置为 false, 则表示不启用该标记,只能使用 组件
- * @default true
- */
- tag?: boolean
-
- /**
- * 遮罩层颜色
- */
- mask?: string | { light: string, dark: string }
-
- /**
- * 文本颜色
- */
- color?: string | { light: string, dark: string }
-
- /**
- * 触发方式
- *
- * @default 'hover'
- */
- trigger?: 'hover' | 'click'
-}
-```
-
-### 语法
-
-```md
-!!需要隐秘的内容!!
-```
-
-如果不想使用 非标准的 `!! !!` 标记语法,你可以将 `plot.tag` 设置为 `false` ,
-然后使用 [``](/guide/features/component/#plot) 组件替代。
-
-### 示例
-
-输入:
-
-```md
-你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
-力量!于是,!!我在床上翻了个身!! !
-```
-
-输出:
-
-:::demo-wrapper
-你知道吗, !!鲁迅!! 曾说过:“ !!我没说过这句话!!! ” 令我醍醐灌顶,深受启发,浑身迸发出无可匹敌的
-力量!于是,!!我在床上翻了个身!! !
-:::
-
-## 选项组
-
-让你的 Markdown 文件支持选项卡。
-
-你需要将选项卡包装在 `tabs` 容器中。
-
-你可以在 `tabs` 容器中添加一个 id 后缀,该后缀将用作选项卡 id。
-所有具有相同 id 的选项卡将共享相同的切换事件。
-
-```md
-::: tabs#fruit
-
-
-
-
-
-:::
-```
-
-在这个容器内,你应该使用 `@tab` 标记来标记和分隔选项卡内容。
-
-在 `@tab` 标记后,你可以添加文本 `:active` 默认激活选项卡,之后的文本将被解析为选项卡标题。
-
-```md
-::: tabs
-
-@tab 标题 1
-
-
-
-@tab 标题 2
-
-
-
-@tab:active 标题 3
-
-
-
-
-
-:::
-```
-
-默认情况下,标题将用作选项卡的值,但你可以使用 id 后缀覆盖它。
-
-```md
-::: tabs
-
-@tab 标题 1
-
-
-
-
-
-@tab 标题 2#值 2
-
-
-
-
-
-:::
-```
-
-你可以在每个选项卡中使用 Vue 语法和组件,并且你可以访问 value 和 isActive,
-表示选项卡的绑定值和选项卡是否处于激活状态。
-
-**输入:**
-
-````
-::: tabs
-@tab npm
-
-npm 应该与 Node.js 被一同安装。
-
-@tab pnpm
-
-```sh
-corepack enable
-corepack use pnpm@8
-```
-
-:::
-````
-
-**输出:**
-
-::: tabs
-@tab npm
-
-npm 应该与 Node.js 被一同安装。
-
-@tab pnpm
-
-```sh
-corepack enable
-corepack use pnpm@8
-```
-
-:::
-
-## 示例容器
-
-有时候,你可能需要在 内容中补充一些 示例,但期望能与 其它内容 分隔开来呈现。
-主题支持在 Markdown 文件中添加示例容器。
-
-### 语法
-
-````md
-::: demo-wrapper
-添加你的示例
-:::
-````
-
-### 选项
-
-- `title="xxx"`:标题
-- `no-padding`:不添加内边距
-- `img`: 仅包含图片时使用
-- `height="xxx"`: 高度
-
-### 示例
-
-仅包含图片:
-
-```md
-::: demo-wrapper img no-padding
-
-:::
-```
-
-**输出:**
-::: demo-wrapper img no-padding
-
-:::
-
-包含 markdown 语法:
-
-```md
-::: demo-wrapper title="标题"
-### 三级标题
-
-这是示例容器中的内容。
-:::
-```
-
-**输出:**
-::: demo-wrapper title="标题"
-
-### 三级标题
-
-这是示例容器中的内容。
-:::
-
-包含 html /vue 代码:
-
-```md
-::: demo-wrapper
-这是标题
-这是段落
-
-
-:::
-```
-
-**输出:**
-::: demo-wrapper
-
-这是标题
-这是段落
-
-
-
-:::
-
## can I use
此功能默认不启用,你可以在配置文件中启用它。
diff --git a/docs/notes/theme/guide/代码/twoslash.md b/docs/notes/theme/guide/代码/twoslash.md
index 8c95aeda..cb3b32ac 100644
--- a/docs/notes/theme/guide/代码/twoslash.md
+++ b/docs/notes/theme/guide/代码/twoslash.md
@@ -14,9 +14,23 @@ permalink: /guide/markdown/experiment/
该功能由 [shiki](https://shiki.style/) 和 [@shikijs/twoslash](https://shiki.style/packages/twoslash) 提供支持,
并整合在 [@vuepress-plume/plugin-shikiji](https://github.com/pengzhanbo/vuepress-theme-plume/tree/main/plugins/plugin-shikiji) 中。
-> [!important]
-> 从 `vuepress@2.0.0-rc.12` 开始,不再需要对 `@vuepress/markdown` 进行额外的 hack 操作,
-> 因此,现在你可以安全的使用这项功能了!
+:::important
+从 `vuepress@2.0.0-rc.12` 开始,不再需要对 `@vuepress/markdown` 进行额外的 hack 操作,
+因此,现在你可以安全的使用这项功能了!
+:::
+
+::: warning
+`twoslash` 是一个比较耗时的功能,由于它需要对代码进行类型编译,如果代码引入的包 比较大,会花费较长时间。
+
+特别的,由于 vuepress 启动时,会预编译所有的 markdown 文件,因此它会直接影响 vuepress 的启动时间,
+如果 包含了比较多的 `twoslash` 代码块,这可能会使 vuepress 启动时间变得很长。
+
+比如,在未使用 `twoslash` 时,vuepress 的启动时间区间大约在 `300ms ~ 1000ms` 之间,而在使用 `twoslash` 后,
+可能某一个 `twoslash` 的代码块编译耗时就需要额外再等待 `500ms` 以上。
+
+但不必担心 markdown 文件热更新时的编译耗时,主题针对 代码高亮编译 耗时做了优化,即使 单个 markdown 文件
+中包含多个 代码块,主题也仅会对 **有变更的代码块** 进行编译,因此热更新的速度依然非常快。
+:::
### 概述
diff --git a/docs/notes/theme/guide/代码/介绍.md b/docs/notes/theme/guide/代码/介绍.md
index 614cfd4c..c636451b 100644
--- a/docs/notes/theme/guide/代码/介绍.md
+++ b/docs/notes/theme/guide/代码/介绍.md
@@ -8,11 +8,33 @@ permalink: /guide/code/intro/
## 概述
-主题 使用 [Shiki](https://github.com/shikijs/shiki) 在 Markdown 代码块实现语法高亮。
+主题 使用 [Shiki](https://shiki.style/) 在 Markdown 代码块实现语法高亮。
+
+:::info
+主题默认 加载了 [Shiki](https://shiki.style/) 支持的超过 190+ 的 语言,这可能导致 在启用 vuepress 服务时,
+需要多等待 **300ms ~ 600ms** 左右的时间来加载所有的 语言。
+
+因此,如果比较在意 vuepress 启动时间,建议修改配置为仅加载 您所需要的 语言。
+
+示例:
+
+```ts
+export default defineUserConfig({
+ theme: plumeTheme({
+ plugins: {
+ shiki: {
+ languages: ['javascript', 'typescript', 'vue', 'bash', 'sh'], // [!code highlight]
+ }
+ }
+ })
+})
+```
+
+:::
## 语言
-[Shiki](https://github.com/shikijs/shiki) 支持 超过 190+ 种语言,
+[Shiki](https://shiki.style/) 支持 超过 190+ 种语言,
你可以在 [languages](https://shiki.style/languages) 查看所有支持的语言列表。
你可以通过以下语法为你使用的 语言所编写的代码 实现高亮效果:
@@ -40,7 +62,7 @@ console.log(a)
## 高亮主题
-[Shiki](https://github.com/shikijs/shiki) 支持 超过 40+ 种高亮主题。
+[Shiki](https://shiki.style/) 支持 超过 40+ 种高亮主题。
你可以在 [Themes](https://shiki.style/themes) 找到所有支持的主题列表,根据个人的喜欢自定义
高亮主题。
@@ -63,7 +85,7 @@ export default defineUserConfig({
## 更多支持
-得益于 [Shiki](https://github.com/shikijs/shiki) 的强大能力,Theme Plume 还为 代码块
+得益于 [Shiki](https://shiki.style/) 的强大能力,Theme Plume 还为 代码块
提供了 更多的 [特性支持](/guide/code/features/),它们让 代码块具备更强的表达能力。
同时,为了方便 更好的 完成 代码演示,Theme Plume 还提供了嵌入 [CodePen](/guide/code/code-pen/),
diff --git a/docs/notes/theme/guide/功能/组件.md b/docs/notes/theme/guide/功能/组件.md
index 05d10baa..657e0b36 100644
--- a/docs/notes/theme/guide/功能/组件.md
+++ b/docs/notes/theme/guide/功能/组件.md
@@ -9,11 +9,15 @@ permalink: /guide/features/component/
## 概述
+VuePress 支持在 Markdown 文件中使用 组件。
+
主题提供了一些具有通用性的组件,可以在任何地方使用。
-## ``
+## 徽章
-标签,用于在页面中增加一些提示信息。
+使用 `` 组件来显示 行内信息,如状态或标签。
+
+将你想显示的内容传递给 `` 组件的 `text` 属性。
### Props
@@ -38,37 +42,37 @@ permalink: /guide/features/component/
- VuePress -
- VuePress -
-## ``
+## 图标
-支持 iconify 所有图标,支持通过 icon name 加载图标。
+支持 iconify 所有图标,通过 icon name 加载图标。
可在 [iconify search](https://icon-sets.iconify.design/) 搜索图标使用。
### Props
-| 名称 | 类型 | 默认值 | 说明 |
-| ----- | ------ | ---------------- | ---------- |
-| name | string | `''` | icon name |
-| color | string | `'currentcolor'` | icon color |
-| size | string | `'1em'` | icon size |
+| 名称 | 类型 | 默认值 | 说明 |
+| ----- | ------ | ---------------- | -------- |
+| name | string | `''` | 图标名 |
+| color | string | `'currentcolor'` | 图标颜色 |
+| size | string | `'1em'` | 图标大小 |
**输入:**
```md
-- home -
-- vscode -
-- twitter -
+- home -
+- vscode -
+- twitter -
```
**输出:**
-- home -
-- vscode -
-- twitter -
+- home -
+- vscode -
+- twitter -
-## ``
+## “隐秘”文本
-[“隐秘”文本](/guide/markdown/advance/#隐秘-文本) 组件。
+使用 `` 组件显示 [“隐秘”文本](/guide/markdown/advance/#隐秘-文本) ,能够更灵活的控制行为。
该组件默认不启用,你需要在 theme 配置中启用。
@@ -97,23 +101,139 @@ export default defineUserConfig({
| mask | `string \| { light: string, dark: string }` | `#000` | 遮罩颜色 |
| color | `string \| { light: string, dark: string }` | `#fff` | 文本颜色 |
-输入:
+**输入:**
```md
- 鼠标悬停 - 悬停时可见
- 点击 - 点击时可见
```
-输出:
+**输出:**
- 鼠标悬停 - 悬停时可见
- 点击 - 点击时可见
-## ``
+## 卡片
-- 类型:`Component`
+使用 `` 组件在页面中显示卡片。
-自定义首页时,提供给 区域 的 包装容器。
+也可以使用 markdown [卡片容器](/guide/markdown/advance/#卡片) 语法,替代 `` 组件。
+
+### Props
+
+| 名称 | 类型 | 默认值 | 说明 |
+| ----- | --------------------------- | ------ | ---------------------------------------------------------------- |
+| title | `string` | `''` | 标题 |
+| icon | `string \| { svg: string }` | `''` | 显示在标题左侧的图标,支持 iconify 所有图标,也可以使用 图片链接 |
+
+### 插槽
+
+| 名称 | 说明 |
+| ------- | ---------- |
+| default | 卡片内容 |
+| title | 自定义标题 |
+
+**输入:**
+
+```md
+
+ 这里是卡片内容。
+
+
+
+
+ 卡片标题
+
+ 这里是卡片内容。
+
+```
+
+**输出:**
+
+
+ 这里是卡片内容。
+
+
+
+
+ 卡片标题
+
+ 这里是卡片内容。
+
+
+## 链接卡片
+
+使用 `` 组件在页面中显示链接卡片。
+
+### Props
+
+| 名称 | 类型 | 默认值 | 说明 |
+| ----------- | --------------------------- | ------ | ---------------------------------------------------------------- |
+| title | `string` | `''` | 标题 |
+| icon | `string \| { svg: string }` | `''` | 显示在标题左侧的图标,支持 iconify 所有图标,也可以使用 图片链接 |
+| href | `string` | `''` | 链接 |
+| description | `string` | `''` | 详情 |
+
+### 插槽
+
+| 名称 | 说明 |
+| ------- | ------------ |
+| default | 卡片详情内容 |
+| title | 自定义标题 |
+
+**输入:**
+
+```md
+
+
+```
+
+**输出:**
+
+
+
+
+## 卡片排列容器
+
+当你需要将多个卡片排列,可以使用 `` 组件。在空间足够时,多个卡片会自动排列。
+
+**输入:**
+
+```md
+
+
+ 这里是卡片内容。
+
+
+ 这里是卡片内容。
+
+
+
+
+
+
+
+```
+
+**输出:**
+
+
+
+ 这里是卡片内容。
+
+
+ 这里是卡片内容。
+
+
+
+
+
+
+
+
+## 首页布局容器
+
+自定义首页时,使用 ``提供给 区域 的 包装容器。
### Props
diff --git a/docs/notes/theme/guide/安装与使用.md b/docs/notes/theme/guide/安装与使用.md
index 030fc3d9..5b265680 100644
--- a/docs/notes/theme/guide/安装与使用.md
+++ b/docs/notes/theme/guide/安装与使用.md
@@ -21,195 +21,185 @@ permalink: /guide/quick-start/
使用本主题,你需要首先新建一个项目,并安装`vuepress@next`以及本主题
-### 步骤 1
+:::: steps
-**创建一个新文件夹,并进入目录:**
+- ### 新建文件夹并进入目录
-``` sh
-mkdir my-blog
-cd my-blog
-```
+ ``` sh
+ mkdir my-blog
+ cd my-blog
+ ```
-### 步骤 2
+- ### 初始化项目
-**初始化项目:**
+ ::: code-tabs
+ @tab pnpm
-::: code-tabs
-@tab pnpm
+ ``` sh
+ git init
+ pnpm init
+ ```
-``` sh
-git init
-pnpm init
-```
+ @tab yarn
-@tab yarn
+ ``` sh
+ git init
+ yarn init
+ ```
-``` sh
-git init
-yarn init
-```
+ @tab npm
-@tab npm
+ ``` sh
+ git init
+ npm init
+ ```
-``` sh
-git init
-npm init
-```
+ :::
-:::
+- ### 安装相关依赖
-### 步骤 3
+ 安装 `vuepress@next` 和 `vuepress-theme-plume` 作为本地依赖。
-**安装相关依赖:**
+ ::: code-tabs
+ @tab pnpm
-安装 `vuepress@next` 和 `vuepress-theme-plume` 作为本地依赖。
+ ```sh
+ # 安装 vuepress
+ pnpm add -D vuepress@next vue
+ # 安装 主题和打包工具
+ pnpm add -D vuepress-theme-plume @vuepress/bundler-vite@next
+ ```
-::: code-tabs
-@tab pnpm
+ @tab yarn
-```sh
-# 安装 vuepress
-pnpm add -D vuepress@next vue
-# 安装 主题和打包工具
-pnpm add -D vuepress-theme-plume @vuepress/bundler-vite@next
-```
+ ``` sh
+ # 安装 vuepress
+ yarn add -D vuepress@next
+ # 安装 主题和打包工具
+ yarn add -D vuepress-theme-plume @vuepress/bundler-vite@next
+ ```
-@tab yarn
+ @tab npm
-``` sh
-# 安装 vuepress
-yarn add -D vuepress@next
-# 安装 主题和打包工具
-yarn add -D vuepress-theme-plume @vuepress/bundler-vite@next
-```
+ ``` sh
+ # 安装 vuepress
+ npm i -D vuepress@next
+ # 安装 主题和打包工具
+ npm i -D vuepress-theme-plume @vuepress/bundler-vite@next
+ ```
-@tab npm
+ :::
-``` sh
-# 安装 vuepress
-npm i -D vuepress@next
-# 安装 主题和打包工具
-npm i -D vuepress-theme-plume @vuepress/bundler-vite@next
-```
+ :::warning
+ 主题当前版本 已适配至 `vuepress@2.0.0-rc.13`,你应该安装这个版本的 VuePress。
+ 高于或低于这个版本,可能会存在潜在的兼容性问题。
+ :::
-:::
+- ### 在 `package.json` 中添加 `script`
-:::warning
-主题当前版本 已适配至 `vuepress@2.0.0-rc.13`,你应该安装这个版本的 VuePress。
-高于或低于这个版本,可能会存在潜在的兼容性问题。
-:::
+ ::: code-tabs
+ @tab package.json
-### 步骤 4
-
-**在 `package.json` 中添加 `script`**
-
-::: code-tabs
-@tab package.json
-
-``` json
-{
- "scripts": {
- "dev": "vuepress dev docs",
- "build": "vuepress build docs"
+ ``` json
+ {
+ "scripts": {
+ "dev": "vuepress dev docs",
+ "build": "vuepress build docs"
+ }
}
-}
-```
+ ```
-:::
+ :::
-`vuepress` 默认将文档源码放在 `docs` 目录下。
+ `vuepress` 默认将文档源码放在 `docs` 目录下。
-### 步骤 5
+- ### 将默认的临时目录和缓存目录添加到`.gitignore` 文件中
-**将默认的临时目录和缓存目录添加到`.gitignore` 文件中**
+ ::: code-tabs
+ @tab .gitignore
-::: code-tabs
-@tab .gitignore
+ ``` txt
+ node_modules
+ .temp
+ .cache
+ ```
-``` txt
-node_modules
-.temp
-.cache
-```
+ @tab sh
-@tab sh
+ ``` sh
+ echo 'node_modules' >> .gitignore
+ echo '.temp' >> .gitignore
+ echo '.cache' >> .gitignore
+ ```
-``` sh
-echo 'node_modules' >> .gitignore
-echo '.temp' >> .gitignore
-echo '.cache' >> .gitignore
-```
+ :::
-:::
+- ### 在 `docs/.vuepress/config.{js,ts}` 中配置主题
-### 步骤 6
+ ::: code-tabs
+ @tab docs/.vuepress/config.js
-**在 `docs/.vuepress/config.{js,ts}` 中配置主题**
+ ``` ts
+ import { defineUserConfig } from 'vuepress'
+ import { viteBundler } from '@vuepress/bundler-vite'
+ import { plumeTheme } from 'vuepress-theme-plume'
-::: code-tabs
-@tab docs/.vuepress/config.js
+ export default defineUserConfig({
+ // 请不要忘记设置默认语言
+ lang: 'zh-CN',
+ theme: plumeTheme({
+ // more...
+ }),
+ bundler: viteBundler(),
+ })
+ ```
-``` ts
-import { defineUserConfig } from 'vuepress'
-import { viteBundler } from '@vuepress/bundler-vite'
-import { plumeTheme } from 'vuepress-theme-plume'
+ :::
-export default defineUserConfig({
- // 请不要忘记设置默认语言
- lang: 'zh-CN',
- theme: plumeTheme({
- // more...
- }),
- bundler: viteBundler(),
-})
-```
+ :::warning
+ 无论是否需要使用 **多语言** ,你都应该为 VuePress 配置 正确 `lang` 选项值。
+ 主题需要根据 `lang` 选项来确定语言环境文本。
+ :::
-:::
+- ### 在 `docs` 目录下新建 `README.md` 文件
-:::warning
-无论是否需要使用 **多语言** ,你都应该为 VuePress 配置 正确 `lang` 选项值。
-主题需要根据 `lang` 选项来确定语言环境文本。
-:::
+ 声明首页配置。
+ ::: code-tabs
+ @tab README.md
-### 步骤 7
+ ``` md
+ ---
+ home: true
+ ---
+ ```
-**在 `docs` 目录下新建 `README.md` 文件**
+ :::
-声明首页配置。
-::: code-tabs
-@tab README.md
+- ### 在本地服务器启动你的文档站点
-``` md
----
-home: true
----
-```
+ ::: code-tabs
+ @tab pnpm
-:::
+ ```sh
+ pnpm dev
+ ```
-### 步骤 8
+ @tab yarn
-**在本地服务器启动你的文档站点:**
+ ``` sh
+ yarn dev
+ ```
-::: code-tabs
-@tab pnpm
+ @tab npm
-```sh
-pnpm dev
-```
+ ``` sh
+ npm run dev
+ ```
-@tab yarn
+ :::
-``` sh
-yarn dev
-```
+ Vuepress 会在 [http://localhost:8080](http://localhost:8080) 。启动一个热重载的开发服务器。当你修改你的 Markdown 文件时,浏览器中的内容也会自动更新。
-@tab npm
+- ### 完成
-``` sh
-npm run dev
-```
-
-:::
-
-Vuepress 会在 [http://localhost:8080](http://localhost:8080) 。启动一个热重载的开发服务器。当你修改你的 Markdown 文件时,浏览器中的内容也会自动更新。
+::::
diff --git a/docs/notes/theme/guide/自定义首页.md b/docs/notes/theme/guide/自定义首页.md
index 68746677..f6ebe8f6 100644
--- a/docs/notes/theme/guide/自定义首页.md
+++ b/docs/notes/theme/guide/自定义首页.md
@@ -112,7 +112,7 @@ home: true
config:
-
type: banner
- banner: https://file.mo7.cc/api/public/bz
+ banner: https://api.pengzhanbo.cn/wallpaper/bing
bannerMask:
light: 0.1
dark: 0.3
diff --git a/plugins/plugin-shikiji/src/node/highlight.ts b/plugins/plugin-shikiji/src/node/highlight.ts
index c4115b0c..0487e4ee 100644
--- a/plugins/plugin-shikiji/src/node/highlight.ts
+++ b/plugins/plugin-shikiji/src/node/highlight.ts
@@ -38,6 +38,7 @@ export async function highlight(
defaultHighlightLang: defaultLang = '',
codeTransformers: userTransformers = [],
whitespace = false,
+ languages = Object.keys(bundledLanguages),
} = options
const highlighter = await getHighlighter({
@@ -45,7 +46,7 @@ export async function highlight(
typeof theme === 'object' && 'light' in theme && 'dark' in theme
? [theme.light, theme.dark]
: [theme],
- langs: [...Object.keys(bundledLanguages), ...(options.languages || [])],
+ langs: languages,
langAlias: options.languageAlias,
})
diff --git a/theme/src/client/components/Blog/VPBlogArchives.vue b/theme/src/client/components/Blog/VPBlogArchives.vue
index 42890e6c..32a6bae4 100644
--- a/theme/src/client/components/Blog/VPBlogArchives.vue
+++ b/theme/src/client/components/Blog/VPBlogArchives.vue
@@ -1,6 +1,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/theme/src/client/components/global/VPCardGrid.vue b/theme/src/client/components/global/VPCardGrid.vue
new file mode 100644
index 00000000..5724bb24
--- /dev/null
+++ b/theme/src/client/components/global/VPCardGrid.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/theme/src/client/components/global/VPLinkCard.vue b/theme/src/client/components/global/VPLinkCard.vue
new file mode 100644
index 00000000..10900737
--- /dev/null
+++ b/theme/src/client/components/global/VPLinkCard.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/theme/src/client/composables/blog-archives.ts b/theme/src/client/composables/blog-archives.ts
new file mode 100644
index 00000000..e090fb34
--- /dev/null
+++ b/theme/src/client/composables/blog-archives.ts
@@ -0,0 +1,31 @@
+import { computed } from 'vue'
+import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
+import { useLocalePostList } from './blog-post-list.js'
+
+export type ShortPostItem = Pick
+
+export function useArchives() {
+ const list = useLocalePostList()
+ const archives = computed(() => {
+ const archives: { label: string, list: ShortPostItem[] }[] = []
+
+ list.value.forEach((item) => {
+ const createTime = item.createTime.split(' ')[0]
+ const year = createTime.split('/')[0]
+ let current = archives.find(archive => archive.label === year)
+ if (!current) {
+ current = { label: year, list: [] }
+ archives.push(current)
+ }
+ current.list.push({
+ title: item.title,
+ path: item.path,
+ createTime: createTime.slice(year.length + 1).replace(/\//g, '-'),
+ })
+ })
+
+ return archives
+ })
+
+ return { archives }
+}
diff --git a/theme/src/client/composables/blog-extract.ts b/theme/src/client/composables/blog-extract.ts
new file mode 100644
index 00000000..43f328f2
--- /dev/null
+++ b/theme/src/client/composables/blog-extract.ts
@@ -0,0 +1,41 @@
+import { useRouteLocale } from 'vuepress/client'
+import { computed } from 'vue'
+import type { PresetLocale } from '../../shared/index.js'
+import { useLocalePostList } from './blog-post-list.js'
+import { useTags } from './blog-tags.js'
+import { useData } from './data.js'
+import { useLocaleLink } from './locale.js'
+
+declare const __PLUME_PRESET_LOCALE__: Record
+
+const presetLocales = __PLUME_PRESET_LOCALE__
+
+export function useBlogExtract() {
+ const { theme } = useData()
+ const locale = useRouteLocale()
+ const postList = useLocalePostList()
+ const { tags: tagsList } = useTags()
+ const blog = computed(() => theme.value.blog || {})
+
+ 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,
+ text: presetLocales[locale.value]?.tag || presetLocales['/'].tag,
+ total: tagsList.value.length,
+ }))
+
+ const archives = computed(() => ({
+ link: archiveLink.value,
+ text: presetLocales[locale.value]?.archive || presetLocales['/'].archive,
+ total: postList.value.length,
+ }))
+
+ return {
+ hasBlogExtract,
+ tags,
+ archives,
+ }
+}
diff --git a/theme/src/client/composables/blog.ts b/theme/src/client/composables/blog-post-list.ts
similarity index 52%
rename from theme/src/client/composables/blog.ts
rename to theme/src/client/composables/blog-post-list.ts
index 92bc0080..7f6b8631 100644
--- a/theme/src/client/composables/blog.ts
+++ b/theme/src/client/composables/blog-post-list.ts
@@ -3,9 +3,8 @@ import { useBlogPostData } from '@vuepress-plume/plugin-blog-data/client'
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
-import { useData, useLocaleLink, useRouteQuery } from '../composables/index.js'
-import { toArray } from '../utils/index.js'
-import { useTagColors } from './tag-colors.js'
+import { useData } from './data.js'
+import { useRouteQuery } from './route-query.js'
const DEFAULT_PER_PAGE = 10
@@ -133,121 +132,3 @@ export function usePostListControl() {
changePage,
}
}
-
-const extractLocales: Record = {
- 'zh-CN': { tags: '标签', archives: '归档' },
- 'en': { tags: 'Tags', archives: 'Archives' },
- 'zh-TW': { tags: '標籤', archives: '歸檔' },
-}
-
-export function useBlogExtract() {
- const { theme } = useData()
- const locale = usePageLang()
- const postList = useLocalePostList()
- const { tags: tagsList } = useTags()
- const blog = computed(() => theme.value.blog || {})
-
- 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,
- text: extractLocales[locale.value]?.tags || extractLocales.en.tags,
- total: tagsList.value.length,
- }))
-
- const archives = computed(() => ({
- link: archiveLink.value,
- text: extractLocales[locale.value]?.archives || extractLocales.en.archives,
- total: postList.value.length,
- }))
-
- return {
- hasBlogExtract,
- tags,
- archives,
- }
-}
-
-export type ShortPostItem = Pick
-
-export function useTags() {
- const list = useLocalePostList()
-
- const colors = useTagColors()
-
- const tags = computed(() => {
- const tagMap: Record = {}
- list.value.forEach((item) => {
- if (item.tags) {
- toArray(item.tags).forEach((tag) => {
- if (tagMap[tag])
- tagMap[tag] += 1
- else
- tagMap[tag] = 1
- })
- }
- })
- return Object.keys(tagMap).map(tag => ({
- name: tag,
- count: tagMap[tag] > 99 ? '99+' : tagMap[tag],
- className: `vp-tag-${colors.value[tag]}`,
- }))
- })
-
- const currentTag = useRouteQuery('tag')
-
- const postList = computed(() => {
- if (!currentTag.value)
- return []
-
- return list.value.filter((item) => {
- if (item.tags)
- return toArray(item.tags).includes(currentTag.value)
-
- return false
- }).map(item => ({
- title: item.title,
- path: item.path,
- createTime: item.createTime.split(' ')[0].replace(/\//g, '-'),
- }))
- })
-
- const handleTagClick = (tag: string) => {
- currentTag.value = tag
- }
-
- return {
- tags,
- currentTag,
- postList,
- handleTagClick,
- }
-}
-
-export function useArchives() {
- const list = useLocalePostList()
- const archives = computed(() => {
- const archives: { label: string, list: ShortPostItem[] }[] = []
-
- list.value.forEach((item) => {
- const createTime = item.createTime.split(' ')[0]
- const year = createTime.split('/')[0]
- let current = archives.find(archive => archive.label === year)
- if (!current) {
- current = { label: year, list: [] }
- archives.push(current)
- }
- current.list.push({
- title: item.title,
- path: item.path,
- createTime: createTime.slice(year.length + 1).replace(/\//g, '-'),
- })
- })
-
- return archives
- })
-
- return { archives }
-}
diff --git a/theme/src/client/composables/blog-tags.ts b/theme/src/client/composables/blog-tags.ts
new file mode 100644
index 00000000..75c9d77c
--- /dev/null
+++ b/theme/src/client/composables/blog-tags.ts
@@ -0,0 +1,62 @@
+import { computed } from 'vue'
+import type { PlumeThemeBlogPostItem } from '../../shared/index.js'
+import { toArray } from '../utils/index.js'
+import { useTagColors } from './tag-colors.js'
+import { useLocalePostList } from './blog-post-list.js'
+import { useRouteQuery } from './route-query.js'
+
+type ShortPostItem = Pick
+
+export function useTags() {
+ const list = useLocalePostList()
+
+ const colors = useTagColors()
+
+ const tags = computed(() => {
+ const tagMap: Record = {}
+ list.value.forEach((item) => {
+ if (item.tags) {
+ toArray(item.tags).forEach((tag) => {
+ if (tagMap[tag])
+ tagMap[tag] += 1
+ else
+ tagMap[tag] = 1
+ })
+ }
+ })
+ return Object.keys(tagMap).map(tag => ({
+ name: tag,
+ count: tagMap[tag] > 99 ? '99+' : tagMap[tag],
+ className: `vp-tag-${colors.value[tag]}`,
+ }))
+ })
+
+ const currentTag = useRouteQuery('tag')
+
+ const postList = computed(() => {
+ if (!currentTag.value)
+ return []
+
+ return list.value.filter((item) => {
+ if (item.tags)
+ return toArray(item.tags).includes(currentTag.value)
+
+ return false
+ }).map(item => ({
+ title: item.title,
+ path: item.path,
+ createTime: item.createTime.split(' ')[0].replace(/\//g, '-'),
+ }))
+ })
+
+ const handleTagClick = (tag: string) => {
+ currentTag.value = tag
+ }
+
+ return {
+ tags,
+ currentTag,
+ postList,
+ handleTagClick,
+ }
+}
diff --git a/theme/src/client/composables/index.ts b/theme/src/client/composables/index.ts
index 53608071..f12154f9 100644
--- a/theme/src/client/composables/index.ts
+++ b/theme/src/client/composables/index.ts
@@ -2,6 +2,7 @@ export * from './theme-data.js'
export * from './dark-mode.js'
export * from './data.js'
export * from './scroll-promise.js'
+export * from './scroll-behavior.js'
export * from './sidebar.js'
export * from './aside.js'
@@ -12,7 +13,10 @@ export * from './edit-link.js'
export * from './latest-updated.js'
export * from './contributors.js'
-export * from './blog.js'
+export * from './blog-post-list.js'
+export * from './blog-extract.js'
+export * from './blog-tags.js'
+export * from './blog-archives.js'
export * from './tag-colors.js'
export * from './locale.js'
diff --git a/theme/src/client/composables/scroll-behavior.ts b/theme/src/client/composables/scroll-behavior.ts
new file mode 100644
index 00000000..da7c3974
--- /dev/null
+++ b/theme/src/client/composables/scroll-behavior.ts
@@ -0,0 +1,25 @@
+import type { Router } from 'vuepress/client'
+import { nextTick } from 'vue'
+import { inBrowser } from '../utils/index.js'
+import { useScrollPromise } from './scroll-promise.js'
+
+export function enhanceScrollBehavior(router: Router) {
+ // handle scrollBehavior with transition
+ const scrollBehavior = router.options.scrollBehavior!
+ router.options.scrollBehavior = async (...args) => {
+ await useScrollPromise().wait()
+ return scrollBehavior(...args)
+ }
+
+ router.beforeEach(() => {
+ if (inBrowser) {
+ document.documentElement.classList.remove('smooth')
+ }
+ })
+
+ router.afterEach(() => nextTick(() => {
+ if (inBrowser) {
+ document.documentElement.classList.add('smooth')
+ }
+ }))
+}
diff --git a/theme/src/client/config.ts b/theme/src/client/config.ts
index cf1c4efb..80e927ea 100644
--- a/theme/src/client/config.ts
+++ b/theme/src/client/config.ts
@@ -2,61 +2,19 @@ import './styles/index.css'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
-import { h } from 'vue'
-import VPBadge from './components/global/VPBadge.vue'
-import { setupDarkMode, setupWatermark, useScrollPromise } from './composables/index.js'
+import { enhanceScrollBehavior, setupDarkMode, setupWatermark } from './composables/index.js'
+import { globalComponents } from './globalComponents.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
-import VPHomeBox from './components/Home/VPHomeBox.vue'
export default defineClientConfig({
enhance({ app, router }) {
setupDarkMode(app)
- // global component
- app.component('Badge', VPBadge)
- app.component('VPBadge', VPBadge) // alias
-
- app.component('DocSearch', () => {
- const SearchComponent
- = app.component('Docsearch') || app.component('SearchBox')
- if (SearchComponent)
- return h(SearchComponent)
-
- return null
- })
-
- app.component('PageComment', (props) => {
- const CommentService = app.component('CommentService')
- if (CommentService)
- return h(CommentService, props)
-
- return null
- })
-
- app.component('Icon', (props) => {
- const Iconify = app.component('Iconify')
- if (Iconify)
- return h(Iconify, props)
-
- return null
- })
-
- /** @deprecated */
- app.component('HomeBox', VPHomeBox)
- app.component('VPHomeBox', VPHomeBox)
-
- // handle scrollBehavior with transition
- const scrollBehavior = router.options.scrollBehavior!
- router.options.scrollBehavior = async (...args) => {
- await useScrollPromise().wait()
- return scrollBehavior(...args)
- }
+ enhanceScrollBehavior(router)
+ globalComponents(app)
},
setup() {
setupWatermark()
},
- layouts: {
- Layout,
- NotFound,
- },
+ layouts: { Layout, NotFound },
}) as ClientConfig
diff --git a/theme/src/client/globalComponents.ts b/theme/src/client/globalComponents.ts
new file mode 100644
index 00000000..2c619d18
--- /dev/null
+++ b/theme/src/client/globalComponents.ts
@@ -0,0 +1,50 @@
+import type { App } from 'vue'
+import { h } from 'vue'
+import VPHomeBox from '@theme/Home/VPHomeBox.vue'
+import VPCard from '@theme/global/VPCard.vue'
+import VPLinkCard from '@theme/global/VPLinkCard.vue'
+import VPBadge from '@theme/global/VPBadge.vue'
+import VPCardGrid from '@theme/global/VPCardGrid.vue'
+
+export function globalComponents(app: App) {
+ app.component('Badge', VPBadge)
+ app.component('VPBadge', VPBadge)
+
+ app.component('VPCard', VPCard)
+ app.component('Card', VPCard)
+
+ app.component('VPCardGrid', VPCardGrid)
+ app.component('CardGrid', VPCardGrid)
+
+ app.component('VPLinkCard', VPLinkCard)
+ app.component('LinkCard', VPLinkCard)
+
+ app.component('DocSearch', () => {
+ const SearchComponent
+ = app.component('Docsearch') || app.component('SearchBox')
+ if (SearchComponent)
+ return h(SearchComponent)
+
+ return null
+ })
+
+ app.component('PageComment', (props) => {
+ const CommentService = app.component('CommentService')
+ if (CommentService)
+ return h(CommentService, props)
+
+ return null
+ })
+
+ app.component('Icon', (props) => {
+ const Iconify = app.component('Iconify')
+ if (Iconify)
+ return h(Iconify, props)
+
+ return null
+ })
+
+ /** @deprecated */
+ app.component('HomeBox', VPHomeBox)
+ app.component('VPHomeBox', VPHomeBox)
+}
diff --git a/theme/src/client/index.ts b/theme/src/client/index.ts
index f4051017..2a17102e 100644
--- a/theme/src/client/index.ts
+++ b/theme/src/client/index.ts
@@ -11,6 +11,9 @@ export { default as Layout } from './layouts/Layout.vue'
export { default as NotFound } from './layouts/NotFound.vue'
export { default as VPBadge } from './components/global/VPBadge.vue'
+export { default as VPCard } from './components/global/VPCard.vue'
+export { default as VPCardGrid } from './components/global/VPCardGrid.vue'
+
export { default as VPImage } from './components/VPImage.vue'
export { default as VPButton } from './components/VPButton.vue'
export { default as VPLink } from './components/VPLink.vue'
diff --git a/theme/src/client/styles/content.css b/theme/src/client/styles/content.css
index bf36305d..8efbcc50 100644
--- a/theme/src/client/styles/content.css
+++ b/theme/src/client/styles/content.css
@@ -39,12 +39,21 @@
}
.vp-doc h3 {
- margin: 32px 0 0;
+ margin: 32px 0 16px;
font-size: 20px;
line-height: 28px;
letter-spacing: -0.01em;
}
+.vp-doc h4,
+.vp-doc h5,
+.vp-doc h6 {
+ margin: 24px 0 16px;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: -0.01em;
+}
+
.vp-doc .header-anchor {
position: relative;
color: inherit;
@@ -89,6 +98,10 @@
}
}
+.vp-doc img {
+ display: inline-block;
+}
+
.vp-doc img + img {
margin-left: 0.5rem;
}
@@ -148,10 +161,6 @@
font-weight: 600;
}
-.vp-doc a > img {
- display: inline-block;
-}
-
/**
* Lists
* -------------------------------------------------------------------------- */
diff --git a/theme/src/client/styles/custom-block.css b/theme/src/client/styles/custom-block.css
new file mode 100644
index 00000000..111247fc
--- /dev/null
+++ b/theme/src/client/styles/custom-block.css
@@ -0,0 +1,208 @@
+/* --------------------- demo-wrapper ------------------------ */
+.vp-doc .demo-wrapper {
+ display: flex;
+ flex-direction: column;
+ min-height: 40px;
+ margin: 40px -16px;
+ border: solid 1px var(--vp-c-divider);
+ border-radius: 8px;
+ box-shadow: var(--vp-shadow-2);
+ transition: var(--t-color);
+ transition-property: border, box-shadow;
+}
+
+.vp-doc .demo-wrapper .demo-head {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ min-height: 0;
+ border-bottom: solid 1px var(--vp-c-divider);
+ transition: border-bottom var(--t-color);
+}
+
+.vp-doc .demo-wrapper .demo-container {
+ min-height: 0;
+ padding: 20px;
+ font-size: 14px;
+ line-height: 22px;
+ background-color: var(--vp-c-bg-alt);
+ border-bottom-right-radius: 8px;
+ border-bottom-left-radius: 8px;
+ transition: background-color var(--t-color);
+}
+
+.vp-doc .demo-wrapper.has-title .demo-head {
+ border-bottom-color: transparent;
+}
+
+.vp-doc .demo-wrapper.only-img {
+ overflow: hidden;
+}
+
+.vp-doc .demo-wrapper.only-img img {
+ display: block;
+}
+
+.vp-doc .demo-wrapper.only-img .demo-container,
+.vp-doc .demo-wrapper.no-padding .demo-container {
+ padding: 0;
+}
+
+.vp-doc .demo-wrapper.has-height .demo-container {
+ height: var(--demo-container-height);
+ overflow-y: auto;
+}
+
+.vp-doc .demo-wrapper .demo-ctrl {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 5px 0 5px 8px;
+}
+
+.vp-doc .demo-wrapper .demo-ctrl i {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ background-color: #ccc;
+ border-radius: 100%;
+ transition: background-color var(--t-color);
+}
+
+.vp-doc .demo-wrapper .demo-ctrl i:nth-child(1) {
+ background-color: var(--vp-c-danger-3);
+}
+
+.vp-doc .demo-wrapper .demo-ctrl i:nth-child(2) {
+ background-color: var(--vp-c-warning-3);
+}
+
+.vp-doc .demo-wrapper .demo-ctrl i:nth-child(3) {
+ background-color: var(--vp-c-green-3);
+}
+
+.vp-doc .demo-wrapper .demo-title {
+ position: relative;
+ min-width: 0;
+ padding: 0 16px;
+ margin: 0 20px -1px;
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--vp-c-text-2);
+ background-color: var(--vp-c-bg-alt);
+ border-top-left-radius: 8px;
+ border-top-right-radius: 8px;
+ transition: var(--t-color);
+ transition-property: color, background-color;
+}
+
+.vp-doc .demo-wrapper .demo-title p {
+ max-width: 100%;
+ margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-wrap: nowrap;
+}
+
+.vp-doc .demo-wrapper .demo-title::after,
+.vp-doc .demo-wrapper .demo-title::before {
+ position: absolute;
+ bottom: 0;
+ z-index: 1;
+ width: 8px;
+ height: 8px;
+ content: " ";
+ transition: background var(--t-color);
+}
+
+.vp-doc .demo-wrapper .demo-title::before {
+ left: 100%;
+ background: radial-gradient(16px at right top, transparent 50%, var(--vp-c-bg-alt) 50%);
+}
+
+.vp-doc .demo-wrapper .demo-title::after {
+ right: 100%;
+ background: radial-gradient(16px at left top, transparent 50%, var(--vp-c-bg-alt) 50%);
+}
+
+.vp-doc .demo-wrapper .demo-container > *:first-child {
+ margin-top: 0;
+}
+
+.vp-doc .demo-wrapper .demo-container > *:last-child {
+ margin-bottom: 0;
+}
+
+@media (min-width: 419px) {
+ .vp-doc .demo-wrapper {
+ margin: 40px 0;
+ }
+}
+
+/* ---------------------------- Steps --------------------------- */
+.vp-doc .vp-steps {
+ margin: 16px 0;
+}
+
+.vp-doc .vp-steps > ol,
+.vp-doc .vp-steps > ul {
+ padding-inline-start: 0;
+ list-style: none;
+}
+
+.vp-doc .vp-steps > ol > li,
+.vp-doc .vp-steps > ul > li {
+ position: relative;
+ min-height: 22px;
+ padding-bottom: 1px;
+ padding-left: 44px;
+}
+
+.vp-doc .vp-steps > ol > li::before,
+.vp-doc .vp-steps > ul > li::before {
+ position: absolute;
+ inset-inline-start: 0;
+ top: 0;
+ width: 28px;
+ height: 28px;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 28px;
+ color: var(--vp-c-text-1);
+ text-align: center;
+ content: counter(list-item);
+ background-color: var(--vp-c-bg-soft);
+ border: solid 1px var(--vp-c-divider);
+ border-radius: 100%;
+ transition: var(--t-color);
+ transition-property: color, background-color, border-color;
+}
+
+.vp-doc .vp-steps > ol > li:not(:last-of-type)::after,
+.vp-doc .vp-steps > ul > li:not(:last-of-type)::after {
+ position: absolute;
+ inset-inline-start: 14px;
+ top: 34px;
+ bottom: 6px;
+ width: 1px;
+ content: "";
+ background-color: var(--vp-c-divider);
+ transition: background-color var(--t-color);
+}
+
+.vp-doc .vp-steps > ol > li > :first-child,
+.vp-doc .vp-steps > ul > li > :first-child {
+ margin-top: 0;
+}
+
+.vp-doc .vp-steps > ol > li > :first-child:where(h1,h2,h3,h4,h5,h6),
+.vp-doc .vp-steps > ul > li > :first-child:where(h1,h2,h3,h4,h5,h6) {
+ padding-top: 0;
+ border-top: none;
+}
+
+.vp-doc .vp-steps > ol > li + li,
+.vp-doc .vp-steps > ul > li + li {
+ margin-top: 0;
+}
diff --git a/theme/src/client/styles/index.css b/theme/src/client/styles/index.css
index a0803720..c5a6c315 100644
--- a/theme/src/client/styles/index.css
+++ b/theme/src/client/styles/index.css
@@ -8,6 +8,7 @@
@import url("./utils.css");
@import url("./content.css");
@import url("./code.css");
+@import url("./custom-block.css");
@import url("./twoslash.css");
@import url("./md-enhance.css");
diff --git a/theme/src/client/styles/md-enhance.css b/theme/src/client/styles/md-enhance.css
index 390bd33b..8a447120 100644
--- a/theme/src/client/styles/md-enhance.css
+++ b/theme/src/client/styles/md-enhance.css
@@ -587,148 +587,6 @@
font-size: 0.8rem;
}
-/* --------------------- demo-wrapper ------------------------ */
-.vp-doc .demo-wrapper {
- display: flex;
- flex-direction: column;
- min-height: 40px;
- margin: 40px -16px;
- border: solid 1px var(--vp-c-divider);
- border-radius: 8px;
- box-shadow: var(--vp-shadow-2);
- transition: var(--t-color);
- transition-property: border, box-shadow;
-}
-
-.vp-doc .demo-wrapper.has-title .demo-head {
- border-bottom-color: transparent;
-}
-
-.vp-doc .demo-wrapper.only-img {
- overflow: hidden;
-}
-
-.vp-doc .demo-wrapper.only-img img {
- display: block;
-}
-
-.vp-doc .demo-wrapper.only-img .demo-container,
-.vp-doc .demo-wrapper.no-padding .demo-container {
- padding: 0;
-}
-
-.vp-doc .demo-wrapper.has-height .demo-container {
- height: var(--demo-container-height);
- overflow-y: auto;
-}
-
-.vp-doc .demo-wrapper .demo-head {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- min-height: 0;
- border-bottom: solid 1px var(--vp-c-divider);
- transition: border-bottom var(--t-color);
-}
-
-.vp-doc .demo-wrapper .demo-ctrl {
- display: flex;
- gap: 5px;
- align-items: center;
- justify-content: flex-start;
- padding: 5px 0 5px 8px;
-}
-
-.vp-doc .demo-wrapper .demo-ctrl i {
- display: inline-block;
- width: 10px;
- height: 10px;
- background-color: #ccc;
- border-radius: 100%;
- transition: background-color var(--t-color);
-}
-
-.vp-doc .demo-wrapper .demo-ctrl i:nth-child(1) {
- background-color: var(--vp-c-danger-3);
-}
-
-.vp-doc .demo-wrapper .demo-ctrl i:nth-child(2) {
- background-color: var(--vp-c-warning-3);
-}
-
-.vp-doc .demo-wrapper .demo-ctrl i:nth-child(3) {
- background-color: var(--vp-c-green-3);
-}
-
-.vp-doc .demo-wrapper .demo-title {
- position: relative;
- min-width: 0;
- padding: 0 16px;
- margin: 0 20px -1px;
- font-size: 14px;
- font-weight: 500;
- color: var(--vp-c-text-2);
- background-color: var(--vp-c-bg-alt);
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
- transition: var(--t-color);
- transition-property: color, background-color;
-}
-
-.vp-doc .demo-wrapper .demo-title p {
- max-width: 100%;
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- text-wrap: nowrap;
-}
-
-.vp-doc .demo-wrapper .demo-title::after,
-.vp-doc .demo-wrapper .demo-title::before {
- position: absolute;
- bottom: 0;
- z-index: 1;
- width: 8px;
- height: 8px;
- content: " ";
- transition: background var(--t-color);
-}
-
-.vp-doc .demo-wrapper .demo-title::before {
- left: 100%;
- background: radial-gradient(16px at right top, transparent 50%, var(--vp-c-bg-alt) 50%);
-}
-
-.vp-doc .demo-wrapper .demo-title::after {
- right: 100%;
- background: radial-gradient(16px at left top, transparent 50%, var(--vp-c-bg-alt) 50%);
-}
-
-.vp-doc .demo-wrapper .demo-container {
- min-height: 0;
- padding: 20px;
- font-size: 14px;
- line-height: 22px;
- background-color: var(--vp-c-bg-alt);
- border-bottom-right-radius: 8px;
- border-bottom-left-radius: 8px;
- transition: background-color var(--t-color);
-}
-
-.vp-doc .demo-wrapper .demo-container > *:first-child {
- margin-top: 0;
-}
-
-.vp-doc .demo-wrapper .demo-container > *:last-child {
- margin-bottom: 0;
-}
-
-@media (min-width: 419px) {
- .vp-doc .demo-wrapper {
- margin: 40px 0;
- }
-}
-
/* --------------------- Markdown Enhance: Mermaid ------------------------ */
.mermaid-actions {
padding-right: 20px;
diff --git a/theme/src/client/styles/normalize.css b/theme/src/client/styles/normalize.css
index 94ce0beb..71be078a 100644
--- a/theme/src/client/styles/normalize.css
+++ b/theme/src/client/styles/normalize.css
@@ -23,7 +23,6 @@ html {
font-size: 16px;
line-height: 1.4;
- scroll-behavior: smooth;
scroll-padding-top: 80px;
}
diff --git a/theme/src/client/styles/utils.css b/theme/src/client/styles/utils.css
index 6991c0c8..0d74fe3a 100644
--- a/theme/src/client/styles/utils.css
+++ b/theme/src/client/styles/utils.css
@@ -12,6 +12,10 @@
margin: 0.3em;
}
+.smooth {
+ scroll-behavior: smooth;
+}
+
/* ----------------- Transition ------------------------ */
.fade-slide-y-enter-active {
transition: all 0.25s ease !important;
diff --git a/theme/src/node/extendsBundlerOptions.ts b/theme/src/node/config/extendsBundlerOptions.ts
similarity index 69%
rename from theme/src/node/extendsBundlerOptions.ts
rename to theme/src/node/config/extendsBundlerOptions.ts
index a19d59dd..b753f17a 100644
--- a/theme/src/node/extendsBundlerOptions.ts
+++ b/theme/src/node/config/extendsBundlerOptions.ts
@@ -1,4 +1,4 @@
-import { addViteConfig, addViteOptimizeDepsInclude, addViteSsrNoExternal } from '@vuepress/helper'
+import { addViteConfig, addViteOptimizeDepsExclude, addViteOptimizeDepsInclude, addViteSsrNoExternal } from '@vuepress/helper'
import type { App } from 'vuepress'
export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
@@ -9,6 +9,7 @@ export function extendsBundlerOptions(bundlerOptions: any, app: App): void {
})
addViteOptimizeDepsInclude(bundlerOptions, app, '@vueuse/core', true)
+ addViteOptimizeDepsExclude(bundlerOptions, app, '@theme')
addViteSsrNoExternal(bundlerOptions, app, [
'@vuepress/helper',
diff --git a/theme/src/node/config/index.ts b/theme/src/node/config/index.ts
index 99c2e188..4b3f606f 100644
--- a/theme/src/node/config/index.ts
+++ b/theme/src/node/config/index.ts
@@ -1,5 +1,12 @@
+export * from './resolveThemeOption.js'
export * from './resolveLocaleOptions.js'
export * from './resolveThemeData.js'
+export * from './resolveProvideData.js'
+export * from './resolveAlias.js'
+
+export * from './extendsBundlerOptions.js'
+export * from './templateBuildRenderer.js'
+
export * from './resolveSearchOptions.js'
export * from './resolvePageHead.js'
export * from './resolveEncrypt.js'
diff --git a/theme/src/node/config/resolveAlias.ts b/theme/src/node/config/resolveAlias.ts
new file mode 100644
index 00000000..e4b66179
--- /dev/null
+++ b/theme/src/node/config/resolveAlias.ts
@@ -0,0 +1,18 @@
+import { fs, path } from 'vuepress/utils'
+import { resolve } from '../utils.js'
+
+export function resolveAlias() {
+ return {
+ ...Object.fromEntries(
+ fs.readdirSync(
+ resolve('client/components'),
+ { encoding: 'utf-8', recursive: true },
+ )
+ .filter(file => file.endsWith('.vue'))
+ .map(file => [
+ path.join('@theme', file),
+ resolve('client/components', file),
+ ]),
+ ),
+ }
+}
diff --git a/theme/src/node/config/resolveProvideData.ts b/theme/src/node/config/resolveProvideData.ts
new file mode 100644
index 00000000..a0bb6b47
--- /dev/null
+++ b/theme/src/node/config/resolveProvideData.ts
@@ -0,0 +1,28 @@
+import type { App } from 'vuepress'
+import { entries, fromEntries, getRootLangPath, isPlainObject } from '@vuepress/helper'
+import type { PlumeThemeEncrypt, PlumeThemePluginOptions } from '../../shared/index.js'
+import { PRESET_LOCALES } from '../locales/index.js'
+import { resolveEncrypt } from './resolveEncrypt.js'
+
+export function resolveProvideData(
+ app: App,
+ plugins: PlumeThemePluginOptions,
+ encrypt?: PlumeThemeEncrypt,
+
+): Record {
+ const root = getRootLangPath(app)
+
+ return {
+ // 注入 加密配置
+ ...resolveEncrypt(encrypt),
+ // 注入水印配置
+ __PLUME_WM_FP__: isPlainObject(plugins.watermark)
+ ? plugins.watermark.fullPage !== false
+ : true,
+ // 注入多语言配置
+ __PLUME_PRESET_LOCALE__: fromEntries(
+ entries(PRESET_LOCALES)
+ .map(([locale, value]) => [locale === root ? '/' : locale, value]),
+ ),
+ }
+}
diff --git a/theme/src/node/config/resolveSearchOptions.ts b/theme/src/node/config/resolveSearchOptions.ts
index 7b07faaf..32d4013d 100644
--- a/theme/src/node/config/resolveSearchOptions.ts
+++ b/theme/src/node/config/resolveSearchOptions.ts
@@ -4,7 +4,10 @@ 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 {
+export function resolveSearchOptions(
+ app: App,
+ { locales, ...options }: SearchPluginOptions = {},
+): SearchPluginOptions {
return {
...options,
locales: getLocaleConfig({
@@ -15,7 +18,10 @@ export function resolveSearchOptions(app: App, { locales, ...options }: SearchPl
}
}
-export function resolveDocsearchOptions(app: App, { locales, ...options }: DocsearchPluginOptions = {}): DocsearchPluginOptions {
+export function resolveDocsearchOptions(
+ app: App,
+ { locales, ...options }: DocsearchPluginOptions = {},
+): DocsearchPluginOptions {
return {
...options,
locales: getLocaleConfig({
diff --git a/theme/src/node/config/resolveThemeOption.ts b/theme/src/node/config/resolveThemeOption.ts
new file mode 100644
index 00000000..39157647
--- /dev/null
+++ b/theme/src/node/config/resolveThemeOption.ts
@@ -0,0 +1,19 @@
+import type { PlumeThemeOptions } from '../../shared/index.js'
+import { logger } from '../utils.js'
+
+export function resolveThemeOptions({ themePlugins, plugins, encrypt, hostname, ...localeOptions }: PlumeThemeOptions) {
+ const pluginOptions = plugins ?? themePlugins ?? {}
+
+ if (themePlugins) {
+ logger.warn(
+ `The 'themePlugins' option is deprecated. Please use 'plugins' instead.`,
+ )
+ }
+
+ return {
+ pluginOptions,
+ encrypt,
+ hostname,
+ localeOptions,
+ }
+}
diff --git a/theme/src/node/templateBuildRenderer.ts b/theme/src/node/config/templateBuildRenderer.ts
similarity index 89%
rename from theme/src/node/templateBuildRenderer.ts
rename to theme/src/node/config/templateBuildRenderer.ts
index fed950ae..a512f14a 100644
--- a/theme/src/node/templateBuildRenderer.ts
+++ b/theme/src/node/config/templateBuildRenderer.ts
@@ -1,5 +1,5 @@
import { type TemplateRendererContext, templateRenderer } from 'vuepress/utils'
-import { getThemePackage } from './utils.js'
+import { getThemePackage } from '../utils.js'
export function templateBuildRenderer(template: string, context: TemplateRendererContext) {
const pkg = getThemePackage()
diff --git a/theme/src/node/index.ts b/theme/src/node/index.ts
index a89a69bb..c1dd2421 100644
--- a/theme/src/node/index.ts
+++ b/theme/src/node/index.ts
@@ -5,4 +5,7 @@ export * from '../shared/index.js'
export { plumeTheme }
+/**
+ * @deprecated 请使用 具名导出 替代 默认导出
+ */
export default plumeTheme
diff --git a/theme/src/node/locales/en.ts b/theme/src/node/locales/en.ts
index a0da0045..06cf33b5 100644
--- a/theme/src/node/locales/en.ts
+++ b/theme/src/node/locales/en.ts
@@ -1,6 +1,5 @@
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
-import type { PlumeThemeLocaleData } from '../../shared/index.js'
-import type { PresetLocale } from '../types.js'
+import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
export const enLocale: PlumeThemeLocaleData = {
selectLanguageName: 'English',
diff --git a/theme/src/node/locales/index.ts b/theme/src/node/locales/index.ts
index 4467b5b0..1faa093b 100644
--- a/theme/src/node/locales/index.ts
+++ b/theme/src/node/locales/index.ts
@@ -1,7 +1,6 @@
import type { DocsearchLocaleOptions } from '@vuepress/plugin-docsearch'
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
-import type { PlumeThemeLocaleData } from '../../shared/index.js'
-import type { PresetLocale } from '../types.js'
+import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
import { zhDocsearchLocale, zhLocale, zhPresetLocale, zhSearchLocale } from './zh.js'
import { enLocale, enPresetLocale, enSearchLocale } from './en.js'
diff --git a/theme/src/node/locales/zh.ts b/theme/src/node/locales/zh.ts
index df0d53dc..c77c8049 100644
--- a/theme/src/node/locales/zh.ts
+++ b/theme/src/node/locales/zh.ts
@@ -1,7 +1,6 @@
import type { DocsearchLocaleOptions } from '@vuepress/plugin-docsearch'
import type { SearchLocaleOptions } from '@vuepress-plume/plugin-search'
-import type { PlumeThemeLocaleData } from '../../shared/index.js'
-import type { PresetLocale } from '../types.js'
+import type { PlumeThemeLocaleData, PresetLocale } from '../../shared/index.js'
export const zhLocale: PlumeThemeLocaleData = {
selectLanguageName: '简体中文',
diff --git a/theme/src/node/plugins/containerPlugins.ts b/theme/src/node/plugins/containerPlugins.ts
index 49743fb0..723f2dde 100644
--- a/theme/src/node/plugins/containerPlugins.ts
+++ b/theme/src/node/plugins/containerPlugins.ts
@@ -39,6 +39,60 @@ export const customContainerPlugins: Plugin[] = [
return '\n'
},
}),
+ /**
+ * :::steps
+ * 1. 步骤 1
+ * xxx
+ * 2. 步骤 2
+ * xxx
+ * 3. ...
+ * :::
+ */
+ containerPlugin({
+ type: 'steps',
+ before() {
+ return ''
+ },
+ after() {
+ return '
'
+ },
+ }),
+ /**
+ * ::: card title="xxx" icon="xxx"
+ * xxx
+ * :::
+ */
+ containerPlugin({
+ type: 'card',
+ before(info) {
+ const title = resolveAttr(info, 'title')
+ const icon = resolveAttr(info, 'icon')
+ return ``
+ },
+ after() {
+ return ''
+ },
+ }),
+
+ /**
+ * :::: card-grid
+ * ::: card
+ * xxx
+ * :::
+ * ::: card
+ * xxx
+ * :::
+ * ::::
+ */
+ containerPlugin({
+ type: 'card-grid',
+ before() {
+ return ''
+ },
+ after() {
+ return ''
+ },
+ }),
]
/**
diff --git a/theme/src/node/theme.ts b/theme/src/node/theme.ts
index 2e1bbbf7..251cdffa 100644
--- a/theme/src/node/theme.ts
+++ b/theme/src/node/theme.ts
@@ -1,62 +1,40 @@
import type { Page, Theme } from 'vuepress/core'
-import { fs, path } from 'vuepress/utils'
-import { isPlainObject } from '@vuepress/helper'
import type { PlumeThemeOptions, PlumeThemePageData } from '../shared/index.js'
import { getPlugins } from './plugins/index.js'
import { extendsPageData, setupPage } from './setupPages.js'
-import { THEME_NAME, logger, resolve, templates } from './utils.js'
-import { resolveEncrypt, resolveLocaleOptions, resolvePageHead } from './config/index.js'
-import { extendsBundlerOptions } from './extendsBundlerOptions.js'
-import { templateBuildRenderer } from './templateBuildRenderer.js'
+import { THEME_NAME, resolve, templates } from './utils.js'
+import {
+ extendsBundlerOptions,
+ resolveAlias,
+ resolveLocaleOptions,
+ resolvePageHead,
+ resolveProvideData,
+ resolveThemeOptions,
+ templateBuildRenderer,
+} from './config/index.js'
import { setupPrepare, watchPrepare } from './prepare/index.js'
-export function plumeTheme({
- themePlugins,
- plugins,
- encrypt,
- hostname,
- ...localeOptions
-}: PlumeThemeOptions = {}): Theme {
- const pluginOptions = plugins ?? themePlugins ?? {}
-
- const watermarkFullPage = isPlainObject(pluginOptions.watermark)
- ? pluginOptions.watermark.fullPage !== false
- : true
-
- if (themePlugins) {
- logger.warn(
- `The 'themePlugins' option is deprecated. Please use 'plugins' instead.`,
- )
- }
+export function plumeTheme(options: PlumeThemeOptions = {}): Theme {
+ const {
+ localeOptions: rawLocaleOptions,
+ pluginOptions,
+ hostname,
+ encrypt,
+ } = resolveThemeOptions(options)
return (app) => {
- localeOptions = resolveLocaleOptions(app, localeOptions)
+ const localeOptions = resolveLocaleOptions(app, rawLocaleOptions)
return {
name: THEME_NAME,
- define: {
- ...resolveEncrypt(encrypt),
- __PLUME_WM_FP__: watermarkFullPage,
- },
+ define: resolveProvideData(app, pluginOptions, encrypt),
templateBuild: templates('build.html'),
clientConfigFile: resolve('client/config.js'),
- alias: {
- ...Object.fromEntries(
- fs.readdirSync(
- resolve('client/components'),
- { encoding: 'utf-8', recursive: true },
- )
- .filter(file => file.endsWith('.vue'))
- .map(file => [
- path.join('@theme', file),
- resolve('client/components', file),
- ]),
- ),
- },
+ alias: resolveAlias(),
plugins: getPlugins({ app, pluginOptions, localeOptions, encrypt, hostname }),
diff --git a/theme/src/node/types.ts b/theme/src/node/types.ts
deleted file mode 100644
index d461eb5f..00000000
--- a/theme/src/node/types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export interface PresetLocale {
- home: string
- blog: string
- tag: string
- archive: string
-}
diff --git a/theme/src/shared/base.ts b/theme/src/shared/base.ts
index 3a361c8e..b7e185ad 100644
--- a/theme/src/shared/base.ts
+++ b/theme/src/shared/base.ts
@@ -38,3 +38,10 @@ export type SocialLinkIconUnion =
| 'xbox'
export type SocialLinkIcon = SocialLinkIconUnion | { svg: string }
+
+export interface PresetLocale {
+ home: string
+ blog: string
+ tag: string
+ archive: string
+}
diff --git a/theme/src/shared/options/locale.ts b/theme/src/shared/options/locale.ts
index 17f0a522..99081b7f 100644
--- a/theme/src/shared/options/locale.ts
+++ b/theme/src/shared/options/locale.ts
@@ -48,9 +48,9 @@ export interface PlumeThemeLocaleData extends LocaleData {
social?: SocialLink[]
/**
- * Navbar config
+ * 导航栏配置
*
- * Set to `false` to disable navbar in current locale
+ * 设置为 `false` 将会禁用导航栏
*/
navbar?: false | NavItem[]
@@ -75,114 +75,119 @@ export interface PlumeThemeLocaleData extends LocaleData {
/**
* 笔记配置, 笔记中的文章默认不会出现在首页文章列表
*
- * 注:你也可以将notes配置到navbar中,默认自动生成在右侧栏目中
+ * 注:也可以将notes配置到navbar中
*/
notes?: false | NotesDataOptions
+ /**
+ * 要显示的标题级别。
+ *
+ * 单个数字表示只显示该级别的标题。
+ *
+ * 如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
+ *
+ * 'deep' 与 [2, 6] 相同,将显示从 到 的所有标题。
+ *
+ * @default [2, 3]
+ */
outline?: ThemeOutline
/**
* 是否显示侧边栏
*
+ * - `false` 表示禁用 右侧边栏
+ * - `true` 表示启用 右侧边栏
+ * - `'left` 表示将有侧边栏移动到文章内容左侧,sidebar 右侧
+ *
* @default true
*/
aside?: boolean | 'left'
/**
- * language text
+ * 选择语言菜单 的文本。
*/
selectLanguageText?: string
/**
- * language aria label
+ * 选择语言菜单 的 `aria-label` 属性。
*/
selectLanguageAriaLabel?: string
/**
- * language name
+ * 语言名称
+ *
+ * 仅能在主题配置的 locales 的内部生效 。它将被用作 locale 的语言名称,展示在 选择语言菜单 内。
*/
selectLanguageName?: string
/**
- * Page meta - edit link config
*
- * Whether to show "Edit this page" or not
+ *
+ * 是否显示 "编辑此页"
*/
editLink?: boolean
/**
- * Page meta - edit link config
+ * "编辑此页" 的文本
*
- * The text to replace the default "Edit this page"
+ * @default "Edit this page"
*/
editLinkText?: string
/**
- * Page meta - edit link config
- *
- * Pattern of edit link
+ * "编辑此页" 的链接匹配模式
*
* @example ':repo/edit/:branch/:path'
*/
editLinkPattern?: string
/**
- * Page meta - edit link config
- *
- * Use `repo` config by default
- *
- * Set this config if your docs is placed in a different repo
+ * 文档仓库配置, 用于生成 Edit this page 链接
*/
docsRepo?: string
/**
- * Page meta - edit link config
- *
- * Set this config if the branch of your docs is not 'main'
+ * 文档仓库分支配置,用于生成 `Edit this page` 链接。
*/
docsBranch?: string
/**
- * Page meta - edit link config
- *
- * Set this config if your docs is placed in sub dir of your `docsRepo`
+ * 文档仓库目录配置,用于生成 `Edit this page` 链接。
*/
docsDir?: string
/**
- * Page meta - last updated config
+ * 最后更新时间
*
- * Whether to show "Last Updated" or not
+ * @default { text: 'Last Updated', formatOptions: { dateStyle: 'short', timeStyle: 'short' } }
*/
lastUpdated?: false | LastUpdatedOptions
/**
- * @deprecated Use `lastUpdated.text` instead.
+ * @deprecated 使用 `lastUpdated.text` 代替.
*
- * Set custom last updated text.
+ * "最后更新时间" 的文本
*
* @default 'Last updated'
*/
lastUpdatedText?: string
/**
- * Page meta - contributors config
- *
- * Whether to show "Contributors" or not
+ * 是否显示贡献者
*/
contributors?: boolean
/**
- * Page meta - contributors config
- *
- * The text to replace the default "Contributors"
+ * 贡献者的文本
*/
contributorsText?: string
- // backToHome?: string
-
/**
- * sidebar menu label
+ * 移动设备下的导航栏中 菜单选项的文字。
+ *
+ * @default 'Menu'
*/
sidebarMenuLabel?: string
/**
- * return to top label
+ * 移动设备下的导航栏中返回顶部的文字。
+ *
+ * @default 'return to top'
*/
returnToTopLabel?: string
@@ -193,15 +198,30 @@ export interface PlumeThemeLocaleData extends LocaleData {
*/
outlineLabel?: string
+ /**
+ * 上一页的文本
+ *
+ * @default 'Previous Page'
+ */
prevPageLabel?: string
+ /**
+ * 下一页的文本
+ *
+ * @default 'Next Page'
+ */
nextPageLabel?: string
/**
* 是否显示外部链接图标
+ *
+ * @default true
*/
externalLinkIcon?: string
+ /**
+ * 页脚配置。
+ */
footer?:
| false
| {
@@ -210,7 +230,7 @@ export interface PlumeThemeLocaleData extends LocaleData {
}
/**
- * 404 page options
+ * 404 页面配置
*/
notFound?: {
code?: string | number