mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
Merge pull request #60 from pengzhanbo/md-power
vuepress-plugin-md-power
This commit is contained in:
commit
65ac90c094
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -54,6 +54,7 @@
|
||||
"vue"
|
||||
],
|
||||
"cSpell.words": [
|
||||
"bilibili",
|
||||
"bumpp",
|
||||
"caniuse",
|
||||
"colours",
|
||||
|
||||
@ -33,6 +33,12 @@ export const zhNotes = definePlumeNotesConfig({
|
||||
dir: '图表',
|
||||
items: ['chart', 'echarts', 'mermaid', 'flowchart'],
|
||||
},
|
||||
{
|
||||
text: '资源嵌入',
|
||||
icon: 'dashicons:embed-video',
|
||||
dir: '嵌入',
|
||||
items: ['pdf', 'bilibili', 'youtube'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
BIN
docs/.vuepress/public/files/sample.pdf
Normal file
BIN
docs/.vuepress/public/files/sample.pdf
Normal file
Binary file not shown.
@ -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,
|
||||
|
||||
@ -77,8 +77,11 @@ config:
|
||||
title: 加密
|
||||
description: 支持全站加密、部分加密(加密目录、加密文章)。
|
||||
-
|
||||
title: 代码复制
|
||||
description: 一键复制代码块中的内容
|
||||
title: 代码
|
||||
description: 代码复制,CodePen演示,Replit演示
|
||||
-
|
||||
title: 资源嵌入
|
||||
description: 图表,视频,PDF
|
||||
-
|
||||
type: text-image
|
||||
title: 博客
|
||||
|
||||
@ -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 文件中导入文件切片。
|
||||
|
||||
@ -42,3 +42,6 @@ VuePress 是一个 [静态站点生成器](https://en.wikipedia.org/wiki/Static_
|
||||
- 👀 支持 搜索、文章评论
|
||||
- 👨💻 支持 浅色/深色 主题 (包括代码高亮)
|
||||
- 📠 markdown 增强,支持 代码块分组、提示容器、任务列表、数学公式、代码演示 等
|
||||
- 📚 代码演示,支持 CodePen, Replit
|
||||
- 📊 嵌入图标,支持 chart.js,Echarts,Mermaid,flowchart
|
||||
- 🎛 资源嵌入,支持 PDF, bilibili视频,youtube视频等
|
||||
|
||||
95
docs/notes/theme/guide/嵌入/bilibili.md
Normal file
95
docs/notes/theme/guide/嵌入/bilibili.md
Normal 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)
|
||||
122
docs/notes/theme/guide/嵌入/pdf.md
Normal file
122
docs/notes/theme/guide/嵌入/pdf.md
Normal 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)
|
||||
75
docs/notes/theme/guide/嵌入/youtube.md
Normal file
75
docs/notes/theme/guide/嵌入/youtube.md
Normal 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)
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
21
plugins/plugin-md-power/LICENSE
Normal file
21
plugins/plugin-md-power/LICENSE
Normal 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.
|
||||
163
plugins/plugin-md-power/README.md
Normal file
163
plugins/plugin-md-power/README.md
Normal 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`。
|
||||
69
plugins/plugin-md-power/package.json
Normal file
69
plugins/plugin-md-power/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
45
plugins/plugin-md-power/src/client/components/Bilibili.vue
Normal file
45
plugins/plugin-md-power/src/client/components/Bilibili.vue
Normal 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>
|
||||
39
plugins/plugin-md-power/src/client/components/PDFViewer.vue
Normal file
39
plugins/plugin-md-power/src/client/components/PDFViewer.vue
Normal 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>
|
||||
54
plugins/plugin-md-power/src/client/components/Replit.vue
Normal file
54
plugins/plugin-md-power/src/client/components/Replit.vue
Normal 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>
|
||||
41
plugins/plugin-md-power/src/client/components/Youtube.vue
Normal file
41
plugins/plugin-md-power/src/client/components/Youtube.vue
Normal 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>
|
||||
107
plugins/plugin-md-power/src/client/composables/pdf.ts
Normal file
107
plugins/plugin-md-power/src/client/composables/pdf.ts
Normal 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>`
|
||||
}
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
55
plugins/plugin-md-power/src/client/composables/size.ts
Normal file
55
plugins/plugin-md-power/src/client/composables/size.ts
Normal 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 }
|
||||
}
|
||||
37
plugins/plugin-md-power/src/client/config.ts
Normal file
37
plugins/plugin-md-power/src/client/config.ts
Normal 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
|
||||
1
plugins/plugin-md-power/src/client/index.ts
Normal file
1
plugins/plugin-md-power/src/client/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '../shared/index.js'
|
||||
5
plugins/plugin-md-power/src/client/options.ts
Normal file
5
plugins/plugin-md-power/src/client/options.ts
Normal 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__
|
||||
6
plugins/plugin-md-power/src/client/shim.d.ts
vendored
Normal file
6
plugins/plugin-md-power/src/client/shim.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module '*.vue' {
|
||||
import type { ComponentOptions } from 'vue'
|
||||
|
||||
const comp: ComponentOptions
|
||||
export default comp
|
||||
}
|
||||
15
plugins/plugin-md-power/src/client/utils/is.ts
Normal file
15
plugins/plugin-md-power/src/client/utils/is.ts
Normal 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))
|
||||
}
|
||||
6
plugins/plugin-md-power/src/client/utils/link.ts
Normal file
6
plugins/plugin-md-power/src/client/utils/link.ts
Normal 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)
|
||||
}
|
||||
179
plugins/plugin-md-power/src/node/features/caniuse.ts
Normal file
179
plugins/plugin-md-power/src/node/features/caniuse.ts
Normal 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 })
|
||||
}
|
||||
109
plugins/plugin-md-power/src/node/features/codepen.ts
Normal file
109
plugins/plugin-md-power/src/node/features/codepen.ts
Normal 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
|
||||
}
|
||||
}
|
||||
2
plugins/plugin-md-power/src/node/features/icons/index.ts
Normal file
2
plugins/plugin-md-power/src/node/features/icons/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './writer.js'
|
||||
export * from './plugin.js'
|
||||
96
plugins/plugin-md-power/src/node/features/icons/plugin.ts
Normal file
96
plugins/plugin-md-power/src/node/features/icons/plugin.ts
Normal 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))
|
||||
}
|
||||
128
plugins/plugin-md-power/src/node/features/icons/writer.ts
Normal file
128
plugins/plugin-md-power/src/node/features/icons/writer.ts
Normal 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] : '')
|
||||
}
|
||||
97
plugins/plugin-md-power/src/node/features/pdf.ts
Normal file
97
plugins/plugin-md-power/src/node/features/pdf.ts
Normal 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
|
||||
}
|
||||
}
|
||||
93
plugins/plugin-md-power/src/node/features/replit.ts
Normal file
93
plugins/plugin-md-power/src/node/features/replit.ts
Normal 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
|
||||
}
|
||||
}
|
||||
115
plugins/plugin-md-power/src/node/features/video/bilibili.ts
Normal file
115
plugins/plugin-md-power/src/node/features/video/bilibili.ts
Normal 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
|
||||
}
|
||||
}
|
||||
106
plugins/plugin-md-power/src/node/features/video/youtube.ts
Normal file
106
plugins/plugin-md-power/src/node/features/video/youtube.ts
Normal 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
|
||||
}
|
||||
}
|
||||
2
plugins/plugin-md-power/src/node/index.ts
Normal file
2
plugins/plugin-md-power/src/node/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './plugin.js'
|
||||
export * from '../shared/index.js'
|
||||
6
plugins/plugin-md-power/src/node/markdown-it-container.d.ts
vendored
Normal file
6
plugins/plugin-md-power/src/node/markdown-it-container.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module 'markdown-it-container' {
|
||||
import type { PluginWithParams } from 'markdown-it'
|
||||
|
||||
const container: PluginWithParams
|
||||
export = container
|
||||
}
|
||||
70
plugins/plugin-md-power/src/node/plugin.ts
Normal file
70
plugins/plugin-md-power/src/node/plugin.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
6
plugins/plugin-md-power/src/node/utils/package.ts
Normal file
6
plugins/plugin-md-power/src/node/utils/package.ts
Normal 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
|
||||
}
|
||||
6
plugins/plugin-md-power/src/node/utils/parseRect.ts
Normal file
6
plugins/plugin-md-power/src/node/utils/parseRect.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function parseRect(str: string, unit = 'px'): string {
|
||||
if (Number.parseFloat(str) === Number(str))
|
||||
return `${str}${unit}`
|
||||
|
||||
return str
|
||||
}
|
||||
41
plugins/plugin-md-power/src/node/utils/resolveAttrs.ts
Normal file
41
plugins/plugin-md-power/src/node/utils/resolveAttrs.ts
Normal 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 }
|
||||
}
|
||||
11
plugins/plugin-md-power/src/node/utils/timeToSeconds.ts
Normal file
11
plugins/plugin-md-power/src/node/utils/timeToSeconds.ts
Normal 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
|
||||
}
|
||||
20
plugins/plugin-md-power/src/shared/caniuse.ts
Normal file
20
plugins/plugin-md-power/src/shared/caniuse.ts
Normal 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
|
||||
}
|
||||
11
plugins/plugin-md-power/src/shared/codepen.ts
Normal file
11
plugins/plugin-md-power/src/shared/codepen.ts
Normal 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
|
||||
}
|
||||
19
plugins/plugin-md-power/src/shared/icons.ts
Normal file
19
plugins/plugin-md-power/src/shared/icons.ts
Normal 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
|
||||
}
|
||||
6
plugins/plugin-md-power/src/shared/index.ts
Normal file
6
plugins/plugin-md-power/src/shared/index.ts
Normal 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'
|
||||
18
plugins/plugin-md-power/src/shared/pdf.ts
Normal file
18
plugins/plugin-md-power/src/shared/pdf.ts
Normal 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
|
||||
}
|
||||
20
plugins/plugin-md-power/src/shared/plugin.ts
Normal file
20
plugins/plugin-md-power/src/shared/plugin.ts
Normal 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
|
||||
}
|
||||
7
plugins/plugin-md-power/src/shared/replit.ts
Normal file
7
plugins/plugin-md-power/src/shared/replit.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { SizeOptions } from './size'
|
||||
|
||||
export interface ReplitTokenMeta extends SizeOptions {
|
||||
title?: string
|
||||
source?: string
|
||||
theme?: string
|
||||
}
|
||||
5
plugins/plugin-md-power/src/shared/size.ts
Normal file
5
plugins/plugin-md-power/src/shared/size.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface SizeOptions {
|
||||
width?: string
|
||||
height?: string
|
||||
ratio?: number | string
|
||||
}
|
||||
25
plugins/plugin-md-power/src/shared/video.ts
Normal file
25
plugins/plugin-md-power/src/shared/video.ts
Normal 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
|
||||
}
|
||||
8
plugins/plugin-md-power/tsconfig.build.json
Normal file
8
plugins/plugin-md-power/tsconfig.build.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
442
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
: ''
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)]
|
||||
}
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user