/* * Version picker widget for the multi-version MCP C# SDK docs site. * * Configuration is provided per-page via a `window.__DOCS__` object that * scripts/inject-version-picker.mjs writes into each page's : * * window.__DOCS__ = { * version: "2.0", // slug of the version this page belongs to * base: "/", // site base path ("/" for a custom domain) * default: "1.x", // slug the site root redirects to * versions: [ // newest first; drives the dropdown * { slug: "2.0", label: "2.0 (preview)", prerelease: true }, * { slug: "1.x", label: "1.x (latest)", prerelease: false } * ] * }; * * The pure helpers (joinPath / stripVersionPrefix / targetPath) are exported so * they can be unit-tested in Node without a DOM. */ export function joinPath(a, b) { const left = String(a || "").replace(/\/+$/, ""); const right = String(b == null ? "" : b).replace(/^\/+/, ""); return (left + "/" + right).replace(/\/{2,}/g, "/"); } // Given the current pathname, return the part after "//". export function stripVersionPrefix(pathname, base, currentSlug) { const prefix = joinPath(joinPath(base, currentSlug), ""); if (pathname.startsWith(prefix)) return pathname.slice(prefix.length); // Tolerate a missing trailing slash (e.g. "/2.0"). const bare = joinPath(base, currentSlug); if (pathname === bare) return ""; return ""; } // Derive the deployment base from the page URL. This keeps picker navigation // within project Pages sites, whose content is hosted below "//". export function getBasePath(cfg, pathname) { const marker = `/${cfg.version}/`; const markerIndex = pathname.indexOf(marker); if (markerIndex !== -1) return pathname.slice(0, markerIndex + 1) || "/"; const bareMarker = `/${cfg.version}`; if (pathname.endsWith(bareMarker)) { return pathname.slice(0, -bareMarker.length + 1) || "/"; } return cfg.base || "/"; } // Compute the equivalent path in another version, preserving the sub-page. export function targetPath(cfg, targetSlug, pathname) { const base = getBasePath(cfg, pathname); const sub = stripVersionPrefix(pathname, base, cfg.version); return joinPath(joinPath(base, targetSlug), sub); } function h(tag, attrs, children) { const el = document.createElement(tag); for (const k in attrs || {}) { if (k === "class") el.className = attrs[k]; else el.setAttribute(k, attrs[k]); } for (const c of children || []) { el.appendChild(typeof c === "string" ? document.createTextNode(c) : c); } return el; } function versionRoot(cfg, slug, pathname) { return joinPath(joinPath(getBasePath(cfg, pathname), slug), ""); } async function navigate(cfg, slug) { const candidate = targetPath(cfg, slug, location.pathname); const home = versionRoot(cfg, slug, location.pathname); // Best effort: keep the reader on the same page if it exists in the target // version; otherwise fall back to that version's landing page. try { const res = await fetch(candidate, { method: "HEAD" }); if (res.ok) { location.assign(candidate + location.hash); return; } } catch (_) { /* HEAD may be blocked (e.g. file://) -- fall through to the version home. */ } location.assign(home); } function previewBadge() { return h("span", { class: "dv-picker__badge" }, ["preview"]); } function buildPicker(cfg) { const current = cfg.versions.find((v) => v.slug === cfg.version); const select = h("select", { class: "dv-picker__select", "aria-label": "Select documentation version", }); for (const v of cfg.versions) { const opt = h("option", { value: v.slug }, [v.label || v.slug]); if (v.slug === cfg.version) opt.selected = true; select.appendChild(opt); } select.addEventListener("change", () => navigate(cfg, select.value)); const children = [h("span", { class: "dv-picker__label" }, ["Version"]), select]; if (current && current.prerelease) { children.push(previewBadge()); } return h("div", { class: "dv-picker" }, children); } function buildPrereleaseBanner() { return h("div", { class: "dv-prerelease-banner", role: "status", }, [ "You are currently viewing", previewBadge(), "documentation.", ]); } function mount(picker) { const brand = document.querySelector(".navbar-brand"); if (brand && brand.parentNode) { picker.classList.add("dv-picker--navbar"); brand.parentNode.insertBefore(picker, brand.nextSibling); return; } picker.classList.add("dv-picker--floating"); document.body.appendChild(picker); } function init() { const cfg = window.__DOCS__; if (!cfg || !Array.isArray(cfg.versions) || cfg.versions.length < 2) return; if (document.querySelector(".dv-picker, .dv-prerelease-banner")) return; // idempotent const current = cfg.versions.find((v) => v.slug === cfg.version); if (!current) return; if (current.prerelease) { document.body.prepend(buildPrereleaseBanner()); } mount(buildPicker(cfg)); } if (typeof document !== "undefined" && typeof document.createElement === "function") { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } }