diff --git a/packages/docs/helper/vue-utils.ts b/packages/docs/helper/vue-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3bcb2be691648f0e3a516000ed0c4cef6ca1667 --- /dev/null +++ b/packages/docs/helper/vue-utils.ts @@ -0,0 +1,108 @@ +import { parse, NodeTypes, type ElementNode } from '@vue/compiler-dom'; +import { type SFCDescriptor, type SFCBlock, type SFCStyleBlock, type SFCTemplateBlock, type SFCScriptBlock } from '@vue/compiler-sfc'; + +function hasSrc(node: ElementNode) { + return node.props.some((p) => { + if (p.type !== NodeTypes.ATTRIBUTE) { + return false; + } + return p.name === 'src'; + }); +} + +function isEmpty(node: ElementNode) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if (child.type !== NodeTypes.TEXT || child.content.trim() !== '') { + return false; + } + } + return true; +} + +function createBlock(node: ElementNode, source: string): SFCBlock { + const type = node.tag; + const loc = node.innerLoc!; + const attrs: Record = {}; + const block: SFCBlock = { + type, + content: source.slice(loc.start.offset, loc.end.offset), + loc, + attrs, + }; + node.props.forEach((p) => { + if (p.type === NodeTypes.ATTRIBUTE) { + const name = p.name; + attrs[name] = p.value ? p.value.content || true : true; + if (name === 'lang') { + block.lang = p.value && p.value.content; + } else if (name === 'src') { + block.src = p.value && p.value.content; + } else if (type === 'style') { + if (name === 'scoped') { + (block as SFCStyleBlock).scoped = true; + } else if (name === 'module') { + (block as SFCStyleBlock).module = attrs[name]; + } + } else if (type === 'script' && name === 'setup') { + (block as SFCScriptBlock).setup = attrs.setup; + } + } + }); + return block; +} + +const sfcCache = new Map(); +// 由于 @vue/compiler-sfc 包体积过大,且当前只需要使用其 parse 函数,因此创建一个简单的 parse 函数 +// 该函数省略了报错,仅返回解析结果 +export function parseSfc(source: string) { + const cache = sfcCache.get(source); + if (cache) { + return cache; + } + const descriptor: Pick = { + template: null, + script: null, + scriptSetup: null, + styles: [], + customBlocks: [], + }; + + const ast = parse(source, { + parseMode: 'sfc', + prefixIdentifiers: true, + }); + + ast.children.forEach((node) => { + if (node.type !== NodeTypes.ELEMENT) { + return; + } + if (node.tag !== 'template' && isEmpty(node) && !hasSrc(node)) { + return; + } + switch (node.tag) { + case 'template': + descriptor.template = createBlock(node, source) as SFCTemplateBlock; + break; + case 'script': { + const scriptBlock = createBlock(node, source) as SFCScriptBlock; + if (scriptBlock.attrs.setup) { + descriptor.scriptSetup = scriptBlock; + } else { + descriptor.script = scriptBlock; + } + break; + } + case 'style': { + const styleBlock = createBlock(node, source) as SFCStyleBlock; + descriptor.styles.push(styleBlock); + break; + } + default: + descriptor.customBlocks.push(createBlock(node, source)); + break; + } + }); + sfcCache.set(source, descriptor); + return descriptor; +} diff --git a/packages/docs/plugins/markdown/highlight.ts b/packages/docs/plugins/markdown/highlight.ts index 010ed5950a734f6027265a6708909fecfedba211..cd2624bbbd5a222f4f6a286f5588d4c3f75786f8 100644 --- a/packages/docs/plugins/markdown/highlight.ts +++ b/packages/docs/plugins/markdown/highlight.ts @@ -1,7 +1,8 @@ -// 优化后代码结构示例 import { escapeHtml } from 'markdown-it/lib/common/utils.mjs'; import { createHighlighterCore, createJavaScriptRegexEngine } from 'shiki'; import { generateCode } from '../../helper/utils'; +// 由于 @vue/compiler-sfc 包体积过大,因此使用简化的 parseSfc 函数 +import { parseSfc } from '../../helper/vue-utils'; const baseConfig = { themes: [import('@shikijs/themes/light-plus'), import('@shikijs/themes/dark-plus')], @@ -12,7 +13,7 @@ const baseConfig = { * @returns 高亮函数 */ export const createHighlighter = async () => { - const [mainHighlighter, vueTemplateHighlighter, parse] = await Promise.all([ + const [mainHighlighter, vueTemplateHighlighter] = await Promise.all([ createHighlighterCore({ ...baseConfig, langs: [ @@ -44,7 +45,6 @@ export const createHighlighter = async () => { import('@shikijs/langs/css'), ], }), - import('@vue/compiler-sfc').then((r) => r.parse), ]); const stripPreCodeReg = /([\s\S]*?)<\/code><\/pre>/; @@ -86,7 +86,7 @@ export const createHighlighter = async () => { } if (lang === 'vue') { - const { descriptor } = parse(code); + const descriptor = parseSfc(code); // vue的template模块需要单独处理,因此分块高亮 const blocks = [descriptor.script, ...descriptor.styles, descriptor.scriptSetup, ...descriptor.customBlocks, descriptor.template] .filter(Boolean) diff --git a/packages/docs/src/App.vue b/packages/docs/src/App.vue index 38ea127aedeb5740d9fb723a7e8348ad37df1eff..35beb917be3d2a4d9a5f55b655ff7c097ab21c5a 100644 --- a/packages/docs/src/App.vue +++ b/packages/docs/src/App.vue @@ -1,6 +1,7 @@