Skip to content

Commit

Permalink
improve support for custom themes
Browse files Browse the repository at this point in the history
  • Loading branch information
jcbize committed Sep 3, 2024
1 parent 6043623 commit 06daaeb
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 56 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ jobs:
bun install
bun run build
mkdir ${{ env.PLUGIN_NAME }}
mkdir ${{ env.PLUGIN_NAME }}/themes
touch ${{ env.PLUGIN_NAME }}/themes/place custom themes here
cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
ls
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ This feature can be turned off in the settings.

### Custom Themes

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.
This plugin comes bundled with a [wide variety of themes](https://expressive-code.com/guides/themes/#using-bundled-themes). In addition, it supports custom JSON theme files compatible with VS Code. To enable custom themes, create a folder containing your theme files, and specify the folder's path relative to your Vault in the plugin settings. After restarting Obsidian, your custom themes will be available in the Theme dropdown.

## Code Block Configuration

Expand Down
65 changes: 36 additions & 29 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadPrism, Plugin, TFile } from 'obsidian';
import { loadPrism, Plugin, TFile, Notice, normalizePath } from 'obsidian';
import { bundledLanguages, getHighlighter, type ThemedToken, type Highlighter, type TokensResult } from 'shiki';
import { ExpressiveCodeEngine, ExpressiveCodeTheme } from '@expressive-code/core';
import { pluginShiki } from '@expressive-code/plugin-shiki';
Expand All @@ -16,11 +16,12 @@ import { LoadedLanguage } from 'src/LoadedLanguage';
import { getECTheme } from 'src/themes/ECTheme';

interface CustomTheme {
id: string;
name: string;
displayName: string;
type: string;
jsonData: Record<string, unknown>;
}
colors?: Record<string, unknown>[];
tokenColors?: Record<string, unknown>[];
}

// some languages break obsidian's `registerMarkdownCodeBlockProcessor`, so we blacklist them
const languageNameBlacklist = new Set(['c++', 'c#', 'f#', 'mermaid']);
Expand Down Expand Up @@ -48,8 +49,8 @@ export default class ShikiPlugin extends Plugin {
customThemes: CustomTheme[] = [];

async onload(): Promise<void> {
await this.loadCustomThemes();
await this.loadSettings();
await this.loadCustomThemes();

this.loadedSettings = structuredClone(this.settings);

Expand Down Expand Up @@ -90,35 +91,41 @@ export default class ShikiPlugin extends Plugin {
}

async loadCustomThemes(): Promise<void> {
// custom themes are disabled unless users specify a folder for them in plugin settings
if (!this.settings.customThemeFolder) return;

// @ts-expect-error TS2339
const themePath = this.app.vault.adapter.path.join(this.app.vault.configDir, 'plugins', this.manifest.id, 'themes');
const themeFolder = normalizePath(this.settings.customThemeFolder);
if (!(await this.app.vault.adapter.exists(themeFolder))) {
new Notice(`${this.manifest.name}\nUnable to open custom themes folder: ${themeFolder}`, 5000);
return;
}

if (! await this.app.vault.adapter.exists(themePath)) {
console.warn(`Path to custom themes does not exist: ${themePath}`);
} else {
const themeList = await this.app.vault.adapter.list(themePath);
const themeFiles = themeList.files.filter(f => f.toLowerCase().endsWith('.json'));

for (let themeFile of themeFiles) {
try {
// not all theme files have proper metadata; some contain invalid JSON
const theme = JSON.parse(await this.app.vault.adapter.read(themeFile));
const baseName = themeFile.substring(`${themePath}/`.length);
const displayName = theme.displayName ?? theme.name ?? baseName;
theme.name = baseName;
theme.type = theme.type ?? 'both';
this.customThemes.push({
id: baseName,
displayName: displayName,
type: theme.type,
jsonData: theme
});
} catch(err) {
console.warn(`Unable to load custom theme file: ${themeFile}`, err);
const themeList = await this.app.vault.adapter.list(themeFolder);
const themeFiles = themeList.files.filter(f => f.toLowerCase().endsWith('.json'));

for (const themeFile of themeFiles) {
const baseName = themeFile.substring(`${themeFolder}/`.length);
try {
// validate that theme file JSON can be parsed and contains colors at a minimum
const theme = JSON.parse(await this.app.vault.adapter.read(themeFile)) as CustomTheme;
if (!theme.colors && !theme.tokenColors) {
throw Error('Invalid JSON theme file.');
}
// what metadata is available in the theme file depends on how it was created
theme.displayName = theme.displayName ?? theme.name ?? baseName;
theme.name = baseName;
theme.type = theme.type ?? 'both';
this.customThemes.push(theme);
} catch (e) {
new Notice(`${this.manifest.name}\nUnable to load custom theme: ${themeFile}`, 5000);
console.warn(`Unable to load custom theme: ${themeFile}`, e);
}
}

// if the user's set theme cannot be loaded (e.g. it was deleted), fall back to default theme
if (!this.customThemes.find(theme => theme.name === this.settings.theme)) {
this.settings.theme = DEFAULT_SETTINGS.theme;
}
}

async loadLanguages(): Promise<void> {
Expand Down
8 changes: 8 additions & 0 deletions src/obsidian-ex.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {};

declare module 'obsidian' {
interface App {
// opens a file or folder with the default application
openWithDefaultApp(path: string): void;
}
}
2 changes: 2 additions & 0 deletions src/settings/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export interface Settings {
disabledLanguages: string[];
customThemeFolder: string;
theme: string;
preferThemeColors: boolean;
inlineHighlighting: boolean;
}

export const DEFAULT_SETTINGS: Settings = {
disabledLanguages: [],
customThemeFolder: '',
theme: 'obsidian-theme',
preferThemeColors: true,
inlineHighlighting: true,
Expand Down
40 changes: 28 additions & 12 deletions src/settings/SettingsTab.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PluginSettingTab, Setting, Platform } from 'obsidian';
import { PluginSettingTab, Setting, Platform, Notice, normalizePath } from 'obsidian';
import type ShikiPlugin from 'src/main';
import { StringSelectModal } from 'src/settings/StringSelectModal';
import { bundledThemesInfo } from 'shiki';
Expand All @@ -18,12 +18,12 @@ export class ShikiSettingsTab extends PluginSettingTab {
// sort custom themes by their display name
this.plugin.customThemes.sort((a, b) => a.displayName.localeCompare(b.displayName));

const customThemes = Object.fromEntries(this.plugin.customThemes.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
const customThemes = Object.fromEntries(this.plugin.customThemes.map(theme => [theme.name, `${theme.displayName} (${theme.type})`]));
const builtInThemes = Object.fromEntries(bundledThemesInfo.map(theme => [theme.id, `${theme.displayName} (${theme.type})`]));
const themes = {
'obsidian-theme': 'Obsidian built-in (both)',
...customThemes,
...builtInThemes
...builtInThemes,
};

new Setting(this.containerEl)
Expand All @@ -39,15 +39,31 @@ export class ShikiSettingsTab extends PluginSettingTab {

if (Platform.isDesktopApp) {
new Setting(this.containerEl)
.setName('Custom themes')
.setDesc('Click to open the folder where you can add your custom JSON theme files. RESTART REQUIRED AFTER CHANGES.')
.addButton(button => {
button.setButtonText('Custom themes...').onClick(() => {
// @ts-expect-error TS2339
const themePath = this.plugin.app.vault.adapter.path.join(this.plugin.app.vault.configDir, 'plugins', this.plugin.manifest.id, 'themes');
// @ts-expect-error TS2339
this.plugin.app.openWithDefaultApp(themePath);
});
.setName('Custom themes folder location')
.setDesc('Folder relative to your Vault where custom JSON theme files are located. RESTART REQUIRED AFTER CHANGES.')
.addText(textbox => {
textbox
.setValue(this.plugin.settings.customThemeFolder)
.onChange(async value => {
this.plugin.settings.customThemeFolder = value;
await this.plugin.saveSettings();
})
.then(textbox => {
textbox.inputEl.style.width = '250px';
});
})
.addExtraButton(button => {
button
.setIcon('folder-open')
.setTooltip('Open custom themes folder')
.onClick(async () => {
const themeFolder = normalizePath(this.plugin.settings.customThemeFolder);
if (await this.app.vault.adapter.exists(themeFolder)) {
this.plugin.app.openWithDefaultApp(themeFolder);
} else {
new Notice(`Unable to open custom themes folder: ${themeFolder}`, 5000);
}
});
});
}

Expand Down
16 changes: 4 additions & 12 deletions src/themes/ThemeMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ export class ThemeMapper {

async getThemeForEC(): Promise<ThemeRegistration> {
if (this.plugin.loadedSettings.theme.toLowerCase().endsWith('.json')) {
let theme = this.plugin.customThemes.find(theme => theme.id === this.plugin.loadedSettings.theme);
if (theme?.jsonData) {
return theme.jsonData;
}
}
else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
return this.plugin.customThemes.find(theme => theme.name === this.plugin.loadedSettings.theme) as ThemeRegistration;
} else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
}

Expand All @@ -50,12 +46,8 @@ export class ThemeMapper {

async getTheme(): Promise<ThemeRegistration> {
if (this.plugin.loadedSettings.theme.toLowerCase().endsWith('.json')) {
let theme = this.plugin.customThemes.find(theme => theme.id === this.plugin.loadedSettings.theme);
if (theme?.jsonData) {
return theme.jsonData;
}
}
else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
return this.plugin.customThemes.find(theme => theme.name === this.plugin.loadedSettings.theme) as ThemeRegistration;
} else if (this.plugin.loadedSettings.theme !== 'obsidian-theme') {
return (await bundledThemes[this.plugin.loadedSettings.theme as BundledTheme]()).default;
}

Expand Down

0 comments on commit 06daaeb

Please sign in to comment.