diff options
Diffstat (limited to 'src/bun.js/api/html_rewriter.zig')
-rw-r--r-- | src/bun.js/api/html_rewriter.zig | 1886 |
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, + ), + ); + } +}; |