From eff2e1e7b31f046c974b88b409296af81b9f5615 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Thu, 22 Jan 2026 18:35:27 -0500 Subject: Add blob view for displaying file contents --- cgithub/views/blob.eta | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 cgithub/views/blob.eta (limited to 'cgithub/views/blob.eta') 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 @@ + + + + + <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path %> + + + +
+

<%= it.repo.owner %>/<%= it.repo.name %>

+
branch: <%= it.branch %>
+
+ path: root<% + 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 { + %>/<%= part %><% + } + } %> +
+ <% if (it.language) { %> +
language: <%= it.language %>
+ <% } %> +
size: <%= it.size %> bytes
+
+ +
<%= it.rawContent %>
+ + + + -- cgit v1.3.1 From a64548630c531d7a7960f5e83d727a8bd423deaa Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Fri, 23 Jan 2026 01:07:48 -0500 Subject: Refactor template layout --- cgithub/views/blob.eta | 38 ++------------------------------ cgithub/views/layout.eta | 41 +++++++++++++++++++++++++++++++++++ cgithub/views/tree.eta | 56 +++++++++++++----------------------------------- 3 files changed, 58 insertions(+), 77 deletions(-) create mode 100644 cgithub/views/layout.eta (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index d3c6a56..566f5b0 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -1,37 +1,3 @@ - - - - - <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path %> - - - -
-

<%= it.repo.owner %>/<%= it.repo.name %>

-
branch: <%= it.branch %>
-
- path: root<% - 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 { - %>/<%= part %><% - } - } %> -
- <% if (it.language) { %> -
language: <%= it.language %>
- <% } %> -
size: <%= it.size %> bytes
-
+<% layout("./layout") %> -
<%= it.rawContent %>
- -
Generated by cgithub
- - +
<%= it.rawContent %>
diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta new file mode 100644 index 0000000..8e5d9d9 --- /dev/null +++ b/cgithub/views/layout.eta @@ -0,0 +1,41 @@ + + + + + <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path || 'root' %> + + + +
+

<%= it.repo.owner %>/<%= it.repo.name %>

+
branch: <%= it.branch %>
+ <% if (it.path) { %> +
+ path: root<% + 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 { + %>/<%= part %><% + } + } %> +
+ <% } %> + <% if (it.language) { %> +
language: <%= it.language %>
+ <% } %> + <% if (it.size !== undefined) { %> +
size: <%= it.size %> bytes
+ <% } %> +
+ + <%~ it.body %> + +
Generated by cgithub
+ + diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index b49668d..911b7ee 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,42 +1,16 @@ - - - - - <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path || 'root' %> - - - -
-

<%= it.repo.owner %>/<%= it.repo.name %>

-
branch: <%= it.branch %>
- <% if (it.path) { %> -
- path: root<% - const pathParts = it.path.split('/').filter(p => p); - let currentPath = ''; - for (const part of pathParts) { - currentPath += (currentPath ? '/' : '') + part; - %>/<%= part %><% - } %> -
- <% } %> -
+<% layout("./layout") %> - - <% for (const item of it.items) { %> - - - - <% } %> -
- <%= item.contentType === 'directory' ? '📁' : '📄' %> - <% if (item.contentType === 'directory') { %> - <%= item.name %> - <% } else { %> - <%= item.name %> - <% } %> -
- -
Generated by cgithub
- - + + <% for (const item of it.items) { %> + + + + <% } %> +
+ <%= item.contentType === 'directory' ? '📁' : '📄' %> + <% if (item.contentType === 'directory') { %> + <%= item.name %> + <% } else { %> + <%= item.name %> + <% } %> +
-- cgit v1.3.1 From 37aa28f1465cfdd26a58c6dd5b752141ea876cde Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Sun, 25 Jan 2026 18:51:27 -0500 Subject: Styling --- cgithub/static/style.css | 65 ++++++++++++++++++++++++++++++++++++------------ cgithub/views/blob.eta | 11 +++++++- cgithub/views/layout.eta | 18 +++++--------- cgithub/views/repo.eta | 8 +++--- 4 files changed, 69 insertions(+), 33 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/static/style.css b/cgithub/static/style.css index e552787..1229fce 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -1,33 +1,66 @@ body { - margin: 0; - padding: 1rem; + margin: 0 1rem; font-family: Helvetica, Arial, sans-serif; -} - -.icon { - height: 1lh; - vertical-align: text-bottom; -} -pre.blob { - margin: 1rem 0; - padding: 1rem; - border: 1px solid #d0d7de; - border-radius: 6px; - overflow-x: auto; - font-family: monospace; + --border-color: #d0d7de; } @media (prefers-color-scheme: dark) { body { background: #0d1117; color: #e6edf3; + --border-color: #30363d; } a { color: #4493f8; } .blob { - border-color: #30363d; background: #161b22; } } + +a { + text-decoration: none; +} + +header, .box { + margin: 1rem 0; + border: 1px solid var(--border-color); + padding: .5rem; +} + +footer { + margin-top: 1rem; + text-align: center; +} + +.icon { + height: 1lh; + vertical-align: text-bottom; +} + +.repo { + margin-bottom: 1rem; + font-size: 120%; + font-weight: bold; +} + +.branch { + display: inline-block; + margin-right: 1rem; + padding: .2rem; + border: 1px solid var(--border-color); +} + +.description { + margin-bottom: .2rem; +} + +.count + .count { + margin-left: 1rem; +} + +pre.box { + /* Instead of scrolling overflow for long lines, grow the box. */ + width: fit-content; +} diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 566f5b0..9b478b5 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -1,3 +1,12 @@ <% layout("./layout") %> -
<%= it.rawContent %>
+
+ <% if (it.language) { %> +
language: <%= it.language %>
+ <% } %> + <% if (it.size !== undefined) { %> +
size: <%= it.size %> bytes
+ <% } %> +
+ +
<%= it.rawContent %>
diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index 982f8c0..a191301 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -2,16 +2,16 @@ - <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path || 'root' %> + <%= it.repo.owner %>/<%= it.repo.name %> - <%= it.path || '/' %>
-

<%= it.repo.owner %>/<%= it.repo.name %>

-
branch <%= it.branch %>
+
<%= it.repo.owner %>/<%= it.repo.name %>
+
branch <%= it.branch %>
<% if (it.path) { %> -
- path: root<% + + (top)<% const pathParts = it.path.split('/').filter(p => p); let currentPath = ''; for (let i = 0; i < pathParts.length; i++) { @@ -24,13 +24,7 @@ %>/<%= part %><% } } %> -
- <% } %> - <% if (it.language) { %> -
language: <%= it.language %>
- <% } %> - <% if (it.size !== undefined) { %> -
size: <%= it.size %> bytes
+ <% } %>
diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta index 4338708..4965e75 100644 --- a/cgithub/views/repo.eta +++ b/cgithub/views/repo.eta @@ -1,7 +1,7 @@ <% layout("./layout") %> <% if (it.info) { %> -
+
<% if (it.info.description) { %>
<%= it.info.description %>
<% } %> @@ -9,13 +9,13 @@ <% } %> <% if (it.info.stars) { %> - stars <%= it.info.stars %> + stars <%= it.info.stars %> <% } %> <% if (it.info.watchers) { %> - watchers <%= it.info.watchers %> + watchers <%= it.info.watchers %> <% } %> <% if (it.info.forks) { %> - forks <%= it.info.forks %> + forks <%= it.info.forks %> <% } %>
<% } %> -- cgit v1.3.1 From 1631e0f74a488a8cf42e3fe699e96e9389bbb596 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Mon, 26 Jan 2026 11:47:06 -0500 Subject: Overview (README) in trees --- cgithub/.prettierrc | 3 +++ cgithub/src/scraper.ts | 56 +++++++++++++++++++++++++++--------------------- cgithub/static/style.css | 43 +++++++++++++++++++++++++++++++++++-- cgithub/views/_tree.eta | 9 ++++++++ cgithub/views/blob.eta | 3 +++ 5 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 cgithub/.prettierrc (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/.prettierrc b/cgithub/.prettierrc new file mode 100644 index 0000000..de753c5 --- /dev/null +++ b/cgithub/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index a405139..c1bdef5 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -4,6 +4,13 @@ interface TreeItem { path: string; } +interface OverviewFile { + displayName: string; + preferredFileType: string; + richText?: string; + loaded: boolean; +} + interface RawPayload { repo: { ownerLogin: string; @@ -22,6 +29,9 @@ interface RawPayload { }[]; size: number; }; + overview?: { + overviewFiles?: OverviewFile[]; + }; } interface GitHubCommon { @@ -35,6 +45,7 @@ interface GitHubCommon { export interface GitHubTree extends GitHubCommon { items: TreeItem[]; + overviewHtml?: Record; } export interface RepoInfo { @@ -62,19 +73,14 @@ async function fetchGitHubPage(path: string): Promise { const 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: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", }; const response = await fetch(`https://github.com/${path}`, { headers }); return response.text(); } -function parsePayload( - html: string, - dataTarget: string, - prop: string[], -): RawPayload | null { +function parsePayload(html: string, dataTarget: string, prop: string[]): RawPayload | null { const regex = new RegExp( ``, "g", @@ -99,9 +105,7 @@ function parsePayload( function parseRepoInfo(html: string): RepoInfo { const descMatch = html.match(/]*class="[^"]*f4[^"]*"[^>]*>([\s\S]*?)<\/p>/); const description = descMatch?.[1]?.replace(/<[^>]+>/g, "").trim() || null; - const websiteMatch = html.match( - /]*rel="noopener noreferrer nofollow"[^>]*href="([^"]+)"/, - ); + const websiteMatch = html.match(/]*rel="noopener noreferrer nofollow"[^>]*href="([^"]+)"/); const starsMatch = html.match(/id="repo-stars-counter-star"[^>]*>([^<]+)/); const watchersMatch = html.match(/(\d+)<\/strong>\s*watching/); const forksMatch = html.match(/id="repo-network-counter"[^>]*>([^<]+)/); @@ -115,6 +119,16 @@ function parseRepoInfo(html: string): RepoInfo { }; } +function extractOverviewHtml(payload: RawPayload): Record | undefined { + const result: Record = {}; + for (const file of payload.overview?.overviewFiles ?? []) { + if (file.loaded && file.richText !== undefined) { + result[file.displayName] = file.richText; + } + } + return Object.keys(result).length > 0 ? result : undefined; +} + function extractGitHub(payload: RawPayload, extra: T): GitHubCommon & T { return { repo: { @@ -127,24 +141,19 @@ function extractGitHub(payload: RawPayload, extra: T): GitHubCommon & T { }; } -export async function getGitHubRepo( - owner: string, - repo: string, -): Promise { +export async function getGitHubRepo(owner: string, repo: string): Promise { const html = await fetchGitHubPage(`${owner}/${repo}`); - const payload = parsePayload(html, "react-partial.embeddedData", [ - "props", - "initialPayload", - ]); + 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); + const overviewHtml = extractOverviewHtml(payload); - return extractGitHub(payload, { items: payload.tree.items, info }); + return extractGitHub(payload, { items: payload.tree.items, info, overviewHtml }); } export async function getGitHubTree( @@ -158,10 +167,7 @@ export async function getGitHubTree( // GitHub uses different formats for root vs subdirectories. const payload = // Root directory format. - parsePayload(html, "react-partial.embeddedData", [ - "props", - "initialPayload", - ]) || + parsePayload(html, "react-partial.embeddedData", ["props", "initialPayload"]) || // Subdirectory format. parsePayload(html, "react-app.embeddedData", ["payload"]); @@ -169,7 +175,9 @@ export async function getGitHubTree( throw new Error("Could not find tree data in embedded JSON"); } - return extractGitHub(payload, { items: payload.tree.items }); + const overviewHtml = extractOverviewHtml(payload); + + return extractGitHub(payload, { items: payload.tree.items, overviewHtml }); } export async function getGitHubBlob( diff --git a/cgithub/static/style.css b/cgithub/static/style.css index 1229fce..ff787d6 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -3,6 +3,7 @@ body { font-family: Helvetica, Arial, sans-serif; --border-color: #d0d7de; + --shade-background: #eeeeee; } @media (prefers-color-scheme: dark) { @@ -23,10 +24,20 @@ a { text-decoration: none; } -header, .box { +body > header, .box { + --box-padding: .5rem; + margin: 1rem 0; border: 1px solid var(--border-color); - padding: .5rem; + padding: var(--box-padding); + + > header:first-child { + margin: calc(-1 * var(--box-padding)); + margin-bottom: 0; + padding: .5rem; + border-bottom: 1px solid var(--border-color); + font-weight: bold; + } } footer { @@ -64,3 +75,31 @@ pre.box { /* Instead of scrolling overflow for long lines, grow the box. */ width: fit-content; } + +.overview { + overflow-wrap: break-word; + &.box { + --box-padding: 1rem; + } + + a { + text-decoration: underline; + } + + h1, h2 { + border-bottom: 1px solid var(--border-color); + } + + img { + max-width: 100%; + } + + code { + background: var(--shade-background); + } + + /* TODO Strip permalinks? */ + svg.octicon-link { + display: none; + } +} diff --git a/cgithub/views/_tree.eta b/cgithub/views/_tree.eta index 2549bb8..f287c03 100644 --- a/cgithub/views/_tree.eta +++ b/cgithub/views/_tree.eta @@ -13,3 +13,12 @@ <% } %> + +<% if (it.overviewHtml) { %> + <% for (const [filename, html] of Object.entries(it.overviewHtml)) { %> +
+
<%= filename %>
+ <%~ html %> +
+ <% } %> +<% } %> diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 9b478b5..7328a62 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -7,6 +7,9 @@ <% if (it.size !== undefined) { %>
size: <%= it.size %> bytes
<% } %> +
+ Raw +
<%= it.rawContent %>
-- cgit v1.3.1 From 64c397e256b01a2e542e84529e947e237c07edd6 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 27 Jan 2026 16:58:56 -0500 Subject: Split branch and path bread crumbs into separate section --- cgithub/static/style.css | 7 +++++-- cgithub/views/_nav.eta | 18 ++++++++++++++++++ cgithub/views/_tree.eta | 2 ++ cgithub/views/blob.eta | 4 +++- cgithub/views/layout.eta | 22 ++-------------------- cgithub/views/repo.eta | 2 +- cgithub/views/tree.eta | 4 ++-- 7 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 cgithub/views/_nav.eta (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/static/style.css b/cgithub/static/style.css index d21bd32..cccb1ba 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -39,7 +39,11 @@ a { color: var(--link-color); } -body > header, .box { +section { + margin: 1rem 0; +} + +.box { --box-padding: .5rem; margin: 1rem 0; @@ -79,7 +83,6 @@ footer { } .repo { - margin-bottom: 1rem; font-size: 120%; font-weight: bold; } diff --git a/cgithub/views/_nav.eta b/cgithub/views/_nav.eta new file mode 100644 index 0000000..338a6a6 --- /dev/null +++ b/cgithub/views/_nav.eta @@ -0,0 +1,18 @@ +
+
branch <%= it.branch %>
+ <% if (it.path && it.path !== "/") { %> + (top)<% + 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 %><%= it.isTree ? "/" : "" %><% + } else { + %>/<%= part %><% + } + } %> + <% } %> +
diff --git a/cgithub/views/_tree.eta b/cgithub/views/_tree.eta index d0c3453..311762f 100644 --- a/cgithub/views/_tree.eta +++ b/cgithub/views/_tree.eta @@ -1,3 +1,5 @@ +<%~ include("./_nav.eta", {...it, isTree: true}) %> + <% for (const item of it.items) { %> diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 7328a62..277b46d 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -1,4 +1,6 @@ -<% layout("./layout") %> +<% layout("./layout.eta") %> + +<%~ include("./_nav.eta", it) %>
<% if (it.language) { %> diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index bfcd370..f053b84 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -6,29 +6,11 @@ -
+
<%= it.repo.owner %>/<%= it.repo.name %>
-
branch <%= it.branch %>
- <% if (it.path) { %> - - (top)<% - 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 { - %>/<%= part %><% - } - } %> - - <% } %>
git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
 git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git
-
+
<%~ it.body %> diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta index 5bebb95..9122775 100644 --- a/cgithub/views/repo.eta +++ b/cgithub/views/repo.eta @@ -1,4 +1,4 @@ -<% layout("./layout") %> +<% layout("./layout.eta") %> <% if (it.info) { %>
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index 7068754..6e675e1 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,3 +1,3 @@ -<% layout("./layout") %> +<% layout("./layout.eta") %> -<%~ include("./_tree", it) %> +<%~ include("./_tree.eta", it) %> -- cgit v1.3.1 From 89a771538a6a673de7cfe658b1db028acdfa0e44 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 28 Jan 2026 13:05:02 -0500 Subject: Render Markdown blobs --- cgithub/src/scraper.test.ts | 38 ++++++++++++++++++++++++++++++++++---- cgithub/src/scraper.ts | 39 +++++++++++++++++++-------------------- cgithub/static/style.css | 10 +++++----- cgithub/views/blob.eta | 10 ++++++---- 4 files changed, 64 insertions(+), 33 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index e7e65ba..f3a472d 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -59,7 +59,7 @@ describe("GitHub scraper", () => { ]); }); - it("should fetch blob", async () => { + it("should fetch text blob", async () => { const data = await getGitHubBlob("actions", "deploy-pages", "main", "LICENSE"); assert.strictEqual(data.repo.owner, "actions"); @@ -67,10 +67,40 @@ describe("GitHub scraper", () => { assert.strictEqual(data.branch, "main"); assert.strictEqual(data.path, "LICENSE"); - assert.strictEqual(data.size, 1068); + assert.strictEqual(data.size, "1.04 KB / 21 lines / 17 loc"); assert.strictEqual(data.language, "Text"); - assert.match(data.rawContent, /^MIT License\n\n/); - assert.match(data.content, /^MIT License\n\n/); + assert.strictEqual(data.rawLines?.[0], "MIT License"); + assert.strictEqual(data.htmlContent, null); + }); + + it("should fetch Markdown blob", async () => { + const data = await getGitHubBlob("actions", "deploy-pages", "main", "README.md"); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.branch, "main"); + assert.strictEqual(data.path, "README.md"); + + assert.strictEqual(data.size, "8.95 KB / 133 lines / 94 loc"); + assert.strictEqual(data.language, "Markdown"); + assert.strictEqual(data.rawLines, null); + const firstLine = + /^

deploy-pages 🚀<\/h1>/; + assert.match(data.htmlContent ?? "", firstLine); + }); + + it("should fetch code blob", async () => { + const data = await getGitHubBlob("actions", "deploy-pages", "main", ".gitattributes"); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.branch, "main"); + assert.strictEqual(data.path, ".gitattributes"); + + assert.strictEqual(data.size, "39 Bytes / 1 lines / 1 loc"); + assert.strictEqual(data.language, "Git Attributes"); + assert.deepStrictEqual(data.rawLines, ["dist/** -diff linguist-generated=true "]); + assert.strictEqual(data.htmlContent, null); }); it("should fetch issues", async () => { diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index bbf7511..c2983c9 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -16,7 +16,6 @@ interface TreeItem { interface OverviewFile { displayName: string; - preferredFileType: string; richText?: string; loaded: boolean; } @@ -64,12 +63,17 @@ interface RawPayload { items: TreeItem[]; }; blob?: { + headerInfo: { + blobSize: string; + lineInfo: { + truncatedLoc: string; + truncatedSloc: string; + }; + }; language?: string; - rawLines?: string[]; - highlightedLines?: { - text?: string; - }[]; - size: number; + rawLines: string[] | null; + colorizedLines: string[] | null; + richText: string | null; }; overview?: { overviewFiles?: OverviewFile[]; @@ -110,9 +114,9 @@ export interface GitHubRepo extends GitHubTree { export interface GitHubBlob extends GitHubNav { language: string | null; - size: number | null; - content: string; - rawContent: string; + size: string; + rawLines: string[] | null; + htmlContent: string | null; } interface Issue { @@ -265,20 +269,15 @@ export async function getGitHubBlob( 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) => line.text || "").join("\n"); - - // Calculate size from raw content if not provided - const size = blob.size ?? (rawContent.length > 0 ? Buffer.byteLength(rawContent, "utf8") : null); + const { headerInfo, language, rawLines, richText } = payload.blob; + // TODO fine tune + const size = `${headerInfo.blobSize} / ${headerInfo.lineInfo.truncatedLoc} lines / ${headerInfo.lineInfo.truncatedSloc} loc`; return extractGitHub(payload, { - content: content || rawContent, - rawContent, - language: blob.language || null, + language: language || null, size, + rawLines, + htmlContent: richText, }); } diff --git a/cgithub/static/style.css b/cgithub/static/style.css index 7618937..1efbb12 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -163,14 +163,14 @@ pre.box { code { background: var(--shade-background); } - - /* TODO Strip permalinks? */ - svg.octicon-link { - display: none; - } } .timestamp { white-space: nowrap; font-size: .8rem; } + +/* TODO Strip permalinks? */ +svg.octicon-link { + display: none; +} diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 277b46d..346afab 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -6,12 +6,14 @@ <% if (it.language) { %>
language: <%= it.language %>
<% } %> - <% if (it.size !== undefined) { %> -
size: <%= it.size %> bytes
- <% } %> +
<%= it.size %>
Raw

-
<%= it.rawContent %>
+<% if (it.htmlContent) { %> +
<%~ it.htmlContent %>
+<% } else { %> +
<%= it.rawLines.join("\n") %>
+<% } %> -- cgit v1.3.1 From 71442a856d2180c68a51a1db480514d611137bd0 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 28 Jan 2026 14:40:22 -0500 Subject: Syntax highlighting Sometimes GitHub returns colorizedLines and sometimes it returns a terser stylingDirectives, which needs to be applied to rawLines. --- cgithub/src/scraper.test.ts | 13 ++++++++++--- cgithub/src/scraper.ts | 43 ++++++++++++++++++++++++++++++++++++++----- cgithub/static/style.css | 22 ++++++++++++++++++++++ cgithub/views/blob.eta | 4 +++- 4 files changed, 73 insertions(+), 9 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index f3a472d..9326747 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -69,7 +69,10 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "1.04 KB / 21 lines / 17 loc"); assert.strictEqual(data.language, "Text"); - assert.strictEqual(data.rawLines?.[0], "MIT License"); + assert.strictEqual(data.textLines?.length, 21); + assert.strictEqual(data.textLines[0], "MIT License"); + assert.strictEqual(data.htmlLines?.length, 21); + assert.strictEqual(data.htmlLines[0], "MIT License"); assert.strictEqual(data.htmlContent, null); }); @@ -83,7 +86,8 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "8.95 KB / 133 lines / 94 loc"); assert.strictEqual(data.language, "Markdown"); - assert.strictEqual(data.rawLines, null); + assert.strictEqual(data.textLines, null); + assert.strictEqual(data.htmlLines, null); const firstLine = /^

deploy-pages 🚀<\/h1>/; assert.match(data.htmlContent ?? "", firstLine); @@ -99,7 +103,10 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "39 Bytes / 1 lines / 1 loc"); assert.strictEqual(data.language, "Git Attributes"); - assert.deepStrictEqual(data.rawLines, ["dist/** -diff linguist-generated=true "]); + assert.deepStrictEqual(data.textLines, ["dist/** -diff linguist-generated=true "]); + assert.deepStrictEqual(data.htmlLines, [ + '\u003cspan class="pl-e"\u003edist\u003c/span\u003e/\u003cspan class="pl-k"\u003e**\u003c/span\u003e \u003cspan class="pl-k"\u003e-\u003c/span\u003e\u003cspan class="pl-v"\u003ediff\u003c/span\u003e \u003cspan class="pl-v"\u003elinguist-generated\u003c/span\u003e=\u003cspan class="pl-c1"\u003etrue\u003c/span\u003e ', + ]); assert.strictEqual(data.htmlContent, null); }); diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index c2983c9..05afc33 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -52,6 +52,8 @@ interface IssueIndexPageQuery { }; } +type StylingDirective = [number, number, string]; + interface RawPayload { repo: { ownerLogin: string; @@ -74,6 +76,7 @@ interface RawPayload { rawLines: string[] | null; colorizedLines: string[] | null; richText: string | null; + stylingDirectives: StylingDirective[][] | null; }; overview?: { overviewFiles?: OverviewFile[]; @@ -82,6 +85,27 @@ interface RawPayload { preloadedQueries?: IssueIndexPageQuery[]; } +function escapeHtml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function applyStyling(line: string, directives: StylingDirective[]): string { + let result = ""; + let lastPos = 0; + for (const [start, end, className] of directives) { + result += escapeHtml(line.substring(lastPos, start)); + result += `${escapeHtml(line.substring(start, end))}`; + lastPos = end; + } + result += escapeHtml(line.substring(lastPos)); + return result; +} + interface GitHubCommon { repo: { owner: string; @@ -115,7 +139,8 @@ export interface GitHubRepo extends GitHubTree { export interface GitHubBlob extends GitHubNav { language: string | null; size: string; - rawLines: string[] | null; + textLines: string[] | null; + htmlLines: string[] | null; htmlContent: string | null; } @@ -269,15 +294,23 @@ export async function getGitHubBlob( throw new Error("Could not find blob data in embedded JSON"); } - const { headerInfo, language, rawLines, richText } = payload.blob; + const blob = payload.blob; + const { headerInfo, stylingDirectives } = blob; + + let htmlLines = blob.colorizedLines; + if (!htmlLines && stylingDirectives && blob.rawLines) { + htmlLines = blob.rawLines.map((line, i) => applyStyling(line, stylingDirectives[i])); + } + // TODO fine tune const size = `${headerInfo.blobSize} / ${headerInfo.lineInfo.truncatedLoc} lines / ${headerInfo.lineInfo.truncatedSloc} loc`; return extractGitHub(payload, { - language: language || null, + language: blob.language || null, size, - rawLines, - htmlContent: richText, + textLines: blob.rawLines, + htmlLines, + htmlContent: blob.richText, }); } diff --git a/cgithub/static/style.css b/cgithub/static/style.css index 1efbb12..2020dc2 100644 --- a/cgithub/static/style.css +++ b/cgithub/static/style.css @@ -25,6 +25,14 @@ oklch(50% 0.15 250), oklch(60% 0.15 250) ); + + --pl-c: light-dark(#6e7781, #8b949e); + --pl-c1: light-dark(#0550ae, #79c0ff); + --pl-e: light-dark(#6639ba, #d2a8ff); + --pl-en: light-dark(#0550ae, #d2a8ff); + --pl-k: light-dark(#cf222e, #ff7b72); + --pl-s: light-dark(#0a3069, #a5d6ff); + --pl-v: light-dark(#953800, #ffa657); } body { @@ -174,3 +182,17 @@ pre.box { svg.octicon-link { display: none; } + +.pl-c { color: var(--pl-c); } +.pl-c1 { color: var(--pl-c1); } +.pl-e { color: var(--pl-e); } +.pl-en { color: var(--pl-en); } +.pl-k { color: var(--pl-k); } +.pl-s { color: var(--pl-s); } +.pl-v { color: var(--pl-v); } +.pl-ent { color: var(--pl-k); } +.pl-mh { font-weight: bold; color: var(--pl-en); } +.pl-bu { color: var(--pl-k); } +.pl-ii { color: var(--text-color); background-color: var(--pl-k); } +.pl-pds { color: var(--pl-s); } +.pl-smi { color: var(--text-color); } diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 346afab..4972209 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -14,6 +14,8 @@ <% if (it.htmlContent) { %>
<%~ it.htmlContent %>
+<% } else if (it.htmlLines) { %> +
<%~ it.htmlLines.join("\n") %>
<% } else { %> -
<%= it.rawLines.join("\n") %>
+
<%= it.textLines.join("\n") %>
<% } %> -- cgit v1.3.1 From 631bc70d94cd12d0432cca1a220a3642eea6976a Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 28 Jan 2026 15:10:00 -0500 Subject: Image support --- cgithub/src/scraper.test.ts | 24 ++++++++++++++++++++++++ cgithub/src/scraper.ts | 18 +++++++++++++----- cgithub/views/blob.eta | 4 +++- 3 files changed, 40 insertions(+), 6 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index 9326747..ae38c44 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -69,6 +69,8 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "1.04 KB / 21 lines / 17 loc"); assert.strictEqual(data.language, "Text"); + assert.strictEqual(data.image, false); + assert.strictEqual(data.textLines?.length, 21); assert.strictEqual(data.textLines[0], "MIT License"); assert.strictEqual(data.htmlLines?.length, 21); @@ -86,6 +88,8 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "8.95 KB / 133 lines / 94 loc"); assert.strictEqual(data.language, "Markdown"); + assert.strictEqual(data.image, false); + assert.strictEqual(data.textLines, null); assert.strictEqual(data.htmlLines, null); const firstLine = @@ -103,6 +107,8 @@ describe("GitHub scraper", () => { assert.strictEqual(data.size, "39 Bytes / 1 lines / 1 loc"); assert.strictEqual(data.language, "Git Attributes"); + assert.strictEqual(data.image, false); + assert.deepStrictEqual(data.textLines, ["dist/** -diff linguist-generated=true "]); assert.deepStrictEqual(data.htmlLines, [ '\u003cspan class="pl-e"\u003edist\u003c/span\u003e/\u003cspan class="pl-k"\u003e**\u003c/span\u003e \u003cspan class="pl-k"\u003e-\u003c/span\u003e\u003cspan class="pl-v"\u003ediff\u003c/span\u003e \u003cspan class="pl-v"\u003elinguist-generated\u003c/span\u003e=\u003cspan class="pl-c1"\u003etrue\u003c/span\u003e ', @@ -124,6 +130,24 @@ describe("GitHub scraper", () => { }); }); + describe("rich file types", () => { + it("should support PNG", async () => { + const data = await getGitHubBlob("github", "docs", "main", "assets/images/site/logo.png"); + + assert.strictEqual(data.repo.owner, "github"); + assert.strictEqual(data.repo.name, "docs"); + assert.strictEqual(data.branch, "main"); + assert.strictEqual(data.path, "assets/images/site/logo.png"); + + assert.strictEqual(data.size, "1.26 KB"); + assert.strictEqual(data.language, null); + assert.strictEqual(data.image, true); + assert.strictEqual(data.textLines, null); + assert.strictEqual(data.htmlLines, null); + assert.strictEqual(data.htmlContent, null); + }); + }); + describe("error handling", () => { it("should throw for non-existent repository", async () => { await assert.rejects(getGitHubRepo("nosuchowner", "nosuchrepo"), (err) => { diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index 05afc33..e58402a 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -68,11 +68,12 @@ interface RawPayload { headerInfo: { blobSize: string; lineInfo: { - truncatedLoc: string; - truncatedSloc: string; + truncatedLoc: string | null; + truncatedSloc: string | null; }; }; - language?: string; + language: string | null; + image: boolean; rawLines: string[] | null; colorizedLines: string[] | null; richText: string | null; @@ -139,6 +140,7 @@ export interface GitHubRepo extends GitHubTree { export interface GitHubBlob extends GitHubNav { language: string | null; size: string; + image: boolean; textLines: string[] | null; htmlLines: string[] | null; htmlContent: string | null; @@ -302,12 +304,18 @@ export async function getGitHubBlob( htmlLines = blob.rawLines.map((line, i) => applyStyling(line, stylingDirectives[i])); } - // TODO fine tune - const size = `${headerInfo.blobSize} / ${headerInfo.lineInfo.truncatedLoc} lines / ${headerInfo.lineInfo.truncatedSloc} loc`; + let size = headerInfo.blobSize; + if (headerInfo.lineInfo.truncatedLoc) { + size += ` / ${headerInfo.lineInfo.truncatedLoc} lines`; + } + if (headerInfo.lineInfo.truncatedSloc) { + size += ` / ${headerInfo.lineInfo.truncatedSloc} loc`; + } return extractGitHub(payload, { language: blob.language || null, size, + image: blob.image, textLines: blob.rawLines, htmlLines, htmlContent: blob.richText, diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index 4972209..ea07059 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -12,7 +12,9 @@

-<% if (it.htmlContent) { %> +<% if (it.image) { %> +
+<% } else if (it.htmlContent) { %>
<%~ it.htmlContent %>
<% } else if (it.htmlLines) { %>
<%~ it.htmlLines.join("\n") %>
-- cgit v1.3.1 From c57123139794c3ef174b62c6bd2a54a60bbf3327 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 27 May 2026 14:02:36 -0400 Subject: Add repo-scoped search support for issues and PRs We do not support hybrid issue/PR search. Any searches on the issues endpoint will redirect to an explicit issue-only search. --- cgithub/src/index.ts | 26 +++++++++++++++++++++++++- cgithub/src/scraper.test.ts | 15 +++++++++++++++ cgithub/src/scraper.ts | 20 ++++++++++---------- cgithub/static/style.css | 13 ++++++++++--- cgithub/views/blob.eta | 2 +- cgithub/views/issues.eta | 2 +- cgithub/views/layout.eta | 8 ++++++++ cgithub/views/tree.eta | 2 +- 8 files changed, 71 insertions(+), 17 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/src/index.ts b/cgithub/src/index.ts index eb3a0ee..db9915f 100644 --- a/cgithub/src/index.ts +++ b/cgithub/src/index.ts @@ -70,7 +70,31 @@ app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { app.get("/:owner/:repo/issues", async (c) => { const { owner, repo } = c.req.param(); - return tryRender(c, "issues.eta", getGitHubIssues(owner, repo)); + const q = c.req.query("q"); + + // GitHub supports combined issue/PR search, apparently for legacy. We just + // redirect to issue-only search. + if (q && !/\bis:issue\b/.test(q)) { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(`is:issue ${q}`)}`); + } + + return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q })); +}); + +app.get("/:owner/:repo/search", async (c) => { + const { owner, repo } = c.req.param(); + const q = c.req.query("q") ?? ""; + const type = c.req.query("type") ?? "issues"; + + if (type === "issues") { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`); + } + + // Redirect unhandled search types (like code, which requires sign in anyway). + c.header("Referrer-Policy", "no-referrer"); + return c.redirect( + `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`, + ); }); app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index 42ddd6e..236b0f5 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -127,6 +127,7 @@ describe("GitHub scraper", () => { assert.strictEqual(data.repo.owner, "actions"); assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, undefined); const issue402 = data.issues.find((issue) => issue.number === 402); assert.ok(issue402); @@ -135,6 +136,20 @@ describe("GitHub scraper", () => { assert.strictEqual(issue402.createdAt, "2025-07-03T19:07:08Z"); }); + it("should fetch filtered issues with q", async () => { + const data = await getGitHubIssues("actions", "deploy-pages", { + q: "is:issue state:closed", + }); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, "is:issue state:closed"); + const issue417 = data.issues.find((issue) => issue.number === 417); + assert.ok(issue417); + assert.strictEqual(issue417.title, "Chats"); + assert.ok(data.issues.every((issue) => issue.title !== "")); + }); + it("should fetch commits", async () => { const data = await getGitHubCommits("actions", "deploy-pages", "main", ""); diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index 04a685b..2412e49 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -258,6 +258,7 @@ interface Issue { export interface GitHubIssues extends GitHubCommon { issues: Issue[]; + q?: string; } export interface GitHubCommits extends GitHubNav { @@ -513,9 +514,14 @@ export async function getGitHubBlob( }); } -export async function getGitHubIssues(owner: string, repo: string): Promise { - const html = await fetchGitHubPage(`${owner}/${repo}/issues`); - +export async function getGitHubIssues( + owner: string, + repo: string, + // We only support is:issue searches (no PR results). + { q }: { q?: string } = {}, +): Promise { + const suffix = q ? `?q=${encodeURIComponent(q)}` : ""; + const html = await fetchGitHubPage(`${owner}/${repo}/issues${suffix}`); const payload = parsePayload(html); let issuesQuery: IssueIndexPageQuery | undefined; @@ -534,13 +540,7 @@ export async function getGitHubIssues(owner: string, repo: string): Promise a { display: block; padding-right: .5rem; diff --git a/cgithub/views/blob.eta b/cgithub/views/blob.eta index ea07059..50f92a9 100644 --- a/cgithub/views/blob.eta +++ b/cgithub/views/blob.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("./layout.eta"); it.searchType = "code" %> <%~ include("./_nav.eta", it) %> diff --git a/cgithub/views/issues.eta b/cgithub/views/issues.eta index 6e4c9ba..646da46 100644 --- a/cgithub/views/issues.eta +++ b/cgithub/views/issues.eta @@ -1,4 +1,4 @@ -<% layout("./layout") %> +<% layout("./layout"); it.searchType = "issues" %>
<% for (const issue of it.issues) { %> diff --git a/cgithub/views/layout.eta b/cgithub/views/layout.eta index ec3c7b8..f31c6e0 100644 --- a/cgithub/views/layout.eta +++ b/cgithub/views/layout.eta @@ -19,6 +19,14 @@ git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.gitCommits <% } %> + + "> + + + <%~ it.body %> diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index 6e675e1..41ded06 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta") %> +<% layout("./layout.eta"); it.searchType = "code" %> <%~ include("./_tree.eta", it) %> -- cgit v1.3.1 From 866f99c414cb3d102483ce21c68a0e62b394baaa Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Tue, 11 Aug 2026 13:32:44 -0400 Subject: Keep the ref picker on the current view and path --- cgithub/public/static/refs.js | 6 +++-- cgithub/src/app.test.ts | 52 ++++++++++++++++++++++++++++++++++++++++++- cgithub/src/app.ts | 12 +++++++--- cgithub/views/_nav.eta | 4 ++-- cgithub/views/_refs.eta | 6 ++++- cgithub/views/_tree.eta | 2 +- cgithub/views/blob.eta | 2 +- cgithub/views/commits.eta | 2 +- 8 files changed, 74 insertions(+), 12 deletions(-) (limited to 'cgithub/views/blob.eta') diff --git a/cgithub/public/static/refs.js b/cgithub/public/static/refs.js index ffff505..f3e07e8 100644 --- a/cgithub/public/static/refs.js +++ b/cgithub/public/static/refs.js @@ -7,7 +7,9 @@ document.addEventListener("DOMContentLoaded", () => { const dialog = document.querySelector(".ref-selector-menu"); if (dialog) { const list = dialog.querySelector(".ref-selector-list"); - const { owner, repo } = dialog.dataset; + // The view and path let the picker link to the same place on another ref + // rather than to the repository root. + const { owner, repo, view, path = "" } = dialog.dataset; const cache = {}; async function load(type) { @@ -17,7 +19,7 @@ document.addEventListener("DOMContentLoaded", () => { } list.textContent = "Loading…"; try { - const res = await fetch(`/api/${owner}/${repo}/refs/${type}`); + const res = await fetch(`/api/${owner}/${repo}/refs/${type}/${view}/${path}`); const html = res.ok ? await res.text() : ""; cache[type] = html || "Failed to load."; list.innerHTML = cache[type]; diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts index 498d8a6..6544f03 100644 --- a/cgithub/src/app.test.ts +++ b/cgithub/src/app.test.ts @@ -8,6 +8,56 @@ import { createApp } from "./app.ts"; const eta = new Eta({ views: path.join(import.meta.dirname, "..", "views") }); const app = createApp(eta); +describe("branch picker", () => { + const repo = { owner: "actions", name: "deploy-pages" }; + const refs = [{ name: "main", isDefault: true }, { name: "v4" }]; + + it("should keep the current view and path", () => { + const blob = eta.render("_refs.eta", { + repo, + refs, + type: "branches", + view: "blob", + path: "src/main.ts", + }); + const tree = eta.render("_refs.eta", { + repo, + refs, + type: "branches", + view: "tree", + path: "src", + }); + + assert.match(blob, /href="\/actions\/deploy-pages\/blob\/main\/src\/main.ts"/); + assert.match(blob, /href="\/actions\/deploy-pages\/blob\/v4\/src\/main.ts"/); + assert.match(tree, /href="\/actions\/deploy-pages\/tree\/v4\/src"/); + }); + + it("should link to the root tree without a path", () => { + const html = eta.render("_refs.eta", { repo, refs, type: "branches", view: "tree", path: "" }); + + assert.match(html, /href="\/actions\/deploy-pages\/tree\/main"/); + }); + + it("should not build ref links for views it does not serve", async () => { + const res = await app.request("http://cgithub.example/api/a/b/refs/branches/raw/README.md"); + + assert.strictEqual(res.status, 404); + }); + + it("should tell the refs endpoint where the picker was opened", () => { + const nav = { repo, branch: "main", path: "src" }; + + const blob = eta.render("_nav.eta", { ...nav, view: "blob", path: "src/main.ts" }); + assert.match(blob, /data-view="blob" data-path="src\/main.ts"/); + assert.match( + eta.render("_nav.eta", { ...nav, view: "tree" }), + /data-view="tree" data-path="src"/, + ); + assert.doesNotMatch(eta.render("_nav.eta", { ...nav, view: "tree", path: "" }), /data-path=/); + }); +}); + describe("redirects to GitHub", () => { // An HTTP redirect would be caught by the extension's declarativeNetRequest // rules and bounced straight back here. @@ -21,7 +71,7 @@ describe("redirects to GitHub", () => { const body = await res.text(); assert.match( body, - //, + //, ); assert.doesNotMatch(body, / - - -
- -
git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
-git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git
- -
- "> - - - -
- - <%~ it.body %> - -
Generated by cgithub commit <%= it.buildCommit %> · View original
- - - - diff --git a/cgithub/views/layouts/base.eta b/cgithub/views/layouts/base.eta new file mode 100644 index 0000000..fb644f5 --- /dev/null +++ b/cgithub/views/layouts/base.eta @@ -0,0 +1,17 @@ + + + + + + <%= it.title %> + + <% for (const src of it.scripts ?? []) { %> + + <% } %> + + + <%~ it.body %> + +
Generated by cgithub commit <%= it.buildCommit %> · View original
+ + diff --git a/cgithub/views/layouts/repo.eta b/cgithub/views/layouts/repo.eta new file mode 100644 index 0000000..e4f7880 --- /dev/null +++ b/cgithub/views/layouts/repo.eta @@ -0,0 +1,59 @@ +<% +layout("/layouts/base.eta", { + title: `${it.repo.owner}/${it.repo.name} - ${it.path || "/"}`, + scripts: ["/static/refs.js"], +}) +%> + +
+ +
git clone https://github.com/<%= it.repo.owner %>/<%= it.repo.name %>.git
+git clone git@github.com:<%= it.repo.owner %>/<%= it.repo.name %>.git
+ +
+ "> + + + +
+ +<%~ it.body %> + + diff --git a/cgithub/views/owner.eta b/cgithub/views/owner.eta index e5667fb..b3304fb 100644 --- a/cgithub/views/owner.eta +++ b/cgithub/views/owner.eta @@ -1,65 +1,54 @@ - - - - - - <%= it.name ? `${it.name} (${it.login})` : it.login %> - - - -
- <% if (it.avatarUrl) { %> - - <% } %> -
<%= it.name || it.login %>
-
<%= it.login %><%= it.type === "organization" ? " (organization)" : "" %>
- <% if (it.location) { %> -
<%= it.location %>
- <% } %> - <% if (it.nav.length > 0) { %> - - <% } %> -
+<% layout("/layouts/base.eta", { title: it.name ? `${it.name} (${it.login})` : it.login }) %> - <% if (it.bio || it.links.length > 0 || it.followers || it.company) { %> -
- <% if (it.bio) { %> -
<%= it.bio %>
- <% } %> - <% for (const link of it.links) { %> - - <% } %> - <% if (it.followers) { %> -
<%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %>
- <% } %> - <% if (it.company) { %> -
<%= it.company %>
- <% } %> -
+
+ <% if (it.avatarUrl) { %> + <% } %> - - <% if (it.pinned.length > 0) { %> -
-

Pinned

- <% for (const repo of it.pinned) { %> - <%~ include("./_ownerRepo", { repo, login: it.login }) %> - <% } %> -
+
<%= it.name || it.login %>
+
<%= it.login %><%= it.type === "organization" ? " (organization)" : "" %>
+ <% if (it.location) { %> +
<%= it.location %>
<% } %> - - <% if (it.repos.length > 0) { %> -
-

Repositories

- <% for (const repo of it.repos) { %> - <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% if (it.nav.length > 0) { %> +
+ <% } %> +
+ +<% if (it.bio || it.links.length > 0 || it.followers || it.company) { %> +
+ <% if (it.bio) { %> +
<%= it.bio %>
+ <% } %> + <% for (const link of it.links) { %> + + <% } %> + <% if (it.followers) { %> +
<%= it.followers %> followers<%= it.following ? ` · ${it.following} following` : "" %>
+ <% } %> + <% if (it.company) { %> +
<%= it.company %>
+ <% } %> +
+<% } %> -
Generated by cgithub
- - +<% if (it.pinned.length > 0) { %> +
+

Pinned

+ <% for (const repo of it.pinned) { %> + <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% } %> +
+<% } %> + +<% if (it.repos.length > 0) { %> +
+

Repositories

+ <% for (const repo of it.repos) { %> + <%~ include("./_ownerRepo", { repo, login: it.login }) %> + <% } %> +
+<% } %> diff --git a/cgithub/views/pulls.eta b/cgithub/views/pulls.eta index 9ec5ca1..697f6b1 100644 --- a/cgithub/views/pulls.eta +++ b/cgithub/views/pulls.eta @@ -1,4 +1,4 @@ -<% layout("./layout"); it.searchType = "pullrequests" %> +<% layout("/layouts/repo.eta"); it.searchType = "pullrequests" %>
<% for (const pull of it.pulls) { %> diff --git a/cgithub/views/release.eta b/cgithub/views/release.eta index 46dd5db..64cca35 100644 --- a/cgithub/views/release.eta +++ b/cgithub/views/release.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <%~ include("./_release.eta", { repo: it.repo, release: it.release, linkToDetail: false }) %> diff --git a/cgithub/views/releases.eta b/cgithub/views/releases.eta index 840908c..4ede28b 100644 --- a/cgithub/views/releases.eta +++ b/cgithub/views/releases.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <% for (const release of it.releases) { %> <%~ include("./_release.eta", { repo: it.repo, release, linkToDetail: true }) %> diff --git a/cgithub/views/repo.eta b/cgithub/views/repo.eta index 5dc6beb..60746df 100644 --- a/cgithub/views/repo.eta +++ b/cgithub/views/repo.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta") %> +<% layout("/layouts/repo.eta") %> <% if (it.info) { %>
diff --git a/cgithub/views/tree.eta b/cgithub/views/tree.eta index 41ded06..fabf5b9 100644 --- a/cgithub/views/tree.eta +++ b/cgithub/views/tree.eta @@ -1,3 +1,3 @@ -<% layout("./layout.eta"); it.searchType = "code" %> +<% layout("/layouts/repo.eta"); it.searchType = "code" %> <%~ include("./_tree.eta", it) %> diff --git a/cgithub/views/wiki.eta b/cgithub/views/wiki.eta index d6047e1..781caff 100644 --- a/cgithub/views/wiki.eta +++ b/cgithub/views/wiki.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta"); it.path = "wiki/" + it.title %> +<% layout("/layouts/repo.eta"); it.path = "wiki/" + it.title %> <%~ include("./_wikiNav.eta", it) %> diff --git a/cgithub/views/wikiPages.eta b/cgithub/views/wikiPages.eta index 58d6d21..eb90781 100644 --- a/cgithub/views/wikiPages.eta +++ b/cgithub/views/wikiPages.eta @@ -1,4 +1,4 @@ -<% layout("./layout.eta"); it.path = "wiki/_pages" %> +<% layout("/layouts/repo.eta"); it.path = "wiki/_pages" %> <%~ include("./_wikiNav.eta", it) %> -- cgit v1.3.1