feat(shiki-vue-wrapper): add Shiki code highlighting component with Vue integration

This commit is contained in:
2026-05-20 19:16:55 +00:00
parent d62855853b
commit 6f417ba514
14 changed files with 1662 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<script setup lang="ts">
import code from './assets/snippet?raw';
import ShikiCode from './ShikiCode/ShikiCode.vue';
</script>
<template>
<main class="h-full w-full flex">
<div class="m-auto max-w-136 rounded-3xl overflow-clip">
<ShikiCode :code line-numbers />
</div>
</main>
</template>
<style>
@import 'tailwindcss';
#app {
@apply h-svh w-screen antialiased;
}
</style>
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ShikiTransformer } from 'shiki/core'
import { useShikiHighlight } from './useShikiHighlight'
const props = withDefaults(
defineProps<{
code: string
lang?: string
theme?: string
themes?: { light: string; dark: string }
transformers?: ShikiTransformer[]
lineNumbers?: boolean
startLine?: number
}>(),
{ lang: 'javascript', lineNumbers: false, startLine: 1 },
);
const { html, isReady, error } = useShikiHighlight({
code: () => props.code,
lang: () => props.lang,
theme: () => props.theme,
themes: () => props.themes,
transformers: () => props.transformers,
})
const gutterStyle = computed(() => {
const total = props.startLine + props.code.split('\n').length - 1;
return {
'--shiki-line-start': String(props.startLine),
'--shiki-gutter-width': `${String(total).length}ch`,
};
});
</script>
<template>
<slot v-if="error" name="error" :error="error" :code="code">
<pre class="shiki-fallback"><code>{{ code }}</code></pre>
</slot>
<div
v-else-if="isReady"
class="shiki-host"
:data-line-numbers="lineNumbers ? '' : undefined"
:style="gutterStyle"
v-html="html"
/>
<slot v-else name="loading">
<pre class="shiki-fallback"><code>{{ code }}</code></pre>
</slot>
</template>
<style>
.shiki-host .shiki {
padding: 1rem;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-zinc-300) var(--color-zinc-950);
}
.shiki-host[data-line-numbers] .shiki code {
counter-reset: shiki-line calc(var(--shiki-line-start, 1) - 1);
}
.shiki-host[data-line-numbers] .shiki code .line::before {
counter-increment: shiki-line;
content: counter(shiki-line);
display: inline-block;
width: var(--shiki-gutter-width, 2ch);
margin-right: 1.25rem;
text-align: right;
color: color-mix(in srgb, currentColor 40%, transparent);
user-select: none;
}
/* shiki иногда оставляет пустую финальную строку — прячем её номер */
.shiki-host[data-line-numbers] .shiki code .line:last-child:empty::before {
content: none;
}
</style>
@@ -0,0 +1,31 @@
import { createHighlighterCore, type HighlighterCore } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
import js from 'shiki/langs/javascript.mjs';
import aurora from 'shiki/themes/aurora-x.mjs';
const createShiki = () =>
createHighlighterCore({
langs: [js],
themes: [aurora],
engine: createJavaScriptRegexEngine(),
});
let instance: Promise<HighlighterCore> | null =
import.meta.hot?.data.shiki ?? null;
export const getShiki = () => {
if (!instance) {
instance = createShiki();
if (import.meta.hot) import.meta.hot.data.shiki = instance;
}
return instance;
}
export const disposeShiki = async () => {
if (!instance) return;
;(await instance).dispose();
instance = null;
if (import.meta.hot) import.meta.hot.data.shiki = undefined;
}
if (import.meta.hot) import.meta.hot.accept();
@@ -0,0 +1,41 @@
import { shallowRef, watchEffect, type MaybeRefOrGetter, toValue } from 'vue';
import type { ShikiTransformer } from 'shiki/core';
export interface UseShikiHighlightOptions {
code: MaybeRefOrGetter<string>;
lang: MaybeRefOrGetter<string>;
theme?: MaybeRefOrGetter<string | undefined>;
themes?: MaybeRefOrGetter<{ light: string; dark: string } | undefined>;
transformers?: MaybeRefOrGetter<ShikiTransformer[] | undefined>;
}
export function useShikiHighlight(options: UseShikiHighlightOptions) {
const html = shallowRef('');
const isReady = shallowRef(false);
const error = shallowRef<Error | null>(null);
watchEffect(async (onCleanup) => {
let cancelled = false;
onCleanup(() => { cancelled = true });
try {
const { getShiki } = await import('./highlighter');
const shiki = await getShiki();
if (cancelled) return;
const themes = toValue(options.themes);
const theme = toValue(options.theme);
html.value = shiki.codeToHtml(toValue(options.code), {
lang: toValue(options.lang),
...(themes ? { themes } : { theme: theme ?? 'aurora-x' }),
transformers: toValue(options.transformers),
});
isReady.value = true;
} catch (e) {
if (!cancelled) error.value = e as Error;
}
})
return { html, isReady, error };
}
+17
View File
@@ -0,0 +1,17 @@
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const uid = '<API_KEY_UID>';
const privateKeyBase64 = '<PRIVATE_KEY_BASE64>';
const privateKeyPem = Buffer.from(privateKeyBase64, 'base64').toString('utf8');
const jwtToken = jwt.sign(
{
exp: Math.floor(Date.now() / 1000) + 60
},
privateKeyPem,
{ algorithm: 'RS256' }
);
console.log(jwtToken);
+4
View File
@@ -0,0 +1,4 @@
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');