fix(ui): avoid caching incomplete code highlighting
Only cache markdown HTML after Shiki has the required fence languages loaded so virtualized assistant messages can re-render with syntax highlighting when remounted.
This commit is contained in:
@@ -123,7 +123,11 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
version: () => resolved().version,
|
version: () => resolved().version,
|
||||||
})
|
})
|
||||||
|
|
||||||
const commitCacheEntry = (snapshot: ReturnType<typeof resolved>, renderedHtml: string) => {
|
const commitCacheEntry = (
|
||||||
|
snapshot: ReturnType<typeof resolved>,
|
||||||
|
renderedHtml: string,
|
||||||
|
options?: { cache?: boolean },
|
||||||
|
) => {
|
||||||
const cacheEntry: RenderCache = {
|
const cacheEntry: RenderCache = {
|
||||||
text: snapshot.text,
|
text: snapshot.text,
|
||||||
html: renderedHtml,
|
html: renderedHtml,
|
||||||
@@ -131,7 +135,9 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||||
}
|
}
|
||||||
setHtml(renderedHtml)
|
setHtml(renderedHtml)
|
||||||
cacheHandle.set(cacheEntry)
|
if (options?.cache ?? true) {
|
||||||
|
cacheHandle.set(cacheEntry)
|
||||||
|
}
|
||||||
notifyRendered()
|
notifyRendered()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +148,10 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
suppressHighlight: !snapshot.highlightEnabled,
|
suppressHighlight: !snapshot.highlightEnabled,
|
||||||
escapeRawHtml: snapshot.escapeRawHtml,
|
escapeRawHtml: snapshot.escapeRawHtml,
|
||||||
})
|
})
|
||||||
|
const shouldCache = !snapshot.highlightEnabled || !markdown.hasPendingCodeHighlight(snapshot.text)
|
||||||
|
|
||||||
if (latestRequestKey === snapshot.requestKey) {
|
if (latestRequestKey === snapshot.requestKey) {
|
||||||
commitCacheEntry(snapshot, rendered)
|
commitCacheEntry(snapshot, rendered, { cache: shouldCache })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,14 +120,7 @@ function resolveLanguage(token: string): { canonical: string | null; raw: string
|
|||||||
return { canonical: null, raw: normalized }
|
return { canonical: null, raw: normalized }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureLanguages(content: string) {
|
function collectCodeFenceLanguages(content: string): string[] {
|
||||||
if (highlightSuppressed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
|
||||||
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
|
||||||
// to miss these and prevent languages from loading.
|
|
||||||
const foundLanguages = new Set<string>()
|
const foundLanguages = new Set<string>()
|
||||||
try {
|
try {
|
||||||
const tokens = marked.lexer(content) as any
|
const tokens = marked.lexer(content) as any
|
||||||
@@ -139,10 +132,44 @@ async function ensureLanguages(content: string) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
// If tokenization fails for any reason, skip language preloading.
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...foundLanguages]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPendingCodeHighlight(content: string): boolean {
|
||||||
|
const languages = collectCodeFenceLanguages(content)
|
||||||
|
for (const token of languages) {
|
||||||
|
const rawToken = normalizeLanguageToken(token)
|
||||||
|
if (!rawToken || rawToken === "text") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { canonical, raw } = resolveLanguage(token)
|
||||||
|
const langKey = canonical || raw
|
||||||
|
if (langKey === "text" || raw === "text") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!highlighter || !loadedLanguages.has(langKey)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureLanguages(content: string) {
|
||||||
|
if (highlightSuppressed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||||
|
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||||
|
// to miss these and prevent languages from loading.
|
||||||
|
const foundLanguages = collectCodeFenceLanguages(content)
|
||||||
|
|
||||||
// Queue language loading tasks
|
// Queue language loading tasks
|
||||||
for (const token of foundLanguages) {
|
for (const token of foundLanguages) {
|
||||||
const rawToken = normalizeLanguageToken(token)
|
const rawToken = normalizeLanguageToken(token)
|
||||||
|
|||||||
Reference in New Issue
Block a user