11import { ExpressiveCodeEngine , ExpressiveCodeTheme } from '@expressive-code/core' ;
22import 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' ;
54import { ThemeMapper } from 'src/themes/ThemeMapper' ;
65import { pluginShiki } from '@expressive-code/plugin-shiki' ;
76import { 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}
0 commit comments