feat: enhance entity management and reactivity in vue-sync-engine
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import code from './assets/snippet?raw';
|
||||
import ShikiCode from './ShikiCode/ShikiCode.vue';
|
||||
// Подсветка выполняется на этапе сборки — в браузер уходит готовый HTML,
|
||||
// без Shiki/грамматик в бандле.
|
||||
import codeHtml from './assets/snippet.ts?shiki';
|
||||
import ShikiStatic from './ShikiCode/ShikiStatic.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 />
|
||||
<ShikiStatic :html="codeHtml" line-numbers />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import type { ShikiTransformer } from 'shiki/core'
|
||||
import { useShikiHighlight } from './useShikiHighlight'
|
||||
import './shiki-host.css'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -47,33 +48,4 @@ const gutterStyle = computed(() => {
|
||||
<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>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import './shiki-host.css'
|
||||
|
||||
// HTML приходит из импорта `*?shiki` — подсветка уже сделана на этапе сборки.
|
||||
// Этот компонент не тянет ни Shiki, ни грамматики в клиентский бандл.
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
html: string
|
||||
lineNumbers?: boolean
|
||||
startLine?: number
|
||||
}>(),
|
||||
{ lineNumbers: false, startLine: 1 },
|
||||
);
|
||||
|
||||
const gutterStyle = computed(() => {
|
||||
const lines = props.html.match(/class="line"/g)?.length ?? 1;
|
||||
const total = props.startLine + lines - 1;
|
||||
return {
|
||||
'--shiki-line-start': String(props.startLine),
|
||||
'--shiki-gutter-width': `${String(total).length}ch`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="shiki-host"
|
||||
:data-line-numbers="lineNumbers ? '' : undefined"
|
||||
:style="gutterStyle"
|
||||
v-html="html"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { Plugin } from 'vite';
|
||||
import {
|
||||
createHighlighterCore,
|
||||
type HighlighterCore,
|
||||
type ShikiTransformer,
|
||||
} from 'shiki/core';
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
||||
|
||||
const SHIKI_QUERY = 'shiki';
|
||||
|
||||
/** Расширение файла -> id грамматики Shiki. */
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'jsx',
|
||||
ts: 'typescript', mts: 'typescript', cts: 'typescript', tsx: 'tsx',
|
||||
vue: 'vue', json: 'json', jsonc: 'jsonc', css: 'css', scss: 'scss',
|
||||
html: 'html', md: 'markdown', py: 'python', rs: 'rust', go: 'go',
|
||||
sh: 'bash', bash: 'bash', yml: 'yaml', yaml: 'yaml', toml: 'toml', sql: 'sql',
|
||||
};
|
||||
|
||||
export interface ShikiPluginOptions {
|
||||
/** Одиночная тема (по умолчанию aurora-x). Игнорируется, если задан `themes`. */
|
||||
theme?: string;
|
||||
/** Парные темы — Shiki отдаёт HTML с CSS-переменными для light/dark. */
|
||||
themes?: { light: string; dark: string };
|
||||
/** Доп. соответствия расширение -> язык поверх дефолтных. */
|
||||
langAlias?: Record<string, string>;
|
||||
/** Трансформеры Shiki (номера строк, диффы и т.п.). */
|
||||
transformers?: ShikiTransformer[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Импорт `./snippet.ts?shiki` возвращает строку с уже подсвеченным HTML.
|
||||
* Вся работа Shiki происходит в Node на этапе сборки/дева — в бандл клиента
|
||||
* не попадает ни движок, ни грамматики. Zero runtime.
|
||||
*
|
||||
* Язык берётся из расширения файла, либо из `?shiki&lang=...`.
|
||||
*/
|
||||
export function shiki(options: ShikiPluginOptions = {}): Plugin {
|
||||
const { theme = 'aurora-x', themes, transformers } = options;
|
||||
const extToLang = { ...EXT_TO_LANG, ...options.langAlias };
|
||||
|
||||
let highlighter: Promise<HighlighterCore> | null = null;
|
||||
const loadedLangs = new Set<string>();
|
||||
const loadedThemes = new Set<string>();
|
||||
|
||||
const getHighlighter = () => {
|
||||
highlighter ??= createHighlighterCore({
|
||||
langs: [],
|
||||
themes: [],
|
||||
engine: createJavaScriptRegexEngine(),
|
||||
});
|
||||
return highlighter;
|
||||
};
|
||||
|
||||
const ensureLang = async (hl: HighlighterCore, lang: string) => {
|
||||
if (loadedLangs.has(lang)) return;
|
||||
const mod = await import(`shiki/langs/${lang}.mjs`);
|
||||
await hl.loadLanguage(mod.default);
|
||||
loadedLangs.add(lang);
|
||||
};
|
||||
|
||||
const ensureTheme = async (hl: HighlighterCore, name: string) => {
|
||||
if (loadedThemes.has(name)) return;
|
||||
const mod = await import(`shiki/themes/${name}.mjs`);
|
||||
await hl.loadTheme(mod.default);
|
||||
loadedThemes.add(name);
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-shiki',
|
||||
enforce: 'pre',
|
||||
|
||||
async load(id) {
|
||||
const [filepath, rawQuery] = id.split('?', 2);
|
||||
if (!rawQuery) return;
|
||||
|
||||
const params = new URLSearchParams(rawQuery);
|
||||
if (!params.has(SHIKI_QUERY)) return;
|
||||
|
||||
const ext = filepath.split('.').pop()?.toLowerCase() ?? '';
|
||||
const lang = params.get('lang') ?? extToLang[ext] ?? ext ?? 'text';
|
||||
|
||||
// Перечитываем исходник сами + регистрируем как зависимость для HMR.
|
||||
const source = await readFile(filepath, 'utf8');
|
||||
this.addWatchFile(filepath);
|
||||
|
||||
const hl = await getHighlighter();
|
||||
await ensureLang(hl, lang);
|
||||
if (themes) {
|
||||
await ensureTheme(hl, themes.light);
|
||||
await ensureTheme(hl, themes.dark);
|
||||
} else {
|
||||
await ensureTheme(hl, theme);
|
||||
}
|
||||
|
||||
const html = hl.codeToHtml(source.replace(/\n$/, ''), {
|
||||
lang,
|
||||
...(themes ? { themes } : { theme }),
|
||||
transformers,
|
||||
});
|
||||
|
||||
return { code: `export default ${JSON.stringify(html)}`, map: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/** Импорт `*?shiki` отдаёт уже подсвеченный HTML (см. vite-plugin-shiki). */
|
||||
declare module '*?shiki' {
|
||||
const html: string;
|
||||
export default html;
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import tailwind from '@tailwindcss/vite';
|
||||
import { shiki } from './src/ShikiCode/vite-plugin-shiki';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwind()],
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwind(),
|
||||
shiki({ theme: 'aurora-x' }),
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user