diff options
| author | Joe Mou <dev@mou.fo> | 2026-04-20 17:14:11 -0400 |
|---|---|---|
| committer | Joe Mou <dev@mou.fo> | 2026-04-20 21:25:43 -0400 |
| commit | dbe8141dce07559c1e16f17800dfd505b0ecdb46 (patch) | |
| tree | 597ca6b5da0c4f488fb9143bd183a1ce0c15a590 | |
| parent | d9a6f6cc7c6a374647486530c6246df980c4be6b (diff) | |
Apply overlapping styling drectives instead of duplicating content
| -rw-r--r-- | cgithub/src/scraper.test.ts | 24 | ||||
| -rw-r--r-- | cgithub/src/scraper.ts | 45 |
2 files changed, 61 insertions, 8 deletions
diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts index a843561..4432231 100644 --- a/cgithub/src/scraper.test.ts +++ b/cgithub/src/scraper.test.ts @@ -95,7 +95,7 @@ describe("GitHub scraper", () => { assert.strictEqual(data.textLines?.[0], "# deploy-pages 🚀"); assert.strictEqual( data.htmlLines?.[0], - '<span class="pl-mh"># deploy-pages 🚀</span>deploy-pages 🚀<span class="pl-en">deploy-pages 🚀</span>', + '<span class="pl-mh"># <span class="pl-en">deploy-pages 🚀</span></span>', ); const firstLine = /^<article class="markdown-body entry-content container-lg" itemprop="text"><div class="markdown-heading" dir="auto"><h1 tabindex="-1" class="heading-element" dir="auto">deploy-pages 🚀<\/h1>/; @@ -197,6 +197,28 @@ describe("GitHub scraper", () => { }); }); + describe("dustinlyons/nixos-config repository", () => { + it("should fetch nix blob with combined overlapping styling directives", async () => { + const data = await getGitHubBlob("dustinlyons", "nixos-config", "main", "flake.nix"); + + assert.strictEqual(data.repo.owner, "dustinlyons"); + assert.strictEqual(data.repo.name, "nixos-config"); + assert.strictEqual(data.path, "flake.nix"); + assert.strictEqual(data.image, false); + assert.strictEqual(data.htmlContent, null); + assert.ok(data.htmlLines !== null && data.htmlLines.length > 0); + + // Line 2 has directives [2,13,"pl-c1"] and [2,13,"pl-s1"] with identical ranges. + // They should be combined into one span, not duplicated. + const line2 = data.htmlLines[1]; + assert.ok(line2.includes('class="pl-c1 pl-s1"'), `expected combined classes in: ${line2}`); + assert.ok( + (line2.match(/description/g) ?? []).length === 1, + `expected "description" to appear once in: ${line2}`, + ); + }); + }); + describe("rich file types", () => { it("should support PNG", async () => { const data = await getGitHubBlob("github", "docs", "main", "assets/images/site/logo.png"); diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts index 5d17e3f..2dcd1d0 100644 --- a/cgithub/src/scraper.ts +++ b/cgithub/src/scraper.ts @@ -153,15 +153,46 @@ function escapeHtml(text: string): string { .replace(/'/g, "'"); } -function applyStyling(line: string, directives: StylingDirective[]): string { +// Renders directives[i..] within [rangeStart, rangeEnd], producing nested spans. +// Returns the rendered content and the index of the first directive not consumed. +function renderRange( + line: string, + directives: StylingDirective[], + i: number, + rangeStart: number, + rangeEnd: number, +): [string, number] { let result = ""; - let lastPos = 0; - for (const [start, end, className] of directives) { - result += escapeHtml(line.substring(lastPos, start)); - result += `<span class="${className}">${escapeHtml(line.substring(start, end))}</span>`; - lastPos = end; + let pos = rangeStart; + + while (i < directives.length) { + const [start, end, className] = directives[i]; + // Stop at directives that start outside or extend past this range. + if (start >= rangeEnd || end > rangeEnd) break; + + // Combine classes for directives with identical ranges. + let classes = className; + let j = i + 1; + while (j < directives.length && directives[j][0] === start && directives[j][1] === end) { + classes += " " + directives[j][2]; + j++; + } + + result += escapeHtml(line.substring(pos, start)); + const [innerContent, nextI] = renderRange(line, directives, j, start, end); + result += `<span class="${classes}">${innerContent}</span>`; + pos = end; + i = nextI; } - result += escapeHtml(line.substring(lastPos)); + + result += escapeHtml(line.substring(pos, rangeEnd)); + return [result, i]; +} + +function applyStyling(line: string, directives: StylingDirective[]): string { + // Sort by start ascending, then end descending so outer spans precede nested ones. + const sorted = [...directives].sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : b[1] - a[1])); + const [result] = renderRange(line, sorted, 0, 0, line.length); return result; } |
