Files
documentation/assets/app.js
2026-10-09 19:57:06 -07:00

1168 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var THEME_KEY = "vyla-theme";
var ICONS = {
system: '<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>',
light: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/>',
dark: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
search: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>',
menu: '<path d="M4 6h16M4 12h10M4 18h16"/>',
doc: '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h6"/>',
hash: '<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>',
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/>',
check: '<path d="m5 12 5 5 9-10"/>',
chevron: '<path d="m6 9 6 6 6-6"/>',
chevronLeft: '<path d="m15 18-6-6 6-6"/>',
chevronRight: '<path d="m9 18 6-6-6-6"/>'
};
var SITE_TREE = [
{
id: "vyla",
title: "Vyla",
path: "/start-here/introduction",
groups: [
{
title: "Start here",
items: [
{ title: "Introduction", file: "/start-here/introduction.md", path: "/start-here/introduction" },
{ title: "Architecture", file: "/start-here/architecture.md", path: "/start-here/architecture" }
]
},
{
title: "Products",
items: [
{ title: "Home", file: "/products/home.md", path: "/products/home" },
{ title: "Vyla Go", file: "/products/go.md", path: "/products/go" },
{ title: "Tv", file: "/products/tv.md", path: "/products/tv" },
{ title: "Player", file: "/products/player.md", path: "/products/player" }
]
},
{
title: "Concepts",
items: [
{ title: "Providers", file: "/concepts/providers.md", path: "/concepts/providers" }
]
}
]
},
{
id: "sdk",
title: "SDK",
path: "/sdk",
groups: [
{
title: "Vyla SDK",
items: [
{ title: "Sdk", file: "/sdk/vyla-sdk/index.md", path: "/sdk" },
{ title: "Sdk Quickstart", file: "/sdk/vyla-sdk/quickstart.md", path: "/sdk/vyla-sdk/quickstart" }
]
}
]
},
{
id: "stream-api",
title: "Stream API",
path: "/stream-api/overview",
groups: [
{
title: "Overview",
items: [
{ title: "Overview", file: "/stream-api/overview/index.md", path: "/stream-api/overview" }
]
},
{
title: "Reference",
items: [
{ title: "Paths", file: "/stream-api/reference/paths.md", path: "/stream-api/reference/paths" },
{ title: "Media Data", file: "/stream-api/reference/media-data.md", path: "/stream-api/reference/media-data" },
{ title: "Operations", file: "/stream-api/reference/operations.md", path: "/stream-api/reference/operations" }
]
},
{
title: "Deployment",
items: [
{ title: "Self Hosting", file: "/stream-api/deployment/self-hosting.md", path: "/stream-api/deployment/self-hosting" }
]
}
]
},
{
id: "misc",
title: "Misc",
path: "/misc/project/credit",
groups: [
{
title: "Project",
items: [
{ title: "Credit", file: "/misc/project/credit.md", path: "/misc/project/credit" },
{ title: "DMCA", file: "/misc/project/DMCA.md", path: "/misc/project/DMCA" }
]
}
]
}
];
var searchData = null;
var searchResults = [];
var searchActive = 0;
function icon(name) {
return '<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + ICONS[name] + '</svg>';
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
function getThemeMode() {
var v = localStorage.getItem(THEME_KEY);
return v === "light" || v === "dark" ? v : "system";
}
function applyTheme() {
var mode = getThemeMode();
var dark = mode === "dark" || (mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", dark);
var logo = document.getElementById("brand-logo");
if (logo) logo.src = dark ? "/assets/images/logo.png" : "/assets/images/dark_logo.png";
}
function setTheme(mode) {
if (mode === "system") localStorage.removeItem(THEME_KEY);
else localStorage.setItem(THEME_KEY, mode);
applyTheme();
refreshThemeUI();
var menu = document.getElementById("theme-menu");
if (menu) menu.classList.remove("open");
}
function toggleThemeMenu(e) {
e.stopPropagation();
var menu = document.getElementById("theme-menu");
if (menu) menu.classList.toggle("open");
}
function refreshThemeUI() {
var mode = getThemeMode();
var btn = document.getElementById("theme-toggle");
if (btn) btn.innerHTML = icon(mode);
document.querySelectorAll("#theme-menu button, #theme-seg button").forEach(function (b) {
b.classList.toggle("on", b.getAttribute("data-mode") === mode);
});
}
function buildThemeControls() {
var btn = document.getElementById("theme-toggle");
var links = document.querySelector("#navbar .links");
if (!btn || !links) return;
var wrap = document.createElement("div");
wrap.className = "theme-wrap";
btn.parentNode.insertBefore(wrap, btn);
wrap.appendChild(btn);
var modes = ["system", "light", "dark"];
var menu = document.createElement("div");
menu.id = "theme-menu";
menu.innerHTML = modes.map(function (m) {
return '<button type="button" data-mode="' + m + '">' + icon(m) + '<span>' + m.charAt(0).toUpperCase() + m.slice(1) + '</span><i class="check">✓</i></button>';
}).join("");
wrap.appendChild(menu);
menu.addEventListener("click", function (e) {
var b = e.target.closest("button");
if (b) setTheme(b.getAttribute("data-mode"));
});
refreshThemeUI();
}
function dismissBanner() {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
localStorage.setItem("vyla-banner-dismissed", "true");
var header = document.getElementById("header-container");
if (header) document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
}
function toggleSidebar() {
var sidebar = document.getElementById("sidebar");
var nav = document.getElementById("navbar");
var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom));
sidebar.style.setProperty("--sidebar-top", top + "px");
var open = sidebar.classList.toggle("open");
document.body.classList.toggle("nav-open", open);
}
function cgSwitch(btn) {
var group = btn.closest(".code-group");
var index = btn.getAttribute("data-tab-index");
group.querySelectorAll(".code-group-tab").forEach(function (t) {
t.classList.toggle("active", t === btn);
});
group.querySelectorAll(".code-group-panel").forEach(function (p, i) {
p.classList.toggle("active", String(i) === index);
});
window.dispatchEvent(new Event("resize"));
}
function parseMarkdown(md) {
var lines = md.replace(/\r\n/g, "\n").split("\n");
var out = [];
var inPre = false;
var preLang = "";
var preCode = [];
var inTable = false;
var tableRows = [];
var inList = false;
var listType = "ul";
var inCodeGroup = false;
var codeGroupTabs = [];
var codeGroupPanels = [];
function flushList() {
if (inList) {
out.push("</" + listType + ">");
inList = false;
}
}
function flushTable() {
if (inTable) {
var html = '<div class="table-wrap"><table>';
if (tableRows.length > 0) {
html += "<thead><tr>";
var ths = tableRows[0];
for (var i = 0; i < ths.length; i++) html += "<th>" + parseInline(ths[i]) + "</th>";
html += "</tr></thead>";
}
if (tableRows.length > 1) {
html += "<tbody>";
for (var r = 1; r < tableRows.length; r++) {
html += "<tr>";
var tds = tableRows[r];
for (var c = 0; c < tds.length; c++) html += "<td>" + parseInline(tds[c]) + "</td>";
html += "</tr>";
}
html += "</tbody>";
}
html += "</table></div>";
out.push(html);
inTable = false;
tableRows = [];
}
}
function slugify(text) {
return text.toLowerCase().replace(/<[^>]+>/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
function parseInline(text) {
return text
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (_, alt, src) {
var cls = src.indexOf("devices.png") !== -1 || src.indexOf("site_preview.png") !== -1 || src.indexOf("site-preview.png") !== -1 ? ' class="vyla-banner"' : "";
return '<img src="' + src + '" alt="' + esc(alt) + '" loading="lazy" decoding="async" fetchpriority="low"' + cls + " />";
})
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (_, label, href) {
return '<a href="' + href + '">' + label + "</a>";
})
.replace(/`([^`]+)`/g, function (_, code) {
return "<code>" + esc(code) + "</code>";
})
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>");
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith(":::code-group")) {
flushList();
flushTable();
inCodeGroup = true;
codeGroupTabs = [];
codeGroupPanels = [];
continue;
}
if (inCodeGroup && line === ":::") {
flushList();
flushTable();
var cgHtml = '<div class="code-group"><div class="code-group-tabs">';
codeGroupTabs.forEach(function (tab, idx) {
var activeClass = idx === 0 ? " active" : "";
cgHtml += '<button class="code-group-tab' + activeClass + '" data-tab-index="' + idx + '" type="button" onclick="cgSwitch(this)">' + esc(tab) + '</button>';
});
cgHtml += '</div><div class="code-group-panels">';
codeGroupPanels.forEach(function (panel, idx) {
var activeClass = idx === 0 ? " active" : "";
var langClass = panel.lang ? ' class="language-' + panel.lang + '"' : ' class="language-none"';
cgHtml += '<div class="code-group-panel' + activeClass + '"><pre><code' + langClass + '>' + esc(panel.code) + '</code></pre></div>';
});
cgHtml += '</div></div>';
out.push(cgHtml);
inCodeGroup = false;
codeGroupTabs = [];
codeGroupPanels = [];
continue;
}
if (line.startsWith("```")) {
flushList();
flushTable();
if (!inPre) {
inPre = true;
var info = line.slice(3).trim();
var tabMatch = info.match(/\[([^\]]+)\]/);
var tabTitle = tabMatch ? tabMatch[1] : "";
preLang = info.replace(/\[([^\]]+)\]/, "").trim();
if (inCodeGroup && tabTitle) {
codeGroupTabs.push(tabTitle);
}
preCode = [];
} else {
inPre = false;
var codeContent = preCode.join("\n");
var resolvedLang = preLang;
if (!resolvedLang) {
if (codeContent.trim().startsWith("{") || codeContent.trim().startsWith("[")) resolvedLang = "json";
else if (codeContent.indexOf("=") !== -1 && !codeContent.includes("function") && !codeContent.includes(";")) resolvedLang = "ini";
else if (codeContent.startsWith("http://") || codeContent.startsWith("https://")) resolvedLang = "http";
else resolvedLang = "bash";
}
if (inCodeGroup) {
codeGroupPanels.push({ lang: resolvedLang, code: codeContent });
} else {
var langClass = resolvedLang ? ' class="language-' + resolvedLang + '"' : ' class="language-none"';
out.push('<div class="code-wrap"><pre><code' + langClass + ">" + esc(codeContent) + "</code></pre></div>");
}
}
continue;
}
if (inPre) {
preCode.push(line);
continue;
}
if (line.startsWith(":::callout-")) {
flushList();
flushTable();
var type = line.slice(11).trim();
out.push('<div class="callout callout-' + type + '"><div class="callout-content">');
continue;
}
if (line === ":::") {
flushList();
flushTable();
out.push("</div></div>");
continue;
}
if (line.startsWith("<div") || line.startsWith("</div") || line.startsWith("<span") || line.startsWith("</span") || line.startsWith("<img") || line.startsWith("<ul") || line.startsWith("</ul") || line.startsWith("<li") || line.startsWith("</li") || line.startsWith("<p class=")) {
flushList();
flushTable();
out.push(line);
continue;
}
if (line.indexOf("|") !== -1 && line.trim().startsWith("|") && line.trim().endsWith("|")) {
flushList();
var cells = line.split("|").slice(1, -1).map(function (c) { return c.trim(); });
if (cells.length > 0 && cells[0].match(/^:?-+:?$/)) {
continue;
}
inTable = true;
tableRows.push(cells);
continue;
} else {
flushTable();
}
if (line.match(/^---+$/)) {
flushList();
out.push("<hr />");
continue;
}
var hMatch = line.match(/^(#{1,6})\s+(.*)$/);
if (hMatch) {
flushList();
var level = hMatch[1].length;
var titleText = parseInline(hMatch[2]);
var rawTitle = hMatch[2];
var id = slugify(rawTitle);
out.push("<h" + level + ' id="' + id + '">' + titleText + "</h" + level + ">");
continue;
}
var olMatch = line.match(/^\d+\.\s+(.*)$/);
if (olMatch) {
if (!inList || listType !== "ol") {
flushList();
inList = true;
listType = "ol";
out.push("<ol>");
}
out.push("<li>" + parseInline(olMatch[1]) + "</li>");
continue;
}
var ulMatch = line.match(/^[-*]\s+(.*)$/);
if (ulMatch) {
if (!inList || listType !== "ul") {
flushList();
inList = true;
listType = "ul";
out.push("<ul>");
}
out.push("<li>" + parseInline(ulMatch[1]) + "</li>");
continue;
}
flushList();
if (line.trim().length === 0) {
continue;
}
if (line.startsWith("> ")) {
out.push("<blockquote><p>" + parseInline(line.slice(2)) + "</p></blockquote>");
continue;
}
out.push("<p>" + parseInline(line) + "</p>");
}
flushList();
flushTable();
return out.join("\n");
}
function resolveRoute(pathname) {
var path = pathname || window.location.pathname;
if (!path.startsWith("/")) path = "/" + path;
path = path.replace(/\/+$/, "") || "/";
for (var t = 0; t < SITE_TREE.length; t++) {
var tab = SITE_TREE[t];
for (var g = 0; g < tab.groups.length; g++) {
var grp = tab.groups[g];
for (var i = 0; i < grp.items.length; i++) {
var item = grp.items[i];
var itemPath = item.path.replace(/\/+$/, "") || "/";
if (itemPath === path) {
return { tab: tab, group: grp, item: item };
}
}
}
}
return {
is404: true,
tab: SITE_TREE[0],
group: { title: "Error" },
item: { title: "404 Not Found", path: path }
};
}
function renderNavTabs(currentTab) {
var nav = document.getElementById("nav-tabs");
if (!nav) return;
nav.innerHTML = SITE_TREE.map(function (tab) {
var activeClass = tab.id === currentTab.id ? " active" : "";
return '<a class="tab-link' + activeClass + '" href="' + tab.path + '">' + esc(tab.title) + "</a>";
}).join("");
}
function renderSidebar(currentTab, currentItem) {
var sidebar = document.getElementById("sidebar");
if (!sidebar) return;
var html = "";
currentTab.groups.forEach(function (group) {
html += '<div class="group">';
html += '<p class="group-title">' + esc(group.title) + "</p>";
html += "<ul>";
group.items.forEach(function (item) {
var activeClass = item.path === currentItem.path ? ' class="active"' : "";
html += '<li><a' + activeClass + ' href="' + item.path + '">' + esc(item.title) + "</a></li>";
});
html += "</ul></div>";
});
sidebar.innerHTML = html;
buildMobileBar(currentTab, currentItem);
}
function buildMobileBar(currentTab, currentItem) {
var oldBar = document.getElementById("mobile-bar");
if (oldBar) oldBar.remove();
var headerContainer = document.getElementById("header-container") || document.getElementById("navbar");
if (!headerContainer) return;
var route = resolveRoute(window.location.pathname);
var bar = document.createElement("div");
bar.id = "mobile-bar";
bar.innerHTML =
'<button id="mobile-toggle" type="button" aria-label="Toggle navigation" onclick="toggleSidebar()">' + icon("menu") + '</button>' +
'<div class="mobile-crumbs">' +
'<span class="crumb-group">' + esc(route.group.title) + '</span>' +
'<span class="crumb-sep">›</span>' +
'<span class="crumb-page" id="mobile-crumb-page">' + esc(currentItem.title) + '</span>' +
'<span class="mobile-crumb-sub-btn" id="mobile-crumb-sub" style="display:none;">' +
'<span class="crumb-sep">›</span>' +
'<span class="crumb-heading" id="mobile-crumb-target"></span>' +
'</span>' +
'</div>';
headerContainer.appendChild(bar);
var pageBtn = bar.querySelector("#mobile-crumb-page");
if (pageBtn) {
pageBtn.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
}
var subBtn = bar.querySelector("#mobile-crumb-sub");
if (subBtn) {
subBtn.addEventListener("click", function () {
var targetId = subBtn.getAttribute("data-target-id");
var targetEl = targetId ? document.getElementById(targetId) : null;
if (targetEl) {
var header = document.getElementById("header-container") || document.getElementById("navbar");
var offset = header ? header.offsetHeight + 16 : 80;
var topPos = targetEl.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({ top: Math.max(0, topPos), behavior: "smooth" });
}
});
}
var sidebar = document.getElementById("sidebar");
if (sidebar) {
var existingTabs = sidebar.querySelector(".mobile-tabs");
if (existingTabs) existingTabs.remove();
var g = document.createElement("div");
g.className = "mobile-tabs";
g.innerHTML =
'<button type="button" class="mt-btn" aria-expanded="false"><span>' + esc(currentTab.title) + "</span>" + icon("chevron") + "</button>" +
'<div class="mt-menu">' + SITE_TREE.map(function (t) {
var onClass = t.id === currentTab.id ? " on" : "";
return '<button type="button" class="mt-item' + onClass + '" data-tab-id="' + t.id + '"><span>' + esc(t.title) + "</span>" + icon("check") + "</button>";
}).join("") + "</div>";
sidebar.insertBefore(g, sidebar.firstChild);
g.querySelector(".mt-btn").addEventListener("click", function () {
g.classList.toggle("open");
});
g.querySelectorAll(".mt-item").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
var targetId = btn.getAttribute("data-tab-id");
var targetTab = SITE_TREE.find(function (t) { return t.id === targetId; });
if (targetTab) {
g.querySelectorAll(".mt-item").forEach(function (el) { el.classList.remove("on"); });
btn.classList.add("on");
g.classList.remove("open");
renderSidebar(targetTab, currentItem);
sidebar.scrollTop = 0;
}
});
});
document.addEventListener("click", function (e) {
if (!g.contains(e.target)) g.classList.remove("open");
});
}
var existingHead = sidebar.querySelector(".side-head");
if (existingHead) existingHead.remove();
var head = document.createElement("div");
head.className = "side-head";
var brand = document.querySelector("#navbar .brand");
if (brand && sidebar) {
var b = brand.cloneNode(true);
b.querySelector("img").removeAttribute("id");
var sideLinks = document.createElement("div");
sideLinks.className = "side-links";
var modes = ["system", "light", "dark"];
var seg = document.createElement("div");
seg.id = "theme-seg";
seg.innerHTML = modes.map(function (m) {
return '<button type="button" data-mode="' + m + '" aria-label="' + m + '">' + icon(m) + '</button>';
}).join("");
seg.addEventListener("click", function (e) {
var btn = e.target.closest("button");
if (btn) setTheme(btn.getAttribute("data-mode"));
});
sideLinks.appendChild(seg);
head.appendChild(b);
head.appendChild(sideLinks);
sidebar.insertBefore(head, sidebar.firstChild);
refreshThemeUI();
}
}
function updateFooterNav(currentItem) {
var allItems = [];
SITE_TREE.forEach(function (tab) {
tab.groups.forEach(function (grp) {
grp.items.forEach(function (itm) {
allItems.push(itm);
});
});
});
var idx = allItems.findIndex(function (it) {
return it.path.replace(/\/+$/, "") === currentItem.path.replace(/\/+$/, "");
});
var prev = idx > 0 ? allItems[idx - 1] : null;
var next = idx >= 0 && idx < allItems.length - 1 ? allItems[idx + 1] : null;
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) {
if (prev) {
prevBtn.href = prev.path;
prevBtn.innerHTML = icon("chevronLeft") + "<span>" + esc(prev.title) + "</span>";
prevBtn.classList.remove("hidden");
} else {
prevBtn.classList.add("hidden");
}
}
if (nextBtn) {
if (next) {
nextBtn.href = next.path;
nextBtn.innerHTML = "<span>" + esc(next.title) + "</span>" + icon("chevronRight");
nextBtn.classList.remove("hidden");
} else {
nextBtn.classList.add("hidden");
}
}
}
function updateToc() {
var main = document.getElementById("main-content");
var tocList = document.getElementById("toc-list");
if (!main || !tocList) return;
var headings = main.querySelectorAll("h2");
var html = "";
headings.forEach(function (h) {
var id = h.id || h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, "-");
h.id = id;
html += '<li><a href="#' + id + '">' + esc(h.textContent) + "</a></li>";
});
tocList.innerHTML = html;
initTocSpy();
}
function initTocSpy() {
var main = document.getElementById("main-content");
var links = Array.prototype.slice.call(document.querySelectorAll("aside#toc a"));
var headings = main ? Array.prototype.slice.call(main.querySelectorAll("h2, h3")) : [];
function update() {
var activeHeading = null;
headings.forEach(function (h) {
if (h.getBoundingClientRect().top <= 160) {
activeHeading = h;
}
});
if (links.length) {
var activeId = activeHeading ? activeHeading.id : "";
links.forEach(function (a) {
var href = a.getAttribute("href").replace(/^#/, "");
a.classList.toggle("active", href === activeId);
});
}
var crumbPage = document.getElementById("mobile-crumb-page");
var crumbSub = document.getElementById("mobile-crumb-sub");
var crumbTarget = document.getElementById("mobile-crumb-target");
if (crumbPage && crumbSub && crumbTarget) {
if (activeHeading && window.scrollY > 80) {
crumbPage.classList.add("muted");
crumbSub.style.display = "inline-flex";
crumbSub.setAttribute("data-target-id", activeHeading.id);
crumbTarget.textContent = activeHeading.textContent;
} else {
crumbPage.classList.remove("muted");
crumbSub.style.display = "none";
crumbSub.removeAttribute("data-target-id");
crumbTarget.textContent = "";
}
}
}
window.addEventListener("scroll", update, { passive: true });
update();
}
function addCopyButtons() {
document.querySelectorAll(".code-group").forEach(function (cg) {
if (cg.querySelector(".cg-copy-btn")) return;
var b = document.createElement("button");
b.type = "button";
b.className = "cg-copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
var activePre = cg.querySelector(".code-group-panel.active pre");
if (!activePre) return;
navigator.clipboard.writeText(activePre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
cg.appendChild(b);
});
document.querySelectorAll("main.prose pre").forEach(function (pre) {
if (pre.closest(".code-group")) return;
if (pre.parentNode.classList.contains("code-wrap") && pre.parentNode.querySelector(".copy-btn")) return;
var wrap = pre.parentNode.classList.contains("code-wrap") ? pre.parentNode : document.createElement("div");
if (!pre.parentNode.classList.contains("code-wrap")) {
wrap.className = "code-wrap";
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(pre);
}
var b = document.createElement("button");
b.type = "button";
b.className = "copy-btn";
b.setAttribute("aria-label", "Copy");
b.innerHTML = icon("copy");
b.onclick = function () {
navigator.clipboard.writeText(pre.innerText).then(function () {
b.innerHTML = icon("check");
setTimeout(function () { b.innerHTML = icon("copy"); }, 1500);
});
};
wrap.appendChild(b);
});
}
function bindScrollFade(box, getScroller) {
function update() {
var el = getScroller();
var max = el.scrollWidth - el.clientWidth;
var left = 0;
var right = 0;
if (max > 1) {
left = Math.min(el.scrollLeft, 56);
right = Math.min(Math.max(Math.round(max - el.scrollLeft), 0), 56);
}
box.style.setProperty("--fade-l", left + "px");
box.style.setProperty("--fade-r", right + "px");
}
box.addEventListener("scroll", update, true);
window.addEventListener("resize", update);
update();
}
function initCodeFades() {
document.querySelectorAll("main.prose pre").forEach(function (pre) {
var code = pre.querySelector("code");
bindScrollFade(pre, function () {
return code && code.scrollWidth > code.clientWidth + 1 ? code : pre;
});
});
}
function wrapTables() {
document.querySelectorAll("main.prose table").forEach(function (table) {
if (table.parentNode.classList.contains("table-wrap")) {
bindScrollFade(table.parentNode, function () { return table.parentNode; });
}
});
}
function navigateTo(path) {
var cleanPath = path.startsWith("/") ? path : "/" + path;
if (window.location.pathname !== cleanPath) {
window.history.pushState(null, "", cleanPath);
}
loadPage();
}
function loadPage() {
var path = window.location.pathname.replace(/\/+$/, "") || "/";
var isHome = path === "/";
document.body.classList.toggle("is-home", isHome);
var contentEl = document.getElementById("main-content");
if (isHome) {
document.title = "Vyla Entertainment — Vyla";
renderNavTabs(SITE_TREE[0]);
if (contentEl) {
contentEl.classList.remove("content-animate", "caught-up");
fetch("/home.txt")
.then(function (res) {
if (!res.ok) throw new Error();
return res.text();
})
.then(function (html) {
contentEl.innerHTML = html;
window.scrollTo(0, 0);
})
.catch(function () {
contentEl.innerHTML = "<h1>Home</h1><p>Create /home.txt to customize this page.</p>";
});
}
var tocList = document.getElementById("toc-list");
if (tocList) tocList.innerHTML = "";
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) prevBtn.classList.add("hidden");
if (nextBtn) nextBtn.classList.add("hidden");
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
return;
}
var route = resolveRoute(window.location.pathname);
renderNavTabs(route.tab);
renderSidebar(route.tab, route.item);
document.title = route.item.title + " — Vyla";
var contentEl = document.getElementById("main-content");
document.body.classList.remove("is-404");
var route = resolveRoute(window.location.pathname);
renderNavTabs(route.tab);
renderSidebar(route.tab, route.item);
document.title = route.item.title + " — Vyla";
var contentEl = document.getElementById("main-content");
if (route.is404) {
document.body.classList.add("is-404");
document.title = "404 - Page Not Found — Vyla";
if (contentEl) {
contentEl.classList.remove("content-animate");
contentEl.innerHTML =
'<div class="error-container">' +
' <div class="error-code">404</div>' +
' <div class="error-title">PAGE NOT FOUND</div>' +
' <p class="error-desc">But if you don\'t change your direction, and if you keep looking, you may end up where you are heading.</p>' +
' <a href="/" class="btn-home">Take me home</a>' +
'</div>';
var tocList = document.getElementById("toc-list");
if (tocList) tocList.innerHTML = "";
var prevBtn = document.getElementById("footer-prev");
var nextBtn = document.getElementById("footer-next");
if (prevBtn) prevBtn.classList.add("hidden");
if (nextBtn) nextBtn.classList.add("hidden");
window.scrollTo(0, 0);
}
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
return;
}
if (contentEl) {
contentEl.classList.remove("content-animate");
contentEl.innerHTML =
'<div class="skeleton-wrap" aria-hidden="true">' +
'<div class="skeleton-line skeleton-eyebrow"></div>' +
'<div class="skeleton-line skeleton-title"></div>' +
'<div class="skeleton-line skeleton-desc"></div>' +
'<div class="skeleton-media"></div>' +
'<div class="skeleton-line skeleton-text w-90"></div>' +
'<div class="skeleton-line skeleton-text w-100"></div>' +
'<div class="skeleton-line skeleton-text w-75"></div>' +
'<div class="skeleton-line skeleton-text w-85"></div>' +
'</div>';
}
fetch(route.item.file)
.then(function (res) {
var ctype = res.headers.get("content-type") || "";
if (!res.ok || ctype.indexOf("text/html") !== -1) throw new Error("Not found");
return res.text();
})
.then(function (md) {
if (contentEl) {
contentEl.innerHTML = parseMarkdown(md);
if (window.Prism) {
window.Prism.highlightAllUnder(contentEl);
}
var eyebrow = document.createElement("div");
eyebrow.className = "eyebrow";
eyebrow.textContent = route.group.title;
var h1 = contentEl.querySelector("h1");
if (h1) h1.parentNode.insertBefore(eyebrow, h1);
contentEl.classList.remove("caught-up");
contentEl.classList.add("content-animate");
var catchUpOnScroll = function () {
if (contentEl.classList.contains("content-animate")) {
contentEl.classList.add("caught-up");
}
window.removeEventListener("scroll", catchUpOnScroll, { passive: true });
window.removeEventListener("wheel", catchUpOnScroll, { passive: true });
window.removeEventListener("touchmove", catchUpOnScroll, { passive: true });
};
window.addEventListener("scroll", catchUpOnScroll, { passive: true });
window.addEventListener("wheel", catchUpOnScroll, { passive: true });
window.addEventListener("touchmove", catchUpOnScroll, { passive: true });
setTimeout(function () {
contentEl.classList.add("caught-up");
window.removeEventListener("scroll", catchUpOnScroll);
window.removeEventListener("wheel", catchUpOnScroll);
window.removeEventListener("touchmove", catchUpOnScroll);
}, 700);
updateFooterNav(route.item);
updateToc();
addCopyButtons();
initCodeFades();
wrapTables();
window.scrollTo(0, 0);
}
})
.catch(function () {
if (contentEl) {
contentEl.innerHTML = "<h1>Page not found</h1><p>The requested page could not be located.</p>";
}
});
var sidebar = document.getElementById("sidebar");
if (sidebar && sidebar.classList.contains("open")) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
}
function getRecent() {
try { return JSON.parse(localStorage.getItem("vyla-recent-v2")) || []; } catch (e) { return []; }
}
function saveRecent(item) {
if (!item) return;
var slim = { title: item.title, crumbs: item.crumbs, href: item.href, type: item.type };
var list = getRecent().filter(function (r) { return r.href !== slim.href; });
list.unshift(slim);
localStorage.setItem("vyla-recent-v2", JSON.stringify(list.slice(0, 5)));
}
function loadSearchData() {
if (searchData) return Promise.resolve(searchData);
var promises = [];
SITE_TREE.forEach(function (tab) {
tab.groups.forEach(function (group) {
group.items.forEach(function (item) {
promises.push(
fetch(item.file)
.then(function (r) { return r.text(); })
.then(function (text) {
return {
title: item.title,
crumbs: [tab.title, group.title, item.title],
href: item.path,
type: "page",
text: text.replace(/[#*`[\]()]/g, " ").slice(0, 3000)
};
})
.catch(function () { return null; })
);
});
});
});
return Promise.all(promises).then(function (results) {
searchData = results.filter(Boolean);
return searchData;
});
}
function renderSearch(keepActive) {
var q = document.getElementById("search-input").value.trim().toLowerCase();
var tokens = q.split(/\s+/).filter(Boolean);
var box = document.getElementById("search-results");
if (!keepActive) searchActive = 0;
if (tokens.length) {
if (!searchData) {
searchResults = [];
box.innerHTML = '<div class="search-label">Indexing pages...</div>';
return;
}
searchResults = searchData
.map(function (it) {
var score = 0;
var t = it.title.toLowerCase();
var txt = it.text.toLowerCase();
tokens.forEach(function (tok) {
if (t.indexOf(tok) !== -1) score += 20;
if (txt.indexOf(tok) !== -1) score += 2;
});
return { it: it, s: score };
})
.filter(function (r) { return r.s > 0; })
.sort(function (a, b) { return b.s - a.s; })
.slice(0, 10)
.map(function (r) { return r.it; });
} else {
searchResults = getRecent();
}
if (!searchResults.length) {
box.innerHTML = '<div class="search-label">' + (tokens.length ? "No results found" : "No recent searches") + "</div>";
return;
}
box.innerHTML = (tokens.length ? "" : '<div class="search-label">Recent searches</div>') + searchResults.map(function (it, i) {
var crumbs = it.crumbs.join('<span class="sep">›</span>');
return '<a class="sr' + (i === searchActive ? " on" : "") + '" href="' + it.href + '" data-i="' + i + '">' +
icon("doc") +
'<div class="sr-body"><div class="t">' + esc(it.title) + "</div>" +
'<div class="p">' + crumbs + "</div></div></a>";
}).join("");
}
function openSearch() {
var input = document.getElementById("search-input");
document.getElementById("search-modal").classList.add("open");
document.body.style.overflow = "hidden";
input.value = "";
renderSearch(false);
loadSearchData().then(function () { renderSearch(false); });
input.focus();
}
function closeSearch() {
document.getElementById("search-modal").classList.remove("open");
document.body.style.overflow = "";
}
function onSearchKey(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
openSearch();
return;
}
var modal = document.getElementById("search-modal");
if (!modal || !modal.classList.contains("open")) return;
if (e.key === "Escape") {
closeSearch();
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
var n = searchResults.length || 1;
searchActive = (searchActive + (e.key === "ArrowDown" ? 1 : -1) + n) % n;
renderSearch(true);
} else if (e.key === "Enter" && searchResults[searchActive]) {
var item = searchResults[searchActive];
saveRecent(item);
closeSearch();
navigateTo(item.href);
}
}
function buildSearch() {
var trigger = document.getElementById("search-trigger");
if (trigger) trigger.innerHTML = icon("search") + "<span>Search...</span><kbd>Ctrl K</kbd>";
var modal = document.createElement("div");
modal.id = "search-modal";
modal.innerHTML =
'<div class="search-backdrop"></div><div class="search-panel"><div class="search-input-row">' + icon("search") +
'<input id="search-input" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"></div>' +
'<div id="search-results"></div><div class="search-footer"><span><kbd>↑</kbd><kbd>↓</kbd>Select</span><span><kbd>↵</kbd>Open</span><span class="close-hint"><kbd>esc</kbd>Close</span></div></div>';
document.body.appendChild(modal);
modal.querySelector(".search-backdrop").addEventListener("click", closeSearch);
document.getElementById("search-input").addEventListener("input", function () { renderSearch(false); });
document.getElementById("search-results").addEventListener("click", function (e) {
var a = e.target.closest(".sr");
if (a) {
e.preventDefault();
saveRecent(searchResults[Number(a.getAttribute("data-i"))]);
closeSearch();
navigateTo(a.getAttribute("href"));
}
});
document.addEventListener("keydown", onSearchKey);
}
document.addEventListener("DOMContentLoaded", function () {
if (localStorage.getItem("vyla-banner-dismissed") === "true") {
var banner = document.getElementById("banner");
if (banner) banner.setAttribute("data-dismissed", "true");
document.body.classList.add("banner-dismissed");
}
var header = document.getElementById("header-container");
if (header) {
requestAnimationFrame(function () {
document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
});
window.addEventListener("resize", function () {
requestAnimationFrame(function () {
document.documentElement.style.setProperty("--header-height", header.offsetHeight + "px");
});
});
}
applyTheme();
buildThemeControls();
buildSearch();
loadPage();
window.addEventListener("popstate", loadPage);
document.addEventListener("click", function (e) {
var a = e.target.closest("a");
if (a) {
var href = a.getAttribute("href");
if (href && href.startsWith("#")) {
return;
}
if (href && href.startsWith("/") && !href.startsWith("//") && a.target !== "_blank") {
e.preventDefault();
navigateTo(href);
return;
}
}
var sidebar = document.getElementById("sidebar");
var menu = document.getElementById("theme-menu");
if (menu && !e.target.closest(".theme-wrap")) menu.classList.remove("open");
if (
sidebar &&
sidebar.classList.contains("open") &&
!sidebar.contains(e.target) &&
!e.target.closest("#mobile-toggle") &&
!e.target.closest(".theme-wrap") &&
!e.target.closest("#theme-seg")
) {
sidebar.classList.remove("open");
document.body.classList.remove("nav-open");
}
});
});