(function () { "use strict"; const SPEC_URL = window.__DOXA_API_SPEC_URL__ || "doxaApi.json"; let apis = []; let activeApiIndex = 0; let spec = null; let activeEndpoint = null; let activeTryTab = "body"; let collapsedGroups = new Set(JSON.parse(localStorage.getItem("DoxaApi.collapsed") || "[]")); const el = { nav: document.getElementById("navContent"), detail: document.getElementById("detailContent"), try: document.getElementById("tryContent"), search: document.getElementById("searchInput"), themeToggle: document.getElementById("themeToggle"), brandTitle: document.getElementById("brandTitle"), brandVersion: document.getElementById("brandVersion"), apiSelector: document.getElementById("apiSelector"), }; function refreshApiSelector(){ if(!el.apiSelector) return; el.apiSelector.innerHTML = apis.map((a,i)=>``).join(''); el.apiSelector.value=String(activeApiIndex); } async function init() { renderSkeleton(); try { const res = await fetch(SPEC_URL); if (!res.ok) throw new Error("HTTP " + res.status); const importedSpec = await res.json(); const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)"); if (extend) { spec.groups = [...(spec.groups||[]), ...(importedSpec.groups||[])]; spec.schemas = Object.assign(spec.schemas||{}, importedSpec.schemas||{}); } else { apis.push(importedSpec); activeApiIndex = apis.length - 1; spec = importedSpec; } refreshApiSelector(); if(spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if(spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; } catch (err) { el.nav.innerHTML = ``; return; } if (spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if (spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; document.title = (spec.info && spec.info.title) || "API Documentation"; renderNav(""); renderOverview(); renderTryEmpty(); bindGlobalEvents(); } function renderSkeleton() { el.nav.innerHTML = Array.from({ length: 6 }) .map(() => `
`) .join(""); } function totalEndpointCount() { return (spec.groups || []).reduce((n, g) => n + g.endpoints.length, 0); } function totalSchemaCount() { return Object.keys(spec.schemas || {}).length; } // Nav rendering function renderNav(filterText) { const term = filterText.trim().toLowerCase(); const groups = spec.groups || []; let html = ``; if (groups.length === 0) { html += ``; el.nav.innerHTML = html; return; } let totalMatches = 0; let groupHtml = ""; for (const group of groups) { const endpoints = group.endpoints.filter((e) => matchesFilter(e, term)); if (term && endpoints.length === 0) continue; totalMatches += endpoints.length; const isCollapsed = collapsedGroups.has(group.name) && !term; groupHtml += ` `; } if (term && totalMatches === 0) { html += ``; } else { html += groupHtml; } el.nav.innerHTML = html; } function navEndpointHtml(group, endpoint) { const isActive = activeEndpoint && activeEndpoint.endpoint.operationId === endpoint.operationId; return ` `; } function matchesFilter(endpoint, term) { if (!term) return true; return ( endpoint.path.toLowerCase().includes(term) || (endpoint.summary || "").toLowerCase().includes(term) || endpoint.method.toLowerCase().includes(term) ); } // Overview / welcome screen function renderOverview() { const info = spec.info || {}; const groups = spec.groups || []; let html = `
`; html += `
`; html += `
API reference
`; html += `

${escapeHtml(info.title || "API Documentation")}

`; if (info.description) { html += `

${escapeHtml(info.description)}

`; } html += `
`; if (info.version) { html += ` ${escapeHtml(info.version)} `; } if (spec.servers && spec.servers.length) { html += ` ${escapeHtml(spec.servers[0])} `; } html += `
`; html += `
`; // hero html += `
`; html += `
${groups.length}
Groups
`; html += `
${totalEndpointCount()}
Endpoints
`; html += `
${totalSchemaCount()}
Schemas
`; html += `
`; html += `

Browse by group

`; for (const group of groups) { html += `
${escapeHtml(group.name)} ${group.endpoints.length}
`; for (const ep of group.endpoints) { html += `
${ep.method} ${escapeHtml(ep.path)} ${escapeHtml(ep.summary || "")}
`; } html += `
`; } html += `
`; el.detail.innerHTML = html; el.detail.scrollTop = 0; } function renderTryEmpty() { el.try.innerHTML = `
Pick an endpoint to send a live request.
`; } function selectEndpoint(group, endpoint) { activeEndpoint = { group, endpoint }; activeTryTab = "body"; document.querySelectorAll(".nav-endpoint").forEach((b) => { b.classList.toggle("active", b.dataset.op === endpoint.operationId); }); const overviewLink = document.querySelector(".nav-overview-link"); if (overviewLink) overviewLink.classList.remove("active"); renderDetail(group, endpoint); renderTry(group, endpoint); } function renderDetail(group, endpoint) { const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`; let html = `
`; html += `
`; html += ``; html += `
${endpoint.method} ${escapeHtml(endpoint.path)}
`; html += `

${escapeHtml(endpoint.summary || endpoint.operationId)}

`; if (endpoint.description) { html += `

${escapeHtml(endpoint.description)}

`; } if (endpoint.deprecated) { html += `
Deprecated - this endpoint may be removed in a future version
`; } html += `
`; if (endpoint.parameters && endpoint.parameters.length > 0) { html += `

Parameters ${endpoint.parameters.length}

`; html += ``; for (const p of endpoint.parameters) { html += ``; } html += `
NameLocated inTypeDescription
${escapeHtml(p.name)}${p.required ? '*' : ""} ${p.in} ${schemaTypeLabel(p.schema)} ${escapeHtml(p.description || "-")}
`; } if (endpoint.requestBody) { html += `

Request body

`; html += `
${renderSchemaTree(endpoint.requestBody.schema, 0)}
`; } if (endpoint.responses && endpoint.responses.length > 0) { html += `

Responses ${endpoint.responses.length}

`; for (const r of endpoint.responses) { const cls = r.statusCode[0] === "2" ? "status-2xx" : r.statusCode[0] === "4" ? "status-4xx" : "status-5xx"; html += `
${r.statusCode} ${escapeHtml(r.description || "")}
`; if (r.schema) { html += `
${renderSchemaTree(r.schema, 0)}
`; } html += `
`; } html += `
`; } html += `
`; el.detail.innerHTML = html; el.detail.scrollTop = 0; const copyBtn = document.getElementById("copyRouteBtn"); if (copyBtn) { copyBtn.addEventListener("click", () => { navigator.clipboard.writeText(endpoint.path).then(() => flashIcon(copyBtn)); }); } } function flashIcon(btn) { const original = btn.innerHTML; btn.innerHTML = ``; setTimeout(() => (btn.innerHTML = original), 1100); } function schemaTypeLabel(schema) { if (!schema) return "any"; if (schema.refName) return schema.refName; if (schema.type === "array") return schemaTypeLabel(schema.items) + "[]"; if (schema.type === "enum") return "enum"; return schema.format ? `${schema.type} (${schema.format})` : schema.type; } function renderSchemaTree(schema, depth) { if (!schema) return `unknown`; const indent = " ".repeat(depth); if (schema.refName && spec.schemas && spec.schemas[schema.refName] && depth < 6) { const resolved = spec.schemas[schema.refName]; return renderSchemaTree({ ...resolved, refName: undefined }, depth); } if (schema.type === "object" && schema.properties) { const required = new Set(schema.required || []); let lines = [`{`]; const entries = Object.entries(schema.properties); entries.forEach(([key, propSchema], i) => { const isReq = required.has(key); const comma = i < entries.length - 1 ? "," : ""; const nullableMark = propSchema && propSchema.nullable ? '?' : ""; lines.push( `${indent} ${escapeHtml(key)}${isReq ? '*' : ""}${nullableMark}: ${renderInlineType(propSchema, depth + 1)}${comma}` ); }); lines.push(`${indent}}`); return lines.join("\n"); } if (schema.type === "array") { return `[\n${indent} ${renderInlineType(schema.items, depth + 1)}\n${indent}]`; } if (schema.type === "enum") { return `enum (${(schema.enumValues || []).join(" | ")})`; } return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""}`; } function renderInlineType(schema, depth) { if (!schema) return `any`; if (schema.refName) { if (spec.schemas && spec.schemas[schema.refName] && depth < 6) { return renderSchemaTree({ ...spec.schemas[schema.refName] }, depth); } return `${escapeHtml(schema.refName)}`; } if (schema.type === "object" && schema.properties) return renderSchemaTree(schema, depth); if (schema.type === "array") { return `Array<${renderInlineType(schema.items, depth)}>`; } if (schema.type === "enum") { return `(${(schema.enumValues || []).join(" | ")})`; } return `${schema.type}${schema.format ? " (" + schema.format + ")" : ""}`; } // Try-it-out pane function renderTry(group, endpoint) { const methodColorVar = `var(--m-${endpoint.method.toLowerCase()}, var(--m-default))`; let html = `
`; html += `
Try it
`; html += `
`; html += `
`; const pathParams = endpoint.parameters.filter((p) => p.in === "path"); const queryParams = endpoint.parameters.filter((p) => p.in === "query"); const headerParams = endpoint.parameters.filter((p) => p.in === "header"); if (pathParams.length) { html += `
Path parameters
`; for (const p of pathParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (queryParams.length) { html += `
Query parameters
`; for (const p of queryParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (headerParams.length) { html += `
Headers
`; for (const p of headerParams) { html += `
${escapeHtml(p.name)}${p.required ? '*' : ""}${schemaTypeLabel(p.schema)}
`; } html += `
`; } if (endpoint.requestBody) { const example = endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0); html += `
Request body (JSON)
`; } html += ``; html += `
`; html += `
`; // tryTabBody html += `
Shell snippet
${buildCurlSnippet(endpoint)}
`; html += `
`; el.try.innerHTML = html; document.getElementById("sendBtn").addEventListener("click", () => sendTryRequest(endpoint)); el.try.querySelectorAll("[data-try-tab]").forEach((btn) => { btn.addEventListener("click", () => { activeTryTab = btn.dataset.tryTab; renderTry(group, endpoint); }); }); const copyCurlBtn = document.getElementById("copyCurlBtn"); if (copyCurlBtn) { copyCurlBtn.addEventListener("click", () => { const text = document.getElementById("curlSnippet").textContent; navigator.clipboard.writeText(text).then(() => { const original = copyCurlBtn.textContent; copyCurlBtn.textContent = "Copied"; setTimeout(() => (copyCurlBtn.innerHTML = `Copy`), 1100); }); }); } } function buildCurlSnippet(endpoint) { const base = (spec.servers && spec.servers[0]) || ""; let path = endpoint.path; // replace path params with placeholder tokens for readability const lines = []; lines.push(`curl -X ${endpoint.method} \\`); lines.push(` "${escapeHtml(base)}${escapeHtml(path)}" \\`); lines.push(` -H "Accept: application/json"`); if (endpoint.requestBody) { const example = endpoint.requestBody.example || generateExampleJson(endpoint.requestBody.schema, 0); lines[lines.length - 1] += " \\"; lines.push(` -H "Content-Type: ${escapeHtml(endpoint.requestBody.contentType || "application/json")}" \\`); lines.push(` -d '${escapeHtml(example)}'`); } return lines.join("\n"); } function generateExampleJson(schema, depth) { const value = generateExampleValue(schema, depth, new Set()); return JSON.stringify(value, null, 2); } function generateExampleValue(schema, depth, seen) { if (!schema || depth > 6) return null; if (schema.refName) { if (seen.has(schema.refName)) return {}; const resolved = spec.schemas && spec.schemas[schema.refName]; if (!resolved) return {}; const nextSeen = new Set(seen); nextSeen.add(schema.refName); return generateExampleValue(resolved, depth + 1, nextSeen); } switch (schema.type) { case "string": if (schema.format === "date-time") return new Date().toISOString(); if (schema.format === "uuid") return "00000000-0000-0000-0000-000000000000"; return "string"; case "integer": return 0; case "number": return 0; case "boolean": return true; case "enum": return (schema.enumValues && schema.enumValues[0]) || "string"; case "array": return [generateExampleValue(schema.items, depth + 1, seen)]; case "object": { if (!schema.properties) return {}; const obj = {}; for (const [key, propSchema] of Object.entries(schema.properties)) { obj[key] = generateExampleValue(propSchema, depth + 1, seen); } return obj; } default: return null; } } async function sendTryRequest(endpoint) { const btn = document.getElementById("sendBtn"); const label = document.getElementById("sendBtnLabel"); const panel = document.getElementById("responsePanel"); let path = endpoint.path; document.querySelectorAll('[data-param-in="path"]').forEach((input) => { const name = input.dataset.paramName; path = path.replace(`{${name}}`, encodeURIComponent(input.value || "")); }); const url = new URL(path, window.location.origin); document.querySelectorAll('[data-param-in="query"]').forEach((input) => { if (input.value) url.searchParams.set(input.dataset.paramName, input.value); }); const headers = { Accept: "application/json" }; document.querySelectorAll('[data-param-in="header"]').forEach((input) => { if (input.value) headers[input.dataset.paramName] = input.value; }); let body = undefined; if (endpoint.requestBody) { headers["Content-Type"] = endpoint.requestBody.contentType || "application/json"; const bodyEl = document.getElementById("tryBody"); body = bodyEl ? bodyEl.value : undefined; } btn.disabled = true; label.textContent = "Sending…"; btn.querySelector("svg").style.display = "none"; btn.insertBefore(spinnerEl(), btn.firstChild); const startTime = performance.now(); try { const res = await fetch(url.toString(), { method: endpoint.method, headers, body: endpoint.method === "GET" || endpoint.method === "HEAD" ? undefined : body, }); const elapsedMs = Math.round(performance.now() - startTime); const contentType = res.headers.get("content-type") || ""; let bodyText; let isJson = false; if (contentType.includes("application/json")) { try { const json = await res.json(); bodyText = JSON.stringify(json, null, 2); isJson = true; } catch { bodyText = await res.text(); } } else { bodyText = await res.text(); } renderResponsePanel(panel, { status: res.status, ok: res.ok, elapsedMs, body: bodyText, isJson, }); } catch (err) { const elapsedMs = Math.round(performance.now() - startTime); renderResponsePanel(panel, { status: null, ok: false, elapsedMs, body: String(err && err.message ? err.message : err), isJson: false, networkError: true, }); } finally { btn.disabled = false; label.textContent = "Send request"; const spinner = btn.querySelector(".spinner"); if (spinner) spinner.remove(); btn.querySelector("svg").style.display = ""; } } function spinnerEl() { const s = document.createElement("span"); s.className = "spinner"; return s; } function renderResponsePanel(panel, result) { const statusClass = result.networkError ? "status-5xx" : result.status < 300 ? "status-2xx" : result.status < 500 ? "status-4xx" : "status-5xx"; const statusLabel = result.networkError ? "Network error" : result.status; panel.innerHTML = `
${statusLabel} ${result.elapsedMs} ms
${result.isJson ? syntaxHighlightJson(result.body) : escapeHtml(result.body) }
`; document.getElementById("copyResponseBtn").addEventListener("click", () => { navigator.clipboard.writeText(result.body).then(() => { const btn = document.getElementById("copyResponseBtn"); btn.lastChild.textContent = "Copied"; setTimeout(() => (btn.lastChild.textContent = "Copy"), 1200); }); }); } function syntaxHighlightJson(json) { const escaped = escapeHtml(json); return escaped.replace( /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(\.\d+)?([eE][+-]?\d+)?)/g, (match) => { let cls = "json-number"; if (/^"/.test(match)) { cls = /:$/.test(match) ? "json-key" : "json-string"; } else if (/true|false/.test(match)) { cls = "json-boolean"; } else if (/null/.test(match)) { cls = "json-null"; } return `${match}`; } ); } // Global events function bindGlobalEvents() { const importBtn = document.getElementById("importBtn"); const importFile = document.getElementById("importFile"); const exportDoxaApiBtn = document.getElementById("exportDoxaApiBtn"); const exportOpenApiBtn = document.getElementById("exportOpenApiBtn"); const exportSwaggerBtn = document.getElementById("exportSwaggerBtn"); importBtn?.addEventListener("click", () => importFile.click()); importFile?.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; const text = await file.text(); const res = await fetch("import", { method: "POST", headers: { "Content-Type": "application/json" }, body: text }); if (!res.ok) { alert(await res.text()); return; } const importedSpec = await res.json(); const extend = apis.length > 0 && window.confirm("Extend the currently selected API?\n\nOK = Extend Current API\nCancel = Import As New API (default)"); if (extend) { spec.groups = [...(spec.groups||[]), ...(importedSpec.groups||[])]; spec.schemas = Object.assign(spec.schemas||{}, importedSpec.schemas||{}); } else { apis.push(importedSpec); activeApiIndex = apis.length - 1; spec = importedSpec; } refreshApiSelector(); if(spec.info && spec.info.title) el.brandTitle.textContent = spec.info.title; if(spec.info && spec.info.version) el.brandVersion.textContent = spec.info.version; activeEndpoint = null; renderNav(""); renderOverview(); renderTryEmpty(); }); function download(url, filename) { const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } exportDoxaApiBtn?.addEventListener("click", () => download("doxaApi.json", "DoxaApi.json")); exportOpenApiBtn?.addEventListener("click", () => download("openapi.json", "openapi.json")); exportSwaggerBtn?.addEventListener("click", () => download("swagger.json", "swagger.json")); el.nav.addEventListener("click", (e) => { const overviewBtn = e.target.closest("[data-overview]"); if (overviewBtn) { activeEndpoint = null; document.querySelectorAll(".nav-endpoint").forEach((b) => b.classList.remove("active")); overviewBtn.classList.add("active"); renderOverview(); renderTryEmpty(); return; } const groupToggle = e.target.closest("[data-toggle-group]"); if (groupToggle) { const name = groupToggle.dataset.toggleGroup; if (collapsedGroups.has(name)) collapsedGroups.delete(name); else collapsedGroups.add(name); localStorage.setItem("DoxaApi.collapsed", JSON.stringify([...collapsedGroups])); renderNav(el.search.value); return; } const endpointBtn = e.target.closest("[data-op]"); if (endpointBtn) { const opId = endpointBtn.dataset.op; for (const group of spec.groups) { const endpoint = group.endpoints.find((ep) => ep.operationId === opId); if (endpoint) { selectEndpoint(group, endpoint); break; } } } }); el.detail.addEventListener("click", (e) => { const row = e.target.closest("[data-op]"); if (!row) return; const opId = row.dataset.op; for (const group of spec.groups) { const endpoint = group.endpoints.find((ep) => ep.operationId === opId); if (endpoint) { selectEndpoint(group, endpoint); break; } } }); el.search.addEventListener("input", () => renderNav(el.search.value)); document.addEventListener("keydown", (e) => { if (e.key === "/" && document.activeElement !== el.search) { e.preventDefault(); el.search.focus(); } if (e.key === "Escape" && document.activeElement === el.search) { el.search.blur(); } }); el.themeToggle.addEventListener("click", () => { const root = document.documentElement; const current = root.getAttribute("data-theme"); const isDark = current === "dark" || (current === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches); const next = isDark ? "light" : "dark"; root.setAttribute("data-theme", next); localStorage.setItem("DoxaApi.theme", next); }); const savedTheme = localStorage.getItem("DoxaApi.theme"); if (savedTheme) document.documentElement.setAttribute("data-theme", savedTheme); } // Helpers function escapeHtml(str) { return String(str ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[c])); } function escapeAttr(str) { return escapeHtml(str); } init(); })(); window.DoxaApiPostmanLike = { methods:["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"], buildRequest:function(){ return { method: document.getElementById("customMethod")?.value || "GET", url: document.getElementById("customUrl")?.value || "", headers: [] }; } };