139 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			139 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
| import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
 | |
| 
 | |
| import crypto from 'node:crypto';
 | |
| import { readdirSync } from 'node:fs';
 | |
| import { join } from 'node:path';
 | |
| 
 | |
| export const rawPathRegexp =
 | |
|   // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
 | |
|   /^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
 | |
| 
 | |
| function rawPathToToken(rawPath: string) {
 | |
|   const [
 | |
|     filepath = '',
 | |
|     extension = '',
 | |
|     region = '',
 | |
|     lines = '',
 | |
|     lang = '',
 | |
|     rawTitle = '',
 | |
|   ] = (rawPathRegexp.exec(rawPath) || []).slice(1);
 | |
| 
 | |
|   const title = rawTitle || filepath.split('/').pop() || '';
 | |
| 
 | |
|   return { extension, filepath, lang, lines, region, title };
 | |
| }
 | |
| 
 | |
| export const demoPreviewPlugin = (md: MarkdownRenderer) => {
 | |
|   md.core.ruler.after('inline', 'demo-preview', (state) => {
 | |
|     const insertComponentImport = (importString: string) => {
 | |
|       const index = state.tokens.findIndex(
 | |
|         (i) => i.type === 'html_block' && i.content.match(/<script setup>/g),
 | |
|       );
 | |
|       if (index === -1) {
 | |
|         const importComponent = new state.Token('html_block', '', 0);
 | |
|         importComponent.content = `<script setup>\n${importString}\n</script>\n`;
 | |
|         state.tokens.splice(0, 0, importComponent);
 | |
|       } else {
 | |
|         if (state.tokens[index]) {
 | |
|           const content = state.tokens[index].content;
 | |
|           state.tokens[index].content = content.replace(
 | |
|             '</script>',
 | |
|             `${importString}\n</script>`,
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|     };
 | |
|     // Define the regular expression to match the desired pattern
 | |
|     const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g;
 | |
|     // Iterate through the Markdown content and replace the pattern
 | |
|     state.src = state.src.replaceAll(regex, (_match, dir) => {
 | |
|       const componentDir = join(process.cwd(), 'src', dir).replaceAll(
 | |
|         '\\',
 | |
|         '/',
 | |
|       );
 | |
| 
 | |
|       let childFiles: string[] = [];
 | |
|       let dirExists = true;
 | |
| 
 | |
|       try {
 | |
|         childFiles =
 | |
|           readdirSync(componentDir, {
 | |
|             encoding: 'utf8',
 | |
|             recursive: false,
 | |
|             withFileTypes: false,
 | |
|           }) || [];
 | |
|       } catch {
 | |
|         dirExists = false;
 | |
|       }
 | |
| 
 | |
|       if (!dirExists) {
 | |
|         return '';
 | |
|       }
 | |
| 
 | |
|       const uniqueWord = generateContentHash(componentDir);
 | |
| 
 | |
|       const ComponentName = `DemoComponent_${uniqueWord}`;
 | |
|       insertComponentImport(
 | |
|         `import ${ComponentName} from '${componentDir}/index.vue'`,
 | |
|       );
 | |
|       const { path: _path } = state.env as MarkdownEnv;
 | |
| 
 | |
|       const index = state.tokens.findIndex((i) => i.content.match(regex));
 | |
| 
 | |
|       if (!state.tokens[index]) {
 | |
|         return '';
 | |
|       }
 | |
| 
 | |
|       state.tokens[index].content =
 | |
|         `<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
 | |
|         `;
 | |
| 
 | |
|       const _dummyToken = new state.Token('', '', 0);
 | |
|       const tokenArray: Array<typeof _dummyToken> = [];
 | |
|       childFiles.forEach((filename) => {
 | |
|         // const slotName = filename.replace(extname(filename), '');
 | |
| 
 | |
|         const templateStart = new state.Token('html_inline', '', 0);
 | |
|         templateStart.content = `<template #${filename}>`;
 | |
|         tokenArray.push(templateStart);
 | |
| 
 | |
|         const resolvedPath = join(componentDir, filename);
 | |
| 
 | |
|         const { extension, filepath, lang, lines, title } =
 | |
|           rawPathToToken(resolvedPath);
 | |
|         // Add code tokens for each line
 | |
|         const token = new state.Token('fence', 'code', 0);
 | |
|         token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
 | |
|           title ? `[${title}]` : ''
 | |
|         }`;
 | |
| 
 | |
|         token.content = `<<< ${filepath}`;
 | |
|         (token as any).src = [resolvedPath];
 | |
|         tokenArray.push(token);
 | |
| 
 | |
|         const templateEnd = new state.Token('html_inline', '', 0);
 | |
|         templateEnd.content = '</template>';
 | |
|         tokenArray.push(templateEnd);
 | |
|       });
 | |
|       const endTag = new state.Token('html_inline', '', 0);
 | |
|       endTag.content = '</DemoPreview>';
 | |
|       tokenArray.push(endTag);
 | |
| 
 | |
|       state.tokens.splice(index + 1, 0, ...tokenArray);
 | |
| 
 | |
|       // console.log(
 | |
|       //   state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
 | |
|       // );
 | |
|       return '';
 | |
|     });
 | |
|   });
 | |
| };
 | |
| 
 | |
| function generateContentHash(input: string, length: number = 10): string {
 | |
|   // 使用 SHA-256 生成哈希值
 | |
|   const hash = crypto.createHash('sha256').update(input).digest('hex');
 | |
| 
 | |
|   // 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
 | |
|   return Number.parseInt(hash, 16).toString(36).slice(0, length);
 | |
| }
 |