mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(plugin-md-power): add demo syntax (#415)
* feat(plugin-md-power): add `demo` syntax
This commit is contained in:
parent
11db9059c5
commit
a9f6bd0a0c
@ -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}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:**
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
::::
|
||||
|
||||
**选项卡:**
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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: '前端(弃用)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -36,6 +36,7 @@ export const theme: Theme = plumeTheme({
|
||||
replit: true,
|
||||
codeSandbox: true,
|
||||
jsfiddle: true,
|
||||
demo: true,
|
||||
npmTo: ['pnpm', 'yarn', 'npm'],
|
||||
repl: {
|
||||
go: true,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
::::
|
||||
|
||||
**选项卡:**
|
||||
|
||||
|
||||
5
docs/notes/theme/guide/代码演示/demo/Counter.module.css
Normal file
5
docs/notes/theme/guide/代码演示/demo/Counter.module.css
Normal file
@ -0,0 +1,5 @@
|
||||
.btn {
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
}
|
||||
18
docs/notes/theme/guide/代码演示/demo/Counter.ts
Normal file
18
docs/notes/theme/guide/代码演示/demo/Counter.ts
Normal 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'),
|
||||
])
|
||||
},
|
||||
})
|
||||
24
docs/notes/theme/guide/代码演示/demo/Counter.vue
Normal file
24
docs/notes/theme/guide/代码演示/demo/Counter.vue
Normal 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>
|
||||
30
docs/notes/theme/guide/代码演示/demo/Toggle.vue
Normal file
30
docs/notes/theme/guide/代码演示/demo/Toggle.vue
Normal 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>
|
||||
30
docs/notes/theme/guide/代码演示/demo/normal-lib.html
Normal file
30
docs/notes/theme/guide/代码演示/demo/normal-lib.html
Normal 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>
|
||||
16
docs/notes/theme/guide/代码演示/demo/normal.html
Normal file
16
docs/notes/theme/guide/代码演示/demo/normal.html
Normal 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>
|
||||
@ -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/) 提供支持。
|
||||
|
||||
807
docs/notes/theme/guide/代码演示/前端演示.md
Normal file
807
docs/notes/theme/guide/代码演示/前端演示.md
Normal 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)
|
||||
|
||||
{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)
|
||||
|
||||
{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
4
docs/shim.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
193
plugins/plugin-md-power/src/client/components/VPDemoNormal.vue
Normal file
193
plugins/plugin-md-power/src/client/components/VPDemoNormal.vue
Normal 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>
|
||||
157
plugins/plugin-md-power/src/client/styles/demo.css
Normal file
157
plugins/plugin-md-power/src/client/styles/demo.css
Normal 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");
|
||||
}
|
||||
39
plugins/plugin-md-power/src/client/utils/shared.ts
Normal file
39
plugins/plugin-md-power/src/client/utils/shared.ts
Normal 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)
|
||||
}
|
||||
114
plugins/plugin-md-power/src/node/demo/demo.ts
Normal file
114
plugins/plugin-md-power/src/node/demo/demo.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
17
plugins/plugin-md-power/src/node/demo/extendPage.ts
Normal file
17
plugins/plugin-md-power/src/node/demo/extendPage.ts
Normal 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),
|
||||
)
|
||||
}
|
||||
13
plugins/plugin-md-power/src/node/demo/index.ts
Normal file
13
plugins/plugin-md-power/src/node/demo/index.ts
Normal 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'
|
||||
43
plugins/plugin-md-power/src/node/demo/markdown.ts
Normal file
43
plugins/plugin-md-power/src/node/demo/markdown.ts
Normal 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>',
|
||||
}
|
||||
200
plugins/plugin-md-power/src/node/demo/normal.ts
Normal file
200
plugins/plugin-md-power/src/node/demo/normal.ts
Normal 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
|
||||
}
|
||||
26
plugins/plugin-md-power/src/node/demo/readme.md
Normal file
26
plugins/plugin-md-power/src/node/demo/readme.md
Normal 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>
|
||||
``
|
||||
:::
|
||||
20
plugins/plugin-md-power/src/node/demo/supports/alias.ts
Normal file
20
plugins/plugin-md-power/src/node/demo/supports/alias.ts
Normal 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
|
||||
}
|
||||
72
plugins/plugin-md-power/src/node/demo/supports/compiler.ts
Normal file
72
plugins/plugin-md-power/src/node/demo/supports/compiler.ts
Normal 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
|
||||
}
|
||||
}
|
||||
42
plugins/plugin-md-power/src/node/demo/supports/file.ts
Normal file
42
plugins/plugin-md-power/src/node/demo/supports/file.ts
Normal 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')
|
||||
}
|
||||
@ -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}`)
|
||||
}
|
||||
130
plugins/plugin-md-power/src/node/demo/vue.ts
Normal file
130
plugins/plugin-md-power/src/node/demo/vue.ts
Normal 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')
|
||||
})
|
||||
}
|
||||
100
plugins/plugin-md-power/src/node/demo/watcher.ts
Normal file
100
plugins/plugin-md-power/src/node/demo/watcher.ts
Normal 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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
`\
|
||||
|
||||
35
plugins/plugin-md-power/src/shared/demo.ts
Normal file
35
plugins/plugin-md-power/src/shared/demo.ts
Normal 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
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -121,6 +121,11 @@ export interface MarkdownPowerPluginOptions {
|
||||
*/
|
||||
fileTree?: boolean | FileTreeOptions
|
||||
|
||||
/**
|
||||
* 是否启用 demo 语法
|
||||
*/
|
||||
demo?: boolean
|
||||
|
||||
/**
|
||||
* 是否启用 caniuse 嵌入语法
|
||||
*
|
||||
|
||||
@ -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
405
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
5
theme/src/client/shim.d.ts
vendored
5
theme/src/client/shim.d.ts
vendored
@ -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' {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user