const httpService = { baseUrl: window.GITEA_SUB_URL || "", async fetchRss() { const resp = await fetch(`${this.baseUrl}/alex.rss`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const text = await resp.text(); return new DOMParser().parseFromString(text, "application/xml"); }, }; const dataDomain = { timeAgo(dateStr) { const diff = (Date.now() - new Date(dateStr)) / 1000; if (diff < 60) return "just now"; if (diff < 3600) return Math.floor(diff / 60) + "m ago"; if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; if (diff < 2592000) return Math.floor(diff / 86400) + "d ago"; if (diff < 31536000) return Math.floor(diff / 2592000) + "mo ago"; return Math.floor(diff / 31536000) + "y ago"; }, esc(str) { return (str || "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); }, safeTitleHtml(rawTitleText) { const doc = new DOMParser().parseFromString(rawTitleText, "text/html"); doc.body .querySelectorAll("*:not(a)") .forEach((el) => el.replaceWith(el.textContent)); return doc.body.innerHTML; }, titlePlainText(rawTitleText) { const doc = new DOMParser().parseFromString(rawTitleText, "text/html"); return doc.body.textContent || rawTitleText; }, activityIcon(titleText) { const t = titleText.toLowerCase(); if (t.includes("push") || t.includes("commit")) return "📤"; if (t.includes("creat") && t.includes("repo")) return "📁"; if (t.includes("fork")) return "🍴"; if (t.includes("open") && t.includes("issue")) return "🔴"; if (t.includes("clos") && t.includes("issue")) return "🟢"; if (t.includes("pull request") || t.includes("merge")) return "🔀"; if (t.includes("tag")) return "🏷️"; if (t.includes("branch")) return "🌿"; if (t.includes("comment")) return "💬"; if (t.includes("release")) return "🚀"; return "⚡"; }, parseCommits(descriptionText) { const doc = new DOMParser().parseFromString(descriptionText, "text/html"); return Array.from(doc.querySelectorAll("a")).map((anchor) => { const sha = anchor.textContent.trim().slice(0, 7); const href = anchor.getAttribute("href") || "#"; let msg = ""; let node = anchor.nextSibling; while (node) { const t = (node.textContent || "").trim(); if (t) { msg = t; break; } node = node.nextSibling; } return { sha, href, msg }; }); }, parseRepos(xmlDoc) { const items = Array.from(xmlDoc.querySelectorAll("channel > item")); const seen = new Map(); for (const item of items) { const titleHtml = item.querySelector("title")?.textContent || ""; const titleDoc = new DOMParser().parseFromString(titleHtml, "text/html"); const anchors = titleDoc.querySelectorAll("a"); if (anchors.length < 2) continue; const repoAnchor = anchors[anchors.length - 1]; const repoName = repoAnchor.textContent.trim(); if (!repoName || seen.has(repoName)) continue; seen.set(repoName, { repoName, repoUrl: repoAnchor.getAttribute("href") || "#", shortName: repoName.includes("/") ? repoName.split("/").pop() : repoName, pubDate: item.querySelector("pubDate")?.textContent || "", firstCommit: this.parseCommits( item.querySelector("description")?.textContent || "", )[0] || null, }); } return Array.from(seen.values()); }, parseActivity(xmlDoc, limit = 20) { return Array.from(xmlDoc.querySelectorAll("channel > item")) .slice(0, limit) .map((item) => { const rawTitle = item.querySelector("title")?.textContent || ""; const titleText = this.titlePlainText(rawTitle); return { titleHtmlSafe: this.safeTitleHtml(rawTitle), titleText, link: item.querySelector("link")?.textContent || "#", pubDate: item.querySelector("pubDate")?.textContent || "", icon: this.activityIcon(titleText), commits: this.parseCommits( item.querySelector("description")?.textContent || "", ).slice(0, 3), }; }); }, }; const uiRendering = { async renderRepos(xmlDoc) { const grid = document.getElementById("repo-grid"); if (!grid) return; const repos = dataDomain.parseRepos(xmlDoc); if (repos.length === 0) { grid.innerHTML = `