// ============================================================================ // CodeEditor — monospace IGCSE pseudocode editor with a highlight overlay, // line-number gutter, and synced scrolling. Lightweight; no dependencies. // ============================================================================ const IGCSE_KW = ['DECLARE','CONSTANT','INPUT','OUTPUT','IF','THEN','ELSE','ENDIF','CASE','OF','OTHERWISE','ENDCASE', 'FOR','TO','STEP','NEXT','WHILE','DO','ENDWHILE','REPEAT','UNTIL','PROCEDURE','ENDPROCEDURE','FUNCTION','RETURNS', 'RETURN','ENDFUNCTION','CALL','BYREF','BYVAL','OPENFILE','READFILE','WRITEFILE','CLOSEFILE','SEEK','GETRECORD', 'PUTRECORD','EOF','FOR','READ','WRITE','APPEND','AND','OR','NOT','MOD','DIV','ARRAY']; const IGCSE_TYPE = ['INTEGER','REAL','STRING','CHAR','BOOLEAN','DATE']; const IGCSE_FN = ['LENGTH','MID','LEFT','RIGHT','UCASE','LCASE','SUBSTRING','ROUND','RANDOM','INT','EOF']; const _esc = (s) => s.replace(/&/g, '&').replace(//g, '>'); function highlight(src) { const kw = new Set(IGCSE_KW), ty = new Set(IGCSE_TYPE), fn = new Set(IGCSE_FN); return src.split('\n').map((line) => { // comments const ci = line.indexOf('//'); let code = line, comment = ''; if (ci >= 0) { code = line.slice(0, ci); comment = line.slice(ci); } // tokenise on strings first let html = ''; const re = /("[^"]*"?|\b[0-9]+\b|[A-Za-z_][A-Za-z0-9_]*|<-|[+\-*/<>=]+|.)/g; let m; while ((m = re.exec(code)) !== null) { const t = m[0]; if (t[0] === '"') html += `${_esc(t)}`; else if (/^[0-9]+$/.test(t)) html += `${t}`; else if (t === '<-') html += `<-`; else if (/^[+\-*/<>=]+$/.test(t)) html += `${_esc(t)}`; else if (/^[A-Za-z_]/.test(t)) { const U = t.toUpperCase(); if (kw.has(U)) html += `${_esc(t)}`; else if (ty.has(U)) html += `${_esc(t)}`; else if (fn.has(U)) html += `${_esc(t)}`; else html += _esc(t); } else html += _esc(t); } if (comment) html += `${_esc(comment)}`; return html || ' '; }).join('\n'); } function CodeEditor({ value, onChange, disabled }) { const taRef = useRef(null); const preRef = useRef(null); const gutRef = useRef(null); const lines = value.split('\n').length; const sync = () => { if (preRef.current && taRef.current) { preRef.current.scrollTop = taRef.current.scrollTop; preRef.current.scrollLeft = taRef.current.scrollLeft; } if (gutRef.current && taRef.current) gutRef.current.scrollTop = taRef.current.scrollTop; }; const onKey = (e) => { if (e.key === 'Tab') { e.preventDefault(); const el = e.target, s = el.selectionStart, en = el.selectionEnd; const nv = value.slice(0, s) + ' ' + value.slice(en); onChange(nv); requestAnimationFrame(() => { el.selectionStart = el.selectionEnd = s + 4; }); } }; return (
{Array.from({ length: lines }, (_, i) =>
{i + 1}
)}