diff --git a/.github/workflows/render-and-publish.yml b/.github/workflows/render-and-publish.yml index de274ae3..62a3609a 100644 --- a/.github/workflows/render-and-publish.yml +++ b/.github/workflows/render-and-publish.yml @@ -91,6 +91,7 @@ jobs: path: site/_site deploy: + if: github.repository == 'ClojureCivitas/clojurecivitas.github.io' needs: build permissions: pages: write diff --git a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css new file mode 100644 index 00000000..2c18db1a --- /dev/null +++ b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_base.css @@ -0,0 +1 @@ +:root{--ve-accent:#1464b5;--ve-accent-soft:#e5f1fb;--ve-warm:#9a4b00;--ve-warm-soft:#fff0df;--ve-purple:#694aa6;--ve-muted:#4f5b66}.quarto-dark{--ve-accent:#73b7ff;--ve-accent-soft:#173653;--ve-warm:#ffc27a;--ve-warm-soft:#4a2d12;--ve-purple:#c5a7ff;--ve-muted:#b9c7d2}#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}mjx-container[display=true]{max-width:100%;overflow-x:auto;overflow-y:hidden}.ve-callout{border:1px solid color-mix(in srgb,var(--ve-accent) 45%,var(--bs-border-color,#dee2e6));border-left:4px solid var(--ve-accent);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%);color:var(--bs-body-color,#212529);padding:1rem 1.15rem;margin:1.4rem 0;border-radius:.35rem}.ve-callout.provisional{border-color:color-mix(in srgb,var(--ve-warm) 60%,var(--bs-border-color,#dee2e6));border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}.ve-callout strong{display:block;margin-bottom:.3rem}.ve-callout p:last-child{margin-bottom:0}.ve-grid,.ve-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.75rem;margin:1.25rem 0}.ve-card,.ve-definition{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:.85rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);overflow-wrap:anywhere}.ve-card h3{font-size:1rem;margin-top:0}.ve-definition dt{font-weight:800;color:var(--ve-accent)}.ve-definition dd{margin:.25rem 0 0}.ve-table-wrap{max-width:100%;overflow-x:auto;margin:1.25rem 0}.ve-table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.ve-table th,.ve-table td{padding:.55rem .7rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-table th:first-child,.ve-table td:first-child{text-align:left}.ve-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}.ve-figure{margin:1.5rem 0;padding:1rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-figure svg{display:block;width:100%;height:auto}.ve-caption,.ve-note{font-size:.9rem;color:var(--ve-muted);margin:.75rem 0 0}.ve-simulator{margin:1.5rem 0}.ve-reading-guide{border-left:4px solid var(--ve-accent);padding:.2rem 0 .2rem 1rem;margin:1rem 0}.ve-reading-guide h3{font-size:1rem;margin:.1rem 0 .35rem}.ve-reading-guide ul{margin-bottom:.2rem}.ve-hierarchy{display:grid;grid-template-columns:minmax(8rem,.7fr) auto minmax(0,1.5fr);gap:.7rem;align-items:center}.ve-hierarchy-node{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.7rem;text-align:center;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.ve-hierarchy-node strong{display:block;color:var(--ve-accent)}.ve-hierarchy-arrow{font-size:1.5rem;color:var(--ve-accent);font-weight:800}.ve-form-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}.ve-sense-gap{grid-column:1/-1;border:2px dashed var(--ve-warm);border-radius:.45rem;padding:.65rem;text-align:center;color:var(--bs-body-color,#212529);background:color-mix(in srgb,var(--bs-body-bg,#fff) 92%,var(--ve-warm) 8%)}.ve-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:767px){.ve-hierarchy{grid-template-columns:minmax(0,1fr)}.ve-hierarchy-arrow{transform:rotate(90deg);text-align:center}.ve-form-list{grid-template-columns:minmax(0,1fr)}}@media(max-width:575px){.ve-table th,.ve-table td{padding:.45rem}.ve-figure{padding:.65rem}} diff --git a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_round.css b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_round.css new file mode 100644 index 00000000..dabb1dfa --- /dev/null +++ b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_round.css @@ -0,0 +1 @@ +.ve-round-shell,.ve-stop-shell{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.65rem;padding:clamp(.8rem,3vw,1.3rem);background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-round-shell h3,.ve-stop-shell h3{margin-top:0}.ve-round-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.55rem;margin:1rem 0}.ve-round-card{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.65rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 94%,var(--ve-accent) 6%);overflow-wrap:anywhere}.ve-round-card strong,.ve-round-card span,.ve-round-card small{display:block}.ve-round-card small{color:var(--ve-muted);margin-top:.25rem}.ve-round-progress{width:100%;height:.55rem;accent-color:var(--ve-accent)}.ve-round-status{min-height:2.8rem;font-variant-numeric:tabular-nums}.ve-stop-banner{display:flex;align-items:baseline;gap:.35rem;flex-wrap:wrap;border-left:4px solid var(--ve-accent);border-radius:.35rem;padding:.65rem .8rem;margin:.8rem 0;background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%)}.ve-stop-banner.is-counterfactual{border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}.ve-stop-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:.8rem;margin:1rem 0}.ve-stop-field{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.65rem}.ve-stop-field>div{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem}.ve-stop-field label{font-weight:700}.ve-stop-field output{font-variant-numeric:tabular-nums;color:var(--ve-muted);text-align:right}.ve-stop-field input{width:100%;accent-color:var(--ve-accent)}.ve-stop-result{border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.75rem;margin:1rem 0}.ve-stop-result ul{margin:.5rem 0 0}.ve-sampling-button:disabled{opacity:.5;cursor:not-allowed}.ve-stop-field input:focus-visible{outline:3px solid color-mix(in srgb,var(--ve-accent) 50%,transparent);outline-offset:3px}@media(max-width:767px){.ve-round-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:400px){.ve-round-grid{grid-template-columns:minmax(0,1fr)}} diff --git a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_sampler.css b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_sampler.css new file mode 100644 index 00000000..72e1ddae --- /dev/null +++ b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_sampler.css @@ -0,0 +1 @@ +.ve-posterior-sampler{border:1px solid var(--bs-border-color,#ced4da);border-radius:.65rem;padding:clamp(.8rem,3vw,1.3rem);min-width:0;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sampling-intro{max-width:50rem}.ve-sampling-controls{display:flex;align-items:end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:1rem 0}.ve-rate-fieldset{border:0;padding:0;margin:0;min-width:0}.ve-rate-fieldset legend{font-size:.85rem;font-weight:700;margin-bottom:.4rem}.ve-button-row{display:flex;gap:.5rem;flex-wrap:wrap}.ve-sampling-button{border:1px solid var(--bs-border-color,#6c757d);border-radius:.35rem;padding:.55rem .85rem;font-weight:600;cursor:pointer;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sampling-button[aria-pressed=true],.ve-sampling-button.ve-primary{border-color:#1464b5;background:#1464b5;color:#fff}.quarto-dark .ve-sampling-button[aria-pressed=true],.quarto-dark .ve-sampling-button.ve-primary{border-color:#73b7ff;background:#73b7ff;color:#10212b}.ve-sampling-button:focus-visible{outline:3px solid color-mix(in srgb,var(--ve-accent) 45%,transparent);outline-offset:2px}.ve-sampling-progress{width:100%;height:.55rem;accent-color:var(--ve-accent)}.ve-sampling-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:1rem;margin-top:1rem}.ve-sample-panel{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:clamp(.65rem,2vw,1rem);margin:0;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sample-panel h4{font-size:1rem;margin:0 0 .25rem}.ve-sample-panel svg{display:block;width:100%;height:auto}.ve-sample-stat{font-variant-numeric:tabular-nums;margin:.2rem 0 .65rem}.ve-sample-note{font-size:.85rem;color:var(--ve-muted);margin:.5rem 0 0}.ve-empty-sample{display:grid;place-items:center;min-height:13rem;border:1px dashed var(--bs-border-color,#adb5bd);border-radius:.35rem;color:var(--ve-muted);text-align:center;padding:1rem}.ve-sampling-status{font-variant-numeric:tabular-nums;margin:.4rem 0}.ve-dot{fill:var(--ve-accent);fill-opacity:.72}.ve-dot-latest{fill:var(--ve-warm);stroke:var(--bs-body-bg,#fff);stroke-width:1.5}.ve-sample-axis{stroke:currentColor;stroke-opacity:.45}.ve-sample-guide{stroke:currentColor;stroke-opacity:.1}.ve-latest-line{stroke:var(--ve-accent);stroke-width:5;stroke-linecap:round}.ve-latest-dot{fill:var(--ve-warm);stroke:var(--bs-body-bg,#fff);stroke-width:2}.ve-draw-breakdown{width:100%;border-collapse:collapse;font-size:.82rem;font-variant-numeric:tabular-nums;margin-top:.75rem}.ve-draw-breakdown th,.ve-draw-breakdown td{padding:.35rem .45rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-draw-breakdown th:first-child{text-align:left}.ve-draw-breakdown-wrap{max-width:100%;overflow-x:auto}@media(max-width:575px){.ve-sampling-controls{align-items:stretch}.ve-sampling-controls>.ve-button-row{width:100%}.ve-sampling-controls>.ve-button-row .ve-sampling-button{flex:1}.ve-sampling-button{padding:.55rem .7rem}} diff --git a/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_series.css b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_series.css new file mode 100644 index 00000000..eedc31ed --- /dev/null +++ b/resources/language_learning/vocabulary_estimation/beta_binomial_first_pass_series.css @@ -0,0 +1 @@ +.series-toc{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.6rem;padding:clamp(.85rem,3vw,1.2rem);margin:0 0 1.4rem;background:var(--bs-body-bg,#fff)}.series-toc h2{font-size:1.2rem;margin:0 0 .55rem}.series-toc p{margin:0 0 .7rem}.series-toc ol{margin:0;padding-left:1.45rem}.series-toc li{padding:.18rem .45rem}.series-status{display:inline-block;margin-left:.35rem;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--ve-muted)}.series-current{margin:.35rem 0 .35rem -.7rem;border-left:4px solid var(--ve-accent);border-radius:.4rem;padding:.6rem .75rem!important;background:color-mix(in srgb,var(--bs-body-bg,#fff) 84%,var(--ve-accent) 16%);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ve-accent) 35%,transparent);font-weight:700}.series-current>a{color:var(--ve-accent)}.series-current .series-status{border-radius:999px;padding:.18rem .48rem;background:var(--ve-accent);color:#fff}.quarto-dark .series-current .series-status{color:#10212b} diff --git a/resources/language_learning/vocabulary_estimation/math_explanations.js b/resources/language_learning/vocabulary_estimation/math_explanations.js new file mode 100644 index 00000000..88a13245 --- /dev/null +++ b/resources/language_learning/vocabulary_estimation/math_explanations.js @@ -0,0 +1,350 @@ +(() => { + const initialise = () => { + const globalButton = document.getElementById('article-explanations-toggle'); + if (!globalButton || globalButton.dataset.initialized === 'true') return; + + const articleMain = document.getElementById('quarto-document-content') || document.querySelector('main'); + const marginQuery = window.matchMedia('(min-width: 1280px)'); + const mathRegistries = Array.from(document.querySelectorAll('.math-explanation-registry')); + const termRegistries = Array.from(document.querySelectorAll('.term-explanation-registry')); + const codeDetails = Array.from(document.querySelectorAll('details.article-code-detail')); + const codeAction = document.getElementById('article-code-action'); + const codeButton = document.getElementById('article-code-toggle'); + const codeStatus = document.getElementById('article-code-status'); + + const makeHelpButton = ({label, controls, classes = []}) => { + const help = document.createElement('button'); + help.type = 'button'; + help.className = ['article-help-icon', ...classes].join(' '); + help.textContent = '?'; + help.setAttribute('aria-label', label); + help.setAttribute('title', label); + help.setAttribute('aria-controls', controls.join(' ')); + help.setAttribute('aria-expanded', 'false'); + return help; + }; + + const ensureLayout = (anchor) => { + const existing = anchor.closest('.article-explanation-layout'); + if (existing && existing.querySelector(':scope > .article-explanation-anchor') === anchor) { + return existing; + } + const layout = document.createElement('div'); + layout.className = 'article-explanation-layout'; + const slot = document.createElement('div'); + slot.className = 'article-explanation-slot'; + anchor.classList.add('article-explanation-anchor'); + anchor.parentElement.insertBefore(layout, anchor); + layout.appendChild(anchor); + layout.appendChild(slot); + return layout; + }; + + const layoutSlot = (layout) => layout.querySelector(':scope > .article-explanation-slot'); + const containsDisplayEquation = (node) => Boolean( + node && node.querySelector('span.math.display, mjx-container[display=true]') + ); + const precedingEquation = (registry) => { + let cursor = registry; + for (let depth = 0; cursor && cursor.parentElement && depth < 4; depth += 1) { + let candidate = cursor.previousElementSibling; + while (candidate) { + if (containsDisplayEquation(candidate)) return candidate; + candidate = candidate.previousElementSibling; + } + if (cursor.parentElement.matches('section')) break; + cursor = cursor.parentElement; + } + return null; + }; + + mathRegistries.forEach((registry, registryIndex) => { + const items = Array.from(registry.querySelectorAll(':scope > details.article-explanation')); + const anchor = precedingEquation(registry); + if (!anchor || items.length === 0) return; + const accentClass = `accent-${(registryIndex % 6) + 1}`; + const layout = ensureLayout(anchor); + layout.classList.add('equation-explanation-layout'); + const slot = layoutSlot(layout); + items.forEach((item) => { + item.classList.add(accentClass); + slot.appendChild(item); + }); + anchor.classList.add('equation-help-anchor'); + const title = registry.dataset.explanationTitle || 'this equation'; + const help = makeHelpButton({ + label: `Explain ${title}`, + controls: items.map((item) => item.id), + classes: ['equation-help-icon', accentClass] + }); + anchor.appendChild(help); + registry.remove(); + }); + + const excludedTextNode = (node) => Boolean(node.parentElement && node.parentElement.closest( + 'script,style,pre,code,svg,details,.article-explanations-toolbar,.article-explanation-registry,.article-help-icon' + )); + const normaliseTerm = (text) => text.toLocaleLowerCase().replace(/[‐‑‒–—]/g, '-'); + const isBoundary = (character) => !character || !/[A-Za-z0-9‐‑‒–—-]/.test(character); + const firstTermMatch = (root, text) => { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + const needle = normaliseTerm(text); + let node; + while ((node = walker.nextNode())) { + if (excludedTextNode(node)) continue; + const haystack = normaliseTerm(node.nodeValue || ''); + let index = haystack.indexOf(needle); + while (index >= 0) { + const afterIndex = index + needle.length; + const plural = haystack[afterIndex] === 's' && isBoundary(haystack[afterIndex + 1]); + if (isBoundary(haystack[index - 1]) && (isBoundary(haystack[afterIndex]) || plural)) { + return {node, index, length: needle.length + (plural ? 1 : 0)}; + } + index = haystack.indexOf(needle, index + 1); + } + } + return null; + }; + const anchorBlockFor = (node) => node.parentElement?.closest('p,blockquote,li,figcaption,h2,h3,h4') || node.parentElement; + const appendInlineHelp = (match, help) => { + const after = match.node.splitText(match.index + match.length); + after.parentNode.insertBefore(help, after); + }; + + termRegistries.forEach((registry) => { + const items = Array.from(registry.querySelectorAll(':scope > details.article-explanation')); + const searchRoot = registry.closest('section') || articleMain; + items.forEach((item) => { + const anchorText = item.dataset.anchorTerm || item.dataset.helpLabel || ''; + const match = firstTermMatch(searchRoot, anchorText); + const fallbackAnchor = registry.previousElementSibling; + const anchor = match ? anchorBlockFor(match.node) : fallbackAnchor; + if (!anchor) return; + const layout = ensureLayout(anchor); + layoutSlot(layout).appendChild(item); + const helpLabel = item.dataset.helpLabel || anchorText; + const category = item.classList.contains('lexical') ? 'lexical' : 'design'; + const help = makeHelpButton({ + label: `Explain ${helpLabel}`, + controls: [item.id], + classes: ['term-help-icon', category] + }); + if (match) appendInlineHelp(match, help); + else anchor.appendChild(help); + }); + registry.remove(); + }); + + const explanations = Array.from(document.querySelectorAll('details.article-explanation')); + const mathExplanations = explanations.filter((item) => item.classList.contains('math-explanation')); + const terminologyExplanations = explanations.filter((item) => item.classList.contains('term-explanation')); + const helpButtons = Array.from(document.querySelectorAll('.article-help-icon')); + const heading = document.getElementById('article-explanations-heading'); + const description = document.getElementById('article-explanations-description'); + const status = document.getElementById('article-explanations-status'); + const itemOrigins = new Map(); + const itemAnchors = new Map(); + explanations.forEach((item) => { + const slot = item.closest('.article-explanation-slot'); + const layout = slot?.closest('.article-explanation-layout'); + itemOrigins.set(item, slot); + itemAnchors.set(item, layout?.querySelector(':scope > .article-explanation-anchor') || null); + }); + const rail = document.createElement('aside'); + rail.className = 'article-explanation-rail'; + rail.setAttribute('aria-label', 'Expanded explanations'); + const railGuide = document.createElement('section'); + railGuide.className = 'article-explanation-rail-guide'; + const railGuideHeading = document.createElement('strong'); + railGuideHeading.textContent = 'Using explanation cards'; + const railGuideText = document.createElement('p'); + railGuideText.textContent = 'Cards relevant to the text in view scroll into place automatically. You can also scroll these cards independently. Select an open card to hide it.'; + const railHideAll = document.createElement('button'); + railHideAll.type = 'button'; + railHideAll.className = 'article-explanation-hide-all'; + railHideAll.textContent = 'Hide all explanations'; + railGuide.appendChild(railGuideHeading); + railGuide.appendChild(railGuideText); + railGuide.appendChild(railHideAll); + const railStack = document.createElement('div'); + railStack.className = 'article-explanation-rail-stack'; + rail.appendChild(railGuide); + rail.appendChild(railStack); + document.body.appendChild(rail); + + const controlledItems = (help) => (help.getAttribute('aria-controls') || '') + .split(/\s+/) + .filter(Boolean) + .map((id) => document.getElementById(id)) + .filter(Boolean); + const setOpen = (item, open) => { + item.open = open; + }; + const layouts = () => Array.from(document.querySelectorAll('.article-explanation-layout')); + const arrangeForViewport = () => { + if (marginQuery.matches) { + explanations.forEach((item) => railStack.appendChild(item)); + } else { + explanations.forEach((item) => itemOrigins.get(item)?.appendChild(item)); + } + }; + const relevantOpenItem = () => { + const viewportCentre = window.innerHeight / 2; + let relevant = null; + let nearestDistance = Number.POSITIVE_INFINITY; + const consideredAnchors = new Set(); + explanations.forEach((item) => { + if (!item.open) return; + const anchor = itemAnchors.get(item); + if (!anchor || consideredAnchors.has(anchor)) return; + consideredAnchors.add(anchor); + const rect = anchor.getBoundingClientRect(); + const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportCentre); + if (distance < nearestDistance) { + relevant = item; + nearestDistance = distance; + } + }); + return relevant; + }; + const centreRelevantExplanation = () => { + explanations.forEach((item) => item.classList.remove('is-relevant')); + if (!marginQuery.matches) return; + const relevant = relevantOpenItem(); + if (!relevant) return; + relevant.classList.add('is-relevant'); + const railRect = rail.getBoundingClientRect(); + const viewportCentreWithinRail = window.innerHeight / 2 - railRect.top; + const targetScroll = relevant.offsetTop + relevant.offsetHeight / 2 - viewportCentreWithinRail; + rail.scrollTop = Math.max(0, targetScroll); + }; + let placementFrame = null; + const schedulePlacement = () => { + if (placementFrame !== null) return; + placementFrame = window.requestAnimationFrame(() => { + placementFrame = null; + centreRelevantExplanation(); + }); + }; + const sync = () => { + const openItems = explanations.filter((item) => item.open); + const allOpen = explanations.length > 0 && openItems.length === explanations.length; + const openMath = mathExplanations.filter((item) => item.open).length; + const openTerms = terminologyExplanations.filter((item) => item.open).length; + + helpButtons.forEach((help) => { + const targets = controlledItems(help); + help.setAttribute('aria-expanded', String(targets.length > 0 && targets.every((item) => item.open))); + }); + layouts().forEach((layout) => { + const slot = layoutSlot(layout); + const hasOpen = explanations.some((item) => item.open && itemOrigins.get(item) === slot); + if (slot) slot.classList.toggle('has-open', hasOpen); + }); + rail.classList.toggle('has-open', openItems.length > 0); + document.body.classList.toggle('article-explanations-open', openItems.length > 0); + if (articleMain) articleMain.classList.toggle('article-explanations-open', openItems.length > 0); + globalButton.setAttribute('aria-pressed', String(allOpen)); + globalButton.textContent = `${allOpen ? 'Hide' : 'Show'} all help`; + if (heading) heading.textContent = 'Reading controls'; + const bothKinds = mathExplanations.length > 0 && terminologyExplanations.length > 0; + const countParts = []; + const progressParts = []; + if (mathExplanations.length > 0) { + countParts.push(`${mathExplanations.length} mathematical item${mathExplanations.length === 1 ? '' : 's'}`); + progressParts.push(`${openMath} of ${mathExplanations.length} mathematical item${mathExplanations.length === 1 ? '' : 's'}`); + } + if (terminologyExplanations.length > 0) { + countParts.push(`${terminologyExplanations.length} terminology item${terminologyExplanations.length === 1 ? '' : 's'}`); + progressParts.push(`${openTerms} of ${terminologyExplanations.length} terminology item${terminologyExplanations.length === 1 ? '' : 's'}`); + } + if (description) { + const helpInstruction = bothKinds + ? 'Use the ? beside a term or equation for optional explanations.' + : mathExplanations.length > 0 + ? 'Use the ? beside an equation for optional explanations.' + : 'Use the ? beside a term for an optional definition.'; + description.textContent = codeDetails.length > 0 + ? `${helpInstruction} Code details remain inline and independent.` + : helpInstruction; + } + if (status) { + status.textContent = openItems.length === 0 + ? `${countParts.join(' and ')} ${explanations.length === 1 ? 'is' : 'are'} hidden.` + : `${progressParts.join(' and ')} shown.`; + } + + const openCodeCount = codeDetails.filter((item) => item.open).length; + const allCodeOpen = codeDetails.length > 0 && openCodeCount === codeDetails.length; + if (codeAction) codeAction.hidden = codeDetails.length === 0; + if (codeButton) { + codeButton.setAttribute('aria-pressed', String(allCodeOpen)); + codeButton.textContent = allCodeOpen ? 'Hide all code details' : 'Show all code details'; + } + if (codeStatus) codeStatus.textContent = `${openCodeCount}/${codeDetails.length} code details open.`; + }; + + globalButton.addEventListener('click', () => { + const shouldOpen = !explanations.every((item) => item.open); + explanations.forEach((item) => setOpen(item, shouldOpen)); + sync(); + schedulePlacement(); + }); + if (codeButton) { + codeButton.addEventListener('click', () => { + const shouldOpen = !codeDetails.every((item) => item.open); + codeDetails.forEach((item) => setOpen(item, shouldOpen)); + sync(); + }); + } + railHideAll.addEventListener('click', () => { + explanations.forEach((item) => setOpen(item, false)); + sync(); + schedulePlacement(); + }); + helpButtons.forEach((help) => { + help.addEventListener('click', () => { + const targets = controlledItems(help); + const shouldOpen = targets.some((item) => !item.open); + targets.forEach((item) => setOpen(item, shouldOpen)); + sync(); + schedulePlacement(); + }); + }); + explanations.forEach((item) => { + item.addEventListener('click', (event) => { + const interactive = event.target instanceof Element && event.target.closest('a,button,input,select,textarea'); + if (!item.open || interactive) return; + event.preventDefault(); + setOpen(item, false); + sync(); + schedulePlacement(); + }); + item.addEventListener('toggle', () => { + sync(); + schedulePlacement(); + }); + }); + codeDetails.forEach((item) => item.addEventListener('toggle', sync)); + marginQuery.addEventListener('change', () => { + arrangeForViewport(); + sync(); + schedulePlacement(); + }); + window.addEventListener('scroll', schedulePlacement, {passive: true}); + window.addEventListener('resize', schedulePlacement); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedulePlacement); + window.setTimeout(schedulePlacement, 250); + window.setTimeout(schedulePlacement, 1000); + globalButton.dataset.initialized = 'true'; + arrangeForViewport(); + sync(); + schedulePlacement(); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initialise, {once: true}); + } else { + initialise(); + } +})(); diff --git a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj index 8480a3d8..f4d38a7d 100644 --- a/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj +++ b/src/language_learning/vocabulary_estimation/beta_binomial_first_pass.clj @@ -11,52 +11,33 @@ :keywords [:vocabulary-estimation :beta-binomial :posterior-predictive :stratified-sampling]}}} (ns language-learning.vocabulary-estimation.beta-binomial-first-pass - (:require [fastmath.random :as random] + (:require [clojure.java.io :as io] + [fastmath.random :as random] [language-learning.vocabulary-estimation.math-explanations :as math] [scicloj.kindly.v4.kind :as kind])) +(defn- resource-text [filename] + (let [text (slurp + (io/resource + (str "language_learning/vocabulary_estimation/" filename)))] + (if (= \newline (last text)) + (subs text 0 (dec (count text))) + text))) + ^:kindly/hide-code (kind/hiccup - [:style - (str - ":root{--ve-accent:#1464b5;--ve-accent-soft:#e5f1fb;--ve-warm:#9a4b00;--ve-warm-soft:#fff0df;--ve-purple:#694aa6;--ve-muted:#4f5b66}" - ".quarto-dark{--ve-accent:#73b7ff;--ve-accent-soft:#173653;--ve-warm:#ffc27a;--ve-warm-soft:#4a2d12;--ve-purple:#c5a7ff;--ve-muted:#b9c7d2}" - "#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}" - "mjx-container[display=true]{max-width:100%;overflow-x:auto;overflow-y:hidden}" - ".ve-callout{border:1px solid color-mix(in srgb,var(--ve-accent) 45%,var(--bs-border-color,#dee2e6));border-left:4px solid var(--ve-accent);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%);color:var(--bs-body-color,#212529);padding:1rem 1.15rem;margin:1.4rem 0;border-radius:.35rem}" - ".ve-callout.provisional{border-color:color-mix(in srgb,var(--ve-warm) 60%,var(--bs-border-color,#dee2e6));border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}" - ".ve-callout strong{display:block;margin-bottom:.3rem}.ve-callout p:last-child{margin-bottom:0}" - ".ve-grid,.ve-definition-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:.75rem;margin:1.25rem 0}" - ".ve-card,.ve-definition{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:.85rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529);overflow-wrap:anywhere}" - ".ve-card h3{font-size:1rem;margin-top:0}.ve-definition dt{font-weight:800;color:var(--ve-accent)}.ve-definition dd{margin:.25rem 0 0}" - ".ve-table-wrap{max-width:100%;overflow-x:auto;margin:1.25rem 0}.ve-table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}" - ".ve-table th,.ve-table td{padding:.55rem .7rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-table th:first-child,.ve-table td:first-child{text-align:left}.ve-table thead th{border-bottom:2px solid var(--bs-border-color,#adb5bd)}" - ".ve-figure{margin:1.5rem 0;padding:1rem;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-figure svg{display:block;width:100%;height:auto}" - ".ve-caption,.ve-note{font-size:.9rem;color:var(--ve-muted);margin:.75rem 0 0}.ve-simulator{margin:1.5rem 0}" - ".ve-reading-guide{border-left:4px solid var(--ve-accent);padding:.2rem 0 .2rem 1rem;margin:1rem 0}.ve-reading-guide h3{font-size:1rem;margin:.1rem 0 .35rem}.ve-reading-guide ul{margin-bottom:.2rem}" - ".ve-hierarchy{display:grid;grid-template-columns:minmax(8rem,.7fr) auto minmax(0,1.5fr);gap:.7rem;align-items:center}.ve-hierarchy-node{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.7rem;text-align:center;background:var(--bs-body-bg,#fff);overflow-wrap:anywhere}.ve-hierarchy-node strong{display:block;color:var(--ve-accent)}" - ".ve-hierarchy-arrow{font-size:1.5rem;color:var(--ve-accent);font-weight:800}.ve-form-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}.ve-sense-gap{grid-column:1/-1;border:2px dashed var(--ve-warm);border-radius:.45rem;padding:.65rem;text-align:center;color:var(--bs-body-color,#212529);background:color-mix(in srgb,var(--bs-body-bg,#fff) 92%,var(--ve-warm) 8%)}" - ".ve-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}" - "@media(max-width:767px){.ve-hierarchy{grid-template-columns:minmax(0,1fr)}.ve-hierarchy-arrow{transform:rotate(90deg);text-align:center}.ve-form-list{grid-template-columns:minmax(0,1fr)}}" - "@media(max-width:575px){.ve-table th,.ve-table td{padding:.45rem}.ve-figure{padding:.65rem}}")]) + [:style (resource-text "beta_binomial_first_pass_base.css")]) ^:kindly/hide-code (math/styles) ^:kindly/hide-code (kind/hiccup - [:style - (str - ".ve-round-shell,.ve-stop-shell{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.65rem;padding:clamp(.8rem,3vw,1.3rem);background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}" - ".ve-round-shell h3,.ve-stop-shell h3{margin-top:0}.ve-round-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.55rem;margin:1rem 0}.ve-round-card{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.65rem;background:color-mix(in srgb,var(--bs-body-bg,#fff) 94%,var(--ve-accent) 6%);overflow-wrap:anywhere}.ve-round-card strong,.ve-round-card span,.ve-round-card small{display:block}.ve-round-card small{color:var(--ve-muted);margin-top:.25rem}.ve-round-progress{width:100%;height:.55rem;accent-color:var(--ve-accent)}.ve-round-status{min-height:2.8rem;font-variant-numeric:tabular-nums}" - ".ve-stop-banner{display:flex;align-items:baseline;gap:.35rem;flex-wrap:wrap;border-left:4px solid var(--ve-accent);border-radius:.35rem;padding:.65rem .8rem;margin:.8rem 0;background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-accent) 10%)}.ve-stop-banner.is-counterfactual{border-left-color:var(--ve-warm);background:color-mix(in srgb,var(--bs-body-bg,#fff) 90%,var(--ve-warm) 10%)}" - ".ve-stop-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:.8rem;margin:1rem 0}.ve-stop-field{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.65rem}.ve-stop-field>div{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem}.ve-stop-field label{font-weight:700}.ve-stop-field output{font-variant-numeric:tabular-nums;color:var(--ve-muted);text-align:right}.ve-stop-field input{width:100%;accent-color:var(--ve-accent)}.ve-stop-result{border:1px solid var(--bs-border-color,#dee2e6);border-radius:.45rem;padding:.75rem;margin:1rem 0}.ve-stop-result ul{margin:.5rem 0 0}.ve-sampling-button:disabled{opacity:.5;cursor:not-allowed}.ve-stop-field input:focus-visible{outline:3px solid color-mix(in srgb,var(--ve-accent) 50%,transparent);outline-offset:3px}" - "@media(max-width:767px){.ve-round-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:400px){.ve-round-grid{grid-template-columns:minmax(0,1fr)}}")]) + [:style (resource-text "beta_binomial_first_pass_round.css")]) ^:kindly/hide-code (kind/hiccup - [:style - ".series-toc{min-width:0;border:1px solid var(--bs-border-color,#ced4da);border-radius:.6rem;padding:clamp(.85rem,3vw,1.2rem);margin:0 0 1.4rem;background:var(--bs-body-bg,#fff)}.series-toc h2{font-size:1.2rem;margin:0 0 .55rem}.series-toc p{margin:0 0 .7rem}.series-toc ol{margin:0;padding-left:1.45rem}.series-toc li{padding:.18rem .45rem}.series-status{display:inline-block;margin-left:.35rem;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--ve-muted)}.series-current{margin:.35rem 0 .35rem -.7rem;border-left:4px solid var(--ve-accent);border-radius:.4rem;padding:.6rem .75rem!important;background:color-mix(in srgb,var(--bs-body-bg,#fff) 84%,var(--ve-accent) 16%);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ve-accent) 35%,transparent);font-weight:700}.series-current>a{color:var(--ve-accent)}.series-current .series-status{border-radius:999px;padding:.18rem .48rem;background:var(--ve-accent);color:#fff}.quarto-dark .series-current .series-status{color:#10212b}"]) + [:style (resource-text "beta_binomial_first_pass_series.css")]) ^:kindly/hide-code (kind/hiccup @@ -665,8 +646,7 @@ ^:kindly/hide-code (kind/hiccup - [:style - ".ve-posterior-sampler{border:1px solid var(--bs-border-color,#ced4da);border-radius:.65rem;padding:clamp(.8rem,3vw,1.3rem);min-width:0;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sampling-intro{max-width:50rem}.ve-sampling-controls{display:flex;align-items:end;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:1rem 0}.ve-rate-fieldset{border:0;padding:0;margin:0;min-width:0}.ve-rate-fieldset legend{font-size:.85rem;font-weight:700;margin-bottom:.4rem}.ve-button-row{display:flex;gap:.5rem;flex-wrap:wrap}.ve-sampling-button{border:1px solid var(--bs-border-color,#6c757d);border-radius:.35rem;padding:.55rem .85rem;font-weight:600;cursor:pointer;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sampling-button[aria-pressed=true],.ve-sampling-button.ve-primary{border-color:#1464b5;background:#1464b5;color:#fff}.quarto-dark .ve-sampling-button[aria-pressed=true],.quarto-dark .ve-sampling-button.ve-primary{border-color:#73b7ff;background:#73b7ff;color:#10212b}.ve-sampling-button:focus-visible{outline:3px solid color-mix(in srgb,var(--ve-accent) 45%,transparent);outline-offset:2px}.ve-sampling-progress{width:100%;height:.55rem;accent-color:var(--ve-accent)}.ve-sampling-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:1rem;margin-top:1rem}.ve-sample-panel{min-width:0;border:1px solid var(--bs-border-color,#dee2e6);border-radius:.5rem;padding:clamp(.65rem,2vw,1rem);margin:0;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.ve-sample-panel h4{font-size:1rem;margin:0 0 .25rem}.ve-sample-panel svg{display:block;width:100%;height:auto}.ve-sample-stat{font-variant-numeric:tabular-nums;margin:.2rem 0 .65rem}.ve-sample-note{font-size:.85rem;color:var(--ve-muted);margin:.5rem 0 0}.ve-empty-sample{display:grid;place-items:center;min-height:13rem;border:1px dashed var(--bs-border-color,#adb5bd);border-radius:.35rem;color:var(--ve-muted);text-align:center;padding:1rem}.ve-sampling-status{font-variant-numeric:tabular-nums;margin:.4rem 0}.ve-dot{fill:var(--ve-accent);fill-opacity:.72}.ve-dot-latest{fill:var(--ve-warm);stroke:var(--bs-body-bg,#fff);stroke-width:1.5}.ve-sample-axis{stroke:currentColor;stroke-opacity:.45}.ve-sample-guide{stroke:currentColor;stroke-opacity:.1}.ve-latest-line{stroke:var(--ve-accent);stroke-width:5;stroke-linecap:round}.ve-latest-dot{fill:var(--ve-warm);stroke:var(--bs-body-bg,#fff);stroke-width:2}.ve-draw-breakdown{width:100%;border-collapse:collapse;font-size:.82rem;font-variant-numeric:tabular-nums;margin-top:.75rem}.ve-draw-breakdown th,.ve-draw-breakdown td{padding:.35rem .45rem;border-bottom:1px solid var(--bs-border-color,#dee2e6);text-align:right;white-space:nowrap}.ve-draw-breakdown th:first-child{text-align:left}.ve-draw-breakdown-wrap{max-width:100%;overflow-x:auto}@media(max-width:575px){.ve-sampling-controls{align-items:stretch}.ve-sampling-controls>.ve-button-row{width:100%}.ve-sampling-controls>.ve-button-row .ve-sampling-button{flex:1}.ve-sampling-button{padding:.55rem .7rem}}"]) + [:style (resource-text "beta_binomial_first_pass_sampler.css")]) ^:kindly/hide-code (kind/hiccup diff --git a/src/language_learning/vocabulary_estimation/math_explanations.clj b/src/language_learning/vocabulary_estimation/math_explanations.clj index ea16a3ef..6d570eae 100644 --- a/src/language_learning/vocabulary_estimation/math_explanations.clj +++ b/src/language_learning/vocabulary_estimation/math_explanations.clj @@ -1,5 +1,6 @@ (ns language-learning.vocabulary-estimation.math-explanations - (:require [scicloj.kindly.v4.kind :as kind])) + (:require [clojure.java.io :as io] + [scicloj.kindly.v4.kind :as kind])) (defn styles [] (kind/hiccup @@ -78,356 +79,12 @@ "@media(max-width:767px){.article-explanations-toolbar{grid-template-columns:minmax(0,1fr);align-items:stretch}.article-explanations-toggle,.article-code-toggle{width:100%}.article-reading-action{width:100%}}")])) (def ^:private explanation-controls-script - "(() => { - const initialise = () => { - const globalButton = document.getElementById('article-explanations-toggle'); - if (!globalButton || globalButton.dataset.initialized === 'true') return; - - const articleMain = document.getElementById('quarto-document-content') || document.querySelector('main'); - const marginQuery = window.matchMedia('(min-width: 1280px)'); - const mathRegistries = Array.from(document.querySelectorAll('.math-explanation-registry')); - const termRegistries = Array.from(document.querySelectorAll('.term-explanation-registry')); - const codeDetails = Array.from(document.querySelectorAll('details.article-code-detail')); - const codeAction = document.getElementById('article-code-action'); - const codeButton = document.getElementById('article-code-toggle'); - const codeStatus = document.getElementById('article-code-status'); - - const makeHelpButton = ({label, controls, classes = []}) => { - const help = document.createElement('button'); - help.type = 'button'; - help.className = ['article-help-icon', ...classes].join(' '); - help.textContent = '?'; - help.setAttribute('aria-label', label); - help.setAttribute('title', label); - help.setAttribute('aria-controls', controls.join(' ')); - help.setAttribute('aria-expanded', 'false'); - return help; - }; - - const ensureLayout = (anchor) => { - const existing = anchor.closest('.article-explanation-layout'); - if (existing && existing.querySelector(':scope > .article-explanation-anchor') === anchor) { - return existing; - } - const layout = document.createElement('div'); - layout.className = 'article-explanation-layout'; - const slot = document.createElement('div'); - slot.className = 'article-explanation-slot'; - anchor.classList.add('article-explanation-anchor'); - anchor.parentElement.insertBefore(layout, anchor); - layout.appendChild(anchor); - layout.appendChild(slot); - return layout; - }; - - const layoutSlot = (layout) => layout.querySelector(':scope > .article-explanation-slot'); - const containsDisplayEquation = (node) => Boolean( - node && node.querySelector('span.math.display, mjx-container[display=true]') - ); - const precedingEquation = (registry) => { - let cursor = registry; - for (let depth = 0; cursor && cursor.parentElement && depth < 4; depth += 1) { - let candidate = cursor.previousElementSibling; - while (candidate) { - if (containsDisplayEquation(candidate)) return candidate; - candidate = candidate.previousElementSibling; - } - if (cursor.parentElement.matches('section')) break; - cursor = cursor.parentElement; - } - return null; - }; - - mathRegistries.forEach((registry, registryIndex) => { - const items = Array.from(registry.querySelectorAll(':scope > details.article-explanation')); - const anchor = precedingEquation(registry); - if (!anchor || items.length === 0) return; - const accentClass = `accent-${(registryIndex % 6) + 1}`; - const layout = ensureLayout(anchor); - layout.classList.add('equation-explanation-layout'); - const slot = layoutSlot(layout); - items.forEach((item) => { - item.classList.add(accentClass); - slot.appendChild(item); - }); - anchor.classList.add('equation-help-anchor'); - const title = registry.dataset.explanationTitle || 'this equation'; - const help = makeHelpButton({ - label: `Explain ${title}`, - controls: items.map((item) => item.id), - classes: ['equation-help-icon', accentClass] - }); - anchor.appendChild(help); - registry.remove(); - }); - - const excludedTextNode = (node) => Boolean(node.parentElement && node.parentElement.closest( - 'script,style,pre,code,svg,details,.article-explanations-toolbar,.article-explanation-registry,.article-help-icon' - )); - const normaliseTerm = (text) => text.toLocaleLowerCase().replace(/[‐‑‒–—]/g, '-'); - const isBoundary = (character) => !character || !/[A-Za-z0-9‐‑‒–—-]/.test(character); - const firstTermMatch = (root, text) => { - const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); - const needle = normaliseTerm(text); - let node; - while ((node = walker.nextNode())) { - if (excludedTextNode(node)) continue; - const haystack = normaliseTerm(node.nodeValue || ''); - let index = haystack.indexOf(needle); - while (index >= 0) { - const afterIndex = index + needle.length; - const plural = haystack[afterIndex] === 's' && isBoundary(haystack[afterIndex + 1]); - if (isBoundary(haystack[index - 1]) && (isBoundary(haystack[afterIndex]) || plural)) { - return {node, index, length: needle.length + (plural ? 1 : 0)}; - } - index = haystack.indexOf(needle, index + 1); - } - } - return null; - }; - const anchorBlockFor = (node) => node.parentElement?.closest('p,blockquote,li,figcaption,h2,h3,h4') || node.parentElement; - const appendInlineHelp = (match, help) => { - const after = match.node.splitText(match.index + match.length); - after.parentNode.insertBefore(help, after); - }; - - termRegistries.forEach((registry) => { - const items = Array.from(registry.querySelectorAll(':scope > details.article-explanation')); - const searchRoot = registry.closest('section') || articleMain; - items.forEach((item) => { - const anchorText = item.dataset.anchorTerm || item.dataset.helpLabel || ''; - const match = firstTermMatch(searchRoot, anchorText); - const fallbackAnchor = registry.previousElementSibling; - const anchor = match ? anchorBlockFor(match.node) : fallbackAnchor; - if (!anchor) return; - const layout = ensureLayout(anchor); - layoutSlot(layout).appendChild(item); - const helpLabel = item.dataset.helpLabel || anchorText; - const category = item.classList.contains('lexical') ? 'lexical' : 'design'; - const help = makeHelpButton({ - label: `Explain ${helpLabel}`, - controls: [item.id], - classes: ['term-help-icon', category] - }); - if (match) appendInlineHelp(match, help); - else anchor.appendChild(help); - }); - registry.remove(); - }); - - const explanations = Array.from(document.querySelectorAll('details.article-explanation')); - const mathExplanations = explanations.filter((item) => item.classList.contains('math-explanation')); - const terminologyExplanations = explanations.filter((item) => item.classList.contains('term-explanation')); - const helpButtons = Array.from(document.querySelectorAll('.article-help-icon')); - const heading = document.getElementById('article-explanations-heading'); - const description = document.getElementById('article-explanations-description'); - const status = document.getElementById('article-explanations-status'); - const itemOrigins = new Map(); - const itemAnchors = new Map(); - explanations.forEach((item) => { - const slot = item.closest('.article-explanation-slot'); - const layout = slot?.closest('.article-explanation-layout'); - itemOrigins.set(item, slot); - itemAnchors.set(item, layout?.querySelector(':scope > .article-explanation-anchor') || null); - }); - const rail = document.createElement('aside'); - rail.className = 'article-explanation-rail'; - rail.setAttribute('aria-label', 'Expanded explanations'); - const railGuide = document.createElement('section'); - railGuide.className = 'article-explanation-rail-guide'; - const railGuideHeading = document.createElement('strong'); - railGuideHeading.textContent = 'Using explanation cards'; - const railGuideText = document.createElement('p'); - railGuideText.textContent = 'Cards relevant to the text in view scroll into place automatically. You can also scroll these cards independently. Select an open card to hide it.'; - const railHideAll = document.createElement('button'); - railHideAll.type = 'button'; - railHideAll.className = 'article-explanation-hide-all'; - railHideAll.textContent = 'Hide all explanations'; - railGuide.appendChild(railGuideHeading); - railGuide.appendChild(railGuideText); - railGuide.appendChild(railHideAll); - const railStack = document.createElement('div'); - railStack.className = 'article-explanation-rail-stack'; - rail.appendChild(railGuide); - rail.appendChild(railStack); - document.body.appendChild(rail); - - const controlledItems = (help) => (help.getAttribute('aria-controls') || '') - .split(/\\s+/) - .filter(Boolean) - .map((id) => document.getElementById(id)) - .filter(Boolean); - const setOpen = (item, open) => { - item.open = open; - }; - const layouts = () => Array.from(document.querySelectorAll('.article-explanation-layout')); - const arrangeForViewport = () => { - if (marginQuery.matches) { - explanations.forEach((item) => railStack.appendChild(item)); - } else { - explanations.forEach((item) => itemOrigins.get(item)?.appendChild(item)); - } - }; - const relevantOpenItem = () => { - const viewportCentre = window.innerHeight / 2; - let relevant = null; - let nearestDistance = Number.POSITIVE_INFINITY; - const consideredAnchors = new Set(); - explanations.forEach((item) => { - if (!item.open) return; - const anchor = itemAnchors.get(item); - if (!anchor || consideredAnchors.has(anchor)) return; - consideredAnchors.add(anchor); - const rect = anchor.getBoundingClientRect(); - const distance = Math.abs((rect.top + rect.bottom) / 2 - viewportCentre); - if (distance < nearestDistance) { - relevant = item; - nearestDistance = distance; - } - }); - return relevant; - }; - const centreRelevantExplanation = () => { - explanations.forEach((item) => item.classList.remove('is-relevant')); - if (!marginQuery.matches) return; - const relevant = relevantOpenItem(); - if (!relevant) return; - relevant.classList.add('is-relevant'); - const railRect = rail.getBoundingClientRect(); - const viewportCentreWithinRail = window.innerHeight / 2 - railRect.top; - const targetScroll = relevant.offsetTop + relevant.offsetHeight / 2 - viewportCentreWithinRail; - rail.scrollTop = Math.max(0, targetScroll); - }; - let placementFrame = null; - const schedulePlacement = () => { - if (placementFrame !== null) return; - placementFrame = window.requestAnimationFrame(() => { - placementFrame = null; - centreRelevantExplanation(); - }); - }; - const sync = () => { - const openItems = explanations.filter((item) => item.open); - const allOpen = explanations.length > 0 && openItems.length === explanations.length; - const openMath = mathExplanations.filter((item) => item.open).length; - const openTerms = terminologyExplanations.filter((item) => item.open).length; - - helpButtons.forEach((help) => { - const targets = controlledItems(help); - help.setAttribute('aria-expanded', String(targets.length > 0 && targets.every((item) => item.open))); - }); - layouts().forEach((layout) => { - const slot = layoutSlot(layout); - const hasOpen = explanations.some((item) => item.open && itemOrigins.get(item) === slot); - if (slot) slot.classList.toggle('has-open', hasOpen); - }); - rail.classList.toggle('has-open', openItems.length > 0); - document.body.classList.toggle('article-explanations-open', openItems.length > 0); - if (articleMain) articleMain.classList.toggle('article-explanations-open', openItems.length > 0); - globalButton.setAttribute('aria-pressed', String(allOpen)); - globalButton.textContent = `${allOpen ? 'Hide' : 'Show'} all help`; - if (heading) heading.textContent = 'Reading controls'; - const bothKinds = mathExplanations.length > 0 && terminologyExplanations.length > 0; - const countParts = []; - const progressParts = []; - if (mathExplanations.length > 0) { - countParts.push(`${mathExplanations.length} mathematical item${mathExplanations.length === 1 ? '' : 's'}`); - progressParts.push(`${openMath} of ${mathExplanations.length} mathematical item${mathExplanations.length === 1 ? '' : 's'}`); - } - if (terminologyExplanations.length > 0) { - countParts.push(`${terminologyExplanations.length} terminology item${terminologyExplanations.length === 1 ? '' : 's'}`); - progressParts.push(`${openTerms} of ${terminologyExplanations.length} terminology item${terminologyExplanations.length === 1 ? '' : 's'}`); - } - if (description) { - const helpInstruction = bothKinds - ? 'Use the ? beside a term or equation for optional explanations.' - : mathExplanations.length > 0 - ? 'Use the ? beside an equation for optional explanations.' - : 'Use the ? beside a term for an optional definition.'; - description.textContent = codeDetails.length > 0 - ? `${helpInstruction} Code details remain inline and independent.` - : helpInstruction; - } - if (status) { - status.textContent = openItems.length === 0 - ? `${countParts.join(' and ')} ${explanations.length === 1 ? 'is' : 'are'} hidden.` - : `${progressParts.join(' and ')} shown.`; - } - - const openCodeCount = codeDetails.filter((item) => item.open).length; - const allCodeOpen = codeDetails.length > 0 && openCodeCount === codeDetails.length; - if (codeAction) codeAction.hidden = codeDetails.length === 0; - if (codeButton) { - codeButton.setAttribute('aria-pressed', String(allCodeOpen)); - codeButton.textContent = allCodeOpen ? 'Hide all code details' : 'Show all code details'; - } - if (codeStatus) codeStatus.textContent = `${openCodeCount}/${codeDetails.length} code details open.`; - }; - - globalButton.addEventListener('click', () => { - const shouldOpen = !explanations.every((item) => item.open); - explanations.forEach((item) => setOpen(item, shouldOpen)); - sync(); - schedulePlacement(); - }); - if (codeButton) { - codeButton.addEventListener('click', () => { - const shouldOpen = !codeDetails.every((item) => item.open); - codeDetails.forEach((item) => setOpen(item, shouldOpen)); - sync(); - }); - } - railHideAll.addEventListener('click', () => { - explanations.forEach((item) => setOpen(item, false)); - sync(); - schedulePlacement(); - }); - helpButtons.forEach((help) => { - help.addEventListener('click', () => { - const targets = controlledItems(help); - const shouldOpen = targets.some((item) => !item.open); - targets.forEach((item) => setOpen(item, shouldOpen)); - sync(); - schedulePlacement(); - }); - }); - explanations.forEach((item) => { - item.addEventListener('click', (event) => { - const interactive = event.target instanceof Element && event.target.closest('a,button,input,select,textarea'); - if (!item.open || interactive) return; - event.preventDefault(); - setOpen(item, false); - sync(); - schedulePlacement(); - }); - item.addEventListener('toggle', () => { - sync(); - schedulePlacement(); - }); - }); - codeDetails.forEach((item) => item.addEventListener('toggle', sync)); - marginQuery.addEventListener('change', () => { - arrangeForViewport(); - sync(); - schedulePlacement(); - }); - window.addEventListener('scroll', schedulePlacement, {passive: true}); - window.addEventListener('resize', schedulePlacement); - if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedulePlacement); - window.setTimeout(schedulePlacement, 250); - window.setTimeout(schedulePlacement, 1000); - globalButton.dataset.initialized = 'true'; - arrangeForViewport(); - sync(); - schedulePlacement(); - }; - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initialise, {once: true}); - } else { - initialise(); - } -})();") + (let [script (slurp + (io/resource + "language_learning/vocabulary_estimation/math_explanations.js"))] + (if (= \newline (last script)) + (subs script 0 (dec (count script))) + script))) (defn global-controls [] (kind/hiccup