Merge pull request #88 from pengzhanbo/RC-62

RC-62
This commit is contained in:
pengzhanbo 2024-06-03 01:24:12 +08:00 committed by GitHub
commit ecc757f40a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 948 additions and 683 deletions

View File

@ -7,16 +7,16 @@
- 主题于 `theme` 目录中进行开发维护。
- 插件于 `plugins` 目录中进行开发维护。
- 示例`docs` 目录中进行开发维护。
- 文档`docs` 目录中进行开发维护。
`plugins` 目录中:
- `plugin-auto-frontmatter` 为 md 文件自动添加 frontmatter。
- `plugin-blog-data` 生成 blog 文章列表数据
- `plugin-notes-data` 生成 notes 数据,管理不同 note 的 `sidebar` 的数据
- `plugin-caniuse`: 添加 `caniuse` 内容容器
- ~~`plugin-caniuse`: 添加 `caniuse` 内容容器,已弃用,不再维护~~
- `plugin-content-update`: 重写 `Content` 组件,提供 `onContentUpdated` 钩子
- `plugin-copy-code`: 为 代码块添加 复制 按钮,并适配 `shikiji`
- ~~`plugin-copy-code`: 为 代码块添加 复制 按钮,并适配 `shikiji`,已弃用,不再维护~~
- `plugin-search`: 为主题提供 全文模糊搜索 功能
- `plugin-shikiji`: 代码高亮插件,支持 highlight、diff、focus、error level
- `plugin-iconify`: 添加全局组件 `Iconify`
@ -28,7 +28,7 @@
开发要求:
- [Node.js](http://nodejs.org/) version 18.16.0+
- [pnpm](https://pnpm.io/zh/) version 8+
- [pnpm](https://pnpm.io/zh/) version 9+
克隆代码仓库,并安装依赖:

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import HomeHero from 'vuepress-theme-plume/client/components/Home/HomeHero.vue'
import { useDarkMode } from 'vuepress-theme-plume/client/composables/darkMode'
import HomeHero from 'vuepress-theme-plume/components/Home/HomeHero.vue'
import { useDarkMode } from 'vuepress-theme-plume/composables'
import type { PlumeThemeHomeHeroTintPlate } from 'vuepress-theme-plume/client'
import { computed, ref, watch } from 'vue'
import DemoWrapper from './DemoWrapper.vue'

View File

@ -54,7 +54,7 @@ permalink: /config/frontmatter/basic/
- 类型: `boolean`
- 默认值: `true`
当前文章内的 外部链接是否显示 外部链接图标 即 “ <ExternalLinkIcon /> ” 图标
当前文章内的 外部链接是否显示 外部链接图标
### backToTop
@ -77,6 +77,20 @@ permalink: /config/frontmatter/basic/
当前文章是否 显示 右侧边栏。
### outline
- 类型: `false | number | [number, number] | 'deep'`
- 默认值: `[2, 3]`
- 详情:
要显示的标题级别。
单个数字表示只显示该级别的标题。
如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
`'deep'``[2, 6]` 相同,将显示从 `<h2>``<h6>` 的所有标题。
### prev
- 类型: `string | { text: string, link: string, icon?: string }`

View File

@ -16,6 +16,79 @@ permalink: /config/basic/
主题使用的插件默认已进行了配置,大多数情况下您不需要进行任何修改,如果需要使用到细致的定制化,请查阅
[此文档](/config/plugins/)
### hostname
- 类型: `string`
- 默认值: `''`
- 详情:
部署站点域名。
`hostname` 配置为有效域名时,主题将会生成 `sitemap``seo` 相关的内容。
### blog
- 类型: `false | BlogOptions`
- 默认值: `{ link: '/blog/', include: ['**/*.md'], exclude: [] }`
- 详情:
博客配置。
```ts
interface BlogOptions {
/**
* blog list link
*
* @default '/blog/'
*/
link?: string
/**
* 在 `blog.dir` 目录中,通过 glob string 配置包含文件
*
* @default - ['**\*.md']
*/
include?: string[]
/**
* 在 `blog.dir` 目录中,通过 glob string 配置排除的文件
*
* README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章
*
* @default - ['.vuepress/', 'node_modules/', '{README,index}.md']
*/
exclude?: string[]
/**
* 分页配置
*/
pagination?: false | {
/**
* 每页显示的文章数量
* @default 10
*/
perPage?: number
}
/**
* 是否启用标签页
* @default true
*/
tags?: boolean
/**
* 是否启用归档页
* @default true
*/
archives?: boolean
}
```
### article
- 类型: `string`
- 默认值: `/article/`
- 详情: 文章链接前缀
### locales
- 类型: `Record<string, PlumeThemeLocaleConfig>`
@ -69,16 +142,6 @@ permalink: /config/basic/
- 默认值: `'Appearance'`
- 详情: 导航栏中的主题切换按钮的文本。
### hostname
- 类型: `string`
- 默认值: `''`
- 详情:
部署站点域名。
`hostname` 配置为有效域名时,主题将会生成 `sitemap``seo` 相关的内容。
### avatar
- 类型: `PlumeThemeAvatar`
@ -162,79 +225,6 @@ export default {
允许显示在导航栏的社交链接。
该配置仅在 PC 端下有效。
### blog
- 类型: `false | BlogOptions`
- 默认值: `{ link: '/blog/', include: ['**/*.md'], exclude: [] }`
- 详情:
博客配置。
```ts
interface BlogOptions {
/**
* blog list link
*
* @default '/blog/'
*/
link?: string
/**
* 在 `blog.dir` 目录中,通过 glob string 配置包含文件
*
* @default - ['**\*.md']
*/
include?: string[]
/**
* 在 `blog.dir` 目录中,通过 glob string 配置排除的文件
*
* README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章
*
* @default - ['.vuepress/', 'node_modules/', '{README,index}.md']
*/
exclude?: string[]
/**
* 分页配置
*/
pagination?: false | {
/**
* 每页显示的文章数量
* @default 10
*/
perPage?: number
/**
* 前一页的文本
* @default 'Prev'
*/
prevPageText?: string
/**
* 后一页的文本
* @default 'Next'
*/
nextPageText?: string
}
/**
* 是否启用标签页
* @default true
*/
tags?: boolean
/**
* 是否启用归档页
* @default true
*/
archives?: boolean
}
```
### article
- 类型: `string`
- 默认值: `/article/`
- 详情: 文章链接前缀
### navbar
- 类型: `NavItem[]`
@ -332,12 +322,30 @@ type NavItem = string | {
- 类型: `false | PlumeThemeNotesOptions`
- 默认值: `{ link: '/note', dir: 'notes', notes: [] }`
- 详情: 笔记配置, 笔记中的文章默认不会出现在首页文章列表
- 详情:
笔记配置, 笔记中的文章默认不会出现在首页文章列表
你可以将配置的notes 配置到 navbar中以便浏览查看
详细配置请查看 [此文档](/config/notes/)
### outline
- 类型: `false | number | [number, number] | 'deep'`
- 默认值: `[2, 3]`
- 详情:
要显示的标题级别。
单个数字表示只显示该级别的标题。
如果传递的是一个元组,第一个数字是最小级别,第二个数字是最大级别。
`'deep'``[2, 6]` 相同,将显示从 `<h2>``<h6>` 的所有标题。
每个页面可以通过 [frontmatter outline](./frontmatter/basic.md#outline) 覆盖层级配置。
### selectLanguageName
- 类型: `string`

View File

@ -23,6 +23,7 @@ export default defineUserConfig({
```
你还可以通过 `:line-numbers` / `:no-line-numbers` 来控制当前代码块是否显示代码行号。
还可以通过在 `:line-numbers` 之后添加 `=` 来自定义起始行号,例如 `:line-numbers=2` 表示代码块中的行号从 `2` 开始。
**输入:**
@ -38,6 +39,12 @@ const line3 = 'This is line 3'
const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
```ts:line-numbers=2
// 行号已启用,并从 2 开始
const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
````
**输出:**
@ -54,6 +61,12 @@ const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
```ts:line-numbers=2
// 行号已启用,并从 2 开始
const line3 = 'This is line 3'
const line4 = 'This is line 4'
```
## 在代码块中实现行高亮
`[lang]` 之后紧跟随 `{xxxx}` ,可以实现行高亮,其中 `xxx` 表示要高亮的行号。

View File

@ -10,17 +10,41 @@ permalink: /guide/custom-style/
支持自定义样式。
该功能由 [@vuepress/plugin-palette](https://v2.vuepress.vuejs.org/zh/reference/plugin/palette.html) 提供支持。
主题虽然使用 [SASS](https://sass-lang.com/) 作为 CSS 预处理器,但所有的颜色使用的是 `CSS Vars` 定义,
因此,你可以创建 一个 css 文件 或 scss 文件,进行覆盖。
主题使用 [SASS](https://sass-lang.com/) 作为 CSS 预处理器。
首先,在 `.vuepress` 目录中,创建一个 `styles/index.css` 文件,
然后在 [客户端配置文件](https://v2.vuepress.vuejs.org/zh/guide/configuration.html#%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) 中,引入该文件即可。
用户可以通过 [style 文件](#style-文件) 来添加额外的样式。
:::code-tabs
@tab .vuepress/client.ts
```ts {1}
import './styles/index.css'
import { defineClientConfig } from 'vuepress/client'
export default defineClientConfig({
// ...
})
```
@tab .vuepress/styles/index.css
```css
:root {
--vp-c-brand-1: #5086a1;
}
```
:::
## Style 文件
Style 文件的路径是 `.vuepress/styles/index.scss`
`.vuepress` 目录中,创建一个如 `custom.css` 的文件,
你可以在这里添加额外的样式,或者覆盖默认样式:
在这里添加额外的样式,或者覆盖默认样式:
``` scss
:root {

View File

@ -12,7 +12,7 @@
"vuepress": "2.0.0-rc.12"
},
"dependencies": {
"@iconify/json": "^2.2.214",
"@iconify/json": "^2.2.215",
"@vuepress/bundler-vite": "2.0.0-rc.12",
"anywhere": "^1.6.0",
"chart.js": "^4.4.3",

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.61",
"private": true,
"packageManager": "pnpm@9.1.2",
"packageManager": "pnpm@9.1.4",
"author": "pengzhanbo <q942450674@outlook.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"keywords": [
@ -15,7 +15,7 @@
],
"engines": {
"node": "^18 || >=20.0.0",
"pnpm": ">=8"
"pnpm": ">=9"
},
"scripts": {
"build": "pnpm run clean && pnpm run build:package",
@ -56,10 +56,10 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"rimraf": "^5.0.7",
"stylelint": "^16.6.0",
"stylelint": "^16.6.1",
"tsconfig-vuepress": "^4.5.0",
"typescript": "^5.4.5",
"vite": "^5.2.11"
"vite": "5.2.11"
},
"lint-staged": {
"*": "eslint --fix"

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-auto-frontmatter",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - auto frontmatter",
"description": "The Plugin for VuePress 2 - auto frontmatter",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-baidu-tongji",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - baidu tongji",
"description": "The Plugin for VuePress 2 - baidu tongji",
"author": "pengzhanbo <volodymyr@foxmail.com> (https://github.com/pengzhanbo/)",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-blog-data",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - blog data",
"description": "The Plugin for VuePress 2 - blog data",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.61",
"private": "true",
"description": "The Plugin for VuePres 2, Support Can-I-Use feature",
"description": "The Plugin for VuePress 2, Support Can-I-Use feature",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-content-update",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - content update",
"description": "The Plugin for VuePress 2 - content update",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.61",
"private": "true",
"description": "The Plugin for VuePres 2 - copy code",
"description": "The Plugin for VuePress 2 - copy code",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -0,0 +1 @@
# @vuepress-plume/plugin-fonts

View File

@ -0,0 +1,47 @@
{
"name": "@vuepress-plume/plugin-fonts",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePress 2 - fonts",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git",
"directory": "plugins/plugin-fonts"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run copy && pnpm run ts",
"clean": "rimraf --glob ./lib ./*.tsbuildinfo",
"copy": "cpx \"src/**/*.{d.ts,vue,css,scss,jpg,png,woff2}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.12"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"fonts",
"vuepress-plugin-fonts"
]
}

View File

@ -0,0 +1,3 @@
import './styles/fonts.css'
export default {}

View File

@ -248,5 +248,3 @@
local("Source Han Sans SC");
unicode-range: U+2018, U+2019, U+201C, U+201D; /* 分别是 ‘’“” */
}
/* Generate the subsetted fonts using: `pyftsubset <file>.woff2 --unicodes="<range>" --output-file="inter-<style>-<subset>.woff2" --flavor=woff2` */

View File

@ -0,0 +1 @@
export * from './plugin.js'

View File

@ -0,0 +1,12 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
export function fontsPlugin(): Plugin {
return {
name: '@vuepress-plume/plugin-fonts',
clientConfigFile: path.resolve(
getDirname(import.meta.url),
'../client/config.js',
),
}
}

View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["./src"]
}

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-iconify",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - iconify",
"description": "The Plugin for VuePress 2 - iconify",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -2,7 +2,7 @@
"name": "vuepress-plugin-md-power",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - markdown power",
"description": "The Plugin for VuePress 2 - markdown power",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
@ -46,19 +46,19 @@
}
},
"dependencies": {
"@iconify/utils": "^2.1.23",
"@vuepress/helper": "2.0.0-rc.31",
"@vueuse/core": "^10.9.0",
"@iconify/utils": "^2.1.24",
"@vuepress/helper": "2.0.0-rc.33",
"@vueuse/core": "^10.10.0",
"local-pkg": "^0.5.0",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.7",
"shiki": "^1.6.0",
"tm-grammars": "^1.12.4",
"shiki": "^1.6.1",
"tm-grammars": "^1.12.5",
"tm-themes": "^1.4.3",
"vue": "^3.4.27"
},
"devDependencies": {
"@iconify/json": "^2.2.214",
"@iconify/json": "^2.2.215",
"@types/markdown-it": "^14.1.1"
},
"publishConfig": {

View File

@ -2,7 +2,7 @@
"name": "vuepress-plugin-netlify-functions",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2, Support Netlify Functions",
"description": "The Plugin for VuePress 2, Support Netlify Functions",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
@ -52,11 +52,11 @@
"dotenv": "^16.4.5",
"esbuild": "^0.21.4",
"execa": "^9.1.0",
"netlify-cli": "^17.23.8",
"netlify-cli": "^17.25.0",
"portfinder": "^1.0.32"
},
"devDependencies": {
"@types/node": "^20.12.12"
"@types/node": "^20.12.13"
},
"publishConfig": {
"access": "public"

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-notes-data",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - notes data",
"description": "The Plugin for VuePress 2 - notes data",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -190,7 +190,7 @@ function initSidebarByConfig(
text: current?.title || text,
link: current?.link,
icon: current?.frontmatter.icon,
items: [],
// items: [],
}
}
else {

View File

@ -3,7 +3,7 @@
"type": "module",
"version": "1.0.0-rc.61",
"private": true,
"description": "The Plugin for VuePres 2",
"description": "The Plugin for VuePress 2",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-search",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - local search",
"description": "The Plugin for VuePress 2 - local search",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
@ -40,9 +40,9 @@
"vuepress": "2.0.0-rc.12"
},
"dependencies": {
"@vuepress/helper": "2.0.0-rc.31",
"@vueuse/core": "^10.9.0",
"@vueuse/integrations": "^10.9.0",
"@vuepress/helper": "2.0.0-rc.33",
"@vueuse/core": "^10.10.0",
"@vueuse/integrations": "^10.10.0",
"chokidar": "^3.6.0",
"focus-trap": "^7.5.4",
"mark.js": "^8.11.1",

View File

@ -2,7 +2,7 @@
"name": "@vuepress-plume/plugin-shikiji",
"type": "module",
"version": "1.0.0-rc.61",
"description": "The Plugin for VuePres 2 - shiki",
"description": "The Plugin for VuePress 2 - shiki",
"author": "pengzhanbo <volodymyr@foxmail.com>",
"license": "MIT",
"homepage": "https://github.com/pengzhanbo/vuepress-theme-plume#readme",
@ -36,16 +36,16 @@
"vuepress": "2.0.0-rc.12"
},
"dependencies": {
"@shikijs/transformers": "^1.6.0",
"@shikijs/twoslash": "^1.6.0",
"@shikijs/transformers": "^1.6.1",
"@shikijs/twoslash": "^1.6.1",
"@types/hast": "^3.0.4",
"@vuepress/helper": "2.0.0-rc.31",
"@vuepress/helper": "2.0.0-rc.33",
"floating-vue": "^5.2.2",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-from-markdown": "^2.0.1",
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-hast": "^13.1.0",
"nanoid": "^5.0.7",
"shiki": "^1.6.0",
"shiki": "^1.6.1",
"twoslash": "^0.2.6",
"twoslash-vue": "^0.2.6"
},

View File

@ -6,6 +6,7 @@ import type { LineNumberOptions } from '../types.js'
const LINE_NUMBERS_REGEXP = /:line-numbers\b/
const NO_LINE_NUMBERS_REGEXP = /:no-line-numbers\b/
const LINE_NUMBERS_START_REGEXP = /:line-numbers=(\d+)\b/
export function lineNumberPlugin(md: Markdown, { lineNumbers = true }: LineNumberOptions = {}): void {
const rawFence = md.renderer.rules.fence!
@ -41,11 +42,15 @@ export function lineNumberPlugin(md: Markdown, { lineNumbers = true }: LineNumbe
)
return rawCode
const startNumbers
= Number(info.match(LINE_NUMBERS_START_REGEXP)?.[1] ?? 1) - 1
const lineNumbersStyle = `style="counter-reset:line-number ${startNumbers}"`
const lineNumbersCode = [...Array(lines.length)]
.map(() => `<div class="line-number"></div>`)
.join('')
const lineNumbersWrapperCode = `<div class="line-numbers" aria-hidden="true">${lineNumbersCode}</div>`
const lineNumbersWrapperCode = `<div class="line-numbers" aria-hidden="true" ${lineNumbersStyle}>${lineNumbersCode}</div>`
const finalCode = rawCode
.replace(/<\/div>$/, `${lineNumbersWrapperCode}</div>`)

761
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -30,14 +30,14 @@
"types": "./lib/client/index.d.ts",
"import": "./lib/client/index.js"
},
"./client/components/*": {
"./components/*": {
"import": "./lib/client/components/*"
},
"./client/composables": {
"./composables": {
"types": "./lib/client/composables/index.d.ts",
"import": "./lib/client/composables/index.js"
},
"./client/composables/*": {
"./composables/*": {
"types": "./lib/client/composables/*.d.ts",
"import": "./lib/client/composables/*.js"
},
@ -67,27 +67,28 @@
},
"dependencies": {
"@pengzhanbo/utils": "^1.1.2",
"@vuepress-plume/plugin-auto-frontmatter": "workspace:~",
"@vuepress-plume/plugin-baidu-tongji": "workspace:~",
"@vuepress-plume/plugin-blog-data": "workspace:~",
"@vuepress-plume/plugin-content-update": "workspace:~",
"@vuepress-plume/plugin-iconify": "workspace:~",
"@vuepress-plume/plugin-notes-data": "workspace:~",
"@vuepress-plume/plugin-search": "workspace:~",
"@vuepress-plume/plugin-shikiji": "workspace:~",
"@vuepress/helper": "2.0.0-rc.31",
"@vuepress/plugin-active-header-links": "2.0.0-rc.31",
"@vuepress/plugin-comment": "2.0.0-rc.31",
"@vuepress/plugin-docsearch": "2.0.0-rc.31",
"@vuepress/plugin-git": "2.0.0-rc.31",
"@vuepress/plugin-markdown-container": "2.0.0-rc.31",
"@vuepress/plugin-medium-zoom": "2.0.0-rc.31",
"@vuepress/plugin-nprogress": "2.0.0-rc.31",
"@vuepress/plugin-reading-time": "2.0.0-rc.31",
"@vuepress/plugin-seo": "2.0.0-rc.31",
"@vuepress/plugin-sitemap": "2.0.0-rc.31",
"@vuepress/plugin-theme-data": "2.0.0-rc.31",
"@vuepress/plugin-watermark": "2.0.0-rc.31",
"@vuepress-plume/plugin-auto-frontmatter": "workspace:*",
"@vuepress-plume/plugin-baidu-tongji": "workspace:*",
"@vuepress-plume/plugin-blog-data": "workspace:*",
"@vuepress-plume/plugin-content-update": "workspace:*",
"@vuepress-plume/plugin-fonts": "workspace:*",
"@vuepress-plume/plugin-iconify": "workspace:*",
"@vuepress-plume/plugin-notes-data": "workspace:*",
"@vuepress-plume/plugin-search": "workspace:*",
"@vuepress-plume/plugin-shikiji": "workspace:*",
"@vuepress/helper": "2.0.0-rc.33",
"@vuepress/plugin-active-header-links": "2.0.0-rc.33",
"@vuepress/plugin-comment": "2.0.0-rc.33",
"@vuepress/plugin-docsearch": "2.0.0-rc.33",
"@vuepress/plugin-git": "2.0.0-rc.33",
"@vuepress/plugin-markdown-container": "2.0.0-rc.33",
"@vuepress/plugin-nprogress": "2.0.0-rc.33",
"@vuepress/plugin-photo-swipe": "2.0.0-rc.33",
"@vuepress/plugin-reading-time": "2.0.0-rc.33",
"@vuepress/plugin-seo": "2.0.0-rc.33",
"@vuepress/plugin-sitemap": "2.0.0-rc.33",
"@vuepress/plugin-theme-data": "2.0.0-rc.33",
"@vuepress/plugin-watermark": "2.0.0-rc.33",
"@vueuse/core": "^10.9.0",
"bcrypt-ts": "^5.0.2",
"date-fns": "^3.6.0",
@ -96,7 +97,7 @@
"nanoid": "^5.0.7",
"vue": "^3.4.27",
"vue-router": "^4.3.2",
"vuepress-plugin-md-enhance": "2.0.0-rc.45",
"vuepress-plugin-md-power": "workspace:~"
"vuepress-plugin-md-enhance": "2.0.0-rc.47",
"vuepress-plugin-md-power": "workspace:*"
}
}

View File

@ -1,7 +1,5 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useMediumZoom } from '@vuepress/plugin-medium-zoom/client'
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
import { useData, useSidebar } from '../composables/index.js'
import { usePageEncrypt } from '../composables/encrypt.js'
import PageAside from './PageAside.vue'
@ -25,9 +23,6 @@ const enableAside = computed(() => {
return hasAside.value && isPageDecrypted.value
})
const zoom = useMediumZoom()
onContentUpdated(() => zoom?.refresh())
</script>
<template>

View File

@ -1,16 +1,16 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
import { useActiveAnchor, useData } from '../composables/index.js'
import { type MenuItem, getHeaders, useActiveAnchor, useData } from '../composables/index.js'
import PageAsideItem from './PageAsideItem.vue'
const { page, theme } = useData()
const { theme, frontmatter } = useData()
const headers = ref(page.value.headers)
const headers = ref<MenuItem[]>([])
const hasOutline = computed(() => headers.value.length > 0)
onContentUpdated(() => {
headers.value = page.value.headers
headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline)
})
const container = ref()
@ -84,6 +84,7 @@ function handlePrint() {
width: 2px;
height: 18px;
background-color: var(--vp-c-brand-1);
border-radius: 2px;
opacity: 0;
transition:
top 0.25s cubic-bezier(0, 1, 0.5, 1),
@ -94,9 +95,9 @@ function handlePrint() {
.outline-title {
display: flex;
align-items: center;
font-size: 13px;
font-size: 14px;
font-weight: 600;
line-height: 28px;
line-height: 32px;
letter-spacing: 0.4px;
}

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import type { PageHeader } from 'vuepress/client'
import type { MenuItem } from '../composables/index.js'
defineProps<{
headers: PageHeader[]
headers: MenuItem[]
root?: boolean
}>()
@ -39,7 +39,9 @@ function handleClick({ target: el }: Event) {
.outline-link {
display: block;
overflow: hidden;
line-height: 28px;
font-size: 14px;
font-weight: 400;
line-height: 32px;
color: var(--vp-c-text-2);
text-overflow: ellipsis;
white-space: nowrap;

View File

@ -70,7 +70,7 @@ onMounted(() => {
text-underline-offset: 4px;
}
@media (min-width: 768px) {
@media (min-width: 1440px) {
.plume-footer {
padding: 24px;
}

View File

@ -1,11 +1,7 @@
import { useMediaQuery } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, onUpdated } from 'vue'
import { throttleAndDebounce } from '../utils/index.js'
import { computed } from 'vue'
import { useSidebar } from './sidebar.js'
const PAGE_OFFSET = 71
export function useAside() {
const { hasSidebar } = useSidebar()
const is960 = useMediaQuery('(min-width: 960px)')
@ -22,114 +18,3 @@ export function useAside() {
isAsideEnabled,
}
}
export function useActiveAnchor(
container: Ref<HTMLElement>,
marker: Ref<HTMLElement>,
) {
const { isAsideEnabled } = useAside()
const onScroll = throttleAndDebounce(setActiveLink, 100)
let prevActiveLink: HTMLAnchorElement | null = null
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
function setActiveLink() {
if (!isAsideEnabled.value)
return
const links = [].slice.call(
container.value.querySelectorAll('.outline-link'),
) as HTMLAnchorElement[]
const anchors = [].slice
.call(document.querySelectorAll('.content .header-anchor'))
.filter((anchor: HTMLAnchorElement) => {
return links.some((link) => {
return link.hash === anchor.hash && anchor.offsetParent !== null
})
}) as HTMLAnchorElement[]
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// page bottom - highlight last one
if (anchors.length && isBottom) {
activateLink(anchors[anchors.length - 1].hash)
return
}
for (let i = 0; i < anchors.length; i++) {
const anchor = anchors[i]
const nextAnchor = anchors[i + 1]
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor)
if (isActive) {
activateLink(hash)
return
}
}
}
function activateLink(hash: string | null) {
if (prevActiveLink)
prevActiveLink.classList.remove('active')
if (hash !== null) {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`,
)
}
const activeLink = prevActiveLink
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = `${activeLink.offsetTop + 33}px`
marker.value.style.opacity = '1'
}
else {
marker.value.style.top = '33px'
marker.value.style.opacity = '0'
}
}
}
function getAnchorTop(anchor: HTMLAnchorElement): number {
return anchor.parentElement!.offsetTop - PAGE_OFFSET
}
function isAnchorActive(
index: number,
anchor: HTMLAnchorElement,
nextAnchor: HTMLAnchorElement | undefined,
): [boolean, string | null] {
const scrollTop = window.scrollY
if (index === 0 && scrollTop === 0)
return [true, null]
if (scrollTop < getAnchorTop(anchor))
return [false, null]
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor))
return [true, anchor.hash]
return [false, null]
}

View File

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

View File

@ -0,0 +1,243 @@
import { onMounted, onUnmounted, onUpdated } from 'vue'
import type { Ref } from 'vue'
import type { PlumeThemeLocaleData } from '../../shared/index.js'
import { throttleAndDebounce } from '../utils/index.js'
import { useAside } from './aside.js'
export interface Header {
/**
* The level of the header
*
* `1` to `6` for `<h1>` to `<h6>`
*/
level: number
/**
* The title of the header
*/
title: string
/**
* The slug of the header
*
* Typically the `id` attr of the header anchor
*/
slug: string
/**
* Link of the header
*
* Typically using `#${slug}` as the anchor hash
*/
link: string
/**
* The children of the header
*/
children: Header[]
}
// cached list of anchor elements from resolveHeaders
const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
element: HTMLHeadElement
children?: MenuItem[]
}
export function getHeaders(range: PlumeThemeLocaleData['outline']): MenuItem[] {
const headers = Array.from(
document.querySelectorAll('.plume-content :where(h1,h2,h3,h4,h5,h6)'),
)
.filter(el => el.id && el.hasChildNodes())
.map((el) => {
const level = Number(el.tagName[1])
return {
element: el as HTMLHeadElement,
title: serializeHeader(el),
link: `#${el.id}`,
level,
}
})
return resolveHeaders(headers, range)
}
function serializeHeader(h: Element): string {
// <hx><a href="#"><span>title</span></a></hx>
const anchor = h.firstChild
const el = anchor?.firstChild
let ret = ''
for (const node of Array.from(el?.childNodes ?? [])) {
if (node.nodeType === 1) {
if (
(node as Element).classList.contains('badge-view')
|| (node as Element).classList.contains('ignore-header')
)
continue
ret += node.textContent
}
else if (node.nodeType === 3) {
ret += node.textContent
}
}
// maybe `<hx><a href="#"></a><a href="xxx"></a</hx>` or more
let next = anchor?.nextSibling
while (next) {
if (next.nodeType === 1 || next.nodeType === 3)
ret += next.textContent
next = next.nextSibling
}
return ret.trim()
}
export function resolveHeaders(headers: MenuItem[], range?: PlumeThemeLocaleData['outline']): MenuItem[] {
if (range === false)
return []
const levelsRange = range || 2
const [high, low]: [number, number]
= typeof levelsRange === 'number'
? [levelsRange, levelsRange]
: levelsRange === 'deep'
? [2, 6]
: levelsRange
headers = headers.filter(h => h.level >= high && h.level <= low)
// clear previous caches
resolvedHeaders.length = 0
// update global header list for active link rendering
for (const { element, link } of headers)
resolvedHeaders.push({ element, link })
const ret: MenuItem[] = []
// eslint-disable-next-line no-labels, no-restricted-syntax
outer: for (let i = 0; i < headers.length; i++) {
const cur = headers[i]
if (i === 0) {
ret.push(cur)
}
else {
for (let j = i - 1; j >= 0; j--) {
const prev = headers[j]
if (prev.level < cur.level) {
;(prev.children || (prev.children = [])).push(cur)
// eslint-disable-next-line no-labels
continue outer
}
}
ret.push(cur)
}
}
return ret
}
export function useActiveAnchor(container: Ref<HTMLElement>, marker: Ref<HTMLElement>): void {
const { isAsideEnabled } = useAside()
let prevActiveLink: HTMLAnchorElement | null = null
const setActiveLink = (): void => {
if (!isAsideEnabled.value)
return
const scrollY = window.scrollY
const innerHeight = window.innerHeight
const offsetHeight = document.body.offsetHeight
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1
// resolvedHeaders may be repositioned, hidden or fix positioned
const headers = resolvedHeaders
.map(({ element, link }) => ({
link,
top: getAbsoluteTop(element),
}))
.filter(({ top }) => !Number.isNaN(top))
.sort((a, b) => a.top - b.top)
// no headers available for active link
if (!headers.length) {
activateLink(null)
return
}
// page top
if (scrollY < 1) {
activateLink(null)
return
}
// page bottom - highlight last link
if (isBottom) {
activateLink(headers[headers.length - 1].link)
return
}
// find the last header above the top of viewport
let activeLink: string | null = null
for (const { link, top } of headers) {
if (top > scrollY + 144)
break
activeLink = link
}
activateLink(activeLink)
}
function activateLink(hash: string | null): void {
if (prevActiveLink)
prevActiveLink.classList.remove('active')
if (hash == null) {
prevActiveLink = null
}
else {
prevActiveLink = container.value.querySelector(
`a[href="${decodeURIComponent(hash)}"]`,
)
}
const activeLink = prevActiveLink
if (activeLink) {
activeLink.classList.add('active')
marker.value.style.top = `${activeLink.offsetTop + 39}px`
marker.value.style.opacity = '1'
}
else {
marker.value.style.top = '33px'
marker.value.style.opacity = '0'
}
}
const onScroll = throttleAndDebounce(setActiveLink, 100)
onMounted(() => {
requestAnimationFrame(setActiveLink)
window.addEventListener('scroll', onScroll)
})
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash)
})
onUnmounted(() => {
window.removeEventListener('scroll', onScroll)
})
}
function getAbsoluteTop(element: HTMLElement): number {
let offsetTop = 0
while (element !== document.body) {
if (element === null) {
// child element is:
// - not attached to the DOM (display: none)
// - set to fixed position (not scrollable)
// - body or html element (null offsetParent)
return Number.NaN
}
offsetTop += element.offsetTop
element = element.offsetParent as HTMLElement
}
return offsetTop
}

View File

@ -1,5 +1,4 @@
@import "vars";
@import "fonts";
@import "normalize";
@import "icons";
@import "social-icons";

View File

@ -672,3 +672,11 @@ html.dark {
--vp-c-plot-dark: var(--vp-c-bg);
--vp-c-bg-plot-dark: var(--vp-c-text-2);
}
/**
* plugin-photo-swipe
* -------------------------------------------------------------------------- */
:root {
--photo-swipe-bullet: var(--vp-c-bg);
--photo-swipe-bullet-active: var(--vp-c-brand-1);
}

View File

@ -11,6 +11,7 @@ const FALLBACK_OPTIONS: PlumeThemeLocaleData = {
article: '/article/',
notes: { link: '/', dir: '/notes/', notes: [] },
navbarSocialInclude: ['github', 'twitter', 'discord', 'facebook'],
outline: [2, 3],
// page meta
editLink: true,

View File

@ -2,7 +2,7 @@ import type { App, PluginConfig } from 'vuepress/core'
import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links'
import { docsearchPlugin } from '@vuepress/plugin-docsearch'
import { gitPlugin } from '@vuepress/plugin-git'
import { mediumZoomPlugin } from '@vuepress/plugin-medium-zoom'
import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe'
import { nprogressPlugin } from '@vuepress/plugin-nprogress'
import { themeDataPlugin } from '@vuepress/plugin-theme-data'
import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter'
@ -20,6 +20,7 @@ import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
import { searchPlugin } from '@vuepress-plume/plugin-search'
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
import { watermarkPlugin } from '@vuepress/plugin-watermark'
import { fontsPlugin } from '@vuepress-plume/plugin-fonts'
import type {
PlumeThemeEncrypt,
PlumeThemeLocaleOptions,
@ -64,6 +65,8 @@ export function getPlugins({
iconifyPlugin(),
fontsPlugin(),
contentUpdatePlugin(),
activeHeaderLinksPlugin({
@ -100,10 +103,9 @@ export function getPlugins({
}))
}
if (pluginOptions.mediumZoom !== false) {
plugins.push(mediumZoomPlugin({
if (pluginOptions.photoSwipe !== false) {
plugins.push(photoSwipePlugin({
selector: '.plume-content > img, .plume-content :not(a) > img',
zoomOptions: { background: 'var(--vp-c-bg)' },
delay: 300,
}))
}

View File

@ -119,6 +119,7 @@ export interface PlumeThemePageFrontmatter {
contributors?: boolean
prev?: string | NavItemWithLink
next?: string | NavItemWithLink
outline?: false | number | [number, number] | 'deep'
backToTop?: boolean
externalLink?: boolean
readingTime?: boolean

View File

@ -74,6 +74,8 @@ export interface PlumeThemeLocaleData extends LocaleData {
*/
notes?: false | NotesDataOptions
outline?: false | number | [number, number] | 'deep'
/**
* language text
*/

View File

@ -45,7 +45,7 @@ export interface PlumeThemePluginOptions {
nprogress?: false
mediumZoom?: false
photoSwipe?: false
markdownEnhance?: false | MarkdownEnhancePluginOptions

View File

@ -17,6 +17,7 @@
"@internal/*": ["./docs/.vuepress/.temp/internal/*"],
"@vuepress-plume/*": ["./plugins/*/src/node/index.ts"],
"vuepress-theme-plume": ["./theme/src/node/index.ts"],
"vuepress-theme-plume/composables": ["./theme/src/client/composables/index.ts"],
"@vuepress-plume/*/client": ["./plugins/*/src/client/index.ts"],
"vuepress-plugin-netlify-functions": [
"./plugins/plugin-netlify-functions/src/node/index.ts"