From 769a4c59eaf3eccdf4246884fecf3afd92e699d0 Mon Sep 17 00:00:00 2001 From: prempyla Date: Fri, 7 Nov 2025 18:35:22 +0530 Subject: [PATCH 1/2] fix(monaco): preserve Markdown font styles (bold, italic, strikethrough) (#1022) --- packages/monaco/src/index.ts | 80 +++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/packages/monaco/src/index.ts b/packages/monaco/src/index.ts index f1b29bb82..9153fee4e 100644 --- a/packages/monaco/src/index.ts +++ b/packages/monaco/src/index.ts @@ -1,7 +1,7 @@ import type { ShikiInternal, ThemeRegistrationResolved } from '@shikijs/types' import type monacoNs from 'monaco-editor-core' import type { MonacoLineToken } from './types' -import { EncodedTokenMetadata, INITIAL } from '@shikijs/vscode-textmate' +import { EncodedTokenMetadata, FontStyle, INITIAL } from '@shikijs/vscode-textmate' import { TokenizerState } from './tokenizer' import { normalizeColor } from './utils' @@ -77,6 +77,7 @@ export function shikiToMonaco( const colorMap: string[] = [] const colorToScopeMap = new Map() + const colorAndStyleToScopeMap = new Map() // Because Monaco does not have the API of reading the current theme, // We hijack it here to keep track of the current theme. @@ -91,7 +92,18 @@ export function shikiToMonaco( colorToScopeMap.clear() theme?.rules.forEach((rule) => { const c = normalizeColor(rule.foreground) - if (c && !colorToScopeMap.has(c)) + if (!c) + return + + const normalizedStyle = normalizeFontStyleString(rule.fontStyle) + + if (normalizedStyle) { + const key = makeColorAndStyleKey(c, normalizedStyle) + if (!colorAndStyleToScopeMap.has(key)) + colorAndStyleToScopeMap.set(key, rule.token) + } + + if (!colorToScopeMap.has(c)) colorToScopeMap.set(c, rule.token) }) _setTheme(themeName) @@ -100,7 +112,14 @@ export function shikiToMonaco( // Set the first theme as the default theme monaco.editor.setTheme(themeIds[0]) - function findScopeByColor(color: string): string | undefined { + function findScopeByColorAndStyle(color: string, fontStyle: FontStyle): string | undefined { + const normalizedStyle = normalizeFontStyleBits(fontStyle) + if (normalizedStyle) { + const key = makeColorAndStyleKey(color, normalizedStyle) + const scoped = colorAndStyleToScopeMap.get(key) + if (scoped) + return scoped + } return colorToScopeMap.get(color) } @@ -141,10 +160,11 @@ export function shikiToMonaco( const startIndex = result.tokens[2 * j] const metadata = result.tokens[2 * j + 1] const color = normalizeColor(colorMap[EncodedTokenMetadata.getForeground(metadata)] || '') + const fontStyle = EncodedTokenMetadata.getFontStyle(metadata) // Because Monaco only support one scope per token, - // we workaround this to use color to trace back the scope - const scope = findScopeByColor(color) || '' + // we workaround this to use color (and font style when available) to trace back the scope + const scope = color ? (findScopeByColorAndStyle(color, fontStyle) || '') : '' tokens.push({ startIndex, scopes: scope }) } @@ -154,3 +174,53 @@ export function shikiToMonaco( } } } + +function normalizeFontStyleBits(fontStyle: FontStyle): string { + if (fontStyle <= FontStyle.None) + return '' + + const styles: string[] = [] + + if (fontStyle & FontStyle.Italic) + styles.push('italic') + if (fontStyle & FontStyle.Bold) + styles.push('bold') + if (fontStyle & FontStyle.Underline) + styles.push('underline') + if (fontStyle & FontStyle.Strikethrough) + styles.push('strikethrough') + + return styles.join(' ') +} + +function normalizeFontStyleString(fontStyle?: string): string { + if (!fontStyle) + return '' + + const styles = new Set( + fontStyle + .split(/[\s,]+/) + .map(style => style.trim().toLowerCase()) + .filter(Boolean), + ) + + styles.delete('') + styles.delete('normal') + styles.delete('none') + + const ordered: string[] = [] + if (styles.has('italic')) + ordered.push('italic') + if (styles.has('bold')) + ordered.push('bold') + if (styles.has('underline')) + ordered.push('underline') + if (styles.has('strikethrough') || styles.has('line-through')) + ordered.push('strikethrough') + + return ordered.join(' ') +} + +function makeColorAndStyleKey(color: string, style: string): string { + return `${color}|${style}` +} From 4b9a1c77ea263d7b3d87e25bf665fc4a37e7e028 Mon Sep 17 00:00:00 2001 From: prempyla Date: Fri, 7 Nov 2025 21:51:43 +0530 Subject: [PATCH 2/2] chore(monaco): clarify fontStyle cleanup logic with comment --- packages/monaco/src/index.ts | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/monaco/src/index.ts b/packages/monaco/src/index.ts index 9153fee4e..77d4b9081 100644 --- a/packages/monaco/src/index.ts +++ b/packages/monaco/src/index.ts @@ -36,11 +36,12 @@ export function textmateThemeToMonacoTheme(theme: ThemeRegistrationResolved): Mo for (const s of scopes) { if (s && (foreground || background || fontStyle)) { + const normalizedFontStyle = normalizeFontStyleString(fontStyle) rules.push({ token: s, foreground: normalizeColor(foreground), background: normalizeColor(background), - fontStyle, + fontStyle: normalizedFontStyle, }) } } @@ -76,8 +77,7 @@ export function shikiToMonaco( } const colorMap: string[] = [] - const colorToScopeMap = new Map() - const colorAndStyleToScopeMap = new Map() + const colorStyleToScopeMap = new Map() // Because Monaco does not have the API of reading the current theme, // We hijack it here to keep track of the current theme. @@ -89,7 +89,7 @@ export function shikiToMonaco( ret.colorMap.forEach((color, i) => { colorMap[i] = color }) - colorToScopeMap.clear() + colorStyleToScopeMap.clear() theme?.rules.forEach((rule) => { const c = normalizeColor(rule.foreground) if (!c) @@ -97,14 +97,10 @@ export function shikiToMonaco( const normalizedStyle = normalizeFontStyleString(rule.fontStyle) - if (normalizedStyle) { - const key = makeColorAndStyleKey(c, normalizedStyle) - if (!colorAndStyleToScopeMap.has(key)) - colorAndStyleToScopeMap.set(key, rule.token) - } + const key = normalizedStyle ? `${c}|${normalizedStyle}` : c - if (!colorToScopeMap.has(c)) - colorToScopeMap.set(c, rule.token) + if (!colorStyleToScopeMap.has(key)) + colorStyleToScopeMap.set(key, rule.token) }) _setTheme(themeName) } @@ -115,12 +111,11 @@ export function shikiToMonaco( function findScopeByColorAndStyle(color: string, fontStyle: FontStyle): string | undefined { const normalizedStyle = normalizeFontStyleBits(fontStyle) if (normalizedStyle) { - const key = makeColorAndStyleKey(color, normalizedStyle) - const scoped = colorAndStyleToScopeMap.get(key) + const scoped = colorStyleToScopeMap.get(`${color}|${normalizedStyle}`) if (scoped) return scoped } - return colorToScopeMap.get(color) + return colorStyleToScopeMap.get(color) } // Do not attempt to tokenize if a line is too long @@ -203,7 +198,7 @@ function normalizeFontStyleString(fontStyle?: string): string { .map(style => style.trim().toLowerCase()) .filter(Boolean), ) - + // Remove default or empty style markers sometimes present in theme data styles.delete('') styles.delete('normal') styles.delete('none') @@ -220,7 +215,3 @@ function normalizeFontStyleString(fontStyle?: string): string { return ordered.join(' ') } - -function makeColorAndStyleKey(color: string, style: string): string { - return `${color}|${style}` -}