-
Notifications
You must be signed in to change notification settings - Fork 181
feat(web): add Shiki/Mermaid theme preference #653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1368888
9a3fa0d
3a7d6b2
123a4c6
f72fb47
38f5116
ffd6d58
a82f981
c7ed18c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <script setup lang="ts"> | ||
| import { computed } from 'vue' | ||
| import { MermaidBlockNode, type CodeBlockNode } from 'markstream-vue' | ||
| import { useSettingsStore } from '@/store/settings' | ||
| import { applyMermaidThemeToSource, resolveMermaidIsDark } from '@/store/settings/mermaid' | ||
|
|
||
| defineOptions({ inheritAttrs: false }) | ||
|
|
||
| const props = defineProps<{ | ||
| node: CodeBlockNode | ||
| loading?: boolean | ||
| isDark?: boolean | ||
| }>() | ||
|
|
||
| const settings = useSettingsStore() | ||
|
|
||
| const themedNode = computed<CodeBlockNode>(() => { | ||
| if (settings.mermaidTheme === 'auto') return props.node | ||
| const content = props.node?.content ?? '' | ||
| const next = applyMermaidThemeToSource(content, settings.mermaidTheme) | ||
| if (next === content) return props.node | ||
| return { ...props.node, content: next } | ||
|
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Mermaid renderer receives the same Useful? React with 👍 / 👎. |
||
| }) | ||
|
|
||
| const themedIsDark = computed(() => | ||
| resolveMermaidIsDark(settings.mermaidTheme, Boolean(props.isDark)), | ||
| ) | ||
| </script> | ||
|
|
||
| <template> | ||
| <MermaidBlockNode | ||
| v-bind="$attrs" | ||
| :node="themedNode" | ||
| :loading="loading" | ||
| :is-dark="themedIsDark" | ||
| /> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,19 @@ | ||
| import { ref } from 'vue' | ||
| import { getLanguageByFilename } from '@/components/file-manager/utils' | ||
| import { useSettingsStore } from '@/store/settings' | ||
|
|
||
| import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki' | ||
|
|
||
| type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme> | ||
|
|
||
| let highlighterPromise: Promise<Highlighter> | null = null | ||
| const loadedLangs = new Set<string>(['plaintext']) | ||
| const loadedThemes = new Set<string>() | ||
|
|
||
| async function getHighlighter(): Promise<Highlighter> { | ||
| if (!highlighterPromise) { | ||
| highlighterPromise = import('shiki').then((m) => | ||
| m.createHighlighter({ themes: ['github-dark', 'github-light'], langs: [] }), | ||
| m.createHighlighter({ themes: [], langs: [] }), | ||
| ) | ||
| } | ||
| return highlighterPromise | ||
|
|
@@ -27,19 +29,42 @@ async function ensureLang(hl: Highlighter, lang: string) { | |
| } | ||
| } | ||
|
|
||
| async function ensureTheme(hl: Highlighter, theme: BundledTheme) { | ||
| if (loadedThemes.has(theme)) return | ||
| try { | ||
| await hl.loadTheme(theme) | ||
| loadedThemes.add(theme) | ||
| } catch { | ||
| loadedThemes.add(theme) | ||
| } | ||
| } | ||
|
|
||
| export function useShikiHighlighter() { | ||
| const settings = useSettingsStore() | ||
| const html = ref('') | ||
| const loading = ref(false) | ||
|
|
||
| const activeThemes = () => ({ | ||
| light: settings.shikiThemeLight as BundledTheme, | ||
| dark: settings.shikiThemeDark as BundledTheme, | ||
| }) | ||
|
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These settings are only read when Useful? React with 👍 / 👎. |
||
|
|
||
| async function ensurePairedThemes(hl: Highlighter) { | ||
| const themes = activeThemes() | ||
| await Promise.all([ensureTheme(hl, themes.light), ensureTheme(hl, themes.dark)]) | ||
| return themes | ||
| } | ||
|
|
||
| async function highlight(code: string, filename: string) { | ||
| loading.value = true | ||
| try { | ||
| const lang = getLanguageByFilename(filename) | ||
| const hl = await getHighlighter() | ||
| await ensureLang(hl, lang) | ||
| const themes = await ensurePairedThemes(hl) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: loadedLangs.has(lang) ? lang : 'plaintext', | ||
| themes: { light: 'github-light', dark: 'github-dark' }, | ||
| themes, | ||
| }) | ||
| } catch { | ||
| html.value = `<pre>${escapeHtml(code)}</pre>` | ||
|
|
@@ -56,9 +81,10 @@ export function useShikiHighlighter() { | |
| const normalized = (lang || 'plaintext').toLowerCase() | ||
| const hl = await getHighlighter() | ||
| await ensureLang(hl, normalized) | ||
| const themes = await ensurePairedThemes(hl) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: loadedLangs.has(normalized) ? normalized : 'plaintext', | ||
| themes: { light: 'github-light', dark: 'github-dark' }, | ||
| themes, | ||
| }) | ||
| } catch { | ||
| html.value = `<pre>${escapeHtml(code)}</pre>` | ||
|
|
@@ -73,8 +99,8 @@ export function useShikiHighlighter() { | |
| const lang = getLanguageByFilename(filename) | ||
| const hl = await getHighlighter() | ||
| await ensureLang(hl, lang) | ||
| const themes = await ensurePairedThemes(hl) | ||
| const effectiveLang = loadedLangs.has(lang) ? lang : 'plaintext' | ||
| const themes = { light: 'github-light', dark: 'github-dark' } | ||
|
|
||
| const oldHtml = oldText | ||
| ? hl.codeToHtml(oldText, { lang: effectiveLang, themes }) | ||
|
|
@@ -95,19 +121,44 @@ export function useShikiHighlighter() { | |
|
|
||
| async function highlightLanguage(code: string, lang: string, options: { | ||
| theme?: BundledTheme | ||
| // Explicit dual-theme override. Pass when the call site needs to dodge the | ||
| // `.dark .shiki span` !important rule that ships in the design system: set | ||
| // both halves to the same theme and shiki emits `--shiki-dark` equal to the | ||
| // light value, so the override resolves back to the picked colors. | ||
| themes?: { light: BundledTheme, dark: BundledTheme } | ||
| transparentPre?: boolean | ||
| } = {}) { | ||
| loading.value = true | ||
| try { | ||
| const hl = await getHighlighter() | ||
| await ensureLang(hl, lang) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: loadedLangs.has(lang) ? lang : 'plaintext', | ||
| ...(options.theme | ||
| ? { theme: options.theme } | ||
| : { themes: { light: 'github-light', dark: 'github-dark' } }), | ||
| transformers: options.transparentPre ? [transparentPreTransformer] : undefined, | ||
| }) | ||
| const effectiveLang = loadedLangs.has(lang) ? lang : 'plaintext' | ||
| const transformers = options.transparentPre ? [transparentPreTransformer] : undefined | ||
| if (options.theme) { | ||
| await ensureTheme(hl, options.theme) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: effectiveLang, | ||
| theme: options.theme, | ||
| transformers, | ||
| }) | ||
| } else if (options.themes) { | ||
| await Promise.all([ | ||
| ensureTheme(hl, options.themes.light), | ||
| ensureTheme(hl, options.themes.dark), | ||
| ]) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: effectiveLang, | ||
| themes: options.themes, | ||
| transformers, | ||
| }) | ||
| } else { | ||
| const themes = await ensurePairedThemes(hl) | ||
| html.value = hl.codeToHtml(code, { | ||
| lang: effectiveLang, | ||
| themes, | ||
| transformers, | ||
| }) | ||
| } | ||
| } catch { | ||
| html.value = `<pre>${escapeHtml(code)}</pre>` | ||
| } finally { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For MarkdownRender's built-in code block renderer, the unified
themeoption is forwarded throughcode-block-props; passing it as a top-levelthemeprop here is ignored. In file previews and release notes that use the default Markstream code block, selecting a custom Shiki theme therefore leaves code blocks on the library default theme, so wrap this as:code-block-props="{ theme: codeBlockTheme }"or use the dedicated code-block theme props.Useful? React with 👍 / 👎.