Skip to content

Commit 1bd5f69

Browse files
committed
Add support for Element#onEndTag
1 parent c6b772f commit 1bd5f69

File tree

7 files changed

+222
-17
lines changed

7 files changed

+222
-17
lines changed

html_rewriter.js.patch

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
--- pkg/html_rewriter.js 2021-07-24 10:12:13.000000000 +0100
2-
+++ pkg2/html_rewriter.js 2021-07-24 10:11:57.000000000 +0100
1+
--- pkg/html_rewriter.js 2022-01-18 17:37:39.000000000 +0000
2+
+++ pkg2/html_rewriter.js 2022-01-18 17:37:19.000000000 +0000
33
@@ -1,7 +1,7 @@
44
let imports = {};
55
imports['__wbindgen_placeholder__'] = module.exports;
@@ -118,7 +118,7 @@
118118
}
119119
/**
120120
* @param {string} content
121-
@@ -530,11 +543,13 @@
121+
@@ -530,17 +543,19 @@
122122
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
123123
var len0 = WASM_VECTOR_LEN;
124124
wasm.element_setInnerContent(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
@@ -130,9 +130,38 @@
130130
wasm.element_removeAndKeepContent(this.ptr);
131131
+ return this;
132132
}
133+
/**
134+
* @param {any} handler
135+
*/
136+
onEndTag(handler) {
137+
- wasm.element_onEndTag(this.ptr, addHeapObject(handler));
138+
+ wasm.element_onEndTag(this.ptr, addHeapObject(handler.bind(this)));
139+
}
133140
}
134141
module.exports.Element = Element;
135-
@@ -579,25 +594,27 @@
142+
@@ -597,6 +612,7 @@
143+
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
144+
var len0 = WASM_VECTOR_LEN;
145+
wasm.endtag_before(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
146+
+ return this;
147+
}
148+
/**
149+
* @param {string} content
150+
@@ -606,11 +622,13 @@
151+
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
152+
var len0 = WASM_VECTOR_LEN;
153+
wasm.endtag_after(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
154+
+ return this;
155+
}
156+
/**
157+
*/
158+
remove() {
159+
wasm.endtag_remove(this.ptr);
160+
+ return this;
161+
}
162+
}
163+
module.exports.EndTag = EndTag;
164+
@@ -656,25 +674,27 @@
136165
var ptr0 = passStringToWasm0(selector, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
137166
var len0 = WASM_VECTOR_LEN;
138167
wasm.htmlrewriter_on(this.ptr, ptr0, len0, addHeapObject(handlers));
@@ -164,23 +193,23 @@
164193
}
165194
/**
166195
* @returns {number}
167-
@@ -638,6 +655,7 @@
196+
@@ -715,6 +735,7 @@
168197
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
169198
var len0 = WASM_VECTOR_LEN;
170199
wasm.textchunk_before(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
171200
+ return this;
172201
}
173202
/**
174203
* @param {string} content
175-
@@ -647,6 +665,7 @@
204+
@@ -724,6 +745,7 @@
176205
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
177206
var len0 = WASM_VECTOR_LEN;
178207
wasm.textchunk_after(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
179208
+ return this;
180209
}
181210
/**
182211
* @param {string} content
183-
@@ -656,11 +675,13 @@
212+
@@ -733,11 +755,13 @@
184213
var ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
185214
var len0 = WASM_VECTOR_LEN;
186215
wasm.textchunk_replace(this.ptr, ptr0, len0, isLikeNone(content_type) ? 0 : addHeapObject(content_type));
@@ -194,8 +223,8 @@
194223
}
195224
/**
196225
* @returns {boolean}
197-
@@ -801,7 +822,8 @@
198-
}, arguments) };
226+
@@ -893,7 +917,8 @@
227+
};
199228

200229
module.exports.__wbg_instanceof_Promise_c6535fc791fcc4d2 = function(arg0) {
201230
- var ret = getObject(arg0) instanceof Promise;
@@ -204,7 +233,7 @@
204233
return ret;
205234
};
206235

207-
@@ -847,5 +869,6 @@
236+
@@ -939,5 +964,6 @@
208237
const wasmModule = new WebAssembly.Module(bytes);
209238
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
210239
wasm = wasmInstance.exports;

src/element.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
use super::end_tag::EndTag;
2+
use super::handlers::{await_promise, make_handler, HandlerJsErrorWrap};
13
use super::*;
4+
use js_sys::{Function as JsFunction, Promise as JsPromise};
25
use lol_html::html_content::Element as NativeElement;
36
use serde_wasm_bindgen::to_value as to_js_value;
7+
use wasm_bindgen::JsCast;
48

59
#[wasm_bindgen]
610
pub struct Element(NativeRefWrap<NativeElement<'static, 'static>>);
@@ -100,4 +104,14 @@ impl Element {
100104
pub fn remove_and_keep_content(&mut self) -> Result<(), JsValue> {
101105
self.0.get_mut().map(|e| e.remove_and_keep_content())
102106
}
107+
108+
#[wasm_bindgen(method, js_name=onEndTag)]
109+
pub fn on_end_tag(&mut self, handler: JsFunction) -> Result<(), JsValue> {
110+
let this = JsValue::NULL;
111+
let stack_ptr = self.0.stack_ptr;
112+
self.0
113+
.get_mut()?
114+
.on_end_tag(make_handler!(handler, EndTag, this, stack_ptr))
115+
.into_js_result()
116+
}
103117
}

src/end_tag.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use super::*;
2+
use lol_html::html_content::EndTag as NativeEndTag;
3+
4+
#[wasm_bindgen]
5+
pub struct EndTag(NativeRefWrap<NativeEndTag<'static>>);
6+
7+
impl_from_native!(NativeEndTag --> EndTag);
8+
9+
#[wasm_bindgen]
10+
impl EndTag {
11+
#[wasm_bindgen(method, getter=name)]
12+
pub fn name(&self) -> JsResult<String> {
13+
self.0.get().map(|e| e.name())
14+
}
15+
16+
#[wasm_bindgen(method, setter=name)]
17+
pub fn set_name(&mut self, name: &str) -> JsResult<()> {
18+
self.0.get_mut().map(|e| e.set_name_str(String::from(name)))
19+
}
20+
21+
pub fn before(
22+
&mut self,
23+
content: &str,
24+
content_type: Option<ContentTypeOptions>,
25+
) -> JsResult<()> {
26+
self.0
27+
.get_mut()
28+
.map(|e| e.before(content, content_type.into_native()))
29+
}
30+
31+
pub fn after(
32+
&mut self,
33+
content: &str,
34+
content_type: Option<ContentTypeOptions>,
35+
) -> JsResult<()> {
36+
self.0
37+
.get_mut()
38+
.map(|e| e.after(content, content_type.into_native()))
39+
}
40+
41+
pub fn remove(&mut self) -> JsResult<()> {
42+
self.0.get_mut().map(|e| e.remove())
43+
}
44+
}

src/handlers.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ unsafe impl Sync for HandlerJsErrorWrap {}
2626
#[wasm_bindgen(raw_module = "./asyncify.js")]
2727
extern "C" {
2828
#[wasm_bindgen(js_name = awaitPromise)]
29-
fn await_promise(stack_ptr: *mut u8, promise: &JsPromise);
29+
pub(crate) fn await_promise(stack_ptr: *mut u8, promise: &JsPromise);
3030
}
3131

3232
macro_rules! make_handler {
3333
($handler:ident, $JsArgType:ident, $this:ident, $stack_ptr:ident) => {
3434
move |arg: &mut _| {
35-
let (js_arg, anchor) = $JsArgType::from_native(arg);
35+
let (js_arg, anchor) = $JsArgType::from_native(arg, $stack_ptr);
3636
let js_arg = JsValue::from(js_arg);
3737

3838
let res = match $handler.call1(&$this, &js_arg) {
@@ -51,6 +51,7 @@ macro_rules! make_handler {
5151
}
5252
};
5353
}
54+
pub(crate) use make_handler;
5455

5556
pub trait IntoNativeHandlers<T> {
5657
fn into_native(self, stack_ptr: *mut u8) -> T;

src/html_rewriter.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export class Element {
1919
readonly namespaceURI: string;
2020
readonly removed: boolean;
2121
tagName: string;
22+
onEndTag(handler: (this: this, endTag: EndTag) => void | Promise<void>): void;
23+
}
24+
25+
export class EndTag {
26+
before(content: string, options?: ContentTypeOptions): this;
27+
after(content: string, options?: ContentTypeOptions): this;
28+
remove(): this;
29+
name: string;
2230
}
2331

2432
export class Comment {
@@ -68,7 +76,10 @@ export interface HTMLRewriterOptions {
6876
}
6977

7078
export class HTMLRewriter {
71-
constructor(outputSink: (chunk: Uint8Array) => void, options?: HTMLRewriterOptions);
79+
constructor(
80+
outputSink: (chunk: Uint8Array) => void,
81+
options?: HTMLRewriterOptions
82+
);
7283
on(selector: string, handlers: ElementHandlers): this;
7384
onDocument(handlers: DocumentHandlers): this;
7485
write(chunk: Uint8Array): Promise<void>;

src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ impl Drop for Anchor<'_> {
3535
//
3636
// When anchor goes out of scope, wrapper becomes poisoned and any attempt to get inner
3737
// object results in exception.
38+
#[derive(Clone)]
3839
struct NativeRefWrap<R> {
3940
inner_ptr: *mut R,
4041
poisoned: Rc<Cell<bool>>,
42+
stack_ptr: *mut u8,
4143
}
4244

4345
impl<R> NativeRefWrap<R> {
44-
pub fn wrap<I>(inner: &mut I) -> (Self, Anchor) {
46+
pub fn wrap<I>(inner: &mut I, stack_ptr: *mut u8) -> (Self, Anchor) {
4547
let wrap = NativeRefWrap {
4648
inner_ptr: unsafe { mem::transmute(inner) },
4749
poisoned: Rc::new(Cell::new(false)),
50+
stack_ptr,
4851
};
4952

5053
let anchor = Anchor::new(Rc::clone(&wrap.poisoned));
@@ -158,8 +161,11 @@ macro_rules! impl_mutations {
158161
macro_rules! impl_from_native {
159162
($Ty:ident --> $JsTy:ident) => {
160163
impl $JsTy {
161-
pub(crate) fn from_native<'r>(inner: &'r mut $Ty) -> (Self, Anchor<'r>) {
162-
let (ref_wrap, anchor) = NativeRefWrap::wrap(inner);
164+
pub(crate) fn from_native<'r>(
165+
inner: &'r mut $Ty,
166+
stack_ptr: *mut u8,
167+
) -> (Self, Anchor<'r>) {
168+
let (ref_wrap, anchor) = NativeRefWrap::wrap(inner, stack_ptr);
163169

164170
($JsTy(ref_wrap), anchor)
165171
}
@@ -171,6 +177,7 @@ mod comment;
171177
mod doctype;
172178
mod document_end;
173179
mod element;
180+
mod end_tag;
174181
mod handlers;
175182
mod html_rewriter;
176183
mod text_chunk;

test/element.spec.ts

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ test("element allows chaining", async (t) => {
134134
})
135135
.transform("<p>test</p>");
136136
});
137-
test("handles element async handler", async (t) => {
137+
test.serial("handles element async handler", async (t) => {
138138
const res = await new HTMLRewriter()
139139
.on("p", {
140140
async element(element) {
@@ -158,3 +158,102 @@ test("handles element class handler", async (t) => {
158158
.transform("<p>test</p>");
159159
t.is(res, "<p>new</p>");
160160
});
161+
162+
test("handles end tag properties", async (t) => {
163+
const res = await new HTMLRewriter()
164+
.on("p", {
165+
element(element) {
166+
element.onEndTag(function (end) {
167+
t.is(this, element);
168+
t.is(end.name, "p");
169+
end.name = "h1";
170+
});
171+
},
172+
})
173+
.transform("<p>test</p>");
174+
t.is(res, "<p>test</h1>");
175+
});
176+
test("handles end tag mutations", async (t) => {
177+
const input = "<p>test</p>";
178+
const beforeAfterExpected = [
179+
"<p>",
180+
"test",
181+
"&lt;span&gt;before&lt;/span&gt;",
182+
"<span>before html</span>",
183+
"</p>",
184+
"<span>after html</span>",
185+
"&lt;span&gt;after&lt;/span&gt;",
186+
].join("");
187+
const removeExpected = "<p>test";
188+
189+
// before/after
190+
let res = await new HTMLRewriter()
191+
.on("p", {
192+
element(element) {
193+
const that = this;
194+
element.onEndTag((end) => {
195+
t.is(this, that);
196+
end.before("<span>before</span>");
197+
end.before("<span>before html</span>", { html: true });
198+
end.after("<span>after</span>");
199+
end.after("<span>after html</span>", { html: true });
200+
});
201+
},
202+
})
203+
.transform(input);
204+
t.is(res, beforeAfterExpected);
205+
206+
// remove
207+
res = await new HTMLRewriter()
208+
.on("p", {
209+
element(element) {
210+
element.onEndTag((end) => {
211+
end.remove();
212+
});
213+
},
214+
})
215+
.transform(input);
216+
t.is(res, removeExpected);
217+
});
218+
test("end tag allows chaining", async (t) => {
219+
t.plan(3);
220+
await new HTMLRewriter()
221+
.on("p", {
222+
element(element) {
223+
element.onEndTag((end) => {
224+
t.is(end.before(""), end);
225+
t.is(end.after(""), end);
226+
t.is(end.remove(), end);
227+
});
228+
},
229+
})
230+
.transform("<p>test</p>");
231+
});
232+
test.serial("handles end tag async handler", async (t) => {
233+
const res = await new HTMLRewriter()
234+
.on("p", {
235+
element(element) {
236+
element.onEndTag(async (end) => {
237+
await wait(50);
238+
end.before("!");
239+
});
240+
},
241+
})
242+
.transform("<p>test</p>");
243+
t.is(res, "<p>test!</p>");
244+
});
245+
test("uses last end tag handler", async (t) => {
246+
const res = await new HTMLRewriter()
247+
.on("p", {
248+
element(element) {
249+
element.onEndTag((end) => {
250+
end.before("1");
251+
});
252+
element.onEndTag((end) => {
253+
end.before("2");
254+
});
255+
},
256+
})
257+
.transform("<p>test</p>");
258+
t.is(res, "<p>test2</p>");
259+
});

0 commit comments

Comments
 (0)