Merge pull request #116 from pengzhanbo/RC-76

RC-76
This commit is contained in:
pengzhanbo 2024-07-11 05:34:52 +08:00 committed by GitHub
commit 0a32536fed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 304 additions and 2328 deletions

View File

@ -11,12 +11,7 @@
`plugins` 目录中:
- `plugin-auto-frontmatter` 为 md 文件自动添加 frontmatter。
- `plugin-blog-data` 生成 blog 文章列表数据
- `plugin-notes-data` 生成 notes 数据,管理不同 note 的 `sidebar` 的数据
- ~~`plugin-caniuse`: 添加 `caniuse` 内容容器,已弃用,不再维护~~
- `plugin-content-update`: 重写 `Content` 组件,提供 `onContentUpdated` 钩子
- ~~`plugin-copy-code`: 为 代码块添加 复制 按钮,并适配 `shikiji`,已弃用,不再维护~~
- `plugin-search`: 为主题提供 全文模糊搜索 功能
- `plugin-shikiji`: 代码高亮插件,支持 highlight、diff、focus、error level
- `plugin-iconify`: 添加全局组件 `Iconify`

View File

@ -108,7 +108,7 @@ export const zhNotes = definePlumeNotesConfig({
{
text: '插件',
items: [
'caniuse',
// 'caniuse',
'iconify',
'shiki',
'md-power',

View File

@ -5,6 +5,10 @@ createTime: 2024/03/06 12:22:57
permalink: /config/plugins/baidu-tongji/
---
::: caution
主题计划在 未来的版本中 从内置插件中移除此插件。
:::
## 概述
为站点添加 百度统计。该插件默认不启用。

View File

@ -7,15 +7,31 @@ permalink: /config/basic/
## 基础配置
### configFile
- 类型: `string`
- 默认值: `''`
- 详情:
自定义主题配置文件的路径。
查看 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 了解更多。
### plugins
- 类型:`PlumeThemePluginOptions`
- 默认值: `{}`
- 详情: 对主题内部使用的插件进行自定义配置。
- 详情:
主题使用的插件默认已进行了配置,大多数情况下您不需要进行任何修改,如果需要使用到细致的定制化,请查阅
对主题内部使用的插件进行自定义配置。
主题使用的插件默认已进行了配置,大多数情况下您不需要进行修改,如果需要使用到细致的定制化,请查阅
[此文档](/config/plugins/)
::: warning
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
:::
### hostname
- 类型: `string`
@ -26,6 +42,10 @@ permalink: /config/basic/
`hostname` 配置为有效域名时,主题将会生成 `sitemap``seo` 相关的内容。
::: warning
该字段不支持在 [主题配置文件 `plume.config.js`](./配置说明.md#主题配置文件) 中进行配置。
:::
### blog
- 类型: `false | BlogOptions`
@ -44,18 +64,16 @@ interface BlogOptions {
link?: string
/**
* 在 `blog.dir` 目录中,通过 glob string 配置包含文件
* 在 `{sourceDir}` 目录中,通过 glob string 配置包含文件
*
* @default - ['**\*.md']
*/
include?: string[]
/**
* 在 `blog.dir` 目录中,通过 glob string 配置排除的文件
* 在 `{sourceDir}` 目录中,通过 glob string 配置排除的文件
*
* README.md 文件一般作为主页或者某个目录下的主页,不应该被读取为 blog文章
*
* @default - ['.vuepress/', 'node_modules/', '{README,index}.md']
* @default - ['.vuepress/', 'node_modules/']
*/
exclude?: string[]
@ -89,6 +107,54 @@ interface BlogOptions {
- 默认值: `/article/`
- 详情: 文章链接前缀
### autoFrontmatter
- 类型: `false | AutoFrontmatter`
- 详情:
是否为 markdown 文件自动添加 frontmatter 配置
```ts
interface AutoFrontmatter {
/**
* glob 匹配,被匹配的文件将会自动生成 frontmatter
*
* @default ['**\/*.md']
*/
include?: string | string[]
/**
* glob 匹配,被匹配的文件将不会自动生成 frontmatter
*/
exclude?: string | string[]
/**
* 是否自动生成 permalink
*
* @default true
*/
permalink?: boolean
/**
* 是否自动生成 createTime
*
* 默认读取 文件创建时间,`createTitme` 比 vuepress 默认的 `date` 时间更精准到秒
*/
createTime?: boolean
/**
* 是否自动生成 author
*
* 默认读取 `profile.name``package.json``author`
*/
author?: boolean
/**
* 是否自动生成 title
*
* 默认读取文件名作为标题
*/
title?: boolean
}
```
### locales
- 类型: `Record<string, PlumeThemeLocaleConfig>`
@ -235,13 +301,22 @@ export default {
- 默认值: `[]`
- 详情: 导航栏配置。
为了配置导航栏元素,你可以将其设置为 导航栏数组 ,其中的每个元素是 `NavItem` 对象、
为了配置导航栏元素,你可以将其设置为 导航栏数组 ,其中的每个元素是 `string` 或 `NavItem` 对象、
- `NavItem` 对象应该有一个 text 字段和一个 link 字段,还有一个可选的 `activeMatch` 字段。
- `string` 表示是一个页面文件路径,或者是一个页面的访问路径。
``` ts
type NavItem = string | {
text: string
link: string
/**
* 当前分组的页面前缀
*/
prefix?: string
/**
* 该分组下的导航项
*/
items?: NavItem[]
/**
* 支持 iconify 图标,直接使用 iconify name 即可自动加载
@ -267,10 +342,11 @@ type NavItem = string | {
// NavbarGroup
{
text: 'Group',
item: ['/group/foo/', '/group/bar/'],
prefix: '/group/',
item: ['foo/', 'bar/'],
},
// 字符串 - 页面文件路径
'/bar/',
'/bar', // 可以直接省略后缀 `.md`
],
}),
}

View File

@ -61,11 +61,18 @@ export default defineUserConfig({
- 简体中文 (zh-CN)
- 英文(美国) (en-US)
::: tip
如果您希望支持更多语言,欢迎通过
[PR](https://github.com/pengzhanbo/vuepress-theme-plume/pulls?q=sort%3Aupdated-desc+is%3Apr+is%3Aopen) 在 主题仓库的 `/theme/src/node/locales` 目录中按照相同的方式添加语言。
:::
## 为每个语言设置主题选项
与站点配置和 `@vuepress/theme-default` 的主题配置相同,`vuepress-theme-plume` 也支持你在主题选项中设置 locale 选项,并为每种语言设置不同的配置。
```ts
```ts :no-line-numbers
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
@ -95,3 +102,49 @@ export default defineUserConfig({
}),
})
```
**使用主题配置文件:**
::: code-tabs
@tab .vuepress/config.ts
```ts
import { defineUserConfig } from 'vuepress'
import { plumeTheme } from 'vuepress-theme-plume'
export default defineUserConfig({
locales: {
'/': {
lang: 'en-US',
},
'/zh/': {
lang: 'zh-CN',
},
},
theme: plumeTheme(),
})
```
@tab .vuepress/plume.config.ts
```ts
import { defineThemeConfig } from 'vuepress-theme-plume'
export default defineThemeConfig({
// 通用配置
// ...
locales: {
'/': {
// 英文配置
// ...
},
'/zh/': {
// 中文配置
// ...
},
},
})
```
:::

View File

@ -5,7 +5,9 @@ createTime: 2024/03/02 10:48:14
permalink: /config/intro/
---
## 概述
## VuePress 配置文件
### 概述
VuePress 站点的基本配置文件是 `.vuepress/config.js` ,但也同样支持 TypeScript 配置文件。
你可以使用 `.vuepress/config.ts` 来得到更好的类型提示。
@ -33,7 +35,10 @@ import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
bundler: viteBundler(),
theme: plumeTheme(),
theme: plumeTheme({
// 在这里配置主题
}),
lang: 'zh-CN',
title: '你好, VuePress ',
@ -41,10 +46,94 @@ export default defineUserConfig({
})
```
## 类型
### 类型
在 VuePress 中,有三种配置类型:
- 站点配置: 这是你在 配置文件 中直接导出的对象
- 主题配置: 传递给 `plumeTheme` 的对象参数
- 页面配置: 由在页面顶部基于 YAML 语法的 Frontmatter 提供
## 主题配置文件
### 概述
一般我们使用 `.vuepress/config.js` 或者 `.vuepress/config.ts` 来配置主题。
```ts
import { plumeTheme } from 'vuepress-theme-plume'
import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
theme: plumeTheme({
// 在这里配置主题
}),
})
```
但是当我们已经启动了 VuePress 服务,对该文件的修改会导致 VuePres 服务重启,然后站点进行全量刷新,
这可能需要等待一段时间才能恢复, 如果你的站点内容不多还能够接受,
而对于一些较大的站点,可能需要等待漫长的时间。
特别是当我们频繁修改,或者修改的间隔较短时,很容易使 VuePress 服务 崩溃,我们不得不手动重启。
**这给我们在编写站点内容时带来的极大的不便。**
为了解决这一问题,主题支持在 单独的 主题配置文件中进行配置。
**对该文件的修改将通过热更新的方式实时生效。**
### 配置
你可以直接在 [VuePress 配置文件](#vuepress-配置文件) 相同的路径下创建一个 `plume.config.js` 文件,这样就可以在该文件中进行主题配置。
你也可以使用 TypeScript 来创建一个 `plume.config.ts` 文件,以获得更好的类型提示。
```txt :no-line-numbers
{sourceDir}/.vuepress/
├── config.ts
└── plume.config.ts // [!code ++]
```
::: code-tabs
@tab plume.config.ts
```ts
import { defineThemeConfig } from 'vuepress-theme-plume'
import navbar from './navbar'
export default defineThemeConfig({
// 在这里配置主题
profile: {
name: 'Your name',
},
navbar,
})
```
:::
主题提供了 `defineThemeConfig(config)` 函数,为主题使用者提供主题配置的类型帮助。
你可以直接在这个文件中配置除了 `plugins` 字段外的其他配置。
### 自定义配置文件路径
如果你不希望按照 VuePress 默认的配置文件路径管理你的主题配置文件,
你也可以在 VuePress 配置文件中指定自己的主题配置文件路径。
```ts
import path from 'node:path'
import { plumeTheme } from 'vuepress-theme-plume'
import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
theme: plumeTheme({
// 在这里定义自己的主题配置文件路径
configFile: path.join(__dirname, 'custom/config.ts'), // [!code ++]
}),
})
```
::: tip
更推荐 使用 主题配置文件 来单独管理 主题配置,你不必再为频繁修改配置而一直等待
VuePress 重启。
:::

View File

@ -11,19 +11,22 @@ tags:
## 介绍
vuepress-theme-plume 是一个基于 VuePress 的主题。适用于 博客、文档 和 知识笔记 。
**vuepress-theme-plume** 是一个基于 VuePress 的主题,适用于 博客、文档 和 知识笔记 。
VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_site_generator) (SSG) 。
专为构建快速、以内容为中心的站点而设计。
简而言之VuePress 获取用 Markdown 编写的内容,对其应用主题,并生成可以轻松部署到任何地方的静态 HTML 页面。
::: tip
本主题 基于 [vuepress-next](https://github.com/vuepress/vuepress-next), 目前处于 RC 阶段。
当前主题 功能和 API 趋于稳定,但在未来的更新中仍有小概率出现破坏更改。
如果您在使用过程中遇到问题,或者有新的想法,请在 [Issues](https://github.com/pengzhanbo/vuepress-theme-plume/issues) 里提出,
如果您在使用过程中遇到问题,或者有新的想法,
请在 [Issues](https://github.com/pengzhanbo/vuepress-theme-plume/issues) 里提出,
也欢迎 通过 [PR](https://github.com/pengzhanbo/vuepress-theme-plume/pulls) 帮助完善主题。
:::
## 优势
@ -33,6 +36,8 @@ VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_
- 大幅度优化了界面、交互,更具美观度,更好的用户体验。
- 同时,还添加了大量的丰富实用的功能,如 代码分组、提示容器、任务列表、数学公式、代码演示、
内容搜索、文章评论、加密 等。
- 新增编译缓存,加快启动速度。
- 支持使用单独的主题配置文件,避免修改配置导致频繁重启 VuePress 服务。
- 大幅度简化了配置,更易于使用,同时还保留了丰富灵活的配置项,满足个性化的需求。
`plume` 主题尽可能的内置你可能需要的功能,以及搭建站点所需要的一般性配置,您无需关注这些细节。

View File

@ -89,10 +89,27 @@ tags:
| sticky | `boolean \| number` | false | 是否置顶, 如果为数字,则数字越大,置顶越靠前 |
| draft | `boolean` | false | 是否为草稿,草稿文章不会被展示 |
除了以上的字段,你还可以使用 [通用 frontmatter 配置](../config/frontmatter/basic.md) 中的字段,
灵活的控制当前页面的行为。
## 文章摘要
如果你想要为文章添加摘要,你可以使用 `<!-- more -->` 注释来标记它。任何在此注释之前的内容会被视为摘要。
***示例:**
```md
---
title: 标题
---
这里的内容会被作为摘要
<!-- more -->
这里的内容不会被作为摘要
```
## 标签页和归档页
主题除了自动生成 **博客文章列表页** 以外,还会自动生成 **标签页****归档页**

View File

@ -11,10 +11,10 @@ tags:
## 概述
在本主题满足了 Blog的基本功能后期望能够 以 note 或者 book 的形式聚合文章,形式上类似于 vuepress 默认主题。
在本主题满足了 Blog 的基本功能后,期望能够 以 note 或者 book 的形式聚合文章,形式上类似于 vuepress 默认主题。
同时也减少配置的复杂度。
它能够让你以更加友好的方式,组织管理你的文档,或者 知识笔记
它能够让你以更加友好的方式,组织管理你的文档。
## 目录
@ -72,8 +72,10 @@ export default defineUserConfig({
主题会根据配置,为对应目录中的 md 文件,生成 永久链接,以及侧边栏。
::: tip
你应该在创建文件之前,先把笔记的目录和链接前缀等配置好,主题需要根据配置,
为目录中的 md 文件生成永久链接,以及侧边栏。
:::
完整配置查看 [notes配置](/config/notes/)

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,126 +0,0 @@
# `@vuepress-plume/plugin-auto-frontmatter`
自动生成 `*.md` 文件的 `frontmatter` 配置。
## Install
```sh
npm install @vuepress-plume/plugin-auto-frontmatter
# or
pnpm add @vuepress-plume/plugin-auto-frontmatter
# or
yarn add @vuepress-plume/plugin-auto-frontmatter
```
## Usage
``` js
// .vuepress/config.[jt]s
import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter'
export default {
// ...
plugins: [
autoFrontmatterPlugin({
formatter: {
createTime(formatTime, file, matter) {
if (formatTime)
return formatTime
return file.createTime
}
}
})
]
// ...
}
```
## `autoFrontmatterPlugin([options])`
### options
`{ include?: string | string[]; exclude?: string | string[]; formatter: Formatter }`
- `include`
include 匹配字符串或数组,匹配需要自动生成 `frontmatter` 的 md文件。
默认预设为 `['**/*.md']`
- `exclude`
exclude 排除不需要的文件
默认预设为: `['!.vuepress/', '!node_modules/']`
- `formatter`
配置`frontmatter`每个字段的生成规则。
```ts
interface MarkdownFile {
filepath: string
relativePath: string
content: string
createTime: Date
stats: fs.Stats
}
interface FormatterFn<T = any, K = object> {
(value: T, file: MarkdownFile, data: K): T
}
type FormatterObject<K = object, T = any> = Record<
string,
FormatterFn<T, K>
>
type FormatterArray = {
include: string | string[]
formatter: FormatterObject
}[]
type Formatter = FormatterObject | FormatterArray
/**
* formatterObj 对象中的 key 即为 frontmatter 配置中的key
* 其方法返回的值将作为 frontmatter[key] 的值
* .md
* ---
* createTime: 2022-03-26T11:46:50.000Z
* ---
*/
const formatterObj: Formatter = {
createTime(formatTime, file, matter) {
if (formatTime)
return formatTime
return file.createTime
}
}
const formatterArr: Formatter = [
{
// 更精细化的匹配某个 md文件支持glob 匹配字符串
include: '**/{README,index}.md',
// formatter 仅对 glob命中的文件有效
formatter: {
home(value, file, matter) {
return value
}
},
},
{
// 通配如果文件没有被其他精细glob命中
// 则使用 通配 formatter
// 如果是数组,必须有且用一个 include 为 * 的 项
include: '*',
formatter: {
title(title) {
return title || '默认标题'
}
}
}
]
```
## Why ?
- **为什么需要这个插件?**
有时候在开发一些主题时,期望使用户更专注于内容的编写,尽可能减少配置性的工作,可以将一些重复性的必要的配置
直接通过本插件自动生成。

View File

@ -1,56 +0,0 @@
{
"name": "@vuepress-plume/plugin-auto-frontmatter",
"type": "module",
"version": "1.0.0-rc.75",
"private": true,
"description": "The Plugin for VuePress 2 - auto frontmatter",
"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-auto-frontmatter"
},
"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}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@pengzhanbo/utils": "^1.1.2",
"chokidar": "^3.6.0",
"create-filter": "^1.1.0",
"fast-glob": "^3.3.2",
"gray-matter": "^4.0.3",
"json2yaml": "^1.1.0"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"autoFrontmatter",
"vuepress-plugin-plugin-auto-frontmatter"
]
}

View File

@ -1,5 +0,0 @@
declare module 'json2yaml' {
const result: any
export default result
}

View File

@ -1,12 +0,0 @@
import type {
AutoFrontmatterOptions,
FrontmatterArray,
FrontmatterObject,
} from '../shared/index.js'
import { autoFrontmatterPlugin } from './plugin.js'
export * from './plugin.js'
export type { AutoFrontmatterOptions, FrontmatterArray, FrontmatterObject }
export default autoFrontmatterPlugin

View File

@ -1,103 +0,0 @@
import { colors, fs, logger } from 'vuepress/utils'
import type { Plugin } from 'vuepress/core'
import chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import grayMatter from 'gray-matter'
import jsonToYaml from 'json2yaml'
import { promiseParallel } from '@pengzhanbo/utils'
import type {
AutoFrontmatterOptions,
FrontmatterArray,
FrontmatterObject,
MarkdownFile,
} from '../shared/index.js'
import { readMarkdown, readMarkdownList } from './readFiles.js'
import { ensureArray, isEmptyObject } from './utils.js'
const PLUGIN_NAME = '@vuepress-plume/plugin-auto-frontmatter'
export function autoFrontmatterPlugin({
include = ['**/*.md'],
exclude = ['.vuepress/**/*', 'node_modules'],
frontmatter = {},
}: AutoFrontmatterOptions = {}): Plugin {
include = ensureArray(include)
exclude = ensureArray(exclude)
const globFilter = createFilter(include, exclude, { resolve: false })
const matterFrontmatter: FrontmatterArray = Array.isArray(frontmatter)
? frontmatter
: [{ include: '*', frontmatter }]
const globFormatter: FrontmatterObject
= matterFrontmatter.find(({ include }) => include === '*')?.frontmatter || {}
const otherFormatters = matterFrontmatter
.filter(({ include }) => include !== '*')
.map(({ include, frontmatter }) => {
return {
include,
filter: createFilter(ensureArray(include), [], { resolve: false }),
frontmatter,
}
})
async function formatMarkdown(file: MarkdownFile): Promise<void> {
const { filepath, relativePath } = file
const current = otherFormatters.find(({ filter }) => filter(relativePath))
const formatter = current?.frontmatter || globFormatter
const { data, content } = grayMatter(file.content)
for (const key in formatter) {
const value = await formatter[key](data[key], file, data)
data[key] = value ?? data[key]
}
try {
const yaml = isEmptyObject(data)
? ''
: jsonToYaml
.stringify(data)
.replace(/\n\s{2}/g, '\n')
.replace(/"/g, '')
.replace(/\s+\n/g, '\n')
const newContent = yaml ? `${yaml}---\n${content}` : content
fs.writeFileSync(filepath, newContent, 'utf-8')
}
catch (e) {
console.error(e)
}
}
return {
name: PLUGIN_NAME,
onInitialized: async (app) => {
const start = performance.now()
const markdownList = await readMarkdownList(app.dir.source(), globFilter)
await promiseParallel(
markdownList.map(file => () => formatMarkdown(file)),
64,
)
if (app.env.isDebug)
logger.info(`\n[${colors.green(PLUGIN_NAME)}] Init time spent: ${(performance.now() - start).toFixed(2)}ms`)
},
onWatched: async (app, watchers) => {
const watcher = chokidar.watch('**/*.md', {
cwd: app.dir.source(),
ignoreInitial: true,
ignored: /(node_modules|\.vuepress)\//,
})
watcher.on('add', async (relativePath) => {
if (!globFilter(relativePath))
return
await formatMarkdown(readMarkdown(app.dir.source(), relativePath))
})
watchers.push(watcher)
},
}
}

View File

@ -1,32 +0,0 @@
import { fs, path } from 'vuepress/utils'
import fg from 'fast-glob'
import type { MarkdownFile } from '../shared/index.js'
type MarkdownFileList = MarkdownFile[]
export async function readMarkdownList(sourceDir: string, filter: (id: string) => boolean): Promise<MarkdownFileList> {
const files: string[] = await fg(['**/*.md'], {
cwd: sourceDir,
ignore: ['node_modules', '.vuepress'],
})
return files
.filter(file => filter(file))
.map(file => readMarkdown(sourceDir, file))
}
export function readMarkdown(sourceDir: string, relativePath: string): MarkdownFile {
const filepath = path.join(sourceDir, relativePath)
const stats = fs.statSync(filepath)
return {
filepath,
relativePath,
content: fs.readFileSync(filepath, 'utf-8'),
createTime: getFileCreateTime(stats),
stats,
}
}
export function getFileCreateTime(stats: fs.Stats): Date {
return stats.birthtime.getFullYear() !== 1970 ? stats.birthtime : stats.atime
}

View File

@ -1,11 +0,0 @@
export function ensureArray<T>(thing: T | T[] | null | undefined): T[] {
if (Array.isArray(thing))
return thing
if (thing === null || thing === undefined)
return []
return [thing]
}
export function isEmptyObject(obj: object) {
return Object.keys(obj).length === 0
}

View File

@ -1,40 +0,0 @@
import type fs from 'node:fs'
export interface MarkdownFile {
filepath: string
relativePath: string
content: string
createTime: Date
stats: fs.Stats
}
export type FrontmatterFn<T = any, K = object> = (
value: T,
file: MarkdownFile,
data: K
) => T | PromiseLike<T>
export type FrontmatterObject<K = object, T = any> = Record<string, FrontmatterFn<T, K>>
export type FrontmatterArray = {
include: string | string[]
frontmatter: FrontmatterObject
}[]
export interface AutoFrontmatterOptions {
/**
* FilterPattern
*/
include?: string | string[]
exclude?: string | string[]
/**
* {
* key(value, file, data) {
* return value
* }
* }
*/
frontmatter?: FrontmatterArray | FrontmatterObject
}

View File

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

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,38 +0,0 @@
# `@vuepress-plume/plugin-blog-data`
## Install
```sh
npm install @vuepress-plume/plugin-blog-data
# or
pnpm add @vuepress-plume/plugin-blog-data
# or
yarn add @vuepress-plume/plugin-blog-data
```
## Usage
``` js
// .vuepress/config.[jt]s
import { blogDataPlugin } from '@vuepress-plume/plugin-blog-data'
export default {
// ...
plugins: [
blogDataPlugin()
]
// ...
}
```
## Options
```ts
interface BlogDataPluginOptions {
include?: string | string[]
exclude?: string | string[]
sortBy?: 'createTime' | false | (<T>(prev: T, next: T) => boolean)
excerpt?: boolean
extendBlogData?: <T = any>(page: T) => Record<string, any>
}
```

View File

@ -1,58 +0,0 @@
{
"name": "@vuepress-plume/plugin-blog-data",
"type": "module",
"version": "1.0.0-rc.75",
"private": "true",
"description": "The Plugin for VuePress 2 - blog data",
"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-blog-data"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./client": {
"types": "./lib/client/index.d.ts",
"import": "./lib/client/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}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@vue/devtools-api": "6.6.3",
"chokidar": "^3.6.0",
"create-filter": "^1.1.0",
"vue": "^3.4.31"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"blogData",
"vuepress-plugin-plugin-blog-data"
]
}

View File

@ -1,7 +0,0 @@
import type { BlogPostData } from '../shared/index.js'
declare module '@internal/blogData' {
const blogPostData: BlogPostData
export { blogPostData }
}

View File

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

View File

@ -1,24 +0,0 @@
import {
blogPostData as blogPostDataRaw,
} from '@internal/blogData'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { BlogPostData } from '../../shared/index.js'
declare const __VUE_HMR_RUNTIME__: Record<string, any>
export type BlogDataRef<T extends BlogPostData = BlogPostData> = Ref<T>
export const blogPostData: BlogDataRef = ref(blogPostDataRaw)
export function useBlogPostData<
T extends BlogPostData = BlogPostData,
>(): BlogDataRef<T> {
return blogPostData as BlogDataRef<T>
}
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateBlogData = (data: BlogPostData) => {
blogPostData.value = data
}
}

View File

@ -1,70 +0,0 @@
import { setupDevtoolsPlugin } from '@vue/devtools-api'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { useBlogPostData } from './composables/index.js'
declare const __VUE_PROD_DEVTOOLS__: boolean
export default defineClientConfig({
enhance({ app }) {
const blogPostData = useBlogPostData()
Object.defineProperties(app.config.globalProperties, {
$blogPostData: {
get() {
return blogPostData.value
},
},
})
// setup devtools in dev mode
if (__VUEPRESS_DEV__ || __VUE_PROD_DEVTOOLS__) {
const PLUGIN_ID = 'org.vuejs.vuepress'
const PLUGIN_LABEL = 'VuePress'
const INSPECTOR_ID = PLUGIN_ID
setupDevtoolsPlugin(
{
// fix recursive reference
app: app as any,
id: PLUGIN_ID,
label: PLUGIN_LABEL,
packageName: '@vuepress-plume/plugin-blog-data',
homepage: 'https://pengzhanbo.cn',
logo: 'https://v2.vuepress.vuejs.org/images/hero.png',
componentStateTypes: ['VuePress'],
},
(api) => {
api.on.inspectComponent((payload) => {
payload.instanceData.state.push({
type: 'VuePress',
key: 'blogPostData',
editable: false,
value: blogPostData.value,
})
})
api.on.getInspectorTree((payload) => {
if (payload.inspectorId !== INSPECTOR_ID)
return
payload.rootNodes.push({
id: 'blog_post_data',
label: 'Blog Post Data',
})
})
api.on.getInspectorState((payload) => {
if (payload.inspectorId !== INSPECTOR_ID)
return
if (payload.nodeId === 'blog_post_data') {
payload.state = {
BlogPostData: [{
key: 'blogPostData',
value: blogPostData.value,
}],
}
}
})
},
)
}
},
}) as ClientConfig

View File

@ -1,5 +0,0 @@
import type { BlogPostData, BlogPostDataItem } from '../shared/index.js'
export * from './composables/index.js'
export type { BlogPostData, BlogPostDataItem }

View File

@ -1,6 +0,0 @@
import { blogDataPlugin } from './plugin.js'
export * from '../shared/index.js'
export { blogDataPlugin }
export default blogDataPlugin

View File

@ -1,52 +0,0 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import { preparedBlogData } from './prepareBlogData.js'
import type { BlogDataPluginOptions } from './index.js'
const __dirname = getDirname(import.meta.url)
export type PluginOption = Omit<BlogDataPluginOptions, 'include' | 'exclude'>
export function blogDataPlugin({
include,
exclude,
...pluginOptions
}: BlogDataPluginOptions = {}): Plugin {
const pageFilter = createFilter(toArray(include), toArray(exclude), {
resolve: false,
})
return {
name: '@vuepress-plume/plugin-blog-data',
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
extendsPage(page) {
if (page.filePathRelative && pageFilter(page.filePathRelative)) {
;(page.data as any).isBlogPost = true
}
},
onPrepared: async app =>
await preparedBlogData(app, pageFilter, pluginOptions),
onWatched(app, watchers) {
const watcher = chokidar.watch('pages/**/*', {
cwd: app.dir.temp(),
ignoreInitial: true,
})
const handler = () => preparedBlogData(app, pageFilter, pluginOptions)
watcher.on('add', handler)
watcher.on('change', handler)
watcher.on('unlink', handler)
watchers.push(watcher)
},
}
}
function toArray(likeArr: string | string[] | undefined): string[] {
if (Array.isArray(likeArr))
return likeArr
return likeArr ? [likeArr] : []
}

View File

@ -1,99 +0,0 @@
import { createHash } from 'node:crypto'
import type { App, Page } from 'vuepress/core'
import { colors, logger } from 'vuepress/utils'
import type { BlogPostData, BlogPostDataItem } from '../shared/index.js'
import type { PluginOption } from './plugin.js'
const HMR_CODE = `
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
if (__VUE_HMR_RUNTIME__.updateBlogData) {
__VUE_HMR_RUNTIME__.updateBlogData(blogPostData)
}
}
if (import.meta.hot) {
import.meta.hot.accept(({ blogPostData }) => {
__VUE_HMR_RUNTIME__.updateBlogData(blogPostData)
})
}
`
const headingRe = /<h(\d)[^>]*>.*?<\/h\1>/gi
const EXCERPT_SPLIT = '<!-- more -->'
let contentHash: string | undefined
export async function preparedBlogData(app: App, pageFilter: (id: string) => boolean, options: PluginOption): Promise<void> {
const start = performance.now()
let pages = app.pages.filter((page) => {
return page.filePathRelative && pageFilter(page.filePathRelative)
})
if (options.pageFilter)
pages = pages.filter(options.pageFilter)
if (options.sortBy) {
pages = pages.sort((prev, next) => {
if (options.sortBy === 'createTime') {
return getTimestamp(prev.frontmatter.createTime as Date)
< getTimestamp(next.frontmatter.createTime as Date)
? 1
: -1
}
else {
return typeof options.sortBy === 'function'
&& options.sortBy(prev, next)
? 1
: -1
}
})
}
const blogData: BlogPostData = pages.map((page: Page) => {
let extended: Partial<BlogPostDataItem> = {}
if (typeof options.extendBlogData === 'function')
extended = options.extendBlogData(page)
const data = {
path: page.path,
title: page.title,
...extended,
}
if (options.excerpt && page.contentRendered.includes(EXCERPT_SPLIT)) {
const contents = page.contentRendered.split(EXCERPT_SPLIT)
let excerpt = contents[0]
// 删除摘要中的标题
excerpt = excerpt.replace(headingRe, '')
data.excerpt = excerpt
}
return data as BlogPostDataItem
})
let content = `\
export const blogPostData = ${JSON.stringify(blogData)};
`
// inject HMR code
if (app.env.isDev)
content += HMR_CODE
const currentHash = hash(content)
if (!contentHash || contentHash !== currentHash) {
contentHash = currentHash
await app.writeTemp('internal/blogData.js', content)
}
if (app.env.isDebug)
logger.info(`\n[${colors.green('@vuepress-plume/plugin-blog-data')}] prepare blog data time spent: ${(performance.now() - start).toFixed(2)}ms`)
}
function getTimestamp(time: Date): number {
return new Date(time).getTime()
}
function hash(content: string): string {
return createHash('md5').update(content).digest('hex')
}

View File

@ -1,19 +0,0 @@
import type { Page } from 'vuepress/core'
export interface BlogDataPluginOptions {
include?: string | string[]
exclude?: string | string[]
sortBy?: 'createTime' | false | (<T>(prev: T, next: T) => boolean)
excerpt?: boolean
extendBlogData?: <T = any>(page: T) => Record<string, any>
pageFilter?: (page: Page) => boolean
}
export type BlogPostData<T extends object = object> = BlogPostDataItem<T>[]
export type BlogPostDataItem<T extends object = object> = {
path: string
title: string
excerpt: string
[x: string]: any
} & T

View File

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "./src",
"paths": {
"@internal/blogData": ["./src/client/blogPostData.d.ts"]
},
"types": ["vuepress/client-types", "vite/client", "webpack-env"],
"outDir": "./lib"
},
"include": ["./src"]
}

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,71 +0,0 @@
# vuepress-plugin-caniuse
VuePress 2 Plugin
VuePress 2 插件
在Markdown中添加 [can-i-use](https://caniuse.com/) 支持这对于你在写前端技术博客时说明某个feature的兼容性时特别有用。
## Install
``` sh
npm install @vuepress-plume/plugin-caniuse
# or
pnpm add @vuepress-plume/plugin-caniuse
# or
yarn add @vuepress-plume/plugin-caniuse
```
## Usage
### 在VuePress 配置文件中添加插件
``` js
// .vuepress/config.[jt]s
import { caniusePlugin } from '@vuepress-plume/plugin-caniuse'
export default {
// ...
plugins: [
caniusePlugin({ mode: 'image' }),
]
// ...
}
```
### 在markdown中编写
``` md
::: caniuse <feature> {{browser_versions}}
:::
```
### Options
- `options.mode`: can-i-use插入文档的模式 支持 `embed``image`, 默认值是 `image`
- `image`: 插入图片
- `embed`: 使用iframe嵌入 can-i-use
### \<feature>
正确取值请参考 [https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)
### \{browser_versions\}`
可选。当前特性在多个版本中的支持情况。
格式: `{number,number,...}` 取值范围为 `-5 ~ 3`
- 小于`0` 表示低于当前浏览器版本的支持情况
- `0` 表示当前浏览器版本的支持情况
- 大于`0` 表示高于当前浏览器版本的支持情况
## Example
``` md
::: caniuse css-matches-pseudo {-2,-1,1}
:::
```
效果:
![can-i-use css-matches-pseudo](https://caniuse.bitsofco.de/image/css-dir-pseudo.webp)

View File

@ -1,58 +0,0 @@
{
"name": "@vuepress-plume/plugin-caniuse",
"type": "module",
"version": "1.0.0-rc.75",
"private": "true",
"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",
"repository": {
"type": "git",
"url": "git+https://github.com/pengzhanbo/vuepress-theme-plume.git",
"directory": "plugins/plugin-caniuse"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"keywords": [
"VuePress",
"plugin",
"vuepress-plugin",
"can-i-use",
"caniuse"
],
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./client": {
"types": "./lib/client/index.d.ts",
"import": "./lib/client/index.js"
},
"./package.json": "./package.json"
},
"main": "lib/node/index.js",
"types": "./lib/node/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "pnpm run ts",
"clean": "rimraf --glob ./lib ./*.tsbuildinfo",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"markdown-it-container": "^4.0.0"
},
"devDependencies": {
"@types/markdown-it": "^14.1.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -1,21 +0,0 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import type { CanIUseMode } from '../shared/index.js'
import { resolveCanIUse } from './resolveCanIUse.js'
declare const __CAN_I_USE_INJECT_MODE__: CanIUseMode
declare const __VUEPRESS_SSR__: boolean
const mode = __CAN_I_USE_INJECT_MODE__
export default defineClientConfig({
enhance({ router }) {
if (__VUEPRESS_SSR__)
return
router.afterEach(() => {
if (mode === 'embed')
resolveCanIUse()
})
},
}) as ClientConfig

View File

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

View File

@ -1,20 +0,0 @@
let isBind = false
export function resolveCanIUse(): void {
if (isBind)
return
isBind = true
window.addEventListener('message', (message) => {
const data = message.data
if (typeof data === 'string' && data.includes('ciu_embed')) {
const [, feature, height] = data.split(':')
const el = document.querySelector(`.ciu_embed[data-feature="${feature}"]:not([data-skip])`)
if (el) {
const h = Number.parseInt(height) + 30
;(el.childNodes[0] as any).height = `${h}px`
el.setAttribute('data-skip', 'true')
}
}
})
}

View File

@ -1,6 +0,0 @@
import { caniusePlugin } from './plugin.js'
export * from './plugin.js'
export * from '../shared/index.js'
export default caniusePlugin

View File

@ -1,6 +0,0 @@
declare module 'markdown-it-container' {
import type { PluginWithParams } from 'markdown-it'
const container: PluginWithParams
export = container
}

View File

@ -1,48 +0,0 @@
import type { Plugin, PluginObject } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import type Token from 'markdown-it/lib/token.mjs'
import container from 'markdown-it-container'
import type { CanIUseMode, CanIUsePluginOptions } from '../shared/index.js'
import { resolveCanIUse } from './resolveCanIUse.js'
const __dirname = getDirname(import.meta.url)
const modeMap: CanIUseMode[] = ['image', 'embed']
const isMode = (mode: CanIUseMode): boolean => modeMap.includes(mode)
export function caniusePlugin({
mode = modeMap[0],
}: CanIUsePluginOptions): Plugin {
mode = isMode(mode) ? mode : modeMap[0]
const type = 'caniuse'
const validateReg = new RegExp(`^${type}(?:$|\s)`)
const pluginObj: PluginObject = {
name: '@vuepress-plume/plugin-caniuse',
clientConfigFile: path.resolve(__dirname, '../client/clientConfig.js'),
define: {
__CAN_I_USE_INJECT_MODE__: mode,
},
}
const validate = (info: string): boolean => {
return validateReg.test(info.trim())
}
const render = (tokens: Token[], index: number): string => {
const token = tokens[index]
if (token.nesting === 1) {
const info = token.info.trim().slice(type.length).trim() || ''
const feature = info.split(/\s+/)[0]
const versions = info.match(/\{(.*)\}/)?.[1] || ''
return feature ? resolveCanIUse(feature, mode, versions) : ''
}
else {
return ''
}
}
pluginObj.extendsMarkdown = (md) => {
md.use(container as any, type, { validate, render })
}
return pluginObj
}

View File

@ -1,46 +0,0 @@
import type { CanIUseMode } from '../shared/index.js'
export function resolveCanIUse(feature: string, mode: CanIUseMode, versions: string): string {
if (!feature)
return ''
if (mode === 'image') {
return `<picture>
<source type="image/webp" srcset="https://caniuse.bitsofco.de/image/${feature}.webp">
<source type="image/png" srcset="https://caniuse.bitsofco.de/image/${feature}.png">
<img src="https://caniuse.bitsofco.de/image/${feature}.jpg" alt="Data on support for the ${feature} feature across the major browsers from caniuse.com">
</picture>`
}
const periods = resolveVersions(versions)
const accessible = 'false'
const image = 'none'
const url = 'https://caniuse.bitsofco.de/embed/index.html'
const src = `${url}?feat=${feature}&periods=${periods}&accessible-colours=${accessible}&image-base=${image}`
return `<div class="ciu_embed" style="margin:16px 0" data-feature="${feature}"><iframe src="${src}" frameborder="0" width="100%" height="400px"></iframe></div>`
}
function resolveVersions(versions: string): string {
if (!versions)
return 'future_1,current,past_1,past_2'
const list = versions
.split(',')
.map(v => Number(v.trim()))
.filter(v => !Number.isNaN(v) && v >= -5 && v <= 3)
list.push(0)
const uniq = [...new Set(list)].sort((a, b) => b - a)
const result: string[] = []
uniq.forEach((v) => {
if (v < 0)
result.push(`past_${Math.abs(v)}`)
if (v === 0)
result.push('current')
if (v > 0)
result.push(`future_${v}`)
})
return result.join(',')
}

View File

@ -1,15 +0,0 @@
export type CanIUseMode = 'embed' | 'image'
/**
* can-i-use plugin options
*/
export interface CanIUsePluginOptions {
/**
*
*
* embed iframe嵌入
*
* image
*/
mode: CanIUseMode
}

View File

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

View File

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

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,26 +0,0 @@
# `@vuepress-plume/plugin-copy-code`
## Install
```sh
npm install @vuepress-plume/plugin-copy-code
# or
pnpm add @vuepress-plume/plugin-copy-code
# or
yarn add @vuepress-plume/plugin-copy-code
```
## Usage
``` js
// .vuepress/config.js
import { copyCodePlugin } from '@vuepress-plume/plugin-copy-code'
export default {
// ...
plugins: [
copyCodePlugin()
]
// ...
}
```

View File

@ -1,56 +0,0 @@
{
"name": "@vuepress-plume/plugin-copy-code",
"type": "module",
"version": "1.0.0-rc.75",
"private": "true",
"description": "The Plugin for VuePress 2 - copy code",
"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-copy-code"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./client": {
"types": "./lib/client/index.d.ts",
"import": "./lib/client/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}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@vuepress-plume/plugin-content-update": "workspace:~",
"vue": "^3.4.31"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"copyCode",
"vuepress-plugin-plugin-copy-code"
]
}

View File

@ -1,11 +0,0 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { setupCopyCode } from './setupCopyCode.js'
import './styles/button.css'
export default defineClientConfig({
setup() {
setupCopyCode()
},
}) as ClientConfig

View File

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

View File

@ -1,142 +0,0 @@
import { nextTick, onMounted } from 'vue'
import { onContentUpdated } from '@vuepress-plume/plugin-content-update/client'
import type { CopyCodeOptions } from '../shared/index.js'
declare const __COPY_CODE_OPTIONS__: CopyCodeOptions
const options = __COPY_CODE_OPTIONS__
const RE_LANGUAGE = /language-(\w+)/
const RE_START_CODE = /^ *(\$|>)/gm
const shells = ['shellscript', 'shell', 'bash', 'sh', 'zsh']
const ignoredNodes = ['.diff.remove', '.vp-copy-ignore']
function isMobile(): boolean {
return navigator
? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/iu.test(
navigator.userAgent,
)
: false
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
export function setupCopyCode(): void {
const insertBtn = (codeBlockEl: HTMLElement): void => {
if (codeBlockEl.hasAttribute('has-copy-code'))
return
const button = document.createElement('button')
button.className = 'copy-code-button'
const parent = codeBlockEl.parentElement
if (parent) {
parent.insertBefore(button, codeBlockEl)
const classes = parent.className
const match = classes.match(RE_LANGUAGE) || []
if (match[1])
button.setAttribute('data-lang', match[1])
}
codeBlockEl.setAttribute('has-copy-code', '')
}
const generateButton = async () => {
const { selector, delay } = options
await nextTick()
await sleep(delay || 0)
const selectors = Array.isArray(selector) ? selector : [selector!]
selectors.forEach((item) => {
document.querySelectorAll<HTMLElement>(item).forEach(insertBtn)
})
}
onMounted(async () => {
if (!isMobile() || options.showInMobile) {
await generateButton()
const timeoutIdMap: WeakMap<HTMLElement, NodeJS.Timeout> = new WeakMap()
window.addEventListener('click', (e) => {
const el = e.target as HTMLElement
if (el.matches('div[class*="language-"] > button.copy-code-button')) {
const parent = el.parentElement
const sibling = el.nextElementSibling
if (!parent || !sibling)
return
// Clone the node and remove the ignored nodes
const clone = sibling.cloneNode(true) as HTMLElement
clone
.querySelectorAll(ignoredNodes.join(','))
.forEach(node => node.remove())
let text = clone.textContent || ''
const lang = el.getAttribute('data-lang') || ''
if (lang && shells.includes(lang))
text = text.replace(RE_START_CODE, '').trim()
copyToClipboard(text).then(() => {
el.classList.add('copied')
clearTimeout(timeoutIdMap.get(el))
const timeoutId = setTimeout(() => {
el.classList.remove('copied')
el.blur()
timeoutIdMap.delete(el)
}, options.duration)
timeoutIdMap.set(el, timeoutId)
})
}
})
}
})
onContentUpdated(() => {
if (!isMobile() || options.showInMobile)
generateButton()
})
}
async function copyToClipboard(text: string) {
try {
return navigator.clipboard.writeText(text)
}
catch {
const element = document.createElement('textarea')
const previouslyFocusedElement = document.activeElement
element.value = text
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '')
element.style.contain = 'strict'
element.style.position = 'absolute'
element.style.left = '-9999px'
element.style.fontSize = '12pt' // Prevent zooming on iOS
const selection = document.getSelection()
const originalRange = selection
? selection.rangeCount > 0 && selection.getRangeAt(0)
: null
document.body.appendChild(element)
element.select()
// Explicit selection workaround for iOS
element.selectionStart = 0
element.selectionEnd = text.length
document.execCommand('copy')
document.body.removeChild(element)
if (originalRange) {
selection!.removeAllRanges() // originalRange can't be truthy when selection is falsy
selection!.addRange(originalRange)
}
// Get the focus back on the previously focused element, if any
if (previouslyFocusedElement) {
; (previouslyFocusedElement as HTMLElement).focus()
}
}
}

View File

@ -1,100 +0,0 @@
:root {
--vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2'/%3E%3C/svg%3E");
--vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' height='20' width='20' stroke='rgba(128,128,128,1)' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m-6 9 2 2 4-4'/%3E%3C/svg%3E");
}
:root {
--vp-code-copy-code-border-color: #e2e2e3;
--vp-code-copy-code-bg: #f6f6f7;
--vp-code-copy-code-hover-border-color: #e2e2e3;
--vp-code-copy-code-hover-bg: #fff;
--vp-code-copy-code-active-text: rgba(60, 60, 67, 0.78);
--vp-code-copy-copied-text-content: "Copied";
}
html[lang="zh-CN"] {
--vp-code-copy-copied-text-content: "已复制";
}
.dark {
--vp-code-copy-code-border-color: #2e2e32;
--vp-code-copy-code-bg: #202127;
--vp-code-copy-code-hover-bg: #1b1b1f;
--vp-code-copy-code-hover-border-color: #2e2e32;
--vp-code-copy-code-active-text: rgba(235, 235, 245, 0.6);
}
.copy-code-button {
position: absolute;
top: 12px;
/* rtl:ignore */
right: 12px;
z-index: 3;
width: 40px;
height: 40px;
cursor: pointer;
background-color: var(--vp-code-copy-code-bg);
background-image: var(--vp-icon-copy);
background-repeat: no-repeat;
background-position: 50%;
background-size: 20px;
border: 1px solid var(--vp-code-copy-code-border-color);
border-radius: 4px;
opacity: 0;
transition:
border-color 0.25s,
background-color 0.25s,
opacity 0.25s;
/* rtl:ignore */
direction: ltr;
}
[class*="language-"]:hover > .copy-code-button,
[class*="language-"] > .copy-code-button:focus,
[class*="language-"] > .copy-code-button.copied {
opacity: 1;
}
[class*="language-"] > .copy-code-button:hover,
[class*="language-"] > .copy-code-button.copied {
background-color: var(--vp-code-copy-code-hover-bg);
border-color: var(--vp-code-copy-code-hover-border-color);
}
[class*="language-"] > .copy-code-button.copied,
[class*="language-"] > .copy-code-button:hover.copied {
background-color: var(--vp-code-copy-code-hover-bg);
background-image: var(--vp-icon-copied);
/* rtl:ignore */
border-radius: 0 4px 4px 0;
}
[class*="language-"] > .copy-code-button.copied::before,
[class*="language-"] > .copy-code-button:hover.copied::before {
position: relative;
top: -1px;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
height: 40px;
padding: 0 10px;
font-size: 12px;
font-weight: 500;
color: var(--vp-code-copy-code-active-text);
text-align: center;
white-space: nowrap;
content: var(--vp-code-copy-copied-text-content);
background-color: var(--vp-code-copy-code-hover-bg);
border: 1px solid var(--vp-code-copy-code-hover-border-color);
/* rtl:ignore */
border-right: 0;
border-radius: 4px 0 0 4px;
/* rtl:ignore */
transform: translateX(calc(-100% - 1px));
}

View File

@ -1,6 +0,0 @@
import { copyCodePlugin } from './plugin.js'
export * from './plugin.js'
export * from '../shared/index.js'
export default copyCodePlugin

View File

@ -1,26 +0,0 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import type { CopyCodeOptions } from '../shared/index.js'
const __dirname = getDirname(import.meta.url)
const defaultOptions: CopyCodeOptions = {
selector: '.theme-default-content div[class*="language-"] pre',
duration: 1500,
delay: 500,
showInMobile: false,
}
export function copyCodePlugin(options: CopyCodeOptions): Plugin {
options = Object.assign({}, defaultOptions, options)
return {
name: '@vuepress-plume/plugin-copy-code',
define: (): Record<string, unknown> => ({
__COPY_CODE_OPTIONS__: options,
}),
clientConfigFile: path.resolve(__dirname, '../client/clientConfig.js'),
}
}

View File

@ -1,29 +0,0 @@
export interface CopyCodeOptions {
/**
*
*
* @default '.theme-default-content dev[class*="language-"] pre'
*/
selector?: string | string[]
/**
*
*
* @description `0`
*
* @default 1500
*/
duration?: number
/**
*
*/
showInMobile?: boolean
/**
* ms
*
* @default 500
*/
delay?: number
}

View File

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

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (C) 2021 - PRESENT by pengzhanbo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,55 +0,0 @@
# `@vuepress-plume/plugin-notes-data`
## Install
```sh
npm install @vuepress-plume/plugin-notes-data
# or
pnpm add @vuepress-plume/plugin-notes-data
# or
yarn add @vuepress-plume/plugin-notes-data
```
## Usage
``` js
// .vuepress/config.[jt]s
import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data'
export default {
// ...
plugins: [
notesDataPlugin()
]
// ...
}
```
## Options
``` ts
interface NotesDataOptions {
dir: string
link: string
include?: string | string[]
exclude?: string | string[]
notes: NotesItem[]
}
interface NotesItem {
dir: string
link: string
text: string
sidebar?: NotesSidebar | 'auto'
}
type NotesSidebar = (NotesSidebarItem | string)[]
interface NotesSidebarItem {
text?: string
link?: string
dir?: string
collapsed?: boolean
items?: NotesSidebar
}
```

View File

@ -1,58 +0,0 @@
{
"name": "@vuepress-plume/plugin-notes-data",
"type": "module",
"version": "1.0.0-rc.75",
"private": "true",
"description": "The Plugin for VuePress 2 - notes data",
"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-notes-data"
},
"bugs": {
"url": "https://github.com/pengzhanbo/vuepress-theme-plume/issues"
},
"exports": {
".": {
"types": "./lib/node/index.d.ts",
"import": "./lib/node/index.js"
},
"./client": {
"types": "./lib/client/index.d.ts",
"import": "./lib/client/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}\" lib",
"ts": "tsc -b tsconfig.build.json"
},
"peerDependencies": {
"vuepress": "2.0.0-rc.14"
},
"dependencies": {
"@vue/devtools-api": "6.6.3",
"chokidar": "^3.6.0",
"create-filter": "^1.1.0",
"vue": "^3.4.31"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"notesData",
"vuepress-plugin-plugin-notes-data"
]
}

View File

@ -1,70 +0,0 @@
import { setupDevtoolsPlugin } from '@vue/devtools-api'
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { useNotesData } from './composables/index.js'
declare const __VUE_PROD_DEVTOOLS__: boolean
export default defineClientConfig({
enhance({ app }) {
const notesData = useNotesData()
Object.defineProperties(app.config.globalProperties, {
$notesData: {
get() {
return notesData.value
},
},
})
// setup devtools in dev mode
if (__VUEPRESS_DEV__ || __VUE_PROD_DEVTOOLS__) {
const PLUGIN_ID = 'org.vuejs.vuepress'
const PLUGIN_LABEL = 'VuePress'
const INSPECTOR_ID = PLUGIN_ID
setupDevtoolsPlugin(
{
// fix recursive reference
app: app as any,
id: PLUGIN_ID,
label: PLUGIN_LABEL,
packageName: '@vuepress-plume/plugin-notes-data',
homepage: 'https://theme-plume.vuejs.press/',
logo: 'https://v2.vuepress.vuejs.org/images/hero.png',
componentStateTypes: ['VuePress'],
},
(api) => {
api.on.inspectComponent((payload) => {
payload.instanceData.state.push({
type: 'VuePress',
key: 'notesData',
editable: false,
value: notesData.value,
})
})
api.on.getInspectorTree((payload) => {
if (payload.inspectorId !== INSPECTOR_ID)
return
payload.rootNodes.push({
id: 'notes_data',
label: 'Notes Data',
})
})
api.on.getInspectorState((payload) => {
if (payload.inspectorId !== INSPECTOR_ID)
return
if (payload.nodeId === 'notes_data') {
payload.state = {
NotesData: [{
key: 'notesData',
value: notesData.value,
}],
}
}
})
},
)
}
},
}) as ClientConfig

View File

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

View File

@ -1,22 +0,0 @@
import { notesData as notesDataRaw } from '@internal/notesData'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { NotesData } from '../../shared/index.js'
declare const __VUE_HMR_RUNTIME__: Record<string, any>
export type NotesDataRef<T extends NotesData = NotesData> = Ref<T>
export const notesData: NotesDataRef = ref(notesDataRaw)
export function useNotesData<
T extends NotesData = NotesData,
>(): NotesDataRef<T> {
return notesData as NotesDataRef<T>
}
if (__VUEPRESS_DEV__ && (import.meta.webpackHot || import.meta.hot)) {
__VUE_HMR_RUNTIME__.updateNotesData = (data: NotesData) => {
notesData.value = data
}
}

View File

@ -1,2 +0,0 @@
export type { NotesData, NotesSidebarItem } from '../shared/index.js'
export * from './composables/index.js'

View File

@ -1,7 +0,0 @@
import type { NotesData } from '../shared/index.js'
declare module '@internal/notesData' {
const notesData: NotesData
export { notesData }
}

View File

@ -1,6 +0,0 @@
import { notesDataPlugin } from './plugin.js'
export * from './plugin.js'
export * from '../shared/index.js'
export default notesDataPlugin

View File

@ -1,22 +0,0 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import type { NotesDataOptions } from '../shared/index.js'
import { prepareNotesData, watchNotesData } from './prepareNotesData.js'
import { wait } from './utils.js'
export function notesDataPlugin(options: NotesDataOptions | NotesDataOptions[]): Plugin {
return {
name: '@vuepress-plume/plugin-notes-data',
clientConfigFile: path.join(
getDirname(import.meta.url),
'../client/clientConfig.js',
),
onPrepared: async (app) => {
await wait(50)
await prepareNotesData(app, options)
},
onWatched: (app, watchers) => watchNotesData(app, watchers, options),
}
}

View File

@ -1,246 +0,0 @@
import { colors, logger, path } from 'vuepress/utils'
import type { App } from 'vuepress/core'
import * as chokidar from 'chokidar'
import { createFilter } from 'create-filter'
import type {
NotesData,
NotesDataOptions,
NotesItemOptions,
NotesSidebar,
NotesSidebarItem,
} from '../shared/index.js'
import { ensureArray, hash, normalizePath } from './utils.js'
const HMR_CODE = `
if (import.meta.webpackHot) {
import.meta.webpackHot.accept()
if (__VUE_HMR_RUNTIME__.updateNotesData) {
__VUE_HMR_RUNTIME__.updateNotesData(notesData)
}
}
if (import.meta.hot) {
import.meta.hot.accept(({ notesData }) => {
__VUE_HMR_RUNTIME__.updateNotesData(notesData)
})
}
`
interface NotePage {
relativePath: string
title: string
link: string
frontmatter: Record<string, any>
}
function resolvedNotesData(app: App, options: NotesDataOptions, result: NotesData) {
const { include, exclude, notes, dir: _dir, link } = options
if (!notes || notes.length === 0)
return
const dir = normalizePath(_dir).replace(/^\//, '')
const filter = createFilter(ensureArray(include), ensureArray(exclude), {
resolve: false,
})
const DIR_PATTERN = new RegExp(`^${normalizePath(path.join(dir, '/'))}`)
const notesPageList: NotePage[] = app.pages
.filter(
page =>
page.filePathRelative
&& page.filePathRelative.startsWith(dir)
&& filter(page.filePathRelative),
)
.map(page => ({
relativePath: page.filePathRelative?.replace(DIR_PATTERN, '') || '',
title: page.title,
link: page.path,
frontmatter: page.frontmatter,
}))
notes.forEach((note) => {
result[normalizePath(path.join('/', link, note.link))] = initSidebar(
note,
notesPageList.filter(page =>
page.relativePath.startsWith(note.dir.trim().replace(/^\/|\/$/g, '')),
),
)
})
}
let contentHash: string | undefined
export async function prepareNotesData(app: App, options: NotesDataOptions | NotesDataOptions[]) {
const start = performance.now()
const notesData: NotesData = {}
const allOptions = ensureArray<NotesDataOptions>(options)
allOptions.forEach(option => resolvedNotesData(app, option, notesData))
let content = `
export const notesData = ${JSON.stringify(notesData, null, 2)}
`
if (app.env.isDev)
content += HMR_CODE
const currentHash = hash(content)
if (!contentHash || contentHash !== currentHash) {
contentHash = currentHash
await app.writeTemp('internal/notesData.js', content)
}
if (app.env.isDebug) {
logger.info(
`\n[${colors.green('@vuepress-plume/plugin-notes-data')}] prepare notes data time spent: ${(performance.now() - start).toFixed(2)}ms`,
)
}
}
export function watchNotesData(app: App, watchers: any[], options: NotesDataOptions | NotesDataOptions[]): void {
const allOptions = ensureArray<NotesDataOptions>(options)
if (!allOptions.length)
return
const [firstLink, ...links] = allOptions.map(option => option.link).filter(Boolean)
if (!firstLink)
return
const dir = path.join('pages', firstLink, '**/*')
const watcher = chokidar.watch(dir, {
cwd: app.dir.temp(),
ignoreInitial: true,
})
links.length && watcher.add(links.map(link => path.join('pages', link, '**/*')))
watcher.on('add', () => prepareNotesData(app, options))
watcher.on('change', () => prepareNotesData(app, options))
watcher.on('unlink', () => prepareNotesData(app, options))
watchers.push(watcher)
}
function initSidebar(note: NotesItemOptions, pages: NotePage[]): NotesSidebarItem[] {
if (!note.sidebar)
return []
if (note.sidebar === 'auto')
return initSidebarByAuto(note, pages)
return initSidebarByConfig(note, pages)
}
function initSidebarByAuto(
note: NotesItemOptions,
pages: NotePage[],
): NotesSidebarItem[] {
let tempPages = pages.map((page) => {
return { ...page, splitPath: page.relativePath.split('/') }
})
const maxIndex = Math.max(...tempPages.map(page => page.splitPath.length))
let nowIndex = 0
while (nowIndex < maxIndex) {
tempPages = tempPages.sort((prev, next) => {
const pi = prev.splitPath?.[nowIndex]?.match(/(\d+)\.(?=[^/]+$)/)?.[1]
const ni = next.splitPath?.[nowIndex]?.match(/(\d+)\.(?=[^/]+$)/)?.[1]
if (!pi || !ni)
return 0
return Number.parseFloat(pi) < Number.parseFloat(ni) ? -1 : 1
})
nowIndex++
}
pages = tempPages.map((page) => {
delete (page as any).splitPath
return page
})
const RE_INDEX = ['index.md', 'README.md', 'readme.md']
const result: NotesSidebarItem[] = []
for (const page of pages) {
const { relativePath, title, link, frontmatter } = page
const paths = relativePath
.slice(note.dir.replace(/^\/|\/$/g, '').length + 1)
.split('/')
let index = 0
let dir: string
let items = result
// eslint-disable-next-line no-cond-assign
while ((dir = paths[index])) {
const text = dir.replace(/\.md$/, '').replace(/^\d+\./, '')
let current = items.find(item => item.text === text)
if (!current) {
current = { text, link: undefined, items: [] }
!RE_INDEX.includes(dir) ? items.push(current) : items.unshift(current)
}
if (dir.endsWith('.md')) {
current.link = link
current.text = title
}
if (frontmatter.icon)
current.icon = frontmatter.icon
items = current.items as NotesSidebarItem[]
index++
}
}
return result
}
function initSidebarByConfig(
{ text, dir, sidebar }: NotesItemOptions,
pages: NotePage[],
): NotesSidebarItem[] {
return (sidebar as NotesSidebar).map((item) => {
if (typeof item === 'string') {
const current = findNotePage(item, dir, pages)
return {
text: current?.title || text,
link: current?.link,
icon: current?.frontmatter.icon,
// items: [],
}
}
else {
const current = findNotePage(item.link || '', dir, pages)
return {
text: item.text || item.dir || current?.title,
collapsed: item.collapsed,
icon: item.icon || current?.frontmatter.icon,
link: item.link,
items: initSidebarByConfig(
{
link: item.link || '',
text: item.text || '',
sidebar: item.items,
dir: normalizePath(path.join(dir, item.dir || '')),
},
pages,
),
}
}
})
}
function findNotePage(
sidebar: string,
dir: string,
notePageList: NotePage[],
): NotePage | undefined {
if (sidebar === '' || sidebar === 'README.md' || sidebar === 'index.md') {
return notePageList.find((page) => {
const relative = page.relativePath
return (
relative === normalizePath(path.join(dir, 'README.md'))
|| relative === normalizePath(path.join(dir, 'index.md'))
)
})
}
else {
return notePageList.find((page) => {
const relative = page.relativePath
return (
relative === normalizePath(path.join(dir, sidebar))
|| relative === normalizePath(path.join(dir, `${sidebar}.md`))
|| page.link === sidebar
)
})
}
}

View File

@ -1,21 +0,0 @@
import { createHash } from 'node:crypto'
export function ensureArray<T>(thing: T | T[] | null | undefined): T[] {
if (Array.isArray(thing))
return thing
if (thing === null || thing === undefined)
return []
return [thing]
}
export function normalizePath(str: string) {
return str.replace(/\\+/g, '/')
}
export function wait(time: number) {
return new Promise(resolve => setTimeout(resolve, time))
}
export function hash(content: string): string {
return createHash('md5').update(content).digest('hex')
}

View File

@ -1,77 +0,0 @@
export interface NotesDataOptions {
/**
*
* @default '/notes/'
*/
dir: string
/**
*
* @default '/'
*/
link: string
/**
* global include
*/
include?: string | string[]
/**
* global exclude
*/
exclude?: string | string[]
/**
*
*/
notes: NotesItemOptions[]
}
export type NotesItemOptions = (Omit<NotesItem, 'text'> & { text?: string })
export interface NotesItem {
/**
*
*/
dir: string
/**
* `notes.link`
*/
link: string
/**
*
*/
text: string
/**
*
*/
sidebar?: NotesSidebar | 'auto'
}
export type NotesSidebar = (NotesSidebarItem | string)[]
export interface NotesSidebarItem {
/**
* 使 `dir`
*/
text?: string
/**
*
*/
link?: string
/**
*
*/
dir?: string
/**
* ,
* @default undefined
*/
collapsed?: boolean
/**
*
*/
items?: NotesSidebar
/**
*
*/
icon?: string | { svg: string }
}
export type NotesData = Record<string, NotesSidebarItem[]>

View File

@ -1,12 +0,0 @@
{
"extends": "../tsconfig.build.json",
"compilerOptions": {
"rootDir": "./src",
"paths": {
"@internal/notesData": ["./src/client/notesData.d.ts"]
},
"types": ["vuepress/client-types", "vite/client", "webpack-env"],
"outDir": "./lib"
},
"include": ["./src"]
}

View File

@ -4,13 +4,9 @@
"composite": true
},
"references": [
{ "path": "./plugin-auto-frontmatter/tsconfig.build.json" },
{ "path": "./plugin-baidu-tongji/tsconfig.build.json" },
{ "path": "./plugin-blog-data/tsconfig.build.json" },
{ "path": "./plugin-caniuse/tsconfig.build.json" },
{ "path": "./plugin-copy-code/tsconfig.build.json" },
{ "path": "./plugin-iconify/tsconfig.build.json" },
{ "path": "./plugin-notes-data/tsconfig.build.json" },
{ "path": "./plugin-fonts/tsconfig.build.json" },
{ "path": "./plugin-shikiji/tsconfig.build.json" },
{ "path": "./plugin-content-update/tsconfig.build.json" },
{ "path": "./plugin-search/tsconfig.build.json" },

85
pnpm-lock.yaml generated
View File

@ -106,67 +106,12 @@ importers:
specifier: ^4.17.21
version: 4.17.21
plugins/plugin-auto-frontmatter:
dependencies:
'@pengzhanbo/utils':
specifier: ^1.1.2
version: 1.1.2
chokidar:
specifier: ^3.6.0
version: 3.6.0
create-filter:
specifier: ^1.1.0
version: 1.1.0
fast-glob:
specifier: ^3.3.2
version: 3.3.2
gray-matter:
specifier: ^4.0.3
version: 4.0.3
json2yaml:
specifier: ^1.1.0
version: 1.1.0
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-baidu-tongji:
dependencies:
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-blog-data:
dependencies:
'@vue/devtools-api':
specifier: 6.6.3
version: 6.6.3
chokidar:
specifier: ^3.6.0
version: 3.6.0
create-filter:
specifier: ^1.1.0
version: 1.1.0
vue:
specifier: ^3.4.31
version: 3.4.31(typescript@5.5.3)
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-caniuse:
dependencies:
markdown-it-container:
specifier: ^4.0.0
version: 4.0.0
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
devDependencies:
'@types/markdown-it':
specifier: ^14.1.1
version: 14.1.1
plugins/plugin-content-update:
dependencies:
vue:
@ -176,18 +121,6 @@ importers:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-copy-code:
dependencies:
'@vuepress-plume/plugin-content-update':
specifier: workspace:~
version: link:../plugin-content-update
vue:
specifier: ^3.4.31
version: 3.4.31(typescript@5.5.3)
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-fonts:
dependencies:
vuepress:
@ -249,24 +182,6 @@ importers:
specifier: ^14.1.1
version: 14.1.1
plugins/plugin-notes-data:
dependencies:
'@vue/devtools-api':
specifier: 6.6.3
version: 6.6.3
chokidar:
specifier: ^3.6.0
version: 3.6.0
create-filter:
specifier: ^1.1.0
version: 1.1.0
vue:
specifier: ^3.4.31
version: 3.4.31(typescript@5.5.3)
vuepress:
specifier: 2.0.0-rc.14
version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.12.10)(typescript@5.5.3))(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
plugins/plugin-search:
dependencies:
'@vuepress/helper':

View File

@ -173,8 +173,8 @@ watch(
max-width: 784px;
}
.vp-doc-container:not(.has-sidebar.has-aside) .content {
max-width: 884px;
.vp-doc-container.is-blog:not(.has-sidebar.has-aside) .content {
max-width: 985px;
}
.vp-doc-container:not(.has-sidebar) .container {

View File

@ -2,7 +2,7 @@ import { path } from 'vuepress/utils'
import { removeLeadingSlash, resolveLocalePath } from 'vuepress/shared'
import { ensureLeadingSlash } from '@vuepress/helper'
import { format } from 'date-fns'
import { uniq } from '@pengzhanbo/utils'
import { toArray, uniq } from '@pengzhanbo/utils'
import type {
AutoFrontmatter,
AutoFrontmatterArray,
@ -194,6 +194,36 @@ export function resolveOptions(
include: '**/{readme,README,index}.md',
frontmatter: {},
},
toArray(localeOptions.blog?.include).length
? {
include: localeOptions.blog?.include,
frontmatter: {
...options.title !== false
? {
title(title: string, { relativePath }) {
if (title)
return title
const basename = path.basename(relativePath || '', '.md')
return basename
},
} as AutoFrontmatterObject
: undefined,
...baseFrontmatter,
...options.permalink !== false
? {
permalink(permalink: string, { relativePath }) {
if (permalink)
return permalink
const locale = resolveLocale(relativePath)
const prefix = withBase(articlePrefix, locale)
return normalizePath(`${prefix}/${nanoid()}/`)
},
} as AutoFrontmatterObject
: undefined,
},
}
: '',
{
include: '*',
frontmatter: {
@ -213,10 +243,7 @@ export function resolveOptions(
permalink(permalink: string, { relativePath }) {
if (permalink)
return permalink
const locale = resolveLocale(relativePath)
const prefix = withBase(articlePrefix, locale)
return normalizePath(`${prefix}/${nanoid()}/`)
return ensureLeadingSlash(normalizePath(relativePath.replace(/\.md$/, '/')))
},
} as AutoFrontmatterObject
: undefined,

View File

@ -21,7 +21,7 @@ export async function findConfigPath(app: App, configPath?: string): Promise<str
}
}
extensions.forEach((ext) => {
paths.push(resolve(cwd, `./${configPath}.${ext}`))
paths.push(resolve(cwd, `./${CONFIG_FILE_NAME}.${ext}`))
paths.push(resolve(cwd, `${source}/${CONFIG_FILE_NAME}.${ext}`))
paths.push(resolve(cwd, `./.vuepress/${CONFIG_FILE_NAME}.${ext}`))
})

View File

@ -7,3 +7,4 @@ export * from './sidebar.js'
export * from './navbar.js'
export * from './notes.js'
export * from './auto-frontmatter.js'
export * from './theme-data.js'

View File

@ -40,7 +40,7 @@ export interface PlumeThemeOptions extends PlumeThemeLocaleOptions {
export type PlumeThemeLocaleOptions = PlumeThemeData
export type PlumeThemeData = PlumeThemeLocaleData & {
locales?: LocaleConfig<Omit<PlumeThemeLocaleData, 'blog'>>
locales?: LocaleConfig<Omit<PlumeThemeLocaleData, 'blog' | 'article'>>
}
export * from './locale.js'

View File

@ -4,12 +4,6 @@
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@internal/blogData": [
"./plugins/plugin-blog-data/src/client/blogPostData.d.ts"
],
"@internal/notesData": [
"./plugins/plugin-notes-data/src/client/notesData.d.ts"
],
"@internal/md-power/replEditorData": [
"./plugins/plugin-md-power/src/client/shim.d.ts"
],
@ -22,9 +16,7 @@
"vuepress-plugin-md-power": [
"./plugins/plugin-md-power/src/node/index.ts"
],
"@theme/*": [
"./theme/src/client/components/*"
]
"@theme/*": ["./theme/src/client/components/*"]
},
"types": ["webpack-env", "vite/client", "vuepress/client-types"]
},