@antdv-next/docs-plugins 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/demo/formatter.d.ts +17 -0
- package/dist/demo/formatter.js +32 -0
- package/dist/demo/get-demo-id.d.ts +9 -0
- package/dist/demo/get-demo-id.js +19 -0
- package/dist/demo/index.d.ts +26 -0
- package/dist/demo/index.js +359 -0
- package/dist/demo/tsToJs.d.ts +9 -0
- package/dist/demo/tsToJs.js +60 -0
- package/dist/demo/types.d.ts +23 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/isolate-styles.d.ts +14 -0
- package/dist/isolate-styles.js +30 -0
- package/dist/markdown.d.ts +39 -0
- package/dist/markdown.js +115 -0
- package/dist/md-plugin.d.ts +18 -0
- package/dist/md-plugin.js +9 -0
- package/dist/md2vue.d.ts +16 -0
- package/dist/md2vue.js +106 -0
- package/dist/plugins/container.d.ts +16 -0
- package/dist/plugins/container.js +53 -0
- package/dist/plugins/demo.d.ts +29 -0
- package/dist/plugins/demo.js +139 -0
- package/dist/plugins/github-alerts.d.ts +6 -0
- package/dist/plugins/github-alerts.js +49 -0
- package/dist/plugins/image.d.ts +12 -0
- package/dist/plugins/image.js +17 -0
- package/dist/plugins/link.d.ts +14 -0
- package/dist/plugins/link.js +25 -0
- package/dist/plugins/pre-wrapper.d.ts +10 -0
- package/dist/plugins/pre-wrapper.js +26 -0
- package/dist/plugins/stackblitz.d.ts +5 -0
- package/dist/plugins/stackblitz.js +21 -0
- package/dist/plugins/table.d.ts +5 -0
- package/dist/plugins/table.js +25 -0
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/utils/short-hash.d.ts +4 -0
- package/dist/utils/short-hash.js +21 -0
- package/package.json +94 -0
- package/src/components/code-demo/code-editor-bridge.vue +36 -0
- package/src/components/code-demo/compile-sfc.ts +207 -0
- package/src/components/code-demo/context.ts +86 -0
- package/src/components/code-demo/expand-icon.vue +12 -0
- package/src/components/code-demo/external-link-icon.vue +5 -0
- package/src/components/code-demo/index.vue +682 -0
- package/src/components/code-demo/virtual.d.ts +38 -0
- package/src/demo/formatter.ts +50 -0
- package/src/demo/get-demo-id.ts +33 -0
- package/src/demo/index.ts +498 -0
- package/src/demo/tsToJs.ts +79 -0
- package/src/demo/types.ts +23 -0
- package/src/index.ts +19 -0
- package/src/isolate-styles.ts +47 -0
- package/src/markdown.ts +188 -0
- package/src/md-plugin.ts +24 -0
- package/src/md2vue.ts +163 -0
- package/src/plugins/container.ts +135 -0
- package/src/plugins/demo.ts +282 -0
- package/src/plugins/github-alerts.ts +69 -0
- package/src/plugins/image.ts +29 -0
- package/src/plugins/link.ts +32 -0
- package/src/plugins/pre-wrapper.ts +49 -0
- package/src/plugins/stackblitz.ts +32 -0
- package/src/plugins/table.ts +39 -0
- package/src/shared.ts +4 -0
- package/src/utils/short-hash.ts +27 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/plugins/github-alerts.ts
|
|
2
|
+
const markerRE = /^\[!(TIP|NOTE|INFO|IMPORTANT|WARNING|CAUTION|DANGER)\]([^\n\r]*)/i;
|
|
3
|
+
function gitHubAlertsPlugin(md, options) {
|
|
4
|
+
const titleMark = {
|
|
5
|
+
tip: options?.tipLabel || "TIP",
|
|
6
|
+
note: options?.noteLabel || "NOTE",
|
|
7
|
+
info: options?.infoLabel || "INFO",
|
|
8
|
+
important: options?.importantLabel || "IMPORTANT",
|
|
9
|
+
warning: options?.warningLabel || "WARNING",
|
|
10
|
+
caution: options?.cautionLabel || "CAUTION",
|
|
11
|
+
danger: options?.dangerLabel || "DANGER"
|
|
12
|
+
};
|
|
13
|
+
md.core.ruler.after("block", "github-alerts", (state) => {
|
|
14
|
+
const tokens = state.tokens;
|
|
15
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
16
|
+
if (tokens[i].type !== "blockquote_open") continue;
|
|
17
|
+
const startIndex = i;
|
|
18
|
+
const open = tokens[startIndex];
|
|
19
|
+
let endIndex = i + 1;
|
|
20
|
+
while (endIndex < tokens.length && (tokens[endIndex].type !== "blockquote_close" || tokens[endIndex].level !== open.level)) endIndex++;
|
|
21
|
+
if (endIndex === tokens.length) continue;
|
|
22
|
+
const close = tokens[endIndex];
|
|
23
|
+
const firstContent = tokens.slice(startIndex, endIndex + 1).find((token) => token.type === "inline");
|
|
24
|
+
if (!firstContent) continue;
|
|
25
|
+
const match = firstContent.content.match(markerRE);
|
|
26
|
+
if (!match) continue;
|
|
27
|
+
const type = match[1].toLowerCase();
|
|
28
|
+
const title = match[2]?.trim() || titleMark[type] || capitalize(type);
|
|
29
|
+
firstContent.content = firstContent.content.slice(match[0].length).trimStart();
|
|
30
|
+
open.type = "github_alert_open";
|
|
31
|
+
open.tag = "div";
|
|
32
|
+
open.meta = {
|
|
33
|
+
title,
|
|
34
|
+
type
|
|
35
|
+
};
|
|
36
|
+
close.type = "github_alert_close";
|
|
37
|
+
close.tag = "div";
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
md.renderer.rules.github_alert_open = function(tokens, idx) {
|
|
41
|
+
const { title, type } = tokens[idx].meta;
|
|
42
|
+
return `<div class="${type} custom-block github-alert"><p class="custom-block-title">${title}</p>\n`;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function capitalize(str) {
|
|
46
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { gitHubAlertsPlugin };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
//#region src/plugins/image.d.ts
|
|
3
|
+
interface ImagePluginOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Support native lazy loading for the `<img>` tag.
|
|
6
|
+
* @default false
|
|
7
|
+
*/
|
|
8
|
+
lazyLoading?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare function imagePlugin(md: MarkdownIt, { lazyLoading }?: ImagePluginOptions): void;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { ImagePluginOptions, imagePlugin };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EXTERNAL_URL_RE } from "../shared.js";
|
|
2
|
+
//#region src/plugins/image.ts
|
|
3
|
+
function imagePlugin(md, { lazyLoading } = {}) {
|
|
4
|
+
const imageRule = md.renderer.rules.image;
|
|
5
|
+
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
6
|
+
const token = tokens[idx];
|
|
7
|
+
let url = token.attrGet("src");
|
|
8
|
+
if (url && !EXTERNAL_URL_RE.test(url)) {
|
|
9
|
+
if (!/^\.?\//.test(url)) url = `./${url}`;
|
|
10
|
+
token.attrSet("src", decodeURIComponent(url));
|
|
11
|
+
}
|
|
12
|
+
if (lazyLoading) token.attrSet("loading", "lazy");
|
|
13
|
+
return imageRule(tokens, idx, options, env, self);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { imagePlugin };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
//#region src/plugins/link.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* 让 markdown 中的外部链接在新标签页打开。
|
|
5
|
+
*
|
|
6
|
+
* 仅对带协议(http(s)://、mailto: 等)的外部链接生效,
|
|
7
|
+
* 站内相对链接保持默认行为,避免影响路由内跳转与锚点导航。
|
|
8
|
+
*
|
|
9
|
+
* 已通过 `markdown-it-attrs` 等方式显式声明的 `target` / `rel`
|
|
10
|
+
* 不会被覆盖,保留作者对单个链接的控制权。
|
|
11
|
+
*/
|
|
12
|
+
declare function linkPlugin(md: MarkdownIt): void;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { linkPlugin };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { EXTERNAL_URL_RE } from "../shared.js";
|
|
2
|
+
//#region src/plugins/link.ts
|
|
3
|
+
/**
|
|
4
|
+
* 让 markdown 中的外部链接在新标签页打开。
|
|
5
|
+
*
|
|
6
|
+
* 仅对带协议(http(s)://、mailto: 等)的外部链接生效,
|
|
7
|
+
* 站内相对链接保持默认行为,避免影响路由内跳转与锚点导航。
|
|
8
|
+
*
|
|
9
|
+
* 已通过 `markdown-it-attrs` 等方式显式声明的 `target` / `rel`
|
|
10
|
+
* 不会被覆盖,保留作者对单个链接的控制权。
|
|
11
|
+
*/
|
|
12
|
+
function linkPlugin(md) {
|
|
13
|
+
const defaultLinkRender = md.renderer.rules.link_open || ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
|
|
14
|
+
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
|
15
|
+
const token = tokens[idx];
|
|
16
|
+
const href = token.attrGet("href");
|
|
17
|
+
if (href && EXTERNAL_URL_RE.test(href)) {
|
|
18
|
+
if (!token.attrGet("target")) token.attrSet("target", "_blank");
|
|
19
|
+
if (!token.attrGet("rel")) token.attrSet("rel", "noopener noreferrer");
|
|
20
|
+
}
|
|
21
|
+
return defaultLinkRender(tokens, idx, options, env, self);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { linkPlugin };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
//#region src/plugins/pre-wrapper.d.ts
|
|
3
|
+
interface Options {
|
|
4
|
+
hasSingleTheme: boolean;
|
|
5
|
+
}
|
|
6
|
+
declare function preWrapperPlugin(md: MarkdownIt, options: Options): void;
|
|
7
|
+
declare function getAdaptiveThemeMarker(options: Options): "" | " ant-code-theme";
|
|
8
|
+
declare function extractTitle(info: string, html?: boolean): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { Options, extractTitle, getAdaptiveThemeMarker, preWrapperPlugin };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/plugins/pre-wrapper.ts
|
|
2
|
+
function preWrapperPlugin(md, options) {
|
|
3
|
+
const fence = md.renderer.rules.fence;
|
|
4
|
+
md.renderer.rules.fence = (...args) => {
|
|
5
|
+
const [tokens, idx] = args;
|
|
6
|
+
const token = tokens[idx];
|
|
7
|
+
token.info = token.info.replace(/\[.*\]/, "");
|
|
8
|
+
const active = / active(?: |$)/.test(token.info) ? " active" : "";
|
|
9
|
+
token.info = token.info.replace(/ active$/, "").replace(/ active /, " ");
|
|
10
|
+
const lang = extractLang(token.info);
|
|
11
|
+
const rawCode = fence(...args);
|
|
12
|
+
return `<div class="language-${lang}${getAdaptiveThemeMarker(options)}${active}"><button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}</div>`;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function getAdaptiveThemeMarker(options) {
|
|
16
|
+
return options.hasSingleTheme ? "" : " ant-code-theme";
|
|
17
|
+
}
|
|
18
|
+
function extractTitle(info, html = false) {
|
|
19
|
+
if (html) return info.replace(/<!--[\s\S]*?-->/g, "").match(/data-title="(.*?)"/)?.[1] || "";
|
|
20
|
+
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || "txt";
|
|
21
|
+
}
|
|
22
|
+
function extractLang(info) {
|
|
23
|
+
return info.trim().replace(/=(\d*)/, "").replace(/:(no-)?line-numbers(\{| |$|=\d*).*/, "").replace(/(-vue|\{| ).*$/, "").replace(/^vue-html$/, "template").replace(/^ansi$/, "");
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { extractTitle, getAdaptiveThemeMarker, preWrapperPlugin };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/plugins/stackblitz.ts
|
|
2
|
+
function stackblitzPlugin(md) {
|
|
3
|
+
const fence = md.renderer.rules.fence;
|
|
4
|
+
md.renderer.rules.fence = (...args) => {
|
|
5
|
+
const [tokens, idx] = args;
|
|
6
|
+
const token = tokens[idx];
|
|
7
|
+
const info = token.info.trim();
|
|
8
|
+
if (info.startsWith("stackblitz")) {
|
|
9
|
+
const code = token.content;
|
|
10
|
+
const titleMatch = info.match(/\{[^}]*title\s*=\s*"([^"]*)"[^}]*\}/);
|
|
11
|
+
const title = titleMatch ? titleMatch[1] : "";
|
|
12
|
+
return `<stackblitz code="${encodeURIComponent(code)}" title="${escapeHtml(title)}"></stackblitz>`;
|
|
13
|
+
}
|
|
14
|
+
return fence(...args);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function escapeHtml(str) {
|
|
18
|
+
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { stackblitzPlugin };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/plugins/table.ts
|
|
2
|
+
function tablePlugin(md) {
|
|
3
|
+
md.core.ruler.push("table_api_attribute", (state) => {
|
|
4
|
+
const tokens = state.tokens;
|
|
5
|
+
let inApiSection = false;
|
|
6
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
7
|
+
const token = tokens[i];
|
|
8
|
+
if (token.type === "heading_open" && token.tag === "h2") {
|
|
9
|
+
const inlineToken = tokens[i + 1];
|
|
10
|
+
if (inlineToken && inlineToken.type === "inline") {
|
|
11
|
+
if (inlineToken.content.trim().toLowerCase() === "api") inApiSection = true;
|
|
12
|
+
else inApiSection = false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (inApiSection && token.type === "table_open") {
|
|
16
|
+
const existingClass = token.attrGet("class") || "";
|
|
17
|
+
const newClass = existingClass ? `${existingClass} component-table-api` : "component-table-api";
|
|
18
|
+
token.attrSet("class", newClass);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { tablePlugin };
|
package/dist/shared.d.ts
ADDED
package/dist/shared.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/shared.ts
|
|
2
|
+
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
|
|
3
|
+
const SCRIPT_REGEX = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
|
|
4
|
+
const STYLE_REGEX = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
|
|
5
|
+
const DOCS_REGEX = /<docs\b[^>]*>[\s\S]*?<\/docs>/gi;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { DOCS_REGEX, EXTERNAL_URL_RE, SCRIPT_REGEX, STYLE_REGEX };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/utils/short-hash.ts
|
|
2
|
+
const BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
3
|
+
function toBase62(num) {
|
|
4
|
+
let str = "";
|
|
5
|
+
do {
|
|
6
|
+
str = BASE62[num % 62] + str;
|
|
7
|
+
num = Math.floor(num / 62);
|
|
8
|
+
} while (num > 0);
|
|
9
|
+
return str;
|
|
10
|
+
}
|
|
11
|
+
function shortHash(str) {
|
|
12
|
+
const bytes = new TextEncoder().encode(str);
|
|
13
|
+
let hash = 2166136261;
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
hash ^= bytes[i];
|
|
16
|
+
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
|
|
17
|
+
}
|
|
18
|
+
return toBase62(hash >>> 0);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { shortHash };
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@antdv-next/docs-plugins",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"private": false,
|
|
6
|
+
"description": "Vite / markdown-it plugins and demo components for docs-base documentation sites",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/antdv-next/docs-plugins.git"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./dist/*": {
|
|
19
|
+
"types": "./dist/*.d.ts",
|
|
20
|
+
"import": "./dist/*.js",
|
|
21
|
+
"default": "./dist/*.js"
|
|
22
|
+
},
|
|
23
|
+
"./component/*": {
|
|
24
|
+
"types": "./src/components/*.d.ts",
|
|
25
|
+
"import": "./src/components/*"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"README.md",
|
|
33
|
+
"dist",
|
|
34
|
+
"package.json",
|
|
35
|
+
"src"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@mdit-vue/plugin-frontmatter": "^3.0.2",
|
|
39
|
+
"@mdit-vue/plugin-headers": "^3.0.2",
|
|
40
|
+
"@mdit-vue/plugin-title": "^3.0.2",
|
|
41
|
+
"@mdit-vue/plugin-toc": "^3.0.2",
|
|
42
|
+
"@mdit-vue/shared": "^3.0.2",
|
|
43
|
+
"@mdit-vue/types": "^3.0.2",
|
|
44
|
+
"@shikijs/markdown-it": "^4.4.1",
|
|
45
|
+
"@shikijs/transformers": "^4.4.1",
|
|
46
|
+
"lru-cache": "^11.5.2",
|
|
47
|
+
"markdown-it": "^14.3.0",
|
|
48
|
+
"markdown-it-anchor": "^9.2.1",
|
|
49
|
+
"markdown-it-async": "^2.2.0",
|
|
50
|
+
"markdown-it-attrs": "^4.5.0",
|
|
51
|
+
"markdown-it-container": "^4.0.0",
|
|
52
|
+
"markdown-it-emoji": "^3.1.0",
|
|
53
|
+
"mlly": "^1.8.2",
|
|
54
|
+
"nanoid": "^5.1.16",
|
|
55
|
+
"oxfmt": "^0.41.0",
|
|
56
|
+
"pathe": "^2.0.3",
|
|
57
|
+
"picomatch": "^4.0.5",
|
|
58
|
+
"postcss-selector-parser": "^7.1.4",
|
|
59
|
+
"shiki": "^4.4.1",
|
|
60
|
+
"vite": "^8.2.0",
|
|
61
|
+
"vue": "^3.5.40"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@antdv-next/icons": "^1.1.1",
|
|
65
|
+
"@antfu/eslint-config": "^7.7.3",
|
|
66
|
+
"@codesandbox/sandpack-themes": "^2.0.21",
|
|
67
|
+
"@types/markdown-it": "14.1.2",
|
|
68
|
+
"@types/markdown-it-attrs": "^4.1.3",
|
|
69
|
+
"@types/markdown-it-container": "^2.0.11",
|
|
70
|
+
"@types/node": "^25.9.5",
|
|
71
|
+
"@types/picomatch": "^4.0.3",
|
|
72
|
+
"@vue/compiler-sfc": "^3.5.40",
|
|
73
|
+
"@vue/tsconfig": "^0.8.1",
|
|
74
|
+
"@vueuse/core": "^14.4.0",
|
|
75
|
+
"antdv-style": "^1.0.0-rc.1",
|
|
76
|
+
"eslint": "^9.39.5",
|
|
77
|
+
"postcss": "^8.5.25",
|
|
78
|
+
"sandpack-vue3": "^3.1.12",
|
|
79
|
+
"sucrase": "^3.35.1",
|
|
80
|
+
"tsdown": "^0.22.14",
|
|
81
|
+
"typescript": "~5.9.3",
|
|
82
|
+
"vue-router": "^4.6.4"
|
|
83
|
+
},
|
|
84
|
+
"publishConfig": {
|
|
85
|
+
"access": "public"
|
|
86
|
+
},
|
|
87
|
+
"scripts": {
|
|
88
|
+
"build": "tsdown",
|
|
89
|
+
"dev": "tsdown --watch",
|
|
90
|
+
"lint": "eslint .",
|
|
91
|
+
"lint:fix": "eslint --fix .",
|
|
92
|
+
"typecheck": "tsc --noEmit"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SandpackCodeEditor, useActiveCode, useSandpack } from 'sandpack-vue3'
|
|
3
|
+
import { watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
readOnly?: boolean
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
(e: 'update:code', code: string): void
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const { code } = useActiveCode()
|
|
14
|
+
const { sandpack } = useSandpack()
|
|
15
|
+
|
|
16
|
+
function resetCode(newCode: string) {
|
|
17
|
+
sandpack.updateCurrentFile(newCode)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
defineExpose({ resetCode })
|
|
21
|
+
|
|
22
|
+
watch(code, (val) => {
|
|
23
|
+
if (val !== undefined) {
|
|
24
|
+
emit('update:code', val)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<SandpackCodeEditor
|
|
31
|
+
:show-tabs="false"
|
|
32
|
+
:show-line-numbers="false"
|
|
33
|
+
:read-only="readOnly"
|
|
34
|
+
style="min-height: 100px;"
|
|
35
|
+
/>
|
|
36
|
+
</template>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as Vue from 'vue'
|
|
2
|
+
|
|
3
|
+
let compilerSfc: typeof import('@vue/compiler-sfc') | null = null
|
|
4
|
+
let sucraseModule: typeof import('sucrase') | null = null
|
|
5
|
+
|
|
6
|
+
// 演示源码可 import 的模块由站点注入,逐模块容错:
|
|
7
|
+
// 某个模块不可用只影响其自身的编辑预览,不阻断整个编译链路
|
|
8
|
+
const resolvedExtraModules = new WeakMap<Record<string, () => Promise<any>>, Record<string, any>>()
|
|
9
|
+
|
|
10
|
+
async function ensureDependencies(extraModules: Record<string, () => Promise<any>>) {
|
|
11
|
+
if (!compilerSfc) {
|
|
12
|
+
compilerSfc = await import('@vue/compiler-sfc')
|
|
13
|
+
}
|
|
14
|
+
if (!sucraseModule) {
|
|
15
|
+
sucraseModule = await import('sucrase')
|
|
16
|
+
}
|
|
17
|
+
if (!resolvedExtraModules.has(extraModules)) {
|
|
18
|
+
const resolved: Record<string, any> = {}
|
|
19
|
+
await Promise.all(
|
|
20
|
+
Object.entries(extraModules).map(async ([name, loader]) => {
|
|
21
|
+
try {
|
|
22
|
+
const mod = await loader()
|
|
23
|
+
resolved[name] = (mod as any).default || mod
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// 依赖不可用时跳过,保持 modulesMap 中无该条目
|
|
27
|
+
}
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
resolvedExtraModules.set(extraModules, resolved)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildModulesMap(extraModules: Record<string, () => Promise<any>>) {
|
|
35
|
+
return {
|
|
36
|
+
vue: Vue,
|
|
37
|
+
...resolvedExtraModules.get(extraModules),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function transformCode(code: string): string {
|
|
42
|
+
let result = code
|
|
43
|
+
|
|
44
|
+
// Named imports: import { a, b as c } from 'module'
|
|
45
|
+
// Need to convert 'as' to ':' for destructuring syntax
|
|
46
|
+
result = result.replace(
|
|
47
|
+
/import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]\s*;?/g,
|
|
48
|
+
(_, names, source) => {
|
|
49
|
+
const transformedNames = names.replace(/\bas\b/g, ':')
|
|
50
|
+
return `const {${transformedNames}} = (__modules__["${source}"] || {});`
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// Default import: import Name from 'module'
|
|
55
|
+
result = result.replace(
|
|
56
|
+
/import\s+(\w+)\s+from\s*['"]([^'"]+)['"]\s*;?/g,
|
|
57
|
+
(_, name, source) => `const ${name} = (__modules__["${source}"] || {}).default || (__modules__["${source}"] || {});`,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Namespace import: import * as Name from 'module'
|
|
61
|
+
result = result.replace(
|
|
62
|
+
/import\s*\*\s*as\s+(\w+)\s+from\s*['"]([^'"]+)['"]\s*;?/g,
|
|
63
|
+
(_, name, source) => `const ${name} = (__modules__["${source}"] || {});`,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Side-effect imports: import 'module'
|
|
67
|
+
result = result.replace(/^\s*import\s*['"][^'"]+['"]\s*(?:;\s*)?$/gm, '')
|
|
68
|
+
|
|
69
|
+
// export default → __exports__.default =
|
|
70
|
+
result = result.replace(/export\s+default\s+/g, '__exports__.default = ')
|
|
71
|
+
|
|
72
|
+
// export function name → function name + track for later export
|
|
73
|
+
const exportedNames: string[] = []
|
|
74
|
+
result = result.replace(/export\s+function\s+(\w+)/g, (_, name) => {
|
|
75
|
+
exportedNames.push(name)
|
|
76
|
+
return `function ${name}`
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
result = result.replace(/export\s+(const|let|var)\s+(\w+)/g, (_, keyword, name) => {
|
|
80
|
+
exportedNames.push(name)
|
|
81
|
+
return `${keyword} ${name}`
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Append exports for named declarations
|
|
85
|
+
for (const name of exportedNames) {
|
|
86
|
+
result += `\n__exports__["${name}"] = ${name};`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function compileSfcSource(
|
|
93
|
+
source: string,
|
|
94
|
+
extraModules: Record<string, () => Promise<any>> = {},
|
|
95
|
+
): Promise<{ component: any, error: string | null }> {
|
|
96
|
+
await ensureDependencies(extraModules)
|
|
97
|
+
|
|
98
|
+
const modulesMap = buildModulesMap(extraModules)
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { compileScript, compileTemplate, parse } = compilerSfc!
|
|
102
|
+
const { transform } = sucraseModule!
|
|
103
|
+
|
|
104
|
+
const id = `live-${Math.random().toString(36).slice(2, 8)}`
|
|
105
|
+
const { descriptor, errors } = parse(source, { filename: 'Demo.vue' })
|
|
106
|
+
|
|
107
|
+
if (errors.length > 0) {
|
|
108
|
+
return { component: null, error: errors.map(e => e.message).join('\n') }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let jsCode: string
|
|
112
|
+
|
|
113
|
+
if (descriptor.scriptSetup) {
|
|
114
|
+
// <script setup> — use inlineTemplate to bake render into setup()
|
|
115
|
+
const compiled = compileScript(descriptor, {
|
|
116
|
+
id,
|
|
117
|
+
inlineTemplate: true,
|
|
118
|
+
})
|
|
119
|
+
jsCode = compiled.content
|
|
120
|
+
}
|
|
121
|
+
else if (descriptor.script) {
|
|
122
|
+
// Options API <script>
|
|
123
|
+
const compiled = compileScript(descriptor, { id })
|
|
124
|
+
jsCode = compiled.content
|
|
125
|
+
|
|
126
|
+
if (descriptor.template) {
|
|
127
|
+
const templateResult = compileTemplate({
|
|
128
|
+
source: descriptor.template.content,
|
|
129
|
+
filename: 'Demo.vue',
|
|
130
|
+
id,
|
|
131
|
+
compilerOptions: {
|
|
132
|
+
bindingMetadata: compiled.bindings,
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
if (templateResult.errors.length) {
|
|
136
|
+
return {
|
|
137
|
+
component: null,
|
|
138
|
+
error: templateResult.errors
|
|
139
|
+
.map(e => (typeof e === 'string' ? e : e.message))
|
|
140
|
+
.join('\n'),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
jsCode += `\n${templateResult.code}`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (descriptor.template) {
|
|
147
|
+
// Template-only component
|
|
148
|
+
const templateResult = compileTemplate({
|
|
149
|
+
source: descriptor.template.content,
|
|
150
|
+
filename: 'Demo.vue',
|
|
151
|
+
id,
|
|
152
|
+
})
|
|
153
|
+
if (templateResult.errors.length) {
|
|
154
|
+
return {
|
|
155
|
+
component: null,
|
|
156
|
+
error: templateResult.errors
|
|
157
|
+
.map(e => (typeof e === 'string' ? e : e.message))
|
|
158
|
+
.join('\n'),
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
jsCode = templateResult.code
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
return { component: null, error: 'No template or script found' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Strip TypeScript type annotations
|
|
168
|
+
try {
|
|
169
|
+
const result = transform(jsCode, {
|
|
170
|
+
transforms: ['typescript'],
|
|
171
|
+
disableESTransforms: true,
|
|
172
|
+
})
|
|
173
|
+
jsCode = result.code
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// May already be plain JS — continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Transform imports / exports to work with new Function
|
|
180
|
+
jsCode = transformCode(jsCode)
|
|
181
|
+
|
|
182
|
+
// Evaluate the compiled code
|
|
183
|
+
const __exports__: Record<string, any> = {}
|
|
184
|
+
// eslint-disable-next-line no-new-func
|
|
185
|
+
const fn = new Function('__modules__', '__exports__', jsCode)
|
|
186
|
+
fn(modulesMap, __exports__)
|
|
187
|
+
|
|
188
|
+
// For options API + separate template, attach the render function
|
|
189
|
+
if (!descriptor.scriptSetup && descriptor.script && descriptor.template) {
|
|
190
|
+
const comp = __exports__.default || {}
|
|
191
|
+
if (__exports__.render) {
|
|
192
|
+
comp.render = __exports__.render
|
|
193
|
+
}
|
|
194
|
+
return { component: comp, error: null }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For template-only, create component with render
|
|
198
|
+
if (!descriptor.scriptSetup && !descriptor.script && descriptor.template) {
|
|
199
|
+
return { component: { render: __exports__.render }, error: null }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { component: __exports__.default, error: null }
|
|
203
|
+
}
|
|
204
|
+
catch (e: any) {
|
|
205
|
+
return { component: null, error: e.message || String(e) }
|
|
206
|
+
}
|
|
207
|
+
}
|