mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
commit
ecc757f40a
@ -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+
|
||||
|
||||
克隆代码仓库,并安装依赖:
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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 }`
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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` 表示要高亮的行号。
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
1
plugins/plugin-fonts/README.md
Normal file
1
plugins/plugin-fonts/README.md
Normal file
@ -0,0 +1 @@
|
||||
# @vuepress-plume/plugin-fonts
|
||||
47
plugins/plugin-fonts/package.json
Normal file
47
plugins/plugin-fonts/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
3
plugins/plugin-fonts/src/client/config.ts
Normal file
3
plugins/plugin-fonts/src/client/config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import './styles/fonts.css'
|
||||
|
||||
export default {}
|
||||
@ -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` */
|
||||
1
plugins/plugin-fonts/src/node/index.ts
Normal file
1
plugins/plugin-fonts/src/node/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './plugin.js'
|
||||
12
plugins/plugin-fonts/src/node/plugin.ts
Normal file
12
plugins/plugin-fonts/src/node/plugin.ts
Normal 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',
|
||||
),
|
||||
}
|
||||
}
|
||||
8
plugins/plugin-fonts/tsconfig.build.json
Normal file
8
plugins/plugin-fonts/tsconfig.build.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -190,7 +190,7 @@ function initSidebarByConfig(
|
||||
text: current?.title || text,
|
||||
link: current?.link,
|
||||
icon: current?.frontmatter.icon,
|
||||
items: [],
|
||||
// items: [],
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
761
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -70,7 +70,7 @@ onMounted(() => {
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 1440px) {
|
||||
.plume-footer {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -10,3 +10,4 @@ export * from './locale.js'
|
||||
export * from './useRouteQuery.js'
|
||||
export * from './watermark.js'
|
||||
export * from './data.js'
|
||||
export * from './outline.js'
|
||||
|
||||
243
theme/src/client/composables/outline.ts
Normal file
243
theme/src/client/composables/outline.ts
Normal 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
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
@import "vars";
|
||||
@import "fonts";
|
||||
@import "normalize";
|
||||
@import "icons";
|
||||
@import "social-icons";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -74,6 +74,8 @@ export interface PlumeThemeLocaleData extends LocaleData {
|
||||
*/
|
||||
notes?: false | NotesDataOptions
|
||||
|
||||
outline?: false | number | [number, number] | 'deep'
|
||||
|
||||
/**
|
||||
* language text
|
||||
*/
|
||||
|
||||
@ -45,7 +45,7 @@ export interface PlumeThemePluginOptions {
|
||||
|
||||
nprogress?: false
|
||||
|
||||
mediumZoom?: false
|
||||
photoSwipe?: false
|
||||
|
||||
markdownEnhance?: false | MarkdownEnhancePluginOptions
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user