Skip to content
File

Blob: src/workerd/api/html-rewriter.h

cpp447 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#pragma once
6 
7#include <workerd/api/http.h>
8#include <workerd/jsg/jsg.h>
9 
10#include <v8.h>
11 
12struct lol_html_HtmlRewriterBuilder;
13struct lol_html_HtmlRewriter;
14struct lol_html_Doctype;
15struct lol_html_DocumentEnd;
16struct lol_html_Comment;
17struct lol_html_TextChunk;
18struct lol_html_Element;
19struct lol_html_EndTag;
20struct lol_html_AttributesIterator;
21struct lol_html_Attribute;
22// Defined in lol_html.h, forward declarations mirrored here so we don't need the header.
23 
24KJ_DECLARE_NON_POLYMORPHIC(lol_html_AttributesIterator);
25 
26namespace workerd::api {
27 
28class Element;
29class EndTag;
30class Comment;
31class Text;
32class Doctype;
33class DocumentEnd;
34 
35// =======================================================================================
36// HTMLRewriter
37 
38class HTMLRewriter: public jsg::Object {
39 public:
40 class Token;
41 class TokenScope;
42 
43 explicit HTMLRewriter();
44 ~HTMLRewriter() noexcept(false);
45 KJ_DISALLOW_COPY_AND_MOVE(HTMLRewriter);
46 
47 static jsg::Ref<HTMLRewriter> constructor(jsg::Lock& js);
48 
49 using ElementCallback = kj::Promise<void>(jsg::Ref<jsg::Object> element);
50 using ElementCallbackFunction = jsg::Function<ElementCallback>;
51 
52 // A struct-like wrapper around element content handlers. I say struct-like, because we only use
53 // this wrapper as a convenience to help us access the three function properties that we expect
54 // to find. In reality, this is more like a "callback interface" in Web IDL terms, since we hang
55 // onto the original object so that we can use it as the `this` argument.
56 struct ElementContentHandlers {
57 jsg::Optional<ElementCallbackFunction> element;
58 jsg::Optional<ElementCallbackFunction> comments;
59 jsg::Optional<ElementCallbackFunction> text;
60 
61 JSG_STRUCT(element, comments, text);
62 
63 JSG_STRUCT_TS_OVERRIDE({
64 element?(element: Element): void | Promise<void>;
65 comments?(comment: Comment): void | Promise<void>;
66 text?(element: Text): void | Promise<void>;
67 });
68 // Specify parameter types for callback functions
69 };
70 
71 // A struct-like wrapper around document content handlers. See the doc comment on
72 // ElementContentHandlers for more information on its idiosyncrasies.
73 struct DocumentContentHandlers {
74 jsg::Optional<ElementCallbackFunction> doctype;
75 jsg::Optional<ElementCallbackFunction> comments;
76 jsg::Optional<ElementCallbackFunction> text;
77 jsg::Optional<ElementCallbackFunction> end;
78 
79 JSG_STRUCT(doctype, comments, text, end);
80 
81 JSG_STRUCT_TS_OVERRIDE({
82 doctype?(doctype: Doctype): void | Promise<void>;
83 comments?(comment: Comment): void | Promise<void>;
84 text?(text: Text): void | Promise<void>;
85 end?(end: DocumentEnd): void | Promise<void>;
86 });
87 // Specify parameter types for callback functions
88 };
89 
90 jsg::Ref<HTMLRewriter> on(kj::String selector, ElementContentHandlers&& handlers);
91 jsg::Ref<HTMLRewriter> onDocument(DocumentContentHandlers&& handlers);
92 
93 // Register element or doctype content handlers. `handlers` must be unwrappable into a
94 // ElementContentHandlers or DocumentContentHandlers struct, respectively. We take it as a
95 // v8::Object so that we can use it as the `this` argument for the function calls.
96 
97 // Create a new Response object that is identical to the input response except that its body is
98 // the result of running the original body through this HTMLRewriter's rewriter. This
99 // function does not run the parser itself -- to drive the parser, you must read the transformed
100 // response body.
101 //
102 // Pre-condition: the input response body is not disturbed.
103 // Post-condition: the input response body is disturbed.
104 jsg::Ref<Response> transform(jsg::Lock& js, jsg::Ref<Response> response);
105 
106 JSG_RESOURCE_TYPE(HTMLRewriter) {
107 JSG_METHOD(on);
108 JSG_METHOD(onDocument);
109 JSG_METHOD(transform);
110 }
111 
112 void visitForMemoryInfo(jsg::MemoryTracker& tracker) const;
113 
114 private:
115 void visitForGc(jsg::GcVisitor& visitor);
116 
117 struct Impl;
118 kj::Own<Impl> impl;
119};
120 
121// A chunk of text or HTML which can be passed to content token mutation functions.
122using Content = kj::OneOf<kj::String, jsg::Ref<ReadableStream>, jsg::Ref<Response>>;
123// TODO(soon): Support ReadableStream/Response types. Requires fibers or lol-html saveable state.
124 
125// Options bag which can be passed to content token mutation functions.
126struct ContentOptions {
127 // True if the Content being passed to the mutation function is HTML. If false, the content will
128 // be escaped (HTML entity-encoded).
129 jsg::Optional<bool> html;
130 
131 JSG_STRUCT(html);
132};
133 
134class Rewriter;
135 
136// =======================================================================================
137// HTML Content Tokens
138//
139// HTML content tokens represent individual chunks of HTML that scripts can manipulate. There are
140// four types: Element, Comment, Text, and Doctype. Each one wraps a corresponding lower-level
141// handle that lol-html passes to our callbacks. These lower-level handles are only valid during
142// the execution (scope) of the callback, which the JS wrapper objects can obviously outlive. To
143// cope with that, we have some machinery (HTMLRewriter::TokenScope/Token) which enforces
144// in-scope access.
145//
146// The Element content token also exposes an AttributesIterator type. This is not a content token
147// per se, but follows the same scoping rule.
148//
149// Note, when generating TypeScript types, definitions to include are collected before overrides are
150// applied. Because ElementCallbackFunction's parameter is always jsg::Ref<jsg::Object> and not the
151// token type, we would not include token types by default as these are only defined in overrides.
152// Therefore, we manually define each token type as a JSG_TS_ROOT(), so it gets visited when
153// collecting definitions.
154 
155class HTMLRewriter::Token: public jsg::Object {
156 public:
157 virtual void htmlContentScopeEnd() = 0;
158 
159 // Each Token subclass has an inner ImplBase subclass which holds a reference
160 // to the rewriter, and the actual underlying lol-html C API handle for the token.
161 template <typename CType>
162 struct ImplBase {
163 ImplBase(CType& element, Rewriter& rewriter);
164 KJ_DISALLOW_COPY_AND_MOVE(ImplBase);
165 ~ImplBase() noexcept(false);
166 
167 // Dispatches calls to the underlying lol_html methods for each event (e.g. before, after, replace).
168 // Handles replacements of each supported type (string, ReadableStream, Body).
169 template <auto Func, auto StreamingFunc>
170 void rewriteContentGeneric(Content content, jsg::Optional<ContentOptions> options);
171 
172 CType& element;
173 Rewriter& rewriter;
174 };
175};
176 
177class Element final: public HTMLRewriter::Token {
178 public:
179 using CType = lol_html_Element;
180 
181 explicit Element(CType& element, Rewriter& wrapper);
182 
183 kj::String getTagName();
184 void setTagName(kj::String tagName);
185 
186 class AttributesIterator;
187 jsg::Ref<AttributesIterator> getAttributes(jsg::Lock& js);
188 
189 bool getRemoved();
190 
191 kj::StringPtr getNamespaceURI();
192 
193 kj::Maybe<kj::String> getAttribute(kj::String name);
194 bool hasAttribute(kj::String name);
195 jsg::Ref<Element> setAttribute(kj::String name, kj::String value);
196 jsg::Ref<Element> removeAttribute(kj::String name);
197 
198 jsg::Ref<Element> before(Content content, jsg::Optional<ContentOptions> options);
199 jsg::Ref<Element> after(Content content, jsg::Optional<ContentOptions> options);
200 
201 jsg::Ref<Element> prepend(Content content, jsg::Optional<ContentOptions> options);
202 jsg::Ref<Element> append(Content content, jsg::Optional<ContentOptions> options);
203 
204 jsg::Ref<Element> replace(Content content, jsg::Optional<ContentOptions> options);
205 jsg::Ref<Element> setInnerContent(Content content, jsg::Optional<ContentOptions> options);
206 
207 jsg::Ref<Element> remove();
208 jsg::Ref<Element> removeAndKeepContent();
209 
210 void onEndTag(HTMLRewriter::ElementCallbackFunction&& callback);
211 
212 JSG_RESOURCE_TYPE(Element) {
213 JSG_INSTANCE_PROPERTY(tagName, getTagName, setTagName);
214 JSG_READONLY_INSTANCE_PROPERTY(attributes, getAttributes);
215 JSG_READONLY_INSTANCE_PROPERTY(removed, getRemoved);
216 JSG_READONLY_INSTANCE_PROPERTY(namespaceURI, getNamespaceURI);
217 
218 JSG_METHOD(getAttribute);
219 JSG_METHOD(hasAttribute);
220 JSG_METHOD(setAttribute);
221 JSG_METHOD(removeAttribute);
222 JSG_METHOD(before);
223 JSG_METHOD(after);
224 JSG_METHOD(prepend);
225 JSG_METHOD(append);
226 JSG_METHOD(replace);
227 JSG_METHOD(remove);
228 JSG_METHOD(removeAndKeepContent);
229 JSG_METHOD(setInnerContent);
230 JSG_METHOD(onEndTag);
231 
232 JSG_TS_ROOT();
233 JSG_TS_OVERRIDE({
234 onEndTag(handler: (tag: EndTag) => void | Promise<void>): void;
235 });
236 // Specify parameter type for onEndTag callback function
237 }
238 
239 private:
240 struct Impl: public HTMLRewriter::Token::ImplBase<CType> {
241 using HTMLRewriter::Token::ImplBase<CType>::ImplBase;
242 ~Impl() noexcept(false);
243 kj::Vector<jsg::Ref<AttributesIterator>> attributesIterators;
244 };
245 
246 kj::Maybe<Impl> impl;
247 
248 void htmlContentScopeEnd() override;
249};
250 
251class Element::AttributesIterator final: public HTMLRewriter::Token {
252 public:
253 using CType = lol_html_AttributesIterator;
254 
255 // lol_html_AttributesIterator has the distinction of being valid only during a content handler
256 // execution scope AND also requiring manual deallocation, so this takes an Own<T> rather than T&.
257 explicit AttributesIterator(kj::Own<CType> iter);
258 
259 struct Next {
260 bool done;
261 jsg::Optional<kj::Array<kj::String>> value;
262 
263 JSG_STRUCT(done, value);
264 };
265 
266 Next next();
267 
268 // Called by Element when its attributes are mutated (setAttribute/removeAttribute),
269 // which invalidates the underlying lol-html iterator's pointers.
270 void invalidate();
271 
272 jsg::Ref<AttributesIterator> self();
273 
274 JSG_RESOURCE_TYPE(AttributesIterator) {
275 JSG_INHERIT_INTRINSIC(v8::kIteratorPrototype);
276 JSG_METHOD(next);
277 JSG_ITERABLE(self);
278 }
279 
280 private:
281 kj::Maybe<kj::Own<CType>> impl;
282 bool mutatedDuringIteration = false;
283 
284 void htmlContentScopeEnd() override;
285};
286 
287class EndTag final: public HTMLRewriter::Token {
288 public:
289 using CType = lol_html_EndTag;
290 
291 explicit EndTag(CType& tag, Rewriter& rewriter);
292 
293 kj::String getName();
294 void setName(kj::String);
295 
296 jsg::Ref<EndTag> before(Content content, jsg::Optional<ContentOptions> options);
297 jsg::Ref<EndTag> after(Content content, jsg::Optional<ContentOptions> options);
298 jsg::Ref<EndTag> remove();
299 
300 JSG_RESOURCE_TYPE(EndTag) {
301 JSG_INSTANCE_PROPERTY(name, getName, setName);
302 
303 JSG_METHOD(before);
304 JSG_METHOD(after);
305 JSG_METHOD(remove);
306 
307 JSG_TS_ROOT();
308 }
309 
310 private:
311 kj::Maybe<HTMLRewriter::Token::ImplBase<CType>> impl;
312 
313 void htmlContentScopeEnd() override;
314};
315 
316class Comment final: public HTMLRewriter::Token {
317 public:
318 using CType = lol_html_Comment;
319 
320 explicit Comment(CType& comment, Rewriter&);
321 
322 kj::String getText();
323 void setText(kj::String);
324 
325 bool getRemoved();
326 
327 jsg::Ref<Comment> before(Content content, jsg::Optional<ContentOptions> options);
328 jsg::Ref<Comment> after(Content content, jsg::Optional<ContentOptions> options);
329 jsg::Ref<Comment> replace(Content content, jsg::Optional<ContentOptions> options);
330 jsg::Ref<Comment> remove();
331 
332 JSG_RESOURCE_TYPE(Comment) {
333 JSG_INSTANCE_PROPERTY(text, getText, setText);
334 JSG_READONLY_INSTANCE_PROPERTY(removed, getRemoved);
335 
336 JSG_METHOD(before);
337 JSG_METHOD(after);
338 JSG_METHOD(replace);
339 JSG_METHOD(remove);
340 
341 JSG_TS_ROOT();
342 JSG_TS_OVERRIDE({
343 before(content: string, options?: ContentOptions): Comment;
344 after(content: string, options?: ContentOptions): Comment;
345 replace(content: string, options?: ContentOptions): Comment;
346 });
347 // Require content to be a string
348 }
349 
350 private:
351 kj::Maybe<CType&> impl;
352 
353 void htmlContentScopeEnd() override;
354};
355 
356class Text final: public HTMLRewriter::Token {
357 public:
358 using CType = lol_html_TextChunk;
359 
360 explicit Text(CType& text, Rewriter& rewriter);
361 
362 kj::String getText();
363 
364 bool getLastInTextNode();
365 
366 bool getRemoved();
367 
368 jsg::Ref<Text> before(Content content, jsg::Optional<ContentOptions> options);
369 jsg::Ref<Text> after(Content content, jsg::Optional<ContentOptions> options);
370 jsg::Ref<Text> replace(Content content, jsg::Optional<ContentOptions> options);
371 jsg::Ref<Text> remove();
372 
373 JSG_RESOURCE_TYPE(Text) {
374 JSG_READONLY_INSTANCE_PROPERTY(text, getText);
375 JSG_READONLY_INSTANCE_PROPERTY(lastInTextNode, getLastInTextNode);
376 JSG_READONLY_INSTANCE_PROPERTY(removed, getRemoved);
377 
378 JSG_METHOD(before);
379 JSG_METHOD(after);
380 JSG_METHOD(replace);
381 JSG_METHOD(remove);
382 
383 JSG_TS_ROOT();
384 }
385 
386 private:
387 kj::Maybe<HTMLRewriter::Token::ImplBase<CType>> impl;
388 
389 void htmlContentScopeEnd() override;
390};
391 
392class Doctype final: public HTMLRewriter::Token {
393 public:
394 using CType = lol_html_Doctype;
395 
396 explicit Doctype(CType& doctype, Rewriter&);
397 
398 kj::Maybe<kj::String> getName();
399 kj::Maybe<kj::String> getPublicId();
400 kj::Maybe<kj::String> getSystemId();
401 
402 JSG_RESOURCE_TYPE(Doctype) {
403 JSG_READONLY_INSTANCE_PROPERTY(name, getName);
404 JSG_READONLY_INSTANCE_PROPERTY(publicId, getPublicId);
405 JSG_READONLY_INSTANCE_PROPERTY(systemId, getSystemId);
406 
407 JSG_TS_ROOT();
408 }
409 
410 private:
411 kj::Maybe<CType&> impl;
412 
413 void htmlContentScopeEnd() override;
414};
415 
416class DocumentEnd final: public HTMLRewriter::Token {
417 public:
418 using CType = lol_html_DocumentEnd;
419 
420 explicit DocumentEnd(CType& documentEnd, Rewriter&);
421 
422 jsg::Ref<DocumentEnd> append(Content content, jsg::Optional<ContentOptions> options);
423 
424 JSG_RESOURCE_TYPE(DocumentEnd) {
425 JSG_METHOD(append);
426 
427 JSG_TS_ROOT();
428 JSG_TS_OVERRIDE({
429 append(content: string, options?: ContentOptions): DocumentEnd;
430 });
431 // Require content to be a string
432 }
433 
434 private:
435 kj::Maybe<CType&> impl;
436 
437 void htmlContentScopeEnd() override;
438};
439 
440#define EW_HTML_REWRITER_ISOLATE_TYPES \
441 api::ContentOptions, api::HTMLRewriter, api::HTMLRewriter::ElementContentHandlers, \
442 api::HTMLRewriter::DocumentContentHandlers, api::Doctype, api::Element, api::EndTag, \
443 api::Comment, api::Text, api::DocumentEnd, api::Element::AttributesIterator, \
444 api::Element::AttributesIterator::Next
445 
446} // namespace workerd::api