vitepress组件示例实现

奴止
Oct 10, 2022
Last edited: 2022-10-12
type
Post
status
Published
date
Oct 10, 2022
slug
vitepress-plugin-demo
summary
记录笔记:如何一步步实现 vitepress 中组件示例功能。
tags
前端开发
vite
vue
category
技术随手记
icon
password
Property
Oct 12, 2022 02:56 PM

前置知识

markdown-it 相关的可以参考文末链接。

支持语法

这部分列举想要实现的语法。

html 格式

 
  • 外部文件
<Demo title="标题" desc="描述信息" src="./my/path/of/demo.vue" />
 
  • 行内代码
<Demo title="标题" desc="描述信息"> <CustomComponent /> <!-- 其它内容 --> <div>others</div> </Demo>
 

markdown container 格式

  • 外部文件
::: demo 标题 src="./my/path/of/demo.vue" 描述文字。 :::
 
  • 行内代码
::: demo the Demo Title Some markdown text render as **`desc`**. ```vue <template> <DemoButton @click="onClick">Click: {{counter}}</DemoButton> </template> <script setup lang="ts"> import { ref } from 'vue' const counter = ref(0) const onClick = () => counter.value++ </script> ``` :::
 

实现

Demo 渲染组件

 
就是一个简单的 vue 组件,接收 langtitledesc 等属性:
<template> <div class="demo-block"> <header class="demo-block__header"> <slot name="title"></slot> </header> <section class="demo-block__desc"> <slot name="desc"><div v-html="desc"></div></slot> </section> <main class="demo-block__preview"> <slot></slot> </main> <footer class="demo-block__code"> <div :class="`language-${lang} `" v-html="decodeURIComponent(highlightedCode)" ></div> </footer> </div> </template> <script lang="ts" setup> withDefaults( defineProps<{ title?: string; desc?: string; lang?: string; highlightedCode?: string; src?: string; }>(), { title: "", desc: "", lang: "vue", } ); </script>
 
在 vitepress 的主题配置中全局引入:
// .vitepress/theme/index.ts import DefaultTheme from 'vitepress/theme' import DemoBlock from '../components/DemoBlock.vue' export default { ...DefaultTheme, enhanceApp({ app, router, siteData }) { app.component('Demo', DemoBlock) } }
 
这样在 .md 文件中就可以直接这样书写,并渲染出示例:
<Demo title="示例"> 示例内容<em>test</em> </Demo>
 

外部文件处理

外部文件示例(通过 src 配置)只需要处理:
  1. 注入该文件依赖为 import XXX from ${src} 语法,XXX 为自动生成的名称,同时会修改示例代码为 <Demo><XXX /></Demo>
  1. 代码片段展示,通过 node.js 的 fs 模块相关 API 直接读取文件内容即可。
 

markdown container 行内代码处理

行内代码示例(如直接写在 <Demo> 内部的代码)通过 vite 插件的虚拟模块(Virtual Modules)来实现,其中用到了 vue/compiler-sfc 中的 compileScript 等 API。
 
首先定义处理行内代码的 vite 插件:
import { parse, compileTemplate, compileScript } from "vue/compiler-sfc"; const virtualModuleId = "virtual:my-module"; const resolvedVirtualModuleId = "\0" + virtualModuleId; const map = {}; const REGEXP_SCRIPT_TAG = /<script\s+/; const hasScriptTag = (content: string) => REGEXP_SCRIPT_TAG.test(content); function PluginParseDemo() { return { name: "plugin-parse-demo", // required, will show up in warnings and errors resolveId(id) { if (id.startsWith(virtualModuleId)) { return resolvedVirtualModuleId + id.substring(virtualModuleId.length); } }, load(id) { if (id.startsWith(resolvedVirtualModuleId)) { const cmpName = id.replace(resolvedVirtualModuleId + "/", ""); const rawCode = map[cmpName]; const { descriptor } = parse(rawCode); if (hasScriptTag(rawCode)) { return compileScript(descriptor, { inlineTemplate: true, id, templateOptions: { source: descriptor.template?.content, }, }).content; } const res = compileTemplate({ source: descriptor.template?.content || rawCode, id, filename: cmpName, }).code; return `${res}\nexport default render`; } }, transform(src, id) { if (id.startsWith(resolvedVirtualModuleId)) { return transform(src, { transforms: ["typescript"] }); } }, }; }
 
然后在 vitepress 的配置中引入:
export default defineConfig({ // 其它配置已省略 vite: { plugins: [PluginParseDemo()], }, });
 

html 格式

只需要在html代码渲染时进行处理即可,重写 md.renderer.rules.html_inlinemd.renderer.rules.html_block
// .vitepress/config.ts // 这里仅示例 html_inline export default defineConfig({ // 其它配置已省略 markdown: { config(md) { const defaultRender = md.renderer.rules.html_inline; md.renderer.rules.html_inline = (tokens, idx, options, env, self) => { const { content } = tokens[idx]; // 不匹配的走默认渲染 if (!/^<Demo\s+/.test(content)) { return defaultRender(tokens, idx, options, env, self); } // `parseDemo` 中利用 `@vue/compiler-core` 的 `baseParse` 解析模板内容 // 并分离出各部分代码 // 标题: title // 描述: desc // 示例代码: demoContent const { demoContent, slotContent, src, title, desc } = parseDemo( content.trim() ); // 添加必要的 setup script // 将行内代码重装为 虚拟模块(vite 自定义插件会进行处理) const { cmpName, rawCode, highlightedCode, lang } = makeSetupScript( env, md, { src, code: demoContent } ); // 拼装出最终用于渲染示例的代码 return `<Demo title="${title}" desc="${desc}" lang="${lang}" code="${encodeURIComponent( rawCode )}" highlightedCode="${encodeURIComponent( highlightedCode )}">\n<${cmpName} />${slotContent}</Demo>`; }; }, }, });

markdown container 格式

扩展 markdown-it-container,支持 ::: demo 这样的语法:
import container from "markdown-it-container"; export default defineConfig({ // 其它配置省略 markdown: { lineNumbers: true, config(md) { md.use(container, "demo", { render: (tokens: Token[], idx: number, options, env, renderer) => { if (tokens[idx].nesting === 1) { const m = tokens[idx].info.trim().match(/^demo\s+(.*)$/); const title = m[1].replace(REGEXP_SRC, ""); const src = m[1].match(REGEXP_SRC); const srcPath = src ? src[1] : ""; let fenceIndex = tokens .slice(idx) .findIndex((item) => item.type === "fence"); fenceIndex = fenceIndex === -1 ? -1 : fenceIndex + idx; if (fenceIndex < 0 && !srcPath) { throw new Error("should specified `src` attrbute or code block!"); } const isDemoImportFile = !!srcPath; if (!isDemoImportFile) { tokens[fenceIndex].hidden = true; } // 与 html_inline 类似不再赘述 const { rawCode, highlightedCode, lang, cmpName } = makeSetupScript( env, md, isDemoImportFile ? { src: srcPath } : { code: tokens[fenceIndex].content } ); // opening tag return `<Demo title="${md.utils.escapeHtml( title )}" lang="${lang}" code="${encodeURIComponent( rawCode )}" highlightedCode="${encodeURIComponent( highlightedCode )}">\n<${cmpName} /><template #desc >`; } else { // closing tag return "</template></Demo>\n"; } }, }); }, }, });

参考

 
CSS实现文本溢出移动端适配