vitepress-theme-demoblock
Version:
vitepress-theme-demoblock
215 lines (208 loc) • 7.63 kB
JavaScript
import mdContainer from 'markdown-it-container';
import path from 'path';
import fs from 'node:fs';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkFrontmatter from 'remark-frontmatter';
import remarkDirective from 'remark-directive';
import remarkStringify from 'remark-stringify';
import { visit } from 'unist-util-visit';
import os from 'os';
import h from 'hash-sum';
const blockPlugin = (md, options) => {
md.use(mdContainer, "demo", {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
const componentName = m && m.length > 1 ? m[1] : "";
const content = tokens[idx + 1].type === "fence" ? tokens[idx + 1].content : "";
const contents = content.replace("<client-only>", "").replace("</client-only>", "");
const stringOptions = JSON.stringify(options, function(key, value) {
if (typeof value === "function") {
return value.toString();
} else {
return value;
}
});
return `<demo customClass="${options?.customClass || ""}" sourceCode="${md.utils.escapeHtml(
contents
)}" options='${stringOptions}'><${componentName} />`;
}
return "</demo>";
}
});
};
const codePlugin = (md, options) => {
const defaultRender = md.renderer.rules.fence;
md.renderer.rules.fence = (tokens, idx, options2, env, self) => {
const token = tokens[idx];
const prevToken = tokens[idx - 1];
const isInDemoContainer = prevToken && prevToken.nesting === 1 && prevToken.info.trim().match(/^demo\s*(.*)$/);
const lang = token.info.trim();
if (isInDemoContainer) {
const content = token.content.replace("<client-only>", "").replace("</client-only>", "");
return `
<template #highlight>
<div v-pre class="language-${lang} vp-adaptive-theme">
<span class="lang">${lang}</span>
${md.options.highlight?.(content, lang, "") || ""}
</div>
</template>`;
}
return defaultRender?.(tokens, idx, options2, env, self);
};
};
const demoblock = (md, options = {}) => {
md.use(blockPlugin, options);
md.use(codePlugin, options);
};
const ScriptSetupMatchPattern = /(<script\s(.*\s)?setup(\s.*)?>)([\s\S]*)(<\/script>)/;
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g;
const rangeRE = /\{(\d*),(\d*)\}$/;
function processIncludes(code, file, root) {
return code.replace(includesRE, (m, m1) => {
if (!m1.length)
return m;
const range = m1.match(rangeRE);
range && (m1 = m1.slice(0, -range[0].length));
const atPresent = m1[0] === "@";
try {
const includePath = atPresent ? path.join(root, m1.slice(m1[1] === "/" ? 2 : 1)) : path.join(path.dirname(file), m1);
let content = fs.readFileSync(includePath, "utf-8");
if (range) {
const [, startLine, endLine] = range;
const lines = content.split(/\r?\n/);
content = lines.slice(
startLine ? parseInt(startLine, 10) - 1 : void 0,
endLine ? parseInt(endLine, 10) : void 0
).join("\n");
}
return processIncludes(content, includePath, root);
} catch (error) {
return m;
}
});
}
const codePattern = /.md.demo.[a-zA-Z0-9]+\.(vue|jsx|tsx)$/;
const hash = (val) => h(val);
const combineVirtualFilename = (id, name, lang) => `${id}.demo.${name}.${lang}`;
async function transformCodeToComponent(id, code, options) {
const blocks = [];
function remarkDemo() {
return (tree) => {
let seed = 0;
const scriptSetup = {
index: -1,
content: ""
};
visit(tree, (node, index) => {
if (node.type === "html") {
const matches = node.value.match(ScriptSetupMatchPattern);
if (!matches)
return;
scriptSetup.index = index;
scriptSetup.content = matches?.[4] ?? "";
}
if (node.type === "containerDirective" && node.name === "demo") {
seed++;
const name = hash(`${id}-demo-${seed}`);
blocks.push({
lang: node.children[0]?.lang,
value: node.children[0]?.value,
name
});
node.name = `demo render-demo-i${name}`;
}
});
if (blocks.length <= 0)
return;
const appendCode = blocks.map((block) => {
const filename = combineVirtualFilename(id, block.name, block.lang);
return `import RenderDemoI${block.name} from '${filename}'`;
}).join(os.EOL);
if (scriptSetup.index !== -1) {
const node = tree.children[scriptSetup.index];
node.value = node.value.replace(
ScriptSetupMatchPattern,
(match, p1, p2, p3, p4, p5) => `${p1}${os.EOL}${appendCode}${os.EOL}${p4}${p5}`
);
} else {
tree.children.push({
type: "html",
value: `<script setup>${os.EOL}${appendCode}${os.EOL}<\/script>`
});
}
};
}
code = processIncludes(code, id, options.root);
const file = await unified().use(remarkParse).use(remarkFrontmatter).use(remarkDirective).use(remarkStringify).use(remarkDemo).process(code);
for (const block of blocks) {
const filename = combineVirtualFilename(id, block.name, block.lang);
const blockId = "/" + path.relative(options.root, filename);
block.absId = filename;
block.relId = blockId;
Demoblocks.set(blockId, block.value);
}
const fileId = "/" + path.relative(options.root, id);
const _blocks = blocks.map(({ lang, name, id: id2 }) => ({ lang, name, id: id2 }));
FileCaches.set(fileId, _blocks);
let c = String(file).replace(/\\:::/g, ":::");
c = c.replace(/\\:/g, ":");
return { code: c, blocks };
}
const Demoblocks = /* @__PURE__ */ new Map();
const FileCaches = /* @__PURE__ */ new Map();
function VitePluginDemoblock() {
const options = {
env: "vitepress",
root: ""
};
return {
name: "vite-plugin-demoblock",
enforce: "pre",
async configResolved(config) {
const isVitepress = config.plugins.find((p) => p.name === "vitepress");
options.env = isVitepress ? "vitepress" : "vite";
options.root = config.root;
},
resolveId(id) {
if (codePattern.test(id)) {
return id;
}
},
load(id) {
if (codePattern.test(id)) {
const blockId = "/" + path.relative(options.root, id);
return Demoblocks.get(id) || Demoblocks.get(blockId);
}
},
async transform(code, id) {
if (id.endsWith(".md")) {
const { code: transformedCode } = await transformCodeToComponent(id, code, options);
return { code: transformedCode, map: null };
}
},
async handleHotUpdate(ctx) {
const { file, server, timestamp } = ctx;
if (file.endsWith(".md")) {
const { blocks } = await transformCodeToComponent(
file,
fs.readFileSync(file, "utf8"),
options
);
const invalidatedModules = /* @__PURE__ */ new Set();
for (const block of blocks) {
const blockId = block.absId;
const mod = server.moduleGraph.getModuleById(blockId);
if (mod) {
server.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
}
}
}
}
};
}
export { VitePluginDemoblock, blockPlugin, codePlugin, demoblock as default, demoblock as demoBlockPlugin, demoblock, demoblock as demoblockPlugin, VitePluginDemoblock as demoblockVitePlugin };