Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/rewriter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ describe("HTMLRewriter integration", () => {
expect(resp.headers.get("access-control-allow-origin")).toBe("*");
});

it("does not double-inject CSS when both head and body exist", async () => {
const resp = await worker.fetch("/browse/https://httpbin.org/html");
if (resp.status !== 200) return;
const html = await resp.text();
const cssCount = (html.match(/text-transform: uppercase/g) || []).length;
expect(cssCount).toBe(1);
const scriptCount = (html.match(/walkAndUppercase/g) || []).length;
// walkAndUppercase appears multiple times within the single script (definition + calls)
// but should NOT appear in a second duplicate script block
expect(scriptCount).toBeGreaterThan(0);
expect(scriptCount).toBeLessThan(10);
Comment on lines +117 to +121
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s duplicate-script assertion is brittle: counting raw "walkAndUppercase" occurrences and bounding it (<10) will break if the script implementation changes (e.g., additional references). A more stable check is to assert a unique signature like "function walkAndUppercase" (or another sentinel) appears exactly once, or to count injected <script> blocks matching the injector output.

Suggested change
const scriptCount = (html.match(/walkAndUppercase/g) || []).length;
// walkAndUppercase appears multiple times within the single script (definition + calls)
// but should NOT appear in a second duplicate script block
expect(scriptCount).toBeGreaterThan(0);
expect(scriptCount).toBeLessThan(10);
const scriptDefCount = (html.match(/function walkAndUppercase\s*\(/g) || []).length;
// Ensure the walkAndUppercase script is injected exactly once (no duplicate script blocks)
expect(scriptDefCount).toBe(1);

Copilot uses AI. Check for mistakes.
});

it("injects resolveForProxy for relative URL handling", async () => {
const resp = await worker.fetch("/browse/https://httpbin.org/html");
if (resp.status !== 200) return;
Expand Down
23 changes: 13 additions & 10 deletions src/rewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,18 @@ class SrcsetRewriter implements HTMLRewriterElementContentHandlers {
}
}

class HeadInjector implements HTMLRewriterElementContentHandlers {
constructor(private targetUrl: string) {}
const INJECTED_CSS = `<style>*:not(input):not(textarea):not(select):not(code):not(pre):not(script):not(style) { text-transform: uppercase !important; } code, pre, textarea, svg { text-transform: none !important; }</style>`;

class ScriptAndStyleInjector implements HTMLRewriterElementContentHandlers {
injected = false;

element(el: Element) {
// no <base> tag — it would redirect /browse/... paths to the target origin
// URLRewriter already resolves all relative URLs to absolute proxy paths
el.append(
`<style>*:not(input):not(textarea):not(select):not(code):not(pre):not(script):not(style) { text-transform: uppercase !important; } code, pre, textarea, svg { text-transform: none !important; }</style>`,
{ html: true },
);
el.append(`<script>${uppercaseScript}</script>`, { html: true });
if (this.injected) return;
this.injected = true;
// prepend into body (fallback for pages without <head>), append into head
const method = el.tagName === "head" ? "append" : "prepend";
el[method](INJECTED_CSS, { html: true });
el[method](`<script>${uppercaseScript}</script>`, { html: true });
Comment on lines 89 to +95
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new body-fallback injection path (pages without a ) isn’t covered by an automated test. Consider adding a deterministic integration/unit test that transforms a minimal HTML document starting with (no ) and asserts the CSS/script are injected (and placed in/at the start of body).

Copilot uses AI. Check for mistakes.
}
}

Expand All @@ -106,8 +107,10 @@ class MetaCSPRemover implements HTMLRewriterElementContentHandlers {

export function buildRewriter(targetUrl: string): HTMLRewriter {
const uppercaser = new TextUppercaser();
const injector = new ScriptAndStyleInjector();
return new HTMLRewriter()
.on("head", new HeadInjector(targetUrl))
.on("head", injector)
.on("body", injector)
.on("meta", new MetaCSPRemover())
.on("a, area", new URLRewriter(targetUrl, "href"))
.on("img", new URLRewriter(targetUrl, "src"))
Expand Down
Loading