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 +![hero](/images/custom-hero.png) +::: +``` + +**输出:** +::: demo-wrapper img no-padding +![hero](/images/custom-hero.png) +::: + +包含 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 -![hero](/images/custom-hero.png) -::: -``` - -**输出:** -::: demo-wrapper img no-padding -![hero](/images/custom-hero.png) -::: - -包含 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