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 @@ -87,6 +87,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
81 changes: 81 additions & 0 deletions docs/guide/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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>
```

```python
import torch as th
print("Hello world")
```

:::


:::code-group

```js
printf("111")
```


```python
import torch as th
print("Hello world")
```

:::


```
.
├─ index.md
├─ foo
│ ├─ index.md
│ ├─ one.md
│ └─ two.md
└─ bar
├─ index.md
├─ three.md
└─ four.md
```


```md
[Home](/) <!-- sends the user to the root index.md -->
[foo](/foo/) <!-- sends the user to index.html of directory foo -->
[foo heading](./#heading) <!-- anchors user to a heading in the foo index file -->
[bar - three](../bar/three) <!-- you can omit extention -->
[bar - three](../bar/three.md) <!-- you can append .md -->
[bar - four](../bar/four.html) <!-- or you can append .html -->
```
20 changes: 20 additions & 0 deletions src/client/app/composables/codeGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { inBrowser } from '../utils.js'

export function useCodeGroup() {
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target as HTMLElement
if (el.matches('.code-group>.tabs-header>button')) {
const codeblocks =
el.parentElement!.parentElement!.querySelectorAll('.code-block')
const buttons =
el.parentElement!.parentElement!.querySelectorAll('button')
const index = parseInt(el.getAttribute('tab-index')!)
codeblocks.forEach((ele) => ele.classList.remove('active'))
buttons.forEach((ele) => ele.classList.remove('active'))
el.classList.add('active')
codeblocks[index].classList.add('active')
}
})
}
}
4 changes: 4 additions & 0 deletions src/client/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { dataSymbol, initData } from './data.js'
import { Content } from './components/Content.js'
import { ClientOnly } from './components/ClientOnly.js'
import { useCopyCode } from './composables/copyCode.js'
import { useCodeGroup } from './composables/codeGroup.js'

const NotFound = Theme.NotFound || (() => '404 Not Found')

Expand Down Expand Up @@ -44,6 +45,9 @@ const VitePressApp = defineComponent({
// setup global copy code handler
useCopyCode()

// setup code group button handler
useCodeGroup()

if (Theme.setup) Theme.setup()
return () => h(Theme.Layout)
}
Expand Down
36 changes: 36 additions & 0 deletions src/client/theme-default/styles/components/vp-code.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,39 @@
html:not(.dark) .vp-code-dark {
display: none;
}

.code-group .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;
}

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

.code-group .tabs-header button:hover {
color: var(--vp-c-gray-light-4);
}

.code-group .tabs-header button.active {
background-color: var(--vp-c-gray-dark-3);
}

.code-group .code-block {
display: none;
}

.code-group .code-block.active {
display: block;
}

.vp-doc .code-group div[class*='language-'] {
border-radius: 0 0 8px 8px;
margin-top: 0;
}
21 changes: 6 additions & 15 deletions src/client/theme-default/styles/components/vp-doc.css
Original file line number Diff line number Diff line change
Expand Up @@ -259,23 +259,14 @@

.vp-doc div[class*='language-'] {
position: relative;
margin: 16px -24px;
background-color: var(--vp-code-block-bg);
overflow-x: auto;
transition: background-color 0.5s;
}

@media (min-width: 640px) {
.vp-doc div[class*='language-'] {
border-radius: 8px;
margin: 16px 0;
}
}

@media (max-width: 639px) {
.vp-doc li div[class*='language-'] {
border-radius: 8px 0 0 8px;
}
.vp-doc div[class*='language-'] {
border-radius: 8px;
margin: 16px 0;
}

.vp-doc div[class*='language-'] + div[class*='language-'],
Expand Down Expand Up @@ -405,7 +396,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 +407,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 @@ -204,6 +204,7 @@

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

--vp-code-line-highlight-color: rgba(0, 0, 0, 0.5);
--vp-code-line-number-color: var(--vp-c-text-dark-3);
Expand Down
24 changes: 24 additions & 0 deletions src/node/markdown/plugins/codeGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Token from 'markdown-it/lib/token'

export const extractCodeTitleAndLang = (token: Token): [string, string] => {
const RE = /(\w*)(?:{[\d,-]+})?\s*\[(.+)\]/
const hint = token.info
.trim()
.replace(codeGroupInternalActiveMark, '')
.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
}
return [codeTitle, lang]
}

export const codeGroupInternalActiveMark = '#vitepress-internal-active#'
53 changes: 53 additions & 0 deletions src/node/markdown/plugins/containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import MarkdownIt from 'markdown-it'
import { RenderRule } from 'markdown-it/lib/renderer'
import Token from 'markdown-it/lib/token'
import container from 'markdown-it-container'
import {
extractCodeTitleAndLang,
codeGroupInternalActiveMark
} from './codeGroup'

export const containerPlugin = (md: MarkdownIt) => {
md.use(...createContainer('tip', 'TIP', md))
Expand All @@ -14,6 +18,7 @@ export const containerPlugin = (md: MarkdownIt) => {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`
})
.use(...createCodeGroup())
.use(container, 'raw', {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`
Expand Down Expand Up @@ -47,3 +52,51 @@ function createContainer(
}
]
}

function createCodeGroup(): ContainerArgs {
const klass = 'code-group'
return [
container,
klass,
{
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
const startTokenId = idx
const endTokenId =
tokens
.slice(startTokenId)
.findIndex(
(token) =>
token.nesting == -1 &&
token.type === 'container_code-group_close'
) + startTokenId
const codeGroupTokens = tokens.slice(startTokenId + 1, endTokenId)

// Mark first code block as active
const firstCodeBlock = codeGroupTokens.findIndex(
(token) => token.type === 'fence' && token.tag === 'code'
)
tokens[
startTokenId + 1 + firstCodeBlock
].info += ` ${codeGroupInternalActiveMark}`
const codeTitles = codeGroupTokens
.filter((token) => token.type === 'fence' && token.tag === 'code')
.map((token) => extractCodeTitleAndLang(token)[0])
let headerBlock = `<div class="code-group">\n<div class="tabs-header">`
const buttonsBlock = codeTitles
.map((title, idx) => {
return `<button tab-index=${idx} ${
idx === 0 ? "class='active'" : ''
}>${title}</button>`
})
.join('\n')
headerBlock += buttonsBlock
headerBlock += `</div>\n`
return headerBlock
} else {
return `</div>\n`
}
}
}
]
}
20 changes: 16 additions & 4 deletions src/node/markdown/plugins/preWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,27 @@
// 4. <!--afterend-->

import MarkdownIt from 'markdown-it'
import {
codeGroupInternalActiveMark,
extractCodeTitleAndLang
} from './codeGroup'

export const preWrapperPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
const lang = tokens[idx].info.trim().replace(/-vue$/, '')
const token = tokens[idx]
const [codeTitle, lang] = extractCodeTitleAndLang(token)
const isActive = token.info.includes(codeGroupInternalActiveMark)
token.info = tokens[idx].info.replace(/\[(.+)\]/, '')
const rawCode = fence(...args)
return `<div class="language-${lang}"><button title="Copy Code" class="copy"></button><span class="lang">${
lang === 'vue-html' ? 'template' : lang
}</span>${rawCode}</div>`

return `<div class="code-block ${isActive ? 'active' : ''}">
<div class="language-${lang}">
<button title="Copy Code" class="copy"></button>
<span class="code-title">${codeTitle}</span>
${rawCode}
</div>
</div>`
}
}