| 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 |