Skip to content

Commit 20e6c8b

Browse files
authoredFeb 15, 2025··
feat(shortcuts): guess embedded language and auto load in shortcuts (#932)
1 parent db6910f commit 20e6c8b

13 files changed

+198
-39
lines changed
 

‎packages/core/src/constructors/bundle-factory.ts

+31-20
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ export function createdBundledHighlighter<BundledLangs extends string, BundledTh
134134
loadTheme(...themes) {
135135
return core.loadTheme(...themes.map(resolveTheme))
136136
},
137+
getBundledLanguages() {
138+
return bundledLanguages
139+
},
140+
getBundledThemes() {
141+
return bundledThemes
142+
},
137143
}
138144
}
139145

@@ -224,53 +230,58 @@ export function makeSingletonHighlighter<L extends string, T extends string>(
224230
return getSingletonHighlighter
225231
}
226232

233+
export interface CreateSingletonShorthandsOptions<L extends string, T extends string> {
234+
/**
235+
* A custom function to guess embedded languages to be loaded.
236+
*/
237+
guessEmbeddedLanguages?: (code: string, lang: string | undefined, highlighter: HighlighterGeneric<L, T>) => Awaitable<string[] | undefined>
238+
}
239+
227240
export function createSingletonShorthands<L extends string, T extends string>(
228241
createHighlighter: CreateHighlighterFactory<L, T>,
242+
config?: CreateSingletonShorthandsOptions<L, T>,
229243
): ShorthandsBundle<L, T> {
230244
const getSingletonHighlighter = makeSingletonHighlighter(createHighlighter)
231245

246+
async function get(code: string, options: CodeToTokensOptions<L, T> | CodeToHastOptions<L, T>): Promise<HighlighterGeneric<L, T>> {
247+
const shiki = await getSingletonHighlighter({
248+
langs: [options.lang as L],
249+
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
250+
})
251+
const langs = await config?.guessEmbeddedLanguages?.(code, options.lang, shiki) as L[]
252+
if (langs) {
253+
await shiki.loadLanguage(...langs)
254+
}
255+
return shiki
256+
}
257+
232258
return {
233259
getSingletonHighlighter(options) {
234260
return getSingletonHighlighter(options)
235261
},
236262

237263
async codeToHtml(code, options) {
238-
const shiki = await getSingletonHighlighter({
239-
langs: [options.lang as L],
240-
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
241-
})
264+
const shiki = await get(code, options)
242265
return shiki.codeToHtml(code, options)
243266
},
244267

245268
async codeToHast(code, options) {
246-
const shiki = await getSingletonHighlighter({
247-
langs: [options.lang as L],
248-
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
249-
})
269+
const shiki = await get(code, options)
250270
return shiki.codeToHast(code, options)
251271
},
252272

253273
async codeToTokens(code, options) {
254-
const shiki = await getSingletonHighlighter({
255-
langs: [options.lang as L],
256-
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
257-
})
274+
const shiki = await get(code, options)
258275
return shiki.codeToTokens(code, options)
259276
},
260277

261278
async codeToTokensBase(code, options) {
262-
const shiki = await getSingletonHighlighter({
263-
langs: [options.lang as L],
264-
themes: [options.theme as T],
265-
})
279+
const shiki = await get(code, options)
266280
return shiki.codeToTokensBase(code, options)
267281
},
268282

269283
async codeToTokensWithThemes(code, options) {
270-
const shiki = await getSingletonHighlighter({
271-
langs: [options.lang as L],
272-
themes: Object.values(options.themes).filter(Boolean) as T[],
273-
})
284+
const shiki = await get(code, options)
274285
return shiki.codeToTokensWithThemes(code, options)
275286
},
276287

‎packages/core/src/constructors/highlighter.ts

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export async function createHighlighterCore(options: HighlighterCoreOptions<fals
2626
codeToTokens: (code, options) => codeToTokens(internal, code, options),
2727
codeToHast: (code, options) => codeToHast(internal, code, options),
2828
codeToHtml: (code, options) => codeToHtml(internal, code, options),
29+
getBundledLanguages: () => ({}),
30+
getBundledThemes: () => ({}),
2931
...internal,
3032
getInternalContext: () => internal,
3133
}
@@ -49,6 +51,8 @@ export function createHighlighterCoreSync(options: HighlighterCoreOptions<true>)
4951
codeToTokens: (code, options) => codeToTokens(internal, code, options),
5052
codeToHast: (code, options) => codeToHast(internal, code, options),
5153
codeToHtml: (code, options) => codeToHtml(internal, code, options),
54+
getBundledLanguages: () => ({}),
55+
getBundledThemes: () => ({}),
5256
...internal,
5357
getInternalContext: () => internal,
5458
}

‎packages/core/src/highlight/code-to-tokens-base.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ function _tokenizeWithTheme(
185185
let tokensWithScopesIndex
186186

187187
if (options.includeExplanation) {
188-
resultWithScopes = grammar.tokenizeLine(line, stateStack)
188+
resultWithScopes = grammar.tokenizeLine(line, stateStack, tokenizeTimeLimit)
189189
tokensWithScopes = resultWithScopes.tokens
190190
tokensWithScopesIndex = 0
191191
}

‎packages/core/src/textmate/registry.ts

-4
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export class Registry extends TextMateRegistry {
6767
this._textmateThemeCache.set(theme, textmateTheme)
6868
}
6969

70-
// @ts-expect-error Access private `_syncRegistry`, but should work in runtime
7170
this._syncRegistry.setTheme(textmateTheme)
7271
}
7372

@@ -100,7 +99,6 @@ export class Registry extends TextMateRegistry {
10099
unbalancedBracketSelectors: lang.unbalancedBracketSelectors || [],
101100
}
102101

103-
// @ts-expect-error Private members, set this to override the previous grammar (that can be a stub)
104102
this._syncRegistry._rawGrammars.set(lang.scopeName, lang)
105103
const g = this.loadGrammarWithConfiguration(lang.scopeName, 1, grammarConfig) as Grammar
106104
g.name = lang.name
@@ -119,9 +117,7 @@ export class Registry extends TextMateRegistry {
119117
this._resolvedGrammars.delete(e.name)
120118
// Reset cache
121119
this._loadedLanguagesCache = null
122-
// @ts-expect-error clear cache
123120
this._syncRegistry?._injectionGrammars?.delete(e.scopeName)
124-
// @ts-expect-error clear cache
125121
this._syncRegistry?._grammars?.delete(e.scopeName)
126122
this.loadLanguage(this._langMap.get(e.name)!)
127123
}

‎packages/shiki/src/bundle-full.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { BundledLanguage } from './langs-bundle-full'
44
import type { BundledTheme } from './themes'
55
import { createdBundledHighlighter, createSingletonShorthands, warnDeprecated } from './core'
66
import { createOnigurumaEngine } from './engine-oniguruma'
7+
import { guessEmbeddedLanguages } from './guess'
78
import { bundledLanguages } from './langs-bundle-full'
89
import { bundledThemes } from './themes'
910

@@ -46,6 +47,7 @@ export const {
4647
BundledTheme
4748
>(
4849
createHighlighter,
50+
{ guessEmbeddedLanguages },
4951
)
5052

5153
/**

‎packages/shiki/src/bundle-web.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { BundledLanguage } from './langs-bundle-web'
44
import type { BundledTheme } from './themes'
55
import { createdBundledHighlighter, createSingletonShorthands, warnDeprecated } from './core'
66
import { createOnigurumaEngine } from './engine-oniguruma'
7+
import { guessEmbeddedLanguages } from './guess'
78
import { bundledLanguages } from './langs-bundle-web'
89
import { bundledThemes } from './themes'
910

@@ -46,6 +47,7 @@ export const {
4647
BundledTheme
4748
>(
4849
createHighlighter,
50+
{ guessEmbeddedLanguages },
4951
)
5052

5153
/**

‎packages/shiki/src/guess.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { HighlighterGeneric } from '@shikijs/types'
2+
3+
export function guessEmbeddedLanguages(
4+
code: string,
5+
_lang: string | undefined,
6+
shiki: HighlighterGeneric<any, any>,
7+
): string[] {
8+
const langs = new Set<string>()
9+
// For HTML code blocks like Vue SFC
10+
for (const match of code.matchAll(/lang=["']([\w-]+)["']/g)) {
11+
langs.add(match[1])
12+
}
13+
// For markdown code blocks
14+
for (const match of code.matchAll(/(?:```|~~~)([\w-]+)/g)) {
15+
langs.add(match[1])
16+
}
17+
// For latex
18+
for (const match of code.matchAll(/\\begin\{([\w-]+)\}/g)) {
19+
langs.add(match[1])
20+
}
21+
22+
// Only include known languages
23+
const bundle = shiki.getBundledLanguages()
24+
return Array.from(langs)
25+
.filter(l => l && bundle[l])
26+
}

‎packages/shiki/test/out/shorthand-markdown1.html

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/shiki/test/out/shorthand-markdown2.html

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect, it } from 'vitest'
2+
import { codeToHtml, getSingletonHighlighter } from '../src'
3+
4+
const inputMarkdown1 = `
5+
This is a markdown file
6+
7+
\`\`\`js
8+
console.log("hello")
9+
\`\`\`
10+
11+
~~~pug
12+
div
13+
p hello
14+
~~~
15+
16+
Even those grammars in markdown are lazy loaded, \`codeToHtml\` shorthand should capture them and load automatically.
17+
`
18+
19+
const inputMarkdown2 = `
20+
Some other languages
21+
22+
\`\`\`js
23+
console.log("hello")
24+
\`\`\`
25+
26+
~~~python
27+
print("hello")
28+
~~~
29+
30+
\`\`\`html
31+
<div class="foo">bar</div>
32+
<style>
33+
.foo {
34+
color: red;
35+
}
36+
</style>
37+
\`\`\`
38+
`
39+
40+
it('codeToHtml', async () => {
41+
const highlighter = await getSingletonHighlighter()
42+
expect(highlighter.getLoadedLanguages())
43+
.toEqual([])
44+
45+
await expect(await codeToHtml(inputMarkdown1, { lang: 'markdown', theme: 'vitesse-light' }))
46+
.toMatchFileSnapshot(`out/shorthand-markdown1.html`)
47+
48+
expect.soft(highlighter.getLoadedLanguages())
49+
.toContain('javascript')
50+
expect.soft(highlighter.getLoadedLanguages())
51+
.toContain('pug')
52+
53+
await expect(await codeToHtml(inputMarkdown2, { lang: 'markdown', theme: 'vitesse-light' }))
54+
.toMatchFileSnapshot(`out/shorthand-markdown2.html`)
55+
56+
expect.soft(highlighter.getLoadedLanguages())
57+
.toContain('python')
58+
expect.soft(highlighter.getLoadedLanguages())
59+
.toContain('html')
60+
61+
expect.soft(highlighter.getLoadedLanguages())
62+
.toMatchInlineSnapshot(`
63+
[
64+
"javascript",
65+
"css",
66+
"html",
67+
"pug",
68+
"python",
69+
"markdown",
70+
"md",
71+
"js",
72+
"jade",
73+
"py",
74+
]
75+
`)
76+
})

‎packages/types/src/highlighter.ts

+8
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ export interface HighlighterGeneric<BundledLangKeys extends string, BundledTheme
143143
* @deprecated
144144
*/
145145
getInternalContext: () => ShikiInternal
146+
/**
147+
* Get bundled languages object
148+
*/
149+
getBundledLanguages: () => Record<BundledLangKeys, LanguageInput>
150+
/**
151+
* Get bundled themes object
152+
*/
153+
getBundledThemes: () => Record<BundledThemeKeys, ThemeInput>
146154
}
147155

148156
/**

‎pnpm-lock.yaml

+13-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pnpm-workspace.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ catalog:
1919
'@rollup/plugin-node-resolve': ^16.0.0
2020
'@rollup/plugin-replace': ^6.0.2
2121
'@rollup/plugin-terser': ^0.4.4
22-
'@shikijs/vscode-textmate': ^10.0.1
22+
'@shikijs/vscode-textmate': ^10.0.2
2323
'@types/fs-extra': ^11.0.4
2424
'@types/hast': ^3.0.4
2525
'@types/markdown-it': ^14.1.2

0 commit comments

Comments
 (0)
Please sign in to comment.