Skip to content

Commit 11746b0

Browse files
committed
custom languages
1 parent 1863186 commit 11746b0

File tree

8 files changed

+776
-121
lines changed

8 files changed

+776
-121
lines changed

exampleVault/customLanguages/odin.json

Lines changed: 636 additions & 0 deletions
Large diffs are not rendered by default.

exampleVault/index.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,37 @@ Inline code
104104
105105
`{jsx} <button role="button" />`
106106
107+
```Odin
108+
package main
109+
110+
import "core:fmt"
111+
112+
main :: proc() {
113+
program := "+ + * 😃 - /"
114+
accumulator := 0
115+
116+
for token in program {
117+
switch token {
118+
case '+': accumulator += 1
119+
case '-': accumulator -= 1
120+
case '*': accumulator *= 2
121+
case '/': accumulator /= 2
122+
case '😃': accumulator *= accumulator
123+
case: // Ignore everything else
124+
}
125+
}
126+
127+
fmt.printf("The program \"%s\" calculates the value %d\n",
128+
program, accumulator)
129+
}
130+
```
131+
132+
133+
```cpp
134+
#include <iostream>
135+
136+
int main() {
137+
std::cout << "Hello World!";
138+
return 0;
139+
}
140+
```

src/CodeBlock.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,15 @@ export class CodeBlock extends MarkdownRenderChild {
55
plugin: ShikiPlugin;
66
source: string;
77
language: string;
8-
languageName: string;
98
ctx: MarkdownPostProcessorContext;
109
cachedMetaString: string;
1110

12-
constructor(plugin: ShikiPlugin, containerEl: HTMLElement, source: string, language: string, languageName: string, ctx: MarkdownPostProcessorContext) {
11+
constructor(plugin: ShikiPlugin, containerEl: HTMLElement, source: string, language: string, ctx: MarkdownPostProcessorContext) {
1312
super(containerEl);
1413

1514
this.plugin = plugin;
1615
this.source = source;
1716
this.language = language;
18-
this.languageName = languageName;
1917
this.ctx = ctx;
2018
this.cachedMetaString = '';
2119
}
@@ -31,7 +29,7 @@ export class CodeBlock extends MarkdownRenderChild {
3129
const startLine = lines[sectionInfo.lineStart];
3230

3331
// regexp to match the text after the code block language
34-
const regex = new RegExp('^[^`~]*?(```+|~~~+)' + this.languageName + ' (.*)', 'g');
32+
const regex = new RegExp('^[^`~]*?(```+|~~~+)' + this.language + ' (.*)', 'g');
3533
const match = regex.exec(startLine);
3634
if (match !== null) {
3735
return match[2];

src/Highlighter.ts

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ExpressiveCodeEngine, ExpressiveCodeTheme } from '@expressive-code/core';
22
import type ShikiPlugin from 'src/main';
3-
import { LoadedLanguage } from 'src/LoadedLanguage';
4-
import { bundledLanguages, createHighlighter, type Highlighter } from 'shiki/index.mjs';
3+
import { bundledLanguages, createHighlighter, type DynamicImportLanguageRegistration, type LanguageRegistration, type Highlighter } from 'shiki/index.mjs';
54
import { ThemeMapper } from 'src/themes/ThemeMapper';
65
import { pluginShiki } from '@expressive-code/plugin-shiki';
76
import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections';
@@ -30,9 +29,10 @@ export class CodeHighlighter {
3029

3130
ec!: ExpressiveCodeEngine;
3231
ecElements!: HTMLElement[];
33-
loadedLanguages!: Map<string, LoadedLanguage>;
32+
loadedLanguages!: string[];
3433
shiki!: Highlighter;
3534
customThemes!: CustomTheme[];
35+
customLanguages!: LanguageRegistration[];
3636

3737
constructor(plugin: ShikiPlugin) {
3838
this.plugin = plugin;
@@ -41,62 +41,46 @@ export class CodeHighlighter {
4141

4242
async load(): Promise<void> {
4343
await this.loadCustomThemes();
44-
45-
await this.loadLanguages();
44+
await this.loadCustomLanguages();
4645

4746
await this.loadEC();
4847
await this.loadShiki();
48+
49+
this.loadedLanguages = this.shiki.getLoadedLanguages();
4950
}
5051

5152
async unload(): Promise<void> {
5253
this.unloadEC();
5354
}
5455

55-
async loadLanguages(): Promise<void> {
56-
this.loadedLanguages = new Map();
56+
async loadCustomLanguages(): Promise<void> {
57+
this.customLanguages = [];
5758

58-
for (const [shikiLanguage, registration] of Object.entries(bundledLanguages)) {
59-
// the last element of the array is seemingly the most recent version of the language
60-
const language = (await registration()).default.at(-1);
61-
const shikiLanguageName = shikiLanguage as keyof typeof bundledLanguages;
59+
if (!this.plugin.loadedSettings.customLanguageFolder) return;
6260

63-
if (language === undefined) {
64-
continue;
65-
}
66-
67-
for (const alias of [language.name, ...(language.aliases ?? [])]) {
68-
if (languageNameBlacklist.has(alias)) {
69-
continue;
70-
}
61+
const languageFolder = normalizePath(this.plugin.loadedSettings.customLanguageFolder);
62+
if (!(await this.plugin.app.vault.adapter.exists(languageFolder))) {
63+
new Notice(`${this.plugin.manifest.name}\nUnable to open custom languages folder: ${languageFolder}`, 5000);
64+
return;
65+
}
7166

72-
if (!this.loadedLanguages.has(alias)) {
73-
const newLanguage = new LoadedLanguage(alias);
74-
newLanguage.addLanguage(shikiLanguageName);
67+
const languageList = await this.plugin.app.vault.adapter.list(languageFolder);
68+
const languageFiles = languageList.files.filter(f => f.toLowerCase().endsWith('.json'));
7569

76-
this.loadedLanguages.set(alias, newLanguage);
70+
for (const languageFile of languageFiles) {
71+
try {
72+
const language = JSON.parse(await this.plugin.app.vault.adapter.read(languageFile)) as LanguageRegistration;
73+
// validate that language file JSON can be parsed and contains at a minimum a scopeName
74+
if (!language.name) {
75+
throw Error('Invalid JSON language file is missing a name property.');
7776
}
7877

79-
this.loadedLanguages.get(alias)!.addLanguage(shikiLanguageName);
80-
}
81-
}
82-
83-
for (const [alias, language] of this.loadedLanguages) {
84-
if (language.languages.length === 1) {
85-
language.setDefaultLanguage(language.languages[0]);
86-
} else {
87-
const defaultLanguage = language.languages.find(lang => lang === alias);
88-
if (defaultLanguage !== undefined) {
89-
language.setDefaultLanguage(defaultLanguage);
90-
} else {
91-
console.warn(`No default language found for ${alias}, using the first language in the list`);
92-
language.setDefaultLanguage(language.languages[0]);
93-
}
78+
this.customLanguages.push(language);
79+
} catch (e) {
80+
new Notice(`${this.plugin.manifest.name}\nUnable to load custom language: ${languageFile}`, 5000);
81+
console.warn(`Unable to load custom language: ${languageFile}`, e);
9482
}
9583
}
96-
97-
for (const disabledLanguage of this.plugin.loadedSettings.disabledLanguages) {
98-
this.loadedLanguages.delete(disabledLanguage);
99-
}
10084
}
10185

10286
async loadCustomThemes(): Promise<void> {
@@ -150,7 +134,7 @@ export class CodeHighlighter {
150134
themes: [new ExpressiveCodeTheme(await this.themeMapper.getThemeForEC())],
151135
plugins: [
152136
pluginShiki({
153-
langs: Object.values(bundledLanguages),
137+
langs: this.getLoadedLanguageRegistrations(),
154138
}),
155139
pluginCollapsibleSections(),
156140
pluginTextMarkers(),
@@ -186,7 +170,7 @@ export class CodeHighlighter {
186170
async loadShiki(): Promise<void> {
187171
this.shiki = await createHighlighter({
188172
themes: [await this.themeMapper.getTheme()],
189-
langs: Object.keys(bundledLanguages),
173+
langs: this.getLoadedLanguageRegistrations(),
190174
});
191175
}
192176

@@ -206,4 +190,12 @@ export class CodeHighlighter {
206190

207191
container.innerHTML = toHtml(this.themeMapper.fixAST(result.renderedGroupAst));
208192
}
193+
194+
getLoadedLanguageRegistrations(): (DynamicImportLanguageRegistration | LanguageRegistration)[] {
195+
return [...Object.values(bundledLanguages), ...this.customLanguages];
196+
}
197+
198+
obsidianSafeLanguageNames(): string[] {
199+
return this.loadedLanguages.filter(lang => !languageNameBlacklist.has(lang));
200+
}
209201
}

src/LoadedLanguage.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/main.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { loadPrism, Plugin, TFile, type MarkdownPostProcessor } from 'obsidian';
2-
import { type ThemedToken, type TokensResult } from 'shiki';
2+
import { type BundledLanguage, type ThemedToken, type TokensResult } from 'shiki';
33
import { CodeBlock } from 'src/CodeBlock';
44
import { createCm6Plugin } from 'src/codemirror/Cm6_ViewPlugin';
55
import { DEFAULT_SETTINGS, type Settings } from 'src/settings/Settings';
@@ -80,27 +80,27 @@ export default class ShikiPlugin extends Plugin {
8080
}
8181

8282
registerCodeBlockProcessors(): void {
83-
for (const [alias, language] of this.highlighter.loadedLanguages) {
83+
for (const language of this.highlighter.obsidianSafeLanguageNames()) {
8484
try {
8585
this.registerMarkdownCodeBlockProcessor(
86-
alias,
86+
language,
8787
async (source, el, ctx) => {
8888
// this is so that we leave the hidden frontmatter code block in reading mode alone
89-
if (alias === 'yaml' && ctx.frontmatter) {
89+
if (language === 'yaml' && ctx.frontmatter) {
9090
const sectionInfo = ctx.getSectionInfo(el);
9191
if (sectionInfo && sectionInfo.lineStart === 0) {
9292
el.addClass('shiki-hide-in-reading-mode');
9393
}
9494
}
9595

96-
const codeBlock = new CodeBlock(this, el, source, language.getDefaultLanguage(), alias, ctx);
96+
const codeBlock = new CodeBlock(this, el, source, language, ctx);
9797

9898
ctx.addChild(codeBlock);
9999
},
100100
-1,
101101
);
102102
} catch (e) {
103-
console.warn(`Failed to register code block processor for ${alias}`, e);
103+
console.warn(`Failed to register code block processor for ${language}`, e);
104104
}
105105
}
106106
}
@@ -154,14 +154,12 @@ export default class ShikiPlugin extends Plugin {
154154
}
155155

156156
getHighlightTokens(code: string, lang: string): TokensResult | undefined {
157-
const shikiLanguage = this.highlighter.loadedLanguages.get(lang);
158-
159-
if (shikiLanguage === undefined) {
157+
if (!this.highlighter.obsidianSafeLanguageNames().includes(lang)) {
160158
return undefined;
161159
}
162160

163161
return this.highlighter.shiki.codeToTokens(code, {
164-
lang: shikiLanguage.getDefaultLanguage(),
162+
lang: lang as BundledLanguage,
165163
theme: this.settings.theme,
166164
});
167165
}

src/settings/Settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface Settings {
22
disabledLanguages: string[];
33
customThemeFolder: string;
4+
customLanguageFolder: string;
45
theme: string;
56
preferThemeColors: boolean;
67
inlineHighlighting: boolean;
@@ -9,6 +10,7 @@ export interface Settings {
910
export const DEFAULT_SETTINGS: Settings = {
1011
disabledLanguages: [],
1112
customThemeFolder: '',
13+
customLanguageFolder: '',
1214
theme: 'obsidian-theme',
1315
preferThemeColors: true,
1416
inlineHighlighting: true,

0 commit comments

Comments
 (0)