Explanation
diff --git a/frontend/src/utils/latex.ts b/frontend/src/utils/latex.ts
index 54d033c..b60d94e 100644
--- a/frontend/src/utils/latex.ts
+++ b/frontend/src/utils/latex.ts
@@ -43,10 +43,26 @@ export const renderLatex = (text: string): string => {
export const highlightCode = (code: string, _lang: string): string => {
// escapeHtml runs first — no raw user content reaches the span injection
const escaped = escapeHtml(code)
- return escaped
- .replace(/(\/\/[^\n]*|#[^\n]*)/g, '')
- .replace(/("[^&]*?"|'[^&]*?'|`[^`]*?`)/g, '
$1')
- .replace(/\b(function|return|if|else|const|let|var|for|while|import|export|from|class|new|this|def|print|in|not|and|or|True|False|None|null|undefined|true|false)\b/g, '
$1')
- .replace(/\b(\d+(?:\.\d+)?)\b/g, '
$1')
- .replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/g, '
$1')
+ const spans: string[] = []
+
+ // Extract comments and strings into placeholders before applying keyword/number/fn regexes
+ // so later passes don't corrupt previously-injected span tag attributes.
+ // is a Unicode private-use-area char that never appears in source code.
+ // Word chars around the index (e.g. _HL0_) prevent \b(\d+)\b from matching the index digits.
+ let result = escaped
+ .replaceAll(/(\/\/[^\n]*|#[^\n]*)/g, (m) => {
+ const i = spans.length
+ spans.push(``)
+ return `_HL${i}_`
+ })
+ .replaceAll(/("[^&]*?"|'[^&]*?'|`[^`]*?`)/g, (m) => {
+ const i = spans.length
+ spans.push(`
${m}`)
+ return `_HL${i}_`
+ })
+ .replaceAll(/\b(function|return|if|else|const|let|var|for|while|import|export|from|class|new|this|def|print|in|not|and|or|True|False|None|null|undefined|true|false)\b/g, '
$1')
+ .replaceAll(/\b(\d+(?:\.\d+)?)\b/g, '
$1')
+ .replaceAll(/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/g, '
$1')
+
+ return result.replaceAll(/_HL(\d+)_/g, (_, i) => spans[Number(i)])
}