diff --git a/frontend/src/components/ExamRunner.tsx b/frontend/src/components/ExamRunner.tsx index 87b3b47..9abd648 100644 --- a/frontend/src/components/ExamRunner.tsx +++ b/frontend/src/components/ExamRunner.tsx @@ -136,7 +136,7 @@ export default function ExamRunner({ exam, state, setState, onSubmit, onExit }: {q.prompt} - + {graded && result && q.type !== 'essay' && (
diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx index 647fda4..ea9c10f 100644 --- a/frontend/src/components/Landing.tsx +++ b/frontend/src/components/Landing.tsx @@ -18,7 +18,7 @@ const SAMPLES = [ { path: '/samples/mixed-demo.json', title: 'Mixed Demo — Every Question Type', meta: '4 questions · Untimed · Media', mark: '◇' }, ] -export default function Landing({ onLoadExam, history, onResume, onClearHistory, onReviewAttempt }: Props) { +export default function Landing({ onLoadExam, history, onResume, onClearHistory, onReviewAttempt }: Readonly) { const [err, setErr] = useState(null) const [dragOver, setDragOver] = useState(false) const fileRef = useRef(null) @@ -40,6 +40,7 @@ export default function Landing({ onLoadExam, history, onResume, onClearHistory, setErr(null) try { const r = await fetch(path) + if (!r.ok) throw new Error(`${r.status} ${r.statusText}`) const data = await r.json() as unknown const msg = validateExam(data) if (msg) throw new Error(msg) @@ -112,14 +113,14 @@ export default function Landing({ onLoadExam, history, onResume, onClearHistory,

Try a sample

{SAMPLES.map((s) => ( -
void loadSample(s.path)}> +
+ ))}
diff --git a/frontend/src/components/Results.tsx b/frontend/src/components/Results.tsx index fff0ec7..72038c4 100644 --- a/frontend/src/components/Results.tsx +++ b/frontend/src/components/Results.tsx @@ -113,7 +113,7 @@ export default function Results({ exam, state, onRestart, onExit, reviewOnly }: {g.q.prompt} - {}} locked graded /> + {}} locked graded /> {g.q.explanation && (
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, '$1') - .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(`${m}`) + 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)]) }