Merge pull request #60 from pengzhanbo/md-power

vuepress-plugin-md-power
This commit is contained in:
pengzhanbo 2024-03-31 01:00:33 +08:00 committed by GitHub
commit 65ac90c094
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 2851 additions and 230 deletions

View File

@ -54,6 +54,7 @@
"vue"
],
"cSpell.words": [
"bilibili",
"bumpp",
"caniuse",
"colours",

View File

@ -33,6 +33,12 @@ export const zhNotes = definePlumeNotesConfig({
dir: '图表',
items: ['chart', 'echarts', 'mermaid', 'flowchart'],
},
{
text: '资源嵌入',
icon: 'dashicons:embed-video',
dir: '嵌入',
items: ['pdf', 'bilibili', 'youtube'],
},
],
},
{

Binary file not shown.

View File

@ -66,6 +66,15 @@ export const theme: Theme = themePlume({
mermaid: true,
flowchart: true,
},
markdownPower: {
pdf: true,
caniuse: true,
bilibili: true,
youtube: true,
icons: true,
codepen: true,
replit: true,
},
comment: {
provider: 'Giscus',
comment: true,

View File

@ -77,8 +77,11 @@ config:
title: 加密
description: 支持全站加密、部分加密(加密目录、加密文章)。
-
title: 代码复制
description: 一键复制代码块中的内容
title: 代码
description: 代码复制CodePen演示Replit演示
-
title: 资源嵌入
description: 图表视频PDF
-
type: text-image
title: 博客

View File

@ -6,6 +6,108 @@ createTime: 2024/03/05 16:27:59
permalink: /guide/markdown/advance/
---
## iconify 图标
在 Markdown 文件中使用 [iconify](https://iconify.design/) 的图标。 主题虽然提供了
[`<Iconify />`](/guide/features/component/#iconify) 组件来支持在 markdown 中使用图标,
但是它需要从远程加载图标,可能速度比较慢。
为此,主题提供了另一种可选的方式,以更简单的方式,在 Markdown 中使用图标,并且将 图标资源编译到
本地静态资源中。
### 配置
该功能默认不启用,你需要在 `theme` 配置中启用。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
icons: true,
},
}
})
})
```
:::
同时,该功能还需要你额外安装 `@iconify/json` 依赖。
::: code-tabs
@tab pnpm
```sh
pnpm add @iconify/json
```
@tab yarn
```sh
yarn add @iconify/json
```
@tab npm
```sh
npm install @iconify/json
```
:::
### 语法
```md
:[collect:name]:
```
设置图标大小和颜色
```md
:[collect:name size]:
:[collect:name /color]:
:[collect:name size/color]:
```
`iconify` 拥有非常多的图标,这些图标被分组为不同的 `collect`,每个 `collect` 都有自己的
图标。
你可以从 <https://icon-sets.iconify.design/> 中获取 collect 和 name。
### 示例
输入:
```md
:[ion:logo-markdown]:
```
输出:
:[ion:logo-markdown]:
该语法为行内语法,因此,你可以在同一行中跟其他 markdown 语法 一起使用。
输入:
```md
github: :[tdesign:logo-github-filled]:
修改颜色::[tdesign:logo-github-filled /#f00]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
```
输出:
github: :[tdesign:logo-github-filled]:
修改颜色::[tdesign:logo-github-filled /#f00]:
修改大小::[tdesign:logo-github-filled 36px]:
修改大小颜色::[tdesign:logo-github-filled 36px/#f00]:
## 选项组
让你的 Markdown 文件支持选项卡。
@ -196,6 +298,25 @@ corepack use pnpm@8
## can I use
此功能默认不启用,你可以在配置文件中启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
caniuse: true, // [!code highlight]
},
}
})
})
```
:::
在编写文章时, 提供嵌入 [can-i-use](https://caniuse.com/) WEB feature 各平台支持说明 的功能。
方便在描述某个 WEB feature 时,能更直观的表述 该特性的支持程度。
@ -203,30 +324,19 @@ corepack use pnpm@8
在你的 文章 markdown文件中使用以下格式
``` md
::: caniuse <feature> {browser_versions}
:::
@[caniuse](feature)
```
**示例: 获取 css 伪类选择器 `:dir()` 在各个浏览器的支持情况图标:**
``` md
::: caniuse css-matches-pseudo
:::
```
效果:
::: caniuse css-matches-pseudo
:::
### 语法
``` md
::: caniuse <feature> {browser_versions}
:::
@[caniuse](feature)
@[caniuse{browser_versions}](feature)
@[caniuse embed_type](feature)
@[caniuse embed_type{browser_versions}](feature)
```
- `<feature>`
- `feature`
必填。 正确取值请参考 [https://caniuse.bitsofco.de/](https://caniuse.bitsofco.de/)
@ -242,6 +352,187 @@ corepack use pnpm@8
- `0` 表示当前浏览器版本的支持情况
- 大于`0` 表示高于当前浏览器版本的支持情况
- `embed_type`
可选。 资源嵌入的类型。
类型: `'embed' | 'image'`
默认值为:`'embed'`
### 示例
**获取 css 伪类选择器 `:dir()` 在各个浏览器的支持情况:**
```md
@[caniuse](css-matches-pseudo)
```
效果:
@[caniuse](css-matches-pseudo)
**以图片的形式,获取 css 伪类选择器 `:dir()` 在各个浏览器的支持情况:**
```md
@[caniuse image](css-matches-pseudo)
```
效果:
@[caniuse image](css-matches-pseudo)
**获取 css 伪类选择器 `:dir()` 特定范围浏览器的支持情况:**
```md
@[caniuse{-2,-1,1,2,3}](css-matches-pseudo)
```
效果:
@[caniuse{-2,-1,1,2,3}](css-matches-pseudo)
## CodePen
主题支持在 Markdown 文件中嵌入 [CodePen](https://codepen.io/)。
### 配置
此功能默认不启用,你可以在配置文件中启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
codepen: true, // [!code highlight]
},
}
})
})
```
:::
### 语法
简单语法:
```md
@[codepen](user/slash)
```
更多选项支持:
```md
@[codepen preview editable tab="css,result" theme="dark" height="500px" width="100%"](user/slash)
```
- `preview`: 是否渲染为预览模式
- `editable`: 是否可编辑
- `tab`: 默认显示的标签, 默认为 `result`,多个使用 `,` 分隔
- `theme`: 主题, 可选值 `dark``light`
- `height`: 容器高度, 默认为 `400px`
- `width`: 容器宽度, 默认为 `100%`
- `user`: CodePen 用户名
- `slash`: CodePen 代码文件名
### 示例
输入:
```md
@[codepen](leimapapa/RwOZQOW)
```
输出:
@[codepen](leimapapa/RwOZQOW)
**预览模式:**
输入:
```md
@[codepen preview](leimapapa/RwOZQOW)
```
输出:
@[codepen preview](leimapapa/RwOZQOW)
**编辑模式:**
输入:
```md
@[codepen editable tab="html,result"](leimapapa/RwOZQOW)
```
输出:
@[codepen editable tab="html,result"](leimapapa/RwOZQOW)
## Replit
主题支持在 Markdown 文件中嵌入 [Replit](https://replit.com/)。
### 配置
此功能默认不启用,你可以在配置文件中启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
replit: true, // [!code highlight]
},
}
})
})
```
:::
### 语法
简单的语法
```md
@[replit](user/repl-name)
```
更多选项
```md
@[replit title="" width="100%" height="450px" theme="dark"](user/repl-name#filepath)
```
- `title`: 标题
- `width`: 容器宽度
- `height`: 容器高度
- `theme`: 主题, 可选值 `dark``light`
- `user`: Replit 用户名
- `repl-name`: Replit repl 名称
- `filepath`: Replit 默认打开的文件路径
输入:
```md
@[replit](@TechPandaPro/Cursor-Hangout#package.json)
````
输出:
@[replit](@TechPandaPro/Cursor-Hangout#package.json)
## 导入文件
主题支持在 Markdown 文件中导入文件切片。

View File

@ -42,3 +42,6 @@ VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_
- 👀 支持 搜索、文章评论
- 👨‍💻‍ 支持 浅色/深色 主题 (包括代码高亮)
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示 等
- 📚 代码演示,支持 CodePen, Replit
- 📊 嵌入图标,支持 chart.jsEchartsMermaidflowchart
- 🎛 资源嵌入,支持 PDF, bilibili视频youtube视频等

View File

@ -0,0 +1,95 @@
---
title: Bilibili 视频
author: pengzhanbo
icon: ri:bilibili-fill
createTime: 2024/03/28 12:26:47
permalink: /guide/embed/video/bilibili/
---
## 概述
有时候,你想在你的文档中嵌入视频,以提高内容的表达能力。
主题提供了 嵌入 Bilibili 视频 的功能。
该功能由 [vuepress-plugin-md-power](/) 提供支持。
## 配置
该功能默认不启用。你需要在主题配置中开启。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
bilibili: true,
},
}
})
})
```
:::
## 语法
简单的语法:
```md
@[bilibili](bvid)
```
带 分P 的视频,在 `bilibili` 后跟随 `p1``p2``p3` 等选项
```md
@[bilibili p1](aid cid)
```
更多选项:
```md
@[bilibili p1 autoplay time="0" width="100%" height="400px" ratio="16:9"](bvid aid cid)
```
**选项说明:**
- bvid: 视频 BV ID
- aid: 视频 AID
- cid: 视频 CID
- autoplay: 是否自动播放
- time: 视频开始播放时间点,单位为秒, 也可以使用 `mm:ss``hh:mm:ss` 格式
- width: 视频宽度
- height: 视频高度
- ratio: 视频比例,默认 `16:9`
对于分P的视频可以省略传入 `bvid`,但需要传入 `aid``cid`
## 示例
### 宽频视频
输入:
```md
@[bilibili](BV1EZ42187Hg)
```
输出:
@[bilibili](BV1EZ42187Hg)
### 竖屏视频
输入:
```md
@[bilibili width="320px" ratio="9:16"](BV1zr42187eg)
```
输出:
@[bilibili width="320px" ratio="9:16"](BV1zr42187eg)

View File

@ -0,0 +1,122 @@
---
title: PDF 阅读
author: pengzhanbo
icon: teenyicons:pdf-outline
createTime: 2024/03/28 13:30:53
permalink: /guide/embed/pdf/
---
## 概述
主题支持在 markdown 中嵌入 PDF 文件,它能够在页面中直接阅读 PDF 。
该功能由 [vuepress-plugin-md-power](/) 提供支持。
## 配置
该功能默认不启用。你需要在主题配置中开启。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
pdf: true,
},
}
})
})
```
:::
## 语法
最简单的语法如下:
```md
@[pdf](url)
```
当需要打开特定页面时,在 `pdf` 后面跟随一个 页数。
```md
@[pdf 2](url)
```
还可以添加更多的 选项到 `@[pdf ]` 中,更灵活的控制行为。
```md
@[pdf 2 no-toolbar width="100%" height="400px" ratio="16:9" zoom="100"](url)
```
- `no-toolbar` - 不显示工具栏
- `width` - 宽度,默认为 100%
- `height` - 高度,默认为 `auto`
- `ratio` - 宽高比, 默认为 `16:9`, 仅当未指定高度时生效
- `zoom` - 缩放比例, 百分比。
## 示例
### 默认
输入:
```md
@[pdf](https://plume.pengzhanbo.cn/files/sample.pdf)
```
输出:
@[pdf](/files/sample.pdf)
### 设置页码为 2
输入:
```md
@[pdf 2](https://plume.pengzhanbo.cn/files/sample.pdf)
```
输出:
@[pdf 2 zoom="95"](/files/sample.pdf)
### 不显示工具栏
输入:
```md
@[pdf no-toolbar](https://plume.pengzhanbo.cn/files/sample.pdf)
```
输出:
@[pdf no-toolbar](/files/sample.pdf)
### 缩放比 90%
输入:
```md
@[pdf zoom="90"](https://plume.pengzhanbo.cn/files/sample.pdf)
```
输出:
@[pdf zoom="90"](/files/sample.pdf)
### 宽高比 21:29
输入:
```md
@[pdf zoom="95" ratio="21:29"](https://plume.pengzhanbo.cn/files/sample.pdf)
```
输出:
@[pdf zoom="95" ratio="21:29"](/files/sample.pdf)

View File

@ -0,0 +1,75 @@
---
title: Youtube 视频
author: pengzhanbo
icon: mdi:youtube
createTime: 2024/03/28 14:30:33
permalink: /guide/embed/video/youtube/
---
## 概述
有时候,你想在你的文档中嵌入视频,以提高内容的表达能力。
主题提供了 嵌入 Youtube 视频 的功能。
该功能由 [vuepress-plugin-md-power](/) 提供支持。
## 配置
该功能默认不启用。你需要在主题配置中开启。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
youtube: true,
},
}
})
})
```
:::
## 语法
简单的语法:
```md
@[youtube](id)
```
更多选项:
```md
@[youtube autoplay loop start="0" end="0" width="100%" height="400px" ratio="16:9"](id)
```
**选项说明:**
- id: 视频 ID
- autoplay: 是否自动播放
- loop: 是否循环播放
- start: 视频开始播放时间点,单位为秒, 也可以使用 `mm:ss``hh:mm:ss` 格式
- end: 视频结束播放时间点,单位为秒, 也可以使用 `mm:ss``hh:mm:ss` 格式
- width: 视频宽度
- height: 视频高度
- ratio: 视频比例,默认 `16:9`
## 示例
### 宽频视频
输入:
```md
@[youtube](0JJPfz5dg20)
```
输出:
@[youtube](0JJPfz5dg20)

View File

@ -12,6 +12,7 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@iconify/json": "^2.2.196",
"@vuepress/bundler-vite": "2.0.0-rc.9",
"anywhere": "^1.6.0",
"chart.js": "^4.4.2",

View File

@ -56,10 +56,10 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"rimraf": "^5.0.5",
"stylelint": "^16.2.1",
"stylelint": "^16.3.1",
"tsconfig-vuepress": "^4.5.0",
"typescript": "^5.4.3",
"vite": "^5.2.4"
"vite": "^5.2.7"
},
"pnpm": {
"patchedDependencies": {

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,163 @@
# vuepress-plugin-md-power
为 vuepress 提供 丰富的 markdown 语法支持。
## 功能
- caniuse 支持,提供前端各种特性在各个浏览器版本中的支持情况查看器
- 嵌入 PDF 支持
- 嵌入 视频支持,当前支持嵌入 bilibili 和 youtube 的视频
- 内联 iconify 图标支持
## 安装
```sh
pnpm add vuepress-plugin-md-power
```
## 使用
```ts
import { defineUserConfig } from 'vuepress'
import { md } from 'vuepress-plugin-md-power'
export default defineUserConfig({
plugins: [
markdownPowerPlugin({
caniuse: true,
pdf: true,
bilibili: true,
youtube: true,
icons: true,
})
]
})
```
### caniuse
插件默认不启用该功能,你需要手动设置 `caniuse``true`
#### 语法
```md
@[caniuse](feature)
@[caniuse image](feature)
@[caniuse embed{versions}](feature)
```
你可以从 [caniuse](https://caniuse.bitsofco.de/) 获取 feature 的值。
默认情况下,插件通过 `iframe` 嵌入 `caniuse` 的支持情况查看器。
你也可以使用 `@[caniuse image](feature)` 直接嵌入图片。
caniuse 默认查看最近的5个浏览器版本。你可以通过 `{versions}` 手动设置查看的浏览器版本。
格式为 `{number,number,...}`。取值范围为 `-5 ~ 3`
- 小于0 表示低于当前浏览器版本的支持情况
- 0 表示当前浏览器版本的支持情况
- 大于0 表示高于当前浏览器版本的支持情况
`{-2,-1,1,2}` 表示查看低于当前 2 个版本 到 高于当前 2 个版本的支持情况。
### pdf
插件默认不启用该功能,你需要手动设置 `pdf``true`
#### 语法
```md
@[pdf](url)
@[pdf 1](url)
@[pdf 1 no-toolbar width="100%" height="600px" zoom="1" ratio="16:9"](url)
```
`url` 只支持绝对路径以及完整的资源链接地址,请勿传入相对路径。
你可以在 `pdf` 后紧跟空格,设置一个数字表示默认显示的 pdf 页码
- `no-toolbar` 表示不显示工具栏
- `width` 设置宽度
- `height` 设置高度
- `zoom` 设置缩放
- `ratio` 设置宽高比, 仅当 `width` 有值, `height` 未设置时有效
### icons
插件默认不启用该功能,你需要手动设置 `icons``true`
你还需要手动安装 `@iconify/json` 依赖。
```sh
pnpm add @iconify/json
```
#### 语法
```md
:[collect:icon]:
:[collect:icon size]:
:[collect:icon /color]:
:[collect:icon size/color]:
```
你可以从 [icon-sets.iconify](https://icon-sets.iconify.design/) 获取 图标集。
显示 `logos` 图标集合下的 `vue` 图标
```md
:[logos:vue]:
```
图标默认大小为 `1em` ,你可以通过 `size` 设置图标大小
```md
:[logos:vue 1.2em]:
```
图标默认颜色为 `currentColor` 你可以通过 `/color` 设置图标颜色
```md
:[logos:vue /blue]:
```
也可以通过 `size/color` 设置图标大小和颜色
```md
:[logos:vue 1.2em/blue]:
```
### bilibili
插件默认不启用该功能,你需要手动设置 `bilibili``true`
#### 语法
```md
@[bilibili](bvid)
@[bilibili autoplay time="0"](bvid)
@[bilibili p1 autoplay time="0" ratio="16:9"](aid cid)
```
- 设置 `autoplay` 以自动播放视频。
- 设置 `time` 以指定开始播放的时间点,单位为秒。还可以传入 `mm:ss` 或者 `hh:mm:ss`
- 如果为 分p非合集还可以设置 `p\d` (第\d 个分p此时可以只传入 `aid``cid`
- 设置 `ratio` 以指定视频的宽高比。
### youtube
插件默认不启用该功能,你需要手动设置 `youtube``true`
#### 语法
```md
@[youtube](id)
@[youtube autoplay loop ratio="16:9" star="0" end="0"](id)
```
- `id` 为 YouTube 视频 ID
- `autoplay` 为是否自动播放
- `loop` 为是否循环播放
- `ratio` 为视频的宽高比
- `star` 为开始时间,单位为秒,还可以传入 `mm:ss` 或者 `hh:mm:ss`
- `end` 为结束时间,单位为秒,还可以传入 `mm:ss` 或者 `hh:mm:ss`

View File

@ -0,0 +1,69 @@
{
"name": "vuepress-plugin-md-power",
"type": "module",
"version": "1.0.0-rc.47",
"description": "The Plugin for VuePres 2 - markdown power",
"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-md-power"
},
"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": {
"@iconify/json": "^2",
"vuepress": "2.0.0-rc.9"
},
"peerDependenciesMeta": {
"@iconify/json": {
"optional": true
}
},
"dependencies": {
"@iconify/utils": "^2.1.22",
"@vueuse/core": "^10.9.0",
"local-pkg": "^0.5.0",
"markdown-it-container": "^4.0.0",
"nanoid": "^5.0.6",
"vue": "^3.4.21"
},
"devDependencies": {
"@iconify/json": "^2.2.196",
"@types/markdown-it": "^13.0.7"
},
"publishConfig": {
"access": "public"
},
"keyword": [
"VuePress",
"vuepress plugin",
"markdown power",
"vuepress-plugin-md-power"
]
}

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { toRefs } from 'vue'
import { useSize } from '../composables/size.js'
const props = defineProps<{
src: string
title: string
width?: string
height?: string
ratio?: string
}>()
const IFRAME_ALLOW = 'accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture'
const options = toRefs(props)
const { el, width, height, resize } = useSize(options)
function onLoad() {
resize()
}
</script>
<template>
<ClientOnly>
<iframe
ref="el"
class="video_bilibili_iframe"
:src="src"
:title="title || 'Bilibili'"
:style="{ width, height }"
:allow="IFRAME_ALLOW"
@load="onLoad"
/>
</ClientOnly>
</template>
<style>
.video_bilibili_iframe {
width: 100%;
margin: 16px auto;
border: none;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { onMounted, toRefs } from 'vue'
import type { PDFTokenMeta } from '../../shared/pdf.js'
import { useSize } from '../composables/size.js'
import { usePDF } from '../composables/pdf.js'
const props = defineProps<PDFTokenMeta>()
const options = toRefs(props)
const { el, width, height, resize } = useSize(options)
onMounted(() => {
if (!el.value)
return
usePDF(el.value, props.src!, {
page: props.page,
zoom: props.zoom,
noToolbar: props.noToolbar,
})
resize()
})
</script>
<template>
<div ref="el" class="pdf-viewer-wrapper" :style="{ width, height }" />
</template>
<style>
.pdf-viewer-wrapper {
position: relative;
overflow: hidden;
border-radius: 4px;
}
.pdf-viewer {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { computed, getCurrentInstance, ref } from 'vue'
import type { ReplitTokenMeta } from '../../shared/replit.js'
const props = defineProps<ReplitTokenMeta>()
const current = getCurrentInstance()
// magic height
const height = ref('47px')
const REPLIT_LINK = 'https://replit.com/'
const isDark = computed(() => current?.appContext.config.globalProperties.$isDark.value)
const link = computed(() => {
const url = new URL(`/${props.source}`, REPLIT_LINK)
url.searchParams.set('embed', 'true')
const theme = props.theme || (isDark.value ? 'dark' : 'light')
url.searchParams.set('theme', theme)
return url.toString()
})
function onload() {
height.value = props.height || '450px'
}
</script>
<template>
<ClientOnly>
<iframe
class="replit-iframe-wrapper"
:src="link"
:title="title || 'Replit'"
:style="{ width, height }"
allowtransparency="true"
allowfullscree="true"
@load="onload"
/>
</ClientOnly>
</template>
<style>
.replit-iframe-wrapper {
width: 100%;
margin: 16px auto;
border: none;
border-top: 1px solid var(--vp-c-divider);
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
transition: border 0.25s;
}
</style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { toRefs } from 'vue'
import { useSize } from '../composables/size.js'
const props = defineProps<{
src: string
title: string
width?: string
height?: string
ratio?: string
}>()
const IFRAME_ALLOW = 'accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture'
const options = toRefs(props)
const { el, width, height, resize } = useSize(options)
</script>
<template>
<ClientOnly>
<iframe
ref="el"
class="video_youtube_iframe"
:src="src"
:title="title || 'Youtube'"
:style="{ width, height }"
:allow="IFRAME_ALLOW"
@load="resize"
/>
</ClientOnly>
</template>
<style>
.video_youtube_iframe {
width: 100%;
margin: 16px auto;
border: none;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,107 @@
import { ensureEndingSlash } from 'vuepress/shared'
import { withBase } from 'vuepress/client'
import type { PDFEmbedType, PDFTokenMeta } from '../../shared/pdf.js'
import { normalizeLink } from '../utils/link.js'
import { pluginOptions } from '../options.js'
import { checkIsMobile, checkIsSafari, checkIsiPad } from '../utils/is.js'
function queryStringify(options: PDFTokenMeta): string {
const { page, noToolbar, zoom } = options
const params = [
`page=${page}`,
`toolbar=${noToolbar ? 0 : 1}`,
`zoom=${zoom}`,
]
let queryString = params.join('&')
if (queryString)
queryString = `#${queryString}`
return queryString
}
export function renderPDF(
el: HTMLElement,
url: string,
embedType: PDFEmbedType,
options: PDFTokenMeta,
): void {
if (!pluginOptions.pdf)
return
url = normalizeLink(url)
const pdfOptions = pluginOptions.pdf === true ? {} : pluginOptions.pdf
const pdfjsUrl = pdfOptions.pdfjsUrl
? `${ensureEndingSlash(withBase(pdfOptions.pdfjsUrl))}web/viewer.html`
: ''
const queryString = queryStringify(options)
const source = embedType === 'pdfjs'
? `${pdfjsUrl}?file=${encodeURIComponent(url)}${queryString}`
: `${url}${queryString}`
const tagName = embedType === 'pdfjs' || embedType === 'iframe'
? 'iframe'
: 'embed'
el.innerHTML = ''
const pdf = document.createElement(tagName)
pdf.className = 'pdf-viewer';
(pdf as any).type = 'application/pdf'
pdf.title = options.title || 'PDF Viewer'
pdf.src = source
if (pdf instanceof HTMLIFrameElement)
pdf.allow = 'fullscreen'
el.appendChild(pdf)
}
export function usePDF(
el: HTMLElement,
url: string,
options: PDFTokenMeta,
): void {
if (typeof window === 'undefined' || !window?.navigator?.userAgent)
return
const { navigator } = window
const { userAgent } = navigator
const isModernBrowser = typeof window.Promise === 'function'
// Quick test for mobile devices.
const isMobileDevice = checkIsiPad(userAgent) || checkIsMobile(userAgent)
// Safari desktop requires special handling
const isSafariDesktop = !isMobileDevice && checkIsSafari(userAgent)
const isFirefoxWithPDFJS
= !isMobileDevice
&& /firefox/iu.test(userAgent)
&& userAgent.split('rv:').length > 1
? Number.parseInt(userAgent.split('rv:')[1].split('.')[0], 10) > 18
: false
// Determines whether PDF support is available
const supportsPDFs
// As of Sept 2020 no mobile browsers properly support PDF embeds
= !isMobileDevice
// We're moving into the age of MIME-less browsers. They mostly all support PDF rendering without plugins.
&& (isModernBrowser
// Modern versions of Firefox come bundled with PDFJS
|| isFirefoxWithPDFJS)
if (!url)
return
if (supportsPDFs || !isMobileDevice) {
const embedType = isSafariDesktop ? 'iframe' : 'embed'
return renderPDF(el, url, embedType, options)
}
if (typeof pluginOptions.pdf === 'object' && pluginOptions.pdf.pdfjsUrl)
return renderPDF(el, url, 'pdfjs', options)
el.innerHTML = `<p>This browser does not support embedding PDFs. Please download the PDF to view it: <a href='${url}' target='_blank'>Download PDF</a></p>`
}

View File

@ -0,0 +1,20 @@
let isBind = false
export function setupCanIUse(): 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

@ -0,0 +1,55 @@
import type { MaybeRef } from '@vueuse/core'
import { useEventListener } from '@vueuse/core'
import type { Ref, ShallowRef, ToRefs } from 'vue'
import { computed, isRef, onMounted, ref, shallowRef, toValue, watch } from 'vue'
import type { SizeOptions } from '../../shared/size.js'
export interface SizeInfo<T extends HTMLElement> {
el: ShallowRef<T | undefined>
width: Ref<string>
height: Ref<string>
resize: () => void
}
export function useSize<T extends HTMLElement>(
options: ToRefs<SizeOptions>,
extraHeight: MaybeRef<number> = 0,
): SizeInfo<T> {
const el = shallowRef<T>()
const width = computed(() => toValue(options.width) || '100%')
const height = ref('auto')
const getRadio = (ratio: number | string | undefined): number => {
if (typeof ratio === 'string') {
const [width, height] = ratio.split(':')
const parsedRadio = Number(width) / Number(height)
if (!Number.isNaN(parsedRadio))
return parsedRadio
}
return typeof ratio === 'number' ? ratio : 16 / 9
}
const getHeight = (width: number): string => {
const height = toValue(options.height)
const ratio = getRadio(toValue(options.ratio))
return height || `${Number(width) / ratio + toValue(extraHeight)}px`
}
const resize = (): void => {
if (el.value)
height.value = getHeight(el.value.offsetWidth)
}
onMounted(() => {
resize()
if (isRef(extraHeight))
watch(extraHeight, resize)
useEventListener('orientationchange', resize)
useEventListener('resize', resize)
})
return { el, width, height, resize }
}

View File

@ -0,0 +1,37 @@
import { defineClientConfig } from 'vuepress/client'
import type { ClientConfig } from 'vuepress/client'
import { pluginOptions } from './options.js'
import { setupCanIUse } from './composables/setupCanIUse.js'
import PDFViewer from './components/PDFViewer.vue'
import Bilibili from './components/Bilibili.vue'
import Youtube from './components/Youtube.vue'
import Replit from './components/Replit.vue'
import '@internal/md-power/icons.css'
declare const __VUEPRESS_SSR__: boolean
export default defineClientConfig({
enhance({ router, app }) {
if (pluginOptions.pdf)
app.component('PDFViewer', PDFViewer)
if (pluginOptions.bilibili)
app.component('VideoBilibili', Bilibili)
if (pluginOptions.youtube)
app.component('VideoYoutube', Youtube)
if (pluginOptions.replit)
app.component('ReplitViewer', Replit)
if (__VUEPRESS_SSR__)
return
if (pluginOptions.caniuse) {
router.afterEach(() => {
setupCanIUse()
})
}
},
}) as ClientConfig

View File

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

View File

@ -0,0 +1,5 @@
import type { MarkdownPowerPluginOptions } from '../shared/index.js'
declare const __MD_POWER_INJECT_OPTIONS__: MarkdownPowerPluginOptions
export const pluginOptions = __MD_POWER_INJECT_OPTIONS__

View File

@ -0,0 +1,6 @@
declare module '*.vue' {
import type { ComponentOptions } from 'vue'
const comp: ComponentOptions
export default comp
}

View File

@ -0,0 +1,15 @@
export function checkIsMobile(ua: string): boolean {
return /\b(?:Android|iPhone)/i.test(ua)
}
export function checkIsSafari(ua: string): boolean {
return /version\/([\w.]+) .*(mobile ?safari|safari)/i.test(ua)
}
export function checkIsiPad(ua: string): boolean {
return [
/\((ipad);[-\w),; ]+apple/i,
/applecoremedia\/[\w.]+ \((ipad)/i,
/\b(ipad)\d\d?,\d\d?[;\]].+ios/i,
].some(item => item.test(ua))
}

View File

@ -0,0 +1,6 @@
import { isLinkHttp } from 'vuepress/shared'
import { withBase } from 'vuepress/client'
export function normalizeLink(url: string): string {
return isLinkHttp(url) ? url : withBase(url)
}

View File

@ -0,0 +1,179 @@
/**
* @[caniuse embed{1,2,3,4}](feature_name)
* @[caniuse image](feature_name)
*/
import type { PluginWithOptions, Token } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import type { Markdown } from 'vuepress/markdown'
import container from 'markdown-it-container'
import type { CanIUseMode, CanIUseOptions, CanIUseTokenMeta } from '../../shared/index.js'
// @[caniuse]()
const minLength = 12
// char codes of '@[caniuse'
const START_CODES = [64, 91, 99, 97, 110, 105, 117, 115, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[caniuse(?:\s*?(embed|image)?(?:{([0-9,\-]*?)})?)\]\(([^)]*)\)/
function createCanIUseRuleBlock(defaultMode: CanIUseMode): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + minLength > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, mode, versions = '', feature] = match
const meta: CanIUseTokenMeta = {
feature,
mode: (mode as CanIUseMode) || defaultMode,
versions,
}
const token = state.push('caniuse', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = mode || defaultMode
state.line = startLine + 1
return true
}
}
function resolveCanIUse({ feature, mode, versions }: CanIUseTokenMeta): string {
if (!feature)
return ''
if (mode === 'image') {
const link = 'https://caniuse.bitsofco.de/image/'
const alt = `Data on support for the ${feature} feature across the major browsers from caniuse.com`
return `<ClientOnly><p><picture>
<source type="image/webp" srcset="${link}${feature}.webp">
<source type="image/png" srcset="${link}${feature}.png">
<img src="${link}${feature}.jpg" alt="${alt}" width="100%">
</picture></p></ClientOnly>`
}
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 `<ClientOnly><div class="ciu_embed" style="margin:16px 0" data-feature="${feature}"><iframe src="${src}" frameborder="0" width="100%" height="400px" title="Can I use${feature}"></iframe></div></ClientOnly>`
}
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(',')
}
/**
* @example
* ```md
* @[caniuse](feature_name)
* ```
*/
export const caniusePlugin: PluginWithOptions<CanIUseOptions> = (
md,
{ mode = 'embed' }: CanIUseOptions = {},
): void => {
md.block.ruler.before(
'import_code',
'caniuse',
createCanIUseRuleBlock(mode),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.caniuse = (tokens, index) => {
const token = tokens[index]
const content = resolveCanIUse(token.meta)
token.content = content
return content
}
}
/**
* @deprecated use caniuse plugin
*
*
* @example
* ```md
* :::caniuse <feature_name>
* :::
* ```
*/
export function legacyCaniuse(
md: Markdown,
{ mode = 'embed' }: CanIUseOptions = {},
): void {
const modeMap: CanIUseMode[] = ['image', 'embed']
const isMode = (mode: CanIUseMode): boolean => modeMap.includes(mode)
mode = isMode(mode) ? mode : modeMap[0]
const type = 'caniuse'
const validateReg = new RegExp(`^${type}\\s+(.*)$`)
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 ''
}
}
md.use(container, type, { validate, render })
}

View File

@ -0,0 +1,109 @@
/**
* @[codepen](user/slash)
* @[codepen preview](user/slash)
* @[codepen preview editable title="" height="400px" tab="css,result" theme="dark"](user/slash)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { CodepenTokenMeta } from '../../shared/codepen.js'
const CODEPEN_LINK = 'https://codepen.io/'
// @[codepen]()
const MIN_LENGTH = 12
// char codes of `@[codepen`
const START_CODES = [64, 91, 99, 111, 100, 101, 112, 101, 110]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[codepen(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createCodepenRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', source] = match
const { attrs } = resolveAttrs(info)
const [user, slash] = source.split('/')
const meta: CodepenTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '400px',
user,
slash,
title: attrs.title,
preview: attrs.preview,
editable: attrs.editable,
tab: attrs.tab ?? 'result',
theme: attrs.theme,
}
const token = state.push('codepen', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveCodepen(meta: CodepenTokenMeta): string {
const { title = 'Codepen', height, width } = meta
const params = new URLSearchParams()
meta.editable && params.set('editable', 'true')
meta.tab && params.set('default-tab', meta.tab)
meta.theme && params.set('theme-id', meta.theme)
const middle = meta.preview ? '/embed/preview/' : '/embed/'
const link = `${CODEPEN_LINK}${meta.user}${middle}${meta.slash}?${params.toString()}`
const style = `width:${width};height:${height};margin:16px auto;border-radius:5px;`
return `<iframe class="code-pen-iframe-wrapper" src="${link}" title="${title}" style="${style}" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">See the Pen <a href="${CODEPEN_LINK}${meta.user}/pen/${meta.slash}">${title}</a> by ${meta.user} (<a href="${CODEPEN_LINK}${meta.user}">@${meta.user}</a>) on <a href="${CODEPEN_LINK}">CodePen</a>.</iframe>`
}
export const codepenPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'codepen',
createCodepenRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.codepen = (tokens, index) => {
const token = tokens[index]
const content = resolveCodepen(token.meta)
token.content = content
return content
}
}

View File

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

View File

@ -0,0 +1,96 @@
/**
* :[mdi:11]:
* :[mdi:11 24px]:
* :[mid:11 /#ccc]:
* :[fluent-mdl2:toggle-filled 128px/#fff]:
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleInline } from 'markdown-it/lib/parser_inline.js'
import { parseRect } from '../../utils/parseRect.js'
type AddIcon = (iconName: string) => string | undefined
function createTokenizer(addIcon: AddIcon): RuleInline {
return (state, silent) => {
let found = false
const max = state.posMax
const start = state.pos
if (state.src.slice(start, start + 2) !== ':[')
return false
if (silent)
return false
// :[]:
if (max - start < 5)
return false
state.pos = start + 2
while (state.pos < max) {
if (state.src.slice(state.pos, state.pos + 2) === ']:') {
found = true
break
}
state.md.inline.skipToken(state)
}
if (!found || start + 2 === state.pos) {
state.pos = start
return false
}
const content = state.src.slice(start + 2, state.pos)
// 不允许前后带有空格
if (/^\s|\s$/.test(content)) {
state.pos = start
return false
}
// found!
state.posMax = state.pos
state.pos = start + 2
const [iconName, options = ''] = content.split(/\s+/)
const [size, color] = options.split('/')
const open = state.push('iconify_open', 'span', 1)
open.markup = ':['
const className = addIcon(iconName)
if (className)
open.attrSet('class', className)
let style = ''
if (size)
style += `width:${parseRect(size)};height:${parseRect(size)};`
if (color)
style += `color:${color};`
if (style)
open.attrSet('style', style)
const text = state.push('text', '', 0)
text.content = className ? '' : iconName
const close = state.push('iconify_close', 'span', -1)
close.markup = ']:'
state.pos = state.posMax + 2
state.posMax = max
return true
}
}
export const iconsPlugin: PluginWithOptions<AddIcon> = (
md,
addIcon = () => '',
) => {
md.inline.ruler.before('emphasis', 'iconify', createTokenizer(addIcon))
}

View File

@ -0,0 +1,128 @@
import type { App } from 'vuepress/core'
import { getIconContentCSS, getIconData } from '@iconify/utils'
import { fs, logger } from 'vuepress/utils'
import { isPackageExists } from 'local-pkg'
import { customAlphabet } from 'nanoid'
import type { IconsOptions } from '../../../shared/icons.js'
import { interopDefault } from '../../utils/package.js'
import { parseRect } from '../../utils/parseRect.js'
export interface IconCacheItem {
className: string
content: string
}
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8)
const iconDataCache = new Map<string, any>()
const URL_CONTENT_RE = /(url\([^]+?\))/
function resolveOption(opt?: boolean | IconsOptions): Required<IconsOptions> {
const options = typeof opt === 'object' ? opt : {}
options.prefix ??= 'vp-mdi'
options.color = options.color === 'currentColor' || !options.color ? 'currentcolor' : options.color
options.size = options.size ? parseRect(`${options.size}`) : '1em'
return options as Required<IconsOptions>
}
export function createIconCSSWriter(app: App, opt?: boolean | IconsOptions) {
const cache = new Map<string, IconCacheItem>()
const isInstalled = isPackageExists('@iconify/json')
const write = (content: string) => app.writeTemp('internal/md-power/icons.css', content)
const options = resolveOption(opt)
const prefix = options.prefix
const defaultContent = getDefaultContent(options)
async function writeCss() {
let css = defaultContent
for (const [, { content, className }] of cache)
css += `.${className} {\n --svg: ${content};\n}\n`
await write(css)
}
function addIcon(iconName: string) {
if (!isInstalled)
return
if (cache.has(iconName))
return cache.get(iconName)!.className
const item: IconCacheItem = {
className: `${prefix}-${nanoid()}`,
content: '',
}
cache.set(iconName, item)
genIconContent(iconName, (content) => {
item.content = content
writeCss()
})
return item.className
}
async function initIcon() {
if (!opt)
return await write('')
if (!isInstalled) {
logger.error('[plugin-md-power]: `@iconify/json` not found! Please install `@iconify/json` first.')
return
}
return await writeCss()
}
return { addIcon, writeCss, initIcon }
}
function getDefaultContent(options: Required<IconsOptions>) {
const { prefix, size, color } = options
return `[class^="${prefix}-"],
[class*=" ${prefix}-"] {
display: inline-block;
width: ${size};
height: ${size};
vertical-align: middle;
color: inherit;
background-color: ${color};
-webkit-mask: var(--svg) no-repeat;
mask: var(--svg) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
`
}
let locate: ((name: string) => any) | undefined
async function genIconContent(iconName: string, cb: (content: string) => void) {
if (!locate) {
const mod = await interopDefault(import('@iconify/json'))
locate = mod.locate
}
const [collect, name] = iconName.split(':')
let iconJson: any = iconDataCache.get(collect)
if (!iconJson) {
const filename = locate(collect)
try {
iconJson = JSON.parse(await fs.readFile(filename, 'utf-8'))
iconDataCache.set(collect, iconJson)
}
catch (e) {
logger.warn(`[plugin-md-power] Can not find icon, ${collect} is missing!`)
}
}
const data = getIconData(iconJson, name)
if (!data)
return logger.error(`[plugin-md-power] Can not read icon in ${collect}, ${name} is missing!`)
const content = getIconContentCSS(data, {
height: data.height || 24,
})
const match = content.match(URL_CONTENT_RE)
return cb(match ? match[1] : '')
}

View File

@ -0,0 +1,97 @@
/**
* @[pdf](/xxx)
* @[pdf 1](/xxx)
* @[pdf 1 no-toolbar width="100%" height="600px" zoom="1" ratio="1:1"](/xxx)
*/
import { path } from 'vuepress/utils'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import type { PDFTokenMeta } from '../../shared/pdf.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
// @[pdf]()
const MIN_LENGTH = 8
// char codes of `@[pdf`
const START_CODES = [64, 91, 112, 100, 102]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[pdf(?:\s+(\d+))?(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createPDFRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, page, info = '', src] = match
const { attrs } = resolveAttrs(info)
const meta: PDFTokenMeta = {
src,
page: +page || 1,
noToolbar: Boolean(attrs.noToolbar ?? false),
zoom: +attrs.zoom || 1,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
title: path.basename(src || ''),
}
const token = state.push('pdf', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolvePDF(meta: PDFTokenMeta): string {
const { title, src, page, noToolbar, width, height, ratio, zoom } = meta
return `<PDFViewer src="${src}" title="${title}" :page="${page}" :no-toolbar="${noToolbar}" width="${width}" height="${height}" ratio="${ratio}" :zoom="${zoom}" />`
}
export const pdfPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'pdf',
createPDFRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.pdf = (tokens, index) => {
const token = tokens[index]
const content = resolvePDF(token.meta)
token.content = content
return content
}
}

View File

@ -0,0 +1,93 @@
/**
* @[replit](user/repl-name)
* @[replit](user/repl-name#filepath)
* @[replit title="" height="400px" width="100%" theme="dark"](user/repl-name)
*/
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { parseRect } from '../utils/parseRect.js'
import type { ReplitTokenMeta } from '../../shared/replit.js'
// @[replit]()
const MIN_LENGTH = 11
// char codes of `@[replit`
const START_CODES = [64, 91, 114, 101, 112, 108, 105, 116]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[replit(?:\s+([^]*?))?\]\(([^)]*?)\)/
function createReplitRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', source] = match
const { attrs } = resolveAttrs(info)
const meta: ReplitTokenMeta = {
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '450px',
source: source.startsWith('@') ? source : `@${source}`,
title: attrs.title,
theme: attrs.theme || '',
}
const token = state.push('replit', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveReplit(meta: ReplitTokenMeta): string {
const { title, height, width, source, theme } = meta
return `<ReplitViewer title="${title || ''}" height="${height}" width="${width}" source="${source}" theme="${theme}" />`
}
export const replitPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'replit',
createReplitRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.replit = (tokens, index) => {
const token = tokens[index]
const content = resolveReplit(token.meta)
token.content = content
return content
}
}

View File

@ -0,0 +1,115 @@
/**
* @[bilibili](bid)
* @[bilibili](aid cid)
* @[bilibili](bid aid cid)
* @[bilibili p1 autoplay time=1](aid cid)
*/
import { URLSearchParams } from 'node:url'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import type { BilibiliTokenMeta } from '../../../shared/video.js'
import { resolveAttrs } from '../../utils/resolveAttrs.js'
import { parseRect } from '../../utils/parseRect.js'
import { timeToSeconds } from '../../utils/timeToSeconds.js'
const BILIBILI_LINK = 'https://player.bilibili.com/player.html'
// @[bilibili]()
const MIN_LENGTH = 13
// char codes of '@[bilibili'
const START_CODES = [64, 91, 98, 105, 108, 105, 98, 105, 108, 105]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[bilibili(?:\s+p(\d+))?(?:\s+([^]*?))?\]\(([^)]*)\)/
function createBilibiliRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, page, info = '', source = ''] = match
const { attrs } = resolveAttrs(info)
const ids = source.trim().split(/\s+/)
const bvid = ids.find(id => id.startsWith('BV'))
const [aid, cid] = ids.filter(id => !id.startsWith('BV'))
const meta: BilibiliTokenMeta = {
page: +page || 1,
bvid,
aid,
cid,
autoplay: attrs.autoplay ?? false,
time: timeToSeconds(attrs.time),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
const token = state.push('video_bilibili', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveBilibili(meta: BilibiliTokenMeta): string {
const params = new URLSearchParams()
meta.bvid && params.set('bvid', meta.bvid)
meta.aid && params.set('aid', meta.aid)
meta.cid && params.set('cid', meta.cid)
meta.page && params.set('p', meta.page.toString())
meta.time && params.set('t', meta.time.toString())
params.set('autoplay', meta.autoplay ? '1' : '0')
const source = `${BILIBILI_LINK}?${params.toString()}`
return `<VideoBilibili src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
}
export const bilibiliPlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'video_bilibili',
createBilibiliRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.video_bilibili = (tokens, index) => {
const token = tokens[index]
const content = resolveBilibili(token.meta)
token.content = content
return content
}
}

View File

@ -0,0 +1,106 @@
/**
* @[youtube](id)
*/
import { URLSearchParams } from 'node:url'
import type { PluginWithOptions } from 'markdown-it'
import type { RuleBlock } from 'markdown-it/lib/parser_block.js'
import type { YoutubeTokenMeta } from '../../../shared/video.js'
import { resolveAttrs } from '../../utils/resolveAttrs.js'
import { parseRect } from '../../utils/parseRect.js'
import { timeToSeconds } from '../../utils/timeToSeconds.js'
const YOUTUBE_LINK = 'https://www.youtube.com/embed/'
// @[youtube]()
const MIN_LENGTH = 13
// char codes of '@[youtube'
const START_CODES = [64, 91, 121, 111, 117, 116, 117, 98, 101]
// regexp to match the import syntax
const SYNTAX_RE = /^@\[youtube(?:\s+([^]*?))?\]\(([^)]*)\)/
function createYoutubeRuleBlock(): RuleBlock {
return (state, startLine, endLine, silent) => {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// return false if the length is shorter than min length
if (pos + MIN_LENGTH > max)
return false
// check if it's matched the start
for (let i = 0; i < START_CODES.length; i += 1) {
if (state.src.charCodeAt(pos + i) !== START_CODES[i])
return false
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(SYNTAX_RE)
if (!match)
return false
// return true as we have matched the syntax
if (silent)
return true
const [, info = '', id = ''] = match
const { attrs } = resolveAttrs(info)
const meta: YoutubeTokenMeta = {
id,
autoplay: attrs.autoplay ?? false,
loop: attrs.loop ?? false,
start: timeToSeconds(attrs.start),
end: timeToSeconds(attrs.end),
title: attrs.title,
width: attrs.width ? parseRect(attrs.width) : '100%',
height: attrs.height ? parseRect(attrs.height) : '',
ratio: attrs.ratio ? parseRect(attrs.ratio) : '',
}
const token = state.push('video_youtube', '', 0)
token.meta = meta
token.map = [startLine, startLine + 1]
token.info = info
state.line = startLine + 1
return true
}
}
function resolveYoutube(meta: YoutubeTokenMeta): string {
const params = new URLSearchParams()
meta.autoplay && params.set('autoplay', '1')
meta.loop && params.set('loop', '1')
meta.start && params.set('start', meta.start.toString())
meta.end && params.set('end', meta.end.toString())
const source = `${YOUTUBE_LINK}/${meta.id}?${params.toString()}`
return `<VideoYoutube src="${source}" width="${meta.width}" height="${meta.height}" ratio="${meta.ratio}" title="${meta.title}" />`
}
export const youtubePlugin: PluginWithOptions<never> = (md) => {
md.block.ruler.before(
'import_code',
'video_youtube',
createYoutubeRuleBlock(),
{
alt: ['paragraph', 'reference', 'blockquote', 'list'],
},
)
md.renderer.rules.video_youtube = (tokens, index) => {
const token = tokens[index]
const content = resolveYoutube(token.meta)
token.content = content
return content
}
}

View File

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

View File

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

View File

@ -0,0 +1,70 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
import type { CanIUseOptions, MarkdownPowerPluginOptions } from '../shared/index.js'
import { caniusePlugin, legacyCaniuse } from './features/caniuse.js'
import { pdfPlugin } from './features/pdf.js'
import { createIconCSSWriter, iconsPlugin } from './features/icons/index.js'
import { bilibiliPlugin } from './features/video/bilibili.js'
import { youtubePlugin } from './features/video/youtube.js'
import { codepenPlugin } from './features/codepen.js'
import { replitPlugin } from './features/replit.js'
const __dirname = getDirname(import.meta.url)
export function markdownPowerPlugin(options: MarkdownPowerPluginOptions = {}): Plugin {
return (app) => {
const { initIcon, addIcon } = createIconCSSWriter(app, options.icons)
return {
name: '@vuepress-plume/plugin-md-power',
clientConfigFile: path.resolve(__dirname, '../client/config.js'),
define: {
__MD_POWER_INJECT_OPTIONS__: options,
},
onInitialized: async () => await initIcon(),
extendsMarkdown(md) {
if (options.caniuse) {
const caniuse = options.caniuse === true ? {} : options.caniuse
// @[caniuse](feature_name)
md.use<CanIUseOptions>(caniusePlugin, caniuse)
// 兼容旧语法
legacyCaniuse(md, caniuse)
}
if (options.pdf) {
// @[pdf](url)
md.use(pdfPlugin)
}
if (options.icons) {
// :[collect:name]:
md.use(iconsPlugin, addIcon)
}
if (options.bilibili) {
// @[bilibili](bvid aid cid)
md.use(bilibiliPlugin)
}
if (options.youtube) {
// @[youtube](id)
md.use(youtubePlugin)
}
if (options.codepen) {
// @[codepen](user/slash)
md.use(codepenPlugin)
}
if (options.replit) {
// @[replit](user/repl-name)
md.use(replitPlugin)
}
},
}
}
}

View File

@ -0,0 +1,6 @@
export type Awaitable<T> = T | Promise<T>
export async function interopDefault<T>(m: Awaitable<T>): Promise<T extends { default: infer U } ? U : T> {
const resolved = await m
return (resolved as any).default || resolved
}

View File

@ -0,0 +1,6 @@
export function parseRect(str: string, unit = 'px'): string {
if (Number.parseFloat(str) === Number(str))
return `${str}${unit}`
return str
}

View File

@ -0,0 +1,41 @@
const RE_ATTR_VALUE = /(?:^|\s+)(?<attr>[\w\d-]+)(?:=\s*(?<quote>['"])(?<value>.+?)\k<quote>)?(?:\s+|$)/
export function resolveAttrs(info: string): {
attrs: Record<string, any>
rawAttrs: string
} {
info = info.trim()
if (!info)
return { rawAttrs: '', attrs: {} }
const attrs: Record<string, string | boolean> = {}
const rawAttrs = info
let matched: RegExpMatchArray | null
// eslint-disable-next-line no-cond-assign
while (matched = info.match(RE_ATTR_VALUE)) {
const { attr, value } = matched.groups || {}
attrs[attr] = value ?? true
info = info.slice(matched[0].length)
}
Object.keys(attrs).forEach((key) => {
let value = attrs[key]
value = typeof value === 'string' ? value.trim() : value
if (value === 'true')
value = true
else if (value === 'false')
value = false
attrs[key] = value
if (key.includes('-')) {
const _key = key.replace(/-(\w)/g, (_, c) => c.toUpperCase())
attrs[_key] = value
}
})
return { attrs, rawAttrs }
}

View File

@ -0,0 +1,11 @@
export function timeToSeconds(time: string): number {
if (!time)
return 0
if (Number.parseFloat(time) === Number(time))
return Number(time)
const [s, m, h] = time.split(':').reverse().map(n => Number(n) || 0)
return s + m * 60 + h * 3600
}

View File

@ -0,0 +1,20 @@
export type CanIUseMode = 'embed' | 'image'
export interface CanIUseTokenMeta {
feature: string
mode: CanIUseMode
versions: string
}
export interface CanIUseOptions {
/**
*
*
* embed iframe嵌入
*
* image
*
* @default 'embed'
*/
mode?: CanIUseMode
}

View File

@ -0,0 +1,11 @@
import type { SizeOptions } from './size'
export interface CodepenTokenMeta extends SizeOptions {
title?: string
user?: string
slash?: string
tab?: string
theme?: string
preview?: boolean
editable?: boolean
}

View File

@ -0,0 +1,19 @@
export interface IconsOptions {
/**
* The prefix of the icon className
* @default 'vp-mdi'
*/
prefix?: string
/**
* The size of the icon
* @default '1em'
*/
size?: string | number
/**
* The color of the icon
* @default 'currentColor'
*/
color?: string
}

View File

@ -0,0 +1,6 @@
export * from './caniuse.js'
export * from './pdf.js'
export * from './icons.js'
export * from './video.js'
export * from './codepen.js'
export * from './plugin.js'

View File

@ -0,0 +1,18 @@
import type { SizeOptions } from './size'
export type PDFEmbedType = 'iframe' | 'embed' | 'pdfjs'
export interface PDFTokenMeta extends SizeOptions {
page?: number
noToolbar?: boolean
zoom?: number
src?: string
title?: string
}
export interface PDFOptions {
/**
* pdfjs url
*/
pdfjsUrl?: string
}

View File

@ -0,0 +1,20 @@
import type { CanIUseOptions } from './caniuse.js'
import type { PDFOptions } from './pdf.js'
import type { IconsOptions } from './icons.js'
export interface MarkdownPowerPluginOptions {
pdf?: boolean | PDFOptions
// new syntax
icons?: boolean | IconsOptions
// video embed
bilibili?: boolean
youtube?: boolean
// code embed
codepen?: boolean
replit?: boolean
caniuse?: boolean | CanIUseOptions
}

View File

@ -0,0 +1,7 @@
import type { SizeOptions } from './size'
export interface ReplitTokenMeta extends SizeOptions {
title?: string
source?: string
theme?: string
}

View File

@ -0,0 +1,5 @@
export interface SizeOptions {
width?: string
height?: string
ratio?: number | string
}

View File

@ -0,0 +1,25 @@
import type { SizeOptions } from './size'
export interface VideoOptions {
bilibili?: boolean
youtube?: boolean
}
export interface BilibiliTokenMeta extends SizeOptions {
title?: string
bvid?: string
aid?: string
cid?: string
autoplay?: boolean
time?: string | number
page?: number
}
export interface YoutubeTokenMeta extends SizeOptions {
title?: string
id: string
autoplay?: boolean
loop?: boolean
start?: string | number
end?: string | number
}

View File

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

View File

@ -52,11 +52,11 @@
"dotenv": "^16.4.5",
"esbuild": "^0.20.2",
"execa": "^8.0.1",
"netlify-cli": "^17.20.1",
"netlify-cli": "^17.21.1",
"portfinder": "^1.0.32"
},
"devDependencies": {
"@types/node": "^20.11.30"
"@types/node": "^20.12.2"
},
"publishConfig": {
"access": "public"

View File

@ -36,15 +36,15 @@
"vuepress": "2.0.0-rc.9"
},
"dependencies": {
"@shikijs/transformers": "^1.2.0",
"@shikijs/twoslash": "^1.2.0",
"@shikijs/transformers": "^1.2.2",
"@shikijs/twoslash": "^1.2.2",
"@types/hast": "^3.0.4",
"floating-vue": "^5.2.2",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-gfm": "^3.0.0",
"mdast-util-to-hast": "^13.1.0",
"nanoid": "^5.0.6",
"shiki": "^1.2.0",
"shiki": "^1.2.2",
"twoslash": "^0.2.5",
"twoslash-vue": "^0.2.5"
},

View File

@ -15,7 +15,8 @@
{ "path": "./plugin-page-collection/tsconfig.build.json" },
{ "path": "./plugin-shikiji/tsconfig.build.json" },
{ "path": "./plugin-content-update/tsconfig.build.json" },
{ "path": "./plugin-search/tsconfig.build.json" }
{ "path": "./plugin-search/tsconfig.build.json" },
{ "path": "./plugin-md-power/tsconfig.build.json" }
],
"files": []
}

442
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,6 @@
"@vuepress-plume/plugin-auto-frontmatter": "workspace:*",
"@vuepress-plume/plugin-baidu-tongji": "workspace:*",
"@vuepress-plume/plugin-blog-data": "workspace:*",
"@vuepress-plume/plugin-caniuse": "workspace:*",
"@vuepress-plume/plugin-content-update": "workspace:*",
"@vuepress-plume/plugin-copy-code": "workspace:*",
"@vuepress-plume/plugin-iconify": "workspace:*",
@ -83,11 +82,12 @@
"@vueuse/core": "^10.9.0",
"bcrypt-ts": "^5.0.2",
"date-fns": "^3.6.0",
"katex": "^0.16.9",
"katex": "^0.16.10",
"lodash.merge": "^4.6.2",
"nanoid": "^5.0.6",
"vue": "^3.4.21",
"vue-router": "4.3.0",
"vuepress-plugin-md-enhance": "2.0.0-rc.32"
"vuepress-plugin-md-enhance": "2.0.0-rc.32",
"vuepress-plugin-md-power": "workspace:*"
}
}

View File

@ -193,7 +193,7 @@ onContentUpdated(() => zoom?.refresh())
@media (min-width: 1440px) {
.plume-page:not(.has-sidebar) .content {
max-width: 784px;
max-width: 884px;
}
.plume-page:not(.has-sidebar) .container {

View File

@ -1,5 +1,5 @@
import { inject, onMounted, provide, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'
import { inject, onMounted, ref } from 'vue'
import type { App, InjectionKey, Ref } from 'vue'
export type DarkModeRef = Ref<boolean>
@ -22,10 +22,18 @@ export function useDarkMode(): DarkModeRef {
* Create dark mode ref and provide as global computed in setup
*/
export function setupDarkMode(): void {
const isDark = ref<boolean>(false)
const isDark = useDarkMode()
onMounted(() => {
if (document.documentElement.classList.contains('dark'))
isDark.value = true
})
provide(darkModeSymbol, isDark)
}
export function injectDarkMode(app: App): void {
const isDark = ref<boolean>(false)
app.provide(darkModeSymbol, isDark)
Object.defineProperty(app.config.globalProperties, '$isDark', {
get: () => isDark,
})
}

View File

@ -60,8 +60,8 @@ export function useLastUpdated() {
const frontmatter = usePageFrontmatter<PlumeThemePageFrontmatter>()
const lang = usePageLang()
const date = computed(() => new Date(page.value.git?.updatedTime ?? ''))
const isoDatetime = computed(() => date.value.toISOString())
const date = computed(() => page.value.git?.updatedTime ? new Date(page.value.git.updatedTime) : null)
const isoDatetime = computed(() => date.value?.toISOString())
const datetime = ref('')
@ -76,13 +76,15 @@ export function useLastUpdated() {
if (frontmatter.value.lastUpdated === false || theme.value.lastUpdated === false)
return
datetime.value = new Intl.DateTimeFormat(
theme.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : undefined,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: 'short',
timeStyle: 'short',
},
).format(date.value)
datetime.value = date.value
? new Intl.DateTimeFormat(
theme.value.lastUpdated?.formatOptions?.forceLocale ? lang.value : undefined,
theme.value.lastUpdated?.formatOptions ?? {
dateStyle: 'short',
timeStyle: 'short',
},
).format(date.value)
: ''
})
})

View File

@ -5,13 +5,15 @@ import type { ClientConfig } from 'vuepress/client'
import { h } from 'vue'
import Badge from './components/global/Badge.vue'
import ExternalLinkIcon from './components/global/ExternalLinkIcon.vue'
import { setupDarkMode, useScrollPromise } from './composables/index.js'
import { injectDarkMode, setupDarkMode, useScrollPromise } from './composables/index.js'
import Layout from './layouts/Layout.vue'
import NotFound from './layouts/NotFound.vue'
import HomeBox from './components/Home/HomeBox.vue'
export default defineClientConfig({
enhance({ app, router }) {
injectDarkMode(app)
// global component
app.component('Badge', Badge)

View File

@ -1,17 +0,0 @@
const colorList = [
'var(--vp-c-brand-1)',
'var(--vp-c-brand-2)',
'var(--vp-c-green-1)',
'var(--vp-c-green-2)',
'var(--vp-c-green-3)',
'var(--vp-c-yellow-1)',
'var(--vp-c-yellow-2)',
'var(--vp-c-yellow-3)',
'var(--vp-c-red-1)',
'var(--vp-c-red-2)',
'var(--vp-c-red-3)',
]
export function getRandomColor() {
return colorList[Math.floor(Math.random() * colorList.length)]
}

View File

@ -10,7 +10,6 @@ import { themeDataPlugin } from '@vuepress/plugin-theme-data'
import { autoFrontmatterPlugin } from '@vuepress-plume/plugin-auto-frontmatter'
import { baiduTongjiPlugin } from '@vuepress-plume/plugin-baidu-tongji'
import { blogDataPlugin } from '@vuepress-plume/plugin-blog-data'
import { caniusePlugin } from '@vuepress-plume/plugin-caniuse'
import { copyCodePlugin } from '@vuepress-plume/plugin-copy-code'
import { iconifyPlugin } from '@vuepress-plume/plugin-iconify'
import { notesDataPlugin } from '@vuepress-plume/plugin-notes-data'
@ -22,6 +21,7 @@ import { seoPlugin } from '@vuepress/plugin-seo'
import { sitemapPlugin } from '@vuepress/plugin-sitemap'
import { contentUpdatePlugin } from '@vuepress-plume/plugin-content-update'
import { searchPlugin } from '@vuepress-plume/plugin-search'
import { markdownPowerPlugin } from 'vuepress-plugin-md-power'
import type {
PlumeThemeEncrypt,
PlumeThemeLocaleOptions,
@ -139,9 +139,6 @@ export function setupPlugins(
}))
}
if (options.caniuse !== false)
plugins.push(caniusePlugin(options.caniuse || { mode: 'embed' }))
if (options.externalLinkIcon !== false) {
plugins.push(externalLinkIconPlugin({
locales: Object.entries(localeOptions.locales || {}).reduce(
@ -205,6 +202,13 @@ export function setupPlugins(
))
}
if (options.markdownPower !== false) {
plugins.push(markdownPowerPlugin({
caniuse: options.caniuse,
...options.markdownPower || {},
}))
}
if (options.comment)
plugins.push(commentPlugin(options.comment))

View File

@ -2,18 +2,20 @@ import type { DocsearchOptions } from '@vuepress/plugin-docsearch'
import type { SearchPluginOptions } from '@vuepress-plume/plugin-search'
import type { AutoFrontmatterOptions } from '@vuepress-plume/plugin-auto-frontmatter'
import type { BaiduTongjiOptions } from '@vuepress-plume/plugin-baidu-tongji'
import type { CanIUsePluginOptions } from '@vuepress-plume/plugin-caniuse'
import type { CopyCodeOptions } from '@vuepress-plume/plugin-copy-code'
import type { ShikiPluginOptions } from '@vuepress-plume/plugin-shikiji'
import type { CommentPluginOptions } from '@vuepress/plugin-comment'
import type { MarkdownEnhanceOptions } from 'vuepress-plugin-md-enhance'
import type { ReadingTimePluginOptions } from '@vuepress/plugin-reading-time'
import type { MarkdownPowerPluginOptions } from 'vuepress-plugin-md-power'
export interface PlumeThemePluginOptions {
/**
* @deprecated `plugin-md-power`
*
* can-i-use
*/
caniuse?: false | CanIUsePluginOptions
caniuse?: false
/**
* external-link-icon
@ -54,6 +56,8 @@ export interface PlumeThemePluginOptions {
markdownEnhance?: false | MarkdownEnhanceOptions
markdownPower?: false | MarkdownPowerPluginOptions
comment?: false | CommentPluginOptions
sitemap?: false

View File

@ -17,6 +17,9 @@
"@vuepress-plume/*/client": ["./plugins/*/src/client/index.ts"],
"vuepress-plugin-netlify-functions": [
"./plugins/plugin-netlify-functions/src/node/index.ts"
],
"vuepress-plugin-md-power": [
"./plugins/plugin-md-power/src/node/index.ts"
]
},
"types": ["webpack-env", "vite/client"]