+
+
+
+
+
+
+
+
+
+```
+
+每一个区域的内容都是可选的。但请注意,不支持存在多个相同的区域。区域的顺序无要求。
+除了 `` 内使用 `json` 格式声明要加载的其他依赖资源。
+
+比如,加载 `jQuery`, 以及 `normalize.css`:
+
+```html
+
xxxx
+
+
+```
+
+---
+
+一个常规的示例:
+
+**输入:**
+
+```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
+
+```
+
+```js
+// js 代码
+```
+
+```css
+/* css 代码 */
+```
+:::
+```
+::::
+
+还可以在 `::: demo` 中包裹 `::: code-tabs` 以获得更好的代码块展示效果:
+
+::::: details 展开查看完整示例代码
+
+````md
+:::: demo title="示例" desc="描述" expanded
+```json
+{
+ "jsLib": [],
+ "cssLib": []
+}
+```
+::: code-tabs
+@tab HTML
+```html
+
+```
+@tab Javascript
+```js
+// js 代码
+```
+@tab CSS
+```css
+/* css 代码 */
+```
+:::
+::::
+```
+:::::
+
+---
+
+一个常规的 容器示例:
+
+**输入:**
+
+::::: details 展开查看完整示例代码
+
+````md
+:::: demo title="常规示例" desc="一个常规示例"
+::: code-tabs
+@tab HTML
+```html
+
+
vuepress-theme-plume
+
+```
+@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
+
+
vuepress-theme-plume
+
+```
+
+@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
+
+
vuepress-theme-plume
+
+
+
+```
+@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
+
+
vuepress-theme-plume
+
+
+
+```
+
+@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 交流群一起讨论。
+
+:::
+```
+
+::::
diff --git a/docs/shim.d.ts b/docs/shim.d.ts
new file mode 100644
index 000000000..d1c3d9ed5
--- /dev/null
+++ b/docs/shim.d.ts
@@ -0,0 +1,4 @@
+declare module '*.module.css' {
+ const classes: { [key: string]: string }
+ export default classes
+}
diff --git a/package.json b/package.json
index f6418c3cf..88b2696ca 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/plugins/plugin-md-power/package.json b/plugins/plugin-md-power/package.json
index 51b2f9675..af9f431dc 100644
--- a/plugins/plugin-md-power/package.json
+++ b/plugins/plugin-md-power/package.json
@@ -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",
diff --git a/plugins/plugin-md-power/src/client/components/VPDemoBasic.vue b/plugins/plugin-md-power/src/client/components/VPDemoBasic.vue
new file mode 100644
index 000000000..117f5c7cc
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/components/VPDemoBasic.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ desc }}
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/plugin-md-power/src/client/components/VPDemoNormal.vue b/plugins/plugin-md-power/src/client/components/VPDemoNormal.vue
new file mode 100644
index 000000000..0e6f8509d
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/components/VPDemoNormal.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ desc }}
+
+
+
+
+
+
+
+
diff --git a/plugins/plugin-md-power/src/client/styles/demo.css b/plugins/plugin-md-power/src/client/styles/demo.css
new file mode 100644
index 000000000..152195a3c
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/styles/demo.css
@@ -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");
+}
diff --git a/plugins/plugin-md-power/src/client/utils/shared.ts b/plugins/plugin-md-power/src/client/utils/shared.ts
new file mode 100644
index 000000000..2026fd29b
--- /dev/null
+++ b/plugins/plugin-md-power/src/client/utils/shared.ts
@@ -0,0 +1,39 @@
+const cache: {
+ [src: string]: Promise
| 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)
+}
diff --git a/plugins/plugin-md-power/src/node/demo/demo.ts b/plugins/plugin-md-power/src/node/demo/demo.ts
new file mode 100644
index 000000000..e53e7e414
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/demo.ts
@@ -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(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 = {
+ 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 = {}
+ 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,
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/demo/extendPage.ts b/plugins/plugin-md-power/src/node/demo/extendPage.ts
new file mode 100644
index 000000000..b643aa828
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/extendPage.ts
@@ -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),
+ )
+}
diff --git a/plugins/plugin-md-power/src/node/demo/index.ts b/plugins/plugin-md-power/src/node/demo/index.ts
new file mode 100644
index 000000000..2acf2c8ce
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/index.ts
@@ -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'
diff --git a/plugins/plugin-md-power/src/node/demo/markdown.ts b/plugins/plugin-md-power/src/node/demo/markdown.ts
new file mode 100644
index 000000000..5343e60fe
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/markdown.ts
@@ -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 `
+ ${md.render(code, { filepath: env.filePath, filepathRelative: env.filePathRelative })}
+
+ ${md.render(`\`\`\`md ${codeSetting}\n${code}\n\`\`\``, {})}
+
+ `
+}
+
+export const markdownContainerRender: DemoContainerRender = {
+ before(app, md, env, meta, codeMap) {
+ const { title, desc, expanded = false } = meta
+ const code = codeMap.md || ''
+ return `
+ ${md.render(code, { filepath: env.filePath, filepathRelative: env.filePathRelative })}
+ `
+ },
+ after: () => '',
+}
diff --git a/plugins/plugin-md-power/src/node/demo/normal.ts b/plugins/plugin-md-power/src/node/demo/normal.ts
new file mode 100644
index 000000000..14e469caa
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/normal.ts
@@ -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 = /
+
+
+ 1
+
+
+
+``
+:::
diff --git a/plugins/plugin-md-power/src/node/demo/supports/alias.ts b/plugins/plugin-md-power/src/node/demo/supports/alias.ts
new file mode 100644
index 000000000..0ab102df5
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/supports/alias.ts
@@ -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
+}
diff --git a/plugins/plugin-md-power/src/node/demo/supports/compiler.ts b/plugins/plugin-md-power/src/node/demo/supports/compiler.ts
new file mode 100644
index 000000000..97585f838
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/supports/compiler.ts
@@ -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 {
+ 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 {
+ 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(func: () => T): () => Promise {
+ let imported: T
+ return async () => {
+ if (!imported) {
+ imported = interopDefault(await func()) as T
+ }
+ return imported
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/demo/supports/file.ts b/plugins/plugin-md-power/src/node/demo/supports/file.ts
new file mode 100644
index 000000000..129379d16
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/supports/file.ts
@@ -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')
+}
diff --git a/plugins/plugin-md-power/src/node/demo/supports/insertScript.ts b/plugins/plugin-md-power/src/node/demo/supports/insertScript.ts
new file mode 100644
index 000000000..19ad5c0bc
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/supports/insertScript.ts
@@ -0,0 +1,16 @@
+import type { DemoFile, MarkdownDemoEnv } from '../../../shared/demo.js'
+
+const SCRIPT_RE = //
+
+export function insertSetupScript({ export: name, path }: DemoFile, env: MarkdownDemoEnv) {
+ const imports = `import ${name ? `${name} from ` : ''}'${path}';`
+ const scriptSetup = env.sfcBlocks!.scriptSetup ??= {
+ type: 'script',
+ content: '',
+ contentStripped: '',
+ tagOpen: '',
+ }
+ scriptSetup.contentStripped = `${imports}\n${scriptSetup.contentStripped}`
+ scriptSetup.content = scriptSetup.content.replace(SCRIPT_RE, matched => `${matched}\n${imports}`)
+}
diff --git a/plugins/plugin-md-power/src/node/demo/vue.ts b/plugins/plugin-md-power/src/node/demo/vue.ts
new file mode 100644
index 000000000..7bffbd2f3
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/demo/vue.ts
@@ -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 `
+ <${name} />
+
+ ${md.render(`\`\`\`${ext}${codeSetting}\n${code}\n\`\`\``, {})}
+
+ `
+}
+
+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 `
+ <${componentName} />
+ \n`
+ },
+ after: () => '',
+}
+
+const IMPORT_RE = /import\s+(?:\w+\s+from\s+)?['"]([^'"]+)['"]/g
+const STYLE_RE = //
+
+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('