Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(@vben/docs): preview components are supported within documents #4250

Merged
merged 2 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/.vitepress/components/demo-preview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue';

import PreviewGroup from './preview-group.vue';

interface Props {
files?: string;
}

const props = withDefaults(defineProps<Props>(), { files: '() => []' });

const parsedFiles = computed(() => {
try {
return JSON.parse(decodeURIComponent(props.files ?? ''));
} catch {
return [];
}
});
</script>

<template>
<div class="border-border shadow-float relative rounded-xl border">
<div
class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6"
>
<div class="flex w-full max-w-[700px] px-2">
<slot v-if="parsedFiles.length > 0"></slot>
<div v-else class="text-destructive text-sm">
<span class="bg-destructive text-foreground rounded-sm px-1 py-1">
ERROR:
</span>
The preview directory does not exist. Please check the 'dir'
parameter.
</div>
</div>
</div>
<PreviewGroup v-if="parsedFiles.length > 0" :files="parsedFiles">
<template v-for="file in parsedFiles" #[file]>
<slot :name="file"></slot>
</template>
</PreviewGroup>
</div>
</template>
1 change: 1 addition & 0 deletions docs/.vitepress/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as DemoPreview } from './demo-preview.vue';
108 changes: 108 additions & 0 deletions docs/.vitepress/components/preview-group.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { computed, ref, useSlots } from 'vue';

import { VbenTooltip } from '@vben-core/shadcn-ui';

import { Code } from 'lucide-vue-next';
import {
TabsContent,
TabsIndicator,
TabsList,
TabsRoot,
TabsTrigger,
} from 'radix-vue';

defineOptions({
inheritAttrs: false,
});

const props = withDefaults(
defineProps<{
files?: string[];
}>(),
{ files: () => [] },
);

const open = ref(false);

const slots = useSlots();

const tabs = computed(() => {
return props.files.map((file) => {
return {
component: slots[file],
label: file,
};
});
});

const currentTab = ref('index.vue');

const toggleOpen = () => {
open.value = !open.value;
};
</script>

<template>
<TabsRoot
v-model="currentTab"
class="bg-background-deep border-border overflow-hidden rounded-b-xl border-t"
@update:model-value="open = true"
>
<div class="border-border bg-background flex border-b-2 pr-2">
<div class="flex w-full items-center justify-between text-[13px]">
<TabsList class="relative flex">
<template v-if="open">
<TabsIndicator
class="absolute bottom-0 left-0 h-[2px] w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] rounded-full transition-[width,transform] duration-300"
>
<div class="size-full bg-[var(--vp-c-indigo-1)]"></div>
</TabsIndicator>
<TabsTrigger
v-for="(tab, index) in tabs"
:key="index"
:value="tab.label"
class="border-box text-foreground px-4 py-3 data-[state=active]:text-[var(--vp-c-indigo-1)]"
tabindex="-1"
>
{{ tab.label }}
</TabsTrigger>
</template>
</TabsList>

<div
:class="{
'py-2': !open,
}"
class="flex items-center"
>
<VbenTooltip side="top">
<template #trigger>
<Code
class="hover:bg-accent size-6.5 cursor-pointer rounded-full p-1.5"
@click="toggleOpen"
/>
</template>
{{ open ? 'Collapse code' : 'Expand code' }}
</VbenTooltip>
</div>
</div>
</div>
<div
:class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`"
class="block overflow-y-scroll bg-[var(--vp-code-block-bg)] transition-all duration-300"
>
<TabsContent
v-for="tab in tabs"
:key="tab.label"
:value="tab.label"
as-child
class="rounded-xl"
>
<div class="text-foreground relative rounded-xl">
<component :is="tab.component" class="border-0" />
</div>
</TabsContent>
</div>
</TabsRoot>
</template>
2 changes: 1 addition & 1 deletion docs/.vitepress/config/en.mts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ function nav(): DefaultTheme.NavItem[] {
},
{
link: '/commercial/technical-support',
text: '🦄 Technical Support',
text: '🦄 Tech Support',
},
{
link: '/sponsor/personal',
Expand Down
4 changes: 2 additions & 2 deletions docs/.vitepress/config/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { withPwa } from '@vite-pwa/vitepress';
import { defineConfigWithTheme } from 'vitepress';

import { en } from './en.mts';
import { shard } from './shard.mts';
import { shared } from './shared.mts';
import { zh } from './zh.mts';

export default withPwa(
defineConfigWithTheme({
...shard,
...shared,
locales: {
en: {
label: 'English',
Expand Down
135 changes: 135 additions & 0 deletions docs/.vitepress/config/plugins/demo-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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);

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);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import type { PwaOptions } from '@vite-pwa/vitepress';
import type { HeadConfig } from 'vitepress';

import { resolve } from 'node:path';

import {
GitChangelog,
GitChangelogMarkdownSection,
} from '@nolebase/vitepress-plugin-git-changelog/vite';
import { defineConfig, type HeadConfig } from 'vitepress';
import tailwind from 'tailwindcss';
import { defineConfig, postcssIsolateStyles } from 'vitepress';

import { demoPreviewPlugin } from './plugins/demo-preview';
import { search as zhSearch } from './zh.mts';

export const shard = defineConfig({
export const shared = defineConfig({
appearance: 'dark',
head: head(),
markdown: {
preConfig(md) {
md.use(demoPreviewPlugin);
},
},
pwa: pwa(),
srcDir: 'src',
themeConfig: {
Expand All @@ -36,11 +45,34 @@ export const shard = defineConfig({
chunkSizeWarningLimit: Infinity,
minify: 'terser',
},
css: {
postcss: {
plugins: [
tailwind(),
postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
],
},
},
json: {
stringify: true,
},
plugins: [
GitChangelog({
mapAuthors: [
{
mapByNameAliases: ['Vben'],
name: 'vben',
username: 'anncwb',
},
{
name: 'vince',
username: 'vince292007',
},
{
name: 'Li Kui',
username: 'likui628',
},
],
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
}),
GitChangelogMarkdownSection(),
Expand Down
Loading
Loading