Skip to content

Commit 7d3bf4b

Browse files
committed
add theme support
1 parent e83e9c1 commit 7d3bf4b

File tree

8 files changed

+167
-96
lines changed

8 files changed

+167
-96
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github: ["mProjectsCode"]
1+
github: ['mProjectsCode']

src/main.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-s
77
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers';
88
import { pluginFrames } from '@expressive-code/plugin-frames';
99
import { ThemeMapper } from 'src/themes/ThemeMapper';
10-
import { EC_THEME } from 'src/themes/ECTheme';
1110
import { CodeBlock } from 'src/CodeBlock';
12-
import { OBSIDIAN_THEME } from 'src/themes/ObsidianTheme';
1311
import { createCm6Plugin } from 'src/codemirror/Cm6_ViewPlugin';
1412
import { DEFAULT_SETTINGS, type Settings } from 'src/settings/Settings';
1513
import { ShikiSettingsTab } from 'src/settings/SettingsTab';
1614
import { filterHighlightAllPlugin } from 'src/PrismPlugin';
1715
import { LoadedLanguage } from 'src/LoadedLanguage';
16+
import { getECTheme } from 'src/themes/ECTheme';
1817

1918
// some languages break obsidian's `registerMarkdownCodeBlockProcessor`, so we blacklist them
2019
const languageNameBlacklist = new Set(['c++', 'c#', 'f#', 'mermaid']);
@@ -34,13 +33,17 @@ export default class ShikiPlugin extends Plugin {
3433
shiki: Highlighter;
3534
// @ts-expect-error TS2564
3635
settings: Settings;
36+
// @ts-expect-error TS2564
37+
loadedSettings: Settings;
3738

3839
async onload(): Promise<void> {
3940
await this.loadSettings();
4041

42+
this.loadedSettings = structuredClone(this.settings);
43+
4144
this.addSettingTab(new ShikiSettingsTab(this));
4245

43-
this.themeMapper = new ThemeMapper();
46+
this.themeMapper = new ThemeMapper(this);
4447
this.activeCodeBlocks = new Map();
4548
this.loadedLanguages = new Map();
4649

@@ -70,7 +73,7 @@ export default class ShikiPlugin extends Plugin {
7073
}),
7174
);
7275

73-
await loadPrism();
76+
await this.registerPrismPlugin();
7477
}
7578

7679
async loadLanguages(): Promise<void> {
@@ -113,14 +116,14 @@ export default class ShikiPlugin extends Plugin {
113116
}
114117
}
115118

116-
for (const disabledLanguage of this.settings.disabledLanguages) {
119+
for (const disabledLanguage of this.loadedSettings.disabledLanguages) {
117120
this.loadedLanguages.delete(disabledLanguage);
118121
}
119122
}
120123

121124
async loadEC(): Promise<void> {
122125
this.ec = new ExpressiveCodeEngine({
123-
themes: [new ExpressiveCodeTheme(this.themeMapper.getTheme())],
126+
themes: [new ExpressiveCodeTheme(await this.themeMapper.getThemeForEC())],
124127
plugins: [
125128
pluginShiki({
126129
langs: Object.values(bundledLanguages),
@@ -130,7 +133,7 @@ export default class ShikiPlugin extends Plugin {
130133
pluginLineNumbers(),
131134
pluginFrames(),
132135
],
133-
styleOverrides: EC_THEME,
136+
styleOverrides: getECTheme(this.loadedSettings),
134137
minSyntaxHighlightingColorContrast: 0,
135138
themeCssRoot: 'div.expressive-code',
136139
defaultProps: {
@@ -151,7 +154,7 @@ export default class ShikiPlugin extends Plugin {
151154

152155
async loadShiki(): Promise<void> {
153156
this.shiki = await getHighlighter({
154-
themes: [OBSIDIAN_THEME],
157+
themes: [await this.themeMapper.getTheme()],
155158
langs: Object.keys(bundledLanguages),
156159
});
157160
}
@@ -225,7 +228,7 @@ export default class ShikiPlugin extends Plugin {
225228

226229
return this.shiki.codeToTokens(code, {
227230
lang: shikiLanguage.getDefaultLanguage(),
228-
theme: 'obsidian-theme',
231+
theme: this.settings.theme,
229232
});
230233
}
231234

src/settings/Settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
export interface Settings {
22
disabledLanguages: string[];
3+
theme: string;
4+
preferThemeColors: boolean;
35
}
46

57
export const DEFAULT_SETTINGS: Settings = {
68
disabledLanguages: [],
9+
theme: 'obsidian-theme',
10+
preferThemeColors: true,
711
};

src/settings/SettingsTab.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PluginSettingTab, Setting } from 'obsidian';
22
import type ShikiPlugin from 'src/main';
33
import { StringSelectModal } from 'src/settings/StringSelectModal';
4+
import { bundledThemesInfo } from 'shiki';
45

56
export class ShikiSettingsTab extends PluginSettingTab {
67
plugin: ShikiPlugin;
@@ -14,6 +15,32 @@ export class ShikiSettingsTab extends PluginSettingTab {
1415
display(): void {
1516
this.containerEl.empty();
1617

18+
const themes = Object.fromEntries(bundledThemesInfo.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
19+
themes['obsidian-theme'] = 'Obsidian built-in (both)';
20+
21+
new Setting(this.containerEl)
22+
.setName('Theme')
23+
.setDesc('Select the theme for the code blocks. RESTART REQUIRED AFTER CHANGES.')
24+
.addDropdown(dropdown => {
25+
dropdown.addOptions(themes);
26+
dropdown.setValue(this.plugin.settings.theme).onChange(async value => {
27+
this.plugin.settings.theme = value;
28+
await this.plugin.saveSettings();
29+
});
30+
});
31+
32+
new Setting(this.containerEl)
33+
.setName('Prefer theme colors')
34+
.setDesc(
35+
'When enabled the plugin will prefer theme colors over CSS variables for things like the code block background. RESTART REQUIRED AFTER CHANGES.',
36+
)
37+
.addToggle(toggle => {
38+
toggle.setValue(this.plugin.settings.preferThemeColors).onChange(async value => {
39+
this.plugin.settings.preferThemeColors = value;
40+
await this.plugin.saveSettings();
41+
});
42+
});
43+
1744
new Setting(this.containerEl)
1845
.setName('Excluded Languages')
1946
.setDesc('Configure language to exclude. RESTART REQUIRED AFTER CHANGES.')

src/themes/ECTheme.ts

Lines changed: 97 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,98 @@
1-
import { type ExpressiveCodeEngineConfig } from '@expressive-code/core';
1+
import { type ExpressiveCodeEngineConfig, type ExpressiveCodeTheme, type StyleValueOrValues, type UnresolvedStyleValue } from '@expressive-code/core';
2+
import { type Settings } from 'src/settings/Settings';
23

3-
export const EC_THEME: ExpressiveCodeEngineConfig['styleOverrides'] = {
4-
borderColor: 'var(--shiki-code-block-border-color)',
5-
borderRadius: 'var(--shiki-code-block-border-radius)',
6-
borderWidth: 'var(--shiki-code-block-border-width)',
7-
codeBackground: 'var(--shiki-code-background)',
8-
codeFontFamily: 'var(--font-monospace)',
9-
codeFontSize: 'var(--code-size)',
10-
codeFontWeight: 'var(--font-normal)',
11-
codeForeground: 'var(--shiki-code-normal)',
12-
codeLineHeight: 'var(--line-height-normal)',
13-
codePaddingBlock: 'var(--size-4-3)',
14-
codePaddingInline: 'var(--size-4-4)',
15-
codeSelectionBackground: 'var(--text-selection)',
16-
focusBorder: 'var(--background-modifier-border)',
17-
scrollbarThumbColor: 'var(--scrollbar-thumb-bg)',
18-
scrollbarThumbHoverColor: 'var(--scrollbar-active-thumb-bg)',
19-
uiFontFamily: 'var(--font-interface)',
20-
uiFontSize: 'var(--font-text-size)',
21-
uiFontWeight: 'var(--font-normal)',
22-
uiLineHeight: 'var(--line-height-normal)',
23-
uiPaddingBlock: 'var(--size-4-2)',
24-
uiPaddingInline: 'var(--size-4-4)',
25-
uiSelectionBackground: 'var(--text-selection)',
26-
uiSelectionForeground: 'var(--text-normal)',
27-
gutterBorderColor: 'var(--shiki-gutter-border-color)',
28-
gutterBorderWidth: 'var(--shiki-gutter-border-width)',
29-
gutterForeground: 'var(--shiki-gutter-text-color)',
30-
gutterHighlightForeground: 'var(--shiki-gutter-text-color-highlight)',
31-
textMarkers: {
32-
delBackground: 'var(--shiki-highlight-red-background)',
33-
delBorderColor: 'var(--shiki-highlight-red)',
34-
delDiffIndicatorColor: 'rgba(var(--shiki-highlight-red-rgb), 1)',
35-
inlineMarkerBorderWidth: 'var(--border-width)',
36-
insBackground: 'var(--shiki-highlight-green-background)',
37-
insBorderColor: 'var(--shiki-highlight-green)',
38-
insDiffIndicatorColor: 'rgba(var(--shiki-highlight-green-rgb), 1)',
39-
lineDiffIndicatorMarginLeft: '0.3rem',
40-
lineMarkerAccentMargin: '0rem',
41-
lineMarkerAccentWidth: '0.15rem',
42-
lineMarkerLabelColor: 'white',
43-
lineMarkerLabelPaddingInline: '0.2rem',
44-
markBackground: 'var(--shiki-highlight-neutral-background)',
45-
markBorderColor: 'var(--shiki-highlight-neutral)',
46-
},
47-
frames: {
48-
editorActiveTabBackground: 'var(--shiki-code-background)',
49-
editorActiveTabBorderColor: 'transparent',
50-
editorActiveTabForeground: 'var(--text-normal)',
51-
editorActiveTabIndicatorBottomColor: 'transparent',
52-
editorActiveTabIndicatorHeight: '2px',
53-
editorActiveTabIndicatorTopColor: 'var(--shiki-highlight-neutral)',
54-
editorBackground: 'var(--shiki-code-background)',
55-
editorTabBarBackground: 'var(--color-primary)',
56-
editorTabBarBorderBottomColor: 'transparent',
57-
editorTabBarBorderColor: 'transparent',
58-
editorTabBorderRadius: 'var(--shiki-code-border-radius)',
59-
editorTabsMarginBlockStart: '0',
60-
editorTabsMarginInlineStart: '0',
61-
frameBoxShadowCssValue: 'none',
62-
inlineButtonBackground: 'var(--background-modifier-hover)',
63-
inlineButtonBackgroundActiveOpacity: '1',
64-
inlineButtonBackgroundHoverOrFocusOpacity: '1',
65-
inlineButtonBackgroundIdleOpacity: '0',
66-
inlineButtonBorder: 'var(--shiki-code-border-color)',
67-
inlineButtonBorderOpacity: '1',
68-
inlineButtonForeground: 'var(--text-normal)',
69-
shadowColor: 'transparent',
70-
terminalBackground: 'var(--shiki-code-background)',
71-
terminalTitlebarBackground: 'var(--shiki-code-background)',
72-
terminalTitlebarBorderBottomColor: 'transparent',
73-
terminalTitlebarDotsForeground: 'var(--shiki-terminal-dots-color)',
74-
terminalTitlebarDotsOpacity: '1',
75-
terminalTitlebarForeground: 'var(--text-normal)',
76-
tooltipSuccessBackground: 'var(--shiki-tooltip-background)',
77-
tooltipSuccessForeground: 'var(--shiki-tooltip-text-color)',
78-
},
79-
};
4+
export function getECTheme(settings: Settings): ExpressiveCodeEngineConfig['styleOverrides'] {
5+
const useThemeColors = settings.preferThemeColors && settings.theme !== 'obsidian-theme';
6+
7+
const backgroundColor: UnresolvedStyleValue = ({ theme }: { theme: ExpressiveCodeTheme }): StyleValueOrValues =>
8+
useThemeColors ? theme.colors['editor.background'] ?? 'var(--shiki-code-background)' : 'var(--shiki-code-background)';
9+
const foregroundColor: UnresolvedStyleValue = ({ theme }: { theme: ExpressiveCodeTheme }): StyleValueOrValues =>
10+
theme.colors['editor.foreground'] ?? 'var(--shiki-code-normal)';
11+
12+
const gutterBorderColor: UnresolvedStyleValue = ({ theme }: { theme: ExpressiveCodeTheme }): StyleValueOrValues =>
13+
useThemeColors ? theme.colors['editorLineNumber.foreground'] ?? 'var(--shiki-gutter-border-color)' : 'var(--shiki-gutter-border-color)';
14+
const gutterTextColor: UnresolvedStyleValue = ({ theme }: { theme: ExpressiveCodeTheme }): StyleValueOrValues =>
15+
useThemeColors ? theme.colors['editorLineNumber.foreground'] ?? 'var(--shiki-gutter-text-color)' : 'var(--shiki-gutter-text-color)';
16+
const gutterTextActiveColor: UnresolvedStyleValue = ({ theme }: { theme: ExpressiveCodeTheme }): StyleValueOrValues =>
17+
useThemeColors
18+
? (theme.colors['editorLineNumber.activeForeground'] || theme.colors['editorLineNumber.foreground']) ?? 'var(--shiki-gutter-text-color-highlight)'
19+
: 'var(--shiki-gutter-text-color-highlight)';
20+
21+
return {
22+
borderColor: 'var(--shiki-code-block-border-color)',
23+
borderRadius: 'var(--shiki-code-block-border-radius)',
24+
borderWidth: 'var(--shiki-code-block-border-width)',
25+
codeBackground: backgroundColor,
26+
codeFontFamily: 'var(--font-monospace)',
27+
codeFontSize: 'var(--code-size)',
28+
codeFontWeight: 'var(--font-normal)',
29+
codeForeground: foregroundColor,
30+
codeLineHeight: 'var(--line-height-normal)',
31+
codePaddingBlock: 'var(--size-4-3)',
32+
codePaddingInline: 'var(--size-4-4)',
33+
codeSelectionBackground: 'var(--text-selection)',
34+
focusBorder: 'var(--background-modifier-border)',
35+
scrollbarThumbColor: 'var(--scrollbar-thumb-bg)',
36+
scrollbarThumbHoverColor: 'var(--scrollbar-active-thumb-bg)',
37+
uiFontFamily: 'var(--font-interface)',
38+
uiFontSize: 'var(--font-text-size)',
39+
uiFontWeight: 'var(--font-normal)',
40+
uiLineHeight: 'var(--line-height-normal)',
41+
uiPaddingBlock: 'var(--size-4-2)',
42+
uiPaddingInline: 'var(--size-4-4)',
43+
uiSelectionBackground: 'var(--text-selection)',
44+
uiSelectionForeground: 'var(--text-normal)',
45+
gutterBorderColor: gutterBorderColor,
46+
gutterBorderWidth: 'var(--shiki-gutter-border-width)',
47+
gutterForeground: gutterTextColor,
48+
gutterHighlightForeground: gutterTextActiveColor,
49+
textMarkers: {
50+
delBackground: 'var(--shiki-highlight-red-background)',
51+
delBorderColor: 'var(--shiki-highlight-red)',
52+
delDiffIndicatorColor: 'rgba(var(--shiki-highlight-red-rgb), 1)',
53+
inlineMarkerBorderWidth: 'var(--border-width)',
54+
insBackground: 'var(--shiki-highlight-green-background)',
55+
insBorderColor: 'var(--shiki-highlight-green)',
56+
insDiffIndicatorColor: 'rgba(var(--shiki-highlight-green-rgb), 1)',
57+
lineDiffIndicatorMarginLeft: '0.3rem',
58+
lineMarkerAccentMargin: '0rem',
59+
lineMarkerAccentWidth: '0.15rem',
60+
lineMarkerLabelColor: 'white',
61+
lineMarkerLabelPaddingInline: '0.2rem',
62+
markBackground: 'var(--shiki-highlight-neutral-background)',
63+
markBorderColor: 'var(--shiki-highlight-neutral)',
64+
},
65+
frames: {
66+
editorActiveTabBackground: backgroundColor,
67+
editorActiveTabBorderColor: 'transparent',
68+
editorActiveTabForeground: 'var(--text-normal)',
69+
editorActiveTabIndicatorBottomColor: 'transparent',
70+
editorActiveTabIndicatorHeight: 'var(--shiki-active-tab-border-width)',
71+
editorActiveTabIndicatorTopColor: 'var(--shiki-active-tab-border-color)',
72+
editorBackground: backgroundColor,
73+
editorTabBarBackground: 'var(--color-primary)',
74+
editorTabBarBorderBottomColor: 'transparent',
75+
editorTabBarBorderColor: 'transparent',
76+
editorTabBorderRadius: 'var(--shiki-code-border-radius)',
77+
editorTabsMarginBlockStart: '0',
78+
editorTabsMarginInlineStart: '0',
79+
frameBoxShadowCssValue: 'none',
80+
inlineButtonBackground: 'var(--background-modifier-hover)',
81+
inlineButtonBackgroundActiveOpacity: '1',
82+
inlineButtonBackgroundHoverOrFocusOpacity: '1',
83+
inlineButtonBackgroundIdleOpacity: '0',
84+
inlineButtonBorder: 'var(--shiki-code-border-color)',
85+
inlineButtonBorderOpacity: '1',
86+
inlineButtonForeground: 'var(--text-normal)',
87+
shadowColor: 'transparent',
88+
terminalBackground: backgroundColor,
89+
terminalTitlebarBackground: backgroundColor,
90+
terminalTitlebarBorderBottomColor: 'transparent',
91+
terminalTitlebarDotsForeground: 'var(--shiki-terminal-dots-color)',
92+
terminalTitlebarDotsOpacity: '1',
93+
terminalTitlebarForeground: 'var(--text-normal)',
94+
tooltipSuccessBackground: 'var(--shiki-tooltip-background)',
95+
tooltipSuccessForeground: 'var(--shiki-tooltip-text-color)',
96+
},
97+
};
98+
}

src/themes/ObsidianTheme.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ export const OBSIDIAN_THEME = {
44
displayName: 'Obsidian Theme',
55
name: 'obsidian-theme',
66
semanticHighlighting: true,
7-
colors: {
8-
'editor.background': 'var(--shiki-code-background)',
9-
'editor.foreground': 'var(--shiki-code-normal)',
10-
},
117
tokenColors: [
128
{
139
scope: ['emphasis'],

src/themes/ThemeMapper.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
import { type ThemeRegistration } from 'shiki';
1+
import { type BundledTheme, bundledThemes, type ThemeRegistration } from 'shiki';
22
import type * as hast_util_to_html_lib_types from 'hast-util-to-html/lib/types';
33
import type * as hast_types from 'hast';
44
import { OBSIDIAN_THEME } from 'src/themes/ObsidianTheme';
5+
import type ShikiPlugin from 'src/main';
56

67
export class ThemeMapper {
8+
plugin: ShikiPlugin;
79
mapCounter: number;
810
mapping: Map<string, string>;
911

10-
constructor() {
12+
constructor(plugin: ShikiPlugin) {
13+
this.plugin = plugin;
14+
1115
this.mapCounter = 0;
1216
this.mapping = new Map();
1317
}
1418

15-
getTheme(): ThemeRegistration {
19+
async getThemeForEC(): Promise<ThemeRegistration> {
20+
if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
21+
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
22+
}
23+
1624
return {
1725
displayName: OBSIDIAN_THEME.displayName,
1826
name: OBSIDIAN_THEME.name,
1927
semanticHighlighting: OBSIDIAN_THEME.semanticHighlighting,
20-
colors: Object.fromEntries(Object.entries(OBSIDIAN_THEME.colors).map(([key, value]) => [key, this.mapColor(value)])),
2128
tokenColors: OBSIDIAN_THEME.tokenColors.map(token => {
2229
const newToken = { ...token };
2330

@@ -34,6 +41,14 @@ export class ThemeMapper {
3441
};
3542
}
3643

44+
async getTheme(): Promise<ThemeRegistration> {
45+
if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
46+
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
47+
}
48+
49+
return OBSIDIAN_THEME;
50+
}
51+
3752
mapColor(color: string): string {
3853
if (this.mapping.has(color)) {
3954
return this.mapping.get(color)!;
@@ -46,6 +61,10 @@ export class ThemeMapper {
4661
}
4762

4863
fixAST(ast: hast_util_to_html_lib_types.Parent): hast_util_to_html_lib_types.Parent {
64+
if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
65+
return ast;
66+
}
67+
4968
ast.children = ast.children.map(child => {
5069
if (child.type === 'element') {
5170
return this.fixNode(child);

0 commit comments

Comments
 (0)