aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/api/html_rewriter.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/bun.js/api/html_rewriter.zig')
-rw-r--r--src/bun.js/api/html_rewriter.zig1886
1 files changed, 1886 insertions, 0 deletions
diff --git a/src/bun.js/api/html_rewriter.zig b/src/bun.js/api/html_rewriter.zig
new file mode 100644
index 000000000..fc91c76ad
--- /dev/null
+++ b/src/bun.js/api/html_rewriter.zig
@@ -0,0 +1,1886 @@
+const std = @import("std");
+const Api = @import("../../api/schema.zig").Api;
+const FilesystemRouter = @import("../../router.zig");
+const http = @import("../../http.zig");
+const JavaScript = @import("../javascript.zig");
+const QueryStringMap = @import("../../url.zig").QueryStringMap;
+const CombinedScanner = @import("../../url.zig").CombinedScanner;
+const bun = @import("../../global.zig");
+const string = bun.string;
+const JSC = @import("../../jsc.zig");
+const js = JSC.C;
+const WebCore = @import("../webcore/response.zig");
+const Router = @This();
+const Bundler = @import("../../bundler.zig");
+const VirtualMachine = JavaScript.VirtualMachine;
+const ScriptSrcStream = std.io.FixedBufferStream([]u8);
+const ZigString = JSC.ZigString;
+const Fs = @import("../../fs.zig");
+const Base = @import("../base.zig");
+const getAllocator = Base.getAllocator;
+const JSObject = JSC.JSObject;
+const JSError = Base.JSError;
+const JSValue = JSC.JSValue;
+const JSGlobalObject = JSC.JSGlobalObject;
+const strings = @import("strings");
+const NewClass = Base.NewClass;
+const To = Base.To;
+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);
+pub 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 {
+ builder: *LOLHTML.HTMLRewriter.Builder,
+ context: LOLHTMLContext,
+
+ pub const Constructor = JSC.NewConstructor(HTMLRewriter, .{ .constructor = constructor }, .{});
+
+ pub const Class = NewClass(
+ HTMLRewriter,
+ .{ .name = "HTMLRewriter" },
+ .{
+ .finalize = finalize,
+ .on = .{
+ .rfn = wrap(HTMLRewriter, "on"),
+ },
+ .onDocument = .{
+ .rfn = wrap(HTMLRewriter, "onDocument"),
+ },
+ .transform = .{
+ .rfn = wrap(HTMLRewriter, "transform"),
+ },
+ },
+ .{},
+ );
+
+ pub fn constructor(
+ ctx: js.JSContextRef,
+ _: js.JSObjectRef,
+ _: []const js.JSValueRef,
+ _: js.ExceptionRef,
+ ) js.JSObjectRef {
+ var rewriter = bun.default_allocator.create(HTMLRewriter) catch unreachable;
+ rewriter.* = HTMLRewriter{
+ .builder = LOLHTML.HTMLRewriter.Builder.init(),
+ .context = .{},
+ };
+ return HTMLRewriter.Class.make(ctx, rewriter);
+ }
+
+ pub fn on(
+ this: *HTMLRewriter,
+ global: *JSGlobalObject,
+ selector_name: ZigString,
+ thisObject: JSC.C.JSObjectRef,
+ listener: JSValue,
+ exception: JSC.C.ExceptionRef,
+ ) JSValue {
+ 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,
+ listener: JSValue,
+ thisObject: JSC.C.JSObjectRef,
+ exception: JSC.C.ExceptionRef,
+ ) JSValue {
+ 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) void {
+ this.finalizeWithoutDestroy();
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn finalizeWithoutDestroy(this: *HTMLRewriter) void {
+ this.context.deinit(bun.default_allocator);
+ }
+
+ pub fn beginTransform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
+ const new_context = this.context;
+ this.context = .{};
+ return BufferOutputSink.init(new_context, global, response, this.builder);
+ }
+
+ pub fn returnEmptyResponse(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
+ var result = bun.default_allocator.create(Response) catch unreachable;
+
+ response.cloneInto(result, getAllocator(global.ref()), global);
+ this.finalizeWithoutDestroy();
+ return JSValue.fromRef(Response.makeMaybePooled(global.ref(), result));
+ }
+
+ pub fn transform(this: *HTMLRewriter, global: *JSGlobalObject, response: *Response) JSValue {
+ var input = response.body.slice();
+
+ if (input.len == 0 and !(response.body.value == .Blob and response.body.value.Blob.needsToReadFile())) {
+ return this.returnEmptyResponse(global, response);
+ }
+
+ return this.beginTransform(global, response);
+ }
+
+ pub const HTMLRewriterLoader = struct {
+ rewriter: *LOLHTML.HTMLRewriter,
+ finalized: bool = false,
+ context: LOLHTMLContext,
+ chunk_size: usize = 0,
+ failed: bool = false,
+ output: JSC.WebCore.Sink,
+ signal: JSC.WebCore.Signal = .{},
+ backpressure: std.fifo.LinearFifo(u8, .Dynamic) = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator),
+
+ pub fn finalize(this: *HTMLRewriterLoader) void {
+ if (this.finalized) return;
+ this.rewriter.deinit();
+ this.backpressure.deinit();
+ this.backpressure = std.fifo.LinearFifo(u8, .Dynamic).init(bun.default_allocator);
+ this.finalized = true;
+ }
+
+ pub fn fail(this: *HTMLRewriterLoader, err: JSC.Node.Syscall.Error) void {
+ this.signal.close(err);
+ this.output.end(err);
+ this.failed = true;
+ this.finalize();
+ }
+
+ pub fn connect(this: *HTMLRewriterLoader, signal: JSC.WebCore.Signal) void {
+ this.signal = signal;
+ }
+
+ pub fn writeToDestination(this: *HTMLRewriterLoader, bytes: []const u8) void {
+ if (this.backpressure.count > 0) {
+ this.backpressure.write(bytes) catch {
+ this.fail(JSC.Node.Syscall.Error.oom);
+ this.finalize();
+ };
+ return;
+ }
+
+ const write_result = this.output.write(.{ .temporary = bun.ByteList.init(bytes) });
+
+ switch (write_result) {
+ .err => |err| {
+ this.fail(err);
+ },
+ .owned_and_done, .temporary_and_done, .into_array_and_done => {
+ this.done();
+ },
+ .pending => |pending| {
+ pending.applyBackpressure(bun.default_allocator, &this.output, pending, bytes);
+ },
+ .into_array, .owned, .temporary => {
+ this.signal.ready(if (this.chunk_size > 0) this.chunk_size else null, null);
+ },
+ }
+ }
+
+ pub fn done(
+ this: *HTMLRewriterLoader,
+ ) void {
+ this.output.end(null);
+ this.signal.close(null);
+ this.finalize();
+ }
+
+ pub fn setup(
+ this: *HTMLRewriterLoader,
+ builder: *LOLHTML.HTMLRewriter.Builder,
+ context: LOLHTMLContext,
+ size_hint: ?usize,
+ output: JSC.WebCore.Sink,
+ ) ?[]const u8 {
+ for (context.document_handlers.items) |doc| {
+ doc.ctx = this;
+ }
+ for (context.element_handlers.items) |doc| {
+ doc.ctx = this;
+ }
+
+ const chunk_size = @maximum(size_hint orelse 16384, 1024);
+ this.rewriter = builder.build(
+ .UTF8,
+ .{
+ .preallocated_parsing_buffer_size = chunk_size,
+ .max_allowed_memory_usage = std.math.maxInt(u32),
+ },
+ false,
+ HTMLRewriterLoader,
+ this,
+ HTMLRewriterLoader.writeToDestination,
+ HTMLRewriterLoader.done,
+ ) catch {
+ output.end();
+ return LOLHTML.HTMLString.lastError().slice();
+ };
+
+ this.chunk_size = chunk_size;
+ this.context = context;
+ this.output = output;
+
+ return null;
+ }
+
+ pub fn sink(this: *HTMLRewriterLoader) JSC.WebCore.Sink {
+ return JSC.WebCore.Sink.init(this);
+ }
+
+ fn writeBytes(this: *HTMLRewriterLoader, bytes: bun.ByteList, comptime deinit_: bool) ?JSC.Node.Syscall.Error {
+ this.rewriter.write(bytes.slice()) catch {
+ return JSC.Node.Syscall.Error{
+ .errno = 1,
+ // TODO: make this a union
+ .path = bun.default_allocator.dupe(u8, LOLHTML.HTMLString.lastError().slice()) catch unreachable,
+ };
+ };
+ if (comptime deinit_) bytes.listManaged(bun.default_allocator).deinit();
+ return null;
+ }
+
+ pub fn write(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
+ switch (data) {
+ .owned => |bytes| {
+ if (this.writeBytes(bytes, true)) |err| {
+ return .{ .err = err };
+ }
+ return .{ .owned = bytes.len };
+ },
+ .owned_and_done => |bytes| {
+ if (this.writeBytes(bytes, true)) |err| {
+ return .{ .err = err };
+ }
+ return .{ .owned_and_done = bytes.len };
+ },
+ .temporary_and_done => |bytes| {
+ if (this.writeBytes(bytes, false)) |err| {
+ return .{ .err = err };
+ }
+ return .{ .temporary_and_done = bytes.len };
+ },
+ .temporary => |bytes| {
+ if (this.writeBytes(bytes, false)) |err| {
+ return .{ .err = err };
+ }
+ return .{ .temporary = bytes.len };
+ },
+ else => unreachable,
+ }
+ }
+
+ pub fn writeUTF16(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
+ return JSC.WebCore.Sink.UTF8Fallback.writeUTF16(HTMLRewriterLoader, this, data, write);
+ }
+
+ pub fn writeLatin1(this: *HTMLRewriterLoader, data: JSC.WebCore.StreamResult) JSC.WebCore.StreamResult.Writable {
+ return JSC.WebCore.Sink.UTF8Fallback.writeLatin1(HTMLRewriterLoader, this, data, write);
+ }
+ };
+
+ pub const BufferOutputSink = struct {
+ global: *JSGlobalObject,
+ bytes: bun.MutableString,
+ rewriter: *LOLHTML.HTMLRewriter,
+ context: LOLHTMLContext,
+ response: *Response,
+ input: JSC.WebCore.Blob = undefined,
+ pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue {
+ var result = bun.default_allocator.create(Response) catch unreachable;
+ var sink = bun.default_allocator.create(BufferOutputSink) catch unreachable;
+ sink.* = BufferOutputSink{
+ .global = global,
+ .bytes = bun.MutableString.initEmpty(bun.default_allocator),
+ .rewriter = undefined,
+ .context = context,
+ .response = result,
+ };
+
+ for (sink.context.document_handlers.items) |doc| {
+ doc.ctx = sink;
+ }
+ for (sink.context.element_handlers.items) |doc| {
+ doc.ctx = sink;
+ }
+
+ sink.rewriter = builder.build(
+ .UTF8,
+ .{
+ .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024),
+ .max_allowed_memory_usage = std.math.maxInt(u32),
+ },
+ false,
+ BufferOutputSink,
+ sink,
+ BufferOutputSink.write,
+ BufferOutputSink.done,
+ ) catch {
+ sink.deinit();
+ bun.default_allocator.destroy(result);
+
+ return throwLOLHTMLError(global);
+ };
+
+ result.* = Response{
+ .allocator = bun.default_allocator,
+ .body = .{
+ .init = .{
+ .status_code = 200,
+ },
+ .value = .{
+ .Locked = .{
+ .global = global,
+ .task = sink,
+ },
+ },
+ },
+ };
+
+ result.body.init.headers = original.body.init.headers;
+ result.body.init.method = original.body.init.method;
+ result.body.init.status_code = original.body.init.status_code;
+
+ result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable;
+ result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable;
+
+ var input: JSC.WebCore.Blob = original.body.value.use();
+
+ const is_pending = input.needsToReadFile();
+ defer if (!is_pending) input.detach();
+
+ if (is_pending) {
+ input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoading, global);
+ } else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| {
+ return error_value;
+ }
+
+ // Hold off on cloning until we're actually done.
+
+ return JSC.JSValue.fromRef(
+ Response.makeMaybePooled(sink.global.ref(), sink.response),
+ );
+ }
+
+ pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: JSC.WebCore.Blob.Store.ReadFile.ResultType) void {
+ switch (bytes) {
+ .err => |err| {
+ if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and
+ sink.response.body.value.Locked.promise == null)
+ {
+ sink.response.body.value = .{ .Empty = .{} };
+ // is there a pending promise?
+ // we will need to reject it
+ } else if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink) and
+ sink.response.body.value.Locked.promise != null)
+ {
+ sink.response.body.value.Locked.callback = null;
+ sink.response.body.value.Locked.task = null;
+ }
+
+ sink.response.body.value.toErrorInstance(err.toErrorInstance(sink.global), sink.global);
+ sink.rewriter.end() catch {};
+ sink.deinit();
+ return;
+ },
+ .result => |data| {
+ _ = sink.runOutputSink(data.buf, true, data.is_temporary);
+ },
+ }
+ }
+
+ pub fn runOutputSink(
+ sink: *BufferOutputSink,
+ bytes: []const u8,
+ is_async: bool,
+ free_bytes_on_end: bool,
+ ) ?JSValue {
+ defer if (free_bytes_on_end)
+ bun.default_allocator.free(bun.constStrToU8(bytes));
+
+ sink.bytes.growBy(bytes.len) catch unreachable;
+ var global = sink.global;
+ var response = sink.response;
+
+ sink.rewriter.write(bytes) catch {
+ sink.deinit();
+ bun.default_allocator.destroy(sink);
+
+ if (is_async) {
+ response.body.value.toErrorInstance(throwLOLHTMLError(global), global);
+
+ return null;
+ } else {
+ return throwLOLHTMLError(global);
+ }
+ };
+
+ sink.rewriter.end() catch {
+ if (!is_async) response.finalize();
+ sink.response = undefined;
+ sink.deinit();
+
+ if (is_async) {
+ response.body.value.toErrorInstance(throwLOLHTMLError(global), global);
+ return null;
+ } else {
+ return throwLOLHTMLError(global);
+ }
+ };
+
+ return null;
+ }
+
+ pub const Sync = enum { suspended, pending, done };
+
+ pub fn done(this: *BufferOutputSink) void {
+ var prev_value = this.response.body.value;
+ var bytes = this.bytes.toOwnedSliceLeaky();
+ this.response.body.value = .{
+ .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global),
+ };
+ prev_value.resolve(
+ &this.response.body.value,
+ this.global,
+ );
+ }
+
+ 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 StreamOutputSink = struct {
+ // global: *JSGlobalObject,
+ // rewriter: *LOLHTML.HTMLRewriter,
+ // context: LOLHTMLContext,
+ // response: *Response,
+ // input: JSC.WebCore.Blob = undefined,
+ // pub fn init(context: LOLHTMLContext, global: *JSGlobalObject, original: *Response, builder: *LOLHTML.HTMLRewriter.Builder) JSValue {
+ // var result = bun.default_allocator.create(Response) catch unreachable;
+ // var sink = bun.default_allocator.create(StreamOutputSink) catch unreachable;
+ // sink.* = StreamOutputSink{
+ // .global = global,
+ // .rewriter = undefined,
+ // .context = context,
+ // .response = result,
+ // };
+
+ // for (sink.context.document_handlers.items) |doc| {
+ // doc.ctx = sink;
+ // }
+ // for (sink.context.element_handlers.items) |doc| {
+ // doc.ctx = sink;
+ // }
+
+ // sink.rewriter = builder.build(
+ // .UTF8,
+ // .{
+ // .preallocated_parsing_buffer_size = @maximum(original.body.len(), 1024),
+ // .max_allowed_memory_usage = std.math.maxInt(u32),
+ // },
+ // false,
+ // StreamOutputSink,
+ // sink,
+ // StreamOutputSink.write,
+ // StreamOutputSink.done,
+ // ) catch {
+ // sink.deinit();
+ // bun.default_allocator.destroy(result);
+
+ // return throwLOLHTMLError(global);
+ // };
+
+ // result.* = Response{
+ // .allocator = bun.default_allocator,
+ // .body = .{
+ // .init = .{
+ // .status_code = 200,
+ // },
+ // .value = .{
+ // .Locked = .{
+ // .global = global,
+ // .task = sink,
+ // },
+ // },
+ // },
+ // };
+
+ // result.body.init.headers = original.body.init.headers;
+ // result.body.init.method = original.body.init.method;
+ // result.body.init.status_code = original.body.init.status_code;
+
+ // result.url = bun.default_allocator.dupe(u8, original.url) catch unreachable;
+ // result.status_text = bun.default_allocator.dupe(u8, original.status_text) catch unreachable;
+
+ // var input: JSC.WebCore.Blob = original.body.value.use();
+
+ // const is_pending = input.needsToReadFile();
+ // defer if (!is_pending) input.detach();
+
+ // if (is_pending) {
+ // input.doReadFileInternal(*StreamOutputSink, sink, onFinishedLoading, global);
+ // } else if (sink.runOutputSink(input.sharedView(), false, false)) |error_value| {
+ // return error_value;
+ // }
+
+ // // Hold off on cloning until we're actually done.
+
+ // return JSC.JSValue.fromRef(
+ // Response.makeMaybePooled(sink.global.ref(), sink.response),
+ // );
+ // }
+
+ // pub fn runOutputSink(
+ // sink: *StreamOutputSink,
+ // bytes: []const u8,
+ // is_async: bool,
+ // free_bytes_on_end: bool,
+ // ) ?JSValue {
+ // defer if (free_bytes_on_end)
+ // bun.default_allocator.free(bun.constStrToU8(bytes));
+
+ // return null;
+ // }
+
+ // pub const Sync = enum { suspended, pending, done };
+
+ // pub fn done(this: *StreamOutputSink) void {
+ // var prev_value = this.response.body.value;
+ // var bytes = this.bytes.toOwnedSliceLeaky();
+ // this.response.body.value = .{
+ // .Blob = JSC.WebCore.Blob.init(bytes, this.bytes.allocator, this.global),
+ // };
+ // prev_value.resolve(
+ // &this.response.body.value,
+ // this.global,
+ // );
+ // }
+
+ // pub fn write(this: *StreamOutputSink, bytes: []const u8) void {
+ // this.bytes.append(bytes) catch unreachable;
+ // }
+
+ // pub fn deinit(this: *StreamOutputSink) void {
+ // this.bytes.deinit();
+
+ // this.context.deinit(bun.default_allocator);
+ // }
+ // };
+};
+
+const DocumentHandler = struct {
+ onDocTypeCallback: ?JSValue = null,
+ onCommentCallback: ?JSValue = null,
+ onTextCallback: ?JSValue = null,
+ onEndCallback: ?JSValue = null,
+ thisObject: JSValue,
+ global: *JSGlobalObject,
+ ctx: ?*HTMLRewriter.BufferOutputSink = null,
+
+ 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 {
+ if (comptime JSC.is_bindgen)
+ unreachable;
+ 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
+ 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,
+ );
+ var promise_: ?*JSC.JSInternalPromise = null;
+ while (!result.isUndefinedOrNull()) {
+ if (result.isError() or result.isAggregateError(this.global)) {
+ @field(zig_element, field_name) = null;
+ return true;
+ }
+
+ var promise = promise_ orelse JSC.JSInternalPromise.resolvedPromise(this.global, result);
+ promise_ = promise;
+ JavaScript.VirtualMachine.vm.event_loop.waitForPromise(promise);
+
+ switch (promise.status(this.global.vm())) {
+ JSC.JSPromise.Status.Pending => unreachable,
+ JSC.JSPromise.Status.Rejected => {
+ JavaScript.VirtualMachine.vm.defaultErrorHandler(promise.result(this.global.vm()), null);
+ @field(zig_element, field_name) = null;
+ return false;
+ },
+ JSC.JSPromise.Status.Fulfilled => {
+ result = promise.result(this.global.vm());
+ break;
+ },
+ }
+
+ break;
+ }
+ @field(zig_element, field_name) = null;
+ return false;
+ }
+ }.callback;
+}
+
+const ElementHandler = struct {
+ onElementCallback: ?JSValue = null,
+ onCommentCallback: ?JSValue = null,
+ onTextCallback: ?JSValue = null,
+ thisObject: JSValue,
+ global: *JSGlobalObject,
+ ctx: ?*HTMLRewriter.BufferOutputSink = null,
+
+ 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, "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;
+ }
+
+ 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 fn onElement(this: *ElementHandler, value: *LOLHTML.Element) bool {
+ return HandlerCallback(
+ ElementHandler,
+ Element,
+ LOLHTML.Element,
+ "element",
+ "onElementCallback",
+ )(this, value);
+ }
+
+ pub const onComment = HandlerCallback(
+ ElementHandler,
+ Comment,
+ LOLHTML.Comment,
+ "comment",
+ "onCommentCallback",
+ );
+
+ pub const onText = HandlerCallback(
+ ElementHandler,
+ TextChunk,
+ LOLHTML.TextChunk,
+ "text_chunk",
+ "onTextCallback",
+ );
+};
+
+pub const ContentOptions = struct {
+ html: bool = false,
+};
+
+const getterWrap = JSC.getterWrap;
+const setterWrap = JSC.setterWrap;
+const wrap = JSC.wrapAsync;
+
+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();
+}
+
+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"),
+ },
+ .finalize = finalize,
+ },
+ .{
+ .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 fn finalize(this: *TextChunk) void {
+ this.text_chunk = null;
+ bun.default_allocator.destroy(this);
+ }
+};
+
+pub const DocType = struct {
+ doctype: ?*LOLHTML.DocType = null,
+
+ pub fn finalize(this: *DocType) void {
+ this.doctype = null;
+ bun.default_allocator.destroy(this);
+ }
+
+ pub const Class = NewClass(
+ DocType,
+ .{
+ .name = "DocType",
+ },
+ .{
+ .finalize = finalize,
+ },
+ .{
+ .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 fn finalize(this: *DocEnd) void {
+ this.doc_end = null;
+ bun.default_allocator.destroy(this);
+ }
+
+ pub const Class = NewClass(
+ DocEnd,
+ .{ .name = "DocEnd" },
+ .{
+ .finalize = finalize,
+ .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 fn finalize(this: *Comment) void {
+ this.comment = null;
+ bun.default_allocator.destroy(this);
+ }
+
+ 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"),
+ },
+ .finalize = finalize,
+ },
+ .{
+ .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 fn finalize(this: *EndTag) void {
+ this.end_tag = null;
+ bun.default_allocator.destroy(this);
+ }
+
+ 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"),
+ },
+ .finalize = finalize,
+ },
+ .{
+ .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,
+
+ const attribute_iterator_path: string = "file:///bun-vfs/lolhtml/AttributeIterator.js";
+ const attribute_iterator_code: string =
+ \\"use strict";
+ \\
+ \\class AttributeIterator {
+ \\ constructor(internal) {
+ \\ this.#iterator = internal;
+ \\ }
+ \\
+ \\ #iterator;
+ \\
+ \\ [Symbol.iterator]() {
+ \\ return this;
+ \\ }
+ \\
+ \\ next() {
+ \\ if (this.#iterator === null)
+ \\ return {done: true};
+ \\ var value = this.#iterator.next();
+ \\ if (!value) {
+ \\ this.#iterator = null;
+ \\ return {done: true};
+ \\ }
+ \\ return {done: false, value: value};
+ \\ }
+ \\}
+ \\
+ \\return new AttributeIterator(internal1);
+ ;
+ threadlocal var attribute_iterator_class: JSC.C.JSObjectRef = undefined;
+ threadlocal var attribute_iterator_loaded: bool = false;
+
+ pub fn getAttributeIteratorJSClass(global: *JSGlobalObject) JSValue {
+ if (attribute_iterator_loaded)
+ return JSC.JSValue.fromRef(attribute_iterator_class);
+ attribute_iterator_loaded = true;
+ var exception_ptr: ?[*]JSC.JSValueRef = null;
+ var name = JSC.C.JSStringCreateStatic("AttributeIteratorGetter", "AttributeIteratorGetter".len);
+ var param_name = JSC.C.JSStringCreateStatic("internal1", "internal1".len);
+ var attribute_iterator_class_ = JSC.C.JSObjectMakeFunction(
+ global.ref(),
+ name,
+ 1,
+ &[_]JSC.C.JSStringRef{param_name},
+ JSC.C.JSStringCreateStatic(attribute_iterator_code.ptr, attribute_iterator_code.len),
+ JSC.C.JSStringCreateStatic(attribute_iterator_path.ptr, attribute_iterator_path.len),
+ 0,
+ exception_ptr,
+ );
+ JSC.C.JSValueProtect(global.ref(), attribute_iterator_class_);
+ attribute_iterator_class = attribute_iterator_class_;
+ return JSC.JSValue.fromRef(attribute_iterator_class);
+ }
+
+ pub fn finalize(this: *AttributeIterator) void {
+ if (this.iterator) |iter| {
+ iter.deinit();
+ this.iterator = null;
+ }
+ bun.default_allocator.destroy(this);
+ }
+
+ pub const Class = NewClass(
+ AttributeIterator,
+ .{ .name = "AttributeIterator" },
+ .{
+ .next = .{
+ .rfn = wrap(AttributeIterator, "next"),
+ },
+ .finalize = finalize,
+ },
+ .{},
+ );
+
+ 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.jsNull();
+ }
+
+ var attribute = this.iterator.?.next() orelse {
+ this.iterator.?.deinit();
+ this.iterator = null;
+ return JSC.JSValue.jsNull();
+ };
+
+ // TODO: don't clone here
+ const value = attribute.value();
+ const name = attribute.name();
+ defer name.deinit();
+ defer value.deinit();
+
+ var strs = [2]ZigString{
+ ZigString.init(name.slice()),
+ ZigString.init(value.slice()),
+ };
+
+ var valid_strs: []ZigString = strs[0..2];
+
+ var array = JSC.JSValue.createStringArray(
+ globalObject,
+ valid_strs.ptr,
+ valid_strs.len,
+ true,
+ );
+
+ return array;
+ }
+};
+pub const Element = struct {
+ element: ?*LOLHTML.Element = null,
+
+ pub const Class = NewClass(
+ Element,
+ .{ .name = "Element" },
+ .{
+ .getAttribute = .{
+ .rfn = wrap(Element, "getAttribute"),
+ },
+ .hasAttribute = .{
+ .rfn = wrap(Element, "hasAttribute"),
+ },
+ .setAttribute = .{
+ .rfn = wrap(Element, "setAttribute"),
+ },
+ .removeAttribute = .{
+ .rfn = wrap(Element, "removeAttribute"),
+ },
+ .before = .{
+ .rfn = wrap(Element, "before"),
+ },
+ .after = .{
+ .rfn = wrap(Element, "after"),
+ },
+ .prepend = .{
+ .rfn = wrap(Element, "prepend"),
+ },
+ .append = .{
+ .rfn = wrap(Element, "append"),
+ },
+ .replace = .{
+ .rfn = wrap(Element, "replace"),
+ },
+ .setInnerContent = .{
+ .rfn = wrap(Element, "setInnerContent"),
+ },
+ .remove = .{
+ .rfn = wrap(Element, "remove"),
+ },
+ .removeAndKeepContent = .{
+ .rfn = wrap(Element, "removeAndKeepContent"),
+ },
+ .onEndTag = .{
+ .rfn = wrap(Element, "onEndTag"),
+ },
+ .finalize = finalize,
+ },
+ .{
+ .tagName = .{
+ .get = getterWrap(Element, "getTagName"),
+ .set = setterWrap(Element, "setTagName"),
+ },
+ .removed = .{
+ .get = getterWrap(Element, "getRemoved"),
+ },
+ .namespaceURI = .{
+ .get = getterWrap(Element, "getNamespaceURI"),
+ },
+ .attributes = .{
+ .get = getterWrap(Element, "getAttributes"),
+ },
+ },
+ );
+
+ pub fn finalize(this: *Element) void {
+ this.element = null;
+ bun.default_allocator.destroy(this);
+ }
+
+ 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 {
+ 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, 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, 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, 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, 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, thisObject: js.JSObjectRef, globalObject: *JSGlobalObject, content: ZigString, contentOptions: ?ContentOptions) JSValue {
+ return contentHandler(
+ this,
+ LOLHTML.Element.after,
+ thisObject,
+ globalObject,
+ content,
+ contentOptions,
+ );
+ }
+
+ /// 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, 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, 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, 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, thisObject: js.JSObjectRef) JSValue {
+ if (this.element == null)
+ return JSValue.jsUndefined();
+
+ 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, thisObject: js.JSObjectRef) JSValue {
+ if (this.element == null)
+ return JSValue.jsUndefined();
+
+ this.element.?.removeAndKeepContent();
+ return JSValue.fromRef(thisObject);
+ }
+
+ pub fn getTagName(this: *Element, globalObject: *JSGlobalObject) JSValue {
+ if (this.element == null)
+ return JSValue.jsUndefined();
+
+ return htmlStringValue(this.element.?.tagName(), globalObject);
+ }
+
+ 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();
+ };
+ }
+
+ 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 {
+ if (this.element == null)
+ return JSValue.jsUndefined();
+
+ return ZigString.init(std.mem.span(this.element.?.namespaceURI())).toValue(globalObject);
+ }
+
+ pub fn getAttributes(this: *Element, globalObject: *JSGlobalObject) JSValue {
+ if (this.element == null)
+ return JSValue.jsUndefined();
+
+ var iter = this.element.?.attributes() orelse return throwLOLHTMLError(globalObject);
+ var attr_iter = bun.default_allocator.create(AttributeIterator) catch unreachable;
+ attr_iter.* = .{ .iterator = iter };
+ var attr = AttributeIterator.Class.make(globalObject.ref(), attr_iter);
+ JSC.C.JSValueProtect(globalObject.ref(), attr);
+ defer JSC.C.JSValueUnprotect(globalObject.ref(), attr);
+ return JSC.JSValue.fromRef(
+ JSC.C.JSObjectCallAsFunction(
+ globalObject.ref(),
+ AttributeIterator.getAttributeIteratorJSClass(globalObject).asObjectRef(),
+ null,
+ 1,
+ @ptrCast([*]JSC.C.JSObjectRef, &attr),
+ null,
+ ),
+ );
+ }
+};