diff options
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | .vscode/settings.json | 1 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | build.zig | 7 | ||||
-rw-r--r-- | integration/bunjs-only-snippets/html-rewriter.test.js | 228 | ||||
m--------- | src/deps/lol-html | 0 | ||||
-rw-r--r-- | src/deps/lol-html.zig | 705 | ||||
-rw-r--r-- | src/javascript/jsc/api/html_rewriter.zig | 1340 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 12 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/BunStream.cpp | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/Path.cpp | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.cpp | 16 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/bindings.zig | 47 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/exports.zig | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers-cpp.h | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.h | 7 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/headers.zig | 1 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/helpers.h | 2 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 4 | ||||
-rw-r--r-- | src/javascript/jsc/node/types.zig | 8 | ||||
-rw-r--r-- | src/jsc.zig | 6 |
21 files changed, 2249 insertions, 150 deletions
diff --git a/.gitmodules b/.gitmodules index 9dd25dae9..61f62fbfb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -29,3 +29,6 @@ path = src/deps/libbacktrace url = https://github.com/ianlancetaylor/libbacktrace ignore = dirty +[submodule "src/deps/lol-html"] + path = src/deps/lol-html + url = https://github.com/cloudflare/lol-html diff --git a/.vscode/settings.json b/.vscode/settings.json index bd7699e02..dcd7498a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,6 +41,7 @@ "src/deps/boringssl": true, "src/deps/openssl": true, "src/deps/zlib": true, + "src/deps/lol-html": true, "integration/snippets/package-json-exports/_node_modules_copy": true }, "C_Cpp.files.exclude": { @@ -266,6 +266,7 @@ ARCHIVE_FILES_WITHOUT_LIBCRYPTO = $(MIMALLOC_FILE_PATH) \ $(BUN_DEPS_OUT_DIR)/libarchive.a \ $(BUN_DEPS_OUT_DIR)/libssl.a \ $(BUN_DEPS_OUT_DIR)/picohttpparser.o \ + $(BUN_DEPS_OUT_DIR)/liblolhtml.a ARCHIVE_FILES = $(ARCHIVE_FILES_WITHOUT_LIBCRYPTO) $(BUN_DEPS_OUT_DIR)/libcrypto.boring.a @@ -312,6 +313,9 @@ bun: vendor-without-check: api analytics node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp zlib boringssl libarchive libbacktrace +lolhtml: + cd $(BUN_DEPS_DIR)/lol-html/ && cd $(BUN_DEPS_DIR)/lol-html/c-api && cargo build --release && cp target/release/liblolhtml.a $(BUN_DEPS_OUT_DIR) + boringssl-build: cd $(BUN_DEPS_DIR)/boringssl && mkdir -p build && cd build && CFLAGS="$(CFLAGS)" cmake $(CMAKE_FLAGS) -GNinja .. && ninja @@ -79,6 +79,11 @@ fn addInternalPackages(step: *std.build.LibExeObjStep, _: std.mem.Allocator, tar .path = pkgPath("src/io/io_stub.zig"), }; + var lol_html: std.build.Pkg = .{ + .name = "lolhtml", + .path = pkgPath("src/deps/lol-html.zig"), + }; + var io = if (target.isDarwin()) io_darwin else if (target.isLinux()) @@ -156,6 +161,7 @@ fn addInternalPackages(step: *std.build.LibExeObjStep, _: std.mem.Allocator, tar step.addPackage(javascript_core); step.addPackage(crash_reporter); step.addPackage(datetime); + step.addPackage(lol_html); } var output_dir: []const u8 = ""; fn panicIfNotFound(comptime filepath: []const u8) []const u8 { @@ -512,6 +518,7 @@ pub fn linkObjectFiles(b: *std.build.Builder, obj: *std.build.LibExeObjStep, tar .{ "libWTF.a", "libWTF.a" }, .{ "libbmalloc.a", "libbmalloc.a" }, .{ "libbacktrace.a", "libbacktrace.a" }, + .{ "liblolhtml.a", "liblolhtml.a" }, }); for (dirs_to_search.slice()) |deps_path| { diff --git a/integration/bunjs-only-snippets/html-rewriter.test.js b/integration/bunjs-only-snippets/html-rewriter.test.js index cd4fad746..77c14e479 100644 --- a/integration/bunjs-only-snippets/html-rewriter.test.js +++ b/integration/bunjs-only-snippets/html-rewriter.test.js @@ -1,8 +1,232 @@ import { describe, it, expect } from "bun:test"; describe("HTMLRewriter", () => { - it("exists globally", () => { + it("exists globally", async () => { expect(typeof HTMLRewriter).toBe("function"); - console.log(HTMLRewriter.name); + expect(typeof HTMLRewriter.constructor).toBe("function"); + }); + 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("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 (t, func) => { + const res = func(new HTMLRewriter(), (comment) => { + expect(comment.removed).toBe(false); + expect(comment.text).toBe("test"); + comment.text = "new"; + }).transform(new Response("<p><!--test--></p>")); + t.is(await res.text(), "<p><!--new--></p>"); + }; + test( + "HTMLRewriter: handles comment properties", + commentPropertiesMacro, + (rw, comments) => rw.on("p", { comments }) + ); + test( + "HTMLRewriter: handles comment mutations", + mutationsMacro, + (rw, comments) => rw.on("p", { comments }), + commentsMutationsInput, + commentsMutationsExpected + ); + + 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>" + ); }); }); diff --git a/src/deps/lol-html b/src/deps/lol-html new file mode 160000 +Subproject 2eed349dcdfa4ff5c19fe7c6e501cfd68760103 diff --git a/src/deps/lol-html.zig b/src/deps/lol-html.zig new file mode 100644 index 000000000..79cbceb75 --- /dev/null +++ b/src/deps/lol-html.zig @@ -0,0 +1,705 @@ +pub const Error = error{Fail}; +const std = @import("std"); + +pub const MemorySettings = extern struct { + preallocated_parsing_buffer_size: usize, + max_allowed_memory_usage: usize, +}; + +pub const HTMLRewriter = opaque { + extern fn lol_html_rewriter_write(rewriter: *HTMLRewriter, chunk: [*]const u8, chunk_len: usize) c_int; + extern fn lol_html_rewriter_end(rewriter: *HTMLRewriter) c_int; + extern fn lol_html_rewriter_free(rewriter: *HTMLRewriter) void; + + pub fn write(rewriter: *HTMLRewriter, chunk: []const u8) Error!void { + if (rewriter.lol_html_rewriter_write(chunk.ptr, chunk.len) < 0) + return error.Fail; + } + + /// Completes rewriting and flushes the remaining output. + /// + /// Returns 0 in case of success and -1 otherwise. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + /// + /// WARNING: after calling this function, further attempts to use the rewriter + /// (other than `lol_html_rewriter_free`) will cause a thread panic. + pub fn end(rewriter: *HTMLRewriter) Error!void { + if (rewriter.lol_html_rewriter_end() < 0) + return error.Fail; + } + + pub fn deinit(this: *HTMLRewriter) void { + this.lol_html_rewriter_free(); + } + + pub const Builder = opaque { + extern fn lol_html_rewriter_builder_new() *HTMLRewriter.Builder; + extern fn lol_html_rewriter_builder_add_element_content_handlers( + builder: *HTMLRewriter.Builder, + selector: *const HTMLSelector, + element_handler: ?lol_html_element_handler_t, + element_handler_user_data: ?*anyopaque, + comment_handler: ?lol_html_comment_handler_t, + comment_handler_user_data: ?*anyopaque, + text_handler: ?lol_html_text_handler_handler_t, + text_handler_user_data: ?*anyopaque, + ) c_int; + extern fn lol_html_rewriter_builder_free(builder: *HTMLRewriter.Builder) void; + extern fn lol_html_rewriter_build( + builder: *HTMLRewriter.Builder, + encoding: [*]const u8, + encoding_len: usize, + memory_settings: MemorySettings, + output_sink: ?fn ([*]const u8, usize, *anyopaque) callconv(.C) void, + output_sink_user_data: *anyopaque, + strict: bool, + ) ?*HTMLRewriter; + extern fn unstable_lol_html_rewriter_build_with_esi_tags( + builder: *HTMLRewriter.Builder, + encoding: [*]const u8, + encoding_len: usize, + memory_settings: MemorySettings, + output_sink: ?fn ([*]const u8, usize, *anyopaque) callconv(.C) void, + output_sink_user_data: *anyopaque, + strict: bool, + ) ?*HTMLRewriter; + + extern fn lol_html_rewriter_builder_add_document_content_handlers( + builder: *HTMLRewriter.Builder, + doctype_handler: ?DirectiveFunctionType(DocType), + doctype_handler_user_data: ?*anyopaque, + comment_handler: ?lol_html_comment_handler_t, + comment_handler_user_data: ?*anyopaque, + text_handler: ?lol_html_text_handler_handler_t, + text_handler_user_data: ?*anyopaque, + doc_end_handler: ?lol_html_doc_end_handler_t, + doc_end_user_data: ?*anyopaque, + ) c_int; + + pub const init = lol_html_rewriter_builder_new; + + /// Adds document-level content handlers to the builder. + /// + /// If a particular handler is not required then NULL can be passed + /// instead. Don't use stub handlers in this case as this affects + /// performance - rewriter skips parsing of the content that doesn't + /// need to be processed. + /// + /// Each handler can optionally have associated user data which will be + /// passed to the handler on each invocation along with the rewritable + /// unit argument. + /// + /// If any of handlers return LOL_HTML_STOP directive then rewriting + /// stops immediately and `write()` or `end()` of the rewriter methods + /// return an error code. + /// + /// WARNING: Pointers passed to handlers are valid only during the + /// handler execution. So they should never be leaked outside of handlers. + pub fn addDocumentContentHandlers( + builder: *HTMLRewriter.Builder, + comptime DocTypeHandler: type, + comptime doctype_handler: ?DirectiveFunctionTypeForHandler(DocType, DocTypeHandler), + doctype_handler_data: ?*DocTypeHandler, + comptime CommentHandler: type, + comptime comment_handler: ?DirectiveFunctionTypeForHandler(Comment, CommentHandler), + comment_handler_data: ?*CommentHandler, + comptime TextChunkHandler: type, + comptime text_chunk_handler: ?DirectiveFunctionTypeForHandler(TextChunk, TextChunkHandler), + text_chunk_handler_data: ?*TextChunkHandler, + comptime DocEndHandler: type, + comptime end_tag_handler: ?DirectiveFunctionTypeForHandler(DocEnd, DocEndHandler), + end_tag_handler_data: ?*DocEndHandler, + ) Error!void { + return switch (builder.lol_html_rewriter_builder_add_document_content_handlers( + if (doctype_handler_data != null) + DirectiveHandler(DocType, DocTypeHandler, doctype_handler.?) + else + null, + doctype_handler_data, + if (comment_handler_data != null) + DirectiveHandler(Comment, CommentHandler, comment_handler.?) + else + null, + comment_handler_data, + if (text_chunk_handler_data != null) + DirectiveHandler(TextChunk, TextChunkHandler, text_chunk_handler.?) + else + null, + text_chunk_handler_data, + if (end_tag_handler_data != null) + DirectiveHandler(DocEnd, DocEndHandler, end_tag_handler.?) + else + null, + end_tag_handler_data, + )) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + /// Adds element content handlers to the builder for the + /// given CSS selector. + /// + /// Selector should be a valid UTF8-string. + /// + /// If a particular handler is not required then NULL can be passed + /// instead. Don't use stub handlers in this case as this affects + /// performance - rewriter skips parsing of the content that doesn't + /// need to be processed. + /// + /// Each handler can optionally have associated user data which will be + /// passed to the handler on each invocation along with the rewritable + /// unit argument. + /// + /// If any of handlers return LOL_HTML_STOP directive then rewriting + /// stops immediately and `write()` or `end()` of the rewriter methods + /// return an error code. + /// + /// Returns 0 in case of success and -1 otherwise. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + /// + /// WARNING: Pointers passed to handlers are valid only during the + /// handler execution. So they should never be leaked outside of handlers. + pub fn addElementContentHandlers( + builder: *HTMLRewriter.Builder, + selector: *HTMLSelector, + comptime ElementHandler: type, + comptime element_handler: ?DirectiveFunctionTypeForHandler(Element, ElementHandler), + element_handler_data: ?*ElementHandler, + comptime CommentHandler: type, + comptime comment_handler: ?DirectiveFunctionTypeForHandler(Comment, CommentHandler), + comment_handler_data: ?*CommentHandler, + comptime TextChunkHandler: type, + comptime text_chunk_handler: ?DirectiveFunctionTypeForHandler(TextChunk, TextChunkHandler), + text_chunk_handler_data: ?*TextChunkHandler, + ) Error!void { + return switch (builder.lol_html_rewriter_builder_add_element_content_handlers( + selector, + if (element_handler_data != null) + DirectiveHandler(Element, ElementHandler, element_handler.?) + else + null, + element_handler_data, + if (comment_handler_data != null) + DirectiveHandler(Comment, CommentHandler, comment_handler.?) + else + null, + comment_handler_data, + if (text_chunk_handler_data != null) + DirectiveHandler(TextChunk, TextChunkHandler, text_chunk_handler.?) + else + null, + text_chunk_handler_data, + )) { + -1 => error.Fail, + 0 => void{}, + else => unreachable, + }; + } + + pub fn build( + builder: *HTMLRewriter.Builder, + encoding: Encoding, + memory_settings: MemorySettings, + strict: bool, + comptime OutputSink: type, + output_sink: *OutputSink, + comptime Writer: (fn (*OutputSink, bytes: []const u8) void), + comptime Done: (fn (*OutputSink) void), + ) Error!*HTMLRewriter { + const encoding_ = Encoding.label.getAssertContains(encoding); + return builder.lol_html_rewriter_build( + encoding_.ptr, + encoding_.len, + memory_settings, + OutputSinkFunction(OutputSink, Writer, Done), + output_sink, + strict, + ) orelse return error.Fail; + } + + fn OutputSinkFunction( + comptime OutputSinkType: type, + comptime Writer: (fn (*OutputSinkType, bytes: []const u8) void), + comptime Done: (fn (*OutputSinkType) void), + ) (fn ([*]const u8, usize, *anyopaque) callconv(.C) void) { + return struct { + fn writeChunk(ptr: [*]const u8, len: usize, user_data: *anyopaque) callconv(.C) void { + @setRuntimeSafety(false); + var this = @ptrCast(*OutputSinkType, @alignCast(@alignOf(*OutputSinkType), user_data)); + switch (len) { + 0 => Done(this), + else => Writer(this, ptr[0..len]), + } + } + }.writeChunk; + } + }; +}; + +pub const HTMLSelector = opaque { + extern fn lol_html_selector_parse(selector: [*]const u8, selector_len: usize) ?*HTMLSelector; + extern fn lol_html_selector_free(selector: *HTMLSelector) void; + + /// Frees the memory held by the parsed selector object. + pub fn deinit(selector: *HTMLSelector) void { + selector.lol_html_selector_free(); + } + + /// Parses given CSS selector string. + /// + /// Returns NULL if parsing error occurs. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + /// + /// WARNING: Selector SHOULD NOT be deallocated if there are any active rewriter + /// builders that accepted it as an argument to `lol_html_rewriter_builder_add_element_content_handlers()` + /// method. Deallocate all dependant rewriter builders first and then + /// use `lol_html_selector_free` function to free the selector. + pub fn parse(selector: []const u8) Error!*HTMLSelector { + if (lol_html_selector_parse(selector.ptr, selector.len)) |ptr| + return ptr + else + return error.Fail; + } +}; +pub const TextChunk = opaque { + extern fn lol_html_text_chunk_content_get(chunk: *const TextChunk) TextChunk.Content; + extern fn lol_html_text_chunk_is_last_in_text_node(chunk: *const TextChunk) bool; + extern fn lol_html_text_chunk_before(chunk: *TextChunk, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_text_chunk_after(chunk: *TextChunk, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_text_chunk_replace(chunk: *TextChunk, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_text_chunk_remove(chunk: *TextChunk) void; + extern fn lol_html_text_chunk_is_removed(chunk: *const TextChunk) bool; + extern fn lol_html_text_chunk_user_data_set(chunk: *const TextChunk, user_data: ?*anyopaque) void; + extern fn lol_html_text_chunk_user_data_get(chunk: *const TextChunk) ?*anyopaque; + + pub const Content = extern struct { + ptr: [*]const u8, + len: usize, + + pub fn slice(this: Content) []const u8 { + return this.ptr[0..this.len]; + } + }; + + pub fn getContent(this: *const TextChunk) TextChunk.Content { + return this.lol_html_text_chunk_content_get(); + } + pub fn isLastInTextNode(this: *const TextChunk) bool { + return this.lol_html_text_chunk_is_last_in_text_node(); + } + /// Inserts the content string before the text chunk either as raw text or as HTML. + /// + /// Content should be a valid UTF8-string. + /// + /// Returns 0 in case of success and -1 otherwise. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + pub fn before(this: *TextChunk, content: []const u8, is_html: bool) Error!void { + if (this.lol_html_text_chunk_before(content.ptr, content.len, is_html) < 0) + return error.Fail; + } + /// Inserts the content string after the text chunk either as raw text or as HTML. + /// + /// Content should be a valid UTF8-string. + /// + /// Returns 0 in case of success and -1 otherwise. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + pub fn after(this: *TextChunk, content: []const u8, is_html: bool) Error!void { + if (this.lol_html_text_chunk_after(content.ptr, content.len, is_html) < 0) + return error.Fail; + } + // Replace the text chunk with the content of the string which is interpreted + // either as raw text or as HTML. + // + // Content should be a valid UTF8-string. + // + // Returns 0 in case of success and -1 otherwise. The actual error message + // can be obtained using `lol_html_take_last_error` function. + pub fn replace(this: *TextChunk, content: []const u8, is_html: bool) Error!void { + if (this.lol_html_text_chunk_replace(content.ptr, content.len, is_html) < 0) + return error.Fail; + } + /// Removes the text chunk. + pub fn remove(this: *TextChunk) void { + return this.lol_html_text_chunk_remove(); + } + pub fn isRemoved(this: *const TextChunk) bool { + return this.lol_html_text_chunk_is_removed(); + } + pub fn setUserData(this: *const TextChunk, comptime Type: type, value: ?*Type) void { + return this.lol_html_text_chunk_user_data_set(value); + } + pub fn getUserData(this: *const TextChunk, comptime Type: type) ?*Type { + return @ptrCast(?*Type, @alignCast(@alignOf(?*Type), this.lol_html_text_chunk_user_data_get())); + } +}; +pub const Element = opaque { + extern fn lol_html_element_get_attribute(element: *const Element, name: [*]const u8, name_len: usize) HTMLString; + extern fn lol_html_element_has_attribute(element: *const Element, name: [*]const u8, name_len: usize) c_int; + extern fn lol_html_element_set_attribute(element: *Element, name: [*]const u8, name_len: usize, value: [*]const u8, value_len: usize) c_int; + extern fn lol_html_element_remove_attribute(element: *Element, name: [*]const u8, name_len: usize) c_int; + extern fn lol_html_element_before(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_prepend(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_append(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_after(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_set_inner_content(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_replace(element: *Element, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_element_remove(element: *const Element) void; + extern fn lol_html_element_remove_and_keep_content(element: *const Element) void; + extern fn lol_html_element_is_removed(element: *const Element) bool; + extern fn lol_html_element_user_data_set(element: *const Element, user_data: ?*anyopaque) void; + extern fn lol_html_element_user_data_get(element: *const Element) ?*anyopaque; + extern fn lol_html_element_on_end_tag(element: *Element, end_tag_handler: lol_html_end_tag_handler_t, user_data: ?*anyopaque) c_int; + + pub fn getAttribute(element: *const Element, name: []const u8) HTMLString { + return lol_html_element_get_attribute(element, name.ptr, name.len); + } + pub fn hasAttribute(element: *const Element, name: []const u8) Error!bool { + return switch (lol_html_element_has_attribute(element, name.ptr, name.len)) { + 0 => false, + 1 => true, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn setAttribute(element: *Element, name: []const u8, value: []const u8) Error!void { + return switch (lol_html_element_set_attribute(element, name.ptr, name.len, value.ptr, value.len)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn removeAttribute(element: *Element, name: []const u8) Error!void { + return switch (lol_html_element_remove_attribute(element, name.ptr, name.len)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn before(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_before(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn prepend(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_prepend(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn append(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_append(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn after(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_after(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn setInnerContent(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_set_inner_content(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + /// Replaces the element with the provided text or HTML content. + /// + /// Content should be a valid UTF8-string. + /// + /// Returns 0 in case of success and -1 otherwise. The actual error message + /// can be obtained using `lol_html_take_last_error` function. + pub fn replace(element: *Element, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_element_replace(element, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn remove(element: *const Element) void { + lol_html_element_remove(element); + } + // Removes the element, but leaves its inner content intact. + pub fn removeAndKeepContent(element: *const Element) void { + lol_html_element_remove_and_keep_content(element); + } + pub fn isRemoved(element: *const Element) bool { + return lol_html_element_is_removed(element); + } + pub fn setUserData(element: *const Element, user_data: ?*anyopaque) void { + lol_html_element_user_data_set(element, user_data); + } + pub fn getUserData(element: *const Element, comptime Type: type) ?*Type { + return @ptrCast(?*Element, @alignCast(@alignOf(?*Element), lol_html_element_user_data_get(element))); + } + pub fn onEndTag(element: *Element, end_tag_handler: lol_html_end_tag_handler_t, user_data: ?*anyopaque) Error!void { + return switch (lol_html_element_on_end_tag(element, end_tag_handler, user_data)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + extern fn lol_html_element_tag_name_get(element: *const Element) HTMLString; + extern fn lol_html_element_tag_name_set(element: *Element, name: [*]const u8, name_len: usize) c_int; + extern fn lol_html_element_namespace_uri_get(element: *const Element) [*:0]const u8; + extern fn lol_html_attributes_iterator_get(element: *const Element) ?*Attribute.Iterator; + + pub fn tagName(element: *const Element) HTMLString { + return lol_html_element_tag_name_get(element); + } + + pub fn setTagName(element: *Element, name: []const u8) Error!void { + return switch (lol_html_element_tag_name_set(element, name.ptr, name.len)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub fn namespaceURI(element: *const Element) [*:0]const u8 { + return lol_html_element_namespace_uri_get(element); + } + + pub fn attributes(element: *const Element) ?*Attribute.Iterator { + return lol_html_attributes_iterator_get(element); + } +}; + +pub const HTMLString = extern struct { + ptr: [*]const u8, + len: usize, + + extern fn lol_html_str_free(str: HTMLString) void; + pub fn deinit(this: HTMLString) void { + // if (this.len > 0) { + lol_html_str_free(this); + // } + } + + pub extern fn lol_html_take_last_error(...) HTMLString; + + pub fn lastError() HTMLString { + return lol_html_take_last_error(); + } + + pub fn slice(this: HTMLString) []const u8 { + @setRuntimeSafety(false); + return this.ptr[0..this.len]; + } +}; + +pub const EndTag = opaque { + extern fn lol_html_end_tag_before(end_tag: *EndTag, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_end_tag_after(end_tag: *EndTag, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_end_tag_remove(end_tag: *EndTag) void; + extern fn lol_html_end_tag_name_get(end_tag: *const EndTag) HTMLString; + extern fn lol_html_end_tag_name_set(end_tag: *EndTag, name: [*]const u8, name_len: usize) c_int; + + pub fn before(end_tag: *EndTag, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_end_tag_before(end_tag, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub fn after(end_tag: *EndTag, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_end_tag_after(end_tag, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + pub fn remove(end_tag: *EndTag) void { + lol_html_end_tag_remove(end_tag); + } + + pub fn getName(end_tag: *const EndTag) HTMLString { + return lol_html_end_tag_name_get(end_tag); + } + + pub fn setName(end_tag: *EndTag, name: []const u8) Error!void { + return switch (lol_html_end_tag_name_set(end_tag, name.ptr, name.len)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } +}; + +pub const Attribute = opaque { + extern fn lol_html_attribute_name_get(attribute: *const Attribute) HTMLString; + extern fn lol_html_attribute_value_get(attribute: *const Attribute) HTMLString; + pub fn name(this: *const Attribute) HTMLString { + return this.lol_html_attribute_name_get(); + } + pub fn value(this: *const Attribute) HTMLString { + return this.lol_html_attribute_value_get(); + } + + pub const Iterator = opaque { + extern fn lol_html_attributes_iterator_free(iterator: *Attribute.Iterator) void; + extern fn lol_html_attributes_iterator_next(iterator: *Attribute.Iterator) ?*const Attribute; + + pub fn next(this: *Iterator) ?*const Attribute { + return lol_html_attributes_iterator_next(this); + } + + pub fn deinit(this: *Iterator) void { + lol_html_attributes_iterator_free(this); + } + }; +}; + +pub const Comment = opaque { + extern fn lol_html_comment_text_get(comment: *const Comment) HTMLString; + extern fn lol_html_comment_text_set(comment: *Comment, text: [*]const u8, text_len: usize) c_int; + extern fn lol_html_comment_before(comment: *Comment, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_comment_after(comment: *Comment, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_comment_replace(comment: *Comment, content: [*]const u8, content_len: usize, is_html: bool) c_int; + extern fn lol_html_comment_remove(comment: *Comment) void; + extern fn lol_html_comment_is_removed(comment: *const Comment) bool; + extern fn lol_html_comment_user_data_set(comment: *const Comment, user_data: ?*anyopaque) void; + extern fn lol_html_comment_user_data_get(comment: *const Comment) ?*anyopaque; + + pub fn getText(comment: *const Comment) HTMLString { + return lol_html_comment_text_get(comment); + } + + pub fn setText(comment: *Comment, text: []const u8) Error!void { + return switch (lol_html_comment_text_set(comment, text.ptr, text.len)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub fn before(comment: *Comment, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_comment_before(comment, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub fn replace(comment: *Comment, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_comment_before(comment, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub fn after(comment: *Comment, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_comment_after(comment, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } + + pub const isRemoved = lol_html_comment_is_removed; + pub const remove = lol_html_comment_remove; +}; + +pub const Directive = enum(c_uint) { + @"stop" = 0, + @"continue" = 1, +}; +pub const lol_html_comment_handler_t = fn (*Comment, ?*anyopaque) callconv(.C) Directive; +pub const lol_html_text_handler_handler_t = fn (*TextChunk, ?*anyopaque) callconv(.C) Directive; +pub const lol_html_element_handler_t = fn (*Element, ?*anyopaque) callconv(.C) Directive; +pub const lol_html_doc_end_handler_t = fn (*DocEnd, ?*anyopaque) callconv(.C) Directive; +pub const lol_html_end_tag_handler_t = fn (*EndTag, ?*anyopaque) callconv(.C) Directive; +pub const DocEnd = opaque { + extern fn lol_html_doc_end_append(doc_end: ?*DocEnd, content: [*]const u8, content_len: usize, is_html: bool) c_int; + + pub fn append(this: *DocEnd, content: []const u8, is_html: bool) Error!void { + return switch (lol_html_doc_end_append(this, content.ptr, content.len, is_html)) { + 0 => void{}, + -1 => error.Fail, + else => unreachable, + }; + } +}; + +fn DirectiveFunctionType(comptime Container: type) type { + return fn (*Container, ?*anyopaque) callconv(.C) Directive; +} + +fn DirectiveFunctionTypeForHandler(comptime Container: type, comptime UserDataType: type) type { + return fn (*UserDataType, *Container) bool; +} + +fn DocTypeHandlerCallback(comptime UserDataType: type) type { + return fn (*DocType, *UserDataType) bool; +} + +pub fn DirectiveHandler(comptime Container: type, comptime UserDataType: type, comptime Callback: (fn (this: *UserDataType, container: *Container) bool)) DirectiveFunctionType(Container) { + return struct { + pub fn callback(this: *Container, user_data: ?*anyopaque) callconv(.C) Directive { + return @intToEnum( + Directive, + @as( + c_uint, + @boolToInt( + Callback( + @ptrCast( + *UserDataType, + @alignCast( + @alignOf(*UserDataType), + user_data.?, + ), + ), + this, + ), + ), + ), + ); + } + }.callback; +} + +pub const DocType = opaque { + extern fn lol_html_doctype_name_get(doctype: *const DocType) HTMLString; + extern fn lol_html_doctype_public_id_get(doctype: *const DocType) HTMLString; + extern fn lol_html_doctype_system_id_get(doctype: *const DocType) HTMLString; + extern fn lol_html_doctype_user_data_set(doctype: *const DocType, user_data: ?*anyopaque) void; + extern fn lol_html_doctype_user_data_get(doctype: *const DocType) ?*anyopaque; + + pub const Callback = fn (*DocType, ?*anyopaque) callconv(.C) Directive; + + pub fn getName(this: *const DocType) HTMLString { + return this.lol_html_doctype_name_get(); + } + pub fn getPublicId(this: *const DocType) HTMLString { + return this.lol_html_doctype_public_id_get(); + } + pub fn getSystemId(this: *const DocType) HTMLString { + return this.lol_html_doctype_system_id_get(); + } +}; + +pub const Encoding = enum { + UTF8, + UTF16, + + const Label = std.enums.EnumMap(Encoding, []const u8); + pub const label: Label = brk: { + var labels = Label{}; + labels.put(.UTF8, "UTF-8"); + labels.put(.UTF16, "UTF-16"); + + break :brk labels; + }; +}; diff --git a/src/javascript/jsc/api/html_rewriter.zig b/src/javascript/jsc/api/html_rewriter.zig index 3a2bc1cc3..3a811b2c1 100644 --- a/src/javascript/jsc/api/html_rewriter.zig +++ b/src/javascript/jsc/api/html_rewriter.zig @@ -29,10 +29,37 @@ const Request = WebCore.Request; const d = Base.d; const FetchEvent = WebCore.FetchEvent; const Response = WebCore.Response; +const LOLHTML = @import("lolhtml"); +const SelectorMap = std.ArrayListUnmanaged(*LOLHTML.HTMLSelector); +const LOLHTMLContext = struct { + selectors: SelectorMap = .{}, + element_handlers: std.ArrayListUnmanaged(*ElementHandler) = .{}, + document_handlers: std.ArrayListUnmanaged(*DocumentHandler) = .{}, + + pub fn deinit(this: *LOLHTMLContext, allocator: std.mem.Allocator) void { + for (this.selectors.items) |selector| { + selector.deinit(); + } + this.selectors.deinit(allocator); + this.selectors = .{}; + + for (this.element_handlers.items) |handler| { + handler.deinit(); + } + this.element_handlers.deinit(allocator); + this.element_handlers = .{}; + + for (this.document_handlers.items) |handler| { + handler.deinit(); + } + this.document_handlers.deinit(allocator); + this.document_handlers = .{}; + } +}; pub const HTMLRewriter = struct { - listeners: *anyopaque, - built: bool = false, + builder: *LOLHTML.HTMLRewriter.Builder, + context: LOLHTMLContext, pub const Class = NewClass( HTMLRewriter, @@ -59,42 +86,480 @@ pub const HTMLRewriter = struct { _: js.ExceptionRef, ) js.JSObjectRef { var rewriter = bun.default_allocator.create(HTMLRewriter) catch unreachable; - rewriter.* = HTMLRewriter{ .listeners = undefined }; + rewriter.* = HTMLRewriter{ + .builder = LOLHTML.HTMLRewriter.Builder.init(), + .context = .{}, + }; return HTMLRewriter.Class.make(ctx, rewriter); } pub fn on( this: *HTMLRewriter, global: *JSGlobalObject, - event: ZigString, + selector_name: ZigString, + thisObject: JSC.C.JSObjectRef, listener: JSValue, + exception: JSC.C.ExceptionRef, ) JSValue { - _ = this; - _ = global; - _ = event; - _ = listener; - return undefined; + var selector_slice = std.fmt.allocPrint(bun.default_allocator, "{}", .{selector_name}) catch unreachable; + + var selector = LOLHTML.HTMLSelector.parse(selector_slice) catch + return throwLOLHTMLError(global); + var handler_ = ElementHandler.init(global, listener, exception); + if (exception.* != null) { + selector.deinit(); + return JSValue.fromRef(exception.*); + } + var handler = getAllocator(global.ref()).create(ElementHandler) catch unreachable; + handler.* = handler_; + + this.builder.addElementContentHandlers( + selector, + + ElementHandler, + ElementHandler.onElement, + if (handler.onElementCallback != null) + handler + else + null, + + ElementHandler, + ElementHandler.onComment, + if (handler.onCommentCallback != null) + handler + else + null, + + ElementHandler, + ElementHandler.onText, + if (handler.onTextCallback != null) + handler + else + null, + ) catch { + selector.deinit(); + return throwLOLHTMLError(global); + }; + + this.context.selectors.append(bun.default_allocator, selector) catch unreachable; + this.context.element_handlers.append(bun.default_allocator, handler) catch unreachable; + return JSValue.fromRef(thisObject); } pub fn onDocument( this: *HTMLRewriter, global: *JSGlobalObject, - event: ZigString, listener: JSValue, + thisObject: JSC.C.JSObjectRef, + exception: JSC.C.ExceptionRef, ) JSValue { - _ = this; - _ = global; - _ = event; - _ = listener; - return undefined; + var handler_ = DocumentHandler.init(global, listener, exception); + if (exception.* != null) { + return JSValue.fromRef(exception.*); + } + + var handler = getAllocator(global.ref()).create(DocumentHandler) catch unreachable; + handler.* = handler_; + + this.builder.addDocumentContentHandlers( + DocumentHandler, + DocumentHandler.onDocType, + if (handler.onDocTypeCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onComment, + if (handler.onCommentCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onText, + if (handler.onTextCallback != null) + handler + else + null, + + DocumentHandler, + DocumentHandler.onEnd, + if (handler.onEndCallback != null) + handler + else + null, + ) catch { + return throwLOLHTMLError(global); + }; + + this.context.document_handlers.append(bun.default_allocator, handler) catch unreachable; + return JSValue.fromRef(thisObject); + } + + pub fn finalize(this: *HTMLRewriter) JSValue { + this.finalizeWithoutDestroy(); + bun.default_allocator.destroy(this); + } + + pub fn finalizeWithoutDestroy(this: *HTMLRewriter) void { + this.context.deinit(bun.default_allocator); } pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue { - _ = this; - _ = global; - _ = response; - return undefined; + var input = response.body.slice(); + var result = bun.default_allocator.create(Response) catch unreachable; + + if (input.len == 0) { + response.cloneInto(result, getAllocator(global.ref())); + this.finalizeWithoutDestroy(); + return JSValue.fromRef(Response.Class.make(global.ref(), result)); + } + + var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable; + sink.* = BufferOutputSink{ + .bytes = bun.MutableString.initEmpty(bun.default_allocator), + .global = global, + .context = this.context, + .rewriter = undefined, + }; + this.context = .{}; + + sink.rewriter = this.builder.build( + .UTF8, + .{ + .preallocated_parsing_buffer_size = input.len, + .max_allowed_memory_usage = std.math.maxInt(u32), + }, + false, + BufferOutputSink, + sink, + BufferOutputSink.write, + BufferOutputSink.done, + ) catch { + this.finalizeWithoutDestroy(); + sink.deinit(); + bun.default_allocator.destroy(sink); + bun.default_allocator.destroy(result); + + return throwLOLHTMLError(global); + }; + + sink.rewriter.write(input) catch { + sink.deinit(); + bun.default_allocator.destroy(sink); + + return throwLOLHTMLError(global); + }; + + sink.rewriter.end() catch { + sink.deinit(); + bun.default_allocator.destroy(sink); + + return throwLOLHTMLError(global); + }; + sink.rewriter.deinit(); + + result.body = JSC.WebCore.Body.@"200"(global.ref()); + result.body.init = response.body.init.clone(bun.default_allocator); + result.body.value = .{ + .String = sink.bytes.toOwnedSlice(), + }; + + if (result.body.init.headers) |*headers| { + headers.putHeaderNumber("content-length", @truncate(u32, result.body.value.String.len), false); + } + + response.body.deinit(response.allocator); + response.body = JSC.WebCore.Body.@"200"(global.ref()); + + return JSValue.fromRef(Response.Class.make(global.ref(), result)); + } + + pub const BufferOutputSink = struct { + global: *JSGlobalObject, + bytes: bun.MutableString, + rewriter: *LOLHTML.HTMLRewriter, + context: LOLHTMLContext, + + pub fn done(this: *BufferOutputSink) void { + _ = this; + } + + pub fn write(this: *BufferOutputSink, bytes: []const u8) void { + this.bytes.append(bytes) catch unreachable; + } + + pub fn deinit(this: *BufferOutputSink) void { + this.bytes.deinit(); + + this.context.deinit(bun.default_allocator); + } + }; + + pub const Processor = struct { + selectors: std.ArrayList(*LOLHTML.HTMLSelector), + }; +}; + +const DocumentHandler = struct { + onDocTypeCallback: ?JSValue = null, + onCommentCallback: ?JSValue = null, + onTextCallback: ?JSValue = null, + onEndCallback: ?JSValue = null, + thisObject: JSValue, + global: *JSGlobalObject, + + pub const onDocType = HandlerCallback( + DocumentHandler, + DocType, + LOLHTML.DocType, + "doctype", + "onDocTypeCallback", + ); + pub const onComment = HandlerCallback( + DocumentHandler, + Comment, + LOLHTML.Comment, + "comment", + "onCommentCallback", + ); + pub const onText = HandlerCallback( + DocumentHandler, + TextChunk, + LOLHTML.TextChunk, + "text_chunk", + "onTextCallback", + ); + pub const onEnd = HandlerCallback( + DocumentHandler, + DocEnd, + LOLHTML.DocEnd, + "doc_end", + "onEndCallback", + ); + + pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) DocumentHandler { + var handler = DocumentHandler{ + .thisObject = thisObject, + .global = global, + }; + + switch (thisObject.jsType()) { + .Object, .ProxyObject, .Cell, .FinalObject => {}, + else => |kind| { + JSC.throwInvalidArguments( + "Expected object but received {s}", + .{std.mem.span(@tagName(kind))}, + global.ref(), + exception, + ); + return undefined; + }, + } + + if (thisObject.get(global, "doctype")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("doctype must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onDocTypeCallback = val; + } + + if (thisObject.get(global, "comments")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("comments must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onCommentCallback = val; + } + + if (thisObject.get(global, "text")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("text must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onTextCallback = val; + } + + if (thisObject.get(global, "end")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("end must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onEndCallback = val; + } + + JSC.C.JSValueProtect(global.ref(), thisObject.asObjectRef()); + return handler; + } + + pub fn deinit(this: *DocumentHandler) void { + if (this.onDocTypeCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onDocTypeCallback = null; + } + + if (this.onCommentCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onCommentCallback = null; + } + + if (this.onTextCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onTextCallback = null; + } + + if (this.onEndCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onEndCallback = null; + } + + JSC.C.JSValueUnprotect(this.global.ref(), this.thisObject.asObjectRef()); + } +}; + +fn HandlerCallback( + comptime HandlerType: type, + comptime ZigType: type, + comptime LOLHTMLType: type, + comptime field_name: string, + comptime callback_name: string, +) (fn (*HandlerType, *LOLHTMLType) bool) { + return struct { + pub fn callback(this: *HandlerType, value: *LOLHTMLType) bool { + var zig_element = bun.default_allocator.create(ZigType) catch unreachable; + @field(zig_element, field_name) = value; + // At the end of this scope, the value is no longer valid + defer { + @field(zig_element, field_name) = null; + } + var args = [1]JSC.C.JSObjectRef{ + ZigType.Class.make(this.global.ref(), zig_element), + }; + var result = JSC.C.JSObjectCallAsFunctionReturnValue( + this.global.ref(), + @field(this, callback_name).?.asObjectRef(), + if (comptime @hasField(HandlerType, "thisObject")) + @field(this, "thisObject").asObjectRef() + else + null, + 1, + &args, + ); + + if (result.isError() or result.isAggregateError(this.global)) { + return true; + } + + return false; + } + }.callback; +} + +const ElementHandler = struct { + onElementCallback: ?JSValue = null, + onCommentCallback: ?JSValue = null, + onTextCallback: ?JSValue = null, + thisObject: JSValue, + global: *JSGlobalObject, + + pub fn init(global: *JSGlobalObject, thisObject: JSValue, exception: JSC.C.ExceptionRef) ElementHandler { + var handler = ElementHandler{ + .thisObject = thisObject, + .global = global, + }; + + switch (thisObject.jsType()) { + .Object, .ProxyObject, .Cell, .FinalObject => {}, + else => |kind| { + JSC.throwInvalidArguments( + "Expected object but received {s}", + .{std.mem.span(@tagName(kind))}, + global.ref(), + exception, + ); + return undefined; + }, + } + + if (thisObject.get(global, "element")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("element must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onElementCallback = val; + } + + if (thisObject.get(global, "comment")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("comment must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onCommentCallback = val; + } + + if (thisObject.get(global, "text")) |val| { + if (val.isUndefinedOrNull() or !val.isCell() or !val.isCallable(global.vm())) { + JSC.throwInvalidArguments("text must be a function", .{}, global.ref(), exception); + return undefined; + } + JSC.C.JSValueProtect(global.ref(), val.asObjectRef()); + handler.onTextCallback = val; + } + + JSC.C.JSValueProtect(global.ref(), thisObject.asObjectRef()); + return handler; + } + + pub fn deinit(this: *ElementHandler) void { + if (this.onElementCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onElementCallback = null; + } + + if (this.onCommentCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onCommentCallback = null; + } + + if (this.onTextCallback) |cb| { + JSC.C.JSValueUnprotect(this.global.ref(), cb.asObjectRef()); + this.onTextCallback = null; + } + + JSC.C.JSValueUnprotect(this.global.ref(), this.thisObject.asObjectRef()); } + + pub const onElement = HandlerCallback( + ElementHandler, + Element, + LOLHTML.Element, + "element", + "onElementCallback", + ); + + pub const onComment = HandlerCallback( + ElementHandler, + Comment, + LOLHTML.Comment, + "comment", + "onCommentCallback", + ); + + pub const onText = HandlerCallback( + ElementHandler, + TextChunk, + LOLHTML.TextChunk, + "text_chunk", + "onTextCallback", + ); }; const ContentOptions = struct { @@ -122,6 +587,22 @@ fn GetterType(comptime Container: type) type { ) js.JSObjectRef; } +fn SetterType(comptime Container: type) type { + return fn ( + this: *Container, + ctx: js.JSContextRef, + obj: js.JSObjectRef, + prop: js.JSStringRef, + value: js.JSValueRef, + exception: js.ExceptionRef, + ) bool; +} + +pub fn free_html_writer_string(_: ?*anyopaque, ptr: ?*anyopaque, len: usize) callconv(.C) void { + var str = LOLHTML.HTMLString{ .ptr = bun.cast([*]const u8, ptr.?), .len = len }; + str.deinit(); +} + pub fn wrap(comptime Container: type, comptime name: string) MethodType(Container) { return struct { const FunctionType = @TypeOf(@field(Container, name)); @@ -131,7 +612,7 @@ pub fn wrap(comptime Container: type, comptime name: string) MethodType(Containe this: *Container, ctx: js.JSContextRef, _: js.JSObjectRef, - _: js.JSObjectRef, + thisObject: js.JSObjectRef, arguments: []const js.JSValueRef, exception: js.ExceptionRef, ) js.JSObjectRef { @@ -140,9 +621,9 @@ pub fn wrap(comptime Container: type, comptime name: string) MethodType(Containe comptime var i: usize = 0; inline while (i < FunctionTypeInfo.args.len) : (i += 1) { - const ArgType = FunctionTypeInfo.args[i].arg_type.?; + const ArgType = comptime FunctionTypeInfo.args[i].arg_type.?; - switch (ArgType) { + switch (comptime ArgType) { *Container => { args[i] = this; }, @@ -150,17 +631,21 @@ pub fn wrap(comptime Container: type, comptime name: string) MethodType(Containe args[i] = ctx.ptr(); }, ZigString => { - var arg: ?JSC.JSValue = iter.next(); - args[i] = (arg orelse { + var string_value = iter.nextEat() orelse { + JSC.throwInvalidArguments("Missing argument", .{}, ctx, exception); + return null; + }; + + if (!string_value.isString()) { JSC.throwInvalidArguments("Expected string", .{}, ctx, exception); return null; - }).getZigString(ctx.ptr()); + } + + args[i] = string_value.getZigString(ctx.ptr()); }, ?ContentOptions => { - var arg: ?JSC.JSValue = iter.next(); - - if (arg) |content_arg| { - if (content_arg.get("html")) |html_val| { + if (iter.nextEat()) |content_arg| { + if (content_arg.get(ctx.ptr(), "html")) |html_val| { args[i] = .{ .html = html_val.toBoolean() }; } } else { @@ -168,9 +653,7 @@ pub fn wrap(comptime Container: type, comptime name: string) MethodType(Containe } }, *Response => { - var arg: ?JSC.JSValue = iter.next(); - - args[i] = (arg orelse { + args[i] = (iter.nextEat() orelse { JSC.throwInvalidArguments("Missing Response object", .{}, ctx, exception); return null; }).as(Response) orelse { @@ -178,15 +661,24 @@ pub fn wrap(comptime Container: type, comptime name: string) MethodType(Containe return null; }; }, + js.JSObjectRef => { + args[i] = thisObject; + if (!JSValue.fromRef(thisObject).isCell() or !JSValue.fromRef(thisObject).isObject()) { + JSC.throwInvalidArguments("Expected object", .{}, ctx, exception); + return null; + } + }, + js.ExceptionRef => { + args[i] = exception; + }, JSValue => { - var arg: ?JSC.JSValue = iter.next(); - - args[i] = arg orelse { + const val = iter.nextEat() orelse { JSC.throwInvalidArguments("Missing argument", .{}, ctx, exception); return null; }; + args[i] = val; }, - else => @compileError("Unexpected Type" ++ @typeName(ArgType)), + else => @compileError("Unexpected Type " ++ @typeName(ArgType)), } } @@ -224,9 +716,521 @@ pub fn getterWrap(comptime Container: type, comptime name: string) GetterType(Co }.callback; } +pub fn setterWrap(comptime Container: type, comptime name: string) SetterType(Container) { + return struct { + const FunctionType = @TypeOf(@field(Container, name)); + const FunctionTypeInfo: std.builtin.TypeInfo.Fn = @typeInfo(FunctionType).Fn; + + pub fn callback( + this: *Container, + ctx: js.JSContextRef, + _: js.JSObjectRef, + _: js.JSStringRef, + value: js.JSValueRef, + exception: js.ExceptionRef, + ) bool { + @call(.{}, @field(Container, name), .{ this, JSC.JSValue.fromRef(value), exception, ctx.ptr() }); + return exception.* == null; + } + }.callback; +} + +fn throwLOLHTMLError(global: *JSGlobalObject) JSValue { + var err = LOLHTML.HTMLString.lastError(); + return ZigString.init(err.slice()).toErrorInstance(global); +} + +fn htmlStringValue(input: LOLHTML.HTMLString, globalObject: *JSGlobalObject) JSValue { + var str = ZigString.init( + input.slice(), + ); + str.detectEncoding(); + + return str.toExternalValueWithCallback( + globalObject, + free_html_writer_string, + ); +} + +pub const TextChunk = struct { + text_chunk: ?*LOLHTML.TextChunk = null, + + pub const Class = NewClass( + TextChunk, + .{ .name = "TextChunk" }, + .{ + .before = .{ + .rfn = wrap(TextChunk, "before"), + }, + .after = .{ + .rfn = wrap(TextChunk, "after"), + }, + + .replace = .{ + .rfn = wrap(TextChunk, "replace"), + }, + + .remove = .{ + .rfn = wrap(TextChunk, "remove"), + }, + }, + .{ + .removed = .{ + .get = getterWrap(TextChunk, "removed"), + }, + .text = .{ + .get = getterWrap(TextChunk, "getText"), + }, + }, + ); + + fn contentHandler(this: *TextChunk, comptime Callback: (fn (*LOLHTML.TextChunk, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.text_chunk.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *TextChunk, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.TextChunk.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *TextChunk, thisObject: js.JSObjectRef) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + this.text_chunk.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getText(this: *TextChunk, global: *JSGlobalObject) JSValue { + if (this.text_chunk == null) + return JSC.JSValue.jsUndefined(); + return ZigString.init(this.text_chunk.?.getContent().slice()).withEncoding().toValue(global); + } + + pub fn removed(this: *TextChunk, _: *JSGlobalObject) JSValue { + return JSC.JSValue.jsBoolean(this.text_chunk.?.isRemoved()); + } +}; + +pub const DocType = struct { + doctype: ?*LOLHTML.DocType = null, + + pub const Class = NewClass( + DocType, + .{ + .name = "DocType", + }, + .{}, + .{ + .name = .{ + .get = getterWrap(DocType, "name"), + }, + .systemId = .{ + .get = getterWrap(DocType, "systemId"), + }, + + .publicId = .{ + .get = getterWrap(DocType, "publicId"), + }, + }, + ); + + /// The doctype name. + pub fn name(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + const str = this.doctype.?.getName().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } + + pub fn systemId(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + + const str = this.doctype.?.getSystemId().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } + + pub fn publicId(this: *DocType, global: *JSGlobalObject) JSValue { + if (this.doctype == null) + return JSC.JSValue.jsUndefined(); + + const str = this.doctype.?.getPublicId().slice(); + if (str.len == 0) + return JSValue.jsNull(); + return ZigString.init(str).toValue(global); + } +}; + +pub const DocEnd = struct { + doc_end: ?*LOLHTML.DocEnd, + + pub const Class = NewClass( + DocEnd, + .{ .name = "DocEnd" }, + .{ + .append = .{ + .rfn = wrap(DocEnd, "append"), + }, + }, + .{}, + ); + + fn contentHandler(this: *DocEnd, comptime Callback: (fn (*LOLHTML.DocEnd, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.doc_end == null) + return JSValue.jsNull(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.doc_end.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn append( + this: *DocEnd, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.DocEnd.append, thisObject, globalObject, content, contentOptions); + } +}; + +pub const Comment = struct { + comment: ?*LOLHTML.Comment = null, + + pub const Class = NewClass( + Comment, + .{ .name = "Comment" }, + .{ + .before = .{ + .rfn = wrap(Comment, "before"), + }, + .after = .{ + .rfn = wrap(Comment, "after"), + }, + + .replace = .{ + .rfn = wrap(Comment, "replace"), + }, + + .remove = .{ + .rfn = wrap(Comment, "remove"), + }, + }, + .{ + .removed = .{ + .get = getterWrap(Comment, "removed"), + }, + .text = .{ + .get = getterWrap(Comment, "getText"), + .set = setterWrap(Comment, "setText"), + }, + }, + ); + + fn contentHandler(this: *Comment, comptime Callback: (fn (*LOLHTML.Comment, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.comment.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *Comment, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.Comment.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *Comment, thisObject: js.JSObjectRef) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + this.comment.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getText(this: *Comment, global: *JSGlobalObject) JSValue { + if (this.comment == null) + return JSValue.jsNull(); + return ZigString.init(this.comment.?.getText().slice()).withEncoding().toValue(global); + } + + pub fn setText( + this: *Comment, + value: JSValue, + exception: JSC.C.ExceptionRef, + global: *JSGlobalObject, + ) void { + if (this.comment == null) + return; + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + this.comment.?.setText(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } + + pub fn removed(this: *Comment, _: *JSGlobalObject) JSValue { + if (this.comment == null) + return JSC.JSValue.jsUndefined(); + return JSC.JSValue.jsBoolean(this.comment.?.isRemoved()); + } +}; + +pub const EndTag = struct { + end_tag: ?*LOLHTML.EndTag, + + pub const Handler = struct { + callback: ?JSC.JSValue, + global: *JSGlobalObject, + + pub const onEndTag = HandlerCallback( + Handler, + EndTag, + LOLHTML.EndTag, + "end_tag", + "callback", + ); + + pub const onEndTagHandler = LOLHTML.DirectiveHandler(LOLHTML.EndTag, Handler, onEndTag); + }; + + pub const Class = NewClass( + EndTag, + .{ .name = "EndTag" }, + .{ + .before = .{ + .rfn = wrap(EndTag, "before"), + }, + .after = .{ + .rfn = wrap(EndTag, "after"), + }, + + .remove = .{ + .rfn = wrap(EndTag, "remove"), + }, + }, + .{ + .name = .{ + .get = getterWrap(EndTag, "getName"), + .set = setterWrap(EndTag, "setName"), + }, + }, + ); + + fn contentHandler(this: *EndTag, comptime Callback: (fn (*LOLHTML.EndTag, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.end_tag == null) + return JSValue.jsNull(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.end_tag.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); + } + + pub fn before( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.before, thisObject, globalObject, content, contentOptions); + } + + pub fn after( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.after, thisObject, globalObject, content, contentOptions); + } + + pub fn replace( + this: *EndTag, + thisObject: js.JSObjectRef, + globalObject: *JSGlobalObject, + content: ZigString, + contentOptions: ?ContentOptions, + ) JSValue { + return this.contentHandler(LOLHTML.EndTag.replace, thisObject, globalObject, content, contentOptions); + } + + pub fn remove(this: *EndTag, thisObject: js.JSObjectRef) JSValue { + if (this.end_tag == null) + return JSC.JSValue.jsUndefined(); + + this.end_tag.?.remove(); + return JSValue.fromRef(thisObject); + } + + pub fn getName(this: *EndTag, global: *JSGlobalObject) JSValue { + if (this.end_tag == null) + return JSC.JSValue.jsUndefined(); + + return ZigString.init(this.end_tag.?.getName().slice()).withEncoding().toValue(global); + } + + pub fn setName( + this: *EndTag, + value: JSValue, + exception: JSC.C.ExceptionRef, + global: *JSGlobalObject, + ) void { + if (this.end_tag == null) + return; + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + this.end_tag.?.setName(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } +}; + +pub const AttributeIterator = struct { + iterator: ?*LOLHTML.Attribute.Iterator = null, + + pub const Class = NewClass( + AttributeIterator, + .{ .name = "AttributeIterator" }, + .{ + .next = .{ + .rfn = wrap(AttributeIterator, "next"), + }, + }, + .{}, + ); + + const value_ = ZigString.init("value"); + const done_ = ZigString.init("done"); + pub fn next( + this: *AttributeIterator, + globalObject: *JSGlobalObject, + ) JSValue { + if (this.iterator == null) { + return JSC.JSValue.createObject2( + globalObject, + &value_, + &done_, + JSC.JSValue.jsUndefined(), + JSC.JSValue.jsBoolean(true), + ); + } + + var attribute = this.iterator.?.next() orelse { + this.iterator.?.deinit(); + this.iterator = null; + return JSC.JSValue.createObject2( + globalObject, + &value_, + &done_, + JSC.JSValue.jsUndefined(), + JSC.JSValue.jsBoolean(true), + ); + }; + + var strs = [2]ZigString{ + htmlStringValue(attribute.name().slice(), globalObject), + htmlStringValue(attribute.value().slice(), globalObject), + }; + + return JSC.JSValue.createObject2( + globalObject, + &value_, + &done_, + JSC.JSValue.createStringArray(globalObject, &strs, 2, false), + JSC.JSValue.jsBoolean(false), + ); + } +}; pub const Element = struct { - global: *JSGlobalObject, - removed: bool = false, + element: ?*LOLHTML.Element = null, pub const Class = NewClass( Element, @@ -268,143 +1272,245 @@ pub const Element = struct { .removeAndKeepContent = .{ .rfn = wrap(Element, "removeAndKeepContent"), }, + .onEndTag = .{ + .rfn = wrap(Element, "onEndTag"), + }, }, .{ .tagName = .{ .get = getterWrap(Element, "getTagName"), + .set = setterWrap(Element, "setTagName"), }, .removed = .{ .get = getterWrap(Element, "getRemoved"), }, - .namespaceUri = .{ + .namespaceURI = .{ .get = getterWrap(Element, "getNamespaceURI"), }, }, ); + pub fn onEndTag( + this: *Element, + globalObject: *JSGlobalObject, + function: JSValue, + thisObject: JSC.C.JSObjectRef, + ) JSValue { + if (this.element == null) + return JSValue.jsNull(); + if (function.isUndefinedOrNull() or !function.isCallable(globalObject.vm())) { + return ZigString.init("Expected a function").withEncoding().toValue(globalObject); + } + + var end_tag_handler = bun.default_allocator.create(EndTag.Handler) catch unreachable; + end_tag_handler.* = .{ .global = globalObject, .callback = function }; + + this.element.?.onEndTag(EndTag.Handler.onEndTagHandler, end_tag_handler) catch { + bun.default_allocator.destroy(end_tag_handler); + return throwLOLHTMLError(globalObject); + }; + + JSC.C.JSValueProtect(globalObject.ref(), function.asObjectRef()); + return JSValue.fromRef(thisObject); + } + // // fn wrap(comptime name: string) /// Returns the value for a given attribute name: ZigString on the element, or null if it is not found. pub fn getAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString) JSValue { - _ = this; - _ = name; - _ = globalObject; - return undefined; + if (this.element == null) + return JSValue.jsNull(); + + var slice = name.toSlice(bun.default_allocator); + defer slice.deinit(); + var attr = this.element.?.getAttribute(slice.slice()).slice(); + + if (attr.len == 0) + return JSC.JSValue.jsNull(); + + var str = ZigString.init( + attr, + ); + + return str.toExternalValueWithCallback( + globalObject, + free_html_writer_string, + ); } - /// Returns a boolean indicating whether an attribute exists on the element. - pub fn hasAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString) JSValue { - _ = this; - _ = name; - _ = globalObject; - return undefined; + /// Returns a boolean indicating whether an attribute exists on the element. + pub fn hasAttribute(this: *Element, global: *JSGlobalObject, name: ZigString) JSValue { + if (this.element == null) + return JSValue.jsBoolean(false); + + var slice = name.toSlice(bun.default_allocator); + defer slice.deinit(); + return JSValue.jsBoolean(this.element.?.hasAttribute(slice.slice()) catch return throwLOLHTMLError(global)); } - /// Sets an attribute to a provided value, creating the attribute if it does not exist. - pub fn setAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString, value: ZigString) JSValue { - _ = this; - _ = name; - _ = globalObject; - _ = value; - return undefined; + /// Sets an attribute to a provided value, creating the attribute if it does not exist. + pub fn setAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name_: ZigString, value_: ZigString) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var name_slice = name_.toSlice(bun.default_allocator); + defer name_slice.deinit(); + + var value_slice = value_.toSlice(bun.default_allocator); + defer value_slice.deinit(); + this.element.?.setAttribute(name_slice.slice(), value_slice.slice()) catch return throwLOLHTMLError(globalObject); + return JSValue.fromRef(thisObject); } /// Removes the attribute. - pub fn removeAttribute(this: *Element, globalObject: *JSGlobalObject, name: ZigString) JSValue { - _ = this; - _ = name; - _ = globalObject; - return undefined; + pub fn removeAttribute(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, name: ZigString) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var name_slice = name.toSlice(bun.default_allocator); + defer name_slice.deinit(); + + this.element.?.removeAttribute( + name_slice.slice(), + ) catch return throwLOLHTMLError(globalObject); + return JSValue.fromRef(thisObject); + } + + fn contentHandler(this: *Element, comptime Callback: (fn (*LOLHTML.Element, []const u8, bool) LOLHTML.Error!void), thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + + var content_slice = content.toSlice(bun.default_allocator); + defer content_slice.deinit(); + + Callback( + this.element.?, + content_slice.slice(), + contentOptions != null and contentOptions.?.html, + ) catch return throwLOLHTMLError(globalObject); + + return JSValue.fromRef(thisObject); } /// Inserts content before the element. - pub fn before(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + pub fn before(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.before, + thisObject, + globalObject, + content, + contentOptions, + ); } /// Inserts content right after the element. - pub fn after(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + pub fn after(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.after, + thisObject, + globalObject, + content, + contentOptions, + ); } - /// nserts content right after the start tag of the element. - pub fn prepend(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + /// Inserts content right after the start tag of the element. + pub fn prepend(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.prepend, + thisObject, + globalObject, + content, + contentOptions, + ); } /// Inserts content right before the end tag of the element. - pub fn append(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + pub fn append(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.append, + thisObject, + globalObject, + content, + contentOptions, + ); } - /// Removes the element and inserts content in place of it. - pub fn replace(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + /// Removes the element and inserts content in place of it. + pub fn replace(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.replace, + thisObject, + globalObject, + content, + contentOptions, + ); } /// Replaces content of the element. - pub fn setInnerContent(this: *Element, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { - _ = this; - _ = content; - _ = globalObject; - _ = contentOptions; - return undefined; + pub fn setInnerContent(this: *Element, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue { + return contentHandler( + this, + LOLHTML.Element.setInnerContent, + thisObject, + globalObject, + content, + contentOptions, + ); } /// Removes the element with all its content. - pub fn remove(this: *Element, globalObject: *JSGlobalObject) JSValue { - _ = this; + pub fn remove(this: *Element, thisObject: js.JSObjectRef) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); - _ = globalObject; - return undefined; + this.element.?.remove(); + return JSValue.fromRef(thisObject); } /// Removes the start tag and end tag of the element but keeps its inner content intact. - pub fn removeAndKeepContent(this: *Element, globalObject: *JSGlobalObject) JSValue { - _ = this; + pub fn removeAndKeepContent(this: *Element, thisObject: js.JSObjectRef) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); - _ = globalObject; - return undefined; + this.element.?.removeAndKeepContent(); + return JSValue.fromRef(thisObject); } pub fn getTagName(this: *Element, globalObject: *JSGlobalObject) JSValue { - _ = this; + if (this.element == null) + return JSValue.jsUndefined(); - _ = globalObject; - return undefined; + return htmlStringValue(this.element.?.tagName(), globalObject); } - pub fn getRemoved(this: *Element, globalObject: *JSGlobalObject) JSValue { - _ = this; + pub fn setTagName(this: *Element, value: JSValue, exception: JSC.C.ExceptionRef, global: *JSGlobalObject) void { + if (this.element == null) + return; + + var text = value.toSlice(global, bun.default_allocator); + defer text.deinit(); + + this.element.?.setTagName(text.slice()) catch { + exception.* = throwLOLHTMLError(global).asObjectRef(); + }; + } - _ = globalObject; - return undefined; + pub fn getRemoved(this: *Element, _: *JSGlobalObject) JSValue { + if (this.element == null) + return JSValue.jsUndefined(); + return JSC.JSValue.jsBoolean(this.element.?.isRemoved()); } pub fn getNamespaceURI(this: *Element, globalObject: *JSGlobalObject) JSValue { - _ = this; + if (this.element == null) + return JSValue.jsUndefined(); - _ = globalObject; - return undefined; + return ZigString.init(std.mem.span(this.element.?.namespaceURI())).toValue(globalObject); } }; diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index a39058e45..cfdc6c110 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -1859,6 +1859,12 @@ const TextDecoder = WebCore.TextDecoder; const TimeoutTask = JSC.BunTimer.Timeout.TimeoutTask; const HTMLRewriter = JSC.Cloudflare.HTMLRewriter; const Element = JSC.Cloudflare.Element; +const Comment = JSC.Cloudflare.Comment; +const TextChunk = JSC.Cloudflare.TextChunk; +const DocType = JSC.Cloudflare.DocType; +const EndTag = JSC.Cloudflare.EndTag; +const DocEnd = JSC.Cloudflare.DocEnd; +const AttributeIterator = JSC.Cloudflare.AttributeIterator; pub const JSPrivateDataPtr = TaggedPointerUnion(.{ ResolveError, @@ -1886,6 +1892,12 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{ TimeoutTask, HTMLRewriter, Element, + Comment, + TextChunk, + DocType, + EndTag, + DocEnd, + AttributeIterator, }); pub inline fn GetJSPrivateData(comptime Type: type, ref: js.JSObjectRef) ?*Type { diff --git a/src/javascript/jsc/bindings/BunStream.cpp b/src/javascript/jsc/bindings/BunStream.cpp index 436c4d2dd..2214454bf 100644 --- a/src/javascript/jsc/bindings/BunStream.cpp +++ b/src/javascript/jsc/bindings/BunStream.cpp @@ -74,7 +74,7 @@ static WritableEvent getWritableEvent(const WTF::String& eventName) } \ } \ JSC::JSValue result = JSC::JSValue::decode( \ - ZigFunction(thisObject->state, globalObject, arguments.data(), argCount) \ + ZigFunction(thisObject->state, globalObject, reinterpret_cast<JSC__JSValue*>(arguments.data()), argCount) \ ); \ JSC::JSObject *obj = result.getObject(); \ if (UNLIKELY(obj != nullptr && obj->isErrorInstance())) { \ diff --git a/src/javascript/jsc/bindings/Path.cpp b/src/javascript/jsc/bindings/Path.cpp index 09ea7ecf6..460cbebd3 100644 --- a/src/javascript/jsc/bindings/Path.cpp +++ b/src/javascript/jsc/bindings/Path.cpp @@ -39,7 +39,7 @@ namespace JSCastingHelpers = JSC::JSCastingHelpers; auto clientData = Bun::clientData(vm); \ auto isWindows = thisObject->get(globalObject, clientData->builtinNames().isWindowsPrivateName()); \ JSC::JSValue result = JSC::JSValue::decode( \ - ZigFunction(globalObject, isWindows.asBoolean(), arguments.data(), argCount) \ + ZigFunction(globalObject, isWindows.asBoolean(), reinterpret_cast<JSC__JSValue*>(arguments.data()), argCount) \ ); \ JSC::JSObject *obj = result.getObject(); \ if (UNLIKELY(obj != nullptr && obj->isErrorInstance())) { \ diff --git a/src/javascript/jsc/bindings/bindings.cpp b/src/javascript/jsc/bindings/bindings.cpp index cc174cbda..40baf6c01 100644 --- a/src/javascript/jsc/bindings/bindings.cpp +++ b/src/javascript/jsc/bindings/bindings.cpp @@ -784,6 +784,22 @@ void JSC__JSValue__toZigString(JSC__JSValue JSValue0, ZigString* arg1, JSC__JSGl arg1->len = str.length(); } +JSC__JSValue ZigString__toExternalValueWithCallback(const ZigString* arg0, JSC__JSGlobalObject* arg1, void (*ArgFn2)(void* arg2, void* arg0, size_t arg1)) +{ + + ZigString str + = *arg0; + if (Zig::isTaggedUTF16Ptr(str.ptr)) { + return JSC::JSValue::encode(JSC::JSValue(JSC::jsOwnedString( + arg1->vm(), + WTF::String(ExternalStringImpl::create(reinterpret_cast<const UChar*>(Zig::untag(str.ptr)), str.len, ArgFn2))))); + } else { + return JSC::JSValue::encode(JSC::JSValue(JSC::jsOwnedString( + arg1->vm(), + WTF::String(ExternalStringImpl::create(reinterpret_cast<const LChar*>(Zig::untag(str.ptr)), str.len, ArgFn2))))); + } +} + JSC__JSValue ZigString__toErrorInstance(const ZigString* str, JSC__JSGlobalObject* globalObject) { return JSC::JSValue::encode(Zig::getErrorInstance(str, globalObject)); diff --git a/src/javascript/jsc/bindings/bindings.zig b/src/javascript/jsc/bindings/bindings.zig index f93589183..6215d9e4a 100644 --- a/src/javascript/jsc/bindings/bindings.zig +++ b/src/javascript/jsc/bindings/bindings.zig @@ -280,6 +280,14 @@ pub const ZigString = extern struct { return shim.cppFn("toExternalValue", .{ this, global }); } + pub fn toExternalValueWithCallback( + this: *const ZigString, + global: *JSGlobalObject, + callback: fn (ctx: ?*anyopaque, ptr: ?*anyopaque, len: usize) callconv(.C) void, + ) JSValue { + return shim.cppFn("toExternalValueWithCallback", .{ this, global, callback }); + } + pub fn to16BitValue(this: *const ZigString, global: *JSGlobalObject) JSValue { return shim.cppFn("to16BitValue", .{ this, global }); } @@ -316,6 +324,7 @@ pub const ZigString = extern struct { "toValueGC", "toErrorInstance", "toExternalU16", + "toExternalValueWithCallback", }; }; @@ -1573,12 +1582,12 @@ pub const String = extern struct { }; }; -pub const JSValue = enum(i64) { +pub const JSValue = enum(u64) { _, pub const shim = Shimmer("JSC", "JSValue", @This()); pub const is_pointer = false; - pub const Type = i64; + pub const Type = u64; const cppFn = shim.cppFn; pub const include = "<JavaScriptCore/JSValue.h>"; @@ -1836,22 +1845,18 @@ pub const JSValue = enum(i64) { return cppFn("getErrorsProperty", .{ this, globalObject }); } - pub fn jsNumberWithType(comptime Number: type, number: Type) JSValue { - return switch (Number) { - f64 => @call(.{ .modifier = .always_inline }, jsNumberFromDouble, .{number}), - u8 => @call(.{ .modifier = .always_inline }, jsNumberFromChar, .{number}), - u16 => @call(.{ .modifier = .always_inline }, jsNumberFromInt32, .{@intCast(i32, number)}), - i32 => @call(.{ .modifier = .always_inline }, jsNumberFromInt32, .{@truncate(i32, number)}), - c_int, i64 => if (number > std.math.maxInt(i32)) - jsNumberFromInt64(@truncate(i64, number)) - else - jsNumberFromInt32(@intCast(i32, number)), - - c_uint, u32 => if (number < std.math.maxInt(i32)) - jsNumberFromInt32(@intCast(i32, number)) - else - jsNumberFromUint64(@intCast(u64, number)), - + pub fn jsNumberWithType(comptime Number: type, number: Number) JSValue { + return switch (comptime Number) { + JSValue => number, + f64 => jsNumberFromDouble(number), + u8 => jsNumberFromChar(number), + u16 => jsNumberFromInt32(@intCast(i32, number)), + i32 => jsNumberFromInt32(@intCast(i32, number)), + c_int => jsNumberFromInt32(@intCast(i32, number)), + i64 => jsNumberFromInt64(@intCast(i64, number)), + c_uint => jsNumberFromUint64(@intCast(u64, number)), + u64 => jsNumberFromUint64(@intCast(u64, number)), + u32 => jsNumberFromInt32(@intCast(i32, number)), else => @compileError("Type transformation missing for number of type: " ++ @typeName(Number)), }; } @@ -2226,11 +2231,11 @@ pub const JSValue = enum(i64) { } pub inline fn asRef(this: JSValue) C_API.JSValueRef { - return @intToPtr(C_API.JSValueRef, @bitCast(usize, @enumToInt(this))); + return @intToPtr(C_API.JSValueRef, @intCast(usize, @enumToInt(this))); } pub inline fn fromRef(this: C_API.JSValueRef) JSValue { - return @intToEnum(JSValue, @bitCast(i64, @ptrToInt(this))); + return @intToEnum(JSValue, @ptrToInt(this)); } pub inline fn asObjectRef(this: JSValue) C_API.JSObjectRef { @@ -2238,7 +2243,7 @@ pub const JSValue = enum(i64) { } pub inline fn asVoid(this: JSValue) *anyopaque { - return @intToPtr(*anyopaque, @bitCast(u64, @enumToInt(this))); + return @intToPtr(*anyopaque, @enumToInt(this)); } pub const Extern = [_][]const u8{ "symbolKeyFor", "symbolFor", "getSymbolDescription", "createInternalPromise", "asInternalPromise", "asArrayBuffer_", "getReadableStreamState", "getWritableStreamState", "fromEntries", "createTypeError", "createRangeError", "createObject2", "getIfPropertyExistsImpl", "jsType", "jsonStringify", "kind_", "isTerminationException", "isSameValue", "getLengthOfArray", "toZigString", "createStringArray", "createEmptyObject", "putRecord", "asPromise", "isClass", "getNameProperty", "getClassName", "getErrorsProperty", "toInt32", "toBoolean", "isInt32", "isIterable", "forEach", "isAggregateError", "toZigException", "isException", "toWTFString", "hasProperty", "getPropertyNames", "getDirect", "putDirect", "getIfExists", "asString", "asObject", "asNumber", "isError", "jsNull", "jsUndefined", "jsTDZValue", "jsBoolean", "jsDoubleNumber", "jsNumberFromDouble", "jsNumberFromChar", "jsNumberFromU16", "jsNumberFromInt32", "jsNumberFromInt64", "jsNumberFromUint64", "isUndefined", "isNull", "isUndefinedOrNull", "isBoolean", "isAnyInt", "isUInt32AsAnyInt", "isInt32AsAnyInt", "isNumber", "isString", "isBigInt", "isHeapBigInt", "isBigInt32", "isSymbol", "isPrimitive", "isGetterSetter", "isCustomGetterSetter", "isObject", "isCell", "asCell", "toString", "toStringOrNull", "toPropertyKey", "toPropertyKeyValue", "toObject", "toString", "getPrototype", "getPropertyByPropertyName", "eqlValue", "eqlCell", "isCallable" }; diff --git a/src/javascript/jsc/bindings/exports.zig b/src/javascript/jsc/bindings/exports.zig index 0a0a045a3..6dfcb82d4 100644 --- a/src/javascript/jsc/bindings/exports.zig +++ b/src/javascript/jsc/bindings/exports.zig @@ -1104,7 +1104,7 @@ pub const ZigConsoleClient = struct { // For detecting circular references pub const Visited = struct { const ObjectPool = @import("../../../pool.zig").ObjectPool; - pub const Map = std.AutoHashMap(i64, void); + pub const Map = std.AutoHashMap(JSValue.Type, void); pub const Pool = ObjectPool( Map, struct { diff --git a/src/javascript/jsc/bindings/headers-cpp.h b/src/javascript/jsc/bindings/headers-cpp.h index 2935db6a5..e3c04d000 100644 --- a/src/javascript/jsc/bindings/headers-cpp.h +++ b/src/javascript/jsc/bindings/headers-cpp.h @@ -1,4 +1,4 @@ -//-- AUTOGENERATED FILE -- 1646833921 +//-- AUTOGENERATED FILE -- 1647061500 // clang-format off #pragma once diff --git a/src/javascript/jsc/bindings/headers.h b/src/javascript/jsc/bindings/headers.h index ffac8456e..e87cb4377 100644 --- a/src/javascript/jsc/bindings/headers.h +++ b/src/javascript/jsc/bindings/headers.h @@ -1,5 +1,5 @@ // clang-format: off -//-- AUTOGENERATED FILE -- 1646833921 +//-- AUTOGENERATED FILE -- 1647061500 #pragma once #include <stddef.h> @@ -118,10 +118,10 @@ typedef void* JSClassRef; typedef struct JSC__AsyncFunctionPrototype JSC__AsyncFunctionPrototype; // JSC::AsyncFunctionPrototype typedef ZigException ZigException; typedef bJSC__SourceCode JSC__SourceCode; // JSC::SourceCode + typedef uint64_t JSC__JSValue; typedef struct JSC__BigIntPrototype JSC__BigIntPrototype; // JSC::BigIntPrototype typedef struct JSC__GeneratorFunctionPrototype JSC__GeneratorFunctionPrototype; // JSC::GeneratorFunctionPrototype typedef ZigString ZigString; - typedef int64_t JSC__JSValue; typedef struct JSC__FunctionPrototype JSC__FunctionPrototype; // JSC::FunctionPrototype typedef bInspector__ScriptArguments Inspector__ScriptArguments; // Inspector::ScriptArguments typedef bJSC__Exception JSC__Exception; // JSC::Exception @@ -191,8 +191,8 @@ typedef void* JSClassRef; typedef JSClassRef JSClassRef; typedef Bun__ArrayBuffer Bun__ArrayBuffer; typedef ZigException ZigException; + typedef uint64_t JSC__JSValue; typedef ZigString ZigString; - typedef int64_t JSC__JSValue; using JSC__JSCell = JSC::JSCell; using JSC__Exception = JSC::Exception; using JSC__JSPromisePrototype = JSC::JSPromisePrototype; @@ -250,6 +250,7 @@ CPP_DECL JSC__JSValue ZigString__to16BitValue(const ZigString* arg0, JSC__JSGlob CPP_DECL JSC__JSValue ZigString__toErrorInstance(const ZigString* arg0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__JSValue ZigString__toExternalU16(const uint16_t* arg0, size_t arg1, JSC__JSGlobalObject* arg2); CPP_DECL JSC__JSValue ZigString__toExternalValue(const ZigString* arg0, JSC__JSGlobalObject* arg1); +CPP_DECL JSC__JSValue ZigString__toExternalValueWithCallback(const ZigString* arg0, JSC__JSGlobalObject* arg1, void (* ArgFn2)(void* arg0, void* arg1, size_t arg2)); CPP_DECL JSC__JSValue ZigString__toValue(const ZigString* arg0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__JSValue ZigString__toValueGC(const ZigString* arg0, JSC__JSGlobalObject* arg1); CPP_DECL JSC__JSValue SystemError__toErrorInstance(const SystemError* arg0, JSC__JSGlobalObject* arg1); diff --git a/src/javascript/jsc/bindings/headers.zig b/src/javascript/jsc/bindings/headers.zig index 7fdca59d9..90a3a46ea 100644 --- a/src/javascript/jsc/bindings/headers.zig +++ b/src/javascript/jsc/bindings/headers.zig @@ -140,6 +140,7 @@ pub extern fn ZigString__to16BitValue(arg0: [*c]const ZigString, arg1: [*c]JSC__ pub extern fn ZigString__toErrorInstance(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; pub extern fn ZigString__toExternalU16(arg0: [*c]const u16, arg1: usize, arg2: [*c]JSC__JSGlobalObject) JSC__JSValue; pub extern fn ZigString__toExternalValue(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; +pub extern fn ZigString__toExternalValueWithCallback(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject, ArgFn2: ?fn (?*anyopaque, ?*anyopaque, usize) callconv(.C) void) JSC__JSValue; pub extern fn ZigString__toValue(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; pub extern fn ZigString__toValueGC(arg0: [*c]const ZigString, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; pub extern fn SystemError__toErrorInstance(arg0: [*c]const SystemError, arg1: [*c]JSC__JSGlobalObject) JSC__JSValue; diff --git a/src/javascript/jsc/bindings/helpers.h b/src/javascript/jsc/bindings/helpers.h index 11d728ac5..9e6da5d66 100644 --- a/src/javascript/jsc/bindings/helpers.h +++ b/src/javascript/jsc/bindings/helpers.h @@ -111,7 +111,7 @@ static bool isTaggedExternalPtr(const unsigned char* ptr) static const WTF::String toString(ZigString str) { if (str.len == 0 || str.ptr == nullptr) { - return WTF::String(); + return WTF::Stzring(); } if (UNLIKELY(isTaggedUTF8Ptr(str.ptr))) { return WTF::String::fromUTF8(untag(str.ptr), str.len); diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 7f35ef8b7..a0198ed65 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -1301,7 +1301,7 @@ pub const Bun = struct { Timer.set(id, globalThis, callback, countdown, false) catch return JSValue.jsUndefined(); - return JSValue.jsNumber(@intCast(i32, id)); + return JSValue.jsNumberWithType(i32, id); } pub fn setInterval( globalThis: *JSGlobalObject, @@ -1315,7 +1315,7 @@ pub const Bun = struct { Timer.set(id, globalThis, callback, countdown, true) catch return JSValue.jsUndefined(); - return JSValue.jsNumber(@intCast(i32, id)); + return JSValue.jsNumberWithType(i32, id); } pub fn clearTimer(id: JSValue, _: *JSGlobalObject) void { diff --git a/src/javascript/jsc/node/types.zig b/src/javascript/jsc/node/types.zig index f9cf5a97d..0879c11be 100644 --- a/src/javascript/jsc/node/types.zig +++ b/src/javascript/jsc/node/types.zig @@ -420,6 +420,14 @@ pub const ArgumentsSlice = struct { return this.remaining[0]; } + + pub fn nextEat(this: *ArgumentsSlice) ?JSC.JSValue { + if (this.remaining.len == 0) { + return null; + } + defer this.eat(); + return this.remaining[0]; + } }; pub fn fileDescriptorFromJS(ctx: JSC.C.JSContextRef, value: JSC.JSValue, exception: JSC.C.ExceptionRef) ?FileDescriptor { diff --git a/src/jsc.zig b/src/jsc.zig index e75b0fb29..fd4c380d3 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -10,6 +10,12 @@ pub const WebCore = @import("./javascript/jsc/webcore.zig"); pub const Cloudflare = struct { pub const HTMLRewriter = @import("./javascript/jsc/api/html_rewriter.zig").HTMLRewriter; pub const Element = @import("./javascript/jsc/api/html_rewriter.zig").Element; + pub const Comment = @import("./javascript/jsc/api/html_rewriter.zig").Comment; + pub const TextChunk = @import("./javascript/jsc/api/html_rewriter.zig").TextChunk; + pub const DocType = @import("./javascript/jsc/api/html_rewriter.zig").DocType; + pub const DocEnd = @import("./javascript/jsc/api/html_rewriter.zig").DocEnd; + pub const EndTag = @import("./javascript/jsc/api/html_rewriter.zig").EndTag; + pub const AttributeIterator = @import("./javascript/jsc/api/html_rewriter.zig").AttributeIterator; }; pub const Jest = @import("./javascript/jsc/test/jest.zig"); pub const API = struct { |