From 8420e6cae057c7eed0c37a34d9a2fd591c9fcdcd Mon Sep 17 00:00:00 2001 From: LaurensBartelds Date: Tue, 21 Apr 2026 22:18:11 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20final=20review=20fixes=20=E2=80=94=20key?= =?UTF-8?q?=20props,=20fetch=20guard,=20highlightCode=20placeholder=20stra?= =?UTF-8?q?tegy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add key={q.id} to QuestionRenderer in ExamRunner and Results to reset component state (Match activeLeft, OrderQuestion init) between questions - Add r.ok check in Landing.loadSample to surface HTTP errors before JSON parse - Add validateExam call in loadSample (was missing — could pass malformed JSON) - Rewrite highlightCode with placeholder strategy: extract comment/string spans before applying keyword/number/fn regexes so later passes don't corrupt previously-injected span attributes; use PUA char + word-wrapped index to avoid SonarLint S6324 and prevent digit regex matching index numbers - Change Landing dropzone from div[role=button] to native button (S6819) - Change sample cards from div[onClick] to button elements (S6848/S1082) - Wrap Landing Props in Readonly<> (S6759) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/ExamRunner.tsx | 2 +- frontend/src/components/Landing.tsx | 7 ++++--- frontend/src/components/Results.tsx | 2 +- frontend/src/utils/latex.ts | 28 ++++++++++++++++++++------ 4 files changed, 28 insertions(+), 11 deletions(-) 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)]) }