(() => { "use strict"; const list = document.querySelector("#endpoint-list"); const template = document.querySelector("#endpoint-template"); const search = document.querySelector("#search"); const filters = document.querySelector("#service-filters"); const count = document.querySelector("#endpoint-count"); const empty = document.querySelector("#empty"); const sidebar = document.querySelector("#sidebar"); const menu = document.querySelector("#menu"); let manifest; let activeService = "all"; const escapeHtml = (value) => String(value) .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); function curlFor(service, endpoint) { const url = `${service.base_url}${endpoint.path.replaceAll(/{([^}]+)}/g, "<$1>")}`; const lines = [`curl -X ${endpoint.method} '${url}'`]; if (endpoint.auth === "bearer" || endpoint.auth === "admin") { lines.push(" -H 'Authorization: Bearer $TOKEN'"); } else if (endpoint.auth === "hmac") { lines.push(" -H 'X-Api-Key: $API_KEY'", " -H 'X-Api-Timestamp: $TIMESTAMP'", " -H 'X-Api-Nonce: $UUID'", " -H 'X-Api-Signature: $SIGNATURE'"); if (endpoint.method !== "GET") lines.push(" -H 'Idempotency-Key: $UUID'"); } if (endpoint.body) { lines.push(" -H 'Content-Type: application/json'", ` -d '${JSON.stringify(endpoint.body)}'`); } return lines.join(" \\\n"); } function endpointId(service, endpoint) { return `${service.id}-${endpoint.method.toLowerCase()}-${endpoint.path .replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`; } function render() { const query = search.value.trim().toLowerCase(); list.replaceChildren(); let shown = 0; manifest.services.forEach((service) => { service.endpoints.forEach((endpoint) => { const haystack = `${service.name} ${endpoint.method} ${endpoint.path} ${endpoint.title} ${endpoint.description} ${endpoint.group}`.toLowerCase(); if ((activeService !== "all" && activeService !== service.id) || !haystack.includes(query)) return; shown += 1; const node = template.content.firstElementChild.cloneNode(true); node.id = endpointId(service, endpoint); node.dataset.service = service.id; const summary = node.querySelector(".endpoint-summary"); const method = node.querySelector(".method"); method.textContent = endpoint.method; method.classList.add(endpoint.method); node.querySelector(".path").textContent = endpoint.path; node.querySelector(".endpoint-title").textContent = endpoint.title; node.querySelector(".auth-badge").textContent = endpoint.auth_label; node.querySelector(".endpoint-description").textContent = endpoint.description; node.querySelector(".endpoint-meta").innerHTML = `${escapeHtml(service.base_url)}${escapeHtml(endpoint.group)}`; const curl = curlFor(service, endpoint); node.querySelector(".endpoint-example").innerHTML = `
cURL
${escapeHtml(curl)}
`; summary.addEventListener("click", () => { const open = node.classList.toggle("open"); summary.setAttribute("aria-expanded", String(open)); if (open) history.replaceState(null, "", `#${node.id}`); }); node.querySelector(".copy").addEventListener("click", (event) => copyText(event.currentTarget, curl)); list.append(node); }); }); count.textContent = `${shown} / ${manifest.total_endpoints}`; empty.hidden = shown !== 0; } function copyText(button, text) { navigator.clipboard.writeText(text).then(() => { const old = button.textContent; button.textContent = "Скопировано"; window.setTimeout(() => { button.textContent = old; }, 1300); }); } function makeFilters() { const choices = [["all", "Все"], ...manifest.services.map((service) => [service.id, service.name])]; choices.forEach(([id, label]) => { const button = document.createElement("button"); button.className = `filter${id === "all" ? " active" : ""}`; button.textContent = label; button.addEventListener("click", () => { activeService = id; filters.querySelectorAll(".filter").forEach((item) => item.classList.toggle("active", item === button)); render(); }); filters.append(button); }); } document.querySelectorAll("[data-copy-target]").forEach((button) => { button.addEventListener("click", () => copyText(button, document.querySelector(`#${button.dataset.copyTarget}`).innerText)); }); search.addEventListener("input", render); document.addEventListener("keydown", (event) => { if (event.key === "/" && document.activeElement !== search) { event.preventDefault(); search.focus(); } if (event.key === "Escape") sidebar.classList.remove("open"); }); menu.addEventListener("click", () => { const open = sidebar.classList.toggle("open"); menu.setAttribute("aria-expanded", String(open)); }); sidebar.querySelectorAll("a").forEach((link) => link.addEventListener("click", () => sidebar.classList.remove("open"))); fetch("/api-manifest.json") .then((response) => { if (!response.ok) throw new Error(`manifest ${response.status}`); return response.json(); }) .then((data) => { manifest = data; makeFilters(); render(); if (location.hash.startsWith("#messenger-") || location.hash.startsWith("#gateway-")) { const target = document.querySelector(location.hash); if (target) { target.classList.add("open"); target.querySelector(".endpoint-summary").setAttribute("aria-expanded", "true"); target.scrollIntoView(); } } }) .catch(() => { empty.hidden = false; empty.textContent = "Не удалось загрузить API catalog."; }); })();