diff options
author | 2023-09-15 21:18:57 -0700 | |
---|---|---|
committer | 2023-09-15 21:18:57 -0700 | |
commit | b54e3f3c042bc37418a646728428910cafc502ef (patch) | |
tree | f36f52c715300c6dd37add073fe76931cc98ab2d | |
parent | 7f2e40af46bdd21984b9df1695895c7f43aba482 (diff) | |
download | bun-b54e3f3c042bc37418a646728428910cafc502ef.tar.gz bun-b54e3f3c042bc37418a646728428910cafc502ef.tar.zst bun-b54e3f3c042bc37418a646728428910cafc502ef.zip |
fix(corking) uncork if needed (#5525)
* fix size limit
* uncork if needed instead of terminating
* undo unrelated changes
-rw-r--r-- | packages/bun-uws/src/AsyncSocket.h | 11 | ||||
-rw-r--r-- | packages/bun-uws/src/Loop.h | 6 | ||||
-rw-r--r-- | packages/bun-uws/src/LoopData.h | 1 | ||||
-rw-r--r-- | test/js/workerd/html-rewriter.test.js | 856 |
4 files changed, 452 insertions, 422 deletions
diff --git a/packages/bun-uws/src/AsyncSocket.h b/packages/bun-uws/src/AsyncSocket.h index 8e3301f24..1051271a2 100644 --- a/packages/bun-uws/src/AsyncSocket.h +++ b/packages/bun-uws/src/AsyncSocket.h @@ -121,18 +121,25 @@ public: void corkUnchecked() { /* What if another socket is corked? */ getLoopData()->corkedSocket = this; + getLoopData()->corkedSocketIsSSL = SSL; } /* Cork this socket. Only one socket may ever be corked per-loop at any given time */ void cork() { /* Extra check for invalid corking of others */ if (getLoopData()->corkOffset && getLoopData()->corkedSocket != this) { - std::cerr << "Error: Cork buffer must not be acquired without checking canCork!" << std::endl; - std::terminate(); + // We uncork the other socket early instead of terminating the program + // is unlikely to be cause any issues and is better than crashing + if(getLoopData()->corkedSocketIsSSL) { + ((AsyncSocket<true> *) getLoopData()->corkedSocket)->uncork(); + } else { + ((AsyncSocket<false> *) getLoopData()->corkedSocket)->uncork(); + } } /* What if another socket is corked? */ getLoopData()->corkedSocket = this; + getLoopData()->corkedSocketIsSSL = SSL; } /* Returns the corked socket or nullptr */ diff --git a/packages/bun-uws/src/Loop.h b/packages/bun-uws/src/Loop.h index 6c7bdfc9e..d3ca45b58 100644 --- a/packages/bun-uws/src/Loop.h +++ b/packages/bun-uws/src/Loop.h @@ -58,12 +58,6 @@ private: for (auto &p : loopData->postHandlers) { p.second((Loop *) loop); } - - /* After every event loop iteration, we must not hold the cork buffer */ - if (loopData->corkedSocket) { - std::cerr << "Error: Cork buffer must not be held across event loop iterations!" << std::endl; - std::terminate(); - } } Loop() = delete; diff --git a/packages/bun-uws/src/LoopData.h b/packages/bun-uws/src/LoopData.h index 986bf0cbd..92bd9ffff 100644 --- a/packages/bun-uws/src/LoopData.h +++ b/packages/bun-uws/src/LoopData.h @@ -98,6 +98,7 @@ public: char *corkBuffer = new char[CORK_BUFFER_SIZE]; unsigned int corkOffset = 0; void *corkedSocket = nullptr; + bool corkedSocketIsSSL = false; /* Per message deflate data */ ZlibContext *zlibContext = nullptr; diff --git a/test/js/workerd/html-rewriter.test.js b/test/js/workerd/html-rewriter.test.js index b0c951197..3f96c22c1 100644 --- a/test/js/workerd/html-rewriter.test.js +++ b/test/js/workerd/html-rewriter.test.js @@ -14,420 +14,448 @@ var setTimeoutAsync = (fn, delay) => { }); }; -// describe("HTMLRewriter", () => { -// it("HTMLRewriter: async replacement", async () => { -// await gcTick(); -// const res = new HTMLRewriter() -// .on("div", { -// async element(element) { -// await setTimeoutAsync(() => { -// element.setInnerContent("<span>replace</span>", { html: true }); -// }, 5); -// }, -// }) -// .transform(new Response("<div>example.com</div>")); -// await gcTick(); -// expect(await res.text()).toBe("<div><span>replace</span></div>"); -// await gcTick(); -// }); - -// it("supports element handlers", async () => { -// var rewriter = new HTMLRewriter(); -// rewriter.on("div", { -// element(element) { -// element.setInnerContent("<blink>it worked!</blink>", { html: true }); -// }, -// }); -// var input = new Response("<div>hello</div>"); -// var output = rewriter.transform(input); -// expect(await output.text()).toBe("<div><blink>it worked!</blink></div>"); -// }); - -// it("(from file) supports element handlers", async () => { -// var rewriter = new HTMLRewriter(); -// rewriter.on("div", { -// element(element) { -// element.setInnerContent("<blink>it worked!</blink>", { html: true }); -// }, -// }); -// await Bun.write("/tmp/html-rewriter.txt.js", "<div>hello</div>"); -// var input = new Response(Bun.file("/tmp/html-rewriter.txt.js")); -// var output = rewriter.transform(input); -// expect(await output.text()).toBe("<div><blink>it worked!</blink></div>"); -// }); - -// it("supports attribute iterator", async () => { -// var rewriter = new HTMLRewriter(); -// var expected = [ -// ["first", ""], -// ["second", "alrihgt"], -// ["third", "123"], -// ["fourth", "5"], -// ["fifth", "helloooo"], -// ]; -// rewriter.on("div", { -// element(element2) { -// for (let attr of element2.attributes) { -// const stack = expected.shift(); -// expect(stack[0]).toBe(attr[0]); -// expect(stack[1]).toBe(attr[1]); -// } -// }, -// }); -// var input = new Response('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>'); -// var output = rewriter.transform(input); -// expect(await output.text()).toBe('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>'); -// expect(expected.length).toBe(0); -// }); - -// it("handles element specific mutations", async () => { -// // prepend/append -// let res = new HTMLRewriter() -// .on("p", { -// element(element) { -// element.prepend("<span>prepend</span>"); -// element.prepend("<span>prepend html</span>", { html: true }); -// element.append("<span>append</span>"); -// element.append("<span>append html</span>", { html: true }); -// }, -// }) -// .transform(new Response("<p>test</p>")); -// expect(await res.text()).toBe( -// [ -// "<p>", -// "<span>prepend html</span>", -// "<span>prepend</span>", -// "test", -// "<span>append</span>", -// "<span>append html</span>", -// "</p>", -// ].join(""), -// ); - -// // setInnerContent -// res = new HTMLRewriter() -// .on("p", { -// element(element) { -// element.setInnerContent("<span>replace</span>"); -// }, -// }) -// .transform(new Response("<p>test</p>")); -// expect(await res.text()).toBe("<p><span>replace</span></p>"); -// res = new HTMLRewriter() -// .on("p", { -// element(element) { -// element.setInnerContent("<span>replace</span>", { html: true }); -// }, -// }) -// .transform(new Response("<p>test</p>")); -// expect(await res.text()).toBe("<p><span>replace</span></p>"); - -// // removeAndKeepContent -// res = new HTMLRewriter() -// .on("p", { -// element(element) { -// element.removeAndKeepContent(); -// }, -// }) -// .transform(new Response("<p>test</p>")); -// expect(await res.text()).toBe("test"); -// }); - -// it("handles element class properties", async () => { -// class Handler { -// constructor(content) { -// this.content = content; -// } - -// // noinspection JSUnusedGlobalSymbols -// element(element) { -// element.setInnerContent(this.content); -// } -// } -// const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("<p>test</p>")); -// expect(await res.text()).toBe("<p>new</p>"); -// }); - -// const commentsMutationsInput = "<p><!--test--></p>"; -// const commentsMutationsExpected = { -// beforeAfter: [ -// "<p>", -// "<span>before</span>", -// "<span>before html</span>", -// "<!--test-->", -// "<span>after html</span>", -// "<span>after</span>", -// "</p>", -// ].join(""), -// replace: "<p><span>replace</span></p>", -// replaceHtml: "<p><span>replace</span></p>", -// remove: "<p></p>", -// }; - -// const commentPropertiesMacro = async func => { -// const res = func(new HTMLRewriter(), comment => { -// expect(comment.removed).toBe(false); -// expect(comment.text).toBe("test"); -// comment.text = "new"; -// expect(comment.text).toBe("new"); -// }).transform(new Response("<p><!--test--></p>")); -// expect(await res.text()).toBe("<p><!--new--></p>"); -// }; - -// it("HTMLRewriter: handles comment properties", () => -// commentPropertiesMacro((rw, comments) => { -// rw.on("p", { comments }); -// return rw; -// })); - -// it("selector tests", async () => { -// const checkSelector = async (selector, input, expected) => { -// const res = new HTMLRewriter() -// .on(selector, { -// element(element) { -// element.setInnerContent("new"); -// }, -// }) -// .transform(new Response(input)); -// expect(await res.text()).toBe(expected); -// }; - -// await checkSelector("*", "<h1>1</h1><p>2</p>", "<h1>new</h1><p>new</p>"); -// await checkSelector("p", "<h1>1</h1><p>2</p>", "<h1>1</h1><p>new</p>"); -// await checkSelector( -// "p:nth-child(2)", -// "<div><p>1</p><p>2</p><p>3</p></div>", -// "<div><p>1</p><p>new</p><p>3</p></div>", -// ); -// await checkSelector( -// "p:first-child", -// "<div><p>1</p><p>2</p><p>3</p></div>", -// "<div><p>new</p><p>2</p><p>3</p></div>", -// ); -// await checkSelector( -// "p:nth-of-type(2)", -// "<div><p>1</p><h1>2</h1><p>3</p><h1>4</h1><p>5</p></div>", -// "<div><p>1</p><h1>2</h1><p>new</p><h1>4</h1><p>5</p></div>", -// ); -// await checkSelector( -// "p:first-of-type", -// "<div><h1>1</h1><p>2</p><p>3</p></div>", -// "<div><h1>1</h1><p>new</p><p>3</p></div>", -// ); -// await checkSelector( -// "p:not(:first-child)", -// "<div><p>1</p><p>2</p><p>3</p></div>", -// "<div><p>1</p><p>new</p><p>new</p></div>", -// ); -// await checkSelector("p.red", '<p class="red">1</p><p>2</p>', '<p class="red">new</p><p>2</p>'); -// await checkSelector("h1#header", '<h1 id="header">1</h1><h1>2</h1>', '<h1 id="header">new</h1><h1>2</h1>'); -// await checkSelector("p[data-test]", "<p data-test>1</p><p>2</p>", "<p data-test>new</p><p>2</p>"); -// await checkSelector( -// 'p[data-test="one"]', -// '<p data-test="one">1</p><p data-test="two">2</p>', -// '<p data-test="one">new</p><p data-test="two">2</p>', -// ); -// await checkSelector( -// 'p[data-test="one" i]', -// '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>', -// '<p data-test="one">new</p><p data-test="OnE">new</p><p data-test="two">3</p>', -// ); -// await checkSelector( -// 'p[data-test="one" s]', -// '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>', -// '<p data-test="one">new</p><p data-test="OnE">2</p><p data-test="two">3</p>', -// ); -// await checkSelector( -// 'p[data-test~="two"]', -// '<p data-test="one two three">1</p><p data-test="one two">2</p><p data-test="one">3</p>', -// '<p data-test="one two three">new</p><p data-test="one two">new</p><p data-test="one">3</p>', -// ); -// await checkSelector( -// 'p[data-test^="a"]', -// '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>', -// '<p data-test="a1">new</p><p data-test="a2">new</p><p data-test="b1">3</p>', -// ); -// await checkSelector( -// 'p[data-test$="1"]', -// '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>', -// '<p data-test="a1">new</p><p data-test="a2">2</p><p data-test="b1">new</p>', -// ); -// await checkSelector( -// 'p[data-test*="b"]', -// '<p data-test="abc">1</p><p data-test="ab">2</p><p data-test="a">3</p>', -// '<p data-test="abc">new</p><p data-test="ab">new</p><p data-test="a">3</p>', -// ); -// await checkSelector( -// 'p[data-test|="a"]', -// '<p data-test="a">1</p><p data-test="a-1">2</p><p data-test="a2">3</p>', -// '<p data-test="a">new</p><p data-test="a-1">new</p><p data-test="a2">3</p>', -// ); -// await checkSelector( -// "div span", -// "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>", -// "<div><h1><span>new</span></h1><span>new</span><b>3</b></div>", -// ); -// await checkSelector( -// "div > span", -// "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>", -// "<div><h1><span>1</span></h1><span>new</span><b>3</b></div>", -// ); -// }); - -// it("supports deleting innerContent", async () => { -// expect( -// await new HTMLRewriter() -// .on("div", { -// element(elem) { -// // https://github.com/oven-sh/bun/issues/2323 -// elem.setInnerContent(""); -// }, -// }) -// .transform(new Response("<div>content</div>")) -// .text(), -// ).toEqual("<div></div>"); -// }); - -// it("supports deleting innerHTML", async () => { -// expect( -// await new HTMLRewriter() -// .on("div", { -// element(elem) { -// // https://github.com/oven-sh/bun/issues/2323 -// elem.setInnerContent("", { html: true }); -// }, -// }) -// .transform(new Response("<div><span>content</span></div>")) -// .text(), -// ).toEqual("<div></div>"); -// }); - -// it("it supports lastInTextNode", async () => { -// let lastInTextNode; - -// await new HTMLRewriter() -// .on("p", { -// text(text) { -// lastInTextNode ??= text.lastInTextNode; -// }, -// }) -// .transform(new Response("<p>Lorem ipsum!</p>")) -// .text(); - -// expect(lastInTextNode).toBeBoolean(); -// }); - -// it("it supports selfClosing", async () => { -// const selfClosing = {}; -// await new HTMLRewriter() -// .on("*", { -// element(el) { -// selfClosing[el.tagName] = el.selfClosing; -// }, -// }) - -// .transform(new Response("<p>Lorem ipsum!<br></p><div />")) -// .text(); - -// expect(selfClosing).toEqual({ -// p: false, -// br: false, -// div: true, -// }); -// }); - -// it("it supports canHaveContent", async () => { -// const canHaveContent = {}; -// await new HTMLRewriter() -// .on("*", { -// element(el) { -// canHaveContent[el.tagName] = el.canHaveContent; -// }, -// }) -// .transform(new Response("<p>Lorem ipsum!<br></p><div /><svg><circle /></svg>")) -// .text(); - -// expect(canHaveContent).toEqual({ -// p: true, -// br: false, -// div: true, -// svg: true, -// circle: false, -// }); -// }); -// }); - -// // By not segfaulting, this test passes -// it("#3334 regression", async () => { -// for (let i = 0; i < 10; i++) { -// const headers = new Headers({ -// "content-type": "text/html", -// }); -// const response = new Response("<div>content</div>", { headers }); - -// const result = await new HTMLRewriter() -// .on("div", { -// element(elem) { -// elem.setInnerContent("new"); -// }, -// }) -// .transform(response) -// .text(); -// expect(result).toEqual("<div>new</div>"); -// } -// Bun.gc(true); -// }); - -// it("#3489", async () => { -// var el; -// await new HTMLRewriter() -// .on("p", { -// element(element) { -// el = element.getAttribute("id"); -// }, -// }) -// .transform(new Response('<p id="Šžõäöü"></p>')) -// .text(); -// expect(el).toEqual("Šžõäöü"); -// }); - -// it("get attribute - ascii", async () => { -// for (let i = 0; i < 10; i++) { -// var el; -// await new HTMLRewriter() -// .on("p", { -// element(element) { -// el = element.getAttribute("id"); -// }, -// }) -// .transform(new Response(`<p id="asciii"></p>`)) -// .text(); -// expect(el).toEqual("asciii"); -// } -// }); - -// it("#3520", async () => { -// const pairs = []; - -// await new HTMLRewriter() -// .on("p", { -// element(element) { -// for (const pair of element.attributes) { -// pairs.push(pair); -// } -// }, -// }) -// .transform(new Response('<p šž="Õäöü" ab="Õäöü" šž="Õäöü" šž="dc" šž="🕵🏻"></p>')) -// .text(); - -// expect(pairs).toEqual([ -// ["šž", "Õäöü"], -// ["ab", "Õäöü"], -// ["šž", "Õäöü"], -// ["šž", "dc"], -// ["šž", "🕵🏻"], -// ]); -// }); +describe("HTMLRewriter", () => { + it("HTMLRewriter: async replacement", async () => { + await gcTick(); + const res = new HTMLRewriter() + .on("div", { + async element(element) { + await setTimeoutAsync(() => { + element.setInnerContent("<span>replace</span>", { html: true }); + }, 5); + }, + }) + .transform(new Response("<div>example.com</div>")); + await gcTick(); + expect(await res.text()).toBe("<div><span>replace</span></div>"); + await gcTick(); + }); + + it("HTMLRewriter: async replacement using fetch + Bun.serve", async () => { + await gcTick(); + let content; + let server; + try { + server = Bun.serve({ + port: 0, + fetch(req) { + return new HTMLRewriter() + .on("div", { + async element(element) { + content = await fetch("https://www.example.com/").then(res => res.text()); + element.setInnerContent(content, { html: true }); + }, + }) + .transform(new Response("<div>example.com</div>")); + }, + }); + + await gcTick(); + const url = `http://localhost:${server.port}`; + expect(await fetch(url).then(res => res.text())).toBe(`<div>${content}</div>`); + await gcTick(); + } finally { + server.stop(); + } + }); + + it("supports element handlers", async () => { + var rewriter = new HTMLRewriter(); + rewriter.on("div", { + element(element) { + element.setInnerContent("<blink>it worked!</blink>", { html: true }); + }, + }); + var input = new Response("<div>hello</div>"); + var output = rewriter.transform(input); + expect(await output.text()).toBe("<div><blink>it worked!</blink></div>"); + }); + + it("(from file) supports element handlers", async () => { + var rewriter = new HTMLRewriter(); + rewriter.on("div", { + element(element) { + element.setInnerContent("<blink>it worked!</blink>", { html: true }); + }, + }); + await Bun.write("/tmp/html-rewriter.txt.js", "<div>hello</div>"); + var input = new Response(Bun.file("/tmp/html-rewriter.txt.js")); + var output = rewriter.transform(input); + expect(await output.text()).toBe("<div><blink>it worked!</blink></div>"); + }); + + it("supports attribute iterator", async () => { + var rewriter = new HTMLRewriter(); + var expected = [ + ["first", ""], + ["second", "alrihgt"], + ["third", "123"], + ["fourth", "5"], + ["fifth", "helloooo"], + ]; + rewriter.on("div", { + element(element2) { + for (let attr of element2.attributes) { + const stack = expected.shift(); + expect(stack[0]).toBe(attr[0]); + expect(stack[1]).toBe(attr[1]); + } + }, + }); + var input = new Response('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>'); + var output = rewriter.transform(input); + expect(await output.text()).toBe('<div first second="alrihgt" third="123" fourth=5 fifth=helloooo>hello</div>'); + expect(expected.length).toBe(0); + }); + + it("handles element specific mutations", async () => { + // prepend/append + let res = new HTMLRewriter() + .on("p", { + element(element) { + element.prepend("<span>prepend</span>"); + element.prepend("<span>prepend html</span>", { html: true }); + element.append("<span>append</span>"); + element.append("<span>append html</span>", { html: true }); + }, + }) + .transform(new Response("<p>test</p>")); + expect(await res.text()).toBe( + [ + "<p>", + "<span>prepend html</span>", + "<span>prepend</span>", + "test", + "<span>append</span>", + "<span>append html</span>", + "</p>", + ].join(""), + ); + + // setInnerContent + res = new HTMLRewriter() + .on("p", { + element(element) { + element.setInnerContent("<span>replace</span>"); + }, + }) + .transform(new Response("<p>test</p>")); + expect(await res.text()).toBe("<p><span>replace</span></p>"); + res = new HTMLRewriter() + .on("p", { + element(element) { + element.setInnerContent("<span>replace</span>", { html: true }); + }, + }) + .transform(new Response("<p>test</p>")); + expect(await res.text()).toBe("<p><span>replace</span></p>"); + + // removeAndKeepContent + res = new HTMLRewriter() + .on("p", { + element(element) { + element.removeAndKeepContent(); + }, + }) + .transform(new Response("<p>test</p>")); + expect(await res.text()).toBe("test"); + }); + + it("handles element class properties", async () => { + class Handler { + constructor(content) { + this.content = content; + } + + // noinspection JSUnusedGlobalSymbols + element(element) { + element.setInnerContent(this.content); + } + } + const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("<p>test</p>")); + expect(await res.text()).toBe("<p>new</p>"); + }); + + const commentsMutationsInput = "<p><!--test--></p>"; + const commentsMutationsExpected = { + beforeAfter: [ + "<p>", + "<span>before</span>", + "<span>before html</span>", + "<!--test-->", + "<span>after html</span>", + "<span>after</span>", + "</p>", + ].join(""), + replace: "<p><span>replace</span></p>", + replaceHtml: "<p><span>replace</span></p>", + remove: "<p></p>", + }; + + const commentPropertiesMacro = async func => { + const res = func(new HTMLRewriter(), comment => { + expect(comment.removed).toBe(false); + expect(comment.text).toBe("test"); + comment.text = "new"; + expect(comment.text).toBe("new"); + }).transform(new Response("<p><!--test--></p>")); + expect(await res.text()).toBe("<p><!--new--></p>"); + }; + + it("HTMLRewriter: handles comment properties", () => + commentPropertiesMacro((rw, comments) => { + rw.on("p", { comments }); + return rw; + })); + + it("selector tests", async () => { + const checkSelector = async (selector, input, expected) => { + const res = new HTMLRewriter() + .on(selector, { + element(element) { + element.setInnerContent("new"); + }, + }) + .transform(new Response(input)); + expect(await res.text()).toBe(expected); + }; + + await checkSelector("*", "<h1>1</h1><p>2</p>", "<h1>new</h1><p>new</p>"); + await checkSelector("p", "<h1>1</h1><p>2</p>", "<h1>1</h1><p>new</p>"); + await checkSelector( + "p:nth-child(2)", + "<div><p>1</p><p>2</p><p>3</p></div>", + "<div><p>1</p><p>new</p><p>3</p></div>", + ); + await checkSelector( + "p:first-child", + "<div><p>1</p><p>2</p><p>3</p></div>", + "<div><p>new</p><p>2</p><p>3</p></div>", + ); + await checkSelector( + "p:nth-of-type(2)", + "<div><p>1</p><h1>2</h1><p>3</p><h1>4</h1><p>5</p></div>", + "<div><p>1</p><h1>2</h1><p>new</p><h1>4</h1><p>5</p></div>", + ); + await checkSelector( + "p:first-of-type", + "<div><h1>1</h1><p>2</p><p>3</p></div>", + "<div><h1>1</h1><p>new</p><p>3</p></div>", + ); + await checkSelector( + "p:not(:first-child)", + "<div><p>1</p><p>2</p><p>3</p></div>", + "<div><p>1</p><p>new</p><p>new</p></div>", + ); + await checkSelector("p.red", '<p class="red">1</p><p>2</p>', '<p class="red">new</p><p>2</p>'); + await checkSelector("h1#header", '<h1 id="header">1</h1><h1>2</h1>', '<h1 id="header">new</h1><h1>2</h1>'); + await checkSelector("p[data-test]", "<p data-test>1</p><p>2</p>", "<p data-test>new</p><p>2</p>"); + await checkSelector( + 'p[data-test="one"]', + '<p data-test="one">1</p><p data-test="two">2</p>', + '<p data-test="one">new</p><p data-test="two">2</p>', + ); + await checkSelector( + 'p[data-test="one" i]', + '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>', + '<p data-test="one">new</p><p data-test="OnE">new</p><p data-test="two">3</p>', + ); + await checkSelector( + 'p[data-test="one" s]', + '<p data-test="one">1</p><p data-test="OnE">2</p><p data-test="two">3</p>', + '<p data-test="one">new</p><p data-test="OnE">2</p><p data-test="two">3</p>', + ); + await checkSelector( + 'p[data-test~="two"]', + '<p data-test="one two three">1</p><p data-test="one two">2</p><p data-test="one">3</p>', + '<p data-test="one two three">new</p><p data-test="one two">new</p><p data-test="one">3</p>', + ); + await checkSelector( + 'p[data-test^="a"]', + '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>', + '<p data-test="a1">new</p><p data-test="a2">new</p><p data-test="b1">3</p>', + ); + await checkSelector( + 'p[data-test$="1"]', + '<p data-test="a1">1</p><p data-test="a2">2</p><p data-test="b1">3</p>', + '<p data-test="a1">new</p><p data-test="a2">2</p><p data-test="b1">new</p>', + ); + await checkSelector( + 'p[data-test*="b"]', + '<p data-test="abc">1</p><p data-test="ab">2</p><p data-test="a">3</p>', + '<p data-test="abc">new</p><p data-test="ab">new</p><p data-test="a">3</p>', + ); + await checkSelector( + 'p[data-test|="a"]', + '<p data-test="a">1</p><p data-test="a-1">2</p><p data-test="a2">3</p>', + '<p data-test="a">new</p><p data-test="a-1">new</p><p data-test="a2">3</p>', + ); + await checkSelector( + "div span", + "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>", + "<div><h1><span>new</span></h1><span>new</span><b>3</b></div>", + ); + await checkSelector( + "div > span", + "<div><h1><span>1</span></h1><span>2</span><b>3</b></div>", + "<div><h1><span>1</span></h1><span>new</span><b>3</b></div>", + ); + }); + + it("supports deleting innerContent", async () => { + expect( + await new HTMLRewriter() + .on("div", { + element(elem) { + // https://github.com/oven-sh/bun/issues/2323 + elem.setInnerContent(""); + }, + }) + .transform(new Response("<div>content</div>")) + .text(), + ).toEqual("<div></div>"); + }); + + it("supports deleting innerHTML", async () => { + expect( + await new HTMLRewriter() + .on("div", { + element(elem) { + // https://github.com/oven-sh/bun/issues/2323 + elem.setInnerContent("", { html: true }); + }, + }) + .transform(new Response("<div><span>content</span></div>")) + .text(), + ).toEqual("<div></div>"); + }); + + it("it supports lastInTextNode", async () => { + let lastInTextNode; + + await new HTMLRewriter() + .on("p", { + text(text) { + lastInTextNode ??= text.lastInTextNode; + }, + }) + .transform(new Response("<p>Lorem ipsum!</p>")) + .text(); + + expect(lastInTextNode).toBeBoolean(); + }); + + it("it supports selfClosing", async () => { + const selfClosing = {}; + await new HTMLRewriter() + .on("*", { + element(el) { + selfClosing[el.tagName] = el.selfClosing; + }, + }) + + .transform(new Response("<p>Lorem ipsum!<br></p><div />")) + .text(); + + expect(selfClosing).toEqual({ + p: false, + br: false, + div: true, + }); + }); + + it("it supports canHaveContent", async () => { + const canHaveContent = {}; + await new HTMLRewriter() + .on("*", { + element(el) { + canHaveContent[el.tagName] = el.canHaveContent; + }, + }) + .transform(new Response("<p>Lorem ipsum!<br></p><div /><svg><circle /></svg>")) + .text(); + + expect(canHaveContent).toEqual({ + p: true, + br: false, + div: true, + svg: true, + circle: false, + }); + }); +}); + +// By not segfaulting, this test passes +it("#3334 regression", async () => { + for (let i = 0; i < 10; i++) { + const headers = new Headers({ + "content-type": "text/html", + }); + const response = new Response("<div>content</div>", { headers }); + + const result = await new HTMLRewriter() + .on("div", { + element(elem) { + elem.setInnerContent("new"); + }, + }) + .transform(response) + .text(); + expect(result).toEqual("<div>new</div>"); + } + Bun.gc(true); +}); + +it("#3489", async () => { + var el; + await new HTMLRewriter() + .on("p", { + element(element) { + el = element.getAttribute("id"); + }, + }) + .transform(new Response('<p id="Šžõäöü"></p>')) + .text(); + expect(el).toEqual("Šžõäöü"); +}); + +it("get attribute - ascii", async () => { + for (let i = 0; i < 10; i++) { + var el; + await new HTMLRewriter() + .on("p", { + element(element) { + el = element.getAttribute("id"); + }, + }) + .transform(new Response(`<p id="asciii"></p>`)) + .text(); + expect(el).toEqual("asciii"); + } +}); + +it("#3520", async () => { + const pairs = []; + + await new HTMLRewriter() + .on("p", { + element(element) { + for (const pair of element.attributes) { + pairs.push(pair); + } + }, + }) + .transform(new Response('<p šž="Õäöü" ab="Õäöü" šž="Õäöü" šž="dc" šž="🕵🏻"></p>')) + .text(); + + expect(pairs).toEqual([ + ["šž", "Õäöü"], + ["ab", "Õäöü"], + ["šž", "Õäöü"], + ["šž", "dc"], + ["šž", "🕵🏻"], + ]); +}); const fixture_html = path.join(import.meta.dir, "../web/fetch/fixture.html"); const fixture_html_content = fs.readFileSync(fixture_html); |