返回 slidev
CodeRunner.vue
根目录 / packages / client / internals / CodeRunner.vue
1 <script setup lang="ts">
2 import type { CodeRunnerOutputs, RawAtValue } from '@slidev/types'
3 import { debounce, toArray } from '@antfu/utils'
4 import { useVModel } from '@vueuse/core'
5 import { computed, onMounted, onUnmounted, ref, shallowRef, toValue, watch, watchSyncEffect } from 'vue'
6 import { useNav } from '../composables/useNav'
7 import { useSlideContext } from '../context'
8 import { makeId } from '../logic/utils'
9 import setupCodeRunners from '../setup/code-runners'
10 import DomElement from './DomElement.vue'
11 import IconButton from './IconButton.vue'
12
13 const props = defineProps<{
14 modelValue: string
15 lang: string
16 autorun: boolean | 'once'
17 height?: string
18 showOutputAt?: RawAtValue
19 highlightOutput: boolean
20 runnerOptions?: Record<string, unknown>
21 }>()
22
23 const emit = defineEmits(['update:modelValue'])
24
25 const { isEmbedded, isPrintMode } = useNav()
26
27 const code = useVModel(props, 'modelValue', emit)
28
29 const { $renderContext, $clicksContext } = useSlideContext()
30 const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.value) && !($renderContext.value === 'overview' && isEmbedded.value))
31
32 const autorun = isPrintMode.value ? 'once' : props.autorun
33 const isRunning = ref(autorun)
34 const outputs = shallowRef<CodeRunnerOutputs>()
35 const runCount = ref(0)
36 const highlightFn = ref<(code: string, lang: string) => string>()
37
38 const hidden = ref(props.showOutputAt)
39 if (props.showOutputAt) {
40 const id = makeId()
41 onMounted(() => {
42 const info = $clicksContext.calculate(props.showOutputAt)
43 if (info) {
44 $clicksContext.register(id, info)
45 watchSyncEffect(() => {
46 hidden.value = !info.isActive.value
47 })
48 }
49 else {
50 hidden.value = false
51 }
52 })
53 onUnmounted(() => $clicksContext.unregister(id))
54 }
55
56 const triggerRun = debounce(200, async () => {
57 if (disabled.value)
58 return
59
60 const { highlight, run } = await setupCodeRunners()
61 highlightFn.value = highlight
62
63 const setAsRunning = setTimeout(() => {
64 isRunning.value = true
65 }, 500)
66
67 outputs.value = await run(code.value, props.lang, props.runnerOptions ?? {})
68 runCount.value += 1
69 isRunning.value = false
70
71 clearTimeout(setAsRunning)
72 })
73
74 if (autorun === 'once')
75 triggerRun()
76 else if (autorun)
77 watch(code, triggerRun, { immediate: true })
78 </script>
79
80 <template>
81 <div
82 v-show="!hidden"
83 class="relative flex flex-col rounded-b border-t border-main"
84 :style="{ height: props.height }"
85 data-waitfor=".slidev-runner-output"
86 >
87 <div v-if="disabled" class="text-sm text-center opacity-50">
88 Code is disabled in the "{{ $renderContext }}" mode
89 </div>
90 <div v-else-if="isRunning" class="text-sm text-center opacity-50">
91 Running...
92 </div>
93 <div v-else-if="!outputs" class="text-sm text-center opacity-50">
94 Click the play button to run the code
95 </div>
96 <div v-else :key="`run-${runCount}`" class="slidev-runner-output">
97 <template v-for="output, _idx1 of toArray(toValue(outputs))" :key="_idx1">
98 <div v-if="'html' in output" v-html="output.html" />
99 <div v-else-if="'error' in output" class="text-red-500">
100 {{ output.error }}
101 </div>
102 <DomElement v-else-if="'element' in output" :element="output.element" />
103 <div v-else class="output-line">
104 <template
105 v-for="item, idx2 in toArray(output)"
106 :key="idx2"
107 >
108 <span
109 v-if="item.highlightLang && highlightFn"
110 class="highlighted"
111 v-html="highlightFn(item.text, item.highlightLang)"
112 />
113 <span v-else :class="item.class">{{ item.text }}</span>
114 <span v-if="idx2 < toArray(output).length - 1" class="separator">,</span>
115 </template>
116 </div>
117 </template>
118 </div>
119 </div>
120 <div v-if="code.trim()" class="absolute right-1 top-1 max-h-full flex gap-1">
121 <IconButton class="w-8 h-8 max-h-full flex justify-center items-center" title="Run code" @click="triggerRun">
122 <div class="i-carbon:play" />
123 </IconButton>
124 </div>
125 </template>
126
127 <style lang="postcss">
128 .slidev-runner-output {
129 @apply px-5 py-3 flex-grow text-xs leading-[.8rem] font-$slidev-code-font-family select-text;
130 }
131
132 .slidev-runner-output .log-type {
133 @apply font-bold op-70;
134
135 &.DBG {
136 @apply text-gray-500;
137 }
138
139 &.LOG {
140 @apply text-blue-500;
141 }
142
143 &.WRN {
144 @apply text-orange-500;
145 }
146
147 &.ERR {
148 @apply text-red-500;
149 }
150 }
151
152 .slidev-runner-output .output-line {
153 @apply flex my-1 w-full;
154 }
155
156 .slidev-runner-output .separator {
157 @apply op-40 mr-1;
158 }
159
160 .slidev-runner-output .highlighted > pre {
161 @apply inline text-wrap !bg-transparent;
162 }
163 </style>
164
164 lines Plain Text