summaryrefslogtreecommitdiff
path: root/cgithub
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-01-25 16:27:48 -0500
committerJoe Mou <dev@mou.fo>2026-01-25 17:46:15 -0500
commit0391e279f80f6406502b4069bf2e13d3f13ed56e (patch)
treebef2cd202af53fa1f394b2f26b2e9eb01bc985b4 /cgithub
parentafbacd71aca76b6fa2e671351d396f071f9fb3be (diff)
Repository page with info
Diffstat (limited to 'cgithub')
-rw-r--r--cgithub/src/index.ts8
-rw-r--r--cgithub/src/scraper.ts51
-rw-r--r--cgithub/views/_tree.eta14
-rw-r--r--cgithub/views/repo.eta23
-rw-r--r--cgithub/views/tree.eta15
5 files changed, 96 insertions, 15 deletions
diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts
index fcd0069..393c5ec 100644
--- a/cgithub/src/index.ts
+++ b/cgithub/src/index.ts
@@ -4,7 +4,7 @@ import { Eta } from "eta";
import { Hono } from "hono";
import * as path from "node:path";
import * as url from "node:url";
-import { getGitHubBlob, getGitHubTree } from "./scraper.js";
+import { getGitHubBlob, getGitHubRepo, getGitHubTree } from "./scraper.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
@@ -13,6 +13,12 @@ const eta = new Eta({ views: path.join(__dirname, "..", "views") });
app.use("/static/*", serveStatic({ root: path.join(__dirname, "..") }));
+app.get("/:owner/:repo", async (c) => {
+ const { owner, repo } = c.req.param();
+ const data = await getGitHubRepo(owner, repo);
+ return c.html(await eta.renderAsync("repo.eta", data));
+});
+
app.get("/:owner/:repo/tree/:branch/:path{.*}?", async (c) => {
const { owner, repo, branch, path = "" } = c.req.param();
const data = await getGitHubTree(owner, repo, branch, path);
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index d6702cc..a405139 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/src/scraper.ts
@@ -37,6 +37,18 @@ export interface GitHubTree extends GitHubCommon {
items: TreeItem[];
}
+export interface RepoInfo {
+ description: string | null;
+ website: string | null;
+ stars: string | null;
+ watchers: string | null;
+ forks: string | null;
+}
+
+export interface GitHubRepo extends GitHubTree {
+ info: RepoInfo;
+}
+
export interface GitHubBlob extends GitHubCommon {
language: string | null;
size: number;
@@ -84,6 +96,25 @@ function parsePayload(
return null;
}
+function parseRepoInfo(html: string): RepoInfo {
+ const descMatch = html.match(/<p[^>]*class="[^"]*f4[^"]*"[^>]*>([\s\S]*?)<\/p>/);
+ const description = descMatch?.[1]?.replace(/<[^>]+>/g, "").trim() || null;
+ const websiteMatch = html.match(
+ /<a[^>]*rel="noopener noreferrer nofollow"[^>]*href="([^"]+)"/,
+ );
+ const starsMatch = html.match(/id="repo-stars-counter-star"[^>]*>([^<]+)/);
+ const watchersMatch = html.match(/<strong>(\d+)<\/strong>\s*watching/);
+ const forksMatch = html.match(/id="repo-network-counter"[^>]*>([^<]+)/);
+
+ return {
+ description,
+ website: websiteMatch?.[1] || null,
+ stars: starsMatch?.[1]?.trim() || null,
+ watchers: watchersMatch?.[1] || null,
+ forks: forksMatch?.[1]?.trim() || null,
+ };
+}
+
function extractGitHub<T>(payload: RawPayload, extra: T): GitHubCommon & T {
return {
repo: {
@@ -96,6 +127,26 @@ function extractGitHub<T>(payload: RawPayload, extra: T): GitHubCommon & T {
};
}
+export async function getGitHubRepo(
+ owner: string,
+ repo: string,
+): Promise<GitHubRepo> {
+ const html = await fetchGitHubPage(`${owner}/${repo}`);
+
+ const payload = parsePayload(html, "react-partial.embeddedData", [
+ "props",
+ "initialPayload",
+ ]);
+
+ if (payload?.tree === undefined) {
+ throw new Error("Could not find tree data in embedded JSON");
+ }
+
+ const info = parseRepoInfo(html);
+
+ return extractGitHub(payload, { items: payload.tree.items, info });
+}
+
export async function getGitHubTree(
owner: string,
repo: string,
diff --git a/cgithub/views/_tree.eta b/cgithub/views/_tree.eta
new file mode 100644
index 0000000..ac7b2b1
--- /dev/null
+++ b/cgithub/views/_tree.eta
@@ -0,0 +1,14 @@
+<table>
+ <% for (const item of it.items) { %>
+ <tr>
+ <td>
+ <span class="icon"><%= item.contentType === 'directory' ? '📁' : '📄' %></span>
+ <% if (item.contentType === 'directory') { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
+ <% } else { %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/blob/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
+ <% } %>
+ </td>
+ </tr>
+ <% } %>
+</table>
diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta
new file mode 100644
index 0000000..0b345df
--- /dev/null
+++ b/cgithub/views/repo.eta
@@ -0,0 +1,23 @@
+<% layout("./layout") %>
+
+<% if (it.info) { %>
+ <div class="repo-info">
+ <% if (it.info.description) { %>
+ <div class="description"><%= it.info.description %></div>
+ <% } %>
+ <% if (it.info.website) { %>
+ <div><a href="<%= it.info.website %>" rel="noopener noreferrer"><%= it.info.website %></a></div>
+ <% } %>
+ <% if (it.info.stars) { %>
+ <span>★ <%= it.info.stars %></span>
+ <% } %>
+ <% if (it.info.watchers) { %>
+ <span>👁 <%= it.info.watchers %></span>
+ <% } %>
+ <% if (it.info.forks) { %>
+ <span>🔀 <%= it.info.forks %></span>
+ <% } %>
+ </div>
+<% } %>
+
+<%~ include("./_tree", it) %>
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta
index 911b7ee..7068754 100644
--- a/cgithub/views/tree.eta
+++ b/cgithub/views/tree.eta
@@ -1,16 +1,3 @@
<% layout("./layout") %>
-<table>
- <% for (const item of it.items) { %>
- <tr>
- <td>
- <span class="icon"><%= item.contentType === 'directory' ? '📁' : '📄' %></span>
- <% if (item.contentType === 'directory') { %>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
- <% } else { %>
- <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/blob/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
- <% } %>
- </td>
- </tr>
- <% } %>
-</table>
+<%~ include("./_tree", it) %>