diff options
| author | Joe Mou <dev@mou.fo> | 2026-09-25 01:48:21 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-09-25 02:15:02 -0400 |
| commit | 226984ad63b063c362e18260a591a3f9a035a8d3 (patch) | |
| tree | 01d6a69ee1ba5234d74c83e78b0c85e44dc07904 /src | |
| parent | 181830285ac128ce0b5946b1c5d4e45fa51612ed (diff) | |
Show each repo nav item only if the GitHub page includes it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.test.ts | 20 | ||||
| -rw-r--r-- | src/scraper.test.ts | 13 | ||||
| -rw-r--r-- | src/scraper.ts | 53 |
3 files changed, 69 insertions, 17 deletions
diff --git a/src/app.test.ts b/src/app.test.ts index 59a2c61..e870666 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -92,6 +92,26 @@ describe("sidebar fragment", () => { }); }); +describe("repository navigation", () => { + const releases = { repo: { owner: "torvalds", name: "linux" }, releases: [] }; + + it("should only link to the tabs GitHub shows", () => { + const html = eta.render("releases.eta", { ...releases, tabs: ["code", "actions", "wiki"] }); + + assert.match(html, /href="\/torvalds\/linux\/wiki"/); + assert.doesNotMatch(html, /href="\/torvalds\/linux\/issues"/); + assert.doesNotMatch(html, /href="\/torvalds\/linux\/pulls"/); + assert.doesNotMatch(html, /href="\/torvalds\/linux\/discussions"/); + }); + + it("should link to every tab without GitHub's navigation to go by", () => { + const html = eta.render("releases.eta", { ...releases, tabs: null }); + + assert.match(html, /href="\/torvalds\/linux\/issues"/); + assert.match(html, /href="\/torvalds\/linux\/wiki"/); + }); +}); + describe("blob view", () => { const blob = { repo: { owner: "justrajdeep", name: "fonts" }, diff --git a/src/scraper.test.ts b/src/scraper.test.ts index a8debd7..83a82f0 100644 --- a/src/scraper.test.ts +++ b/src/scraper.test.ts @@ -45,6 +45,10 @@ describe("GitHub scraper", () => { assert.ok(data.items.length > 0); assert.ok(typeof data.overviewHtml?.["README.md"] === "string"); + + assert.ok(data.tabs?.includes("issues")); + assert.ok(data.tabs?.includes("pull-requests")); + assert.ok(!data.tabs?.includes("wiki")); }); it("should fetch tree", async () => { @@ -434,6 +438,7 @@ describe("GitHub scraper", () => { assert.strictEqual(data.repo.owner, "microsoft"); assert.strictEqual(data.repo.name, "vscode"); assert.strictEqual(data.title, "Home"); + assert.ok(data.tabs?.includes("wiki")); assert.strictEqual(data.updatedBy, "Greg Van Liew"); assert.strictEqual(data.updatedAt, "2019-11-22T03:08:32Z"); @@ -496,6 +501,14 @@ describe("GitHub scraper", () => { assert.strictEqual(home.updatedAt, "2018-02-08T18:57:56Z"); }); + it("should leave out the tabs of features a repository has disabled", async () => { + const data = await getGitHubReleases("torvalds", "linux"); + + assert.ok(data.tabs?.includes("code")); + assert.ok(!data.tabs?.includes("issues")); + assert.ok(!data.tabs?.includes("pull-requests")); + }); + it("should redirect to the repository when it has no wiki", async () => { await assert.rejects(getGitHubWiki("git", "git"), (err) => { assert(err instanceof InternalRedirectError, "error should be an InternalRedirectError"); diff --git a/src/scraper.ts b/src/scraper.ts index 19b8318..ac1820a 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -297,7 +297,12 @@ interface GitHubCommon { }; } -interface GitHubNav extends GitHubCommon { +interface GitHubRepoPage extends GitHubCommon { + // Enabled features, like issues or wiki. + tabs: string[] | null; +} + +interface GitHubNav extends GitHubRepoPage { branch: string; path: string; } @@ -347,12 +352,12 @@ interface Issue { numPRs?: number; } -export interface GitHubIssues extends GitHubCommon { +export interface GitHubIssues extends GitHubRepoPage { issues: Issue[]; q?: string; } -export interface GitHubPulls extends GitHubCommon { +export interface GitHubPulls extends GitHubRepoPage { pulls: Issue[]; q?: string; } @@ -384,7 +389,7 @@ export type IssueTimelineItem = | ({ kind: "comment" } & IssueComment) | ({ kind: "event" } & IssueEvent); -export interface GitHubIssue extends GitHubCommon { +export interface GitHubIssue extends GitHubRepoPage { number: number; titleHtml: string; state: string; @@ -437,7 +442,7 @@ export interface DiscussionComment { hiddenReplies: number; } -export interface GitHubDiscussions extends GitHubCommon { +export interface GitHubDiscussions extends GitHubRepoPage { discussions: DiscussionSummary[]; categories: DiscussionCategory[]; // The category the list is narrowed to, if any. @@ -445,7 +450,7 @@ export interface GitHubDiscussions extends GitHubCommon { q?: string; } -export interface GitHubDiscussion extends GitHubCommon { +export interface GitHubDiscussion extends GitHubRepoPage { number: number; titleHtml: string; author: string | null; @@ -520,11 +525,11 @@ export interface ReleaseAsset { publishedAt?: string; } -export interface GitHubReleases extends GitHubCommon { +export interface GitHubReleases extends GitHubRepoPage { releases: Release[]; } -export interface GitHubRelease extends GitHubCommon { +export interface GitHubRelease extends GitHubRepoPage { release: Release; } @@ -534,7 +539,7 @@ export interface WikiPage { updatedAt?: string; } -export interface GitHubWiki extends GitHubCommon { +export interface GitHubWiki extends GitHubRepoPage { title: string; updatedBy: string | null; updatedAt: string | null; @@ -544,7 +549,7 @@ export interface GitHubWiki extends GitHubCommon { pages: WikiPage[]; } -export interface GitHubWikiPages extends GitHubCommon { +export interface GitHubWikiPages extends GitHubRepoPage { pages: WikiPage[]; } @@ -657,7 +662,13 @@ function extractOverviewHtml( return Object.keys(result).length > 0 ? result : undefined; } -function extractGitHub<T>(payload: AppPayload | null, extra: T): GitHubNav & T { +function parseRepoTabs(html: string): string[] | null { + const nav = /<nav\b[^>]*\baria-label="Repository"[^>]*>([\s\S]*?)<\/nav>/.exec(html); + if (!nav) return null; + return Array.from(nav[1].matchAll(/\bid="([a-z-]+)-tab"/g), (match) => match[1]); +} + +function extractGitHub<T>(html: string, payload: AppPayload | null, extra: T): GitHubNav & T { const layoutRoute = payload?.codeViewLayoutRoute; if (layoutRoute === undefined) { throw new Error("Missing codeViewLayoutRoute"); @@ -668,6 +679,7 @@ function extractGitHub<T>(payload: AppPayload | null, extra: T): GitHubNav & T { owner: layoutRoute.repo.ownerLogin, name: layoutRoute.repo.name, }, + tabs: parseRepoTabs(html), branch: layoutRoute.refInfo.name, path: layoutRoute.path, ...extra, @@ -695,7 +707,7 @@ export async function getGitHubRepo(owner: string, repo: string): Promise<GitHub }; const overviewHtml = extractOverviewHtml(repoRoute.overview?.overviewFiles); - return extractGitHub(payload, { items: repoRoute.tree.items, info, overviewHtml }); + return extractGitHub(html, payload, { items: repoRoute.tree.items, info, overviewHtml }); } export async function getGitHubTree( @@ -715,7 +727,7 @@ export async function getGitHubTree( const overviewHtml = extractOverviewHtml(treeRoute.tree.readme ? [treeRoute.tree.readme] : []); - return extractGitHub(payload, { items: treeRoute.tree.items, overviewHtml }); + return extractGitHub(html, payload, { items: treeRoute.tree.items, overviewHtml }); } export async function getGitHubBlob( @@ -748,7 +760,7 @@ export async function getGitHubBlob( size += ` / ${blob.headerInfo.lineInfo.truncatedSloc} loc`; } - return extractGitHub(payload, { + return extractGitHub(html, payload, { language: blob.language || null, size, image: blob.image, @@ -833,7 +845,7 @@ export async function getGitHubIssues( }; }); - return { repo: { owner, name: repo }, issues, q }; + return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), issues, q }; } // GitHub's payloads carry absolute URLs; the same paths on our host are ours. @@ -988,6 +1000,7 @@ export async function getGitHubIssue( return { repo: { owner, name: repo }, + tabs: parseRepoTabs(html), number: issue.number, titleHtml: issue.titleHTML, state: issue.state, @@ -1051,7 +1064,7 @@ export async function getGitHubPulls( pulls.push({ number, title, state, createdAt, labels }); } - return { repo: { owner, name: repo }, pulls, q }; + return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), pulls, q }; } // A category link names the category on its own in a discussion's header and @@ -1128,6 +1141,7 @@ export async function getGitHubDiscussions( return { repo: { owner, name: repo }, + tabs: parseRepoTabs(html), discussions, categories, category: categories.find((c) => c.slug === category) ?? null, @@ -1220,6 +1234,7 @@ export async function getGitHubDiscussion( return { repo: { owner, name: repo }, + tabs: parseRepoTabs(html), number, titleHtml: title ? getInnerHTML(title).trim() : "", author: meta ? selectText("a[href]", meta) : null, @@ -1255,6 +1270,7 @@ export async function getGitHubCommits( owner, name: repo, }, + tabs: parseRepoTabs(html), branch, path, commitGroups: commitsRoute.commitGroups, @@ -1467,6 +1483,7 @@ export async function getGitHubReleases(owner: string, repo: string): Promise<Gi owner, name: repo, }, + tabs: parseRepoTabs(html), releases, }; } @@ -1493,6 +1510,7 @@ export async function getGitHubReleaseBase( owner, name: repo, }, + tabs: parseRepoTabs(html), release, }; } @@ -1763,6 +1781,7 @@ export async function getGitHubWiki( return { repo: { owner, name: repo }, + tabs: parseRepoTabs(html), title: directText(heading), updatedBy, updatedAt: time ? getAttributeValue(time, "datetime") || null : null, @@ -1792,5 +1811,5 @@ export async function getGitHubWikiPages(owner: string, repo: string): Promise<G pages.push({ name, href, updatedAt: time ? getAttributeValue(time, "datetime") : undefined }); } - return { repo: { owner, name: repo }, pages }; + return { repo: { owner, name: repo }, tabs: parseRepoTabs(html), pages }; } |
