summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cgithub/src/index.ts23
-rw-r--r--cgithub/src/scraper.ts77
-rw-r--r--cgithub/static/style.css14
-rw-r--r--cgithub/views/blob.eta37
-rw-r--r--cgithub/views/tree.eta2
5 files changed, 151 insertions, 2 deletions
diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts
index df0a116..a546f13 100644
--- a/cgithub/src/index.ts
+++ b/cgithub/src/index.ts
@@ -2,7 +2,7 @@ import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Eta } from "eta";
-import { getGitHubTree } from "./scraper.js";
+import { getGitHubTree, getGitHubBlob } from "./scraper.js";
const app = new Hono();
const eta = new Eta({ views: "./views" });
@@ -30,6 +30,27 @@ app.get("/:owner/:repo/tree/:branch/:path{.*}?", async (c) => {
}
});
+app.get("/:owner/:repo/blob/:branch/:path{.*}", async (c) => {
+ try {
+ const owner = c.req.param("owner");
+ const repo = c.req.param("repo");
+ const branch = c.req.param("branch");
+ const path = c.req.param("path");
+
+ const data = await getGitHubBlob(owner, repo, branch, path);
+ const html = await eta.renderAsync("blob.eta", data);
+
+ return c.html(html);
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : "Unknown error";
+ return c.html(
+ `<html><body><h1>Error</h1><p>${errorMessage}</p></body></html>`,
+ 500,
+ );
+ }
+});
+
const port = parseInt(process.env.PORT || "3000", 10);
serve({ fetch: app.fetch, port }, (info) => {
console.log(`Server running on http://localhost:${info.port}`);
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index add03b2..fa8d655 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/src/scraper.ts
@@ -15,6 +15,20 @@ export interface GitHubTree {
};
}
+export interface GitHubBlob {
+ path: string;
+ branch: string;
+ content: string;
+ rawContent: string;
+ language: string | null;
+ size: number;
+ repo: {
+ name: string;
+ owner: string;
+ isPublic: boolean;
+ };
+}
+
export async function getGitHubTree(
owner: string,
repo: string,
@@ -89,3 +103,66 @@ export async function getGitHubTree(
},
};
}
+
+export async function getGitHubBlob(
+ owner: string,
+ repo: string,
+ branch: string,
+ path: string,
+): Promise<GitHubBlob> {
+ const url = `https://github.com/${owner}/${repo}/blob/${branch}/${path}`;
+
+ const response = await fetch(url, {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
+ Accept:
+ "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.5",
+ },
+ });
+ const html = await response.text();
+
+ // GitHub embeds file data in a JSON script tag
+ let payload;
+
+ const match = html.match(
+ /<script type="application\/json" data-target="react-app\.embeddedData">([^<]+)<\/script>/,
+ );
+ if (match) {
+ try {
+ const data = JSON.parse(match[1]);
+ if (data.payload?.blob) {
+ payload = data.payload;
+ }
+ } catch (e) {
+ // Skip invalid JSON
+ }
+ }
+
+ if (!payload) {
+ throw new Error("Could not find blob data in embedded JSON");
+ }
+
+ const blob = payload.blob;
+ const rawLines = blob.rawLines || [];
+ const rawContent = rawLines.join("\n");
+ const highlightedLines = blob.highlightedLines || [];
+ const content = highlightedLines
+ .map((line: any) => line.text || "")
+ .join("\n");
+
+ return {
+ path: payload.path,
+ branch: payload.refInfo.name,
+ content: content || rawContent,
+ rawContent,
+ language: blob.language || null,
+ size: blob.size,
+ repo: {
+ name: payload.repo.name,
+ owner: payload.repo.ownerLogin,
+ isPublic: payload.repo.public,
+ },
+ };
+}
diff --git a/cgithub/static/style.css b/cgithub/static/style.css
index 3e19563..bd36dce 100644
--- a/cgithub/static/style.css
+++ b/cgithub/static/style.css
@@ -1,6 +1,16 @@
body {
margin: 0;
padding: 1rem;
+ font-family: Helvetica, Arial, sans-serif;
+}
+
+pre.blob {
+ margin: 1rem 0;
+ padding: 1rem;
+ border: 1px solid #d0d7de;
+ border-radius: 6px;
+ overflow-x: auto;
+ font-family: monospace;
}
@media (prefers-color-scheme: dark) {
@@ -11,4 +21,8 @@ body {
a {
color: #4493f8;
}
+ .blob {
+ border-color: #30363d;
+ background: #161b22;
+ }
}
diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta
new file mode 100644
index 0000000..d3c6a56
--- /dev/null
+++ b/cgithub/views/blob.eta
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title><%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path %></title>
+ <link rel="stylesheet" href="/static/style.css">
+</head>
+<body>
+ <header>
+ <h1><%= it.repo.owner %>/<%= it.repo.name %></h1>
+ <div>branch: <%= it.branch %></div>
+ <div>
+ path: <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>">root</a><%
+ const pathParts = it.path.split('/').filter(p => p);
+ let currentPath = '';
+ for (let i = 0; i < pathParts.length; i++) {
+ const part = pathParts[i];
+ currentPath += (currentPath ? '/' : '') + part;
+ const isLast = i === pathParts.length - 1;
+ if (isLast) {
+ %>/<%= part %><%
+ } else {
+ %>/<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>/<%= currentPath %>"><%= part %></a><%
+ }
+ } %>
+ </div>
+ <% if (it.language) { %>
+ <div>language: <%= it.language %></div>
+ <% } %>
+ <div>size: <%= it.size %> bytes</div>
+ </header>
+
+ <pre class="blob"><code><%= it.rawContent %></code></pre>
+
+ <footer>Generated by cgithub</footer>
+</body>
+</html>
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta
index 4194216..b49668d 100644
--- a/cgithub/views/tree.eta
+++ b/cgithub/views/tree.eta
@@ -30,7 +30,7 @@
<% if (item.contentType === 'directory') { %>
<a href="/<%= it.repo.owner %>/<%= it.repo.name %>/tree/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
<% } else { %>
- <%= item.name %>
+ <a href="/<%= it.repo.owner %>/<%= it.repo.name %>/blob/<%= it.branch %>/<%= item.path %>"><%= item.name %></a>
<% } %>
</td>
</tr>