aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Mou <dev@mou.fo>2026-04-20 17:14:11 -0400
committerJoe Mou <dev@mou.fo>2026-04-20 21:25:43 -0400
commitf179a3f77c6617b4375e6149cc696ae5dab4d171 (patch)
treec12be415c1bfa7c806082d899cb51df5fc6543c3
parentd2001c480e6f9fe171fe249c4bfeb7f540a70b13 (diff)
Apply overlapping styling drectives instead of duplicating content
-rw-r--r--src/scraper.test.ts24
-rw-r--r--src/scraper.ts45
2 files changed, 61 insertions, 8 deletions
diff --git a/src/scraper.test.ts b/src/scraper.test.ts
index a843561..4432231 100644
--- a/src/scraper.test.ts
+++ b/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/src/scraper.ts b/src/scraper.ts
index 5d17e3f..2dcd1d0 100644
--- a/src/scraper.ts
+++ b/src/scraper.ts
@@ -153,15 +153,46 @@ function escapeHtml(text: string): string {
.replace(/'/g, "&#039;");
}
-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;
}