UNPKG

vitepress-theme-demoblock

Version:
215 lines (208 loc) 7.63 kB
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 };