const std = @import("std"); const config = @import("../../core/config.zig"); const model = @import("model.zig"); const Endpoint = model.Endpoint; const endpoints = model.endpoints; pub fn render(allocator: std.mem.Allocator) ![]const u8 { var group_buttons: std.Io.Writer.Allocating = .init(allocator); defer group_buttons.deinit(); try group_buttons.writer.print("", .{endpoints.len}); for (model.group_order) |group| { const total = model.groupCount(group); if (total == 0) continue; try group_buttons.writer.print( "", .{ std.json.fmt(group, .{}), try model.escapeHtml(allocator, group), total }, ); } const sorted = try allocator.alloc(Endpoint, endpoints.len); defer allocator.free(sorted); @memcpy(sorted, endpoints[0..]); std.mem.sort(Endpoint, sorted, {}, compareEndpoints); var cards: std.Io.Writer.Allocating = .init(allocator); defer cards.deinit(); for (sorted, 0..) |endpoint, i| { try renderEndpointCard(allocator, &cards.writer, endpoint, i == 0); } const public_url = config.publicUrl(); const public_host = displayHost(public_url); return std.fmt.allocPrint(allocator, \\ \\ \\ \\ \\ \\ \\ zds api \\ \\ \\ \\ \\ \\ \\
\\
\\ zds \\ \\
\\
\\

{s}

\\

api reference

\\

this pds exposes {d} documented http operations, including xrpc methods at /xrpc/{{NSID}}.

\\
\\ {d} operations \\ {d} groups \\ {d} xrpc methods \\ {d} experimental \\ {s} permissioned data \\
\\
\\
\\ \\
{s}
\\
\\
{s}
\\
\\ \\ \\ , .{ try model.escapeHtml(allocator, public_url), css, try model.escapeHtml(allocator, public_host), endpoints.len, endpoints.len, model.groupCountTotal(), model.xrpcCount(), model.experimentalCount(), if (config.permissionedData()) "enabled" else "disabled", group_buttons.written(), cards.written(), script, }); } fn displayHost(url: []const u8) []const u8 { if (std.mem.startsWith(u8, url, "https://")) return url["https://".len..]; if (std.mem.startsWith(u8, url, "http://")) return url["http://".len..]; return url; } fn compareEndpoints(_: void, a: Endpoint, b: Endpoint) bool { const group_a = groupRank(a.group); const group_b = groupRank(b.group); if (group_a != group_b) return group_a < group_b; const path_order = std.mem.order(u8, a.path, b.path); if (path_order != .eq) return path_order == .lt; return methodRank(a.method) < methodRank(b.method); } fn groupRank(group: []const u8) usize { for (model.group_order, 0..) |ordered, i| { if (std.mem.eql(u8, group, ordered)) return i; } return model.group_order.len; } fn methodRank(method: []const u8) usize { if (std.mem.eql(u8, method, "GET")) return 0; if (std.mem.eql(u8, method, "HEAD")) return 1; if (std.mem.eql(u8, method, "POST")) return 2; return 3; } fn renderEndpointCard(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint, open: bool) !void { const method_class = try model.lower(allocator, endpoint.method); const details_open = if (open) " open" else ""; const nsid = endpoint.nsid(); const curl = try renderCurl(allocator, endpoint); const experimental = model.isExperimental(endpoint); const experimental_badge = if (experimental) "experimental" else ""; const search_text = try std.fmt.allocPrint( allocator, "{s} {s} {s} {s} {s} {s}", .{ endpoint.method, endpoint.path, endpoint.group, endpoint.auth, endpoint.summary, if (experimental) "experimental" else "" }, ); try writer.print( \\
\\ {s}{s}{s}{s} \\
\\

{s}

, .{ std.json.fmt(endpoint.group, .{}), std.json.fmt(endpoint.method, .{}), std.json.fmt(search_text, .{}), details_open, method_class, try model.escapeHtml(allocator, endpoint.method), try model.escapeHtml(allocator, endpoint.path), experimental_badge, try model.escapeHtml(allocator, endpoint.auth), try model.escapeHtml(allocator, endpoint.summary), }); if (nsid) |id| { try writer.print("
NSID{s}
", .{try model.escapeHtml(allocator, id)}); } try renderMetadata(allocator, writer, endpoint); if (experimental) { try writer.writeAll("

Experimental. Availability is controlled by ZDS_PERMISSIONED_DATA; semantics may change while the protocol is still being shaped.

"); } if (endpoint.notes.len > 0) { try writer.print("

{s}

", .{try model.escapeHtml(allocator, endpoint.notes)}); } try writer.print( \\
\\ \\
{s}
\\
, .{ try model.escapeHtml(allocator, curl), try model.escapeHtml(allocator, curl) }); try writer.writeAll("
"); } fn renderList(allocator: std.mem.Allocator, writer: *std.Io.Writer, label: []const u8, items: []const []const u8) !void { if (items.len == 0) return; try writer.print("

{s}

", .{label}); try writer.writeAll("
"); } fn renderMetadata(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint) !void { if (endpoint.params.len == 0 and endpoint.body.len == 0 and endpoint.response.len == 0) { try writer.writeAll("

No field metadata yet.

"); return; } try writer.writeAll("
"); try renderList(allocator, writer, "Query", endpoint.params); try renderList(allocator, writer, "Body", endpoint.body); try renderList(allocator, writer, "Response", endpoint.response); try writer.writeAll("
"); } fn renderCurl(allocator: std.mem.Allocator, endpoint: Endpoint) ![]const u8 { const token = if (std.mem.indexOf(u8, endpoint.auth, "bearer") != null or std.mem.indexOf(u8, endpoint.auth, "service") != null) " \\\n -H 'authorization: Bearer $TOKEN'" else ""; const body = if (std.mem.eql(u8, endpoint.method, "POST")) " \\\n -H 'content-type: application/json' \\\n --data '{}'" else ""; return std.fmt.allocPrint( allocator, "curl -sS -X {s}{s} \"{s}{s}\"{s}", .{ endpoint.method, token, config.publicUrl(), endpoint.path, body }, ); } const css = \\:root{--bg:#070807;--panel:#101210;--panel-2:#151915;--line:#273026;--text:#f0f1ec;--muted:#aaa69b;--dim:#69645c;--accent:#91adff;--green:#39c178;--pink:#f05f8e;--focus:#c9d7ff;--glass:rgba(255,255,255,.085);--accent-bg:color-mix(in srgb,var(--green) 13%,var(--bg));--accent-panel:color-mix(in srgb,var(--green) 9%,var(--panel));--accent-panel-2:color-mix(in srgb,var(--accent) 8%,var(--panel-2));color-scheme:dark} \\*{box-sizing:border-box}html{min-height:100%;background:var(--accent-bg);overflow-x:hidden}body{position:relative;margin:0;min-height:100vh;overflow-x:hidden;background:radial-gradient(circle at 20% 0%,color-mix(in srgb,var(--green) 15%,transparent),transparent 35%),radial-gradient(circle at 94% 18%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 32%),linear-gradient(135deg,var(--accent-bg),var(--bg) 58%);color:var(--text);font:15px/1.55 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}body::before,body::after{content:"";position:fixed;inset:-18vmax;z-index:0;pointer-events:none}body::before{background:linear-gradient(108deg,transparent 0 22%,rgba(255,255,255,.10) 24%,transparent 27% 52%,rgba(255,255,255,.055) 54%,transparent 58%),linear-gradient(72deg,transparent 0 38%,color-mix(in srgb,var(--accent) 11%,transparent) 44%,transparent 52%);filter:blur(20px) saturate(1.15);opacity:.58;transform:rotate(-4deg)}body::after{background:radial-gradient(ellipse at 22% 32%,rgba(255,255,255,.09),transparent 28%),radial-gradient(ellipse at 72% 18%,color-mix(in srgb,var(--green) 14%,transparent),transparent 31%),radial-gradient(ellipse at 66% 78%,color-mix(in srgb,var(--pink) 12%,transparent),transparent 30%);filter:blur(34px);opacity:.44}a{color:inherit}.shell{position:relative;z-index:1;width:min(100%,780px);margin:0 auto;padding:18px 16px 46px}header{position:relative;z-index:10;display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:50px;margin:0 0 26px;padding:8px 0;color:var(--muted);font-size:12px}.brand{flex:0 0 auto;display:inline-flex;align-items:center;color:var(--text);text-decoration:none;font-weight:700;font-size:14px;letter-spacing:0}.brand:hover{opacity:.82}nav{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:7px;padding:4px;border:1px solid color-mix(in srgb,var(--line) 58%,white 14%);border-radius:18px;background:linear-gradient(110deg,rgba(255,255,255,.16),transparent 22% 76%,rgba(255,255,255,.08)),radial-gradient(circle at 20% 0%,color-mix(in srgb,var(--green) 12%,transparent),transparent 46%),color-mix(in srgb,var(--accent-panel) 72%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.22),inset 0 -18px 42px rgba(255,255,255,.035),0 8px 28px rgba(0,0,0,.12);backdrop-filter:blur(26px) saturate(1.26);-webkit-backdrop-filter:blur(26px) saturate(1.26)}nav a{color:var(--muted);text-decoration:none}.icon-link,.nav-link{position:relative;min-width:34px;height:34px;display:inline-grid;place-items:center;border:1px solid color-mix(in srgb,var(--line) 74%,white 10%);border-radius:12px;background:radial-gradient(circle at 30% 18%,rgba(255,255,255,.20),transparent 32%),linear-gradient(145deg,rgba(255,255,255,.12),rgba(255,255,255,.03)),color-mix(in srgb,var(--accent-panel) 74%,transparent);color:var(--muted);text-decoration:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 10px 28px rgba(0,0,0,.14)}.nav-link{padding:0 11px}.logo-link::after{content:"";position:absolute;inset:5px;border-radius:9px;background:color-mix(in srgb,var(--glass) 62%,var(--accent-panel));box-shadow:inset 0 1px 0 rgba(255,255,255,.18)}.icon-link img{position:relative;z-index:1;width:22px;height:22px;object-fit:contain;display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45))}.icon-link:hover,.nav-link:hover{color:var(--text);border-color:color-mix(in srgb,var(--green) 55%,var(--line));background:color-mix(in srgb,var(--accent-panel-2) 86%,transparent)} \\.intro{display:grid;gap:13px;margin:52px 0 26px}.eyebrow{margin:0;color:var(--green);font-size:12px;font-weight:700;text-transform:lowercase}.eyebrow::before{content:"/";color:var(--dim);margin-right:2px}h1{margin:0;color:var(--text);font-size:clamp(42px,10vw,82px);line-height:.95;letter-spacing:0;text-transform:lowercase}.lede{max-width:none;margin:0;color:var(--muted);font-size:15px}.lede code{color:#7dd3fc}.xrpc-expression,.route-expression{display:inline-flex;white-space:nowrap}.xrpc-expression{gap:.35ch}.stats{display:flex;gap:9px;flex-wrap:wrap;margin-top:5px}.stats span{border:1px solid color-mix(in srgb,var(--line) 70%,white 8%);border-radius:12px;padding:9px 11px;color:var(--muted);background:color-mix(in srgb,var(--accent-panel) 64%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.10)}.stats strong{color:var(--text)} \\.tools{position:relative;z-index:5;padding:12px 0 15px;margin-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--line) 58%,transparent)}.search{display:grid;gap:7px;margin-bottom:11px}.search span{font-size:11px;text-transform:lowercase;color:var(--dim)}.search input{width:100%;border:1px solid color-mix(in srgb,var(--line) 76%,white 6%);border-radius:12px;background:color-mix(in srgb,var(--panel) 86%,transparent);color:var(--text);padding:12px 13px;font:inherit;outline:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.08)}.search input:focus{border-color:var(--focus)}.chips{display:flex;flex-wrap:wrap;gap:7px}.chip{border:1px solid color-mix(in srgb,var(--line) 78%,white 6%);border-radius:999px;background:color-mix(in srgb,var(--accent-panel) 50%,transparent);color:var(--muted);padding:7px 10px;font:inherit;font-size:12px;cursor:pointer;text-transform:lowercase}.chip.active{background:var(--text);border-color:var(--text);color:var(--bg)}.chip span{opacity:.68} \\.endpoints{display:grid;gap:9px}.endpoint{border:1px solid color-mix(in srgb,var(--line) 76%,white 6%);border-radius:12px;background:color-mix(in srgb,var(--panel) 88%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.08),0 14px 38px rgba(0,0,0,.16);overflow:hidden}.endpoint[hidden]{display:none}.endpoint summary{list-style:none;display:grid;grid-template-columns:auto minmax(0,1fr);gap:11px;align-items:start;padding:13px 14px;cursor:pointer}.endpoint summary::-webkit-details-marker{display:none}.endpoint code{font-family:inherit;overflow-wrap:anywhere}.summary-main{min-width:0;display:grid;gap:7px}.summary-main>code{min-width:0;line-height:1.35;word-break:normal}.method{font-size:11px;font-weight:700;border-radius:8px;padding:4px 6px;color:#071008}.method.get{background:#7dd3fc}.method.post{background:var(--green)}.method.head{background:#f5d85d}.labels{display:flex;gap:6px;align-items:center;justify-content:flex-start;flex-wrap:wrap}.badge,.auth{font-size:11px;border:1px solid color-mix(in srgb,var(--line) 76%,white 5%);border-radius:999px;padding:4px 7px;white-space:nowrap;text-transform:lowercase}.auth{color:var(--muted)}.badge.experimental{color:#071008;background:color-mix(in srgb,var(--pink) 82%,white 16%);border-color:color-mix(in srgb,var(--pink) 84%,white 6%)}.body{border-top:1px solid color-mix(in srgb,var(--line) 78%,transparent);padding:14px;display:grid;gap:14px}.body p{margin:0;color:var(--muted);line-height:1.55}.experimental-copy{border:1px solid color-mix(in srgb,var(--pink) 42%,var(--line));border-radius:10px;padding:10px;background:color-mix(in srgb,var(--pink) 8%,transparent)}.experimental-copy code{color:var(--text)}.nsid{display:flex;gap:8px;align-items:center;color:var(--muted);min-width:0}.nsid span{font-size:10px;text-transform:lowercase;color:var(--pink)}.nsid code{color:var(--text);min-width:0}.meta-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}.meta-grid>div{border:1px solid color-mix(in srgb,var(--line) 78%,white 4%);border-radius:10px;padding:10px;background:rgba(255,255,255,.018)}h2{font-size:11px;text-transform:lowercase;color:var(--dim);font-weight:400;margin:0 0 7px}ul{margin:0;padding-left:18px}.muted{color:var(--dim)}.compact{font-size:12px}.note{border-left:2px solid var(--pink);padding-left:10px}.snippet{position:relative}.copy{position:absolute;right:8px;top:8px;border:1px solid color-mix(in srgb,var(--line) 78%,white 6%);border-radius:10px;background:color-mix(in srgb,var(--panel) 88%,transparent);color:var(--muted);font:inherit;font-size:11px;padding:5px 8px;cursor:pointer;text-transform:lowercase}.copy:hover,.copy:focus{color:var(--text);border-color:color-mix(in srgb,var(--green) 55%,var(--line));outline:none}.copy.done{background:var(--green);border-color:var(--green);color:#071008}pre{margin:0;white-space:pre-wrap;background:#090b09;color:#d6ead8;border-radius:10px;border:1px solid color-mix(in srgb,var(--line) 78%,white 4%);padding:13px 78px 13px 11px;overflow:auto;font:inherit;font-size:13px} \\@media (min-width:680px){.shell{padding-top:24px}nav{gap:8px}} \\@media (max-width:720px){.intro{margin-top:34px}.meta-grid{grid-template-columns:1fr}nav{justify-content:flex-end}.lede{font-size:14px}} \\@media (max-width:430px){.shell{padding-left:14px;padding-right:14px}header{gap:8px}nav{gap:4px;padding:3px;border-radius:16px}.icon-link,.nav-link{min-width:32px;height:32px;border-radius:10px}.logo-link::after{inset:5px;border-radius:8px}.icon-link img{width:20px;height:20px}.nav-link{padding:0 9px}} \\@media (prefers-color-scheme:light){:root{--bg:#f6f4ed;--panel:#fffdf7;--panel-2:#ece8dd;--line:#d8d0c0;--text:#161412;--muted:#625c53;--dim:#8f8679;--accent:#315dcb;--green:#1b7340;--pink:#b72e5c;--focus:#163f9f;--glass:rgba(255,255,255,.72);--accent-bg:color-mix(in srgb,var(--green) 11%,var(--bg));--accent-panel:color-mix(in srgb,var(--green) 5%,var(--panel));--accent-panel-2:color-mix(in srgb,var(--accent) 5%,var(--panel-2));color-scheme:light}pre{background:#101510;color:#ecf7ec}} ; const script = \\const search=document.querySelector("#search"); \\const chips=[...document.querySelectorAll(".chip")]; \\const cards=[...document.querySelectorAll(".endpoint")]; \\let group="all"; \\function apply(){ \\ const q=search.value.trim().toLowerCase(); \\ for(const card of cards){ \\ const groupMatch=group==="all"||card.dataset.group===group; \\ const text=(card.dataset.search||"").toLowerCase(); \\ card.hidden=!(groupMatch&&text.includes(q)); \\ } \\} \\search.addEventListener("input",apply); \\for(const chip of chips){ \\ chip.addEventListener("click",()=>{ \\ group=chip.dataset.group; \\ for(const other of chips) other.classList.toggle("active",other===chip); \\ apply(); \\ }); \\} \\for(const button of document.querySelectorAll(".copy")){ \\ button.addEventListener("click",async()=>{ \\ const ok=await copyText(button.dataset.copy||""); \\ button.classList.toggle("done",ok); \\ button.textContent=ok?"Copied":"Failed"; \\ setTimeout(()=>{button.classList.remove("done");button.textContent="Copy";},1400); \\ }); \\} \\async function copyText(text){ \\ if(typeof navigator!=="undefined"&&navigator.clipboard&&navigator.clipboard.writeText){ \\ try{await navigator.clipboard.writeText(text);return true;}catch(_){} \\ } \\ const area=document.createElement("textarea"); \\ area.value=text; \\ area.setAttribute("readonly",""); \\ area.style.position="fixed"; \\ area.style.opacity="0"; \\ document.body.appendChild(area); \\ area.select(); \\ let ok=false; \\ try{ok=document.execCommand("copy");}catch(_){} \\ area.remove(); \\ return ok; \\} ;