diff --git a/src/rewriter.test.ts b/src/rewriter.test.ts index f379c4e..30f1f90 100644 --- a/src/rewriter.test.ts +++ b/src/rewriter.test.ts @@ -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); + }); + it("injects resolveForProxy for relative URL handling", async () => { const resp = await worker.fetch("/browse/https://httpbin.org/html"); if (resp.status !== 200) return; diff --git a/src/rewriter.ts b/src/rewriter.ts index e654b2c..d14858a 100644 --- a/src/rewriter.ts +++ b/src/rewriter.ts @@ -81,17 +81,18 @@ class SrcsetRewriter implements HTMLRewriterElementContentHandlers { } } -class HeadInjector implements HTMLRewriterElementContentHandlers { - constructor(private targetUrl: string) {} +const INJECTED_CSS = ``; + +class ScriptAndStyleInjector implements HTMLRewriterElementContentHandlers { + injected = false; element(el: Element) { - // no tag — it would redirect /browse/... paths to the target origin - // URLRewriter already resolves all relative URLs to absolute proxy paths - el.append( - ``, - { html: true }, - ); - el.append(``, { html: true }); + if (this.injected) return; + this.injected = true; + // prepend into body (fallback for pages without ), append into head + const method = el.tagName === "head" ? "append" : "prepend"; + el[method](INJECTED_CSS, { html: true }); + el[method](``, { html: true }); } } @@ -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"))