Skip to content

Commit

Permalink
feat(shortcuts): guess embedded language and auto load in shortcuts (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Feb 15, 2025
1 parent db6910f commit 20e6c8b
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 39 deletions.
51 changes: 31 additions & 20 deletions packages/core/src/constructors/bundle-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ export function createdBundledHighlighter<BundledLangs extends string, BundledTh
loadTheme(...themes) {
return core.loadTheme(...themes.map(resolveTheme))
},
getBundledLanguages() {
return bundledLanguages
},
getBundledThemes() {
return bundledThemes
},
}
}

Expand Down Expand Up @@ -224,53 +230,58 @@ export function makeSingletonHighlighter<L extends string, T extends string>(
return getSingletonHighlighter
}

export interface CreateSingletonShorthandsOptions<L extends string, T extends string> {
/**
* A custom function to guess embedded languages to be loaded.
*/
guessEmbeddedLanguages?: (code: string, lang: string | undefined, highlighter: HighlighterGeneric<L, T>) => Awaitable<string[] | undefined>
}

export function createSingletonShorthands<L extends string, T extends string>(
createHighlighter: CreateHighlighterFactory<L, T>,
config?: CreateSingletonShorthandsOptions<L, T>,
): ShorthandsBundle<L, T> {
const getSingletonHighlighter = makeSingletonHighlighter(createHighlighter)

async function get(code: string, options: CodeToTokensOptions<L, T> | CodeToHastOptions<L, T>): Promise<HighlighterGeneric<L, T>> {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
})
const langs = await config?.guessEmbeddedLanguages?.(code, options.lang, shiki) as L[]
if (langs) {
await shiki.loadLanguage(...langs)
}
return shiki
}

return {
getSingletonHighlighter(options) {
return getSingletonHighlighter(options)
},

async codeToHtml(code, options) {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
})
const shiki = await get(code, options)
return shiki.codeToHtml(code, options)
},

async codeToHast(code, options) {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
})
const shiki = await get(code, options)
return shiki.codeToHast(code, options)
},

async codeToTokens(code, options) {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: ('theme' in options ? [options.theme] : Object.values(options.themes)) as T[],
})
const shiki = await get(code, options)
return shiki.codeToTokens(code, options)
},

async codeToTokensBase(code, options) {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: [options.theme as T],
})
const shiki = await get(code, options)
return shiki.codeToTokensBase(code, options)
},

async codeToTokensWithThemes(code, options) {
const shiki = await getSingletonHighlighter({
langs: [options.lang as L],
themes: Object.values(options.themes).filter(Boolean) as T[],
})
const shiki = await get(code, options)
return shiki.codeToTokensWithThemes(code, options)
},

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/constructors/highlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export async function createHighlighterCore(options: HighlighterCoreOptions<fals
codeToTokens: (code, options) => codeToTokens(internal, code, options),
codeToHast: (code, options) => codeToHast(internal, code, options),
codeToHtml: (code, options) => codeToHtml(internal, code, options),
getBundledLanguages: () => ({}),
getBundledThemes: () => ({}),
...internal,
getInternalContext: () => internal,
}
Expand All @@ -49,6 +51,8 @@ export function createHighlighterCoreSync(options: HighlighterCoreOptions<true>)
codeToTokens: (code, options) => codeToTokens(internal, code, options),
codeToHast: (code, options) => codeToHast(internal, code, options),
codeToHtml: (code, options) => codeToHtml(internal, code, options),
getBundledLanguages: () => ({}),
getBundledThemes: () => ({}),
...internal,
getInternalContext: () => internal,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/highlight/code-to-tokens-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function _tokenizeWithTheme(
let tokensWithScopesIndex

if (options.includeExplanation) {
resultWithScopes = grammar.tokenizeLine(line, stateStack)
resultWithScopes = grammar.tokenizeLine(line, stateStack, tokenizeTimeLimit)
tokensWithScopes = resultWithScopes.tokens
tokensWithScopesIndex = 0
}
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/textmate/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export class Registry extends TextMateRegistry {
this._textmateThemeCache.set(theme, textmateTheme)
}

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

Expand Down Expand Up @@ -100,7 +99,6 @@ export class Registry extends TextMateRegistry {
unbalancedBracketSelectors: lang.unbalancedBracketSelectors || [],
}

// @ts-expect-error Private members, set this to override the previous grammar (that can be a stub)
this._syncRegistry._rawGrammars.set(lang.scopeName, lang)
const g = this.loadGrammarWithConfiguration(lang.scopeName, 1, grammarConfig) as Grammar
g.name = lang.name
Expand All @@ -119,9 +117,7 @@ export class Registry extends TextMateRegistry {
this._resolvedGrammars.delete(e.name)
// Reset cache
this._loadedLanguagesCache = null
// @ts-expect-error clear cache
this._syncRegistry?._injectionGrammars?.delete(e.scopeName)
// @ts-expect-error clear cache
this._syncRegistry?._grammars?.delete(e.scopeName)
this.loadLanguage(this._langMap.get(e.name)!)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/shiki/src/bundle-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BundledLanguage } from './langs-bundle-full'
import type { BundledTheme } from './themes'
import { createdBundledHighlighter, createSingletonShorthands, warnDeprecated } from './core'
import { createOnigurumaEngine } from './engine-oniguruma'
import { guessEmbeddedLanguages } from './guess'
import { bundledLanguages } from './langs-bundle-full'
import { bundledThemes } from './themes'

Expand Down Expand Up @@ -46,6 +47,7 @@ export const {
BundledTheme
>(
createHighlighter,
{ guessEmbeddedLanguages },
)

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/shiki/src/bundle-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BundledLanguage } from './langs-bundle-web'
import type { BundledTheme } from './themes'
import { createdBundledHighlighter, createSingletonShorthands, warnDeprecated } from './core'
import { createOnigurumaEngine } from './engine-oniguruma'
import { guessEmbeddedLanguages } from './guess'
import { bundledLanguages } from './langs-bundle-web'
import { bundledThemes } from './themes'

Expand Down Expand Up @@ -46,6 +47,7 @@ export const {
BundledTheme
>(
createHighlighter,
{ guessEmbeddedLanguages },
)

/**
Expand Down
26 changes: 26 additions & 0 deletions packages/shiki/src/guess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { HighlighterGeneric } from '@shikijs/types'

export function guessEmbeddedLanguages(
code: string,
_lang: string | undefined,
shiki: HighlighterGeneric<any, any>,
): string[] {
const langs = new Set<string>()
// For HTML code blocks like Vue SFC
for (const match of code.matchAll(/lang=["']([\w-]+)["']/g)) {
langs.add(match[1])
}
// For markdown code blocks
for (const match of code.matchAll(/(?:```|~~~)([\w-]+)/g)) {
langs.add(match[1])
}
// For latex
for (const match of code.matchAll(/\\begin\{([\w-]+)\}/g)) {
langs.add(match[1])
}

// Only include known languages
const bundle = shiki.getBundledLanguages()
return Array.from(langs)
.filter(l => l && bundle[l])
}
14 changes: 14 additions & 0 deletions packages/shiki/test/out/shorthand-markdown1.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions packages/shiki/test/out/shorthand-markdown2.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions packages/shiki/test/shorthands-markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect, it } from 'vitest'
import { codeToHtml, getSingletonHighlighter } from '../src'

const inputMarkdown1 = `
This is a markdown file
\`\`\`js
console.log("hello")
\`\`\`
~~~pug
div
p hello
~~~
Even those grammars in markdown are lazy loaded, \`codeToHtml\` shorthand should capture them and load automatically.
`

const inputMarkdown2 = `
Some other languages
\`\`\`js
console.log("hello")
\`\`\`
~~~python
print("hello")
~~~
\`\`\`html
<div class="foo">bar</div>
<style>
.foo {
color: red;
}
</style>
\`\`\`
`

it('codeToHtml', async () => {
const highlighter = await getSingletonHighlighter()
expect(highlighter.getLoadedLanguages())
.toEqual([])

await expect(await codeToHtml(inputMarkdown1, { lang: 'markdown', theme: 'vitesse-light' }))
.toMatchFileSnapshot(`out/shorthand-markdown1.html`)

expect.soft(highlighter.getLoadedLanguages())
.toContain('javascript')
expect.soft(highlighter.getLoadedLanguages())
.toContain('pug')

await expect(await codeToHtml(inputMarkdown2, { lang: 'markdown', theme: 'vitesse-light' }))
.toMatchFileSnapshot(`out/shorthand-markdown2.html`)

expect.soft(highlighter.getLoadedLanguages())
.toContain('python')
expect.soft(highlighter.getLoadedLanguages())
.toContain('html')

expect.soft(highlighter.getLoadedLanguages())
.toMatchInlineSnapshot(`
[
"javascript",
"css",
"html",
"pug",
"python",
"markdown",
"md",
"js",
"jade",
"py",
]
`)
})
8 changes: 8 additions & 0 deletions packages/types/src/highlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ export interface HighlighterGeneric<BundledLangKeys extends string, BundledTheme
* @deprecated
*/
getInternalContext: () => ShikiInternal
/**
* Get bundled languages object
*/
getBundledLanguages: () => Record<BundledLangKeys, LanguageInput>
/**
* Get bundled themes object
*/
getBundledThemes: () => Record<BundledThemeKeys, ThemeInput>
}

/**
Expand Down
Loading

0 comments on commit 20e6c8b

Please sign in to comment.