feat(plugin-md-power): add demo syntax (#415)

* feat(plugin-md-power): add `demo` syntax
This commit is contained in:
pengzhanbo 2025-01-10 11:17:23 +08:00 committed by GitHub
parent 11db9059c5
commit a9f6bd0a0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 2625 additions and 156 deletions

View File

@ -27,7 +27,7 @@ export async function createPackageJson(
let version = await getPackageManagerVersion(packageManager)
if (version) {
if (packageManager === 'yarn' && version.startsWith('1'))
version = '4.5.0'
version = '4.6.0'
pkg.packageManager = `${packageManager}@${version}`
}
}

View File

@ -106,7 +106,6 @@ export default defineUserConfig({
* @see https://theme-plume.vuejs.press/config/plugins/markdown-enhance/
*/
// markdownEnhance: {
// demo: true,
// chartjs: true,
// echarts: true,
// mermaid: true,
@ -131,6 +130,7 @@ export default defineUserConfig({
// codeSandbox: true, // 启用嵌入 codeSandbox 语法 @[codeSandbox](id)
// jsfiddle: true, // 启用嵌入 jsfiddle 语法 @[jsfiddle](user/id)
// npmTo: true, // 启用 npm-to 容器 ::: npm-to
// demo: true, // 启用 demo 容器 ::: demo
// repl: { // 启用 代码演示容器
// go: true, // ::: go-repl
// rust: true, // ::: rust-repl

View File

@ -254,26 +254,36 @@ const c = a + b
**code demo:**
::: normal-demo Demo 演示
:::: demo title="Demo" desc="A normal demo"
::: code-tabs
@tab HTML
```html
<h1>Hello Word!</h1>
<p><span id="very">Very</span>Powerful!</p>
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
```
@tab Javascript
```js
document.querySelector('#very').addEventListener('click', () => {
alert('Very Powerful')
})
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
```
@tab CSS
```css
span {
color: red;
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
**tab card**

View File

@ -254,26 +254,36 @@ const c = a + b
**代码演示:**
::: normal-demo Demo 演示
:::: demo title="常规示例" desc="一个常规示例"
::: code-tabs
@tab HTML
```html
<h1>Hello Word!</h1>
<p><span id="very">非常</span>强大!</p>
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
```
@tab Javascript
```js
document.querySelector('#very').addEventListener('click', () => {
alert('非常强大')
})
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
```
@tab CSS
```css
span {
color: red;
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
**选项卡:**

View File

@ -24,7 +24,7 @@ export default defineUserConfig({
['meta', { name: 'google-site-verification', content: 'AaTP7bapCAcoO9ZGE67ilpy99GL6tYqtD30tRHjO9Ps' }],
],
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules'],
pagePatterns: ['**/*.md', '!**/*.snippet.md', '!.vuepress', '!node_modules', '!docs/notes/theme/guide/代码演示/demo/*'],
extendsBundlerOptions(bundlerOptions, app) {
addViteOptimizeDepsInclude(bundlerOptions, app, '@simonwep/pickr')

View File

@ -64,7 +64,7 @@ export const themeGuide = defineNoteConfig({
icon: 'carbon:demo',
collapsed: true,
items: [
'前端',
'前端演示',
'rust',
'golang',
'kotlin',
@ -72,6 +72,7 @@ export const themeGuide = defineNoteConfig({
'jsFiddle',
'codeSandbox',
'replit',
{ link: '前端', text: '前端(弃用)' },
],
},
{

View File

@ -36,6 +36,7 @@ export const theme: Theme = plumeTheme({
replit: true,
codeSandbox: true,
jsfiddle: true,
demo: true,
npmTo: ['pnpm', 'yarn', 'npm'],
repl: {
go: true,

View File

@ -348,26 +348,36 @@ const c = a + b
**代码演示:**
::: normal-demo Demo 演示
:::: demo title="常规示例" desc="一个常规示例"
::: code-tabs
@tab HTML
```html
<h1>Hello Word!</h1>
<p><span id="very">非常</span>强大!</p>
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
```
@tab Javascript
```js
document.querySelector('#very').addEventListener('click', () => {
alert('非常强大')
})
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
```
@tab CSS
```css
span {
color: red;
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
**选项卡:**

View File

@ -0,0 +1,5 @@
.btn {
padding: 0 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}

View File

@ -0,0 +1,18 @@
import { defineComponent, h, ref } from 'vue'
import styles from './Counter.module.css'
export default defineComponent({
setup() {
const count = ref(0)
return () => h('div', {
class: 'counter',
}, [
h('p', `计数器:${count.value}`),
h('button', {
type: 'button',
class: styles.btn,
onClick: () => count.value += 1,
}, '+ 1'),
])
},
})

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="counter">
<p>
计数器{{ count }}
</p>
<button type="button" class="btn" @click="count += 1">
+ 1
</button>
</div>
</template>
<style scoped>
.btn {
padding: 0 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { useToggle } from '@vueuse/core'
const [value, toggle] = useToggle()
</script>
<template>
<div>
<p>Value: {{ value ? 'ON' : 'OFF' }}</p>
<div style="display: flex;gap: 16px;">
<button class="btn" @click="toggle()">
Toggle
</button>
<button class="btn" @click="value = true">
Set On
</button>
<button class="btn" @click="value = false">
Set Off
</button>
</div>
</div>
</template>
<style scoped>
.btn {
padding: 0 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,30 @@
<div id="app">
<h3>vuepress-theme-plume</h3>
<p id="message"></p>
<datetime id="datetime"></datetime>
</div>
<script>
$('#message', document).text('So Awesome!')
const datetime = $('#datetime', document)
setInterval(() => {
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
}, 1000)
</script>
<style lang="css">
#app {
font-size: 2em;
text-align: center;
}
</style>
<script type="config">
{
"jsLib": [
"https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js",
"https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js"
],
"cssLib": ["https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"]
}
</script>

View File

@ -0,0 +1,16 @@
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
<script lang="ts">
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
</script>
<style lang="css">
#app {
font-size: 2em;
text-align: center;
}
</style>

View File

@ -1,11 +1,26 @@
---
title: 前端
author: pengzhanbo
icon: icon-park-outline:html-five
createTime: 2024/04/04 11:39:05
permalink: /guide/repl/frontend/
permalink: /guide/repl/frontend-deprecated/
badge:
text: 弃用
type: danger
---
::: caution 弃用说明
[vuepress-plugin-md-enhance](https://plugin-md-enhance.vuejs.press/zh/) 在 未来的版本会将 `demo` 相关的
功能迁移至 [vuepress/ecosystem](https://github.com/vuepress/ecosystem), 详细进度请查看 [ecosystem#293](https://github.com/vuepress/ecosystem/pull/293) 。
但在基于用户反馈后发现,此功能在实际场景中使用表现不符合预期。存在以下问题:
- `vue-demo` 仅能用于简单的组件演示,不能支持如 组件库、`composables-api` 等外部依赖的演示。
- 演示代码仅能内联在 markdown 文件中,对于代码量较大的演示的编写体验不佳。
- 在浏览器运行时通过异步加载 `babel` 转换源代码,一方面需要额外的等待时间,另一方面对于企业内网环境可能不支持。
主题重新对 前端代码演示 功能进行了重新设计和重构,请查看新的 [前端代码演示](./前端演示.md) 。
:::
## 概述
前端代码演示 由 [vuepress-plugin-md-enhance](https://plugin-md-enhance.vuejs.press/zh/) 提供支持。

View File

@ -0,0 +1,807 @@
---
title: 前端演示
icon: icon-park-outline:html-five
createTime: 2025/01/08 21:34:26
permalink: /guide/repl/frontend/
badge:
text: 1.0.0-rc.127 +
---
::: important [旧的前端代码演示](./前端.md) 已弃用,请迁移至此新的方案。
旧的方案由 [vuepress-plugin-md-enhance](https://plugin-md-enhance.vuejs.press/zh/) 提供,感谢在过去
提供的代码演示的支持,在 `vuepress-plugin-md-enhance` 中代码演示功能也将迁移至 [vuepress/ecosystem](https://github.com/vuepress/ecosystem)
详情请查看 [vuepress/ecosystem#293](https://github.com/vuepress/ecosystem/pull/293) 。
:::
::: details 为什么要重新设计?
前端代码演示是一个很有用的功能,但是在旧的方案中,所实现的功能与实际使用场景预期不符。
旧的方案中,比如 `vue-demo` 仅能支持一些简单的 vue 组件演示,且不能直接导入项目中的依赖,仅能通过
加载外部脚本支持更多功能,且并没提供对 `vue sfc` 的完全支持,仅能进行简单的代码演示。
而且对脚本代码的编译是在浏览器运行时,先从 CDN 请求加载 `babel`,完成后再通过 `babel` 进行转换,
这需要额外的等待时间完成,同时对于企业内部的项目,在内网环境中无法请求外部资源,导致演示无法正常展示。
在新的方案中,所有的演示代码均是在 nodejs 运行时进行编译转换,因此在浏览器运行时可直接展示演示代码,无需额外的等待时间。
且得益于 nodejs 强大的能力,可以完全支持 `vue sfc` 的完整功能,且可以直接导入项目中的依赖,让你的演示更加丰富。
更符合实际的使用场景。
:::
## 概述
此功能支持在 页面中 嵌入 代码演示 功能。前端代码演示由两个主要区域组成:
==渲染区== 和 ==代码区== 。
其中,**渲染区** 用于展示代码的执行结果,包括 UI 渲染和 交互;**代码区** 用于展示源代码,默认是折叠的。
主题提供了 三种不同的 前端代码演示支持:
- ==vue 组件演示== 支持 `vue` 组件的演示,像编写一个 `vue` 组件一样编写你的演示代码,可以用于演示如 组件库、`composables-api` 等外部依赖。
- ==markdown 演示==:支持 `markdown` 的演示。
- ==普通代码演示== :支持原生的 `HTML` + `JS/TS` + `CSS/Less/Sass/Stylus` 的代码演示,像编写一个网页一样编写你的演示代码。
主题还提供了 两种不同的使用方式编写演示代码:
- 嵌入演示代码文件:
```md
@[demo type](url)
```
可以通过简单的嵌入语法,从文件中导入演示代码。
- demo 容器内联演示代码:
````md
::: demo type
``` [lang]
code
```
:::
````
直接在 markdown 文件中编写演示代码,使用 `demo` 容器包裹即可。
## 配置
前端代码演示 由 [vuepress-plugin-md-power](../../config/plugins/markdownPower.md) 提供支持。
前端 代码演示 默认不启用,你可以通过配置来启用它。
::: code-tabs
@tab .vuepress/config.ts
```ts
export default defineUserConfig({
theme: plumeTheme({
plugins: {
markdownPower: {
demo: true, // 启用新的代码演示功能 // [!code ++]
},
markdownEnhance: {
demo: false, // 禁用旧的代码演示功能 // [!code warning]
}
}
})
})
```
:::
## 语言支持
代码演示支持以下 语言:
- javascript
- typescript
- html
- css
- less
- sass
- stylus
对于 css 预处理语言,你需要在项目中安装对应的预处理器,如 `less``sass``stylus` 等。
## 嵌入语法
不同的代码演示均使用相同的嵌入语法,你可以快速掌握它们的使用方法。
```md
<!-- 语法 -->
@[demo](url)
@[demo [type]](url)
@[demo [type] title="" desc="" expanded code-setting=""](url)
```
`@[demo](url)` 是一个固定的语法格式。
`[type]` 表示类型,支持 三个不同的值:
- `normal`: 普通代码演示类型。当不传入 `[type]` 参数时,默认为 `normal` 类型。
- `vue`: vue 组件演示类型。
- `markdown`: markdown 演示类型。
`url` 表示演示代码文件的路径,可以是相对路径或绝对路径,
- 相对路径,以 `./``../` 开头,表示相对于当前的 markdown 文件路径。
- 绝对路径,以 `/` 开头,表示从 [vuepress 源目录路径](../项目结构.md#文档源目录) 开始。
```md
<!-- 普通代码演示 -->
@[demo](./demo/normal.html)
@[demo normal](./demo/normal.html)
@[demo](/.vuepress/demo/normal.html)
<!-- vue 组件演示 -->
@[demo vue](./demo/Counter.vue)
@[demo vue](./demo/Counter.ts)
@[demo](/.vuepress/demo/Counter.vue)
<!-- markdown 演示 -->
@[demo markdown](./demo/example.md)
@[demo markdown](/.vuepress/demo/example.md)
```
其它额外参数:
- `title="xxx"` :演示标题
- `desc="xxx"`:演示描述
- `expanded`:展开代码区域
- `code-setting="xxx"`:代码设置,值将被拼接在 ` ``` [lang]` 之后,用于给代码块添加配置。
`code-setting=":lines-number"`,则会在代码块后面添加 `:lines-number`,使代码块支持显示行号。
`code-setting=":collapsed-lines=10"`,则会在代码块后面添加 `:collapsed-lines=10`,使代码块从第 210行开始折叠。
```md
@[demo vue expanded title="标题" desc="描述" code-setting=":collapsed-lines=10"](./demo/Counter.vue)
```
## demo 容器内联演示
demo 容器内联演示 使用 `demo` 容器包裹演示代码,可以在 markdown 文件中快速地编写演示代码,如下:
```md
::: demo [type] title="" desc="" expanded
<!-- 代码块 -->
:::
```
所有参数与 `@[demo](url)` 语法相同。
````md
<!-- 普通代码演示 -->
::: demo
```html
<!-- html 代码 -->
```
``` js
// js 代码
```
``` css
/* css 代码 */
```
:::
<!-- vue 组件演示 -->
::: demo vue
``` vue
<!-- vue 代码 -->
```
:::
<!-- markdown 演示 -->
::: demo markdown
``` md
<!-- markdown 代码 -->
```
:::
````
还可以在 `::: demo` 容器中 使用 `::: code-tabs` 容器包裹代码块,以获得更好的交互效果。
````md
:::: demo
::: code-tabs
@tab HTML
```html
<!-- html 代码 -->
```
@tab javascript
``` js
// js 代码
```
@tab css
``` css
/* css 代码 */
```
::::
````
当期望使用 Typescript 或 `Less/Sass/Stylus` 时,通过修改 ` ``` [lang]` 的值即可:
````md
:::: demo
::: code-tabs
@tab HTML
```html
<!-- html 代码 -->
```
@tab Typescript
``` ts
// ts 代码
```
@tab Scss
``` scss
/* scss 代码 */
```
::::
````
## vue 组件演示
vue 组件演示 是一个很强大的功能,对于演示代码不做任何限制,这甚至完全取决于 `bundler` 对于 vue 的支持。
你还可以直接在演示代码中导入项目中安装的依赖,就像你在写一个 vue 项目的组件一样。
因此,你可以直接使用它来为 你的组件库 提供演示示例,或者为你的 `composables-api` 提供演示示例。
### 嵌入语法
你可以直接使用以下方式在页面中嵌入一个 vue 组件演示:
**输入:**
```md
@[demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"](./demo/Counter.vue)
```
::: details 查看 `./demo/Counter.vue` 代码
@[code](./demo/Counter.vue)
:::
**输出:**
@[demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"](./demo/Counter.vue)
---
也可以嵌入一个 `.ts` 编写的 vue 组件:
**输入:**
```md
@[demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"](./demo/Counter.ts)
```
::: details 查看 `./demo/Counter.ts` 代码
::: code-tabs
@tab Counter.ts
@[code](./demo/Counter.ts)
@tab Counter.module.css
@[code](./demo/Counter.module.css)
:::
**输出:**
@[demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"](./demo/Counter.ts)
:::info 对于 `.js/.ts` 编写的组件,请使用 `css module` 来编写样式以实现样式隔离
:::
---
可以在演示代码中导入外部依赖,
以导入 `@vueuse/core` 中的 `useToggle()` 为例:
**输入:**
```md
@[demo vue title="useToggle" desc="useToggle() 演示"](./demo/Toggle.vue)
```
::: details ./demo/Toggle.vue
@[code](./demo/Toggle.vue)
:::
**输出:**
@[demo vue title="useToggle" desc="useToggle() 演示"](./demo/Toggle.vue)
### 容器语法
在 markdown 文件中使用 `demo` 容器包裹演示代码,可以快速地编写演示代码,如下:
**输入:**
:::: details 展开查看完整代码
````md
::: demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"
```vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="counter">
<p>计数器:{{ count }}</p>
<button type="button" class="btn" @click="count += 1">
+ 1
</button>
</div>
</template>
<style scoped>
.btn {
padding: 0 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
</style>
```
:::
````
::::
**输出:**
::: demo vue title="计数器" desc="点击 +1 按钮,计数器自增 1"
```vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="counter">
<p>计数器:{{ count }}</p>
<button type="button" class="btn" @click="count += 1">
+ 1
</button>
</div>
</template>
<style scoped>
.btn {
padding: 0 8px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
</style>
```
:::
:::::: warning
vue demo 容器语法虽然也支持 使用 `.js/ts + css` 的方式来嵌入演示代码,
但主题不推荐这样做。因为 样式无法被隔离,这可能导致样式污染。
::::: details 参考示例
````md
:::: demo vue title="标题" desc="描述"
::: code-tabs
@tab Counter.ts
```ts
import { defineComponent, ref } from 'vue'
export default defineComponent({
// code
})
```
@tab Counter.css
```css
/* css code */
```
:::
::::
````
:::::
::::::
## 普通代码演示
普通代码演示支持 `html``css/less/sass/stylus``js/ts` 语言。
适合于相对简单的代码演示,比如 一个样式渲染效果,一个交互效果,一个功能 等。
普通代码演示还支持跳转到 `codePen``jsFiddle` 中查看。
同时,也支持通过 外部链接 的方式引入 第三方的库,比如 `jQuery` `dayjs` 等。
::: important `document` 的差异
普通代码演示 的代码 运行在 `ShadowDOM` 中,从而实现与 站点其他内容的隔离。避免对环境的污染。
因此,在普通演示的脚本代码中,**全局对象 `document` 指向的是 `ShadowDOM`** ,请着重注意此差异。
如果您需要使用 浏览器的全局对象,请使用 `window.document` 代替 `document`
如果引入了如 `JQuery` 库,由于此差异,`$(selector)` 的行为会发生变化,要查询 `ShadowDOM` 中的元素,
需要使用 `$(selector, document)`,即在第二个参数中传入 `document` 作为查询的上下文。
:::
::: warning 不建议过于复杂的演示。
:::
### 嵌入语法
使用嵌入语法时,对于导入的 代码演示文件,使用 `.html` 作为文件后缀。在 `.html` 文件中,
你可以像编写一个 HTML 页面一样编写 演示代码:
```html
<!-- html 代码 -->
<div id="app">
演示内容
<div>
<!-- 脚本内容,使用 lang 属性设置语言, 默认为 js -->
<script lang="ts">
</script>
<!-- 样式内容,使用 lang 属性设置语言, 默认为 css -->
<style lang="css">
</style>
<!-- 可选的配置文件 json 格式 -->
<script type="config">
{
"jsLib": [],
"cssLib": []
}
</script>
```
每一个区域的内容都是可选的。但请注意,不支持存在多个相同的区域。区域的顺序无要求。
除了 `<script>``<style>` 之外的内容,都被认为是 HTML 代码。
你可以在 `<script type="config"></script>` 内使用 `json` 格式声明要加载的其他依赖资源。
比如,加载 `jQuery` 以及 `normalize.css`:
```html
<div>xxxx</div>
<script type="config">
{
"jsLib": [
"https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
],
"cssLib": [
"https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"
]
}
</script>
```
---
一个常规的示例:
**输入:**
```md
@[demo title="示例" desc="这是一个常规演示"](./demo/normal.html)
```
::: details 查看 `./demo/normal.html`代码
@[code](./demo/normal.html)
:::
**输出:**
@[demo title="示例" desc="这是一个常规演示"](./demo/normal.html)
---
引入 `jQuery` , `dayjs``normalize.css` 的示例:
**输入:**
```md
@[demo title="示例" desc="这是一个常规演示"](./demo/normal-lib.html)
```
::: details 查看 `./demo/normal-lib.html`代码
@[code](./demo/normal-lib.html)
:::
**输出:**
@[demo title="示例" desc="这是一个常规演示"](./demo/normal-lib.html)
### 容器语法
在 markdown 文件中使用 demo 容器包裹演示代码,可以快速地编写演示代码,如下:
:::: details 展开查看完整示例代码
````md
::: demo title="示例" desc="描述" expanded
```json
{
"jsLib": [],
"cssLib": []
}
```
```html
<!-- html 代码 -->
```
```js
// js 代码
```
```css
/* css 代码 */
```
:::
```
::::
还可以在 `::: demo` 中包裹 `::: code-tabs` 以获得更好的代码块展示效果:
::::: details 展开查看完整示例代码
````md
:::: demo title="示例" desc="描述" expanded
```json
{
"jsLib": [],
"cssLib": []
}
```
::: code-tabs
@tab HTML
```html
<!-- html 代码 -->
```
@tab Javascript
```js
// js 代码
```
@tab CSS
```css
/* css 代码 */
```
:::
::::
```
:::::
---
一个常规的 容器示例:
**输入:**
::::: details 展开查看完整示例代码
````md
:::: demo title="常规示例" desc="一个常规示例"
::: code-tabs
@tab HTML
```html
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
```
@tab Javascript
```js
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
```
@tab CSS
```css
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
````
:::::
**输出:**
:::: demo title="常规示例" desc="一个常规示例"
::: code-tabs
@tab HTML
```html
<div id="app">
<h3>vuepress-theme-plume</h3>
</div>
```
@tab Javascript
```js
const a = 'So Awesome!'
const app = document.querySelector('#app')
app.appendChild(window.document.createElement('small')).textContent = a
```
@tab CSS
```css
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
---
引入 jQuery , dayjs 和 normalize.css 的示例:
**输入:**
::::: details 展开查看完整示例代码
````md
:::: demo title="常规示例" desc="一个常规示例"
```json
{
"jsLib": [
"https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js",
"https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js"
],
"cssLib": ["https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"]
}
```
::: code-tabs
@tab HTML
```html
<div id="app">
<h3>vuepress-theme-plume</h3>
<p id="message"></p>
<datetime id="datetime"></datetime>
</div>
```
@tab Javascript
```js
$('#message', document).text('So Awesome!')
const datetime = $('#datetime', document)
setInterval(() => {
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
}, 1000)
```
@tab CSS
```css
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
````
:::::
**输出:**
:::: demo title="常规示例" desc="一个常规示例"
```json
{
"jsLib": [
"https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js",
"https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js"
],
"cssLib": ["https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css"]
}
```
::: code-tabs
@tab HTML
```html
<div id="app">
<h3>vuepress-theme-plume</h3>
<p id="message"></p>
<datetime id="datetime"></datetime>
</div>
```
@tab Javascript
```js
$('#message', document).text('So Awesome!')
const datetime = $('#datetime', document)
setInterval(() => {
datetime.text(dayjs().format('YYYY-MM-DD HH:mm:ss'))
}, 1000)
```
@tab CSS
```css
#app {
font-size: 2em;
text-align: center;
}
```
:::
::::
## Markdown 演示
在页面中演示 markdown 源代码 和渲染结果。
### 嵌入语法
**输入:**
```md
@[demo markdown title="公告板" desc="公告板代码示例"](/.vuepress/bulletin.md)
```
::: details 展开查看 `/.vuepress/bulletin.md` 代码
@[code](../../../../.vuepress/bulletin.md)
:::
**输出:**
@[demo markdown title="公告板" desc="公告板代码示例"](/.vuepress/bulletin.md)
### 容器语法
**输入:**
:::::: details 展开查看完整代码
````md
:::: demo markdown title="公告板" desc="公告板代码示例"
```md
::: center
**QQ 交流群:** [792882761](https://qm.qq.com/q/FbPPoOIscE)
![QQ qr_code](/images/qq_qrcode.png){width="618" height="616" style="width: 200px"}
您在使用过程中遇到任何问题,欢迎通过 [issue](https://github.com/pengzhanbo/vuepress-theme-plume/issues/new/choose) 反馈。也欢迎加入我们的 QQ 交流群一起讨论。
:::
```
::::
````
::::::
**输出:**
:::: demo markdown title="公告板" desc="公告板代码示例"
```md
::: center
**QQ 交流群:** [792882761](https://qm.qq.com/q/FbPPoOIscE)
![QQ qr_code](/images/qq_qrcode.png){width="618" height="616" style="width: 200px"}
您在使用过程中遇到任何问题,欢迎通过 [issue](https://github.com/pengzhanbo/vuepress-theme-plume/issues/new/choose) 反馈。也欢迎加入我们的 QQ 交流群一起讨论。
:::
```
::::

4
docs/shim.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.module.css' {
const classes: { [key: string]: string }
export default classes
}

View File

@ -44,9 +44,11 @@
"@commitlint/config-conventional": "^19.6.0",
"@pengzhanbo/eslint-config-vue": "^1.22.1",
"@pengzhanbo/stylelint-config": "^1.22.1",
"@types/less": "^3.0.7",
"@types/lodash.merge": "^4.6.9",
"@types/minimist": "^1.2.5",
"@types/node": "^22.10.5",
"@types/stylus": "^0.48.43",
"@types/webpack-env": "^1.18.5",
"@vitest/coverage-istanbul": "^2.1.8",
"bumpp": "^9.9.2",
@ -57,12 +59,14 @@
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.17.0",
"husky": "^9.1.7",
"less": "^4.2.1",
"lint-staged": "^15.3.0",
"markdown-it": "^14.1.0",
"memfs": "^4.15.3",
"minimist": "^1.2.8",
"rimraf": "^6.0.1",
"stylelint": "^16.12.0",
"stylus": "^0.64.0",
"tsconfig-vuepress": "^5.2.1",
"tsup": "^8.3.5",
"typescript": "^5.7.2",

View File

@ -42,9 +42,14 @@
"peerDependencies": {
"artplayer": "^5.2.0",
"dashjs": "^4.7.4",
"esbuild": "^0.24.2",
"hls.js": "^1.5.18",
"less": "^4.2.1",
"markdown-it": "^14.0.0",
"mpegts.js": "1.7.3",
"sass": "^1.83.0",
"sass-embedded": "^1.83.0",
"stylus": "0.64.0",
"vuepress": "catalog:"
},
"peerDependenciesMeta": {
@ -74,8 +79,10 @@
"@mdit/plugin-tasklist": "^0.14.0",
"@vuepress/helper": "catalog:",
"@vueuse/core": "catalog:",
"chokidar": "catalog:",
"image-size": "^1.2.0",
"local-pkg": "catalog:",
"lru-cache": "^11.0.2",
"markdown-it-container": "^4.0.0",
"nanoid": "catalog:",
"shiki": "^1.26.1",

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { ref } from 'vue'
import '../styles/demo.css'
const props = defineProps<{
type?: 'vue' | 'markdown'
title?: string
desc?: string
expanded?: boolean
}>()
const showCode = ref(props.expanded ?? true)
function toggleCode() {
showCode.value = !showCode.value
}
</script>
<template>
<div class="vp-demo-wrapper">
<div class="demo-draw">
<slot />
</div>
<div v-if="title || desc" class="demo-info">
<p v-if="title" class="title">
{{ title }}
</p>
<p v-if="desc" class="desc">
{{ desc }}
</p>
</div>
<div class="demo-ctrl">
<span class="vpi-demo-code" @click="toggleCode" />
</div>
<div v-show="showCode" class="demo-code">
<slot name="code" />
</div>
</div>
</template>

View File

@ -0,0 +1,193 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { computed, onMounted, ref, useId, useTemplateRef, watch } from 'vue'
import { loadScript, loadStyle } from '../utils/shared.js'
import Loading from './icons/Loading.vue'
import '../styles/demo.css'
const props = defineProps<{
title?: string
desc?: string
expanded?: boolean
config?: {
html: string
css: string
script: string
jsLib: string[]
cssLib: string[]
}
}>()
const draw = useTemplateRef<HTMLDivElement>('draw')
const id = useId()
const loaded = ref(true)
const resourcesEl = useTemplateRef<HTMLDivElement>('resourcesEl')
const resources = computed<{
name: string
items: { name: string, url: string }[]
}[]>(() => {
if (!props.config)
return []
return [
{ name: 'JavaScript', items: props.config.jsLib.map(url => ({ name: normalizeName(url), url })) },
{ name: 'CSS', items: props.config.cssLib.map(url => ({ name: normalizeName(url), url })) },
].filter(i => i.items.length)
})
function normalizeName(url: string) {
return url.slice(url.lastIndexOf('/') + 1)
}
const showResources = ref(false)
function toggleResources() {
showResources.value = !showResources.value
}
onClickOutside(resourcesEl, () => {
showResources.value = false
})
onMounted(() => {
if (!draw.value)
return
const root = draw.value.attachShadow({ mode: 'open' })
watch(() => props.config, async () => {
root.innerHTML = props.config?.html ?? ''
props.config?.cssLib?.forEach(url => loadStyle(url, root))
if (props.config?.css) {
const style = document.createElement('style')
style.innerHTML = props.config?.css ?? ''
root.appendChild(style)
}
if (props.config?.jsLib?.length) {
loaded.value = false
await Promise.all(props.config.jsLib.map(url => loadScript(url)))
.catch(e => console.warn(e))
loaded.value = true
}
if (props.config?.script) {
const script = document.createElement('script')
script.type = 'text/javascript'
script.innerHTML = `;(function(document){\n${props.config.script}\n})(document.querySelector('#VPDemoNormalDraw${id}').shadowRoot);`
root.appendChild(script)
}
}, { immediate: true })
})
const fence = useTemplateRef<HTMLDivElement>('fence')
const data = ref<{
js: string
css: string
html: string
jsType: string
cssType: string
}>({ js: '', css: '', html: '', jsType: '', cssType: '' })
onMounted(() => {
if (!fence.value)
return
data.value.html = props.config?.html ?? ''
const els = Array.from(fence.value.querySelectorAll('div[class*="language-"]'))
for (const el of els) {
const lang = el.className.match(/language-(\w+)/)?.[1] ?? ''
const content = el.querySelector('pre')?.textContent ?? ''
if (lang === 'js' || lang === 'javascript') {
data.value.js = content
data.value.jsType = 'js'
}
if (lang === 'ts' || lang === 'typescript') {
data.value.js = content
data.value.jsType = 'ts'
}
if (lang === 'css' || lang === 'scss' || lang === 'less' || lang === 'stylus' || lang === 'styl') {
data.value.css = content
data.value.cssType = lang === 'styl' ? 'stylus' : lang
}
}
})
const showCode = ref(props.expanded ?? false)
function toggleCode() {
showCode.value = !showCode.value
}
</script>
<template>
<div class="vp-demo-wrapper normal">
<div class="demo-draw">
<Loading v-if="!loaded" />
<div :id="`VPDemoNormalDraw${id}`" ref="draw" />
</div>
<div v-if="title || desc" class="demo-info">
<p v-if="title" class="title">
{{ title }}
</p>
<p v-if="desc" class="desc">
{{ desc }}
</p>
</div>
<div class="demo-ctrl">
<div class="extra">
<form action="https://codepen.io/pen/define" method="POST" target="_blank" enctype="application/x-www-form-urlencoded;charset=utf-8">
<input
type="hidden" name="data" :value="JSON.stringify({
title: title || 'Demo',
description: desc || '',
html: data.html,
css: data.css,
js: data.js,
js_pre_processor: data.jsType === 'ts' ? 'typescript' : 'none',
css_pre_processor: data.cssType,
css_external: config?.cssLib?.join(';'),
js_external: config?.jsLib?.join(';'),
})"
>
<button type="submit" title="CodePen" aria-label="CodePen">
<span class="vpi-demo-codepen" />
</button>
</form>
<form action="https://jsfiddle.net/api/post/library/pure/" method="POST" target="_blank" enctype="application/x-www-form-urlencoded;charset=UTF-8" accept-charset="UTF-8">
<button type="submit" title="jsFiddle" aria-label="jsFiddle">
<span class="vpi-demo-jsfiddle bg" />
</button>
<input type="hidden" name="wrap" value="b">
<input type="hidden" name="html" :value="data.html">
<input type="hidden" name="js" :value="data.js">
<input type="hidden" name="css" :value="data.cssType === 'scss' || data.cssType === 'css' ? data.css : config?.css || ''">
<input type="hidden" name="panel_css" :value="data.cssType === 'scss' ? 1 : 0">
<input type="hidden" name="panel_js" :value="data.jsType === 'ts' ? 4 : 0">
<input type="hidden" name="title" :value="title || 'Demo'">
<input type="hidden" name="description" :value="desc || ''">
<input type="hidden" name="resources" :value="[...(config?.jsLib || []), ...(config?.cssLib || [])].join(',')">
</form>
</div>
<div v-if="resources.length" class="demo-resources">
<span ref="resourcesEl" class="vpi-demo-resources" title="Resources" aria-label="Resources" @click="toggleResources" />
<Transition name="fade">
<div v-show="showResources" class="demo-resources-container">
<div v-for="{ name, items } in resources" :key="name" class="demo-resources-list">
<p>{{ name }}</p>
<ul v-for="item in items" :key="item.url">
<li>
<a :href="item.url" target="_blank" rel="noopener noreferrer" class="no-icon" aria-label="{{ item.name }}">{{ item.name }}</a>
</li>
</ul>
</div>
</div>
</Transition>
</div>
<span class="vpi-demo-code" @click="toggleCode" />
</div>
<div v-show="showCode" ref="fence" class="demo-code">
<slot />
</div>
</div>
</template>

View File

@ -0,0 +1,157 @@
.vp-demo-wrapper {
margin: 16px 0;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
transition: border-color var(--vp-t-color);
}
.vp-demo-wrapper .demo-draw {
padding: 24px;
}
.vp-demo-wrapper .demo-info .title {
display: flex;
align-items: center;
margin-top: 0;
margin-bottom: 8px;
font-size: 18px;
font-weight: bolder;
}
.vp-demo-wrapper .demo-info .title::before {
display: inline-block;
width: 16px;
height: 0;
margin-right: 8px;
content: "";
border-top: 1px solid var(--vp-c-divider);
transition: border-color var(--vp-t-color);
}
.vp-demo-wrapper .demo-info .title::after {
display: inline-block;
flex: 1;
height: 0;
margin-left: 8px;
content: "";
border-top: 1px solid var(--vp-c-divider);
transition: border-color var(--vp-t-color);
}
.vp-demo-wrapper .demo-info .desc {
padding: 0 24px;
margin-top: 8px;
}
.vp-demo-wrapper .demo-info p:last-child {
margin-bottom: 16px;
}
.vp-demo-wrapper .demo-ctrl {
display: flex;
gap: 16px;
justify-content: flex-end;
padding: 8px 24px;
border-top: 1px dotted var(--vp-c-divider);
transition: border-color var(--vp-t-color);
}
.vp-demo-wrapper .demo-ctrl .extra {
display: flex;
flex: 1;
gap: 16px;
align-items: center;
justify-content: flex-start;
}
.vp-demo-wrapper .demo-ctrl [class*="vpi-"] {
font-size: 20px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: color var(--vp-t-color);
}
.vp-demo-wrapper .demo-ctrl [class*="vpi-"]:hover {
color: var(--vp-c-text-1);
}
.vp-demo-wrapper .demo-ctrl form,
.vp-demo-wrapper .demo-ctrl button {
padding: 0;
margin: 0;
line-height: 1;
}
.vp-demo-wrapper .demo-resources {
position: relative;
}
.vp-demo-wrapper .demo-code {
border-top: 1px solid var(--vp-c-divider);
transition: border-color var(--vp-t-color);
}
.vp-demo-wrapper .demo-code div[class*="language-"],
.vp-demo-wrapper .demo-code .vp-code-tabs-nav {
margin: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.vp-demo-wrapper .demo-code > div[class*="language-"]:not(:last-of-type) {
border-bottom: 2px dotted var(--vp-c-divider);
border-radius: 0;
}
.vp-demo-wrapper .demo-code > div[class*="language-"] + div[class*="language-"] {
margin-top: 0;
}
.vp-demo-wrapper .demo-resources-container {
position: absolute;
top: 100%;
right: -24px;
z-index: 10;
width: max-content;
padding: 8px 12px;
font-size: 14px;
background-color: var(--vp-c-bg);
border: solid 1px var(--vp-c-divider);
border-radius: 8px;
box-shadow: var(--vp-shadow-2);
transition: var(--vp-t-color);
transition-property: border, box-shadow, background-color;
}
.vp-demo-wrapper .demo-resources-container .demo-resources-list > p {
margin: 0;
line-height: 20px;
color: var(--vp-c-text-3);
transition: color var(--vp-t-color);
}
.vp-demo-wrapper .demo-resources-container .demo-resources-list:not(:first-of-type) {
margin-top: 8px;
}
.vp-demo-wrapper .demo-resources-container .demo-resources-list ul {
padding: 0;
margin: 0;
list-style: none;
}
.vpi-demo-code {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M14.18 4.276a.75.75 0 0 1 .531.918l-3.973 14.83a.75.75 0 0 1-1.45-.389l3.974-14.83a.75.75 0 0 1 .919-.53m2.262 3.053a.75.75 0 0 1 1.059-.056l1.737 1.564c.737.662 1.347 1.212 1.767 1.71c.44.525.754 1.088.754 1.784c0 .695-.313 1.258-.754 1.782c-.42.499-1.03 1.049-1.767 1.711l-1.737 1.564a.75.75 0 0 1-1.004-1.115l1.697-1.527c.788-.709 1.319-1.19 1.663-1.598c.33-.393.402-.622.402-.818s-.072-.424-.402-.817c-.344-.409-.875-.89-1.663-1.598l-1.697-1.527a.75.75 0 0 1-.056-1.06m-8.94 1.06a.75.75 0 1 0-1.004-1.115L4.761 8.836c-.737.662-1.347 1.212-1.767 1.71c-.44.525-.754 1.088-.754 1.784c0 .695.313 1.258.754 1.782c.42.499 1.03 1.049 1.767 1.711l1.737 1.564a.75.75 0 0 0 1.004-1.115l-1.697-1.527c-.788-.709-1.319-1.19-1.663-1.598c-.33-.393-.402-.622-.402-.818s.072-.424.402-.817c.344-.409.875-.89 1.663-1.598z'/%3E%3C/svg%3E");
}
.vpi-demo-codepen {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1024' height='1024' viewBox='0 0 1024 1024'%3E%3Cpath fill='%23000' d='m911.7 385.3l-.3-1.5c-.2-1-.3-1.9-.6-2.9c-.2-.6-.4-1.1-.5-1.7c-.3-.8-.5-1.7-.9-2.5c-.2-.6-.5-1.1-.8-1.7c-.4-.8-.8-1.5-1.2-2.3c-.3-.5-.6-1.1-1-1.6c-.8-1.2-1.7-2.4-2.6-3.6c-.5-.6-1.1-1.3-1.7-1.9c-.4-.5-.9-.9-1.4-1.3c-.6-.6-1.3-1.1-1.9-1.6c-.5-.4-1-.8-1.6-1.2c-.2-.1-.4-.3-.6-.4L531.1 117.8a34.3 34.3 0 0 0-38.1 0L127.3 361.3c-.2.1-.4.3-.6.4c-.5.4-1 .8-1.6 1.2c-.7.5-1.3 1.1-1.9 1.6c-.5.4-.9.9-1.4 1.3c-.6.6-1.2 1.2-1.7 1.9c-1 1.1-1.8 2.3-2.6 3.6c-.3.5-.7 1-1 1.6c-.4.7-.8 1.5-1.2 2.3c-.3.5-.5 1.1-.8 1.7c-.3.8-.6 1.7-.9 2.5c-.2.6-.4 1.1-.5 1.7c-.2.9-.4 1.9-.6 2.9l-.3 1.5q-.3 2.25-.3 4.5v243.5q0 2.25.3 4.5l.3 1.5l.6 2.9c.2.6.3 1.1.5 1.7c.3.9.6 1.7.9 2.5c.2.6.5 1.1.8 1.7c.4.8.7 1.5 1.2 2.3c.3.5.6 1.1 1 1.6c.5.7.9 1.4 1.5 2.1l1.2 1.5c.5.6 1.1 1.3 1.7 1.9c.4.5.9.9 1.4 1.3c.6.6 1.3 1.1 1.9 1.6c.5.4 1 .8 1.6 1.2c.2.1.4.3.6.4L493 905.7c5.6 3.8 12.3 5.8 19.1 5.8c6.6 0 13.3-1.9 19.1-5.8l365.6-243.5c.2-.1.4-.3.6-.4c.5-.4 1-.8 1.6-1.2c.7-.5 1.3-1.1 1.9-1.6c.5-.4.9-.9 1.4-1.3c.6-.6 1.2-1.2 1.7-1.9l1.2-1.5l1.5-2.1c.3-.5.7-1 1-1.6c.4-.8.8-1.5 1.2-2.3c.3-.5.5-1.1.8-1.7c.3-.8.6-1.7.9-2.5c.2-.5.4-1.1.5-1.7c.3-.9.4-1.9.6-2.9l.3-1.5q.3-2.25.3-4.5V389.8c-.3-1.5-.4-3-.6-4.5M546.4 210.5l269.4 179.4l-120.3 80.4l-149-99.6V210.5zm-68.8 0v160.2l-149 99.6l-120.3-80.4zM180.7 454.1l86 57.5l-86 57.5zm296.9 358.5L208.3 633.2l120.3-80.4l149 99.6zM512 592.8l-121.6-81.2L512 430.3l121.6 81.2zm34.4 219.8V652.4l149-99.6l120.3 80.4zM843.3 569l-86-57.5l86-57.5z'/%3E%3C/svg%3E");
}
.vpi-demo-jsfiddle {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='180' viewBox='0 0 256 180'%3E%3Cpath fill='%230084ff' d='M148.1 0c42.8 0 77.598 34.087 78.393 76.452l.014 1.481l-.011.866l1.46.76c16.183 8.773 26.938 25.332 27.964 44.018l.061 1.52l.019 1.418c0 29.117-23.397 52.75-52.428 53.295l-1.365.008H54.053C24.094 179.357 0 155.102 0 125.276c0-17.387 8.273-33.328 21.838-43.511l1.287-.938l.271-.19l-.135-.684a39 39 0 0 1-.438-3.347l-.11-1.694l-.037-1.705c0-21.519 17.547-38.95 39.173-38.95a39 39 0 0 1 16.063 3.445l1.483.706l.915.478l.978-1.623A78.37 78.37 0 0 1 144.718.072l1.721-.055zm0 11.13a67.24 67.24 0 0 0-60.69 38.113c-1.53 3.187-5.607 4.157-8.41 2c-4.908-3.776-10.875-5.856-17.151-5.856c-15.495 0-28.043 12.465-28.043 27.82c0 2.852.43 5.638 1.261 8.27a5.565 5.565 0 0 1-2.473 6.468c-13.215 7.815-21.464 21.854-21.464 37.33c0 23.308 18.526 42.367 41.76 43.376l1.249.038h148.103c23.526.144 42.628-18.783 42.628-42.174c0-17.244-10.49-32.572-26.266-39.1a5.57 5.57 0 0 1-3.43-4.87l.002-.586l.15-2.415l.047-1.246l-.012-1.798c-.768-36.225-30.578-65.37-67.262-65.37m16.167 70.493c17.519 0 31.876 13.362 31.876 30.052s-14.357 30.053-31.876 30.053c-10.548 0-19.386-5.284-31.203-16.729l-2.58-2.547l-3.436-3.525q-6.525-6.955-6.774-7.468l-1.321-1.363l-2.384-2.395a140 140 0 0 0-4.457-4.226l-2.087-1.835c-7.155-6.106-12.769-8.886-18.292-8.886c-11.543 0-20.746 8.564-20.746 18.921c0 10.358 9.203 18.922 20.746 18.922c6.002 0 10.482-1.965 14.584-5.612a35 35 0 0 0 1.57-1.491l2.941-3.133a5.565 5.565 0 0 1 8.5 7.161l-.51.591l-2.033 2.191a50 50 0 0 1-3.072 2.998c-6.013 5.348-13.03 8.426-21.98 8.426c-17.519 0-31.876-13.362-31.876-30.053c0-16.69 14.357-30.052 31.876-30.052c11.678 0 21.26 6.476 35.11 20.62q8.632 9.135 8.88 9.644l2.53 2.59c11.124 11.178 18.65 16.12 26.014 16.12c11.543 0 20.746-8.564 20.746-18.922c0-10.357-9.203-18.921-20.746-18.921c-6.002 0-10.482 1.965-14.584 5.612a35 35 0 0 0-1.57 1.49l-1.311 1.373l-1.63 1.76a5.565 5.565 0 0 1-8.108-7.625l2.15-2.318a50 50 0 0 1 3.073-2.998c6.013-5.347 13.03-8.425 21.98-8.425'/%3E%3C/svg%3E");
}
.vpi-demo-resources {
--icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m21 14l-9 6l-9-6m18-4l-9 6l-9-6l9-6z'/%3E%3C/svg%3E");
}

View File

@ -0,0 +1,39 @@
const cache: {
[src: string]: Promise<void> | undefined
} = {}
export function loadScript(src: string) {
if (__VUEPRESS_SSR__)
return Promise.resolve()
if (document.querySelector(`script[src="${src}"]`)) {
if (cache[src])
return cache[src]
return Promise.resolve()
}
const script = document.createElement('script')
script.src = src
document.body.appendChild(script)
cache[src] = new Promise((resolve, reject) => {
script.onload = () => {
resolve()
delete cache[src]
}
script.onerror = reject
})
return cache[src]
}
export function loadStyle(href: string, target: ShadowRoot) {
if (__VUEPRESS_SSR__)
return
if (target.querySelector(`link[href="${href}"]`))
return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
target.appendChild(link)
}

View File

@ -0,0 +1,114 @@
import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
import type Token from 'markdown-it/lib/token.mjs'
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import container from 'markdown-it-container'
import { createEmbedRuleBlock } from '../embed/createEmbedRuleBlock.js'
import { resolveAttrs } from '../utils/resolveAttrs.js'
import { markdownContainerRender, markdownEmbed } from './markdown.js'
import { normalContainerRender, normalEmbed } from './normal.js'
import { normalizeAlias } from './supports/alias.js'
import { vueContainerRender, vueEmbed } from './vue.js'
export function demoEmbed(app: App, md: Markdown) {
createEmbedRuleBlock<DemoMeta>(md, {
type: 'demo',
syntaxPattern: /^@\[demo(?:\s(vue|normal|markdown))?\s?(.*)\]\((.*)\)/,
meta: ([, type, info, url]) => ({
type: (type || 'normal') as DemoMeta['type'],
url,
...resolveAttrs(info).attrs,
}),
content: (meta, content, env: MarkdownDemoEnv) => {
const { url, type } = meta
if (!url) {
console.warn('[vuepress-plugin-md-power] Invalid demo url: ', url)
return content
}
if (type === 'vue') {
return vueEmbed(app, md, env, meta)
}
if (type === 'normal') {
return normalEmbed(app, md, env, meta)
}
if (type === 'markdown') {
return markdownEmbed(app, md, env, meta)
}
return content
},
})
}
const INFO_RE = /(vue|normal|markdown)?\s?(.*)/
const renderMap: Record<string, DemoContainerRender> = {
vue: vueContainerRender,
normal: normalContainerRender,
markdown: markdownContainerRender,
}
export function demoContainer(app: App, md: Markdown) {
let currentRender: DemoContainerRender | undefined
const render: RenderRule = (
tokens: Token[],
index: number,
_,
env: MarkdownDemoEnv,
): string => {
const token = tokens[index]
if (token.nesting === 1) {
const meta = getContainerMeta(token.info)
meta.url = `${index}`
currentRender = renderMap[meta.type]
return currentRender?.before(
app,
md,
env,
meta,
parseCodeMapping(tokens, index, currentRender.token),
) || ''
}
else {
const res = currentRender?.after() || ''
currentRender = undefined
return res
}
}
md.use(container, 'demo', { render })
}
function parseCodeMapping(
tokens: Token[],
index: number,
cb?: (token: Token, tokens: Token[], index: number) => void,
) {
const codeMap: Record<string, string> = {}
for (
let i = index + 1;
!(tokens[i].nesting === -1
&& tokens[i].type === 'container_demo_close');
++i
) {
const token = tokens[i]
if (token.type === 'fence') {
codeMap[normalizeAlias(token.info)] = token.content.trim()
cb?.(token, tokens, i)
}
}
return codeMap
}
function getContainerMeta(info: string): DemoMeta {
const [, type, raw] = (info.trim().slice(4).trim() || '').match(INFO_RE) || []
const { attrs } = resolveAttrs(raw)
return {
url: '',
type: (type || 'normal') as DemoMeta['type'],
...attrs,
}
}

View File

@ -0,0 +1,17 @@
import type { Page } from 'vuepress'
import type { MarkdownDemoEnv } from '../../shared/demo.js'
export function extendsPageWithDemo(page: Page): void {
const markdownEnv = page.markdownEnv as MarkdownDemoEnv
const demoFiles = markdownEnv.demoFiles ?? []
page.deps.push(
...demoFiles
.filter(({ type }) => type === 'markdown')
.map(({ path }) => path),
)
;((page.frontmatter.gitInclude as string[] | undefined) ??= []).push(
...demoFiles.filter(({ gitignore }) => !gitignore).map(({ path }) => path),
)
}

View File

@ -0,0 +1,13 @@
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import { demoContainer, demoEmbed } from './demo.js'
import { createDemoRender } from './watcher.js'
export function demoPlugin(app: App, md: Markdown) {
createDemoRender()
demoEmbed(app, md)
demoContainer(app, md)
}
export * from './extendPage.js'
export * from './watcher.js'

View File

@ -0,0 +1,43 @@
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import { findFile, readFileSync } from './supports/file.js'
export function markdownEmbed(
app: App,
md: Markdown,
env: MarkdownDemoEnv,
{ url, title, desc, codeSetting = '', expanded = false }: DemoMeta,
): string {
const filepath = findFile(app, env, url)
const code = readFileSync(filepath)
if (code === false) {
console.warn('[vuepress-plugin-md-power] Cannot read markdown file:', filepath)
return ''
}
const demo: DemoFile = { type: 'markdown', path: filepath }
env.demoFiles ??= []
if (!env.demoFiles.some(d => d.path === filepath)) {
env.demoFiles.push(demo)
}
return `<VPDemoBasic type="markdown"${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>
${md.render(code, { filepath: env.filePath, filepathRelative: env.filePathRelative })}
<template #code>
${md.render(`\`\`\`md ${codeSetting}\n${code}\n\`\`\``, {})}
</template>
</VPDemoBasic>`
}
export const markdownContainerRender: DemoContainerRender = {
before(app, md, env, meta, codeMap) {
const { title, desc, expanded = false } = meta
const code = codeMap.md || ''
return `<VPDemoBasic type="markdown"${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>
${md.render(code, { filepath: env.filePath, filepathRelative: env.filePathRelative })}
<template #code>`
},
after: () => '</template></VPDemoBasic>',
}

View File

@ -0,0 +1,200 @@
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import fs from 'node:fs'
import path from 'node:path'
import { compileScript, compileStyle } from './supports/compiler.js'
import { findFile, readFileSync, writeFileSync } from './supports/file.js'
import { insertSetupScript } from './supports/insertScript.js'
import { addTask, checkDemoRender, markDemoRender } from './watcher.js'
interface NormalCode {
html?: string
script?: string
css?: string
imports?: string
jsType: 'ts' | 'js'
cssType: 'css' | 'scss' | 'less' | 'stylus'
}
const CONFIG_RE = /<script type="config">([\s\S]*?)\n<\/script>/
const SCRIPT_RE = /<script\s?(?:lang="(\w+)")?>([\s\S]*?)\n<\/script>/
const STYLE_RE = /<style\s?(?:lang="(\w+)")?>([\s\S]*?)\n<\/style>/
const scriptSupported = ['ts', 'js']
const styleSupported = ['css', 'scss', 'less', 'stylus', 'styl']
const target = 'md-power/demo/normal'
const FENCE = '```'
export function parseEmbedCode(code: string): NormalCode {
const res: NormalCode = { html: '', css: '', script: '', imports: '', jsType: 'js', cssType: 'css' }
res.html = code
.replace(CONFIG_RE, (_, config) => {
res.imports = config
return ''
})
.replace(SCRIPT_RE, (_, lang, script) => {
res.script = script
res.jsType = scriptSupported.includes(lang) ? lang : 'js'
return ''
})
.replace(STYLE_RE, (_, lang, style) => {
res.css = style
res.cssType = styleSupported.includes(lang) ? lang === 'styl' ? 'stylus' : lang : 'css'
return ''
})
return res
}
function codeToHtml(md: Markdown, source: NormalCode, info: string): string {
let content = '::: code-tabs\n'
if (source.html)
content += `@tab HTML\n${FENCE}html ${info}\n${source.html.trim()}\n${FENCE}\n`
if (source.script) {
const title = source.jsType === 'ts' ? 'Typescript' : 'Javascript'
content += `@tab ${title}\n${FENCE}${source.jsType} ${info}\n${source.script.trim()}\n${FENCE}\n`
}
if (source.css) {
const title = source.cssType === 'stylus' ? 'Stylus' : source.cssType.toUpperCase()
content += `@tab ${title}\n${FENCE}${source.cssType} ${info}\n${source.css.trim()}\n${FENCE}\n`
}
content += '\n:::'
return md.render(content, {})
}
export async function compileCode(code: NormalCode, output: string) {
markDemoRender()
const res = { jsLib: [], cssLib: [], script: '', css: '', html: '' }
if (!fs.existsSync(output))
writeFileSync(output, `import { ref } from "vue"\nexport default ref(${JSON.stringify(res, null, 2)})`)
try {
if (code.imports) {
const imports = JSON.parse(code.imports)
res.jsLib = imports.jsLib ?? []
res.cssLib = imports.cssLib ?? []
}
if (code.script) {
res.script = await compileScript(code.script.trim(), code.jsType)
}
if (code.css) {
res.css = await compileStyle(code.css.trim(), code.cssType)
}
if (code.html) {
res.html = code.html.trim()
}
}
catch (e) {
console.error('[vuepress-plugin-md-power] demo parse error: \n', e)
}
writeFileSync(output, `import { ref } from "vue"\nexport default ref(${JSON.stringify(res, null, 2)})`)
checkDemoRender()
}
export function normalEmbed(
app: App,
md: Markdown,
env: MarkdownDemoEnv,
{ url, title, desc, codeSetting = '', expanded = false }: DemoMeta,
): string {
const filepath = findFile(app, env, url)
const code = readFileSync(filepath)
if (code === false) {
console.warn('[vuepress-plugin-md-power] Cannot read demo file:', filepath)
return ''
}
const source = parseEmbedCode(code)
const prefix = (env.filePathRelative || '').replace(/\.md$/, '').replace(/\//g, '-')
const basename = path.basename(filepath).replace(/-|\./g, '_')
const name = `Demo${basename[0].toUpperCase()}${basename.slice(1)}`
const demo: DemoFile = { type: 'normal', export: name, path: filepath }
const output = app.dir.temp(target, `${prefix}-${name}.js`)
compileCode(source, output)
addTask(app, filepath, output)
env.demoFiles ??= []
if (!env.demoFiles.some(d => d.path === filepath)) {
env.demoFiles.push(demo)
insertSetupScript({ ...demo, path: output }, env)
}
return `<VPDemoNormal :config="${name}"${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>
${codeToHtml(md, source, codeSetting)}
</VPDemoNormal>`
}
export const normalContainerRender: DemoContainerRender = {
before(app, md, env, meta, codeMap) {
const { url, title, desc, expanded = false } = meta
const name = `DemoContainer${url}`
const prefix = (env.filePathRelative || '').replace(/\.md$/, '').replace(/\//g, '-')
const output = app.dir.temp(path.join(target, `${prefix}-${name}.js`))
env.demoFiles ??= []
if (!env.demoFiles.some(d => d.path === output)) {
const demo: DemoFile = { type: 'normal', export: name, gitignore: true, path: output }
env.demoFiles.push(demo)
insertSetupScript(demo, env)
}
const source = parseContainerCode(codeMap)
compileCode(source, output)
return `<VPDemoNormal :config="${name}"${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>`
},
after: () => '</VPDemoNormal>',
token(token) {
if (token.info.trim().startsWith('json')) {
token.hidden = true
token.type = 'text'
token.content = ''
}
},
}
function parseContainerCode(map: Record<string, string>): NormalCode {
const res: NormalCode = { html: '', css: '', script: '', imports: '', jsType: 'js', cssType: 'css' }
if (map.html) {
res.html = map.html
}
if (map.js) {
res.script = map.js
res.jsType = 'js'
}
if (map.ts) {
res.script = map.ts
res.jsType = 'ts'
}
if (map.css) {
res.css = map.css
res.cssType = 'css'
}
if (map.less) {
res.css = map.less
res.cssType = 'less'
}
if (map.scss) {
res.css = map.scss
res.cssType = 'scss'
}
if (map.styl || map.stylus) {
res.css = map.styl || map.stylus
res.cssType = 'stylus'
}
if (map.json) {
res.imports = map.json
}
return res
}

View File

@ -0,0 +1,26 @@
# demo 嵌入
demo 目前主要聚焦于实现 vue component demo不考虑其他场景。
此功能提供两种方式来嵌入 demo。
方式一 嵌入语法:
@[demo vue](/xxx.vue)
方式二 容器语法:
::: demo vue
```vue
<script setup>
</script>
<template>
<div>1</div>
</template>
<style>
</style>
``
:::

View File

@ -0,0 +1,20 @@
export function normalizeAlias(info: string): string {
const [lang] = info.trim().split(/\s+|:|\{/)
switch (lang) {
case 'vue':
return 'vue'
case 'js':
case 'javascript':
return 'js'
case 'ts':
case 'typescript':
return 'ts'
case 'stylus':
case 'styl':
return 'stylus'
case 'md':
case 'markdown':
return 'md'
}
return lang
}

View File

@ -0,0 +1,72 @@
import { isPackageExists } from 'local-pkg'
import { LRUCache } from 'lru-cache'
import { interopDefault } from '../../utils/package.js'
const cache = new LRUCache({ max: 64 })
const compiler = {
script: importer(async () => {
const { transform } = await import('esbuild')
return transform
}),
less: importer(() => import('less')),
sass: importer(async () => {
if (isPackageExists('sass-embedded')) {
return await import('sass-embedded')
}
return await import('sass')
}),
stylus: importer(() => import('stylus')),
}
export async function compileScript(source: string, type: 'ts' | 'js'): Promise<string> {
const key = `${type}:::${source}`
if (cache.has(key))
return cache.get(key) as string
const transform = await compiler.script()
const res = await transform(source, {
target: 'es2018',
format: 'cjs',
loader: type === 'ts' ? 'ts' : 'js',
sourcemap: false,
})
cache.set(key, res.code)
return res.code
}
export async function compileStyle(source: string, type: 'css' | 'less' | 'scss' | 'stylus'): Promise<string> {
const key = `${type}:::${source}`
if (cache.has(key))
return cache.get(key) as string
if (type === 'css')
return source
if (type === 'less') {
const less = await compiler.less()
const res = await less.render(source)
cache.set(key, res.css)
return res.css
}
if (type === 'scss') {
const sass = await compiler.sass()
const res = sass.compileString(source)
cache.set(key, res.css)
return res.css
}
if (type === 'stylus') {
const stylus = await compiler.stylus()
const res = stylus.render(source)
cache.set(key, res)
return res
}
return source
}
export function importer<T>(func: () => T): () => Promise<T> {
let imported: T
return async () => {
if (!imported) {
imported = interopDefault(await func()) as T
}
return imported
}
}

View File

@ -0,0 +1,42 @@
import type { App } from 'vuepress'
import type { MarkdownEnv } from 'vuepress/markdown'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
const require = createRequire(process.cwd())
export function findFile(app: App, env: MarkdownEnv, url: string): string {
if (url.startsWith('/'))
return app.dir.source(url.slice(1))
if (url.startsWith('./') || url.startsWith('../'))
return app.dir.source(path.dirname(env.filePathRelative!), url)
if (url.startsWith('@source/')) {
return app.dir.source(url.slice('@source/'.length))
}
try {
return require.resolve(url)
}
catch {
return url
}
}
export function readFileSync(filepath: string): string | false {
try {
return fs.readFileSync(filepath, 'utf-8')
}
catch {
return false
}
}
export function writeFileSync(filepath: string, content: string): void {
const dirname = path.dirname(filepath)
fs.mkdirSync(dirname, { recursive: true })
fs.writeFileSync(filepath, content, 'utf-8')
}

View File

@ -0,0 +1,16 @@
import type { DemoFile, MarkdownDemoEnv } from '../../../shared/demo.js'
const SCRIPT_RE = /<script.*?>/
export function insertSetupScript({ export: name, path }: DemoFile, env: MarkdownDemoEnv) {
const imports = `import ${name ? `${name} from ` : ''}'${path}';`
const scriptSetup = env.sfcBlocks!.scriptSetup ??= {
type: 'script',
content: '<script setup>\n</script>',
contentStripped: '',
tagOpen: '<script setup>',
tagClose: '</script>',
}
scriptSetup.contentStripped = `${imports}\n${scriptSetup.contentStripped}`
scriptSetup.content = scriptSetup.content.replace(SCRIPT_RE, matched => `${matched}\n${imports}`)
}

View File

@ -0,0 +1,130 @@
import type { App } from 'vuepress'
import type { Markdown } from 'vuepress/markdown'
import type { DemoContainerRender, DemoFile, DemoMeta, MarkdownDemoEnv } from '../../shared/demo.js'
import path from 'node:path'
import { findFile, readFileSync, writeFileSync } from './supports/file.js'
import { insertSetupScript } from './supports/insertScript.js'
export function vueEmbed(
app: App,
md: Markdown,
env: MarkdownDemoEnv,
{ url, title, desc, codeSetting = '', expanded = false }: DemoMeta,
): string {
const filepath = findFile(app, env, url)
const code = readFileSync(filepath)
if (code === false) {
console.warn('[vuepress-plugin-md-power] Cannot read vue file:', filepath)
return ''
}
const basename = path.basename(filepath).replace(/-|\./g, '_')
const ext = path.extname(filepath).slice(1)
const name = `Demo${basename[0].toUpperCase()}${basename.slice(1)}`
const demo: DemoFile = { type: 'vue', export: name, path: filepath }
env.demoFiles ??= []
if (!env.demoFiles.some(d => d.path === filepath)) {
env.demoFiles.push(demo)
insertSetupScript(demo, env)
}
return `<VPDemoBasic type="vue"${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>
<${name} />
<template #code>
${md.render(`\`\`\`${ext}${codeSetting}\n${code}\n\`\`\``, {})}
</template>
</VPDemoBasic>`
}
const target = 'md-power/demo/vue'
export const vueContainerRender: DemoContainerRender = {
before: (app, md, env, meta, codeMap) => {
const { url, title, desc, expanded = false } = meta
const componentName = `DemoContainer${url}`
const prefix = (env.filePathRelative || '').replace(/\.md$/, '').replace(/\//g, '-')
env.demoFiles ??= []
const output = app.dir.temp(path.join(target, `${prefix}-${componentName}`))
// generate script file
if (codeMap.vue || codeMap.js || codeMap.ts) {
let scriptOutput = output
let content = ''
if (codeMap.vue) {
scriptOutput += '.vue'
content = transformStyle(codeMap.vue)
}
else if (codeMap.ts) {
scriptOutput += '.ts'
content = codeMap.ts
}
else if (codeMap.js) {
scriptOutput += '.js'
content = codeMap.js
}
content = transformImports(content, env.filePath || '')
const script: DemoFile = { type: 'vue', export: componentName, path: scriptOutput, gitignore: true }
writeFileSync(scriptOutput, content)
if (!env.demoFiles.some(d => d.path === scriptOutput)) {
env.demoFiles.push(script)
insertSetupScript(script, env)
}
}
// generate style file
if (codeMap.css || codeMap.scss || codeMap.less || codeMap.stylus) {
let styleOutput = output
let content = ''
if (codeMap.css) {
styleOutput += '.css'
content = codeMap.css
}
else if (codeMap.scss) {
styleOutput += '.scss'
content = codeMap.scss
}
else if (codeMap.less) {
styleOutput += '.less'
content = codeMap.less
}
else if (codeMap.stylus) {
styleOutput += '.styl'
content = codeMap.stylus
}
writeFileSync(styleOutput, content)
const style: DemoFile = { type: 'css', path: styleOutput, gitignore: true }
if (!env.demoFiles.some(d => d.path === styleOutput)) {
env.demoFiles.push(style)
insertSetupScript(style, env)
}
}
return `<VPDemoBasic${title ? ` title="${title}"` : ''}${desc ? ` desc="${desc}"` : ''}${expanded ? ' expanded' : ''}>
<${componentName} />
<template #code>\n`
},
after: () => '</template></VPDemoBasic>',
}
const IMPORT_RE = /import\s+(?:\w+\s+from\s+)?['"]([^'"]+)['"]/g
const STYLE_RE = /<style.*?>/
function transformImports(code: string, filepath: string): string {
return code.replace(IMPORT_RE, (matched, url) => {
if (url.startsWith('./') || url.startsWith('../')) {
return matched.replace(url, `${path.resolve(path.dirname(filepath), url)}`)
}
return matched
})
}
function transformStyle(code: string): string {
return code.replace(STYLE_RE, (matched) => {
if (matched.includes('scoped')) {
return matched
}
return matched.replace('<style', '<style scoped')
})
}

View File

@ -0,0 +1,100 @@
import type { App } from 'vuepress'
import fs from 'node:fs'
import { type FSWatcher, watch } from 'chokidar'
import { compileCode, parseEmbedCode } from './normal.js'
import { readFileSync } from './supports/file.js'
/**
* demo markdown render
* vuepress onPrepared demo
*/
let renderDone: null | ((...args: any[]) => void) = null
let renderCount = 0
let renderPromise!: Promise<void>
export function createDemoRender() {
renderPromise = new Promise((resolve) => {
renderDone = resolve
})
}
export async function waitDemoRender() {
if (renderCount === 0) {
renderDone?.()
renderDone = null
}
await renderPromise
}
export function markDemoRender() {
renderCount++
}
export function checkDemoRender() {
if (renderCount > 0) {
renderCount--
}
if (renderCount === 0) {
renderDone?.()
renderDone = null
}
}
let watcher: FSWatcher | null = null
// path: runner
const tasks: Record<string, string> = {}
const target = 'md-power/demo/watcher.txt'
export function demoWatcher(app: App, watchers: any[]) {
if (!watcher) {
watcher = watch([], { ignoreInitial: true })
}
Object.keys(tasks).forEach((path) => {
watcher!.add(path)
})
const code = readFileSync(app.dir.temp(target))
if (code) {
const paths = JSON.parse(code || '{}') as Record<string, string>
Object.entries(paths).forEach(([path, output]) => {
watcher!.add(path)
tasks[path] = output
})
}
updateWatchFiles(app)
watcher.on('change', (path) => {
if (tasks[path]) {
const code = readFileSync(path)
if (code === false)
return
const source = parseEmbedCode(code)
compileCode(source, tasks[path])
}
})
watcher.on('unlink', (path) => {
delete tasks[path]
watcher!.unwatch(path)
})
watchers.push(() => {
watcher!.close()
watcher = null
})
}
export function addTask(app: App, path: string, output: string) {
if (tasks[path])
return
tasks[path] = output
if (watcher) {
watcher.add(path)
}
updateWatchFiles(app)
}
async function updateWatchFiles(app: App) {
await fs.promises.writeFile(app.dir.temp(target), JSON.stringify(tasks))
}

View File

@ -1,5 +1,5 @@
import type { RuleOptions } from 'markdown-it/lib/ruler.mjs'
import type { Markdown } from 'vuepress/markdown'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
export interface EmbedRuleBlockOptions<Meta extends Record<string, any>> {
/**
@ -14,7 +14,7 @@ export interface EmbedRuleBlockOptions<Meta extends Record<string, any>> {
syntaxPattern: RegExp
ruleOptions?: RuleOptions
meta: (match: RegExpMatchArray) => Meta
content: (meta: Meta) => string
content: (meta: Meta, content: string, env: MarkdownEnv) => string
}
// @[name]()
@ -51,7 +51,8 @@ export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<s
}
// check if it's matched the syntax
const match = state.src.slice(pos, max).match(syntaxPattern)
const content = state.src.slice(pos, max)
const match = content.match(syntaxPattern)
if (!match)
return false
@ -63,6 +64,7 @@ export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<s
const token = state.push(name, '', 0)
token.meta = meta(match)
token.content = content
token.map = [startLine, startLine + 1]
state.line = startLine + 1
@ -72,9 +74,9 @@ export function createEmbedRuleBlock<Meta extends Record<string, any> = Record<s
ruleOptions,
)
md.renderer.rules[name] = (tokens, index) => {
md.renderer.rules[name] = (tokens, index, _, env: MarkdownEnv) => {
const token = tokens[index]
token.content = content(token.meta)
token.content = content(token.meta, token.content, env)
return token.content
}
}

View File

@ -87,6 +87,7 @@ export const definitions: Definitions = {
'CSS': 'vscode-icons:file-type-css',
'less': 'vscode-icons:file-type-less',
'Less': 'vscode-icons:file-type-less',
'LESS': 'vscode-icons:file-type-less',
'scss': 'vscode-icons:file-type-scss',
'Scss': 'vscode-icons:file-type-scss',
'SCSS': 'vscode-icons:file-type-scss',

View File

@ -3,6 +3,7 @@ import type { MarkdownPowerPluginOptions } from '../shared/index.js'
import { addViteOptimizeDepsInclude } from '@vuepress/helper'
import { isPackageExists } from 'local-pkg'
import { containerPlugin } from './container/index.js'
import { demoPlugin, demoWatcher, extendsPageWithDemo, waitDemoRender } from './demo/index.js'
import { embedSyntaxPlugin } from './embed/index.js'
import { docsTitlePlugin } from './enhance/docsTitle.js'
import { imageSizePlugin } from './enhance/imageSize.js'
@ -46,8 +47,27 @@ export function markdownPowerPlugin(
embedSyntaxPlugin(md, options)
inlineSyntaxPlugin(md, options)
if (options.demo)
demoPlugin(app, md)
await containerPlugin(app, md, options)
await imageSizePlugin(app, md, options.imageSize)
},
onPrepared: async () => {
if (options.demo)
await waitDemoRender()
},
onWatched(app, watchers) {
if (options.demo) {
demoWatcher(app, watchers)
}
},
extendsPage: (page) => {
if (options.demo)
extendsPageWithDemo(page)
},
}
}

View File

@ -75,6 +75,13 @@ export async function prepareConfigFile(app: App, options: MarkdownPowerPluginOp
enhances.add(`app.component('AudioReader', AudioReader)`)
}
if (options.demo) {
imports.add(`import VPDemoBasic from '${CLIENT_FOLDER}components/VPDemoBasic.vue'`)
imports.add(`import VPDemoNormal from '${CLIENT_FOLDER}components/VPDemoNormal.vue'`)
enhances.add(`app.component('VPDemoBasic', VPDemoBasic)`)
enhances.add(`app.component('VPDemoNormal', VPDemoNormal)`)
}
return app.writeTemp(
'md-power/config.js',
`\

View File

@ -0,0 +1,35 @@
import type Token from 'markdown-it/lib/token.mjs'
import type { App } from 'vuepress'
import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
export interface DemoFile {
type: 'vue' | 'normal' | 'css' | 'markdown'
export?: string
path: string
gitignore?: boolean
}
export interface MarkdownDemoEnv extends MarkdownEnv {
demoFiles?: DemoFile[]
}
export interface DemoMeta {
type: 'vue' | 'normal' | 'markdown'
url: string
title?: string
desc?: string
codeSetting?: string
expanded?: boolean
}
export interface DemoContainerRender {
before: (
app: App,
md: Markdown,
env: MarkdownDemoEnv,
meta: DemoMeta,
codeMap: Record<string, string>
) => string
after: () => string
token?: (token: Token, tokens: Token[], index: number) => void
}

View File

@ -2,6 +2,7 @@ export * from './caniuse.js'
export * from './codepen.js'
export * from './codeSandbox.js'
export * from './codeTabs.js'
export * from './demo.js'
export * from './fileTree.js'
export * from './icons.js'
export * from './jsfiddle.js'

View File

@ -121,6 +121,11 @@ export interface MarkdownPowerPluginOptions {
*/
fileTree?: boolean | FileTreeOptions
/**
* demo
*/
demo?: boolean
/**
* caniuse
*

View File

@ -3,7 +3,7 @@ import { argv } from '../../scripts/tsup-args.js'
const config = [
{ dir: 'composables', files: ['codeRepl.ts', 'pdf.ts', 'rustRepl.ts', 'size.ts', 'audio.ts'] },
{ dir: 'utils', files: ['http.ts', 'is.ts', 'link.ts', 'sleep.ts'] },
{ dir: 'utils', files: ['http.ts', 'is.ts', 'link.ts', 'sleep.ts', 'shared.ts'] },
{ dir: '', files: ['index.ts', 'options.ts'] },
]

405
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,11 @@ declare module '*.vue' {
export default comp
}
declare module '*.module.css' {
const classes: { [key: string]: string }
export default classes
}
declare const __VUEPRESS_DEV__: string
declare module '@internal/articleTagColors' {

View File

@ -17,6 +17,21 @@
}
/* ----------------- Transition ------------------------ */
.fade-enter-active {
transition: 0.25s ease !important;
transition-property: opacity;
}
.fade-leave-active {
transition: 0.25s cubic-bezier(0, 1, 0.3, 1) !important;
transition-property: opacity;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-slide-y-enter-active {
transition: 0.15s ease !important;
transition-property: opacity, transform;