diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a24c1be1f109..b800ec8b0eb9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -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' }, diff --git a/docs/guide/test.md b/docs/guide/test.md new file mode 100644 index 000000000000..741b4cda3c48 --- /dev/null +++ b/docs/guide/test.md @@ -0,0 +1,81 @@ +# Code Groups + +test + +:::code-group + +```js [app.vue] + +``` + +```js{3,4} [layouts/custom.vue] + +``` + +```js{1-3,5} [layouts/default.vue] +export default { + name: 'MyComponent' + // ... +} + +``` + +```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](/) +[foo](/foo/) +[foo heading](./#heading) +[bar - three](../bar/three) +[bar - three](../bar/three.md) +[bar - four](../bar/four.html) +``` diff --git a/src/client/app/composables/codeGroup.ts b/src/client/app/composables/codeGroup.ts new file mode 100644 index 000000000000..927885d536f6 --- /dev/null +++ b/src/client/app/composables/codeGroup.ts @@ -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') + } + }) + } +} diff --git a/src/client/app/index.ts b/src/client/app/index.ts index 84bd1c23da8b..cc9d93e1391c 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -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') @@ -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) } diff --git a/src/client/theme-default/styles/components/vp-code.css b/src/client/theme-default/styles/components/vp-code.css index ee74e5b27d70..158570608470 100644 --- a/src/client/theme-default/styles/components/vp-code.css +++ b/src/client/theme-default/styles/components/vp-code.css @@ -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; +} diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 909fbc108f7c..3c803a5c8d18 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -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-'], @@ -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; @@ -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; } diff --git a/src/client/theme-default/styles/vars.css b/src/client/theme-default/styles/vars.css index dff17f204251..fbe43984cc95 100644 --- a/src/client/theme-default/styles/vars.css +++ b/src/client/theme-default/styles/vars.css @@ -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); diff --git a/src/node/markdown/plugins/codeGroup.ts b/src/node/markdown/plugins/codeGroup.ts new file mode 100644 index 000000000000..a18b6f44e914 --- /dev/null +++ b/src/node/markdown/plugins/codeGroup.ts @@ -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#' diff --git a/src/node/markdown/plugins/containers.ts b/src/node/markdown/plugins/containers.ts index 40480a10ce41..cb5a38f17632 100644 --- a/src/node/markdown/plugins/containers.ts +++ b/src/node/markdown/plugins/containers.ts @@ -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)) @@ -14,6 +18,7 @@ export const containerPlugin = (md: MarkdownIt) => { render: (tokens: Token[], idx: number) => tokens[idx].nesting === 1 ? `
\n` : `
\n` }) + .use(...createCodeGroup()) .use(container, 'raw', { render: (tokens: Token[], idx: number) => tokens[idx].nesting === 1 ? `
\n` : `
\n` @@ -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 = `
\n
` + const buttonsBlock = codeTitles + .map((title, idx) => { + return `` + }) + .join('\n') + headerBlock += buttonsBlock + headerBlock += `
\n` + return headerBlock + } else { + return `
\n` + } + } + } + ] +} diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts index 7a6b22e7eca0..b18db0c0e032 100644 --- a/src/node/markdown/plugins/preWrapper.ts +++ b/src/node/markdown/plugins/preWrapper.ts @@ -8,15 +8,27 @@ // 4. 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 `
${ - lang === 'vue-html' ? 'template' : lang - }${rawCode}
` + + return `
+
+ + ${codeTitle} + ${rawCode} +
+
` } }