(function(){ "use strict"; const root=document.getElementById("reader-review"); if(!root)return; const reviews=[...root.querySelectorAll(".reader-question-review")]; const passages=[...root.querySelectorAll(".reader-passage")]; const nav=[...root.querySelectorAll("[data-review-target]")]; const previous=document.getElementById("reader-previous"); const next=document.getElementById("reader-next"); const evidenceStatus=document.getElementById("reader-evidence-status"); let current=0; function setEvidenceStatus(message){if(evidenceStatus)evidenceStatus.textContent=message} function normalizedWithMap(text){ const chars=[],map=[];let lastSpace=false; const replacements={"’":"'","‘":"'","“":"\"","”":"\"","–":"-","—":"-","\u00a0":" "}; for(let i=0;imark.replaceWith(document.createTextNode(mark.textContent)));container.normalize()} function referenceParts(reference){return reference.split(/\s*(?:\.{3,}|…|\/)\s*/).map(normalize).filter(part=>part.length>15).sort((a,b)=>b.length-a.length)} function textNodes(container){const walker=document.createTreeWalker(container,NodeFilter.SHOW_TEXT,{acceptNode:n=>n.nodeValue.trim()?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_REJECT});const nodes=[];while(walker.nextNode())nodes.push(walker.currentNode);return nodes} function wrapNode(node,start,end){const range=document.createRange();range.setStart(node,start);range.setEnd(node,end);const mark=document.createElement("mark");mark.className="reader-evidence-highlight";range.surroundContents(mark);return mark} function exactMatch(container,reference){ const parts=referenceParts(reference); for(const node of textNodes(container)){ const mapped=normalizedWithMap(node.nodeValue); for(const part of parts){const index=mapped.text.indexOf(part);if(index!==-1){const start=mapped.map[index],end=(mapped.map[index+part.length-1]||start)+1;return wrapNode(node,start,end)}} } return null; } function fuzzyMatch(container,reference){ const wanted=new Set(normalize(reference).split(" ").filter(word=>word.length>3));if(!wanted.size)return null; let best=null; for(const node of textNodes(container)){ const regex=/[^.!?]+[.!?]?/g;let match; while((match=regex.exec(node.nodeValue))){const words=new Set(normalize(match[0]).split(" ").filter(word=>word.length>3));let overlap=0;wanted.forEach(word=>{if(words.has(word))overlap++});const score=overlap/Math.max(1,Math.min(wanted.size,words.size));if(!best||score>best.score)best={node,start:match.index,end:match.index+match[0].length,score}} } return best&&best.score>=.34?wrapNode(best.node,best.start,best.end):null; } function highlight(review,scroll){ passages.forEach(passage=>clearHighlight(passage)); const passage=passages.find(item=>item.dataset.passageSection===review.dataset.sectionId);if(!passage)return null; const reference=review.dataset.reference||"";if(!reference){setEvidenceStatus("No evidence sentence attached");return null} const mark=exactMatch(passage,reference)||fuzzyMatch(passage,reference); setEvidenceStatus(mark?"Evidence highlighted for this question":"Reference shown with the answer"); if(mark&&scroll)mark.scrollIntoView({behavior:"smooth",block:"center"}); return mark; } function show(index,scrollEvidence,scrollReview){ current=Math.max(0,Math.min(index,reviews.length-1));const review=reviews[current]; reviews.forEach((item,i)=>item.classList.toggle("is-active",i===current));passages.forEach(item=>item.hidden=item.dataset.passageSection!==review.dataset.sectionId);nav.forEach((item,i)=>item.classList.toggle("is-current",i===current)); previous.disabled=current===0;next.disabled=current===reviews.length-1;highlight(review,!!scrollEvidence); nav[current]?.scrollIntoView({behavior:"smooth",block:"nearest",inline:"center"});if(scrollReview)review.scrollIntoView({behavior:"smooth",block:"start"}); } reviews.forEach((review,index)=>{ const button=review.querySelector(".reader-show-evidence"); if(!button)return; button.addEventListener("click",()=>{ show(index,false,false); const mark=highlight(review,true); if(!mark){ button.classList.add("is-unmatched"); button.innerHTML=' Evidence sentence not matched'; }else{ button.classList.remove("is-unmatched"); button.innerHTML=' Shown in passage'; window.setTimeout(()=>{button.innerHTML=' Show in passage'},1600); } }); }); nav.forEach(button=>button.addEventListener("click",()=>show(Number(button.dataset.reviewTarget),false,true))); previous.addEventListener("click",()=>show(current-1,false,true));next.addEventListener("click",()=>show(current+1,false,true)); document.addEventListener("keydown",event=>{if(event.key==="ArrowRight"&¤t0)show(current-1,false,true)}); /* Number, Previous, and Next controls are authoritative. An intersection observer used to change `current` again while smooth scrolling, which could land users on a neighbouring question. */ show(0,false,false); })();