diff --git a/docs/src/lib/components/code/index.ts b/docs/src/lib/components/code/index.ts index f0bb2c5e..ac65a134 100644 --- a/docs/src/lib/components/code/index.ts +++ b/docs/src/lib/components/code/index.ts @@ -29,6 +29,8 @@ const carta = new Carta({ } }); +export const highlighter = await carta.highlighter(); + /** * Highlights the code blocks. * @param codeBlocks The code blocks to highlight @@ -39,22 +41,23 @@ export async function highlightCodeBlocks( ): Promise> { const highlightedCodeBlocks: HighlightedCodeBlocks = {} as HighlightedCodeBlocks; - const highlighter = await carta.highlighter(); if (!highlighter) { throw new Error('Failed to get highlighter'); } - const loadedLanguages = highlighter.getLoadedLanguages(); + const shiki = highlighter.shikiHighlighter(); + + const loadedLanguages = shiki.getLoadedLanguages(); for (const key in codeBlocks) { const codeBlock = codeBlocks[key]; if (isBundleLanguage(codeBlock.lang) && !loadedLanguages.includes(codeBlock.lang)) { - await highlighter.loadLanguage(codeBlock.lang); + await shiki.loadLanguage(codeBlock.lang); loadedLanguages.push(codeBlock.lang); } - const html = highlighter.codeToHtml(codeBlock.code, { + const html = shiki.codeToHtml(codeBlock.code, { lang: codeBlock.lang, theme: 'houston' }); diff --git a/docs/src/routes/api/utilities/+page.server.ts b/docs/src/routes/api/utilities/+page.server.ts index 33eadde8..59617b0b 100644 --- a/docs/src/routes/api/utilities/+page.server.ts +++ b/docs/src/routes/api/utilities/+page.server.ts @@ -35,7 +35,9 @@ const codeBlocks = { lang: 'ts', code: deindent` const highlighter = await carta.highlighter(); - const userTheme = carta.theme;` + const shiki = highlighter.shikiHighlighter(); + const html = await shiki.codeToHtml('console.log('Hello World!')', { lang: 'js' }); + ` }, isBundleLanguage: { lang: 'ts', diff --git a/docs/src/routes/api/utilities/+page.svelte b/docs/src/routes/api/utilities/+page.svelte index 9e20bfb5..7e59d68b 100644 --- a/docs/src/routes/api/utilities/+page.svelte +++ b/docs/src/routes/api/utilities/+page.svelte @@ -46,7 +46,7 @@ Carta.highlighter -

Get the Shiki highlighter.

+

Get the highlighter used by Carta.

diff --git a/docs/src/routes/plugins/math/+page.server.ts b/docs/src/routes/plugins/math/+page.server.ts index 32800275..3b7e9469 100644 --- a/docs/src/routes/plugins/math/+page.server.ts +++ b/docs/src/routes/plugins/math/+page.server.ts @@ -1,4 +1,4 @@ -import { highlightCodeBlocks } from '$lib/components/code'; +import { highlightCodeBlocks, highlighter } from '$lib/components/code'; import { deindent } from '$lib/utils'; const codeBlocks = { @@ -39,11 +39,11 @@ const codeBlocks = { ` }, usageInline: { - lang: 'cartamd', + lang: highlighter?.settings.langHash as string, code: 'Pythagorean theorem: $a^2+b^2=c^2$' }, usageBlock: { - lang: 'cartamd', + lang: highlighter?.settings.langHash as string, code: deindent` **Laplace** transform: $$ diff --git a/packages/carta-md/src/lib/internal/carta.ts b/packages/carta-md/src/lib/internal/carta.ts index 2f5bf015..76ae7d0a 100644 --- a/packages/carta-md/src/lib/internal/carta.ts +++ b/packages/carta-md/src/lib/internal/carta.ts @@ -194,6 +194,12 @@ export interface Plugin { onLoad?: (data: { carta: Carta }) => void; } +const USE_HIGHLIGHTER = + BROWSER || + // Replaced at build time to tree-shake shiki on the server, if specified + typeof __ENABLE_CARTA_SSR_HIGHLIGHTER__ === 'undefined' || + __ENABLE_CARTA_SSR_HIGHLIGHTER__ === true; + export class Carta { public readonly sanitizer?: (html: string) => string; public readonly historyOptions?: TextAreaHistoryOptions; @@ -233,13 +239,7 @@ export class Carta { public async highlighter(): Promise { if (this.mHighlighter) return this.mHighlighter; - if ( - !BROWSER && - // Replaced at build time to tree-shake shiki on the server, if specified - typeof __ENABLE_CARTA_SSR_HIGHLIGHTER__ !== 'undefined' && - __ENABLE_CARTA_SSR_HIGHLIGHTER__ === false - ) - return; + if (!USE_HIGHLIGHTER) return; this.mHighlighter = (async () => { const hl = await import('./highlight'); @@ -434,12 +434,7 @@ export class Carta { * @returns Rendered html. */ public async render(markdown: string): Promise { - if ( - BROWSER || - // Replaced at build time to tree-shake shiki on the server, if specified - typeof __ENABLE_CARTA_SSR_HIGHLIGHTER__ === 'undefined' || - __ENABLE_CARTA_SSR_HIGHLIGHTER__ === true - ) { + if (USE_HIGHLIGHTER) { const hl = await import('./highlight'); const { loadNestedLanguages } = hl; diff --git a/packages/carta-md/src/lib/internal/components/Input.svelte b/packages/carta-md/src/lib/internal/components/Input.svelte index 277ad28e..d3c9527c 100644 --- a/packages/carta-md/src/lib/internal/components/Input.svelte +++ b/packages/carta-md/src/lib/internal/components/Input.svelte @@ -109,24 +109,8 @@ const highlight = async (text: string) => { const highlighter = await carta.highlighter(); if (!highlighter) return; - let html: string; - const hl = await import('$lib/internal/highlight'); - const { isSingleTheme } = hl; - - if (isSingleTheme(highlighter.theme)) { - // Single theme - html = highlighter.codeToHtml(text, { - lang: highlighter.lang, - theme: highlighter.theme - }); - } else { - // Dual theme - html = highlighter.codeToHtml(text, { - lang: highlighter.lang, - themes: highlighter.theme - }); - } + const html = highlighter.codeToHtml(text); if (carta.sanitizer) { highlighted = carta.sanitizer(html); diff --git a/packages/carta-md/src/lib/internal/highlight.ts b/packages/carta-md/src/lib/internal/highlight.ts index 9d487628..180a7c0b 100644 --- a/packages/carta-md/src/lib/internal/highlight.ts +++ b/packages/carta-md/src/lib/internal/highlight.ts @@ -1,18 +1,5 @@ -import { - getHighlighter, - type BundledTheme, - type ThemeInput, - type StringLiteralUnion, - type BundledLanguage, - type SpecialLanguage, - type LanguageInput, - type LanguageRegistration, - type HighlighterGeneric, - bundledLanguages, - bundledThemes, - type ThemeRegistration -} from 'shiki'; -import type { Intellisense } from './utils'; +import * as shiki from 'shiki'; +import type { Intellisense, MaybePromise } from './utils'; /** * Custom TextMate grammar rule for the highlighter. @@ -20,51 +7,34 @@ import type { Intellisense } from './utils'; export type GrammarRule = { name: string; type: 'block' | 'inline'; - definition: LanguageRegistration['repository'][string]; + definition: shiki.LanguageRegistration['repository'][string]; }; /** * Custom TextMate highlighting rule for the highlighter. */ export type HighlightingRule = { - light: NonNullable[number]; - dark: NonNullable[number]; + light: NonNullable[number]; + dark: NonNullable[number]; }; -/** - * Shiki options for the highlighter. - */ -export type ShikiOptions = { - themes?: Array>; - langs?: (LanguageInput | StringLiteralUnion | SpecialLanguage)[]; -}; - -type CustomMarkdownLangName = Awaited<(typeof import('./assets/markdown'))['default']['name']>; -type DefaultLightThemeName = Awaited<(typeof import('./assets/theme-light'))['default']['name']>; -type DefaultDarkThemeName = Awaited<(typeof import('./assets/theme-dark'))['default']['name']>; -export const customMarkdownLangName: CustomMarkdownLangName = 'cartamd'; -export const defaultLightThemeName: DefaultLightThemeName = 'carta-light'; -export const defaultDarkThemeName: DefaultDarkThemeName = 'carta-dark'; -export const loadDefaultTheme = async (): Promise<{ - light: ThemeRegistration; - dark: ThemeRegistration; -}> => ({ - light: structuredClone((await import('./assets/theme-light')).default), - dark: structuredClone((await import('./assets/theme-dark')).default) -}); - /** * Language for the highlighter. */ -export type Language = Intellisense; +export type Language = Intellisense; + /** * Theme name for the highlighter. */ -export type ThemeName = Intellisense; +export type ThemeName = Intellisense< + shiki.BundledTheme | DefaultLightThemeName | DefaultDarkThemeName +>; + /** * Theme for the highlighter. */ -export type Theme = ThemeName | ThemeRegistration; +export type Theme = ThemeName | shiki.ThemeRegistration; + /** * Dual theme for light and dark mode. */ @@ -73,158 +43,387 @@ export type DualTheme = { dark: Theme; }; +/** + * Shiki options for the highlighter. + */ +export type ShikiOptions = { + themes?: Array>; + langs?: ( + | shiki.LanguageInput + | shiki.StringLiteralUnion + | shiki.SpecialLanguage + )[]; +}; + /** * Options for the highlighter. */ export type HighlighterOptions = { + /** + * Custom grammar rules for the highlighter. + */ grammarRules: GrammarRule[]; + /** + * Custom highlighting rules for the highlighter. + */ highlightingRules: HighlightingRule[]; + /** + * Theme for the highlighter. + */ theme: Theme | DualTheme; + /** + * Additional options for shiki. + */ shiki?: ShikiOptions; }; +type CustomMarkdownLangName = Awaited<(typeof import('./assets/markdown'))['default']['name']>; +type DefaultLightThemeName = Awaited<(typeof import('./assets/theme-light'))['default']['name']>; +type DefaultDarkThemeName = Awaited<(typeof import('./assets/theme-dark'))['default']['name']>; + +export const customMarkdownLangName: CustomMarkdownLangName = 'cartamd'; +export const defaultLightThemeName: DefaultLightThemeName = 'carta-light'; +export const defaultDarkThemeName: DefaultDarkThemeName = 'carta-dark'; + /** - * Loads the highlighter instance, with custom rules and options. Uses Shiki under the hood. - * @param rules Custom rules for the highlighter, from plugins. - * @param options Custom options for the highlighter. - * @returns The highlighter instance. + * Load the return the default light and dark themes. + * @returns The default light and dark themes. */ -export async function loadHighlighter({ - grammarRules, - highlightingRules, - theme, - shiki -}: HighlighterOptions): Promise { - // Inject rules into the custom markdown language - const injectGrammarRules = ( - lang: Awaited<(typeof import('./assets/markdown'))['default']>, - rules: GrammarRule[] - ) => { - lang.repository = { - ...langDefinition.repository, - ...Object.fromEntries(rules.map(({ name, definition }) => [name, definition])) - }; - for (const rule of rules) { - if (rule.type === 'block') { - lang.repository.block.patterns.unshift({ include: `#${rule.name}` }); - } else { - lang.repository.inline.patterns.unshift({ include: `#${rule.name}` }); - } - } - }; +export async function loadDefaultTheme() { + return { + light: structuredClone((await import('./assets/theme-light')).default), + dark: structuredClone((await import('./assets/theme-dark')).default) + } satisfies DualTheme; +} + +/** + * Checks if a language is a bundled language. + * @param lang The language to check. + * @returns Whether the language is a bundled language. + */ +export const isBundleLanguage = (lang: string): lang is shiki.BundledLanguage => + Object.keys(shiki.bundledLanguages).includes(lang); + +/** + * Checks if a theme is a bundled theme. + * @param theme The theme to check. + * @returns Whether the theme is a bundled theme. + */ +export const isBundleTheme = (theme: string): theme is shiki.BundledTheme => + Object.keys(shiki.bundledThemes).includes(theme); - const injectHighlightRules = (theme: ThemeRegistration, rules: HighlightingRule[]) => { - if (theme.type === 'light') { - theme.tokenColors ||= []; - theme.tokenColors.unshift(...rules.map(({ light }) => light)); +/** + * Checks if a theme is a dual theme. + * @param theme The theme to check. + * @returns Whether the theme is a dual theme. + */ +export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme => + typeof theme == 'object' && 'light' in theme && 'dark' in theme; + +/** + * Checks if a theme is a single theme. + * @param theme The theme to check. + * @returns Whether the theme is a single theme. + */ +export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme); + +/** + * Checks if a theme is a theme registration. + * @param theme The theme to check. + * @returns Whether the theme is a theme registration. + */ +export const isThemeRegistration = (theme: Theme): theme is shiki.ThemeRegistration => + typeof theme == 'object'; + +/** + * Injects custom grammar rules into the language definition. + * @param lang The language definition to inject the rules into. + * @param rules The grammar rules to inject. + * @returns The language definition with the injected rules. + */ +export function injectGrammarRules( + lang: Awaited<(typeof import('./assets/markdown'))['default']>, + rules: GrammarRule[] +) { + lang.repository = { + ...lang.repository, + ...Object.fromEntries(rules.map(({ name, definition }) => [name, definition])) + }; + for (const rule of rules) { + if (rule.type === 'block') { + lang.repository.block.patterns.unshift({ include: `#${rule.name}` }); } else { - theme.tokenColors ||= []; - theme.tokenColors.unshift(...rules.map(({ dark }) => dark)); + lang.repository.inline.patterns.unshift({ include: `#${rule.name}` }); } + } +} + +/** + * Injects custom highlighting rules into the theme. + * @param theme The theme to inject the rules into. + * @param rules The highlighting rules to inject. + * @returns The theme with the injected rules. + */ +export function injectHighlightRules(theme: shiki.ThemeRegistration, rules: HighlightingRule[]) { + if (theme.type === 'light') { + theme.tokenColors ||= []; + theme.tokenColors.unshift(...rules.map(({ light }) => light)); + } else { + theme.tokenColors ||= []; + theme.tokenColors.unshift(...rules.map(({ dark }) => dark)); + } +} + +/** + * Highlighter instance for the highlighter manager. + */ +export type Highlighter = { + /** + * Shortcut for highlighting a code block using the highlighter specific language + * and theme. + * @param code The code to highlight. + * @returns The highlighted code. + */ + codeToHtml: (code: string) => string; + /** + * Gets the underlying shiki highlighter instance. + * @returns The underlying shiki highlighter instance. + */ + shikiHighlighter: () => ShikiHighlighter; + settings: { + /** + * The language hash assigned to this highlighter instance. + * It's the id of the language registered to shiki. + */ + langHash: string; + /** + * The theme hash assigned to this highlighter instance. + * It's the id of the theme registered to shiki. + */ + themeHash: + | string + | { + light: string; + dark: string; + }; + }; + /** + * Reexported utilities functions. + */ + utils: { + isBundleLanguage: typeof isBundleLanguage; + isBundleTheme: typeof isBundleTheme; + isDualTheme: typeof isDualTheme; + isSingleTheme: typeof isSingleTheme; + isThemeRegistration: typeof isThemeRegistration; }; +}; + +export type ShikiHighlighter = shiki.HighlighterGeneric; + +export type HighlighterManager = { + shikiHighlighter: ShikiHighlighter; + highlighters: Highlighter[]; +}; + +// Store a single highlighter manager, with multiple languages if necessary. +let manager: MaybePromise | null = null; + +async function getManager() { + if (manager !== null) return manager; + + // Immediately assign a promise to the manager variable + // to prevent multiple calls to getManager from creating multiple instances + // since shiki.getHighlighter is an async function. + manager = (async () => ({ + shikiHighlighter: await shiki.getHighlighter({ + langs: [], + themes: [] + }), + highlighters: [] + }))(); + + return manager; +} + +/** + * Simple hash function to generate a hash from a string. + */ +function simpleHash(str: string, seed = 0) { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +} + +function langAndGrammarHash(lang: string, rules: GrammarRule[]) { + const grammarHash = rules + .map(({ name }) => name) + .toSorted() + .join(''); + return `${lang}-${simpleHash(`${lang}${grammarHash}`)}`; +} + +function themeAndHighlightingHash(theme: ThemeName, rules: HighlightingRule[]) { + const highlightingHash = rules + .map((rule) => `${rule.light}${rule.dark}`) + .toSorted() + .join(''); + return `${theme}-${simpleHash(`${theme}${highlightingHash}`)}`; +} + +export async function loadHighlighter(options: HighlighterOptions): Promise { + const { grammarRules, highlightingRules, theme, shiki: shikiOptions } = options; + const manager = await getManager(); // Additional themes and languages provided by the user - const themes = shiki?.themes ?? []; - const langs = shiki?.langs ?? []; + const langs = shikiOptions?.langs ?? []; + const themes = shikiOptions?.themes ?? []; - const highlighter: HighlighterGeneric = await getHighlighter({ - themes, - langs - }); + const loadedLangs = manager.shikiHighlighter.getLoadedLanguages(); + const loadedThemes = manager.shikiHighlighter.getLoadedThemes(); + + const langsToLoad = langs.filter( + (lang) => typeof lang != 'string' || !loadedLangs.includes(lang) + ); + const themesToLoad = themes.filter( + (theme) => typeof theme != 'string' || !loadedThemes.includes(theme) + ); + + manager.shikiHighlighter.loadLanguage(...(langsToLoad as shiki.LanguageInput[])); + manager.shikiHighlighter.loadTheme(...(themesToLoad as shiki.ThemeInput[])); // Custom markdown language const langDefinition = (await import('./assets/markdown')).default; - injectGrammarRules(langDefinition, grammarRules); - await highlighter.loadLanguage(langDefinition); + const langHash = langAndGrammarHash(langDefinition.name, grammarRules); + + // Check if there is an existing highlighter with the same language and grammar hash + const langAlreadyLoaded = manager.highlighters.find( + (highlighter) => highlighter.settings.langHash === langHash + ); + + if (!langAlreadyLoaded) { + const langClone = structuredClone(langDefinition); + // Load the custom language + injectGrammarRules(langClone, grammarRules); + (langClone as shiki.LanguageRegistration).name = langHash; + await manager.shikiHighlighter.loadLanguage(langClone); + } - // Custom themes + let themeHash: string | { light: string; dark: string }; + + // Themes if (isSingleTheme(theme)) { - let registration: ThemeRegistration; + let themeRegistration: shiki.ThemeRegistration; if (isThemeRegistration(theme)) { - registration = theme; + themeRegistration = theme; } else { - registration = (await bundledThemes[theme as BundledTheme]()).default; + themeRegistration = (await shiki.bundledThemes[theme as shiki.BundledTheme]()).default; } - injectHighlightRules(registration, highlightingRules); + themeHash = themeAndHighlightingHash(themeRegistration.name ?? 'unknown', highlightingRules); + + const existingHighlighter = manager.highlighters.find( + (highlighter) => highlighter.settings.themeHash === themeHash + ); - await highlighter.loadTheme(registration); + if (!existingHighlighter) { + const langClone: shiki.ThemeRegistration = structuredClone(langDefinition); + injectHighlightRules(langClone, highlightingRules); + langClone.name = themeHash; + await manager.shikiHighlighter.loadTheme(langClone); + } } else { const { light, dark } = theme; - let lightRegistration: ThemeRegistration; - let darkRegistration: ThemeRegistration; + let lightRegistration: shiki.ThemeRegistration; + let darkRegistration: shiki.ThemeRegistration; if (isThemeRegistration(light)) { lightRegistration = light; } else { - lightRegistration = (await bundledThemes[light as BundledTheme]()).default; + lightRegistration = (await shiki.bundledThemes[light as shiki.BundledTheme]()).default; } if (isThemeRegistration(dark)) { darkRegistration = dark; } else { - darkRegistration = (await bundledThemes[dark as BundledTheme]()).default; + darkRegistration = (await shiki.bundledThemes[dark as shiki.BundledTheme]()).default; } - injectHighlightRules(lightRegistration, highlightingRules); - injectHighlightRules(darkRegistration, highlightingRules); + const lightHash = themeAndHighlightingHash( + lightRegistration.name ?? 'unknown', + highlightingRules + ); + const darkHash = themeAndHighlightingHash( + darkRegistration.name ?? 'unknown', + highlightingRules + ); + + themeHash = { light: lightHash, dark: darkHash }; + + const existingHighlighter = manager.highlighters.find( + (highlighter) => + highlighter.settings.themeHash === lightHash || highlighter.settings.themeHash === darkHash + ); - await highlighter.loadTheme(lightRegistration); - await highlighter.loadTheme(darkRegistration); + if (!existingHighlighter) { + const lightClone: shiki.ThemeRegistration = structuredClone(lightRegistration); + const darkClone: shiki.ThemeRegistration = structuredClone(darkRegistration); + injectHighlightRules(lightClone, highlightingRules); + injectHighlightRules(darkClone, highlightingRules); + lightClone.name = lightHash; + darkClone.name = darkHash; + await manager.shikiHighlighter.loadTheme(lightClone); + await manager.shikiHighlighter.loadTheme(darkClone); + } } - return { - theme, - lang: customMarkdownLangName, - ...highlighter + const highlighter: Highlighter = { + codeToHtml: (code) => { + if (isSingleTheme(theme)) { + // Single theme + return manager.shikiHighlighter.codeToHtml(code, { + lang: langHash, + theme: themeHash as string + }); + } else { + // Dual theme + return manager.shikiHighlighter.codeToHtml(code, { + lang: langHash, + themes: { + light: (themeHash as { light: string; dark: string }).light as string, + dark: (themeHash as { light: string; dark: string }).dark as string + } + }); + } + }, + shikiHighlighter: () => manager.shikiHighlighter, + settings: { + langHash, + themeHash + }, + utils: { + isBundleLanguage, + isBundleTheme, + isDualTheme, + isSingleTheme, + isThemeRegistration + } }; -} -export interface Highlighter extends HighlighterGeneric { - /** - * The language specified for the highlighter. - */ - theme: Theme | DualTheme; - /** - * The theme specified for the highlighter. - */ - lang: Language; -} -/** - * Checks if a language is a bundled language. - * @param lang The language to check. - * @returns Whether the language is a bundled language. - */ -export const isBundleLanguage = (lang: string): lang is BundledLanguage => - Object.keys(bundledLanguages).includes(lang); -/** - * Checks if a theme is a bundled theme. - * @param theme The theme to check. - * @returns Whether the theme is a bundled theme. - */ -export const isBundleTheme = (theme: string): theme is BundledTheme => - Object.keys(bundledThemes).includes(theme); -/** - * Checks if a theme is a dual theme. - * @param theme The theme to check. - * @returns Whether the theme is a dual theme. - */ -export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme => - typeof theme == 'object' && 'light' in theme && 'dark' in theme; -/** - * Checks if a theme is a single theme. - * @param theme The theme to check. - * @returns Whether the theme is a single theme. - */ -export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme); -/** - * Checks if a theme is a theme registration. - * @param theme The theme to check. - * @returns Whether the theme is a theme registration. - */ -export const isThemeRegistration = (theme: Theme): theme is ThemeRegistration => - typeof theme == 'object'; + manager.highlighters.push(highlighter); + return highlighter; +} /** * Find all nested languages in the markdown text and load them into the highlighter. @@ -252,11 +451,11 @@ export const loadNestedLanguages = async (highlighter: Highlighter, text: string text = text.replaceAll('\r\n', '\n'); // Normalize line endings const languages = findNestedLanguages(text); - const loadedLanguages = highlighter.getLoadedLanguages(); + const loadedLanguages = highlighter.shikiHighlighter().getLoadedLanguages(); let updated = false; for (const lang of languages) { if (isBundleLanguage(lang) && !loadedLanguages.includes(lang)) { - await highlighter.loadLanguage(lang); + await highlighter.shikiHighlighter().loadLanguage(lang); loadedLanguages.push(lang); updated = true; } diff --git a/packages/carta-md/src/lib/internal/utils.ts b/packages/carta-md/src/lib/internal/utils.ts index ee953c4c..065496a5 100644 --- a/packages/carta-md/src/lib/internal/utils.ts +++ b/packages/carta-md/src/lib/internal/utils.ts @@ -5,6 +5,7 @@ type Union = T | (U & Nothing); export type Intellisense = Union; export type MaybeArray = T | Array; +export type MaybePromise = T | Promise; export type NonNullable = Exclude; /** diff --git a/packages/plugin-code/package.json b/packages/plugin-code/package.json index b39687f6..dbabf5e6 100644 --- a/packages/plugin-code/package.json +++ b/packages/plugin-code/package.json @@ -23,7 +23,7 @@ "typescript-cp": "^0.1.8" }, "peerDependencies": { - "carta-md": "^4.0.0" + "carta-md": "^4.9.0" }, "files": [ "dist" diff --git a/packages/plugin-code/src/index.ts b/packages/plugin-code/src/index.ts index 23a473fb..8feaa0f5 100644 --- a/packages/plugin-code/src/index.ts +++ b/packages/plugin-code/src/index.ts @@ -6,24 +6,6 @@ export type CodeExtensionOptions = Omit theme?: Theme | DualTheme; }; -// FIXME: find a better solution then copy-pasting these functions in next version. -// However, when importing from carta-md, this causes a MODULE_NOT_FOUND error -// for some reason. -/** - * Checks if a theme is a dual theme. - * @param theme The theme to check. - * @returns Whether the theme is a dual theme. - */ -export const isDualTheme = (theme: Theme | DualTheme): theme is DualTheme => - typeof theme == 'object' && 'light' in theme && 'dark' in theme; - -/** - * Checks if a theme is a single theme. - * @param theme The theme to check. - * @returns Whether the theme is a single theme. - */ -export const isSingleTheme = (theme: Theme | DualTheme): theme is Theme => !isDualTheme(theme); - /** * Carta code highlighting plugin. Themes available on [GitHub](https://github.com/speed-highlight/core/tree/main/dist/themes). */ @@ -38,14 +20,19 @@ export const code = (options?: CodeExtensionOptions): Plugin => { const highlighter = await carta.highlighter(); if (highlighter) { + const shikiHighlighter = highlighter.shikiHighlighter(); + if (!theme) { - theme = highlighter.theme; // Use the theme specified in the highlighter + theme = highlighter.settings.themeHash; // Use the theme specified in the highlighter } - if (isSingleTheme(theme)) { - processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, theme }); + if (highlighter.utils.isSingleTheme(theme)) { + processor.use(rehypeShikiFromHighlighter, shikiHighlighter, { ...options, theme }); } else { - processor.use(rehypeShikiFromHighlighter, highlighter, { ...options, themes: theme }); + processor.use(rehypeShikiFromHighlighter, shikiHighlighter, { + ...options, + themes: theme + }); } } } diff --git a/packages/plugin-math/package.json b/packages/plugin-math/package.json index 77451134..f0da2d75 100644 --- a/packages/plugin-math/package.json +++ b/packages/plugin-math/package.json @@ -30,7 +30,7 @@ "!dist/**/*.spec.*" ], "peerDependencies": { - "carta-md": "^4.0.0", + "carta-md": "^4.9.0", "svelte": "^5.0.0" }, "devDependencies": { diff --git a/packages/plugin-math/src/lib/index.ts b/packages/plugin-math/src/lib/index.ts index e2891a6a..bea93128 100644 --- a/packages/plugin-math/src/lib/index.ts +++ b/packages/plugin-math/src/lib/index.ts @@ -51,7 +51,7 @@ export const math = (options?: MathExtensionOptions): Plugin => { onLoad: async ({ carta }) => { const highlighter = await carta.highlighter(); if (!highlighter) return; - await highlighter.loadLanguage('tex'); + await highlighter.shikiHighlighter().loadLanguage('tex'); carta.input?.update(); }, transformers: [ diff --git a/packages/plugin-tikz/package.json b/packages/plugin-tikz/package.json index 06e372ea..54db49d2 100644 --- a/packages/plugin-tikz/package.json +++ b/packages/plugin-tikz/package.json @@ -28,7 +28,7 @@ "vite-raw-plugin": "^1.0.2" }, "peerDependencies": { - "carta-md": "^4.0.0", + "carta-md": "^4.9.0", "unified": "^11.0.0" }, "files": [ diff --git a/packages/plugin-tikz/src/index.ts b/packages/plugin-tikz/src/index.ts index f54c7e55..9b879d61 100644 --- a/packages/plugin-tikz/src/index.ts +++ b/packages/plugin-tikz/src/index.ts @@ -33,7 +33,7 @@ export const tikz = (options?: TikzExtensionOptions): Plugin => { onLoad: async ({ carta }) => { const highlighter = await carta.highlighter(); if (!highlighter) return; - await highlighter.loadLanguage('tex'); + await highlighter.shikiHighlighter().loadLanguage('tex'); carta.input?.update(); }, transformers: [ diff --git a/scripts/build.js b/scripts/build.js index 1bf20c58..283a8c32 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -17,4 +17,4 @@ for (const [index, target] of packages.entries()) { } } -spinner.succeed(`All packages and docs built`); +spinner.succeed(`All packages built`);