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: Support code group in Markdown #1242

Closed
wants to merge 13 commits into from
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ function sidebarGuide() {
collapsible: true,
items: [
{ text: 'Markdown', link: '/guide/markdown' },
{ text: 'Code Group', link: '/guide/test' },
{ text: 'Asset Handling', link: '/guide/asset-handling' },
{ text: 'Frontmatter', link: '/guide/frontmatter' },
{ text: 'Using Vue in Markdown', link: '/guide/using-vue' },
Expand Down
51 changes: 51 additions & 0 deletions docs/guide/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup>
import { VPCodeGroup } from "vitepress/theme";
</script>

# Code Groups

test

:::code-group

```js [app.vue]
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```

```js{3,4} [layouts/custom.vue]
<template>
<div>
Some *custom* layout
<slot />
</div>
</template>
```

```js{1-3,5} [layouts/default.vue]
export default {
name: 'MyComponent'
// ...
}
<template>
<div>
Some *custom* layout
<slot />
</div>
</template>
```

```js
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```

:::


89 changes: 89 additions & 0 deletions src/client/theme-default/components/VPCodeGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';

interface Tab {
label: string;
VoVAllen marked this conversation as resolved.
Show resolved Hide resolved
elm: Element;
}

const codeblocks = ref<HTMLElement | null>(null)
let tabs = ref<Array<Tab>>([])
const activeTabIndex = ref(0)

onMounted(() => {
if (codeblocks !== undefined) {
codeblocks.value?.querySelectorAll(".code-block").forEach((element, i) => {
const title = element.querySelector("span.code-title")?.textContent!
tabs.value.push({
label: title,
elm: element
})
})
switchTab(0)
}
})

function switchTab(i: number) {
tabs.value.forEach((tab) => {
tab["elm"].classList.remove('active')
})
tabs.value[i]["elm"].classList.add('active')
}

watch(activeTabIndex, (newValue, oldValue) => {
switchTab(newValue)
})

</script>

<template>
<div class="code-group">
<div class="tabs-header">
<button v-for="({ label }, i) in tabs" :key="label" :class="[activeTabIndex === i && 'active']"
@click="activeTabIndex = i">{{ label }}</button>
</div>
<div ref="codeblocks">
<slot />
</div>
</div>
</template>

<style scoped>
.tabs-header {
border-radius: 8px 8px 0 0;
padding: 0 12px 0 12px;
background-color: var(--vp-code-block-tab-header-bg);
transition: background-color 0.5s;
}

.tabs-header button {
padding: 6px 8px 6px 8px;
margin: 8px 0 8px 0;
border-radius: 8px;
color: white;
outline: none;
}

.tabs-header button:hover {
color: rgba(255, 255, 255, 0.82);
}


.tabs-header button.active {
background-color: rgb(63 63 70);
VoVAllen marked this conversation as resolved.
Show resolved Hide resolved
}


:slotted(.code-block) {
display: none;
}

:slotted(.code-block.active) {
display: block;
}

:slotted(div[class*='language-']) {
border-radius: 0 0 8px 8px !important;
margin-top: 0;
}
</style>
1 change: 1 addition & 0 deletions src/client/theme-default/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { default as VPTeamPage } from './components/VPTeamPage.vue'
export { default as VPTeamPageTitle } from './components/VPTeamPageTitle.vue'
export { default as VPTeamPageSection } from './components/VPTeamPageSection.vue'
export { default as VPTeamMembers } from './components/VPTeamMembers.vue'
export { default as VPCodeGroup } from './components/VPCodeGroup.vue'

const theme: Theme = {
Layout,
Expand Down
6 changes: 3 additions & 3 deletions src/client/theme-default/styles/components/vp-doc.css
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@
content: 'Copied';
}

.vp-doc [class*='language-'] > span.lang {
.vp-doc [class*='language-'] > span.code-title {
position: absolute;
top: 6px;
right: 12px;
Expand All @@ -416,8 +416,8 @@
transition: color 0.4s, opacity 0.4s;
}

.vp-doc [class*='language-']:hover > button.copy + span.lang,
.vp-doc [class*='language-'] > button.copy:focus + span.lang {
.vp-doc [class*='language-']:hover > button.copy + span.code-title,
.vp-doc [class*='language-'] > button.copy:focus + span.code-title {
opacity: 0;
}

Expand Down
1 change: 1 addition & 0 deletions src/client/theme-default/styles/vars.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@

--vp-code-block-color: var(--vp-c-text-dark-1);
--vp-code-block-bg: #292d3e;
--vp-code-block-tab-header-bg: #363b51;

--vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
--vp-code-line-number-color: var(--vp-c-text-dark-3);
Expand Down
7 changes: 7 additions & 0 deletions src/node/markdown/plugins/containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export const containerPlugin = (md: MarkdownIt) => {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`
})
.use(container, 'code-group', {
render: (tokens: Token[], idx: number) => {
return tokens[idx].nesting === 1
? `<VPCodeGroup>\n`
: `</VPCodeGroup>\n`
}
})
}

type ContainerArgs = [typeof container, string, { render: RenderRule }]
Expand Down
28 changes: 24 additions & 4 deletions src/node/markdown/plugins/preWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,32 @@ import MarkdownIt from 'markdown-it'

export const preWrapperPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
const RE = /(\w*)(?:{[\d,-]+})?\s*\[(.+)\]/
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
const lang = tokens[idx].info.trim().replace(/-vue$/, '')
const token = tokens[idx]
const hint = token.info.trim().replace(/-vue$/, '')
let codeTitle = ''
let lang = hint
if (RE.test(hint)) {
const matchGroup = RE.exec(hint)
if (matchGroup && matchGroup.length == 3) {
lang = matchGroup[1].trim()
codeTitle = matchGroup[2]
}
} else {
// Use language name as code title if not specified
codeTitle = lang === 'vue-html' ? 'template' : lang
}
token.info = tokens[idx].info.replace(/\[(.+)\]/, '')
const rawCode = fence(...args)
return `<div class="language-${lang}"><button class="copy"></button><span class="lang">${
lang === 'vue-html' ? 'template' : lang
}</span>${rawCode}</div>`

return `<div class="code-block">
<div class="language-${lang}">
<button class="copy"></button>
<span class="code-title">${codeTitle}</span>
${rawCode}
</div>
</div>`
}
}