Skip to content

Commit 58ec5b5

Browse files
committed
chore: improve langAlias handling
1 parent ef112ae commit 58ec5b5

File tree

4 files changed

+153
-11
lines changed

4 files changed

+153
-11
lines changed

packages/@expressive-code/plugin-shiki/src/core.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ExpressiveCodeLine, type ExpressiveCodePlugin, ExpressiveCodeTheme, InlineStyleAnnotation } from '@expressive-code/core'
2-
import type { ThemedToken, ShikiTransformer, Awaitable, RegexEngine, ThemeInput, StringLiteralUnion } from 'shiki/core'
2+
import type { ThemedToken, ShikiTransformer, Awaitable, RegexEngine, ThemeInput } from 'shiki/core'
33
import { ensureLanguagesAreLoaded, ensureThemeIsLoaded, getCachedHighlighter, runHighlighterTask, type ShikiHighlighter } from './highlighter'
4-
import type { LanguageInput } from './languages'
4+
import type { LanguageInput, LanguageAlias } from './languages'
55
import { runPreprocessHook, runTokensHook, validateTransformers } from './transformers'
66

77
export interface PluginShikiCoreOptions<L extends string> {
@@ -12,11 +12,13 @@ export interface PluginShikiCoreOptions<L extends string> {
1212
* The values can either be bundled languages, or additional languages
1313
* defined in `langs`.
1414
*
15+
* Note that when referencing a language provided in `langs`, the value
16+
* must match the `name` property (the language ID) of the language object
17+
* that was provided in `langs`.
18+
*
1519
* @example { 'mjs': 'javascript' }
1620
*/
17-
// TODO: This should be simply L but for backwards compat, need to support string. This should be changed so that you cannot provide
18-
// a mapping to a language that does not exist in the bundle.
19-
langAlias?: Record<string, StringLiteralUnion<L>> | undefined
21+
langAlias?: LanguageAlias<L> | undefined
2022
/**
2123
* By default, the additional languages defined in `langs` are only available in
2224
* top-level code blocks contained directly in their parent Markdown or MDX document.
@@ -53,7 +55,15 @@ export interface PluginShikiCoreOptions<L extends string> {
5355

5456
export interface PluginShikiBundleOptions<L extends string, T extends string> extends PluginShikiCoreOptions<L> {
5557
/**
56-
* A list of languages from your `bundledLangs` that you want eagerly loaded.
58+
* A list of additional languages that should be available for syntax highlighting.
59+
*
60+
* You can pass any of the language input types supported by Shiki, e.g.:
61+
* - `import('./some-exported-grammar.mjs')`
62+
* - `async () => JSON.parse(await fs.readFile('some-json-grammar.json', 'utf-8'))`
63+
*
64+
* Any languages specified will be eagerly loaded.
65+
*
66+
* See the Shiki documentation for more information on [Loading Custom Languages](https://shiki.style/guide/load-lang).
5767
*/
5868
langs?: LanguageInput[] | undefined
5969
/**
@@ -83,7 +93,7 @@ export interface PluginShikiBundleOptions<L extends string, T extends string> ex
8393
bundledThemes: Record<T, ThemeInput>
8494
}
8595

86-
export interface PluginShikiWithHighlighterOptions<L extends string, T extends string> extends PluginShikiCoreOptions<L> {
96+
export interface PluginShikiWithHighlighterOptions<L extends string, T extends string> extends Omit<PluginShikiCoreOptions<L>, 'langAlias'> {
8797
/**
8898
* Allows full control over the highlighter used.
8999
*
@@ -129,13 +139,18 @@ enum FontStyle {
129139
}
130140

131141
export function pluginShikiBundle<L extends string, T extends string>(options: PluginShikiBundleOptions<L, T>): ExpressiveCodePlugin {
132-
return pluginShikiWithHighlighter({
142+
return createPlugin({
133143
...options,
134144
highlighter: () => getCachedHighlighter(options),
135145
})
136146
}
137147

138148
export function pluginShikiWithHighlighter<L extends string, T extends string>(options: PluginShikiWithHighlighterOptions<L, T>): ExpressiveCodePlugin {
149+
return createPlugin(options)
150+
}
151+
152+
type CreatePluginOptions<L extends string, T extends string> = PluginShikiWithHighlighterOptions<L, T> & { langAlias?: LanguageAlias<L> | undefined }
153+
function createPlugin<L extends string, T extends string>(options: CreatePluginOptions<L, T>): ExpressiveCodePlugin {
139154
const { langAlias = {}, highlighter: getHighlighter } = options
140155

141156
// Validate all configured transformers

packages/@expressive-code/plugin-shiki/src/highlighter.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { StyleVariant } from '@expressive-code/core'
22
import { ExpressiveCodeTheme, getStableObjectHash } from '@expressive-code/core'
33
import type { BundledLanguage, HighlighterGeneric, ThemeRegistration, LanguageInput as ShikiLanguageInput } from 'shiki'
44
import { createdBundledHighlighter, isSpecialLang } from 'shiki'
5-
import type { LanguageInput, LanguageRegistration, ShikiLanguageRegistration } from './languages'
5+
import type { LanguageInput, LanguageRegistration, ShikiLanguageRegistration, LanguageAlias } from './languages'
66
import { getNestedCodeBlockInjectionLangs } from './languages'
77
import type { PluginShikiBundleOptions, PluginShikiWithHighlighterOptions } from './core'
88

@@ -76,7 +76,11 @@ export async function ensureThemeIsLoaded<L extends string, T extends string>(hi
7676
}
7777

7878
export async function ensureLanguagesAreLoaded<L extends string, T extends string>(
79-
options: Omit<PluginShikiWithHighlighterOptions<L, T>, 'langs' | 'highlighter'> & { highlighter: ShikiHighlighter<L, T>; langs?: (LanguageInput | string)[] | undefined }
79+
options: Omit<PluginShikiWithHighlighterOptions<L, T>, 'langs' | 'highlighter' | 'langAlias'> & {
80+
highlighter: ShikiHighlighter<L, T>
81+
langs?: (LanguageInput | string)[] | undefined
82+
langAlias?: LanguageAlias<L> | undefined
83+
}
8084
) {
8185
const { highlighter, langs = [], langAlias = {}, injectLangsIntoNestedCodeBlocks } = options
8286
const failedLanguages = new Set<string>()

packages/@expressive-code/plugin-shiki/src/languages.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { LanguageRegistration as ShikiLanguageRegistration, MaybeGetter, MaybeArray } from 'shiki'
1+
import type { LanguageRegistration as ShikiLanguageRegistration, MaybeGetter, MaybeArray, StringLiteralUnion } from 'shiki'
22

33
// Extract or rebuild non-exported types from Shiki
44
type IShikiRawRepository = ShikiLanguageRegistration['repository']
@@ -38,6 +38,8 @@ export interface LanguageRegistration extends Omit<ShikiLanguageRegistration, 'r
3838

3939
export type LanguageInput = MaybeGetter<MaybeArray<LanguageRegistration>>
4040

41+
export type LanguageAlias<L extends string> = Record<string, StringLiteralUnion<L>>
42+
4143
export { ShikiLanguageRegistration }
4244

4345
/**

packages/@expressive-code/plugin-shiki/test/rendering.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,127 @@ describe('Language handling', { timeout: 5 * 1000 }, async () => {
513513
// Ensure that no warnings were logged
514514
expect(warnings.join('\n')).toEqual('')
515515
})
516+
test('Supports alias after adding custom languages', async ({ task: { name: testName } }) => {
517+
let colorAssertionExecuted = false
518+
const warnings: string[] = []
519+
520+
await renderAndOutputHtmlSnapshot({
521+
testName,
522+
testBaseDir: __dirname,
523+
fixtures: [
524+
{
525+
fixtureName: '',
526+
themes,
527+
code: astroTestCode,
528+
language: 'alias-for-added-test-language',
529+
meta: '',
530+
plugins: [
531+
pluginShiki({
532+
langs: [addedTestLanguage],
533+
langAlias: {
534+
'alias-for-added-test-language': 'added-test-language',
535+
},
536+
}),
537+
],
538+
engineOptions: {
539+
logger: {
540+
warn: (message) => warnings.push(message),
541+
},
542+
},
543+
blockValidationFn: ({ renderedGroupAst }) => {
544+
const html = toHtml(renderedGroupAst)
545+
546+
// Select the contents of all spans that do not have the default
547+
// dracula theme foreground color
548+
const spansWithColorAndContent = [...html.matchAll(/<span [^>]*?style="--0:([^"]*?)">(.*?)<\/span>/g)]
549+
const highlightedSpans = spansWithColorAndContent.filter((match) => match[1].toLowerCase() !== themes[0].fg.toLowerCase())
550+
const highlightedContents = highlightedSpans.map((match) => match[2])
551+
expect(highlightedContents).toEqual([
552+
// Keywords
553+
'import',
554+
'import',
555+
'import',
556+
'const',
557+
// Strings
558+
'"content-wrapper"',
559+
'"test"',
560+
'"large"',
561+
])
562+
563+
colorAssertionExecuted = true
564+
},
565+
},
566+
],
567+
})
568+
569+
expect(colorAssertionExecuted).toBe(true)
570+
571+
// Ensure that no warnings were logged
572+
expect(warnings.join('\n')).toEqual('')
573+
})
574+
test('Supports alias after adding custom languages with shiki bundle', async ({ task: { name: testName } }) => {
575+
let colorAssertionExecuted = false
576+
const warnings: string[] = []
577+
578+
await renderAndOutputHtmlSnapshot({
579+
testName,
580+
testBaseDir: __dirname,
581+
fixtures: [
582+
{
583+
fixtureName: '',
584+
themes,
585+
code: astroTestCode,
586+
language: 'alias-for-added-test-language',
587+
meta: '',
588+
plugins: [
589+
pluginShikiBundle<string, string>({
590+
langs: [addedTestLanguage],
591+
langAlias: {
592+
'alias-for-added-test-language': 'added-test-language',
593+
},
594+
engine: createJavaScriptRegexEngine,
595+
bundledLangs: {},
596+
bundledThemes: {
597+
// themes are loaded via test utils and passed directly in so no need to support any themes within the bundle
598+
},
599+
}),
600+
],
601+
engineOptions: {
602+
logger: {
603+
warn: (message) => warnings.push(message),
604+
},
605+
},
606+
blockValidationFn: ({ renderedGroupAst }) => {
607+
const html = toHtml(renderedGroupAst)
608+
609+
// Select the contents of all spans that do not have the default
610+
// dracula theme foreground color
611+
const spansWithColorAndContent = [...html.matchAll(/<span [^>]*?style="--0:([^"]*?)">(.*?)<\/span>/g)]
612+
const highlightedSpans = spansWithColorAndContent.filter((match) => match[1].toLowerCase() !== themes[0].fg.toLowerCase())
613+
const highlightedContents = highlightedSpans.map((match) => match[2])
614+
expect(highlightedContents).toEqual([
615+
// Keywords
616+
'import',
617+
'import',
618+
'import',
619+
'const',
620+
// Strings
621+
'"content-wrapper"',
622+
'"test"',
623+
'"large"',
624+
])
625+
626+
colorAssertionExecuted = true
627+
},
628+
},
629+
],
630+
})
631+
632+
expect(colorAssertionExecuted).toBe(true)
633+
634+
// Ensure that no warnings were logged
635+
expect(warnings.join('\n')).toEqual('')
636+
})
516637
test('Allows overriding bundled languages', async ({ task: { name: testName } }) => {
517638
let colorAssertionExecuted = false
518639
const warnings: string[] = []

0 commit comments

Comments
 (0)