syntax-probe.mjs (3451B)
1 // Browser-only presentation checks: no evaluator/compiler substitution. 2 const assert=(ok,message)=>{if(!ok)throw Error(message);}; 3 export async function syntaxProbe(){ 4 const source=document.getElementById('source-editor'), input=document.getElementById('expression-editor'); 5 const wrapper=input.closest('.syntax-editor'), mirror=wrapper.querySelector('pre'); 6 assert(source.closest('.syntax-editor').querySelector('.syntax-definition'),'Loaded source declarations are highlighted'); 7 const sourceMirror=source.closest('.syntax-editor').querySelector('pre'); 8 await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); 9 assert(Math.abs(sourceMirror.scrollHeight-source.scrollHeight)<=2,'Source mirror wrapping matches native editor: '+JSON.stringify({viewport:innerWidth,input:source.scrollHeight,mirror:sourceMirror.scrollHeight,width:source.clientWidth,mirrorWidth:sourceMirror.style.width,lineHeight:getComputedStyle(source).lineHeight})); 10 assert(document.querySelector('#source .syntax-definition'),'Read-only source gets the same tokens'); 11 assert(wrapper.querySelector('[aria-hidden=true]')&&input.tagName==='TEXTAREA','Decorative mirror leaves native accessible textarea intact'); 12 const saved=input.value; 13 const set=value=>{input.value=value;input.dispatchEvent(new Event('input'));}; 14 const caret=position=>{input.focus();input.setSelectionRange(position,position);input.dispatchEvent(new Event('select'));}; 15 set('let named x = [x (t)] in named "λ🙂["'); 16 assert(mirror.querySelector('.syntax-definition').textContent==='named','Local declaration color'); 17 assert(mirror.textContent===input.value,'Mirror preserves all source bytes'); 18 assert(getComputedStyle(mirror.querySelector('.syntax-definition')).color!==getComputedStyle(mirror.querySelector('.syntax-name')).color,'Definition names have a dedicated color'); 19 caret(input.value.indexOf('['));assert(mirror.querySelectorAll('.bracket-match').length===2,'Caret highlights bracket pair'); 20 caret(input.value.lastIndexOf('['));assert(!mirror.querySelector('.bracket-match'),'String brackets are ignored'); 21 input.setSelectionRange(0,4);input.dispatchEvent(new Event('select'));assert(!mirror.querySelector('.bracket-match'),'Range selections have no caret match'); 22 set('(t');caret(0);assert(mirror.querySelector('.bracket-unmatched'),'Unclosed delimiter marked without compiler interaction'); 23 set('<img src=x onerror="throw 1">');assert(!mirror.querySelector('img')&&mirror.textContent===input.value,'Source cannot become HTML'); 24 input.dispatchEvent(new CompositionEvent('compositionstart')); 25 assert(wrapper.classList.contains('composing')&&getComputedStyle(wrapper.querySelector('.syntax-viewport')).visibility==='hidden','IME uses visible native text, not a duplicate mirror'); 26 set('"λ🙂"');input.dispatchEvent(new CompositionEvent('compositionend'));assert(!wrapper.classList.contains('composing'),'IME restores highlighting'); 27 set(Array(80).fill('"λ🙂" t (t t) '+ 'longIdentifier '.repeat(12)).join('\n')+'\n'); 28 await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); 29 assert(Math.abs(mirror.scrollHeight-input.scrollHeight)<=2,'Wrapping and trailing-line heights match native textarea'); 30 input.scrollTop=300;input.dispatchEvent(new Event('scroll')); 31 assert(mirror.style.transform.includes('-300px'),'Scrolling keeps decoration aligned'); 32 input.blur();set(saved);input.scrollTop=0;input.dispatchEvent(new Event('scroll')); 33 }