chore: tweak

This commit is contained in:
pengzhanbo 2024-07-11 05:28:15 +08:00
parent 0142fe9fa3
commit ab1c30eac3
65 changed files with 2 additions and 2300 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

@ -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

@ -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"]
},