// JSON syntax highlighter function highlightJSON(json: string): string { return json .replace(/&/g, "&") .replace(//g, ">") .replace(/"([^"]+)":/g, '"$1":') .replace(/: "([^"]*)"/g, ': "$1"') .replace(/: (\d+\.?\d*)/g, ': $1') .replace(/: (true|false|null)/g, ': $1'); } // HTML/CSS syntax highlighter function highlightHTMLCSS(code: string): string { // First escape HTML entities let highlighted = code .replace(/&/g, "&") .replace(//g, ">"); // HTML comments highlighted = highlighted.replace( /<!--(.*?)-->/g, '<!--$1-->', ); // Split by `; if (buttonCodeEl) { const highlighted = highlightHTMLCSS(buttonCodeRaw); buttonCodeEl.innerHTML = highlighted; } // Auto-fill redirect URI with current page URL const currentUrl = window.location.origin + window.location.pathname; redirectUriInput.value = currentUrl; // Auto-fill client ID with a test URL clientIdInput.value = window.location.origin; // Update documentation examples with current origin const origin = window.location.origin; const authUrlEl = document.getElementById("authUrl"); const tokenUrlEl = document.getElementById("tokenUrl"); const profileMeUrlEl = document.getElementById("profileMeUrl"); if (authUrlEl) authUrlEl.textContent = `${origin}/auth/authorize`; if (tokenUrlEl) tokenUrlEl.textContent = `${origin}/auth/token`; if (profileMeUrlEl) profileMeUrlEl.textContent = `"${origin}/u/username"`; // Check if we're handling a callback const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); const state = urlParams.get("state"); const error = urlParams.get("error"); if (error) { // OAuth error response showResult( `Error: ${error}\n${urlParams.get("error_description") || ""}`, "error", ); resultSection.style.display = "block"; } else if (code && state) { // We have a callback with authorization code handleCallback(code, state); } // Start OAuth flow startBtn.addEventListener("click", async () => { const clientId = clientIdInput.value.trim(); const redirectUri = redirectUriInput.value.trim(); if (!clientId || !redirectUri) { alert("Please fill in client ID and redirect URI"); return; } // Get selected scopes const scopeCheckboxes = document.querySelectorAll( 'input[name="scope"]:checked', ); const scopes = Array.from(scopeCheckboxes).map( (cb) => (cb as HTMLInputElement).value, ); if (scopes.length === 0) { alert("Please select at least one scope"); return; } // Generate PKCE parameters const codeVerifier = generateRandomString(64); const codeChallenge = await sha256(codeVerifier); const state = generateRandomString(32); // Store PKCE values in localStorage for callback localStorage.setItem("oauth_code_verifier", codeVerifier); localStorage.setItem("oauth_state", state); localStorage.setItem("oauth_client_id", clientId); localStorage.setItem("oauth_redirect_uri", redirectUri); // Build authorization URL const authUrl = new URL("/auth/authorize", window.location.origin); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", codeChallenge); authUrl.searchParams.set("code_challenge_method", "S256"); authUrl.searchParams.set("scope", scopes.join(" ")); // Redirect to authorization endpoint window.location.href = authUrl.toString(); }); // Handle OAuth callback function handleCallback(code: string, state: string) { const storedState = localStorage.getItem("oauth_state"); if (state !== storedState) { showResult("Error: State mismatch (CSRF attack?)", "error"); resultSection.style.display = "block"; return; } callbackSection.style.display = "block"; callbackInfo.innerHTML = `
Authorization Code:${code}
State: ${state} ✓ (verified)
${highlightedJson}`;
} catch {
resultDiv.textContent = text;
}
} else {
resultDiv.textContent = text;
}
resultDiv.className = `result show ${type}`;
}
// Convert HTML documentation to Markdown by parsing the DOM
function extractMarkdown(): string {
const lines: string[] = [];
// Get title and subtitle from header
const h1 = document.querySelector("header h1");
const subtitle = document.querySelector("header .subtitle");
if (h1) {
lines.push(`# ${h1.textContent}`);
lines.push("");
}
if (subtitle) {
lines.push(subtitle.textContent || "");
lines.push("");
}
// Process each section (skip TOC and OAuth tester)
const sections = document.querySelectorAll(".section");
sections.forEach((section) => {
// Skip the OAuth tester section
if (section.id === "tester") return;
processElement(section, lines);
lines.push("");
});
return lines.join("\n");
}
function processElement(el: Element, lines: string[], indent = 0): void {
const tag = el.tagName.toLowerCase();
// Headers
if (tag === "h2") {
lines.push(`## ${el.textContent}`);
lines.push("");
} else if (tag === "h3") {
lines.push(`### ${el.textContent}`);
lines.push("");
}
// Paragraphs
else if (tag === "p") {
lines.push(el.textContent || "");
lines.push("");
}
// Lists
else if (tag === "ul" || tag === "ol") {
const items = el.querySelectorAll(":scope > li");
items.forEach((li, i) => {
const prefix = tag === "ol" ? `${i + 1}. ` : "- ";
const text = getTextContent(li);
lines.push(`${prefix}${text}`);
});
lines.push("");
}
// Tables
else if (tag === "table") {
const headers: string[] = [];
const rows: string[][] = [];
// Get headers
el.querySelectorAll("thead th").forEach((th) => {
headers.push(th.textContent?.trim() || "");
});
// Get rows
el.querySelectorAll("tbody tr").forEach((tr) => {
const row: string[] = [];
tr.querySelectorAll("td").forEach((td) => {
row.push(td.textContent?.trim() || "");
});
rows.push(row);
});
// Format as markdown table
if (headers.length > 0) {
lines.push(`| ${headers.join(" | ")} |`);
lines.push(`|${headers.map(() => "-------").join("|")}|`);
rows.forEach((row) => {
lines.push(`| ${row.join(" | ")} |`);
});
lines.push("");
}
}
// Code blocks
else if (tag === "pre") {
const code = el.querySelector("code");
if (code) {
// Detect language from class or content
let lang = "";
const text = code.textContent || "";
if (text.includes("GET ") || text.includes("POST ")) {
lang = "http";
} else if (text.includes("{") && text.includes('"')) {
lang = "json";
}
lines.push(`\`\`\`${lang}`);
lines.push(text.trim());
lines.push("```");
lines.push("");
}
}
// Info boxes
else if (el.classList.contains("info-box")) {
const strong = el.querySelector("strong");
const text = el.textContent?.trim() || "";
if (strong) {
// Extract content after the strong tag
const afterStrong = text
.substring(strong.textContent?.length || 0)
.trim();
lines.push(`> **${strong.textContent}** ${afterStrong}`);
} else {
lines.push(`> ${text}`);
}
lines.push("");
}
// Process children for sections and divs
else if (tag === "section" || tag === "div") {
Array.from(el.children).forEach((child) => {
processElement(child, lines, indent);
});
}
}
// Get text content, preserving inline code formatting
function getTextContent(el: Element): string {
let text = "";
el.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
} else if (node.nodeType === Node.ELEMENT_NODE) {
const elem = node as Element;
if (elem.tagName.toLowerCase() === "code") {
text += `\`${elem.textContent}\``;
} else if (elem.tagName.toLowerCase() === "strong") {
text += `**${elem.textContent}**`;
} else {
text += elem.textContent;
}
}
});
return text.trim();
}
// Copy markdown to clipboard
copyMarkdownBtn.addEventListener("click", async () => {
const markdown = extractMarkdown();
try {
await navigator.clipboard.writeText(markdown);
copyMarkdownBtn.textContent = "copied! ✓";
setTimeout(() => {
copyMarkdownBtn.textContent = "copy as markdown";
}, 2000);
} catch (error) {
console.error("Failed to copy:", error);
alert("Failed to copy to clipboard");
}
});
// Copy button code to clipboard
copyButtonCodeBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(buttonCodeRaw);
copyButtonCodeBtn.textContent = "copied! ✓";
setTimeout(() => {
copyButtonCodeBtn.textContent = "copy button code";
}, 2000);
} catch (error) {
console.error("Failed to copy:", error);
alert("Failed to copy to clipboard");
}
});
// Add interactive hover effect to demo button
demoButton.addEventListener("click", (e) => {
e.preventDefault();
});