Skip to content

Commit 75ade3f

Browse files
jergasonclaude
andauthored
inject CSS and script into body when head tag is missing (#6)
pages like old.reddit.com start with <body> and have no <head> tag. our HeadInjector only fired on <head>, so these pages got no CSS text-transform, no uppercase script, no fetch/XHR patching — nothing. replaces HeadInjector with ScriptAndStyleInjector that registers on both head and body. uses an injected flag to prevent double injection. appends to head (normal), prepends to body (fallback). Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent d180154 commit 75ade3f

2 files changed

Lines changed: 26 additions & 10 deletions

File tree

src/rewriter.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,19 @@ describe("HTMLRewriter integration", () => {
108108
expect(resp.headers.get("access-control-allow-origin")).toBe("*");
109109
});
110110

111+
it("does not double-inject CSS when both head and body exist", async () => {
112+
const resp = await worker.fetch("/browse/https://httpbin.org/html");
113+
if (resp.status !== 200) return;
114+
const html = await resp.text();
115+
const cssCount = (html.match(/text-transform: uppercase/g) || []).length;
116+
expect(cssCount).toBe(1);
117+
const scriptCount = (html.match(/walkAndUppercase/g) || []).length;
118+
// walkAndUppercase appears multiple times within the single script (definition + calls)
119+
// but should NOT appear in a second duplicate script block
120+
expect(scriptCount).toBeGreaterThan(0);
121+
expect(scriptCount).toBeLessThan(10);
122+
});
123+
111124
it("injects resolveForProxy for relative URL handling", async () => {
112125
const resp = await worker.fetch("/browse/https://httpbin.org/html");
113126
if (resp.status !== 200) return;

src/rewriter.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,18 @@ class SrcsetRewriter implements HTMLRewriterElementContentHandlers {
8181
}
8282
}
8383

84-
class HeadInjector implements HTMLRewriterElementContentHandlers {
85-
constructor(private targetUrl: string) {}
84+
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>`;
85+
86+
class ScriptAndStyleInjector implements HTMLRewriterElementContentHandlers {
87+
injected = false;
8688

8789
element(el: Element) {
88-
// no <base> tag — it would redirect /browse/... paths to the target origin
89-
// URLRewriter already resolves all relative URLs to absolute proxy paths
90-
el.append(
91-
`<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>`,
92-
{ html: true },
93-
);
94-
el.append(`<script>${uppercaseScript}</script>`, { html: true });
90+
if (this.injected) return;
91+
this.injected = true;
92+
// prepend into body (fallback for pages without <head>), append into head
93+
const method = el.tagName === "head" ? "append" : "prepend";
94+
el[method](INJECTED_CSS, { html: true });
95+
el[method](`<script>${uppercaseScript}</script>`, { html: true });
9596
}
9697
}
9798

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

107108
export function buildRewriter(targetUrl: string): HTMLRewriter {
108109
const uppercaser = new TextUppercaser();
110+
const injector = new ScriptAndStyleInjector();
109111
return new HTMLRewriter()
110-
.on("head", new HeadInjector(targetUrl))
112+
.on("head", injector)
113+
.on("body", injector)
111114
.on("meta", new MetaCSPRemover())
112115
.on("a, area", new URLRewriter(targetUrl, "href"))
113116
.on("img", new URLRewriter(targetUrl, "src"))

0 commit comments

Comments
 (0)