diff options
| -rw-r--r-- | src/index.ts | 8 | ||||
| -rw-r--r-- | src/scraper.ts | 51 | ||||
| -rw-r--r-- | views/_tree.eta | 14 | ||||
| -rw-r--r-- | views/repo.eta | 23 | ||||
| -rw-r--r-- | views/tree.eta | 15 |
5 files changed, 96 insertions, 15 deletions
diff --git a/src/index.ts b/src/index.ts index fcd0069..393c5ec 100644 --- a/src/index.ts +++ b/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/src/scraper.ts b/src/scraper.ts index d6702cc..a405139 100644 --- a/src/scraper.ts +++ b/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/views/_tree.eta b/views/_tree.eta new file mode 100644 index 0000000..ac7b2b1 --- /dev/null +++ b/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/views/repo.eta b/views/repo.eta new file mode 100644 index 0000000..0b345df --- /dev/null +++ b/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/views/tree.eta b/views/tree.eta index 911b7ee..7068754 100644 --- a/views/tree.eta +++ b/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) %> |
