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