Skip to content

perf: use Shiki shorthand #2026

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions packages/slidev/node/setups/shiki.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import type { MarkdownItShikiOptions } from '@shikijs/markdown-it'
import type { ShikiSetup } from '@slidev/types'
import type { Highlighter } from 'shiki'
import type { LanguageInput, ShorthandsBundle } from 'shiki/core'
import fs from 'node:fs/promises'
import { bundledLanguages, createHighlighter } from 'shiki'
import { red } from 'kolorist'
import { bundledLanguages, bundledThemes } from 'shiki/bundle/full'
import { createdBundledHighlighter, createSingletonShorthands } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
import { loadSetups } from './load'

let cachedRoots: string[] | undefined
let cachedShiki: {
shiki: Highlighter
shiki: ShorthandsBundle<string, string>
shikiOptions: MarkdownItShikiOptions
} | undefined

export default async function setupShiki(roots: string[]) {
// Here we use shallow equality because when server is restarted, the roots will be different object.
if (cachedRoots === roots)
return cachedShiki!
cachedShiki?.shiki.dispose()

const options = await loadSetups<ShikiSetup>(
roots,
Expand All @@ -29,6 +31,34 @@ export default async function setupShiki(roots: string[]) {
}],
)
const mergedOptions = Object.assign({}, ...options)
const mergedLanguages: Record<string, LanguageInput> = bundledLanguages
for (const option of options) {
const langs = option?.langs
if (Array.isArray(langs)) {
for (const lang of langs.flat()) {
if (typeof lang === 'function') {
console.error(red('[slidev] `langs` option in shiki setup cannot be array containing functions. Please use `{ name: loaderFunction }` format instead.'))
}
else if (typeof lang === 'string') {
// Shiki's built-in languages
// In Node environment, they can be loaded on demand
// So all languages are available
// The language names are only used in the browser environment
}
else {
mergedLanguages[lang.name] = lang
for (const alias of lang.aliases || [])
mergedLanguages[alias] = lang
}
}
}
else if (typeof option?.langs === 'object') {
Object.assign(mergedLanguages, option.langs)
}
else {
console.error(red('[slidev] Invalid langs option in shiki setup:'), langs)
}
}

if ('theme' in mergedOptions && 'themes' in mergedOptions)
delete mergedOptions.theme
Expand All @@ -50,11 +80,12 @@ export default async function setupShiki(roots: string[]) {
if (mergedOptions.themes)
mergedOptions.defaultColor = false

const shiki = await createHighlighter({
...mergedOptions,
langs: mergedOptions.langs ?? Object.keys(bundledLanguages),
themes: 'themes' in mergedOptions ? Object.values(mergedOptions.themes) : [mergedOptions.theme],
const createHighlighter = createdBundledHighlighter<string, string>({
langs: mergedLanguages,
themes: bundledThemes,
engine: createJavaScriptRegexEngine,
})
const shiki = createSingletonShorthands<string, string>(createHighlighter)

cachedRoots = roots
return cachedShiki = {
Expand Down
4 changes: 2 additions & 2 deletions packages/slidev/node/syntax/markdown-it/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ResolvedSlidevOptions } from '@slidev/types'
import type MagicString from 'magic-string'
import type MarkdownIt from 'markdown-it'
import type { MarkdownItAsync } from 'markdown-it-async'
import { taskLists as MarkdownItTaskList } from '@hedgedoc/markdown-it-plugins'
// @ts-expect-error missing types
import MarkdownItFootnote from 'markdown-it-footnote'
Expand All @@ -12,7 +12,7 @@ import MarkdownItLink from './markdown-it-link'
import MarkdownItShiki from './markdown-it-shiki'
import MarkdownItVDrag from './markdown-it-v-drag'

export async function useMarkdownItPlugins(md: MarkdownIt, options: ResolvedSlidevOptions, markdownTransformMap: Map<string, MagicString>) {
export async function useMarkdownItPlugins(md: MarkdownItAsync, options: ResolvedSlidevOptions, markdownTransformMap: Map<string, MagicString>) {
const { roots, data: { features, config } } = options

if (config.highlighter === 'shiki') {
Expand Down
4 changes: 2 additions & 2 deletions packages/slidev/node/syntax/markdown-it/markdown-it-shiki.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ResolvedSlidevOptions } from '@slidev/types'
import type { ShikiTransformer } from 'shiki'
import { isTruthy } from '@antfu/utils'
import { fromHighlighter } from '@shikijs/markdown-it/core'
import { fromAsyncCodeToHtml } from '@shikijs/markdown-it/async'
import { escapeVueInCode } from '../transform/utils'

export default async function MarkdownItShiki({ data: { config }, mode, utils }: ResolvedSlidevOptions) {
Expand All @@ -27,7 +27,7 @@ export default async function MarkdownItShiki({ data: { config }, mode, utils }:
} satisfies ShikiTransformer,
].filter(isTruthy) as ShikiTransformer[]

return fromHighlighter(utils.shiki, {
return fromAsyncCodeToHtml(utils.shiki.codeToHtml, {
...utils.shikiOptions,
transformers,
})
Expand Down
17 changes: 11 additions & 6 deletions packages/slidev/node/syntax/transform/magic-move.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { MarkdownTransformContext } from '@slidev/types'
import lz from 'lz-string'
import { codeToKeyedTokens } from 'shiki-magic-move/core'
import { toKeyedTokens } from 'shiki-magic-move/core'
import { reCodeBlock } from './code-wrapper'
import { normalizeRangeStr } from './utils'

Expand All @@ -14,6 +14,7 @@
* Transform magic-move code blocks
*/
export function transformMagicMove(ctx: MarkdownTransformContext) {
const { codeToTokens } = ctx.options.utils.shiki

Check failure on line 17 in packages/slidev/node/syntax/transform/magic-move.ts

View workflow job for this annotation

GitHub Actions / test (lts/*, ubuntu-latest)

test/transform-all.test.ts > transform-all

TypeError: Cannot destructure property 'codeToTokens' of 'ctx.options.utils.shiki' as it is undefined. ❯ transformMagicMove packages/slidev/node/syntax/transform/magic-move.ts:17:11 ❯ test/transform-all.test.ts:49:5

Check failure on line 17 in packages/slidev/node/syntax/transform/magic-move.ts

View workflow job for this annotation

GitHub Actions / test (lts/*, windows-latest)

test/transform-all.test.ts > transform-all

TypeError: Cannot destructure property 'codeToTokens' of 'ctx.options.utils.shiki' as it is undefined. ❯ transformMagicMove packages/slidev/node/syntax/transform/magic-move.ts:17:11 ❯ test/transform-all.test.ts:49:5

Check failure on line 17 in packages/slidev/node/syntax/transform/magic-move.ts

View workflow job for this annotation

GitHub Actions / test (lts/*, macos-latest)

test/transform-all.test.ts > transform-all

TypeError: Cannot destructure property 'codeToTokens' of 'ctx.options.utils.shiki' as it is undefined. ❯ transformMagicMove packages/slidev/node/syntax/transform/magic-move.ts:17:11 ❯ test/transform-all.test.ts:49:5
ctx.s.replace(
reMagicMoveBlock,
(full, options = '{}', _attrs = '', body: string) => {
Expand All @@ -25,13 +26,17 @@
const defaultLineNumbers = parseLineNumbersOption(options) ?? ctx.options.data.config.lineNumbers

const ranges = matches.map(i => normalizeRangeStr(i[2]))
const steps = matches.map((i) => {
const steps = Promise.all(matches.map(async (i) => {
const lang = i[1]
const lineNumbers = parseLineNumbersOption(i[3]) ?? defaultLineNumbers
return codeToKeyedTokens(ctx.options.utils.shiki, i[5].trimEnd(), {
const code = i[5].trimEnd()
const options = {
...ctx.options.utils.shikiOptions,
lang: i[1] as any,
}, lineNumbers)
})
lang,
}
const { tokens } = await codeToTokens(code, options)
return toKeyedTokens(code, tokens, JSON.stringify([lang, 'themes' in options ? options.themes : options.theme]), lineNumbers)
}))
const compressed = lz.compressToBase64(JSON.stringify(steps))
return `<ShikiMagicMove v-bind="${options}" steps-lz="${compressed}" :step-ranges='${JSON.stringify(ranges)}' />`
},
Expand Down
4 changes: 2 additions & 2 deletions packages/types/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MarkdownItShikiOptions } from '@shikijs/markdown-it/index.mjs'
import type { HighlighterGeneric } from 'shiki/types.mjs'
import type { ShorthandsBundle } from 'shiki/core'
import type { SlidevData } from './types'

export interface RootsInfo {
Expand Down Expand Up @@ -51,7 +51,7 @@ export interface ResolvedSlidevOptions extends RootsInfo, SlidevEntryOptions {
}

export interface ResolvedSlidevUtils {
shiki: HighlighterGeneric<any, any>
shiki: ShorthandsBundle<string, string>
shikiOptions: MarkdownItShikiOptions
indexHtml: string
define: Record<string, string>
Expand Down
5 changes: 2 additions & 3 deletions packages/types/src/setups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Awaitable } from '@antfu/utils'
import type { KatexOptions } from 'katex'
import type { MermaidConfig } from 'mermaid'
import type * as monaco from 'monaco-editor'
import type { BuiltinLanguage, BuiltinTheme, CodeOptionsMeta, CodeOptionsThemes, CodeToHastOptionsCommon, Highlighter, LanguageInput } from 'shiki'
import type { BuiltinLanguage, BuiltinTheme, CodeOptionsMeta, CodeOptionsThemes, CodeToHastOptionsCommon, LanguageInput, LanguageRegistration, MaybeArray } from 'shiki'
import type { VitePluginConfig as UnoCssConfig } from 'unocss/vite'
import type { App, ComputedRef, Ref } from 'vue'
import type { Router, RouteRecordRaw } from 'vue-router'
Expand Down Expand Up @@ -57,8 +57,7 @@ export type ShikiSetupReturn =
& CodeOptionsThemes<BuiltinTheme>
& CodeOptionsMeta
& {
setup: (highlighter: Highlighter) => Awaitable<void>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this function is never called. And I don't know how to support it because there's no longer a Highlighter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get the highlighter with:

  const shiki = createSingletonShorthands<string, string>(createHighlighter)
  const highlighter = await shiki.getSingletonHighlighter()

But I don't mind removing it if no one uses it

langs: (LanguageInput | BuiltinLanguage)[]
langs: (MaybeArray<LanguageRegistration> | BuiltinLanguage)[] | Record<string, LanguageInput>
}
>

Expand Down
Loading
Loading