Skip to content

Commit 8f670f9

Browse files
yuyinwsautofix-ci[bot]antfu
authored
feat: enhance code block (#2178)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Anthony Fu <[email protected]>
1 parent 0ac03d4 commit 8f670f9

File tree

17 files changed

+606
-31
lines changed

17 files changed

+606
-31
lines changed

demo/starter/slides.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,10 @@ image: https://cover.sli.dev
142142

143143
Use code snippets and get the highlighting directly, and even types hover!
144144

145-
```ts {all|5|7|7-8|10|all} twoslash
145+
```ts [filename-example.ts] {all|4|6|6-7|9|all} twoslash
146146
// TwoSlash enables TypeScript hover information
147147
// and errors in markdown code blocks
148148
// More at https://shiki.style/packages/twoslash
149-
150149
import { computed, ref } from 'vue'
151150

152151
const count = ref(0)
@@ -155,7 +154,7 @@ const doubled = computed(() => count.value * 2)
155154
doubled.value = 2
156155
```
157156

158-
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="334" color="#953" width="2" arrowSize="1" />
157+
<arrow v-click="[4, 5]" x1="350" y1="310" x2="195" y2="342" color="#953" width="2" arrowSize="1" />
159158

160159
<!-- This allow you to embed external code blocks -->
161160
<<< @/snippets/external.ts#snippet

docs/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ declare module 'vue' {
4343
CarbonUserSpeaker: typeof import('~icons/carbon/user-speaker')['default']
4444
CarbonVideo: typeof import('~icons/carbon/video')['default']
4545
CodeBlockWrapper: typeof import('./node_modules/@slidev/client/builtin/CodeBlockWrapper.vue')['default']
46+
CodeGroup: typeof import('./node_modules/@slidev/client/builtin/CodeGroup.vue')['default']
4647
CodiconAdd: typeof import('~icons/codicon/add')['default']
4748
CodiconEye: typeof import('~icons/codicon/eye')['default']
4849
CodiconGlobe: typeof import('~icons/codicon/globe')['default']

docs/features/code-groups.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
depends:
3+
- guide/syntax#code-block
4+
tags: [codeblock]
5+
description: |
6+
Group multiple code blocks and automatically match icon by the title name.
7+
---
8+
9+
# Code Groups
10+
11+
> [!NOTE]
12+
> This feature requires [MDC Syntax](/features/mdc#mdc-syntax). Enable `mdc: true` to use it.
13+
14+
You can group multiple code blocks like this:
15+
16+
````md
17+
::code-group
18+
19+
```sh [npm]
20+
npm i @slidev/cli
21+
```
22+
23+
```sh [yarn]
24+
yarn add @slidev/cli
25+
```
26+
27+
```sh [pnpm]
28+
pnpm add @slidev/cli
29+
```
30+
31+
::
32+
````
33+
34+
## Title Icon Matching
35+
36+
`code groups` and `code block` also supports the automatically icon matching by the title name:
37+
38+
![code-groups-demo](/assets/code-groups-demo.png)
39+
40+
::: details All builtin icons
41+
42+
```js
43+
const builtinIcons = {
44+
// package managers
45+
'pnpm': 'i-vscode-icons:file-type-light-pnpm',
46+
'npm': 'i-vscode-icons:file-type-npm',
47+
'yarn': 'i-vscode-icons:file-type-yarn',
48+
'bun': 'i-vscode-icons:file-type-bun',
49+
'deno': 'i-vscode-icons:file-type-deno',
50+
// frameworks
51+
'vue': 'i-vscode-icons:file-type-vue',
52+
'svelte': 'i-vscode-icons:file-type-svelte',
53+
'angular': 'i-vscode-icons:file-type-angular',
54+
'react': 'i-vscode-icons:file-type-reactjs',
55+
'next': 'i-vscode-icons:file-type-light-next',
56+
'nuxt': 'i-vscode-icons:file-type-nuxt',
57+
'solid': 'logos:solidjs-icon',
58+
'astro': 'i-vscode-icons:file-type-light-astro',
59+
// bundlers
60+
'rollup': 'i-vscode-icons:file-type-rollup',
61+
'webpack': 'i-vscode-icons:file-type-webpack',
62+
'vite': 'i-vscode-icons:file-type-vite',
63+
'esbuild': 'i-vscode-icons:file-type-esbuild',
64+
// configuration files
65+
'package.json': 'i-vscode-icons:file-type-node',
66+
'tsconfig.json': 'i-vscode-icons:file-type-tsconfig',
67+
'.npmrc': 'i-vscode-icons:file-type-npm',
68+
'.editorconfig': 'i-vscode-icons:file-type-editorconfig',
69+
'.eslintrc': 'i-vscode-icons:file-type-eslint',
70+
'.eslintignore': 'i-vscode-icons:file-type-eslint',
71+
'eslint.config': 'i-vscode-icons:file-type-eslint',
72+
'.gitignore': 'i-vscode-icons:file-type-git',
73+
'.gitattributes': 'i-vscode-icons:file-type-git',
74+
'.env': 'i-vscode-icons:file-type-dotenv',
75+
'.env.example': 'i-vscode-icons:file-type-dotenv',
76+
'.vscode': 'i-vscode-icons:file-type-vscode',
77+
'tailwind.config': 'vscode-icons:file-type-tailwind',
78+
'uno.config': 'i-vscode-icons:file-type-unocss',
79+
'unocss.config': 'i-vscode-icons:file-type-unocss',
80+
'.oxlintrc': 'i-vscode-icons:file-type-oxlint',
81+
'vue.config': 'i-vscode-icons:file-type-vueconfig',
82+
// filename extensions
83+
'.mts': 'i-vscode-icons:file-type-typescript',
84+
'.cts': 'i-vscode-icons:file-type-typescript',
85+
'.ts': 'i-vscode-icons:file-type-typescript',
86+
'.tsx': 'i-vscode-icons:file-type-typescript',
87+
'.mjs': 'i-vscode-icons:file-type-js',
88+
'.cjs': 'i-vscode-icons:file-type-js',
89+
'.json': 'i-vscode-icons:file-type-json',
90+
'.js': 'i-vscode-icons:file-type-js',
91+
'.jsx': 'i-vscode-icons:file-type-js',
92+
'.md': 'i-vscode-icons:file-type-markdown',
93+
'.py': 'i-vscode-icons:file-type-python',
94+
'.ico': 'i-vscode-icons:file-type-favicon',
95+
'.html': 'i-vscode-icons:file-type-html',
96+
'.css': 'i-vscode-icons:file-type-css',
97+
'.scss': 'i-vscode-icons:file-type-scss',
98+
'.yml': 'i-vscode-icons:file-type-light-yaml',
99+
'.yaml': 'i-vscode-icons:file-type-light-yaml',
100+
'.php': 'i-vscode-icons:file-type-php',
101+
}
102+
```
103+
104+
:::
105+
106+
## Custom Icon
107+
108+
You can also specify the icon manually by the following steps:
109+
110+
1. Add the icon to the `uno.config.ts` file:
111+
112+
```ts [uno.config.ts] {3-5}
113+
import { defineConfig } from 'unocss'
114+
115+
export default defineConfig({
116+
safelist: [
117+
'i-vscode-icons:file-type-coverage',
118+
],
119+
})
120+
```
121+
122+
2. Use the icon in the code block with the `~icon~` syntax:
123+
124+
````md
125+
```sh [npm ~i-vscode-icons:file-type-coverage~]
126+
npm i @slidev/cli
127+
```
128+
````

docs/guide/syntax.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ More about code blocks:
148148
<LinkCard link="features/shiki-magic-move" />
149149
<LinkCard link="features/twoslash" />
150150
<LinkCard link="features/import-snippet" />
151+
<LinkCard link="features/code-groups" />
151152

152153
## LaTeX Blocks {#latex-block}
153154

216 KB
Loading

packages/client/builtin/CodeBlockWrapper.vue

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ Learn more: https://sli.dev/guide/syntax.html#line-highlighting
1212
-->
1313

1414
<script setup lang="ts">
15-
import type { PropType } from 'vue'
15+
import type { PropType, Ref } from 'vue'
1616
import { useClipboard } from '@vueuse/core'
17-
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
17+
import { computed, inject, onMounted, onUnmounted, ref, watchEffect } from 'vue'
1818
import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
1919
import { useSlideContext } from '../context'
2020
import { configs } from '../env'
21+
import TitleIcon from '../internals/TitleIcon.vue'
2122
import { makeId, updateCodeHighlightRange } from '../logic/utils'
2223
2324
const props = defineProps({
@@ -45,6 +46,10 @@ const props = defineProps({
4546
type: String,
4647
default: undefined,
4748
},
49+
title: {
50+
type: String,
51+
default: undefined,
52+
},
4853
})
4954
5055
const { $clicksContext: clicks } = useSlideContext()
@@ -115,6 +120,13 @@ function copyCode() {
115120
if (code)
116121
copy(code)
117122
}
123+
124+
// code block title
125+
const activeTitle = inject<Ref<string> | null>('activeTitle', null)
126+
127+
const isBlockTitleShow = computed(() => {
128+
return activeTitle === null && props.title
129+
})
118130
</script>
119131

120132
<template>
@@ -123,17 +135,26 @@ function copyCode() {
123135
class="slidev-code-wrapper relative group"
124136
:class="{
125137
'slidev-code-line-numbers': props.lines,
138+
'active': activeTitle === title,
126139
}"
127140
:style="{
128141
'max-height': props.maxHeight,
129142
'overflow-y': props.maxHeight ? 'scroll' : undefined,
130143
'--start': props.startLine,
131144
}"
145+
:data-title="title"
132146
>
147+
<div v-if="isBlockTitleShow" class="slidev-code-block-title">
148+
<TitleIcon :title="title" />
149+
<div class="leading-1em">
150+
{{ title.replace(/~([^~]+)~/g, '').trim() }}
151+
</div>
152+
</div>
133153
<slot />
134154
<button
135155
v-if="configs.codeCopy"
136-
class="slidev-code-copy absolute top-0 right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
156+
class="slidev-code-copy absolute right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
157+
:class="isBlockTitleShow ? 'top-10' : 'top-0'"
137158
:title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
138159
>
139160
<ph-check-circle v-if="copied" class="p-2 w-8 h-8" />

packages/client/builtin/CodeGroup.vue

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script setup lang="ts">
2+
import { onMounted, provide, ref, useTemplateRef } from 'vue'
3+
import TitleIcon from '../internals/TitleIcon.vue'
4+
5+
const codeGroupBlocksRef = useTemplateRef('codeGroupBlocksRef')
6+
const activeTitle = ref('')
7+
8+
provide('activeTitle', activeTitle)
9+
const tabs = ref<string[]>([])
10+
11+
onMounted(() => {
12+
const codeGroupBlocks = codeGroupBlocksRef.value
13+
let isActiveSet = false
14+
15+
codeGroupBlocks?.querySelectorAll('.slidev-code-wrapper')?.forEach((block) => {
16+
const title = block.getAttribute('data-title') || ''
17+
if (title) {
18+
if (!isActiveSet) {
19+
activeTitle.value = title
20+
isActiveSet = true
21+
}
22+
tabs.value.push(title)
23+
}
24+
})
25+
})
26+
</script>
27+
28+
<template>
29+
<div class="slidev-code-group">
30+
<div class="slidev-code-group-tabs">
31+
<div v-for="tab in tabs" :key="tab" class="flex items-center">
32+
<div
33+
class="slidev-code-tab"
34+
:style="{
35+
borderColor: activeTitle === tab ? 'var(--slidev-theme-primary)' : 'transparent',
36+
color: activeTitle === tab ? 'var(--slidev-code-tab-active-text-color)' : 'var(--slidev-code-tab-text-color)',
37+
}"
38+
@click="activeTitle = tab"
39+
>
40+
<TitleIcon :title="tab" />
41+
42+
<div>
43+
{{ tab.replace(/~([^~]+)~/g, '').trim() }}
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
<div ref="codeGroupBlocksRef" class="slidev-code-group-blocks">
49+
<slot />
50+
</div>
51+
</div>
52+
</template>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
title: string
4+
}>()
5+
6+
const builtinIcons: Record<string, string> = {
7+
// package managers
8+
'pnpm': 'i-vscode-icons:file-type-light-pnpm',
9+
'npm': 'i-vscode-icons:file-type-npm',
10+
'yarn': 'i-vscode-icons:file-type-yarn',
11+
'bun': 'i-vscode-icons:file-type-bun',
12+
'deno': 'i-vscode-icons:file-type-deno',
13+
// frameworks
14+
'vue': 'i-vscode-icons:file-type-vue',
15+
'svelte': 'i-vscode-icons:file-type-svelte',
16+
'angular': 'i-vscode-icons:file-type-angular',
17+
'react': 'i-vscode-icons:file-type-reactjs',
18+
'next': 'i-vscode-icons:file-type-light-next',
19+
'nuxt': 'i-vscode-icons:file-type-nuxt',
20+
'solid': 'logos:solidjs-icon',
21+
'astro': 'i-vscode-icons:file-type-light-astro',
22+
// bundlers
23+
'rollup': 'i-vscode-icons:file-type-rollup',
24+
'webpack': 'i-vscode-icons:file-type-webpack',
25+
'vite': 'i-vscode-icons:file-type-vite',
26+
'esbuild': 'i-vscode-icons:file-type-esbuild',
27+
// configuration files
28+
'package.json': 'i-vscode-icons:file-type-node',
29+
'tsconfig.json': 'i-vscode-icons:file-type-tsconfig',
30+
'.npmrc': 'i-vscode-icons:file-type-npm',
31+
'.editorconfig': 'i-vscode-icons:file-type-editorconfig',
32+
'.eslintrc': 'i-vscode-icons:file-type-eslint',
33+
'.eslintignore': 'i-vscode-icons:file-type-eslint',
34+
'eslint.config': 'i-vscode-icons:file-type-eslint',
35+
'.gitignore': 'i-vscode-icons:file-type-git',
36+
'.gitattributes': 'i-vscode-icons:file-type-git',
37+
'.env': 'i-vscode-icons:file-type-dotenv',
38+
'.env.example': 'i-vscode-icons:file-type-dotenv',
39+
'.vscode': 'i-vscode-icons:file-type-vscode',
40+
'tailwind.config': 'vscode-icons:file-type-tailwind',
41+
'uno.config': 'i-vscode-icons:file-type-unocss',
42+
'unocss.config': 'i-vscode-icons:file-type-unocss',
43+
'.oxlintrc': 'i-vscode-icons:file-type-oxlint',
44+
'vue.config': 'i-vscode-icons:file-type-vueconfig',
45+
// filename extensions
46+
'.mts': 'i-vscode-icons:file-type-typescript',
47+
'.cts': 'i-vscode-icons:file-type-typescript',
48+
'.ts': 'i-vscode-icons:file-type-typescript',
49+
'.tsx': 'i-vscode-icons:file-type-typescript',
50+
'.mjs': 'i-vscode-icons:file-type-js',
51+
'.cjs': 'i-vscode-icons:file-type-js',
52+
'.json': 'i-vscode-icons:file-type-json',
53+
'.js': 'i-vscode-icons:file-type-js',
54+
'.jsx': 'i-vscode-icons:file-type-js',
55+
'.md': 'i-vscode-icons:file-type-markdown',
56+
'.py': 'i-vscode-icons:file-type-python',
57+
'.ico': 'i-vscode-icons:file-type-favicon',
58+
'.html': 'i-vscode-icons:file-type-html',
59+
'.css': 'i-vscode-icons:file-type-css',
60+
'.scss': 'i-vscode-icons:file-type-scss',
61+
'.yml': 'i-vscode-icons:file-type-light-yaml',
62+
'.yaml': 'i-vscode-icons:file-type-light-yaml',
63+
'.php': 'i-vscode-icons:file-type-php',
64+
}
65+
66+
function matchIcon(title: string) {
67+
const colonMatch = title.match(/~([^~]+)~/g)
68+
if (colonMatch && colonMatch.length > 0) {
69+
const icon = colonMatch[0].slice(1, -1)
70+
return icon
71+
}
72+
73+
const sortedKeys = Object.keys(builtinIcons).sort((a, b) => b.length - a.length)
74+
for (const key of sortedKeys) {
75+
if (title.toLowerCase().includes(key.toLowerCase())) {
76+
return builtinIcons[key]
77+
}
78+
}
79+
return ''
80+
}
81+
</script>
82+
83+
<template>
84+
<div v-if="matchIcon(title)" :class="`${matchIcon(title)} w-3.5 h-3.5 relative`" />
85+
</template>

0 commit comments

Comments
 (0)