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') 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