PageSourceSearch

https://exe.dev/docs/_static/docs/docs.js?v=e5dfe267fc

js exe.dev collected 2026-10-02 10:27:41 UTC 24,443 bytes, 614 lines download raw bytes

1// docs.js — in-place navigation for hub.exe.dev/docs.
2//
3// Every docs URL is a complete server-rendered page. This script fetches the
4// next page on click, swaps the article in, updates the address bar, and
5// flips the sidebar between its two shapes:
6//
7//   overview  every section, its featured docs, then "… N more"
8//   section   one section in full, with a way back up to the overview
9//
10// The sidebar only changes shape when it has to: if the panel on screen
11// already lists the page you open, it stays put. The one page (/docs/all)
12// has a third shape, its own table of contents, which follows the reader.
13//
14// The API explorer is one of those sections. Its terminal is mounted the
15// first time you visit and then kept alive, hidden, while you read docs.
16
17const EXPLORER = "/api"; // the API explorer, a docs section at its own path
18const STEP = EXPLORER + "/step/"; // + a tour step's id
19
20const $ = (sel, root = document) => root.querySelector(sel);
21const docView = $("#doc-view");
22const explorerView = $("#explorer-view");
23const sidebar = $("#docs-sidebar");
24const searchForm = $(".docs-search");
25const searchInput = searchForm?.querySelector("input");
26let nav = $("#docs-nav");
27
28let explorer = null;
29let explorerLoading = null;
30let navToken = 0;
31let pageKey = location.pathname + location.search;
32const prefetched = new Map(); // url -> { at, page: Promise<{url, html}> }
33
34const isExplorerPath = (p) => p === EXPLORER || p.startsWith(STEP);
35const stepOf = (p) => (p.startsWith(STEP) ? p.slice(STEP.length) : "");
36
37// The explorer keeps the latest command it ran in ?cmd=, so the address bar
38// is a link that runs it again. Spaces read as +, and flags keep their =.
39function withCommand(url, command) {
40  const params = new URLSearchParams(url.search);
41  params.delete("cmd");
42  const rest = params.toString();
43  const value = encodeURIComponent(command).replace(/%20/g, "+").replace(/%(3D|2C|2F|3A|40)/g, (m) => decodeURIComponent(m));
44  url.search = (rest ? rest + "&" : "") + "cmd=" + value;
45  return url;
46}
47
48// Opening an explorer URL: a tour step shows its tip and types its command,
49// and ?cmd= types that command instead. Back and forward only show the tip.
50function openExplorerAt(ex, url, fromPop) {
51  const step = stepOf(url.pathname);
52  const cmd = fromPop ? "" : url.searchParams.get("cmd")?.trim() || "";
53  if (step) ex.selectStep(step, { run: !fromPop && !cmd });
54  if (cmd) ex.typeAndRun(cmd);
55}
56
57// Only docs pages and the explorer are routed in place; markdown, images,
58// and the rest of the hub load normally.
59function routable(url) {
60  if (url.origin !== location.origin) return false;
61  const p = url.pathname;
62  if (p !== "/docs" && !p.startsWith("/docs/") && !isExplorerPath(p)) return false;
63  if (p.startsWith("/docs/_")) return false; // assets, and the admins' editor
64  return !p.slice(p.lastIndexOf("/") + 1).includes(".");
65}
66
67// ------------------------------------------------------------ sidebar
68
69const isTOC = (n) => n.hasAttribute("data-toc");
70
71function showPanel(section, { resetScroll = false } = {}) {
72  let shown = null;
73  for (const panel of nav.querySelectorAll(".nav-section")) {
74    const on = Boolean(section) && panel.dataset.section === section;
75    panel.hidden = !on;
76    if (on) shown = panel;
77  }
78  const overview = $(".nav-overview", nav);
79  if (overview) overview.hidden = Boolean(shown);
80  if (resetScroll) sidebar.scrollTop = 0;
81  return shown || overview || nav;
82}
83
84// Marks the sidebar's link to path. Links to #anchors are places on this
85// page, not pages; the table of contents marks those as you scroll.
86function markActive(path) {
87  const links = document.querySelectorAll("#docs-nav a[href], .explorer-steps a[href]");
88  for (const a of links) {
89    if (a.hasAttribute("data-nav-back") || a.dataset.navPeek || a.getAttribute("href").startsWith("#")) continue;
90    const on = new URL(a.href, location.href).pathname === path;
91    a.classList.toggle("active", on);
92    if (on && !a.classList.contains("sidebar-heading")) a.setAttribute("aria-current", "page");
93    else a.removeAttribute("aria-current");
94  }
95}
96
97// The phone-sized tour rail scrolls sideways; keep the current step in view
98// without ever scrolling the page.
99function revealStepChip() {
100  const rail = $(".explorer-steps");
101  const chip = rail?.querySelector("a.active")?.parentElement;
102  if (!chip || rail.offsetParent === null) return;
103  if (chip.offsetLeft < rail.scrollLeft || chip.offsetLeft + chip.offsetWidth > rail.scrollLeft + rail.clientWidth) {
104    rail.scrollLeft = chip.offsetLeft - 12;
105  }
106}
107
108function revealActive(panel) {
109  const link = panel.querySelector("a.active:not(.sidebar-heading)");
110  if (!link) return;
111  const box = sidebar.getBoundingClientRect();
112  const r = link.getBoundingClientRect();
113  if (r.top < box.top || r.bottom > box.bottom) {
114    sidebar.scrollTop = Math.max(0, sidebar.scrollTop + r.top - box.top - sidebar.clientHeight / 3);
115  }
116}
117
118// ------------------------------------------------------------ one page
119
120// On the one page the sidebar lists every doc; mark the one being read and
121// keep it in view. toc is [{ link, target }] in page order, or null.
122let toc = null;
123let tocFrame = 0;
124
125function followTOC() {
126  toc = null;
127  if (!isTOC(nav)) return;
128  toc = [...nav.querySelectorAll('a.sidebar-link[href^="#"]')]
129    .map((link) => ({ link, target: document.getElementById(decodeURIComponent(link.hash.slice(1))) }))
130    .filter((t) => t.target);
131  markReading();
132}
133
134function markReading() {
135  tocFrame = 0;
136  if (!toc) return;
137  // The doc whose heading last scrolled past the top bar is the one on screen.
138  const line = $(".docs-topbar").offsetHeight + 40;
139  let reading = toc[0];
140  for (const t of toc) {
141    if (t.target.getBoundingClientRect().top > line) break;
142    reading = t;
143  }
144  for (const { link } of toc) {
145    const on = link === reading?.link;
146    if (link.classList.contains("active") === on) continue;
147    link.classList.toggle("active", on);
148    if (on) link.setAttribute("aria-current", "location");
149    else link.removeAttribute("aria-current");
150  }
151  if (reading) revealActive(nav);
152}
153
154addEventListener("scroll", () => {
155  if (toc && !tocFrame) tocFrame = requestAnimationFrame(markReading);
156}, { passive: true });
157
158const visiblePanel = () => nav.querySelector(".nav-overview:not([hidden]), .nav-section:not([hidden])");
159const panelKey = (panel) => (panel?.classList.contains("nav-section") ? panel.dataset.section : "");
160
161// Does this panel list the page as an entry (not as a heading or "more")?
162function lists(panel, path) {
163  return [...panel.querySelectorAll("a.sidebar-link[href]")].some((a) => !a.dataset.navPeek && a.pathname === path);
164}
165
166// The panel to show for a page: the one on screen if it already lists the
167// page, otherwise the server's choice for it.
168function panelFor(path, suggested) {
169  const current = visiblePanel();
170  return current && lists(current, path) ? panelKey(current) : suggested;
171}
172
173function applyNav(section, path) {
174  const before = nav.dataset.section;
175  nav.dataset.section = section || "";
176  const panel = showPanel(section, { resetScroll: before !== nav.dataset.section });
177  markActive(path);
178  revealActive(panel);
179  revealStepChip();
180}
181
182// "All sections": back up to the overview without leaving the page, with
183// the section you were in scrolled into view.
184function showOverview() {
185  const current = nav.dataset.section;
186  nav.dataset.section = "";
187  const overview = showPanel("");
188  const heading = [...overview.querySelectorAll(".sidebar-heading")]
189    .find((a) => a.pathname === (current === "api-explorer" ? EXPLORER : "/docs/section/" + current));
190  if (heading) {
191    sidebar.scrollTop = Math.max(0, heading.offsetTop - sidebar.offsetTop - 60);
192    heading.focus({ preventScroll: true });
193  }
194}
195
196// "… N more": open one section in the sidebar; the article stays put.
197function peekSection(section) {
198  nav.dataset.section = section;
199  const panel = showPanel(section, { resetScroll: true });
200  $(".nav-back", panel)?.focus({ preventScroll: true });
201}
202
203// ------------------------------------------------------------ navigation
204
205function saveScroll() {
206  history.replaceState({ ...(history.state || {}), docs: true, scrollY: window.scrollY }, "");
207}
208
209function fetchPage(url) {
210  const key = url.pathname + url.search;
211  const hit = prefetched.get(key);
212  if (hit && Date.now() - hit.at < 15000) {
213    prefetched.delete(key);
214    return hit.page;
215  }
216  return fetch(url, { credentials: "same-origin", headers: { "X-Docs-Navigation": "1" } }).then(async (res) => {
217    const type = res.headers.get("content-type") || "";
218    if (!type.includes("text/html")) throw new Error("not a page");
219    return { url: res.url, html: await res.text() };
220  });
221}
222
223function prefetch(url) {
224  const key = url.pathname + url.search;
225  if (key === pageKey || prefetched.has(key)) return;
226  const page = fetchPage(url);
227  page.catch(() => prefetched.delete(key));
228  prefetched.set(key, { at: Date.now(), page });
229}
230
231async function navigate(url, { push = true, replace = false, fromPop = false, scrollY = 0 } = {}) {
232  const token = ++navToken;
233  if (!fromPop) saveScroll();
234
235  // Moving between tour steps needs no server round trip.
236  if (isExplorerPath(url.pathname) && document.body.dataset.view === "explorer" && explorer) {
237    commit(url, { push, replace });
238    const overview = $(".nav-overview", nav);
239    applyNav(panelFor(url.pathname, lists(overview, url.pathname) ? "" : "api-explorer"), url.pathname);
240    const title = [...nav.querySelectorAll(".nav-step")].find((a) => a.pathname === url.pathname)?.querySelector(".nav-step-title")?.textContent;
241    document.title = title ? `API explorer: ${title} - exe.dev docs` : "API explorer - exe.dev docs";
242    openExplorerAt(explorer, url, fromPop);
243    return;
244  }
245
246  document.body.classList.add("docs-loading");
247  let page;
248  try {
249    page = await fetchPage(url);
250  } catch {
251    location.assign(url.href);
252    return;
253  } finally {
254    if (token === navToken) document.body.classList.remove("docs-loading");
255  }
256  if (token !== navToken) return;
257
258  const finalURL = new URL(page.url || url.href);
259  finalURL.hash = url.hash;
260  const doc = new DOMParser().parseFromString(page.html, "text/html");
261  if (!routable(finalURL) || !doc.getElementById("docs-main")) {
262    location.assign(finalURL.href);
263    return;
264  }
265  commit(finalURL, { push, replace });
266  await render(doc, finalURL, { fromPop, scrollY });
267}
268
269function commit(url, { push, replace }) {
270  const state = { docs: true, scrollY: 0 };
271  if (push && url.href !== location.href) history.pushState(state, "", url);
272  else if (replace || url.href !== location.href) history.replaceState(state, "", url);
273  pageKey = url.pathname + url.search;
274}
275
276function syncHead(doc) {
277  document.title = doc.title;
278  for (const sel of ['meta[name="description"]', 'link[rel="canonical"]', 'link[rel="alternate"][type="text/markdown"]:not([href="/llms.txt"])']) {
279    const next = doc.head.querySelector(sel);
280    const cur = document.head.querySelector(sel);
281    if (cur && next) cur.replaceWith(next);
282    else if (cur) cur.remove();
283    else if (next) document.head.append(next);
284  }
285  // The sidebar's own foot (One Page or Many Pages), not a section panel's.
286  const foot = doc.querySelector("#docs-sidebar > .sidebar-foot");
287  if (foot) $("#docs-sidebar > .sidebar-foot").innerHTML = foot.innerHTML;
288}
289
290function setView(view) {
291  const was = document.body.dataset.view;
292  document.body.dataset.view = view;
293  docView.hidden = view === "explorer";
294  explorerView.hidden = view !== "explorer";
295  if (was === "explorer" && view !== "explorer") explorer?.deactivate();
296}
297
298async function render(doc, url, { fromPop, scrollY }) {
299  toc = null;
300  // Take the fresh sidebar when a docs deploy landed while this tab was
301  // open, or when moving onto or off the one page, whose sidebar is its
302  // table of contents.
303  const version = doc.body.dataset.docsVersion || "";
304  const fresh = doc.getElementById("docs-nav");
305  const suggested = fresh?.dataset.section || ""; // read before fresh moves out of doc
306  const reshaped = fresh && isTOC(fresh) !== isTOC(nav);
307  if (fresh && (reshaped || version !== (document.body.dataset.docsVersion || ""))) {
308    nav.replaceWith(document.adoptNode(fresh));
309    nav = $("#docs-nav");
310    document.body.dataset.docsVersion = version;
311    if (reshaped) sidebar.scrollTop = 0;
312  }
313  syncHead(doc);
314  const view = doc.body.dataset.view;
315  applyNav(panelFor(url.pathname, suggested), url.pathname);
316
317  if (view === "explorer") {
318    if (!explorerView.firstElementChild) {
319      explorerView.replaceChildren(...[...doc.getElementById("explorer-view").childNodes].map((n) => document.adoptNode(n)));
320    }
321    setView("explorer");
322    window.scrollTo(0, 0);
323    const ex = await loadExplorer();
324    ex.activate();
325    markActive(url.pathname);
326    revealStepChip();
327    openExplorerAt(ex, url, fromPop);
328    return;
329  }
330
331  const refocus = document.activeElement?.closest?.(".search-page-form") ? document.activeElement : null;
332  const caret = refocus ? [refocus.selectionStart, refocus.selectionEnd] : null;
333  docView.replaceChildren(...[...doc.getElementById("doc-view").childNodes].map((n) => document.adoptNode(n)));
334  setView(view);
335  enhance(docView);
336
337  if (refocus) {
338    const input = $(".search-page-form input", docView);
339    input?.focus({ preventScroll: true });
340    if (input && caret) input.setSelectionRange(...caret);
341  }
342  if (document.activeElement !== searchInput && searchInput) {
343    searchInput.value = view === "search" ? url.searchParams.get("q") || "" : "";
344  }
345
346  const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
347  if (target) target.scrollIntoView();
348  else window.scrollTo(0, fromPop ? scrollY : 0);
349  followTOC();
350}
351
352function loadExplorer() {
353  if (explorer) return Promise.resolve(explorer);
354  explorerLoading ||= import(explorerView.dataset.module).then((mod) => (explorer = mod));
355  return explorerLoading;
356}
357
358// ------------------------------------------------------------ doc content
359
360const COPY_ICON = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
361const CHECK_ICON = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
362
363async function copyText(text) {
364  try {
365    await navigator.clipboard.writeText(text);
366  } catch {
367    const ta = Object.assign(document.createElement("textarea"), { value: text });
368    ta.style.cssText = "position:fixed;top:0;left:0;opacity:0";
369    document.body.append(ta);
370    ta.select();
371    document.execCommand("copy");
372    ta.remove();
373  }
374}
375
376function enhance(root) {
377  for (const content of root.querySelectorAll(".doc-content")) {
378    for (const code of content.querySelectorAll("pre > code")) {
379      const pre = code.parentElement;
380      if (pre.parentElement.classList.contains("copy-code-wrap")) continue;
381      const wrap = document.createElement("div");
382      wrap.className = "copy-code-wrap";
383      pre.replaceWith(wrap);
384      wrap.append(pre);
385      const btn = document.createElement("button");
386      btn.type = "button";
387      btn.className = "copy-code-button";
388      btn.setAttribute("aria-label", "Copy code");
389      btn.innerHTML = COPY_ICON;
390      btn.addEventListener("click", async () => {
391        await copyText(code.textContent);
392        btn.innerHTML = CHECK_ICON;
393        btn.classList.add("is-copied");
394        setTimeout(() => {
395          btn.innerHTML = COPY_ICON;
396          btn.classList.remove("is-copied");
397        }, 2000);
398      });
399      wrap.append(btn);
400    }
401    for (const h of content.querySelectorAll("h1[id], h2[id], h3[id], h4[id]")) {
402      if (h.querySelector(".anchor-link")) continue;
403      const a = Object.assign(document.createElement("a"), { href: "#" + h.id, className: "anchor-link", textContent: "#" });
404      a.setAttribute("aria-label", "Link to this section");
405      h.append(a);
406    }
407    for (const el of content.querySelectorAll("#dns-checker")) mountDNSChecker(el);
408    for (const el of content.querySelectorAll("#command-quoter")) mountQuoter(el);
409  }
410  const md = $("[data-copy-markdown]", root);
411  md?.addEventListener("click", async () => {
412    const res = await fetch(md.dataset.copyMarkdown);
413    if (!res.ok) return;
414    await copyText(await res.text());
415    md.classList.add("copied");
416    md.dataset.tooltip = "Copied!";
417    setTimeout(() => {
418      md.classList.remove("copied");
419      md.dataset.tooltip = "Copy as Markdown";
420    }, 2000);
421  });
422}
423
424// The DNS checker asks exed's /api/dns-check, so it only works where an exe
425// stack serves these pages (they carry data-public-host). On the hub, point
426// at exe.dev's copy, keeping a support link's ?domain=&vm=.
427function mountDNSChecker(el) {
428  if (document.body.dataset.publicHost) {
429    import("./dns-check.js").then((m) => m.mountDNSChecker(el));
430    return;
431  }
432  const there = new URL("https://exe.dev/docs/cnames#check-your-dns");
433  const here = new URLSearchParams(location.search);
434  for (const k of ["domain", "vm"]) if (here.has(k)) there.searchParams.set(k, here.get(k));
435  const note = Object.assign(document.createElement("p"), { className: "doc-widget-note" });
436  note.append("The DNS checker runs on exe.dev: ", Object.assign(document.createElement("a"), { href: there.href, textContent: "check your DNS there" }), ".");
437  el.replaceWith(note);
438}
439
440// A command bound for a VM through POST /exec is parsed twice: by the
441// exe.dev lobby's shell lexer, then by the VM's shell. Show both layers.
442function shellWord(s) {
443  if (s === "") return "''";
444  if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(s)) return s;
445  if (!s.includes("'")) return `'${s}'`;
446  if (!/["\\$`]/.test(s)) return `"${s}"`;
447  return `'${s.replace(/'/g, `'\\''`)}'`;
448}
449
450function mountQuoter(el) {
451  const examples = [
452    ["simple", "whoami"],
453    ["pipeline", "cd /srv && ls | head -3"],
454    ["quoted", "echo 'hello world'"],
455    ["both quotes", `printf '%s\\n' "it's ready"`],
456    ["detached", "setsid nohup ./build.sh > /tmp/build.log 2>&1 & echo started"],
457  ];
458  const box = document.createElement("div");
459  box.className = "command-quoter";
460  box.innerHTML = `<label for="quoter-command">Command to run on the VM</label>
461    <textarea id="quoter-command" rows="2" spellcheck="false"></textarea>
462    <div class="command-quoter-examples"></div>
463    <div class="command-quoter-out" aria-live="polite">
464      <strong>ssh exe.dev</strong><pre><code data-out="lobby"></code></pre>
465      <strong>curl</strong><pre><code data-out="curl"></code></pre>
466    </div>`;
467  const input = $("textarea", box);
468  const update = () => {
469    const lobby = `ssh my-vm ${shellWord(input.value)}`;
470    $('[data-out="lobby"]', box).textContent = lobby;
471    $('[data-out="curl"]', box).textContent =
472      `curl -X POST https://exe.dev/exec \\\n     -H "Authorization: Bearer $TOKEN" \\\n     -d ${shellWord(lobby)}`;
473  };
474  for (const [label, cmd] of examples) {
475    const b = Object.assign(document.createElement("button"), { type: "button", textContent: label });
476    b.addEventListener("click", () => { input.value = cmd; update(); });
477    $(".command-quoter-examples", box).append(b);
478  }
479  input.value = examples[3][1];
480  input.addEventListener("input", update);
481  update();
482  el.replaceWith(box);
483}
484
485// ------------------------------------------------------------ wiring
486
487document.addEventListener("click", (e) => {
488  if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
489  const a = e.target.closest?.("a[href]");
490  if (!a || (a.target && a.target !== "_self") || a.hasAttribute("download")) return;
491  if (a.hasAttribute("data-nav-back")) {
492    e.preventDefault();
493    showOverview();
494    return;
495  }
496  if (a.dataset.navPeek) {
497    e.preventDefault();
498    peekSection(a.dataset.navPeek);
499    return;
500  }
501  const url = new URL(a.href, location.href);
502  if (!routable(url)) return;
503  if (url.pathname + url.search === pageKey && url.hash) return; // in-page anchor
504  e.preventDefault();
505  navigate(url);
506});
507
508document.addEventListener("pointerover", (e) => {
509  const a = e.target.closest?.("a[href]");
510  if (!a || a.dataset.navPeek || a.hasAttribute("data-nav-back") || e.pointerType === "touch") return;
511  const url = new URL(a.href, location.href);
512  if (routable(url) && !isExplorerPath(url.pathname)) prefetch(url);
513});
514
515addEventListener("popstate", (e) => {
516  const url = new URL(location.href);
517  if (url.pathname + url.search === pageKey) {
518    const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
519    if (target) target.scrollIntoView();
520    return;
521  }
522  navigate(url, { push: false, fromPop: true, scrollY: e.state?.scrollY || 0 });
523});
524
525// Searching from the sidebar (or the search page on phones) updates the
526// results as you type.
527function searchURL(q) {
528  const url = new URL("/docs/search", location.origin);
529  if (q.trim()) url.searchParams.set("q", q.trim());
530  return url;
531}
532let searchTimer = 0;
533function onSearchInput(value, immediate) {
534  clearTimeout(searchTimer);
535  const onSearch = location.pathname === "/docs/search";
536  if (!value.trim() && !onSearch) return;
537  const go = () => navigate(searchURL(value), { push: !onSearch, replace: onSearch });
538  if (immediate) go();
539  else searchTimer = setTimeout(go, 160);
540}
541searchInput?.addEventListener("input", () => onSearchInput(searchInput.value, false));
542document.addEventListener("input", (e) => {
543  if (e.target.closest?.(".search-page-form")) onSearchInput(e.target.value, false);
544});
545document.addEventListener("submit", (e) => {
546  const form = e.target.closest?.(".docs-search, .search-page-form");
547  if (!form) return;
548  e.preventDefault();
549  onSearchInput($("input", form).value, true);
550});
551
552document.addEventListener("keydown", (e) => {
553  if (e.key === "Escape" && e.target === searchInput) {
554    searchInput.blur();
555    return;
556  }
557  if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return;
558  if (document.body.dataset.view === "explorer") return; // the terminal wants slashes
559  const t = e.target;
560  if (t.closest?.("input, textarea, select, [contenteditable]")) return;
561  e.preventDefault();
562  if (searchInput && sidebar.offsetParent !== null) searchInput.focus();
563  else navigate(searchURL(""));
564});
565
566explorerView.addEventListener("explorer:command", (e) => {
567  if (!isExplorerPath(location.pathname)) return;
568  commit(withCommand(new URL(location.href), e.detail.command), { replace: true });
569});
570
571explorerView.addEventListener("explorer:reset", () => {
572  if (isExplorerPath(location.pathname) && (stepOf(location.pathname) || location.search)) {
573    commit(new URL(EXPLORER, location.origin), { replace: true });
574    document.title = "API explorer - exe.dev docs";
575  }
576  markActive(EXPLORER);
577});
578
579// Sign in brings you back to where you are now, even after moves that
580// fetched no page (tour steps, anchors). data-login names the parameter:
581// "redirect" for the hub's login, "next" for the hub's /signin on exe.dev.
582const login = $("[data-login]");
583function aimLogin() {
584  const u = new URL(login.href);
585  u.searchParams.set(login.dataset.login, location.pathname + location.search + location.hash);
586  login.href = u.href;
587}
588for (const ev of ["pointerenter", "focus", "click"]) login?.addEventListener(ev, aimLogin);
589
590// ------------------------------------------------------------ boot
591
592history.scrollRestoration = "manual";
593// An admin's "committed" notice is for this visit, not for the address bar.
594if (new URLSearchParams(location.search).has("saved")) {
595  const clean = new URL(location.href);
596  clean.searchParams.delete("saved");
597  history.replaceState(history.state, "", clean);
598  pageKey = clean.pathname + clean.search;
599}
600history.replaceState({ ...(history.state || {}), docs: true }, "");
601enhance(docView);
602revealActive(nav.querySelector(".nav-section:not([hidden])") || nav);
603{
604  const target = location.hash && document.getElementById(decodeURIComponent(location.hash.slice(1)));
605  if (target) target.scrollIntoView();
606}
607followTOC();
608if (document.body.dataset.view === "explorer") {
609  loadExplorer().then((ex) => {
610    revealStepChip();
611    ex.activate();
612    openExplorerAt(ex, new URL(location.href), false);
613  });
614}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.