Files
bun.sh/src/javascript/jsc/api/html_rewriter.zig
2022-03-22 04:44:39 -07:00

1604 lines
52 KiB
Zig

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);
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()));
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 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,
.headers = null,
},
.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 (input.needsToReadFile()) {
input.doReadFileInternal(*BufferOutputSink, sink, onFinishedLoadingWrap, global);
} else if (sink.runOutputSink(input.sharedView(), 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 onFinishedLoadingWrap(sink: *anyopaque, bytes: anyerror![]u8) void {
onFinishedLoading(bun.cast(*BufferOutputSink, sink), bytes);
}
pub fn onFinishedLoading(sink: *BufferOutputSink, bytes: anyerror![]u8) void {
var input = sink.input;
defer input.detach();
const data = bytes catch |err| {
if (sink.response.body.value == .Locked and @ptrToInt(sink.response.body.value.Locked.task) == @ptrToInt(sink)) {
sink.response.body.value = .{ .Empty = .{} };
}
sink.response.body.value.toError(err, sink.global);
sink.rewriter.end() catch {};
sink.deinit();
return;
};
_ = sink.runOutputSink(data, true);
}
pub fn runOutputSink(sink: *BufferOutputSink, bytes: []const u8, is_async: bool) ?JSValue {
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),
};
if (prev_value.Locked.promise) |promise| {
prev_value.Locked.promise = null;
promise.asInternalPromise().?.resolve(this.global, JSC.JSValue.fromRef(
Response.makeMaybePooled(
this.global.ref(),
this.response,
),
));
}
}
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);
}
};
};
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,
),
);
}
};