返回 slidev
CodeBlockWrapper.vue
根目录 / packages / client / builtin / CodeBlockWrapper.vue
1 <!--
2 Line highlighting for code blocks.
3 (auto transformed, you don't need to use this component directly)
4
5 Usage:
6
7 ```ts {1,3-5|2,4}
8 const your_code = 'here'
9 ```
10
11 Learn more: https://sli.dev/guide/syntax.html#line-highlighting
12 -->
13
14 <script setup lang="ts">
15 import type { PropType, Ref } from 'vue'
16 import { useClipboard } from '@vueuse/core'
17 import { computed, inject, onMounted, onUnmounted, ref, watchEffect } from 'vue'
18 import { CLASS_VCLICK_HIDDEN, CLICKS_MAX } from '../constants'
19 import { useSlideContext } from '../context'
20 import { configs } from '../env'
21 import TitleIcon from '../internals/TitleIcon.vue'
22 import { makeId, updateCodeHighlightRange } from '../logic/utils'
23
24 const props = defineProps({
25 ranges: {
26 type: Array as PropType<string[]>,
27 default: () => [],
28 },
29 finally: {
30 type: [String, Number],
31 default: 'last',
32 },
33 startLine: {
34 type: Number,
35 default: 1,
36 },
37 lines: {
38 type: Boolean,
39 default: configs.lineNumbers,
40 },
41 at: {
42 type: [String, Number],
43 default: '+1',
44 },
45 maxHeight: {
46 type: String,
47 default: undefined,
48 },
49 title: {
50 type: String,
51 default: '',
52 },
53 })
54 const { $frontmatter, $clicksContext: clicks } = useSlideContext()
55 const isCodeCopy = computed(() => $frontmatter?.codeCopy ?? configs.codeCopy)
56
57 const el = ref<HTMLDivElement>()
58 const id = makeId()
59
60 onUnmounted(() => {
61 clicks!.unregister(id)
62 })
63
64 watchEffect(() => {
65 el.value?.classList.toggle('slidev-code-line-numbers', props.lines)
66 })
67
68 onMounted(() => {
69 if (!clicks || !props.ranges?.length)
70 return
71
72 const clicksInfo = clicks.calculateSince(props.at, props.ranges.length - 1)
73 clicks.register(id, clicksInfo)
74
75 const index = computed(() => clicksInfo ? Math.max(0, clicks.current - clicksInfo.start + 1) : CLICKS_MAX)
76
77 const finallyRange = computed(() => {
78 return props.finally === 'last' ? props.ranges.at(-1) : props.finally.toString()
79 })
80
81 watchEffect(() => {
82 if (!el.value)
83 return
84
85 let rangeStr = props.ranges[index.value] ?? finallyRange.value
86 const hide = rangeStr === 'hide'
87 el.value.classList.toggle(CLASS_VCLICK_HIDDEN, hide)
88 if (hide)
89 rangeStr = props.ranges[index.value + 1] ?? finallyRange.value
90
91 const pre = el.value.querySelector('.shiki')!
92 const lines = Array.from(pre.querySelectorAll('code > .line'))
93 const linesCount = lines.length
94
95 updateCodeHighlightRange(
96 rangeStr,
97 linesCount,
98 props.startLine,
99 no => [lines[no]],
100 )
101
102 // Scroll to the highlighted line if `maxHeight` is set
103 if (props.maxHeight) {
104 const highlightedEls = Array.from(pre.querySelectorAll('.line.highlighted')) as HTMLElement[]
105 const height = highlightedEls.reduce((acc, el) => el.offsetHeight + acc, 0)
106 if (height > el.value.offsetHeight) {
107 highlightedEls[0].scrollIntoView({ behavior: 'smooth', block: 'start' })
108 }
109 else if (highlightedEls.length > 0) {
110 const middleEl = highlightedEls[Math.round((highlightedEls.length - 1) / 2)]
111 middleEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
112 }
113 }
114 })
115 })
116
117 const { copied, copy } = useClipboard()
118
119 function copyCode() {
120 const code = el.value?.querySelector('.slidev-code')?.textContent
121 if (code)
122 copy(code)
123 }
124
125 // code block title
126 const activeTitle = inject<Ref<string> | null>('activeTitle', null)
127
128 const isBlockTitleShow = computed(() => {
129 return activeTitle === null && props.title
130 })
131 </script>
132
133 <template>
134 <div
135 ref="el"
136 class="slidev-code-wrapper relative group"
137 :class="{
138 'slidev-code-line-numbers': props.lines,
139 'active': activeTitle === title,
140 }"
141 :style="{
142 'max-height': props.maxHeight,
143 'overflow-y': props.maxHeight ? 'scroll' : undefined,
144 '--start': props.startLine,
145 }"
146 :data-title="title"
147 >
148 <div v-if="isBlockTitleShow" class="slidev-code-block-title">
149 <TitleIcon :title="title" />
150 <div class="leading-1em">
151 {{ title.replace(/~([^~]+)~/g, '').trim() }}
152 </div>
153 </div>
154 <slot />
155 <button
156 v-if="isCodeCopy"
157 class="slidev-code-copy absolute right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
158 :class="isBlockTitleShow ? 'top-10' : 'top-0'"
159 :title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
160 >
161 <ph-check-circle v-if="copied" class="p-2 w-8 h-8" />
162 <ph-clipboard v-else class="p-2 w-8 h-8" />
163 </button>
164 </div>
165 </template>
166
166 lines Plain Text