-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
ShikiHighlight.vue
214 lines (182 loc) · 6.01 KB
/
ShikiHighlight.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<!--
Styling syntax highlighting is a bit messy.
### There are three main presentational styles:
1. Inline
2. Multi-line with line numbers
3. Multi-line without line numbers
The rules for what conditional classes are applied are inside of the
shikiWrapperClasses computed property.
### The styles are spread across the template and the style block
1. Utility classes that affect all modes and don't need to target a specific
Shiki-only global style are placed on the top-most div.
2. Styling the line numbers themselves must be done inside of a <style>
block because the selectors are too complex.
3. When using style blocks without WindiCSS classes, you must use !important.
-->
<template>
<div class="cursor-text text-left">
<div
v-if="highlighterInitialized"
ref="codeEl"
:class="[
'shiki-wrapper',
// All styles contain these utility classes
'overflow-scroll hover:border-indigo-200 text-14px leading-24px font-normal',
/**
* 1. Single line is forced onto one line without any borders. It loses
* any additional padding.
*
* 2. Multi-line without line-numbers adds padding to compensate for the
* lack of margin-right that the line-numbers usually add. It has a
* border.
*
* 3. Multi-line with line-numbers doesn't have the padding, because the
* line numbers have margin-right.
*
* 4. Any of these can be wrapped with whitespace: pre-wrap. When using
* with line-numbers, the breaks will create a new line.
*/
{
'inline': props.inline,
'wrap': props.wrap,
'line-numbers': props.lineNumbers,
'p-8px': !props.lineNumbers && !props.inline && !props.codeframe,
'p-2px': props.codeframe,
},
props.class,
]"
@click="copyOnClick ? () => copyCode() : () => { }"
v-html="highlightedCode"
/>
<pre
v-else
class="border rounded font-normal border-gray-100 py-8px text-14px leading-24px overflow-scroll"
:class="[props.class, lineNumbers ? 'pl-56px' : 'pl-8px']"
>{{ trimmedCode }}</pre>
<CopyButton
v-if="copyButton"
variant="outline"
tabindex="-1"
class="bg-white ml-auto -mt-32px sticky"
:class="numberOfLines === 1 ? 'bottom-5px right-5px' : 'bottom-8px right-8px'"
:text="code"
no-icon
/>
</div>
</template>
<script lang="ts" setup>
import type { Ref } from 'vue'
import { computed, onBeforeMount, ref } from 'vue'
import CopyButton from '../gql-components/CopyButton.vue'
import { initHighlighter, langsSupported, highlighter } from './highlight'
import type { CyLangType } from './highlight'
import { useClipboard } from '../gql-components/useClipboard'
const highlighterInitialized = ref(false)
onBeforeMount(async () => {
await initHighlighter()
highlighterInitialized.value = true
})
const props = withDefaults(defineProps<{
code: string
initialLine?: number
lang: CyLangType | undefined
lineNumbers?: boolean
inline?: boolean
wrap?: boolean
copyOnClick?: boolean
copyButton?: boolean
codeframe?: boolean
skipTrim?: boolean
class?: string | string[] | Record<string, any>
}>(), {
lineNumbers: false,
inline: false,
wrap: false,
copyOnClick: false,
codeframe: false,
initialLine: 1,
copyButton: false,
skipTrim: false,
class: undefined,
})
const resolvedLang = computed(() => {
switch (props.lang) {
case 'javascript':
case 'js':
case 'jsx':
return 'jsx'
case 'typescript':
case 'ts':
case 'tsx':
return 'tsx'
default:
return props.lang && langsSupported.includes(props.lang)
? props.lang
// if the language is not recognized use plaintext
: 'plaintext'
}
})
const trimmedCode = computed(() => props.skipTrim ? props.code : props.code.trim())
const highlightedCode = computed(() => {
return highlighter?.codeToHtml(trimmedCode.value, resolvedLang.value)
})
const codeEl: Ref<HTMLElement | null> = ref(null)
const { copy } = useClipboard()
const copyCode = () => {
if (codeEl.value) {
const text = codeEl.value.innerText
copy(text)
}
}
const numberOfLines = computed(() => props.code.trim().split('\n').length)
</script>
<!-- This is a scoped style, but we're able to style *outside* of the
ShikiHighligh component. The reason this is possible is because we're using
the special "deep" selector exposed by Vue.
We want to do this because we want to retain the scoped style block to
avoid colliding with styles elsewhere in the document.
-->
<style lang="scss" scoped>
$offset: 1.1em;
.inline:deep(.shiki) {
@apply bg-gray-50 py-1 px-2 text-gray-500 inline-block;
}
.shiki-wrapper {
&:deep(.shiki) {
@apply border-r-transparent min-w-max border-r-10px;
}
&.wrap:deep(.line) {
white-space: pre-wrap;
}
&.line-numbers:deep(.shiki) {
@apply py-8px;
code {
counter-reset: step;
counter-increment: step calc(v-bind('props.initialLine') - 1);
// Keep bg-gray-50 synced with the box-shadows.
.line::before,
.line:first-child::before {
@apply bg-gray-50 text-right mr-16px min-w-40px px-8px text-gray-500 inline-block sticky;
left: 0px !important;
content: counter(step);
counter-increment: step;
}
// Adding padding to the top and bottom of these children adds unwanted
// line-height to the line. This doesn't look good when you select the text.
// To avoid this, we use box-shadows and offset the parent container.
:not(.line:only-child) {
&:first-child:before {
box-shadow: 0 (-1 * $offset) theme('colors.gray.50') !important;
}
&:last-child::before {
box-shadow: 0 $offset theme('colors.gray.50') !important;
}
}
// If this rule was used for all of them, the gray would overlap between rows.
.line:only-child::before {
box-shadow: (-1 * $offset) 0 0 $offset theme('colors.gray.50') !important;
}
}
}
}
</style>