feat(shiki-vue-wrapper): add Shiki code highlighting component with Vue integration
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>shiki-vue-wrapper</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "shiki-vue-wrapper",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"shiki": "^4.1.0",
|
||||||
|
"tailwindcss": "^4.3.0",
|
||||||
|
"vue": "^3.5.34"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.9.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"typescript": "~6.0.3",
|
||||||
|
"vite": "^8.0.13",
|
||||||
|
"vue-tsc": "^3.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1355
File diff suppressed because it is too large
Load Diff
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
|
||||||
|
createApp(App).mount('#app');
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/assets/*"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import tailwind from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue(), tailwind()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user