Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion scripts/docs-site/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ function pageMarkdownForCopy(control){const source=control.closest(".article")?.
function copyPageMarkdown(control){return copyText(pageMarkdownForCopy(control),control)}
function handleDocsControlClick(e){const copyCode=e.target.closest("[data-code-copy]");if(copyCode){copyText(codeTextForCopy(copyCode.closest(".oc-code")),copyCode);return true}const copyPrompt=e.target.closest("[data-prompt-copy]");if(copyPrompt){const prompt=copyPrompt.closest(".oc-prompt")?.textContent?.replace(/Copy\\s*$/,"").trim()||"";copyText(prompt,copyPrompt);return true}const copyPage=e.target.closest("[data-copy-page]");if(copyPage){copyPageMarkdown(copyPage);return true}const headingAnchor=e.target.closest("[data-heading-anchor]");if(headingAnchor){const url=new URL(location.href);url.hash=headingAnchor.dataset.headingAnchor||"";copyText(url.href,headingAnchor);return true}const feedback=e.target.closest("[data-feedback-value]");if(feedback){const result=feedback.closest(".page-feedback")?.querySelector("[data-feedback-result]");if(result)result.value=feedback.dataset.feedbackValue==="yes"?"Thanks.":"Noted.";return true}return false}
function scrollTarget(hash){if(hash){document.getElementById(decodeURIComponent(hash.slice(1)))?.scrollIntoView()}else{scrollTo(0,0)}}
async function navigateTo(url,replace=false){if(navigating)return false;navigating=true;closeLanguage();try{const res=await fetch(url.href,{credentials:"same-origin"});if(!res.ok||!res.headers.get("content-type")?.includes("text/html"))return false;const nextDoc=new DOMParser().parseFromString(await res.text(),"text/html");if(!nextDoc.querySelector(".main"))return false;syncSidebar(nextDoc);swap(".header-left",nextDoc);swapTabs(nextDoc);swap(".main",nextDoc);syncStickyHeaderOffset();syncTocDisclosure();initCodeGroups();initMermaid();document.title=nextDoc.title;history[replace?"replaceState":"pushState"]({docs:true},"",url.href);setNavOpen(false);scrollTarget(url.hash);return true}catch{return false}finally{navigating=false}}
let tocObserver=null;let tocScrollHandler=null;
function tocLinkId(link){try{return decodeURIComponent(link.hash.slice(1))}catch{return link.hash.slice(1)}}
function setActiveTocLink(id){const toc=document.querySelector(".toc");if(!toc)return;let active=null;toc.querySelectorAll('a[href^="#"]').forEach(link=>{const on=Boolean(id&&tocLinkId(link)===id);link.classList.toggle("active",on);if(on)active=link});active?.scrollIntoView({block:"nearest"})}
function currentTocHeadingId(headings){const top=parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop)||120;const scroller=document.scrollingElement||document.documentElement;if(scroller.scrollTop+innerHeight>=scroller.scrollHeight-2)return headings.at(-1)?.id||"";let current=headings[0];for(const heading of headings){if(heading.getBoundingClientRect().top<=top+1)current=heading;else break}return current?.id||""}
function initTocScrollspy(){tocObserver?.disconnect();tocObserver=null;if(tocScrollHandler){removeEventListener("scroll",tocScrollHandler);tocScrollHandler=null;}const toc=document.querySelector(".toc");if(!toc)return;const links=[...toc.querySelectorAll('a[href^="#"]')];const headings=[...document.querySelectorAll(".doc h2[id],.doc h3[id]")].filter(heading=>links.some(link=>tocLinkId(link)===heading.id));links.forEach(link=>link.classList.remove("active"));if(!headings.length)return;const sync=()=>setActiveTocLink(currentTocHeadingId(headings));let tocScrollRaf=0;tocScrollHandler=()=>{cancelAnimationFrame(tocScrollRaf);tocScrollRaf=requestAnimationFrame(sync)};addEventListener("scroll",tocScrollHandler,{passive:true});requestAnimationFrame(sync);if(!("IntersectionObserver" in window)){sync();return}tocObserver=new IntersectionObserver(sync,{rootMargin:"-120px 0px -70% 0px",threshold:[0,1]});headings.forEach(heading=>tocObserver.observe(heading))}
async function navigateTo(url,replace=false){if(navigating)return false;navigating=true;closeLanguage();try{const res=await fetch(url.href,{credentials:"same-origin"});if(!res.ok||!res.headers.get("content-type")?.includes("text/html"))return false;const nextDoc=new DOMParser().parseFromString(await res.text(),"text/html");if(!nextDoc.querySelector(".main"))return false;syncSidebar(nextDoc);swap(".header-left",nextDoc);swapTabs(nextDoc);swap(".main",nextDoc);syncStickyHeaderOffset();syncTocDisclosure();initCodeGroups();initMermaid();document.title=nextDoc.title;history[replace?"replaceState":"pushState"]({docs:true},"",url.href);setNavOpen(false);scrollTarget(url.hash);initTocScrollspy();return true}catch{return false}finally{navigating=false}}
function openSearch(){modal?.classList.add("open");setTimeout(()=>input?.focus(),0);pagefindReady ||= import(withBase("/pagefind/pagefind.js")).then(m=>m.init?.().then?.(()=>m)??m)}
function escapeHtml(text){return String(text).replace(/[&<>"']/g,ch=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[ch]))}
function trimTrailingPunctuation(value){return value.replace(/[.,;!?]+$/,"")}
Expand Down Expand Up @@ -114,6 +119,7 @@ initChat();
initCodeGroups();
initMermaid();
scrollActiveNavLink();
initTocScrollspy();
document.addEventListener("click",async e=>{const pageActions=e.target.closest(".page-actions");document.querySelectorAll(".page-actions-more[open]").forEach(menu=>{if(!pageActions?.contains(menu))menu.removeAttribute("open")});const toc=e.target.closest(".toc");document.querySelectorAll(".toc[open]").forEach(menu=>{if(compactTocQuery.matches&&!toc?.contains(menu))menu.open=false});if(e.target.closest(".toc a")&&compactTocQuery.matches)e.target.closest(".toc").open=false;if(handleDocsControlClick(e))return;const theme=e.target.closest("[data-theme-toggle]");if(theme){const next=root.dataset.theme==="dark"?"light":"dark";root.dataset.theme=next;localStorage.setItem("theme",next);initMermaid(true);return}const trigger=e.target.closest("[data-language-trigger]");if(trigger){e.stopPropagation();toggleLanguage();return}const picker=document.querySelector("[data-language-picker]");if(picker&&!picker.contains(e.target))closeLanguage();const navToggle=e.target.closest("[data-nav-toggle]");if(navToggle){setNavOpen(!document.body.classList.contains("nav-open"));return}if(e.target.closest("[data-nav-close]")){setNavOpen(false);return}if(document.body.classList.contains("nav-open")&&!e.target.closest(".sidebar")){setNavOpen(false);return}if(e.target.closest("[data-search-open]")){openSearch();return}if(e.target.closest("[data-search-close]")){modal?.classList.remove("open");return}const link=e.target.closest("a[href]");if(!link)return;if(link.closest("[data-language-picker]"))return;if(link.target||link.download||!isPlainLeftClick(e))return;const url=new URL(link.href,location.href);if(url.pathname===location.pathname&&url.search===location.search&&url.hash)return;if(!isDocsPage(url))return;e.preventDefault();modal?.classList.remove("open");const ok=await navigateTo(url);if(!ok)location.href=url.href});
modal?.addEventListener("click",e=>{if(e.target===modal)modal.classList.remove("open")});addEventListener("keydown",e=>{if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="k"){e.preventDefault();openSearch()}if(e.key==="Escape"){modal?.classList.remove("open");document.querySelectorAll(".page-actions-more[open]").forEach(menu=>menu.removeAttribute("open"));document.querySelectorAll(".toc[open]").forEach(menu=>{if(compactTocQuery.matches)menu.open=false});closeLanguage();setNavOpen(false)}});
addEventListener("popstate",()=>navigateTo(new URL(location.href),true));
Expand Down
9 changes: 9 additions & 0 deletions scripts/docs-site/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,15 @@ if (!/\.toc\{position:fixed;left:calc\(24px \+ 220px \+ 34px\);top:calc\(var\(--
|| !/\.toc\[open\] nav\{display:grid;gap:2px\}/.test(siteCss)) {
throw new Error("assets: compact table of contents dropdown is missing for mid-width pages");
}
if (!/let tocObserver=null/.test(siteJs)
|| !/function initTocScrollspy/.test(siteJs)
|| !/new IntersectionObserver/.test(siteJs)
|| !/rootMargin:"-120px 0px -70% 0px"/.test(siteJs)
|| !/scroller\.scrollTop\+innerHeight>=scroller\.scrollHeight-2/.test(siteJs)
|| !/scrollTarget\(url\.hash\);initTocScrollspy\(\)/.test(siteJs)
|| !/scrollActiveNavLink\(\);\s*initTocScrollspy\(\);\s*document\.addEventListener\("click"/.test(siteJs)) {
throw new Error("assets: table-of-contents scrollspy is missing");
}
if (!/function setNavOpen/.test(siteJs) || !/body\.nav-open:before/.test(siteCss) || !/data-nav-close/.test(index)) {
throw new Error("assets: mobile navigation drawer state is missing");
}
Expand Down
55 changes: 55 additions & 0 deletions scripts/docs-site/visual-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const browser = await chromium.launch({ headless: true });

try {
await checkDesktop();
await checkTocScrollspy();
await checkAmbientCodePage();
await checkMobile();
await checkLightMode();
Expand Down Expand Up @@ -376,6 +377,60 @@ async function checkDesktop() {
await page.close();
}

async function checkTocScrollspy() {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(`${base}/channels/discord`, { waitUntil: "networkidle" });
await page.locator(".toc a").first().waitFor({ state: "visible" });
const initial = await page.evaluate(() => ({
active: [...document.querySelectorAll(".toc a.active")].map((link) => link.hash),
tocCount: document.querySelectorAll(".toc a").length,
}));
if (initial.tocCount < 4 || initial.active.length !== 1) {
throw new Error(`toc initial active state failed: ${JSON.stringify(initial)}`);
}

const scrolled = await scrollToTocItem(page, 4);
if (!scrolled || scrolled.activeHash !== scrolled.expectedHash || scrolled.scrollY < 700) {
throw new Error(`toc scrollspy failed on long page: ${JSON.stringify(scrolled)}`);
}

await page.click('a.nav-link[href$="/channels/telegram"]');
await page.waitForURL("**/channels/telegram");
await page.locator(".toc a").first().waitFor({ state: "visible" });
const afterPjax = await scrollToTocItem(page, 2);
if (!afterPjax
|| afterPjax.pathname !== "/channels/telegram"
|| afterPjax.activeHash !== afterPjax.expectedHash
|| afterPjax.activeCount !== 1) {
throw new Error(`toc scrollspy failed after PJAX navigation: ${JSON.stringify(afterPjax)}`);
}
await page.close();
}

async function scrollToTocItem(page, index) {
const expected = await page.evaluate((targetIndex) => {
const links = [...document.querySelectorAll(".toc a")];
const items = links
.map((link) => ({ hash: link.hash, id: decodeURIComponent(link.hash.slice(1)) }))
.filter((item) => item.id && document.getElementById(item.id));
const item = items[Math.min(targetIndex, items.length - 1)];
document.getElementById(item?.id)?.scrollIntoView();
return item?.hash ?? null;
}, index);
if (!expected) return null;
await page.waitForFunction((hash) => [...document.querySelectorAll(".toc a.active")].some((link) => link.hash === hash), expected);
return page.evaluate((expectedHash) => {
const active = [...document.querySelectorAll(".toc a.active")];
return {
expectedHash,
activeHash: active[0]?.hash ?? null,
activeCount: active.length,
pathname: location.pathname,
scrollY,
};
}, expected);
}

async function checkMobile() {
const page = await browser.newPage({ viewport: { width: 390, height: 980 }, isMobile: true });
await page.goto(`${base}/__elements`, { waitUntil: "networkidle" });
Expand Down