Skip to content

Commit 6043623

Browse files
committed
add support for custom themes (#11)
1 parent 9a7f432 commit 6043623

File tree

5 files changed

+87
-5
lines changed

5 files changed

+87
-5
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
bun install
2727
bun run build
2828
mkdir ${{ env.PLUGIN_NAME }}
29+
mkdir ${{ env.PLUGIN_NAME }}/themes
30+
touch ${{ env.PLUGIN_NAME }}/themes/place custom themes here
2931
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
3032
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
3133
ls

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ Some inline code `{jsx} <button role="button" />`.
3232

3333
This feature can be turned off in the settings.
3434

35+
### Custom Themes
36+
37+
This plugin comes with a [wide variety of themes](https://expressive-code.com/guides/themes/#using-bundled-themes) bundled in. In addition, it supports custom JSON theme files compatible with VS Code. Simply place your custom JSON theme files in the plugin's `themes` folder, which can be accessed from the settings. The custom themes will show up in the Theme dropdown after restarting Obsidian.
38+
3539
## Code Block Configuration
3640

3741
To configure the code block you add the configuration options on the same line as the opening triple backticks.

src/main.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import { filterHighlightAllPlugin } from 'src/PrismPlugin';
1515
import { LoadedLanguage } from 'src/LoadedLanguage';
1616
import { getECTheme } from 'src/themes/ECTheme';
1717

18+
interface CustomTheme {
19+
id: string;
20+
displayName: string;
21+
type: string;
22+
jsonData: Record<string, unknown>;
23+
}
24+
1825
// some languages break obsidian's `registerMarkdownCodeBlockProcessor`, so we blacklist them
1926
const languageNameBlacklist = new Set(['c++', 'c#', 'f#', 'mermaid']);
2027

@@ -38,7 +45,10 @@ export default class ShikiPlugin extends Plugin {
3845
// @ts-expect-error TS2564
3946
loadedSettings: Settings;
4047

48+
customThemes: CustomTheme[] = [];
49+
4150
async onload(): Promise<void> {
51+
await this.loadCustomThemes();
4252
await this.loadSettings();
4353

4454
this.loadedSettings = structuredClone(this.settings);
@@ -79,6 +89,38 @@ export default class ShikiPlugin extends Plugin {
7989
await this.registerPrismPlugin();
8090
}
8191

92+
async loadCustomThemes(): Promise<void> {
93+
94+
// @ts-expect-error TS2339
95+
const themePath = this.app.vault.adapter.path.join(this.app.vault.configDir, 'plugins', this.manifest.id, 'themes');
96+
97+
if (! await this.app.vault.adapter.exists(themePath)) {
98+
console.warn(`Path to custom themes does not exist: ${themePath}`);
99+
} else {
100+
const themeList = await this.app.vault.adapter.list(themePath);
101+
const themeFiles = themeList.files.filter(f => f.toLowerCase().endsWith('.json'));
102+
103+
for (let themeFile of themeFiles) {
104+
try {
105+
// not all theme files have proper metadata; some contain invalid JSON
106+
const theme = JSON.parse(await this.app.vault.adapter.read(themeFile));
107+
const baseName = themeFile.substring(`${themePath}/`.length);
108+
const displayName = theme.displayName ?? theme.name ?? baseName;
109+
theme.name = baseName;
110+
theme.type = theme.type ?? 'both';
111+
this.customThemes.push({
112+
id: baseName,
113+
displayName: displayName,
114+
type: theme.type,
115+
jsonData: theme
116+
});
117+
} catch(err) {
118+
console.warn(`Unable to load custom theme file: ${themeFile}`, err);
119+
}
120+
}
121+
}
122+
}
123+
82124
async loadLanguages(): Promise<void> {
83125
for (const [shikiLanguage, registration] of Object.entries(bundledLanguages)) {
84126
// the last element of the array is seemingly the most recent version of the language

src/settings/SettingsTab.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PluginSettingTab, Setting } from 'obsidian';
1+
import { PluginSettingTab, Setting, Platform } from 'obsidian';
22
import type ShikiPlugin from 'src/main';
33
import { StringSelectModal } from 'src/settings/StringSelectModal';
44
import { bundledThemesInfo } from 'shiki';
@@ -15,8 +15,16 @@ export class ShikiSettingsTab extends PluginSettingTab {
1515
display(): void {
1616
this.containerEl.empty();
1717

18-
const themes = Object.fromEntries(bundledThemesInfo.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
19-
themes['obsidian-theme'] = 'Obsidian built-in (both)';
18+
// sort custom themes by their display name
19+
this.plugin.customThemes.sort((a, b) => a.displayName.localeCompare(b.displayName));
20+
21+
const customThemes = Object.fromEntries(this.plugin.customThemes.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
22+
const builtInThemes = Object.fromEntries(bundledThemesInfo.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
23+
const themes = {
24+
'obsidian-theme': 'Obsidian built-in (both)',
25+
...customThemes,
26+
...builtInThemes
27+
};
2028

2129
new Setting(this.containerEl)
2230
.setName('Theme')
@@ -29,6 +37,20 @@ export class ShikiSettingsTab extends PluginSettingTab {
2937
});
3038
});
3139

40+
if (Platform.isDesktopApp) {
41+
new Setting(this.containerEl)
42+
.setName('Custom themes')
43+
.setDesc('Click to open the folder where you can add your custom JSON theme files. RESTART REQUIRED AFTER CHANGES.')
44+
.addButton(button => {
45+
button.setButtonText('Custom themes...').onClick(() => {
46+
// @ts-expect-error TS2339
47+
const themePath = this.plugin.app.vault.adapter.path.join(this.plugin.app.vault.configDir, 'plugins', this.plugin.manifest.id, 'themes');
48+
// @ts-expect-error TS2339
49+
this.plugin.app.openWithDefaultApp(themePath);
50+
});
51+
});
52+
}
53+
3254
new Setting(this.containerEl)
3355
.setName('Prefer theme colors')
3456
.setDesc(

src/themes/ThemeMapper.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export class ThemeMapper {
1717
}
1818

1919
async getThemeForEC(): Promise<ThemeRegistration> {
20-
if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
20+
if (this.plugin.loadedSettings.theme.toLowerCase().endsWith('.json')) {
21+
let theme = this.plugin.customThemes.find(theme => theme.id === this.plugin.loadedSettings.theme);
22+
if (theme?.jsonData) {
23+
return theme.jsonData;
24+
}
25+
}
26+
else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
2127
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
2228
}
2329

@@ -43,7 +49,13 @@ export class ThemeMapper {
4349
}
4450

4551
async getTheme(): Promise<ThemeRegistration> {
46-
if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
52+
if (this.plugin.loadedSettings.theme.toLowerCase().endsWith('.json')) {
53+
let theme = this.plugin.customThemes.find(theme => theme.id === this.plugin.loadedSettings.theme);
54+
if (theme?.jsonData) {
55+
return theme.jsonData;
56+
}
57+
}
58+
else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
4759
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
4860
}
4961

0 commit comments

Comments
 (0)