demo-preview.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
  2. import crypto from 'node:crypto';
  3. import { readdirSync } from 'node:fs';
  4. import { join } from 'node:path';
  5. export const rawPathRegexp =
  6. // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
  7. /^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
  8. function rawPathToToken(rawPath: string) {
  9. const [
  10. filepath = '',
  11. extension = '',
  12. region = '',
  13. lines = '',
  14. lang = '',
  15. rawTitle = '',
  16. ] = (rawPathRegexp.exec(rawPath) || []).slice(1);
  17. const title = rawTitle || filepath.split('/').pop() || '';
  18. return { extension, filepath, lang, lines, region, title };
  19. }
  20. export const demoPreviewPlugin = (md: MarkdownRenderer) => {
  21. md.core.ruler.after('inline', 'demo-preview', (state) => {
  22. const insertComponentImport = (importString: string) => {
  23. const index = state.tokens.findIndex(
  24. (i) => i.type === 'html_block' && i.content.match(/<script setup>/g),
  25. );
  26. if (index === -1) {
  27. const importComponent = new state.Token('html_block', '', 0);
  28. importComponent.content = `<script setup>\n${importString}\n</script>\n`;
  29. state.tokens.splice(0, 0, importComponent);
  30. } else {
  31. if (state.tokens[index]) {
  32. const content = state.tokens[index].content;
  33. state.tokens[index].content = content.replace(
  34. '</script>',
  35. `${importString}\n</script>`,
  36. );
  37. }
  38. }
  39. };
  40. // Define the regular expression to match the desired pattern
  41. const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g;
  42. // Iterate through the Markdown content and replace the pattern
  43. state.src = state.src.replaceAll(regex, (_match, dir) => {
  44. const componentDir = join(process.cwd(), 'src', dir).replaceAll(
  45. '\\',
  46. '/',
  47. );
  48. let childFiles: string[] = [];
  49. let dirExists = true;
  50. try {
  51. childFiles =
  52. readdirSync(componentDir, {
  53. encoding: 'utf8',
  54. recursive: false,
  55. withFileTypes: false,
  56. }) || [];
  57. } catch {
  58. dirExists = false;
  59. }
  60. if (!dirExists) {
  61. return '';
  62. }
  63. const uniqueWord = generateContentHash(componentDir);
  64. const ComponentName = `DemoComponent_${uniqueWord}`;
  65. insertComponentImport(
  66. `import ${ComponentName} from '${componentDir}/index.vue'`,
  67. );
  68. const { path: _path } = state.env as MarkdownEnv;
  69. const index = state.tokens.findIndex((i) => i.content.match(regex));
  70. if (!state.tokens[index]) {
  71. return '';
  72. }
  73. const firstString = 'index.vue';
  74. childFiles = childFiles.sort((a, b) => {
  75. if (a === firstString) return -1;
  76. if (b === firstString) return 1;
  77. return a.localeCompare(b, 'en', { sensitivity: 'base' });
  78. });
  79. state.tokens[index].content =
  80. `<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
  81. `;
  82. const _dummyToken = new state.Token('', '', 0);
  83. const tokenArray: Array<typeof _dummyToken> = [];
  84. childFiles.forEach((filename) => {
  85. // const slotName = filename.replace(extname(filename), '');
  86. const templateStart = new state.Token('html_inline', '', 0);
  87. templateStart.content = `<template #${filename}>`;
  88. tokenArray.push(templateStart);
  89. const resolvedPath = join(componentDir, filename);
  90. const { extension, filepath, lang, lines, title } =
  91. rawPathToToken(resolvedPath);
  92. // Add code tokens for each line
  93. const token = new state.Token('fence', 'code', 0);
  94. token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
  95. title ? `[${title}]` : ''
  96. }`;
  97. token.content = `<<< ${filepath}`;
  98. (token as any).src = [resolvedPath];
  99. tokenArray.push(token);
  100. const templateEnd = new state.Token('html_inline', '', 0);
  101. templateEnd.content = '</template>';
  102. tokenArray.push(templateEnd);
  103. });
  104. const endTag = new state.Token('html_inline', '', 0);
  105. endTag.content = '</DemoPreview>';
  106. tokenArray.push(endTag);
  107. state.tokens.splice(index + 1, 0, ...tokenArray);
  108. // console.log(
  109. // state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
  110. // );
  111. return '';
  112. });
  113. });
  114. };
  115. function generateContentHash(input: string, length: number = 10): string {
  116. // 使用 SHA-256 生成哈希值
  117. const hash = crypto.createHash('sha256').update(input).digest('hex');
  118. // 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
  119. return Number.parseInt(hash, 16).toString(36).slice(0, length);
  120. }