docs: rendering example demos on the server side (#17472)

* docs: rendering example demos on the server side

* fix: render error

* chore: update dependencies

* chore: return all teleports
This commit is contained in:
qiang 2024-08-22 14:33:13 +08:00 committed by GitHub
parent 9234661993
commit 7b6e2c2600
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 256 additions and 270 deletions

View File

@ -85,5 +85,24 @@ const config: UserConfig = {
},
},
},
postRender(context) {
// Inject the teleport markup
if (context.teleports) {
const body = Object.entries(context.teleports).reduce(
(all, [key, value]) => {
if (key.startsWith('#el-popper-container-')) {
return `${all}<div id="${key.slice(1)}">${value}</div>`
}
return all
},
context.teleports.body || ''
)
context.teleports = { ...context.teleports, body }
}
return context
},
}
export default config

View File

@ -1,66 +1,19 @@
import path from 'path'
import fs from 'fs'
import mdContainer from 'markdown-it-container'
import { docRoot } from '@element-plus/build-utils'
import externalLinkIcon from '../plugins/external-link-icon'
import tableWrapper from '../plugins/table-wrapper'
import tooltip from '../plugins/tooltip'
import tag from '../plugins/tag'
import headers from '../plugins/headers'
import createDemoContainer from '../plugins/demo'
import { ApiTableContainer } from '../plugins/api-table'
import type Token from 'markdown-it/lib/token'
import type Renderer from 'markdown-it/lib/renderer'
import type MarkdownIt from 'markdown-it'
interface ContainerOpts {
marker?: string | undefined
validate?(params: string): boolean
render?(
tokens: Token[],
index: number,
options: any,
env: any,
self: Renderer
): string
}
export const mdPlugin = (md: MarkdownIt) => {
md.use(headers)
md.use(externalLinkIcon)
md.use(tableWrapper)
md.use(tooltip)
md.use(tag)
md.use(mdContainer, 'demo', {
validate(params) {
return !!params.trim().match(/^demo\s*(.*)$/)
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
if (tokens[idx].nesting === 1 /* means the tag is opening */) {
const description = m && m.length > 1 ? m[1] : ''
const sourceFileToken = tokens[idx + 2]
let source = ''
const sourceFile = sourceFileToken.children?.[0].content ?? ''
if (sourceFileToken.type === 'inline') {
source = fs.readFileSync(
path.resolve(docRoot, 'examples', `${sourceFile}.vue`),
'utf-8'
)
}
if (!source) throw new Error(`Incorrect source file: ${sourceFile}`)
return `<Demo :demos="demos" source="${encodeURIComponent(
md.render(`\`\`\` vue\n${source}\`\`\``)
)}" path="${sourceFile}" raw-source="${encodeURIComponent(
source
)}" description="${encodeURIComponent(md.render(description))}">`
} else {
return '</Demo>'
}
},
} as ContainerOpts)
md.use(mdContainer, 'demo', createDemoContainer(md))
md.use(ApiTableContainer)
}

View File

@ -0,0 +1,55 @@
import path from 'path'
import fs from 'fs'
import { docRoot } from '@element-plus/build-utils'
import type MarkdownIt from 'markdown-it'
import type Token from 'markdown-it/lib/token'
import type Renderer from 'markdown-it/lib/renderer'
interface ContainerOpts {
marker?: string | undefined
validate?(params: string): boolean
render?(
tokens: Token[],
index: number,
options: any,
env: any,
self: Renderer
): string
}
function createDemoContainer(md: MarkdownIt): ContainerOpts {
return {
validate(params) {
return !!params.trim().match(/^demo\s*(.*)$/)
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
if (tokens[idx].nesting === 1 /* means the tag is opening */) {
const description = m && m.length > 1 ? m[1] : ''
const sourceFileToken = tokens[idx + 2]
let source = ''
const sourceFile = sourceFileToken.children?.[0].content ?? ''
if (sourceFileToken.type === 'inline') {
source = fs.readFileSync(
path.resolve(docRoot, 'examples', `${sourceFile}.vue`),
'utf-8'
)
}
if (!source) throw new Error(`Incorrect source file: ${sourceFile}`)
return `<Demo source="${encodeURIComponent(
md.render(`\`\`\` vue\n${source}\`\`\``)
)}" path="${sourceFile}" raw-source="${encodeURIComponent(
source
)}" description="${encodeURIComponent(md.render(description))}">
<template #source><ep-${sourceFile.replaceAll('/', '-')}/></template>`
} else {
return '</Demo>\n'
}
},
}
}
export default createDemoContainer

View File

@ -1,5 +1,6 @@
import fs from 'fs'
import path from 'path'
import { camelize } from '@vue/shared'
import glob from 'fast-glob'
import { docRoot, docsDirName, projRoot } from '@element-plus/build-utils'
import { REPO_BRANCH, REPO_PATH } from '@element-plus/build-constants'
@ -35,9 +36,7 @@ export function MarkdownTransform(): Plugin {
const append: Append = {
headers: [],
footers: [],
scriptSetups: [
`const demos = import.meta.glob('../../examples/${componentId}/*.vue', { eager: true })`,
],
scriptSetups: getExampleImports(componentId),
}
code = transformVpScriptSetup(code, append)
@ -144,3 +143,22 @@ ${linksText}`
return code
}
const getExampleImports = (componentId: string) => {
const examplePath = path.resolve(docRoot, 'examples', componentId)
if (!fs.existsSync(examplePath)) return []
const files = fs.readdirSync(examplePath)
const imports: string[] = []
for (const item of files) {
if (!/\.vue$/.test(item)) continue
const file = item.replace(/\.vue$/, '')
const name = camelize(`Ep-${componentId}-${file}`)
imports.push(
`import ${name} from '../../examples/${componentId}/${file}.vue'`
)
}
return imports
}

View File

@ -1,4 +1,7 @@
import ElementPlus from 'element-plus'
import ElementPlus, {
ID_INJECTION_KEY,
ZINDEX_INJECTION_KEY,
} from 'element-plus'
import VPApp, { NotFound, globals } from '../vitepress'
import { define } from '../utils/types'
@ -11,6 +14,8 @@ export default define<Theme>({
Layout: VPApp,
enhanceApp: ({ app }) => {
app.use(ElementPlus)
app.provide(ID_INJECTION_KEY, { prefix: 1024, current: 0 })
app.provide(ZINDEX_INJECTION_KEY, { current: 0 })
globals.forEach(([name, Comp]) => {
app.component(name, Comp)

View File

@ -5,6 +5,8 @@ import DarkIcon from '../icons/dark.vue'
import LightIcon from '../icons/light.vue'
import type { SwitchInstance } from 'element-plus'
defineOptions({ inheritAttrs: false })
const darkMode = ref(isDark.value)
const switchRef = ref<SwitchInstance>()
@ -67,6 +69,7 @@ const beforeChange = () => {
<el-switch
ref="switchRef"
v-model="darkMode"
v-bind="$attrs"
:before-change="beforeChange"
:active-action-icon="DarkIcon"
:inactive-action-icon="LightIcon"

View File

@ -1,28 +0,0 @@
<script setup lang="ts">
defineProps({
file: {
type: String,
required: true,
},
demo: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="example-showcase">
<ClientOnly>
<component :is="demo" v-if="demo" v-bind="$attrs" />
</ClientOnly>
</div>
</template>
<style lang="scss" scoped>
.example-showcase {
padding: 1.5rem;
margin: 0.5px;
background-color: var(--bg-color);
}
</style>

View File

@ -2,6 +2,10 @@
import { computed } from 'vue'
const props = defineProps({
visible: {
type: Boolean,
required: true,
},
source: {
type: String,
required: true,
@ -14,7 +18,7 @@ const decoded = computed(() => {
</script>
<template>
<div class="example-source-wrapper">
<div v-show="visible" class="example-source-wrapper">
<div class="example-source" v-html="decoded" />
</div>
</template>

View File

@ -19,7 +19,6 @@ const sponsor = computed(() => sponsorLocale[lang.value])
<aside ref="container" class="toc-wrapper">
<nav class="toc-content">
<h3 class="toc-content__heading">Contents</h3>
<ClientOnly>
<el-anchor :offset="70" :bound="120">
<el-anchor-link
v-for="{ link, text, children } in headers"
@ -40,7 +39,6 @@ const sponsor = computed(() => sponsorLocale[lang.value])
</template>
</el-anchor-link>
</el-anchor>
</ClientOnly>
<!-- <SponsorLarge
class="mt-8 toc-ads flex flex-col"
item-style="width: 180px; height: 55px;"

View File

@ -5,18 +5,14 @@ import { CaretTop } from '@element-plus/icons-vue'
import { useLang } from '../composables/lang'
import { useSourceCode } from '../composables/source-code'
import { usePlayground } from '../composables/use-playground'
import demoBlockLocale from '../../i18n/component/demo-block.json'
import Example from './demo/vp-example.vue'
import SourceCode from './demo/vp-source-code.vue'
const props = defineProps<{
demos: object
source: string
path: string
rawSource: string
description?: string
description: string
}>()
const vm = getCurrentInstance()!
@ -31,21 +27,9 @@ const lang = useLang()
const demoSourceUrl = useSourceCode(toRef(props, 'path'))
const sourceCodeRef = ref<HTMLButtonElement>()
const formatPathDemos = computed(() => {
const demos = {}
Object.keys(props.demos).forEach((key) => {
demos[key.replace('../../examples/', '').replace('.vue', '')] =
props.demos[key].default
})
return demos
})
const locale = computed(() => demoBlockLocale[lang.value])
const decodedDescription = computed(() =>
decodeURIComponent(props.description!)
)
const decodedDescription = computed(() => decodeURIComponent(props.description))
const onPlaygroundClick = () => {
const { link } = usePlayground(props.rawSource)
@ -76,12 +60,13 @@ const copyCode = async () => {
</script>
<template>
<ClientOnly>
<!-- danger here DO NOT USE INLINE SCRIPT TAG -->
<p text="sm" v-html="decodedDescription" />
<div text="sm" m="y-4" v-html="decodedDescription" />
<div class="example">
<Example :file="path" :demo="formatPathDemos[path]" />
<div class="example-showcase">
<slot name="source" />
</div>
<ElDivider class="m-0" />
@ -167,7 +152,7 @@ const copyCode = async () => {
</div>
<ElCollapseTransition>
<SourceCode v-show="sourceVisible" :source="source" />
<SourceCode :visible="sourceVisible" :source="source" />
</ElCollapseTransition>
<Transition name="el-fade-in-linear">
@ -186,7 +171,6 @@ const copyCode = async () => {
</div>
</Transition>
</div>
</ClientOnly>
</template>
<style scoped lang="scss">
@ -194,6 +178,12 @@ const copyCode = async () => {
border: 1px solid var(--border-color);
border-radius: var(--el-border-radius-base);
.example-showcase {
padding: 1.5rem;
margin: 0.5px;
background-color: var(--bg-color);
}
.op-btns {
padding: 0.5rem;
display: flex;

View File

@ -155,7 +155,6 @@ declare module 'vue' {
VpDemo: typeof import('./.vitepress/vitepress/components/vp-demo.vue')['default']
VpDocContent: typeof import('./.vitepress/vitepress/components/vp-doc-content.vue')['default']
VpEditLink: typeof import('./.vitepress/vitepress/components/doc-content/vp-edit-link.vue')['default']
VpExample: typeof import('./.vitepress/vitepress/components/demo/vp-example.vue')['default']
VpFooter: typeof import('./.vitepress/vitepress/components/globals/vp-footer.vue')['default']
VpHamburger: typeof import('./.vitepress/vitepress/components/navbar/vp-hamburger.vue')['default']
VpHeroContent: typeof import('./.vitepress/vitepress/components/vp-hero-content.vue')['default']

View File

@ -8,7 +8,14 @@
>
<div class="title">{{ radius.name }}</div>
<div class="value">
<code>border-radius: {{ getValue(radius.type) || '0px' }}</code>
<code>
border-radius:
{{
radius.type
? useCssVar(`--el-border-radius-${radius.type}`)
: '"0px"'
}}
</code>
</div>
<div
class="radius"
@ -24,6 +31,7 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useCssVar } from '@vueuse/core'
const radiusGroup = ref([
{
@ -43,14 +51,6 @@ const radiusGroup = ref([
type: 'round',
},
])
const getValue = (type: string) => {
const getCssVarValue = (prefix, type) =>
getComputedStyle(document.documentElement).getPropertyValue(
`--el-${prefix}-${type}`
)
return getCssVarValue('border-radius', type)
}
</script>
<style scoped>
.demo-radius .title {

View File

@ -9,8 +9,8 @@
format="HH:mm:ss"
:value="value1"
/>
<el-button class="countdown-footer" type="primary" @click="reset"
>Reset
<el-button class="countdown-footer" type="primary" @click="reset">
Reset
</el-button>
</el-col>
<el-col :span="8">

View File

@ -124,7 +124,7 @@ importers:
version: 1.43.1
'@vitejs/plugin-vue':
specifier: ^2.3.3
version: 2.3.3(vue@3.2.37)
version: 2.3.3(vite@2.9.15)(vue@3.2.37)
'@vitejs/plugin-vue-jsx':
specifier: ^1.3.10
version: 1.3.10
@ -256,7 +256,7 @@ importers:
version: 4.0.1
element-plus:
specifier: npm:element-plus@latest
version: 2.7.7(vue@3.2.37)
version: 2.8.0(vue@3.2.37)
normalize.css:
specifier: ^8.0.1
version: 8.0.1
@ -365,7 +365,7 @@ importers:
version: 5.0.5(rollup@2.75.7)
'@vitejs/plugin-vue':
specifier: ^2.3.3
version: 2.3.3(vue@3.2.37)
version: 2.3.3(vite@2.9.15)(vue@3.2.37)
'@vitejs/plugin-vue-jsx':
specifier: ^1.3.10
version: 1.3.10
@ -4455,20 +4455,6 @@ packages:
estree-walker: 2.0.2
picomatch: 2.3.1
/@rollup/pluginutils@5.1.0:
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@rollup/pluginutils@5.1.0(rollup@2.75.7):
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
@ -5240,19 +5226,6 @@ packages:
dependencies:
vite: 2.9.15(sass@1.53.0)
vue: 3.2.37
dev: true
/@vitejs/plugin-vue@2.3.3(vue@3.2.37):
resolution: {integrity: sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==}
engines: {node: '>=12.0.0'}
peerDependencies:
vite: ^2.5.10
vue: ^3.2.25
peerDependenciesMeta:
vite:
optional: true
dependencies:
vue: 3.2.37
/@vitejs/plugin-vue@5.0.5(vite@5.3.3)(vue@3.4.31):
resolution: {integrity: sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==}
@ -5428,7 +5401,7 @@ packages:
optional: true
dependencies:
'@babel/types': 7.24.0
'@rollup/pluginutils': 5.1.0
'@rollup/pluginutils': 5.1.0(rollup@3.29.4)
'@vue/compiler-sfc': 3.4.31
ast-kit: 0.11.3
local-pkg: 0.5.0
@ -8296,8 +8269,8 @@ packages:
resolution: {integrity: sha512-OCcF+LwdgFGcsYPYC5keEEFC2XT0gBhrYbeGzHCx7i9qRFbzO/AqTmc/C/1xNhJj+JA7rzlN7mpBuStshh96Cg==}
dev: true
/element-plus@2.7.7(vue@3.2.37):
resolution: {integrity: sha512-7ucUiDAxevyBE8JbXBTe9ofHhS047VmWMLoksE45zZ08XSnhnyG7WUuk3gmDbAklfVMHedb9sEV3OovPUWt+Sw==}
/element-plus@2.8.0(vue@3.2.37):
resolution: {integrity: sha512-7ngapVlVlQAjocVqD4MUKvKXlBneT9DSDk2mmBOSLRFWNm/HLDT15ozmsvUBfy18sajnyUeSIHTtINE8gfrGMg==}
peerDependencies:
vue: ^3.2.0
dependencies:
@ -10570,7 +10543,6 @@ packages:
/immutable@4.1.0:
resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==}
dev: true
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
@ -14242,7 +14214,6 @@ packages:
chokidar: 3.5.3
immutable: 4.1.0
source-map-js: 1.0.2
dev: true
/sax@1.2.1:
resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==}
@ -14496,7 +14467,6 @@ packages:
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
dev: true
/source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
@ -16331,7 +16301,6 @@ packages:
sass: 1.53.0
optionalDependencies:
fsevents: 2.3.3
dev: true
/vite@5.1.6(@types/node@18.19.25)(sass@1.53.0):
resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==}

View File

@ -90,6 +90,7 @@ declare module 'vue' {
ElSkeleton: typeof import('../packages/element-plus')['ElSkeleton']
ElSkeletonItem: typeof import('../packages/element-plus')['ElSkeletonItem']
ElStatistic: typeof import('../packages/element-plus')['ElStatistic']
ElCountdown: typeof import('../packages/element-plus')['ElCountdown']
ElCheckTag: typeof import('../packages/element-plus')['ElCheckTag']
ElDescriptions: typeof import('../packages/element-plus')['ElDescriptions']
ElDescriptionsItem: typeof import('../packages/element-plus')['ElDescriptionsItem']