feat: add code-group feature (#728) (#1560)

close #728
close #1242

Co-authored-by: "Jinjing.Zhou" <allenzhou@tensorchord.ai>
Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
pull/1700/head
Divyansh Singh 2 years ago committed by GitHub
parent 2343bd17c2
commit a684b67ec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -1,3 +1,4 @@
/__tests__/e2e/.vitepress/cache
/coverage
/src/client/shared.ts
/src/node/shared.ts

@ -413,7 +413,7 @@ export default {
}
```
## Colored diffs in Code Blocks
## Colored Diffs in Code Blocks
Adding the `// [!code --]` or `// [!code ++]` comments on a line will create a diff of that line, while keeping the colors of the codeblock.
@ -447,7 +447,7 @@ export default {
}
```
## Errors and warnings
## Errors and Warnings in Code Blocks
Adding the `// [!code warning]` or `// [!code error]` comments on a line will color it accordingly.
@ -549,11 +549,72 @@ You can also specify the language inside the braces (`{}`) like this:
<<< @/snippets/snippet.cs{c#}
<!-- with line highlighting: -->
<<< @/snippets/snippet.cs{1,2,4-6 c#}
```
This is helpful if source language cannot be inferred from your file extension.
## Code Groups
You can group multiple code blocks like this:
**Input**
````md
::: code-group
```js [config.js]
/**
* @type {import('vitepress').UserConfig}
*/
const config = {
// ...
}
export default config
```
```ts [config.ts]
import type { UserConfig } from 'vitepress'
const config: UserConfig = {
// ...
}
export default config
```
:::
````
**Output**
::: code-group
```js [config.js]
/**
* @type {import('vitepress').UserConfig}
*/
const config = {
// ...
}
export default config
```
```ts [config.ts]
import type { UserConfig } from 'vitepress'
const config: UserConfig = {
// ...
}
export default config
```
:::
## Markdown File Inclusion
You can include a markdown file in another markdown file like this:

@ -0,0 +1,84 @@
# Code Groups
::: code-group
```txt-vue{1}
{{ 1 + 1 }}
```
```js [app.vue]
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
```
<!-- kkk -->
```vue-html{3,4} [layouts/custom.vue]
<template>
<div>
Some *custom* layout
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur imperdiet mi in nunc faucibus consequat.
<slot />
</div>
</template>
```
```js{1-3,5} [layouts/default.vue]
export default {
name: 'MyComponent'
// ...
}
<template>
<div>
Some *custom* layout
<slot />
</div>
</template>
```
:::
- in list
- ::: code-group
```js
printf('111')
```
```python
import torch as th
print("Hello world")
```
```
import torch as th
print("Hello world")
```
:::
```
.
├─ index.md
├─ foo
│ ├─ index.md
│ ├─ one.md
│ └─ two.md
└─ bar
├─ index.md
├─ three.md
└─ four.md
```
- ```md{1-3,5}
[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 -->
```

@ -84,6 +84,7 @@
"@vue/devtools-api": "^6.4.5",
"@vueuse/core": "^9.6.0",
"body-scroll-lock": "4.0.0-beta.0",
"nanoid": "3.3.4",
"shiki": "^0.11.1",
"vite": "^4.0.0",
"vue": "^3.2.45"

@ -59,6 +59,7 @@ importers:
markdown-it-emoji: ^2.0.2
micromatch: ^4.0.5
minimist: ^1.2.7
nanoid: 3.3.4
npm-run-all: ^4.1.5
ora: ^5.4.1
picocolors: ^1.0.0
@ -91,6 +92,7 @@ importers:
'@vue/devtools-api': 6.4.5
'@vueuse/core': 9.6.0_vue@3.2.45
body-scroll-lock: 4.0.0-beta.0
nanoid: 3.3.4
shiki: 0.11.1
vite: 4.0.0_@types+node@18.11.13
vue: 3.2.45

@ -0,0 +1,23 @@
import { inBrowser } from 'vitepress'
export function useCodeGroups() {
if (inBrowser) {
window.addEventListener('click', (e) => {
const el = e.target as HTMLInputElement
if (el.matches('.vp-code-group input')) {
// input <- .tabs <- .vp-code-group
const group = el.parentElement?.parentElement
const i = Array.from(group?.querySelectorAll('input') || []).indexOf(el)
const current = group?.querySelector('div[class*="language-"].active')
const next = group?.querySelectorAll('div[class*="language-"]')?.[i]
if (current && next && current !== next) {
current.classList.remove('active')
next.classList.add('active')
}
}
})
}
}

@ -1,4 +1,4 @@
import { inBrowser } from '../utils.js'
import { inBrowser } from 'vitepress'
export function useCopyCode() {
if (inBrowser) {

@ -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 { useCodeGroups } from './composables/codeGroups.js'
const NotFound = Theme.NotFound || (() => '404 Not Found')
@ -43,6 +44,8 @@ const VitePressApp = defineComponent({
// setup global copy code handler
useCopyCode()
// setup global code groups handler
useCodeGroups()
if (Theme.setup) Theme.setup()
return () => h(Theme.Layout)

@ -4,6 +4,7 @@ import './styles/base.css'
import './styles/utils.css'
import './styles/components/custom-block.css'
import './styles/components/vp-code.css'
import './styles/components/vp-code-group.css'
import './styles/components/vp-doc.css'
import './styles/components/vp-sponsor.css'

@ -0,0 +1,87 @@
.vp-code-group {
margin-top: 16px;
}
.vp-code-group .tabs {
position: relative;
display: flex;
margin-right: -24px;
margin-left: -24px;
padding: 0 12px;
background-color: var(--vp-code-tab-bg);
overflow: auto;
}
.vp-code-group .tabs::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 1px;
background-color: var(--vp-code-tab-divider);
content: '';
}
@media (min-width: 640px) {
.vp-code-group .tabs {
margin-right: 0;
margin-left: 0;
border-radius: 8px 8px 0 0;
}
}
.vp-code-group .tabs input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.vp-code-group .tabs label {
position: relative;
display: inline-block;
border-bottom: 1px solid transparent;
padding: 0 12px;
line-height: 48px;
font-size: 14px;
font-weight: 500;
color: var(--vp-code-tab-text-color);
background-color: var(--vp-code-tab-bg);
white-space: nowrap;
cursor: pointer;
transition: color 0.25s;
}
.vp-code-group .tabs label::after {
position: absolute;
right: 8px;
bottom: -1px;
left: 8px;
z-index: 10;
height: 1px;
content: '';
background-color: transparent;
transition: background-color 0.25s;
}
.vp-code-group label:hover {
color: var(--vp-code-tab-hover-text-color);
}
.vp-code-group input:checked + label {
color: var(--vp-code-tab-active-text-color);
}
.vp-code-group input:checked + label::after {
background-color: var(--vp-code-tab-active-bar-color);
}
.vp-code-group div[class*='language-'] {
display: none;
margin-top: 0 !important;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
.vp-code-group div[class*='language-'].active {
display: block;
}

@ -228,10 +228,20 @@
--vp-code-copy-code-hover-bg: rgba(255, 255, 255, 0.05);
--vp-code-copy-code-active-text: var(--vp-c-text-dark-2);
--vp-code-tab-text-color: var(--vp-c-text-dark-2);
--vp-code-tab-bg: var(--vp-code-block-bg);
--vp-code-tab-divider: var(--vp-c-divider-dark-2);
--vp-code-tab-hover-text-color: var(--vp-c-text-dark-1);
--vp-code-tab-active-text-color: var(--vp-c-text-dark-1);
--vp-code-tab-active-bar-color: var(--vp-c-brand);
}
.dark {
--vp-code-block-bg: var(--vp-c-bg-alt);
--vp-code-block-bg: var(--vp-c-black);
/* --vp-code-tab: var(--vp-c-black-mute);*/
/* --vp-code-tab-hover: var(--vp-c-gray-dark-4);*/
}
/**

@ -2,6 +2,8 @@ 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 { nanoid } from 'nanoid'
import { extractTitle } from './preWrapper'
export const containerPlugin = (md: MarkdownIt) => {
md.use(...createContainer('tip', 'TIP', md))
@ -18,6 +20,7 @@ export const containerPlugin = (md: MarkdownIt) => {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div class="vp-raw">\n` : `</div>\n`
})
.use(...createCodeGroup())
}
type ContainerArgs = [typeof container, string, { render: RenderRule }]
@ -47,3 +50,42 @@ function createContainer(
}
]
}
function createCodeGroup(): ContainerArgs {
return [
container,
'code-group',
{
render(tokens, idx) {
if (tokens[idx].nesting === 1) {
const name = nanoid(5)
let tabs = ''
let checked = 'checked="checked"'
for (
let i = idx + 1;
!(
tokens[i].nesting === -1 &&
tokens[i].type === 'container_code-group_close'
);
++i
) {
if (tokens[i].type === 'fence' && tokens[i].tag === 'code') {
const title = extractTitle(tokens[i].info)
const id = nanoid(7)
tabs += `<input type="radio" name="group-${name}" id="tab-${id}" ${checked}><label for="tab-${id}">${title}</label>`
if (checked) {
tokens[i].info += ' active'
checked = ''
}
}
}
return `<div class="vp-code-group"><div class="tabs">${tabs}</div><div class="blocks">\n`
}
return `</div></div>\n`
}
}
]
}

@ -20,8 +20,9 @@ import type { ThemeOptions } from '../markdown'
* [{ line: number, classes: string[] }]
*/
const attrsToLines = (attrs: string): HtmlRendererOptions['lineOptions'] => {
attrs = attrs.replace(/.*?([\d,-]+).*/, '$1').trim()
const result: number[] = []
if (!attrs.trim()) {
if (!attrs) {
return []
}
attrs

@ -1,22 +1,24 @@
// markdown-it plugin for wrapping <pre> ... </pre>.
//
// If your plugin was chained before preWrapper, you can add additional element directly.
// If your plugin was chained after preWrapper, you can use these slots:
// 1. <!--beforebegin-->
// 2. <!--afterbegin-->
// 3. <!--beforeend-->
// 4. <!--afterend-->
import MarkdownIt from 'markdown-it'
export const preWrapperPlugin = (md: MarkdownIt) => {
export function 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 { info } = args[0][args[1]]
const lang = extractLang(info)
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="language-${lang}${
/ active( |$)/.test(info) ? ' active' : ''
}"><button title="Copy Code" class="copy"></button><span class="lang">${lang}</span>${rawCode}</div>`
}
}
export function extractTitle(info: string) {
return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt'
}
const extractLang = (info: string) => {
return info
.trim()
.replace(/(-vue|{| ).*$/, '')
.replace(/^vue-html$/, 'template')
}

Loading…
Cancel
Save