Skip to content

Commit e4dec23

Browse files
prempylaantfu
andauthored
fix(monaco): preserve Markdown font styles (#1107)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 1a7c8e8 commit e4dec23

File tree

1 file changed

+74
-12
lines changed

1 file changed

+74
-12
lines changed

packages/monaco/src/index.ts

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ShikiInternal, ThemeRegistrationResolved } from '@shikijs/types'
22
import type * as monacoNs from 'monaco-editor-core'
33
import type { MonacoLineToken } from './types'
4-
import { EncodedTokenMetadata, INITIAL } from '@shikijs/vscode-textmate'
4+
import { EncodedTokenMetadata, FontStyle, INITIAL } from '@shikijs/vscode-textmate'
55
import { TokenizerState } from './tokenizer'
66
import { normalizeColor } from './utils'
77

@@ -36,11 +36,15 @@ export function textmateThemeToMonacoTheme(theme: ThemeRegistrationResolved): Mo
3636
continue
3737
const scopes = Array.isArray(scope) ? scope : scope ? [scope] : []
3838

39+
const normalizedFontStyle = normalizeFontStyleString(fontStyle)
40+
const normalizedForeground = normalizeColor(foreground)
41+
const normalizedBackground = normalizeColor(background)
42+
3943
rules.push(...scopes.map(s => ({
4044
token: s,
41-
foreground: normalizeColor(foreground),
42-
background: normalizeColor(background),
43-
fontStyle,
45+
foreground: normalizedForeground,
46+
background: normalizedBackground,
47+
fontStyle: normalizedFontStyle,
4448
})))
4549
}
4650
}
@@ -74,7 +78,7 @@ export function shikiToMonaco(
7478
}
7579

7680
const colorMap: string[] = []
77-
const colorToScopeMap = new Map<string, string>()
81+
const colorStyleToScopeMap = new Map<string, string>()
7882

7983
// Because Monaco does not have the API of reading the current theme,
8084
// We hijack it here to keep track of the current theme.
@@ -86,20 +90,25 @@ export function shikiToMonaco(
8690
ret.colorMap.forEach((color, i) => {
8791
colorMap[i] = color
8892
})
89-
colorToScopeMap.clear()
93+
colorStyleToScopeMap.clear()
9094
theme?.rules.forEach((rule) => {
9195
const c = normalizeColor(rule.foreground)
92-
if (c && !colorToScopeMap.has(c))
93-
colorToScopeMap.set(c, rule.token)
96+
if (!c)
97+
return
98+
99+
const key = getColorStyleKey(c, normalizeFontStyleString(rule.fontStyle))
100+
if (!colorStyleToScopeMap.has(key))
101+
colorStyleToScopeMap.set(key, rule.token)
94102
})
95103
_setTheme(themeName)
96104
}
97105

98106
// Set the first theme as the default theme
99107
monaco.editor.setTheme(themeIds[0])
100108

101-
function findScopeByColor(color: string): string | undefined {
102-
return colorToScopeMap.get(color)
109+
function findScopeByColorAndStyle(color: string, fontStyle: FontStyle): string | undefined {
110+
const key = getColorStyleKey(color, normalizeFontStyleBits(fontStyle))
111+
return colorStyleToScopeMap.get(key)
103112
}
104113

105114
// Do not attempt to tokenize if a line is too long
@@ -139,10 +148,11 @@ export function shikiToMonaco(
139148
const startIndex = result.tokens[2 * j]
140149
const metadata = result.tokens[2 * j + 1]
141150
const color = normalizeColor(colorMap[EncodedTokenMetadata.getForeground(metadata)] || '')
151+
const fontStyle = EncodedTokenMetadata.getFontStyle(metadata)
142152

143153
// Because Monaco only support one scope per token,
144-
// we workaround this to use color to trace back the scope
145-
const scope = findScopeByColor(color) || ''
154+
// we workaround this to use color (and font style when available) to trace back the scope
155+
const scope = color ? (findScopeByColorAndStyle(color, fontStyle) || '') : ''
146156
tokens.push({ startIndex, scopes: scope })
147157
}
148158

@@ -152,3 +162,55 @@ export function shikiToMonaco(
152162
}
153163
}
154164
}
165+
166+
function normalizeFontStyleBits(fontStyle: FontStyle): string {
167+
if (fontStyle <= FontStyle.None)
168+
return ''
169+
170+
const styles: string[] = []
171+
172+
if (fontStyle & FontStyle.Italic)
173+
styles.push('italic')
174+
if (fontStyle & FontStyle.Bold)
175+
styles.push('bold')
176+
if (fontStyle & FontStyle.Underline)
177+
styles.push('underline')
178+
if (fontStyle & FontStyle.Strikethrough)
179+
styles.push('strikethrough')
180+
181+
return styles.join(' ')
182+
}
183+
184+
const VALID_FONT_STYLES = [
185+
'italic',
186+
'bold',
187+
'underline',
188+
'strikethrough',
189+
] as const
190+
191+
const VALID_FONT_ALIASES: Record<string, typeof VALID_FONT_STYLES[number]> = {
192+
'line-through': 'strikethrough',
193+
}
194+
195+
function normalizeFontStyleString(fontStyle?: string): string {
196+
if (!fontStyle)
197+
return ''
198+
199+
const styles = new Set(
200+
fontStyle
201+
.split(/[\s,]+/)
202+
.map(style => style.trim().toLowerCase())
203+
.map(style => VALID_FONT_ALIASES[style] || style)
204+
.filter(Boolean),
205+
)
206+
207+
return VALID_FONT_STYLES
208+
.filter(style => styles.has(style))
209+
.join(' ')
210+
}
211+
212+
function getColorStyleKey(color: string, fontStyle: string): string {
213+
if (!fontStyle)
214+
return color
215+
return `${color}|${fontStyle}`
216+
}

0 commit comments

Comments
 (0)