diff options
Diffstat (limited to 'cgithub')
| -rw-r--r-- | cgithub/src/index.ts | 23 | ||||
| -rw-r--r-- | cgithub/src/scraper.ts | 77 | ||||
| -rw-r--r-- | cgithub/static/style.css | 14 | ||||
| -rw-r--r-- | cgithub/views/blob.eta | 37 | ||||
| -rw-r--r-- | cgithub/views/tree.eta | 2 |
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> |
