const types = @import("types.zig");
const js = @import("js_helpers.zig");
const std = types.std;
const qjs = types.qjs;
const beam = types.beam;
const e = types.e;
const lxb = @cImport(@cInclude("lexbor_bridge.h"));
// ──────────────────── JS class IDs ────────────────────
pub var document_class_id: qjs.JSClassID = 0;
pub var element_class_id: qjs.JSClassID = 0;
// ──────────────────── Opaque data attached to JS objects ────────────────────
const NodeMap = std.AutoHashMapUnmanaged(usize, qjs.JSValue);
pub const DocumentData = struct {
doc: *lxb.lxb_html_document_t,
css_parser: *lxb.lxb_css_parser_t,
selectors: *lxb.lxb_selectors_t,
node_map: NodeMap,
};
fn get_ctor_proto(ctx: *qjs.JSContext, name: [*:0]const u8) qjs.JSValue {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const ctor = qjs.JS_GetPropertyStr(ctx, global, name);
defer qjs.JS_FreeValue(ctx, ctor);
if (js.js_is_exception(ctor) or js.is_undefined(ctor)) return js.JS_UNDEFINED;
const proto = qjs.JS_GetPropertyStr(ctx, ctor, "prototype");
return proto;
}
// ──────────────────── Helpers ────────────────────
fn get_document_data(ctx: *qjs.JSContext) ?*DocumentData {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const doc_val = qjs.JS_GetPropertyStr(ctx, global, "document");
defer qjs.JS_FreeValue(ctx, doc_val);
if (!qjs.JS_IsObject(doc_val)) return null;
const ptr = qjs.JS_GetOpaque(doc_val, document_class_id);
if (ptr == null) return null;
return @ptrCast(@alignCast(ptr));
}
fn node_to_js(ctx: *qjs.JSContext, node: *lxb.lxb_dom_node_t) qjs.JSValue {
const dd = get_document_data(ctx) orelse return js.js_undefined();
const key = @intFromPtr(node);
// Return existing JS wrapper if one exists (object identity)
if (dd.node_map.get(key)) |cached| {
return qjs.JS_DupValue(ctx, cached);
}
const obj = qjs.JS_NewObjectClass(ctx, @intCast(element_class_id));
if (js.js_is_exception(obj)) return obj;
_ = qjs.JS_SetOpaque(obj, @ptrCast(node));
install_element_proto(ctx, obj);
set_node_prototype(ctx, obj, node);
// Cache with owned reference — prevents GC while node is in the map.
// Freed by evict_subtree (innerHTML/textContent) or document_finalizer.
const dup = qjs.JS_DupValue(ctx, obj);
dd.node_map.put(types.gpa, key, dup) catch {
qjs.JS_FreeValue(ctx, dup);
};
return obj;
}
fn set_node_prototype(ctx: *qjs.JSContext, obj: qjs.JSValue, node: *lxb.lxb_dom_node_t) void {
const node_type = lxb.qb_node_type(node);
const proto_name: ?[*:0]const u8 = switch (node_type) {
lxb.QB_NODE_TYPE_ELEMENT => blk: {
const elem = lxb.qb_node_as_element(node) orelse break :blk "Element";
const ns = lxb.qb_element_namespace(elem);
if (ns == lxb.QB_NS_SVG)
break :blk "SVGElement"
else if (ns == lxb.QB_NS_MATHML)
break :blk "MathMLElement"
else
break :blk "HTMLElement";
},
lxb.QB_NODE_TYPE_TEXT => "Text",
lxb.QB_NODE_TYPE_COMMENT => "Comment",
lxb.QB_NODE_TYPE_DOCUMENT_FRAGMENT => "DocumentFragment",
else => null,
};
if (proto_name) |name| {
const proto = get_ctor_proto(ctx, name);
defer qjs.JS_FreeValue(ctx, proto);
if (!js.is_undefined(proto)) {
_ = qjs.JS_SetPrototype(ctx, obj, proto);
}
}
// Set per-instance Symbol.toStringTag for HTML elements (HTMLDivElement, etc.)
if (node_type == lxb.QB_NODE_TYPE_ELEMENT) {
if (lxb.qb_node_as_element(node)) |elem| {
if (lxb.qb_element_namespace(elem) != lxb.QB_NS_SVG and
lxb.qb_element_namespace(elem) != lxb.QB_NS_MATHML)
{
var len: usize = 0;
const name = lxb.qb_element_qualified_name(elem, &len);
if (name != null and len > 0) {
const tag_str = html_element_tostring_tag(@ptrCast(name[0..len]));
set_tostring_tag(ctx, obj, tag_str);
}
}
}
}
}
fn set_tostring_tag(ctx: *qjs.JSContext, obj: qjs.JSValue, tag: []const u8) void {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const sym = qjs.JS_GetPropertyStr(ctx, global, "Symbol");
defer qjs.JS_FreeValue(ctx, sym);
const to_string_tag = qjs.JS_GetPropertyStr(ctx, sym, "toStringTag");
defer qjs.JS_FreeValue(ctx, to_string_tag);
const tag_val = qjs.JS_NewStringLen(ctx, tag.ptr, tag.len);
const atom = qjs.JS_ValueToAtom(ctx, to_string_tag);
defer qjs.JS_FreeAtom(ctx, atom);
_ = qjs.JS_DefinePropertyValue(ctx, obj, atom, tag_val, 0);
}
fn html_element_tostring_tag(tag_lower: []const u8) []const u8 {
// Map common tag names to their spec HTMLXxxElement names
if (eqlI(tag_lower, "div")) return "HTMLDivElement";
if (eqlI(tag_lower, "span")) return "HTMLSpanElement";
if (eqlI(tag_lower, "a")) return "HTMLAnchorElement";
if (eqlI(tag_lower, "p")) return "HTMLParagraphElement";
if (eqlI(tag_lower, "img")) return "HTMLImageElement";
if (eqlI(tag_lower, "input")) return "HTMLInputElement";
if (eqlI(tag_lower, "button")) return "HTMLButtonElement";
if (eqlI(tag_lower, "form")) return "HTMLFormElement";
if (eqlI(tag_lower, "table")) return "HTMLTableElement";
if (eqlI(tag_lower, "tr")) return "HTMLTableRowElement";
if (eqlI(tag_lower, "td")) return "HTMLTableCellElement";
if (eqlI(tag_lower, "th")) return "HTMLTableCellElement";
if (eqlI(tag_lower, "ul")) return "HTMLUListElement";
if (eqlI(tag_lower, "ol")) return "HTMLOListElement";
if (eqlI(tag_lower, "li")) return "HTMLLIElement";
if (eqlI(tag_lower, "select")) return "HTMLSelectElement";
if (eqlI(tag_lower, "option")) return "HTMLOptionElement";
if (eqlI(tag_lower, "textarea")) return "HTMLTextAreaElement";
if (eqlI(tag_lower, "label")) return "HTMLLabelElement";
if (eqlI(tag_lower, "h1")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "h2")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "h3")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "h4")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "h5")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "h6")) return "HTMLHeadingElement";
if (eqlI(tag_lower, "script")) return "HTMLScriptElement";
if (eqlI(tag_lower, "style")) return "HTMLStyleElement";
if (eqlI(tag_lower, "link")) return "HTMLLinkElement";
if (eqlI(tag_lower, "meta")) return "HTMLMetaElement";
if (eqlI(tag_lower, "title")) return "HTMLTitleElement";
if (eqlI(tag_lower, "body")) return "HTMLBodyElement";
if (eqlI(tag_lower, "head")) return "HTMLHeadElement";
if (eqlI(tag_lower, "html")) return "HTMLHtmlElement";
if (eqlI(tag_lower, "br")) return "HTMLBRElement";
if (eqlI(tag_lower, "hr")) return "HTMLHRElement";
if (eqlI(tag_lower, "pre")) return "HTMLPreElement";
if (eqlI(tag_lower, "iframe")) return "HTMLIFrameElement";
if (eqlI(tag_lower, "canvas")) return "HTMLCanvasElement";
if (eqlI(tag_lower, "video")) return "HTMLVideoElement";
if (eqlI(tag_lower, "audio")) return "HTMLAudioElement";
if (eqlI(tag_lower, "source")) return "HTMLSourceElement";
if (eqlI(tag_lower, "nav")) return "HTMLElement";
if (eqlI(tag_lower, "header")) return "HTMLElement";
if (eqlI(tag_lower, "footer")) return "HTMLElement";
if (eqlI(tag_lower, "main")) return "HTMLElement";
if (eqlI(tag_lower, "section")) return "HTMLElement";
if (eqlI(tag_lower, "article")) return "HTMLElement";
if (eqlI(tag_lower, "aside")) return "HTMLElement";
return "HTMLUnknownElement";
}
fn eqlI(a: []const u8, b: []const u8) bool {
if (a.len != b.len) return false;
for (a, b) |ca, cb| {
const la: u8 = if (ca >= 'A' and ca <= 'Z') ca + 32 else ca;
const lb: u8 = if (cb >= 'A' and cb <= 'Z') cb + 32 else cb;
if (la != lb) return false;
}
return true;
}
fn js_to_node(val: qjs.JSValue) ?*lxb.lxb_dom_node_t {
const ptr = qjs.JS_GetOpaque(val, element_class_id);
if (ptr == null) return null;
return @ptrCast(@alignCast(ptr));
}
fn to_lxb(s: []const u8) [*c]const lxb.lxb_char_t {
return @ptrCast(s.ptr);
}
fn str_arg(ctx: ?*qjs.JSContext, argv: [*c]qjs.JSValue, idx: usize) ?[]const u8 {
var len: usize = 0;
const ptr = qjs.JS_ToCStringLen(ctx, &len, argv[idx]);
if (ptr == null) return null;
return ptr[0..len];
}
fn free_str(ctx: ?*qjs.JSContext, ptr: [*c]const u8) void {
qjs.JS_FreeCString(ctx, ptr);
}
// ──────────────────── Serialization callback ────────────────────
fn serialize_callback(data: [*c]const u8, len: usize, ctx: ?*anyopaque) callconv(.c) lxb.lxb_status_t {
const list: *std.ArrayList(u8) = @ptrCast(@alignCast(ctx.?));
list.appendSlice(types.gpa, data[0..len]) catch return 1;
return 0;
}
// ──────────────────── document methods ────────────────────
fn doc_create_element(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "createElement requires a tag name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const tag = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid tag name");
defer free_str(ctx, tag.ptr);
const dom_doc = lxb.qb_dom_document(dd.doc);
const elem = lxb.qb_create_element(dom_doc, to_lxb(tag), tag.len) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to create element");
return node_to_js(ctx.?, lxb.qb_element_as_node(elem).?);
}
fn doc_create_text_node(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "createTextNode requires text");
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const text = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid text");
defer free_str(ctx, text.ptr);
const dom_doc = lxb.qb_dom_document(dd.doc);
const text_node = lxb.qb_create_text_node(dom_doc, to_lxb(text), text.len) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to create text node");
return node_to_js(ctx.?, lxb.qb_text_as_node(text_node).?);
}
fn doc_create_element_ns(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 2) return qjs.JS_ThrowTypeError(ctx, "createElementNS requires a namespace and a qualified name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const dom_doc = lxb.qb_dom_document(dd.doc);
// First arg: namespace URI (may be null)
var ns: ?[]const u8 = null;
if (!qjs.JS_IsNull(argv[0])) {
ns = str_arg(ctx, argv, 0);
}
defer if (ns) |s| free_str(ctx, s.ptr);
// Second arg: qualified name (e.g. "svg" or "xlink:href")
const qname = str_arg(ctx, argv, 1) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid qualified name");
defer free_str(ctx, qname.ptr);
// Split qualified name into prefix:localName
var prefix: ?[]const u8 = null;
var local_name = qname;
if (std.mem.indexOfScalar(u8, qname, ':')) |colon| {
prefix = qname[0..colon];
local_name = qname[colon + 1 ..];
}
const ns_ptr = if (ns) |s| to_lxb(s) else null;
const ns_len = if (ns) |s| s.len else 0;
const prefix_ptr = if (prefix) |p| to_lxb(p) else null;
const prefix_len = if (prefix) |p| p.len else 0;
const elem = lxb.qb_create_element_ns(
dom_doc,
to_lxb(local_name),
local_name.len,
ns_ptr,
ns_len,
prefix_ptr,
prefix_len,
) orelse return qjs.JS_ThrowTypeError(ctx, "Failed to create element");
return node_to_js(ctx.?, lxb.qb_element_as_node(elem).?);
}
fn doc_create_document_fragment(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const dom_doc = lxb.qb_dom_document(dd.doc);
const frag = lxb.qb_create_document_fragment(dom_doc) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to create document fragment");
return node_to_js(ctx.?, frag);
}
fn doc_create_comment(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const dom_doc = lxb.qb_dom_document(dd.doc);
if (argc >= 1) {
const data = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid comment data");
defer free_str(ctx, data.ptr);
const comment = lxb.qb_create_comment(dom_doc, to_lxb(data), data.len) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to create comment");
return node_to_js(ctx.?, comment);
} else {
const comment = lxb.qb_create_comment(dom_doc, to_lxb(""), 0) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to create comment");
return node_to_js(ctx.?, comment);
}
}
fn doc_get_element_by_id(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getElementById requires an id");
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const id = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid id");
defer free_str(ctx, id.ptr);
const body_node = lxb.qb_body(dd.doc) orelse return js.js_null();
const body_elem = lxb.qb_node_as_element(body_node) orelse return js.js_null();
const dom_doc = lxb.qb_dom_document(dd.doc);
const collection = lxb.qb_collection_make(dom_doc, 1) orelse return js.js_null();
defer lxb.qb_collection_destroy(collection);
const status = lxb.qb_elements_by_attr(body_elem, collection, to_lxb("id"), 2, to_lxb(id), id.len);
if (status != 0 or lxb.qb_collection_length(collection) == 0)
return js.js_null();
const elem = lxb.qb_collection_element(collection, 0);
return node_to_js(ctx.?, lxb.qb_element_as_node(elem.?).?);
}
fn doc_get_elements_by_class_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getElementsByClassName requires a class name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewArray(ctx);
const name = str_arg(ctx, argv, 0) orelse return qjs.JS_NewArray(ctx);
defer free_str(ctx, name.ptr);
const root_elem = lxb.qb_node_as_element(lxb.qb_body(dd.doc) orelse return qjs.JS_NewArray(ctx)) orelse return qjs.JS_NewArray(ctx);
return elements_by_class_name(ctx, dd, root_elem, name);
}
fn el_get_elements_by_class_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getElementsByClassName requires a class name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewArray(ctx);
const node = js_to_node(this) orelse return qjs.JS_NewArray(ctx);
const root_elem = lxb.qb_node_as_element(node) orelse return qjs.JS_NewArray(ctx);
const name = str_arg(ctx, argv, 0) orelse return qjs.JS_NewArray(ctx);
defer free_str(ctx, name.ptr);
return elements_by_class_name(ctx, dd, root_elem, name);
}
fn elements_by_class_name(ctx: ?*qjs.JSContext, dd: *DocumentData, root_elem: *lxb.lxb_dom_element_t, name: []const u8) qjs.JSValue {
const dom_doc = lxb.qb_dom_document(dd.doc);
const collection = lxb.qb_collection_make(dom_doc, 16) orelse return qjs.JS_NewArray(ctx);
defer lxb.qb_collection_destroy(collection);
_ = lxb.qb_elements_by_class_name(root_elem, collection, to_lxb(name), name.len);
return collection_to_js_array(ctx, collection);
}
fn doc_get_elements_by_tag_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getElementsByTagName requires a tag name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewArray(ctx);
const name = str_arg(ctx, argv, 0) orelse return qjs.JS_NewArray(ctx);
defer free_str(ctx, name.ptr);
const root_elem = lxb.qb_node_as_element(lxb.qb_body(dd.doc) orelse return qjs.JS_NewArray(ctx)) orelse return qjs.JS_NewArray(ctx);
return elements_by_tag_name(ctx, dd, root_elem, name);
}
fn el_get_elements_by_tag_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getElementsByTagName requires a tag name");
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewArray(ctx);
const node = js_to_node(this) orelse return qjs.JS_NewArray(ctx);
const root_elem = lxb.qb_node_as_element(node) orelse return qjs.JS_NewArray(ctx);
const name = str_arg(ctx, argv, 0) orelse return qjs.JS_NewArray(ctx);
defer free_str(ctx, name.ptr);
return elements_by_tag_name(ctx, dd, root_elem, name);
}
fn elements_by_tag_name(ctx: ?*qjs.JSContext, dd: *DocumentData, root_elem: *lxb.lxb_dom_element_t, name: []const u8) qjs.JSValue {
const dom_doc = lxb.qb_dom_document(dd.doc);
const collection = lxb.qb_collection_make(dom_doc, 16) orelse return qjs.JS_NewArray(ctx);
defer lxb.qb_collection_destroy(collection);
_ = lxb.qb_elements_by_tag_name(root_elem, collection, to_lxb(name), name.len);
return collection_to_js_array(ctx, collection);
}
fn collection_to_js_array(ctx: ?*qjs.JSContext, collection: *lxb.lxb_dom_collection_t) qjs.JSValue {
const arr = qjs.JS_NewArray(ctx);
const len = lxb.qb_collection_length(collection);
var i: usize = 0;
while (i < len) : (i += 1) {
const elem = lxb.qb_collection_element(collection, i) orelse continue;
const node = lxb.qb_element_as_node(elem) orelse continue;
_ = qjs.JS_SetPropertyUint32(ctx, arr, @intCast(i), node_to_js(ctx.?, node));
}
return arr;
}
// ──────────────────── querySelector / querySelectorAll ────────────────────
const SelectorCtx = struct {
results: *std.ArrayList(*lxb.lxb_dom_node_t),
find_one: bool,
};
fn selector_callback(node_ptr: ?*anyopaque, _: c_uint, ctx_ptr: ?*anyopaque) callconv(.c) lxb.lxb_status_t {
const sctx: *SelectorCtx = @ptrCast(@alignCast(ctx_ptr.?));
const node: *lxb.lxb_dom_node_t = @ptrCast(@alignCast(node_ptr.?));
sctx.results.append(types.gpa, node) catch return 1;
if (sctx.find_one) return 1;
return 0;
}
fn do_query_selector(ctx: ?*qjs.JSContext, root: *lxb.lxb_dom_node_t, dd: *DocumentData, selector: []const u8, find_one: bool) qjs.JSValue {
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return if (find_one) js.js_null() else qjs.JS_NewArray(ctx);
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
var sctx = SelectorCtx{ .results = &results, .find_one = find_one };
_ = lxb.qb_selectors_find(dd.selectors, root, list, selector_callback, @ptrCast(&sctx));
if (find_one) {
defer results.deinit(types.gpa);
if (results.items.len == 0) return js.js_null();
return node_to_js(ctx.?, results.items[0]);
}
return make_owned_node_list(ctx.?, results);
}
fn make_owned_node_list(ctx: *qjs.JSContext, nodes: std.ArrayList(*lxb.lxb_dom_node_t)) qjs.JSValue {
const arr = qjs.JS_NewArray(ctx);
if (js.js_is_exception(arr)) {
var mut_nodes = nodes;
mut_nodes.deinit(types.gpa);
return arr;
}
for (nodes.items, 0..) |node, i| {
const elem_js = node_to_js(ctx, node);
_ = qjs.JS_SetPropertyUint32(ctx, arr, @intCast(i), elem_js);
}
var mut_nodes = nodes;
mut_nodes.deinit(types.gpa);
return arr;
}
fn query_selector(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "querySelector requires a selector");
const selector = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid selector");
defer free_str(ctx, selector.ptr);
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const root = js_to_node(this) orelse (lxb.qb_doc_as_node(dd.doc) orelse return js.js_null());
return do_query_selector(ctx, root, dd, selector, true);
}
fn query_selector_all(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "querySelectorAll requires a selector");
const selector = str_arg(ctx, argv, 0) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid selector");
defer free_str(ctx, selector.ptr);
const dd = get_document_data(ctx.?) orelse return qjs.JS_ThrowTypeError(ctx, "No document");
const root = js_to_node(this) orelse (lxb.qb_doc_as_node(dd.doc) orelse return qjs.JS_NewArray(ctx));
return do_query_selector(ctx, root, dd, selector, false);
}
// ──────────────────── Element property accessors ────────────────────
fn el_get_tag_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
if (lxb.qb_node_type(node) != lxb.QB_NODE_TYPE_ELEMENT) return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
return element_tag_name(ctx.?, elem);
}
fn element_tag_name(ctx: *qjs.JSContext, elem: *lxb.lxb_dom_element_t) qjs.JSValue {
var len: usize = 0;
const name = lxb.qb_element_qualified_name(elem, &len);
if (name == null) return js.js_undefined();
const ns = lxb.qb_element_namespace(elem);
if (ns == lxb.QB_NS_HTML or ns == 0) {
var buf: [256]u8 = undefined;
const src: [*]const u8 = @ptrCast(name);
if (len <= buf.len) {
for (0..len) |i| {
const c = src[i];
buf[i] = if (c >= 'a' and c <= 'z') c - 32 else c;
}
return qjs.JS_NewStringLen(ctx, &buf, len);
}
}
return qjs.JS_NewStringLen(ctx, @ptrCast(name), len);
}
fn el_get_id(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
var len: usize = 0;
const val = lxb.qb_element_get_attribute(elem, to_lxb("id"), 2, &len);
if (val == null) return qjs.JS_NewStringLen(ctx, "", 0);
return qjs.JS_NewStringLen(ctx, @ptrCast(val), len);
}
fn el_set_id(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const val = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, val.ptr);
_ = lxb.qb_element_set_attribute(elem, to_lxb("id"), 2, to_lxb(val), val.len);
return js.js_undefined();
}
fn el_get_class_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
var len: usize = 0;
const val = lxb.qb_element_get_attribute(elem, to_lxb("class"), 5, &len);
if (val == null) return qjs.JS_NewStringLen(ctx, "", 0);
return qjs.JS_NewStringLen(ctx, @ptrCast(val), len);
}
fn el_get_attribute(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "getAttribute requires a name");
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const name = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, name.ptr);
var len: usize = 0;
const val = lxb.qb_element_get_attribute(elem, to_lxb(name), name.len, &len);
if (val == null) return js.js_null();
return qjs.JS_NewStringLen(ctx, @ptrCast(val), len);
}
fn el_set_attribute(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 2) return qjs.JS_ThrowTypeError(ctx, "setAttribute requires name and value");
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const name = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, name.ptr);
const val = str_arg(ctx, argv, 1) orelse return js.js_undefined();
defer free_str(ctx, val.ptr);
_ = lxb.qb_element_set_attribute(elem, to_lxb(name), name.len, to_lxb(val), val.len);
return js.js_undefined();
}
fn el_remove_attribute(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "removeAttribute requires a name");
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const name = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, name.ptr);
_ = lxb.qb_element_remove_attribute(elem, to_lxb(name), name.len);
return js.js_undefined();
}
fn el_has_attribute(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "hasAttribute requires a name");
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const name = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, name.ptr);
var len: usize = 0;
const val = lxb.qb_element_get_attribute(elem, to_lxb(name), name.len, &len);
return if (val != null) js.js_true() else js.js_false();
}
// ──────────────────── Tree manipulation ────────────────────
fn el_append_child(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "appendChild requires a node");
const parent = js_to_node(this) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid parent");
const child = js_to_node(argv[0]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid child");
if (lxb.qb_node_type(child) == lxb.QB_NODE_TYPE_DOCUMENT_FRAGMENT) {
var c: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(child);
while (c) |node| {
const next = lxb.qb_node_next(node);
lxb.qb_node_insert_child(parent, node);
c = next;
}
} else {
if (lxb.qb_node_parent(child) != null) lxb.qb_node_remove(child);
lxb.qb_node_insert_child(parent, child);
}
return qjs.JS_DupValue(ctx, argv[0]);
}
fn el_remove_child(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = this;
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "removeChild requires a node");
const child = js_to_node(argv[0]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid child");
lxb.qb_node_remove(child);
return qjs.JS_DupValue(ctx, argv[0]);
}
fn el_insert_before(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 2) return qjs.JS_ThrowTypeError(ctx, "insertBefore requires 2 arguments");
const parent = js_to_node(this) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid parent");
const new_node = js_to_node(argv[0]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid new node");
if (lxb.qb_node_type(new_node) == lxb.QB_NODE_TYPE_DOCUMENT_FRAGMENT) {
var c: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(new_node);
if (qjs.JS_IsNull(argv[1])) {
while (c) |node| {
const next = lxb.qb_node_next(node);
lxb.qb_node_insert_child(parent, node);
c = next;
}
} else {
const ref_node = js_to_node(argv[1]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid reference node");
while (c) |node| {
const next = lxb.qb_node_next(node);
lxb.qb_node_insert_before(ref_node, node);
c = next;
}
}
} else {
if (qjs.JS_IsNull(argv[1])) {
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_child(parent, new_node);
} else {
const ref_node = js_to_node(argv[1]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid reference node");
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_before(ref_node, new_node);
}
}
return qjs.JS_DupValue(ctx, argv[0]);
}
fn el_replace_child(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 2) return qjs.JS_ThrowTypeError(ctx, "replaceChild requires 2 arguments");
_ = this;
const new_node = js_to_node(argv[0]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid new node");
const old_node = js_to_node(argv[1]) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid old node");
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_before(old_node, new_node);
lxb.qb_node_remove(old_node);
return qjs.JS_DupValue(ctx, argv[1]);
}
fn el_clone_node(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return qjs.JS_ThrowTypeError(ctx, "Invalid node");
var deep: c_int = 0;
if (argc >= 1) {
deep = if (qjs.JS_ToBool(ctx, argv[0]) != 0) 1 else 0;
}
const cloned = lxb.qb_node_clone(node, deep) orelse
return qjs.JS_ThrowTypeError(ctx, "Failed to clone node");
return node_to_js(ctx.?, cloned);
}
fn el_contains(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1 or qjs.JS_IsNull(argv[0])) return js.js_false();
const node = js_to_node(this) orelse return js.js_false();
const other = js_to_node(argv[0]) orelse return js.js_false();
_ = ctx;
if (node == other) return js.js_true();
var current: ?*lxb.lxb_dom_node_t = lxb.qb_node_parent(other);
while (current) |c| {
if (c == node) return js.js_true();
current = lxb.qb_node_parent(c);
}
return js.js_false();
}
fn el_remove(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
_ = ctx;
const node = js_to_node(this) orelse return js.js_undefined();
lxb.qb_node_remove(node);
return js.js_undefined();
}
fn el_before(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var i: usize = 0;
while (i < @as(usize, @intCast(argc))) : (i += 1) {
const new_node = js_to_node(argv[i]) orelse {
const text = str_arg(ctx, argv, i) orelse continue;
defer free_str(ctx, text.ptr);
const dd = get_document_data(ctx.?) orelse continue;
const text_node = lxb.qb_create_text_node(lxb.qb_dom_document(dd.doc), to_lxb(text), text.len) orelse continue;
lxb.qb_node_insert_before(node, lxb.qb_text_as_node(text_node).?);
continue;
};
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_before(node, new_node);
}
return js.js_undefined();
}
fn el_after(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var ref = node;
var i: usize = 0;
while (i < @as(usize, @intCast(argc))) : (i += 1) {
const new_node = js_to_node(argv[i]) orelse {
const text = str_arg(ctx, argv, i) orelse continue;
defer free_str(ctx, text.ptr);
const dd = get_document_data(ctx.?) orelse continue;
const text_node = lxb.qb_create_text_node(lxb.qb_dom_document(dd.doc), to_lxb(text), text.len) orelse continue;
const tn = lxb.qb_text_as_node(text_node).?;
lxb.qb_node_insert_after(ref, tn);
ref = tn;
continue;
};
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_after(ref, new_node);
ref = new_node;
}
return js.js_undefined();
}
fn el_prepend(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const parent = js_to_node(this) orelse return js.js_undefined();
const first = lxb.qb_node_first_child(parent);
var i: usize = @intCast(argc);
while (i > 0) {
i -= 1;
const new_node = js_to_node(argv[i]) orelse {
const text = str_arg(ctx, argv, i) orelse continue;
defer free_str(ctx, text.ptr);
const dd = get_document_data(ctx.?) orelse continue;
const text_node = lxb.qb_create_text_node(lxb.qb_dom_document(dd.doc), to_lxb(text), text.len) orelse continue;
const tn = lxb.qb_text_as_node(text_node).?;
if (first) |f| {
lxb.qb_node_insert_before(f, tn);
} else {
lxb.qb_node_insert_child(parent, tn);
}
continue;
};
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
if (first) |f| {
lxb.qb_node_insert_before(f, new_node);
} else {
lxb.qb_node_insert_child(parent, new_node);
}
}
return js.js_undefined();
}
fn el_append(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const parent = js_to_node(this) orelse return js.js_undefined();
var i: usize = 0;
while (i < @as(usize, @intCast(argc))) : (i += 1) {
const new_node = js_to_node(argv[i]) orelse {
const text = str_arg(ctx, argv, i) orelse continue;
defer free_str(ctx, text.ptr);
const dd = get_document_data(ctx.?) orelse continue;
const text_node = lxb.qb_create_text_node(lxb.qb_dom_document(dd.doc), to_lxb(text), text.len) orelse continue;
lxb.qb_node_insert_child(parent, lxb.qb_text_as_node(text_node).?);
continue;
};
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_child(parent, new_node);
}
return js.js_undefined();
}
fn el_replace_with(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var i: usize = 0;
while (i < @as(usize, @intCast(argc))) : (i += 1) {
const new_node = js_to_node(argv[i]) orelse {
const text = str_arg(ctx, argv, i) orelse continue;
defer free_str(ctx, text.ptr);
const dd = get_document_data(ctx.?) orelse continue;
const text_node = lxb.qb_create_text_node(lxb.qb_dom_document(dd.doc), to_lxb(text), text.len) orelse continue;
lxb.qb_node_insert_before(node, lxb.qb_text_as_node(text_node).?);
continue;
};
if (lxb.qb_node_parent(new_node) != null) lxb.qb_node_remove(new_node);
lxb.qb_node_insert_before(node, new_node);
}
lxb.qb_node_remove(node);
return js.js_undefined();
}
fn el_matches(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "matches requires a selector");
const node = js_to_node(this) orelse return js.js_false();
const dd = get_document_data(ctx.?) orelse return js.js_false();
const selector = str_arg(ctx, argv, 0) orelse return js.js_false();
defer free_str(ctx, selector.ptr);
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return js.js_false();
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
defer results.deinit(types.gpa);
const parent = lxb.qb_node_parent(node) orelse return js.js_false();
var sctx = SelectorCtx{ .results = &results, .find_one = false };
_ = lxb.qb_selectors_find(dd.selectors, parent, list, selector_callback, @ptrCast(&sctx));
for (results.items) |r| {
if (r == node) return js.js_true();
}
return js.js_false();
}
fn el_closest(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_ThrowTypeError(ctx, "closest requires a selector");
const dd = get_document_data(ctx.?) orelse return js.js_null();
const selector = str_arg(ctx, argv, 0) orelse return js.js_null();
defer free_str(ctx, selector.ptr);
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return js.js_null();
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
const root = lxb.qb_doc_as_node(dd.doc) orelse return js.js_null();
var current: ?*lxb.lxb_dom_node_t = js_to_node(this);
while (current) |c| {
if (lxb.qb_node_type(c) == lxb.QB_NODE_TYPE_ELEMENT) {
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
defer results.deinit(types.gpa);
var sctx = SelectorCtx{ .results = &results, .find_one = false };
_ = lxb.qb_selectors_find(dd.selectors, root, list, selector_callback, @ptrCast(&sctx));
for (results.items) |r| {
if (r == c) return node_to_js(ctx.?, c);
}
}
current = lxb.qb_node_parent(c);
}
return js.js_null();
}
fn el_get_inner_html(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var buf = std.ArrayList(u8){};
defer buf.deinit(types.gpa);
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (child) |c| {
_ = lxb.qb_serialize_tree(c, serialize_callback, @ptrCast(&buf));
child = lxb.qb_node_next(c);
}
return qjs.JS_NewStringLen(ctx, @ptrCast(buf.items.ptr), buf.items.len);
}
fn el_set_inner_html(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const dd = get_document_data(ctx.?) orelse return js.js_undefined();
const html_str = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, html_str.ptr);
remove_all_children(ctx.?, node);
// Parse fragment
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const frag_node = lxb.qb_parse_fragment(dd.doc, elem, to_lxb(html_str), html_str.len) orelse return js.js_undefined();
// Move children from fragment to node
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(frag_node);
while (child) |c| {
const next = lxb.qb_node_next(c);
lxb.qb_node_insert_child(node, c);
child = next;
}
return js.js_undefined();
}
fn el_get_outer_html(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var buf = std.ArrayList(u8){};
defer buf.deinit(types.gpa);
_ = lxb.qb_serialize_tree(node, serialize_callback, @ptrCast(&buf));
return qjs.JS_NewStringLen(ctx, @ptrCast(buf.items.ptr), buf.items.len);
}
fn el_get_text_content(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
var len: usize = 0;
const text = lxb.qb_node_text_content(node, &len);
if (text == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_dom_document_destroy_text(lxb.qb_node_owner_document(node), @constCast(text));
return qjs.JS_NewStringLen(ctx, @ptrCast(text), len);
}
fn el_set_text_content(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const text = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, text.ptr);
remove_all_children(ctx.?, node);
if (text.len > 0) {
_ = lxb.qb_node_text_content_set(node, to_lxb(text), text.len);
}
return js.js_undefined();
}
fn remove_all_children(ctx: *qjs.JSContext, node: *lxb.lxb_dom_node_t) void {
const dd = get_document_data(ctx);
while (lxb.qb_node_first_child(node)) |child| {
if (dd) |d| evict_subtree(ctx, d, child);
lxb.qb_node_remove(child);
}
}
fn evict_subtree(ctx: *qjs.JSContext, dd: *DocumentData, node: *lxb.lxb_dom_node_t) void {
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (child) |c| {
evict_subtree(ctx, dd, c);
child = lxb.qb_node_next(c);
}
if (dd.node_map.fetchRemove(@intFromPtr(node))) |kv| {
qjs.JS_FreeValue(ctx, kv.value);
}
}
// ──────────────────── Tree navigation ────────────────────
fn el_get_parent_node(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const parent = lxb.qb_node_parent(node) orelse return js.js_null();
return node_to_js(ctx.?, parent);
}
fn el_get_children(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return qjs.JS_NewArray(ctx);
const arr = qjs.JS_NewArray(ctx);
var idx: u32 = 0;
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (child) |c| {
if (lxb.qb_node_type(c) == lxb.QB_NODE_TYPE_ELEMENT) {
_ = qjs.JS_SetPropertyUint32(ctx, arr, idx, node_to_js(ctx.?, c));
idx += 1;
}
child = lxb.qb_node_next(c);
}
return arr;
}
fn el_get_child_nodes(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return qjs.JS_NewArray(ctx);
const arr = qjs.JS_NewArray(ctx);
var idx: u32 = 0;
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (child) |c| {
_ = qjs.JS_SetPropertyUint32(ctx, arr, idx, node_to_js(ctx.?, c));
idx += 1;
child = lxb.qb_node_next(c);
}
return arr;
}
fn el_get_first_child(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const first = lxb.qb_node_first_child(node) orelse return js.js_null();
return node_to_js(ctx.?, first);
}
fn el_get_next_sibling(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const next = lxb.qb_node_next(node) orelse return js.js_null();
return node_to_js(ctx.?, next);
}
fn el_get_last_child(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const last = lxb.qb_node_last_child(node) orelse return js.js_null();
return node_to_js(ctx.?, last);
}
fn el_get_previous_sibling(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const prev = lxb.qb_node_prev(node) orelse return js.js_null();
return node_to_js(ctx.?, prev);
}
fn el_get_node_type(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
return qjs.JS_NewInt32(ctx, @intCast(lxb.qb_node_type(node)));
}
fn el_get_node_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const nt = lxb.qb_node_type(node);
if (nt == lxb.QB_NODE_TYPE_ELEMENT) {
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
return element_tag_name(ctx.?, elem);
} else if (nt == lxb.QB_NODE_TYPE_TEXT) {
return qjs.JS_NewString(ctx, "#text");
} else if (nt == lxb.QB_NODE_TYPE_COMMENT) {
return qjs.JS_NewString(ctx, "#comment");
} else if (nt == lxb.QB_NODE_TYPE_DOCUMENT) {
return qjs.JS_NewString(ctx, "#document");
} else if (nt == lxb.QB_NODE_TYPE_DOCUMENT_FRAGMENT) {
return qjs.JS_NewString(ctx, "#document-fragment");
}
return js.js_undefined();
}
fn el_get_parent_element(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const parent = lxb.qb_node_parent(node) orelse return js.js_null();
if (lxb.qb_node_type(parent) != lxb.QB_NODE_TYPE_ELEMENT) return js.js_null();
return node_to_js(ctx.?, parent);
}
fn el_set_class_name(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_undefined();
const elem = lxb.qb_node_as_element(node) orelse return js.js_undefined();
const val = str_arg(ctx, argv, 0) orelse return js.js_undefined();
defer free_str(ctx, val.ptr);
_ = lxb.qb_element_set_attribute(elem, to_lxb("class"), 5, to_lxb(val), val.len);
return js.js_undefined();
}
// ──────────────────── CSS style helpers (called from JS CSSStyleDeclaration) ────────────────────
fn css_get_property(ctx: ?*qjs.JSContext, _: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 2) return qjs.JS_NewStringLen(ctx, "", 0);
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewStringLen(ctx, "", 0);
const style_str = str_arg(ctx, argv, 0) orelse return qjs.JS_NewStringLen(ctx, "", 0);
defer free_str(ctx, style_str.ptr);
const prop_name = str_arg(ctx, argv, 1) orelse return qjs.JS_NewStringLen(ctx, "", 0);
defer free_str(ctx, prop_name.ptr);
const decls = lxb.qb_css_parse_declarations(dd.css_parser, to_lxb(style_str), style_str.len);
if (decls == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_declarations_destroy(decls);
var out_len: usize = 0;
const result = lxb.qb_css_declaration_get_property(decls, to_lxb(prop_name), prop_name.len, &out_len);
if (result == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_free_string(result);
return qjs.JS_NewStringLen(ctx, result, out_len);
}
fn css_get_priority(ctx: ?*qjs.JSContext, _: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 2) return qjs.JS_NewStringLen(ctx, "", 0);
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewStringLen(ctx, "", 0);
const style_str = str_arg(ctx, argv, 0) orelse return qjs.JS_NewStringLen(ctx, "", 0);
defer free_str(ctx, style_str.ptr);
const prop_name = str_arg(ctx, argv, 1) orelse return qjs.JS_NewStringLen(ctx, "", 0);
defer free_str(ctx, prop_name.ptr);
const decls = lxb.qb_css_parse_declarations(dd.css_parser, to_lxb(style_str), style_str.len);
if (decls == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_declarations_destroy(decls);
var out_len: usize = 0;
const result = lxb.qb_css_declaration_get_priority(decls, to_lxb(prop_name), prop_name.len, &out_len);
if (result == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_free_string(result);
return qjs.JS_NewStringLen(ctx, result, out_len);
}
fn css_serialize_declarations(ctx: ?*qjs.JSContext, _: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
if (argc < 1) return qjs.JS_NewStringLen(ctx, "", 0);
const dd = get_document_data(ctx.?) orelse return qjs.JS_NewStringLen(ctx, "", 0);
const style_str = str_arg(ctx, argv, 0) orelse return qjs.JS_NewStringLen(ctx, "", 0);
defer free_str(ctx, style_str.ptr);
const decls = lxb.qb_css_parse_declarations(dd.css_parser, to_lxb(style_str), style_str.len);
if (decls == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_declarations_destroy(decls);
var out_len: usize = 0;
const result = lxb.qb_css_declarations_serialize(decls, &out_len);
if (result == null) return qjs.JS_NewStringLen(ctx, "", 0);
defer lxb.qb_css_free_string(result);
return qjs.JS_NewStringLen(ctx, result, out_len);
}
// ──────────────────── EventTarget methods (delegate to JS helpers) ────────────────────
fn call_global_helper(ctx: ?*qjs.JSContext, this: qjs.JSValue, name: [*:0]const u8, argc: c_int, argv: [*c]qjs.JSValue) qjs.JSValue {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const helper = qjs.JS_GetPropertyStr(ctx, global, name);
defer qjs.JS_FreeValue(ctx, helper);
if (!qjs.JS_IsFunction(ctx, helper)) return js.js_undefined();
return qjs.JS_Call(ctx, helper, this, argc, argv);
}
fn el_add_event_listener(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return call_global_helper(ctx, this, "__qb_addEventListener", argc, argv);
}
fn el_remove_event_listener(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return call_global_helper(ctx, this, "__qb_removeEventListener", argc, argv);
}
fn el_dispatch_event(ctx: ?*qjs.JSContext, this: qjs.JSValue, argc: c_int, argv: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return call_global_helper(ctx, this, "__qb_dispatchEvent", argc, argv);
}
fn el_get_style(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const helper = qjs.JS_GetPropertyStr(ctx, global, "__qb_get_style");
defer qjs.JS_FreeValue(ctx, helper);
if (!qjs.JS_IsFunction(ctx, helper)) return qjs.JS_NewObject(ctx);
var args = [_]qjs.JSValue{this};
return qjs.JS_Call(ctx, helper, global, 1, &args);
}
fn el_get_class_list(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const global = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, global);
const helper = qjs.JS_GetPropertyStr(ctx, global, "__qb_get_class_list");
defer qjs.JS_FreeValue(ctx, helper);
if (!qjs.JS_IsFunction(ctx, helper)) return qjs.JS_NewObject(ctx);
var args = [_]qjs.JSValue{this};
const result = qjs.JS_Call(ctx, helper, global, 1, &args);
return result;
}
fn el_get_node_value(ctx: ?*qjs.JSContext, this: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const node = js_to_node(this) orelse return js.js_null();
const nt = lxb.qb_node_type(node);
if (nt == lxb.QB_NODE_TYPE_TEXT or nt == lxb.QB_NODE_TYPE_COMMENT) {
var len: usize = 0;
const text = lxb.qb_node_text_content(node, &len);
if (text == null) return js.js_null();
defer lxb.qb_dom_document_destroy_text(lxb.qb_node_owner_document(node), @constCast(text));
return qjs.JS_NewStringLen(ctx, @ptrCast(text), len);
}
return js.js_null();
}
// ──────────────────── document.body / document.head / document.documentElement ────────────────────
fn doc_get_body(ctx: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const dd = get_document_data(ctx.?) orelse return js.js_null();
const body = lxb.qb_body(dd.doc) orelse return js.js_null();
return node_to_js(ctx.?, body);
}
fn doc_get_head(ctx: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const dd = get_document_data(ctx.?) orelse return js.js_null();
const head = lxb.qb_head(dd.doc) orelse return js.js_null();
return node_to_js(ctx.?, head);
}
fn doc_get_document_element(ctx: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
const dd = get_document_data(ctx.?) orelse return js.js_null();
const root = lxb.qb_document_element(dd.doc) orelse return js.js_null();
return node_to_js(ctx.?, root);
}
fn doc_get_node_type(ctx: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return qjs.JS_NewInt32(ctx, 9);
}
fn doc_get_node_name(ctx: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return qjs.JS_NewString(ctx, "#document");
}
// ──────────────────── Install element prototype methods ────────────────────
fn define_getter(ctx: *qjs.JSContext, obj: qjs.JSValue, name: [*:0]const u8, getter: *const qjs.JSCFunction) void {
const atom = qjs.JS_NewAtom(ctx, name);
defer qjs.JS_FreeAtom(ctx, atom);
_ = qjs.JS_DefinePropertyGetSet(
ctx,
obj,
atom,
qjs.JS_NewCFunction(ctx, getter, name, 0),
js.js_undefined(),
qjs.JS_PROP_HAS_GET | qjs.JS_PROP_CONFIGURABLE,
);
}
fn define_getter_setter(ctx: *qjs.JSContext, obj: qjs.JSValue, name: [*:0]const u8, getter: *const qjs.JSCFunction, setter: *const qjs.JSCFunction) void {
const atom = qjs.JS_NewAtom(ctx, name);
defer qjs.JS_FreeAtom(ctx, atom);
_ = qjs.JS_DefinePropertyGetSet(
ctx,
obj,
atom,
qjs.JS_NewCFunction(ctx, getter, name, 0),
qjs.JS_NewCFunction(ctx, setter, name, 1),
qjs.JS_PROP_HAS_GET | qjs.JS_PROP_HAS_SET | qjs.JS_PROP_CONFIGURABLE,
);
}
fn install_element_proto(ctx: *qjs.JSContext, obj: qjs.JSValue) void {
// Query methods
_ = qjs.JS_SetPropertyStr(ctx, obj, "querySelector", qjs.JS_NewCFunction(ctx, &query_selector, "querySelector", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "querySelectorAll", qjs.JS_NewCFunction(ctx, &query_selector_all, "querySelectorAll", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "getElementsByClassName", qjs.JS_NewCFunction(ctx, &el_get_elements_by_class_name, "getElementsByClassName", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "getElementsByTagName", qjs.JS_NewCFunction(ctx, &el_get_elements_by_tag_name, "getElementsByTagName", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "matches", qjs.JS_NewCFunction(ctx, &el_matches, "matches", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "closest", qjs.JS_NewCFunction(ctx, &el_closest, "closest", 1));
// Attribute methods
_ = qjs.JS_SetPropertyStr(ctx, obj, "getAttribute", qjs.JS_NewCFunction(ctx, &el_get_attribute, "getAttribute", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "setAttribute", qjs.JS_NewCFunction(ctx, &el_set_attribute, "setAttribute", 2));
_ = qjs.JS_SetPropertyStr(ctx, obj, "removeAttribute", qjs.JS_NewCFunction(ctx, &el_remove_attribute, "removeAttribute", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "hasAttribute", qjs.JS_NewCFunction(ctx, &el_has_attribute, "hasAttribute", 1));
// Tree manipulation
_ = qjs.JS_SetPropertyStr(ctx, obj, "appendChild", qjs.JS_NewCFunction(ctx, &el_append_child, "appendChild", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "removeChild", qjs.JS_NewCFunction(ctx, &el_remove_child, "removeChild", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "insertBefore", qjs.JS_NewCFunction(ctx, &el_insert_before, "insertBefore", 2));
_ = qjs.JS_SetPropertyStr(ctx, obj, "replaceChild", qjs.JS_NewCFunction(ctx, &el_replace_child, "replaceChild", 2));
_ = qjs.JS_SetPropertyStr(ctx, obj, "cloneNode", qjs.JS_NewCFunction(ctx, &el_clone_node, "cloneNode", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "contains", qjs.JS_NewCFunction(ctx, &el_contains, "contains", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "remove", qjs.JS_NewCFunction(ctx, &el_remove, "remove", 0));
_ = qjs.JS_SetPropertyStr(ctx, obj, "before", qjs.JS_NewCFunction(ctx, &el_before, "before", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "after", qjs.JS_NewCFunction(ctx, &el_after, "after", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "prepend", qjs.JS_NewCFunction(ctx, &el_prepend, "prepend", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "append", qjs.JS_NewCFunction(ctx, &el_append, "append", 1));
_ = qjs.JS_SetPropertyStr(ctx, obj, "replaceWith", qjs.JS_NewCFunction(ctx, &el_replace_with, "replaceWith", 1));
// EventTarget
_ = qjs.JS_SetPropertyStr(ctx, obj, "addEventListener", qjs.JS_NewCFunction(ctx, &el_add_event_listener, "addEventListener", 2));
_ = qjs.JS_SetPropertyStr(ctx, obj, "removeEventListener", qjs.JS_NewCFunction(ctx, &el_remove_event_listener, "removeEventListener", 2));
_ = qjs.JS_SetPropertyStr(ctx, obj, "dispatchEvent", qjs.JS_NewCFunction(ctx, &el_dispatch_event, "dispatchEvent", 1));
// classList — delegates to JS-side DOMTokenList via __qb_get_class_list
define_getter(ctx, obj, "classList", &el_get_class_list);
// style — delegates to JS-side CSSStyleDeclaration via __qb_get_style
define_getter(ctx, obj, "style", &el_get_style);
// Read-only properties
define_getter(ctx, obj, "tagName", &el_get_tag_name);
define_getter(ctx, obj, "nodeName", &el_get_node_name);
define_getter(ctx, obj, "nodeType", &el_get_node_type);
define_getter(ctx, obj, "nodeValue", &el_get_node_value);
define_getter(ctx, obj, "outerHTML", &el_get_outer_html);
define_getter(ctx, obj, "parentNode", &el_get_parent_node);
define_getter(ctx, obj, "parentElement", &el_get_parent_element);
define_getter(ctx, obj, "children", &el_get_children);
define_getter(ctx, obj, "childNodes", &el_get_child_nodes);
define_getter(ctx, obj, "firstChild", &el_get_first_child);
define_getter(ctx, obj, "lastChild", &el_get_last_child);
define_getter(ctx, obj, "nextSibling", &el_get_next_sibling);
define_getter(ctx, obj, "previousSibling", &el_get_previous_sibling);
// Read-write properties
define_getter_setter(ctx, obj, "id", &el_get_id, &el_set_id);
define_getter_setter(ctx, obj, "className", &el_get_class_name, &el_set_class_name);
define_getter_setter(ctx, obj, "innerHTML", &el_get_inner_html, &el_set_inner_html);
define_getter_setter(ctx, obj, "textContent", &el_get_text_content, &el_set_text_content);
}
// ──────────────────── Class finalizers ────────────────────
fn document_finalizer(rt: ?*qjs.JSRuntime, val: qjs.JSValue) callconv(.c) void {
const ptr = qjs.JS_GetOpaque(val, document_class_id);
if (ptr == null) return;
const dd: *DocumentData = @ptrCast(@alignCast(ptr));
var it = dd.node_map.iterator();
while (it.next()) |entry| {
qjs.JS_FreeValueRT(rt, entry.value_ptr.*);
}
dd.node_map.deinit(types.gpa);
lxb.qb_selectors_destroy(dd.selectors);
lxb.qb_css_parser_destroy(dd.css_parser);
_ = lxb.qb_document_destroy(dd.doc);
types.gpa.destroy(dd);
}
const document_class_def = qjs.JSClassDef{
.class_name = "Document",
.finalizer = &document_finalizer,
.gc_mark = &document_gc_mark,
.call = null,
.exotic = null,
};
fn document_gc_mark(rt: ?*qjs.JSRuntime, val: qjs.JSValue, mark_func: ?*const qjs.JS_MarkFunc) callconv(.c) void {
const ptr = qjs.JS_GetOpaque(val, document_class_id);
if (ptr == null) return;
const dd: *DocumentData = @ptrCast(@alignCast(ptr));
var it = dd.node_map.iterator();
while (it.next()) |entry| {
qjs.JS_MarkValue(rt, entry.value_ptr.*, mark_func);
}
}
const element_class_def = qjs.JSClassDef{
.class_name = "Element",
.finalizer = null,
.gc_mark = null,
.call = null,
.exotic = null,
};
// ──────────────────── Public: install DOM globals ────────────────────
fn dom_constructor_stub(_: ?*qjs.JSContext, _: qjs.JSValue, _: c_int, _: [*c]qjs.JSValue) callconv(.c) qjs.JSValue {
return js.JS_UNDEFINED;
}
fn make_dom_ctor(ctx: *qjs.JSContext, global: qjs.JSValue, name: [*:0]const u8, parent_proto: qjs.JSValue) qjs.JSValue {
const proto = qjs.JS_NewObject(ctx);
if (!js.is_undefined(parent_proto)) {
_ = qjs.JS_SetPrototype(ctx, proto, parent_proto);
}
set_tostring_tag(ctx, proto, std.mem.span(name));
const ctor = qjs.JS_NewCFunction2(ctx, &dom_constructor_stub, name, 0, qjs.JS_CFUNC_constructor, 0);
_ = qjs.JS_SetPropertyStr(ctx, ctor, "prototype", qjs.JS_DupValue(ctx, proto));
_ = qjs.JS_SetPropertyStr(ctx, proto, "constructor", qjs.JS_DupValue(ctx, ctor));
_ = qjs.JS_SetPropertyStr(ctx, global, name, ctor);
return proto;
}
fn install_dom_constructors(ctx: *qjs.JSContext, global: qjs.JSValue) void {
const node_p = make_dom_ctor(ctx, global, "Node", js.JS_UNDEFINED);
defer qjs.JS_FreeValue(ctx, node_p);
const element_p = make_dom_ctor(ctx, global, "Element", node_p);
defer qjs.JS_FreeValue(ctx, element_p);
const html_p = make_dom_ctor(ctx, global, "HTMLElement", element_p);
qjs.JS_FreeValue(ctx, html_p);
const svg_p = make_dom_ctor(ctx, global, "SVGElement", element_p);
qjs.JS_FreeValue(ctx, svg_p);
const mathml_p = make_dom_ctor(ctx, global, "MathMLElement", element_p);
qjs.JS_FreeValue(ctx, mathml_p);
const doc_p = make_dom_ctor(ctx, global, "Document", node_p);
qjs.JS_FreeValue(ctx, doc_p);
const frag_p = make_dom_ctor(ctx, global, "DocumentFragment", node_p);
qjs.JS_FreeValue(ctx, frag_p);
const text_p = make_dom_ctor(ctx, global, "Text", node_p);
qjs.JS_FreeValue(ctx, text_p);
const comment_p = make_dom_ctor(ctx, global, "Comment", node_p);
qjs.JS_FreeValue(ctx, comment_p);
}
pub fn install(ctx: *qjs.JSContext, global: qjs.JSValue) ?*DocumentData {
const rt = qjs.JS_GetRuntime(ctx);
// class IDs allocated under shared types.class_ids_mutex in worker.zig
_ = qjs.JS_NewClass(rt, document_class_id, &document_class_def);
_ = qjs.JS_NewClass(rt, element_class_id, &element_class_def);
install_dom_constructors(ctx, global);
const doc = lxb.qb_document_create() orelse return null;
const html = "<!DOCTYPE html><html><head></head><body></body></html>";
if (lxb.qb_document_parse(doc, html, html.len) != 0) {
_ = lxb.qb_document_destroy(doc);
return null;
}
const css_parser = lxb.qb_css_parser_create() orelse {
_ = lxb.qb_document_destroy(doc);
return null;
};
const selectors = lxb.qb_selectors_create() orelse {
lxb.qb_css_parser_destroy(css_parser);
_ = lxb.qb_document_destroy(doc);
return null;
};
const dd = types.gpa.create(DocumentData) catch return null;
dd.* = .{ .doc = doc, .css_parser = css_parser, .selectors = selectors, .node_map = .{} };
const doc_obj = qjs.JS_NewObjectClass(ctx, @intCast(document_class_id));
if (js.js_is_exception(doc_obj)) return null;
_ = qjs.JS_SetOpaque(doc_obj, @ptrCast(dd));
{
const doc_proto = get_ctor_proto(ctx, "Document");
defer qjs.JS_FreeValue(ctx, doc_proto);
if (!js.is_undefined(doc_proto)) {
_ = qjs.JS_SetPrototype(ctx, doc_obj, doc_proto);
}
}
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "createElement", qjs.JS_NewCFunction(ctx, &doc_create_element, "createElement", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "createElementNS", qjs.JS_NewCFunction(ctx, &doc_create_element_ns, "createElementNS", 2));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "createTextNode", qjs.JS_NewCFunction(ctx, &doc_create_text_node, "createTextNode", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "createDocumentFragment", qjs.JS_NewCFunction(ctx, &doc_create_document_fragment, "createDocumentFragment", 0));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "createComment", qjs.JS_NewCFunction(ctx, &doc_create_comment, "createComment", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "getElementById", qjs.JS_NewCFunction(ctx, &doc_get_element_by_id, "getElementById", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "getElementsByClassName", qjs.JS_NewCFunction(ctx, &doc_get_elements_by_class_name, "getElementsByClassName", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "getElementsByTagName", qjs.JS_NewCFunction(ctx, &doc_get_elements_by_tag_name, "getElementsByTagName", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "querySelector", qjs.JS_NewCFunction(ctx, &query_selector, "querySelector", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "querySelectorAll", qjs.JS_NewCFunction(ctx, &query_selector_all, "querySelectorAll", 1));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "addEventListener", qjs.JS_NewCFunction(ctx, &el_add_event_listener, "addEventListener", 2));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "removeEventListener", qjs.JS_NewCFunction(ctx, &el_remove_event_listener, "removeEventListener", 2));
_ = qjs.JS_SetPropertyStr(ctx, doc_obj, "dispatchEvent", qjs.JS_NewCFunction(ctx, &el_dispatch_event, "dispatchEvent", 1));
define_getter(ctx, doc_obj, "body", &doc_get_body);
define_getter(ctx, doc_obj, "head", &doc_get_head);
define_getter(ctx, doc_obj, "documentElement", &doc_get_document_element);
define_getter(ctx, doc_obj, "nodeType", &doc_get_node_type);
define_getter(ctx, doc_obj, "nodeName", &doc_get_node_name);
_ = qjs.JS_SetPropertyStr(ctx, global, "document", doc_obj);
// CSS style helpers (called from CSSStyleDeclaration in style.ts)
_ = qjs.JS_SetPropertyStr(ctx, global, "__qb_css_get_property", qjs.JS_NewCFunction(ctx, &css_get_property, "__qb_css_get_property", 2));
_ = qjs.JS_SetPropertyStr(ctx, global, "__qb_css_get_priority", qjs.JS_NewCFunction(ctx, &css_get_priority, "__qb_css_get_priority", 2));
_ = qjs.JS_SetPropertyStr(ctx, global, "__qb_css_serialize", qjs.JS_NewCFunction(ctx, &css_serialize_declarations, "__qb_css_serialize", 1));
return dd;
}
// ──────────────────── Elixir-facing DOM operations ────────────────────
// These run on the worker thread with direct access to DocumentData,
// bypassing QuickJS entirely. Results are returned as BEAM terms.
pub fn do_dom_query(dd: *DocumentData, selector: []const u8, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const root = lxb.qb_doc_as_node(dd.doc) orelse return beam.make_into_atom("nil", .{ .env = env }).v;
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return beam.make_into_atom("nil", .{ .env = env }).v;
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
var sctx = SelectorCtx{ .results = &results, .find_one = true };
_ = lxb.qb_selectors_find(dd.selectors, root, list, selector_callback, @ptrCast(&sctx));
defer results.deinit(types.gpa);
if (results.items.len == 0) return beam.make_into_atom("nil", .{ .env = env }).v;
return node_to_floki_term(dd, results.items[0], env);
}
pub fn do_dom_query_all(dd: *DocumentData, selector: []const u8, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const root = lxb.qb_doc_as_node(dd.doc) orelse return beam.make_empty_list(.{ .env = env }).v;
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return beam.make_empty_list(.{ .env = env }).v;
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
var sctx = SelectorCtx{ .results = &results, .find_one = false };
_ = lxb.qb_selectors_find(dd.selectors, root, list, selector_callback, @ptrCast(&sctx));
defer results.deinit(types.gpa);
const opts = .{ .env = env };
var elixir_list = beam.make_empty_list(opts);
var i: usize = results.items.len;
while (i > 0) {
i -= 1;
const term = beam.term{ .v = node_to_floki_term(dd, results.items[i], env) };
elixir_list = beam.make_list_cell(term, elixir_list, opts);
}
return elixir_list.v;
}
pub fn do_dom_text(dd: *DocumentData, selector: []const u8, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const opts = .{ .env = env };
const root = lxb.qb_doc_as_node(dd.doc) orelse return beam.make(@as([]const u8, ""), opts).v;
const list = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list == null)
return beam.make(@as([]const u8, ""), opts).v;
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
var sctx = SelectorCtx{ .results = &results, .find_one = true };
_ = lxb.qb_selectors_find(dd.selectors, root, list, selector_callback, @ptrCast(&sctx));
defer results.deinit(types.gpa);
if (results.items.len == 0) return beam.make(@as([]const u8, ""), opts).v;
var len: usize = 0;
const text = lxb.qb_node_text_content(results.items[0], &len);
if (text == null) return beam.make(@as([]const u8, ""), opts).v;
defer lxb.qb_dom_document_destroy_text(lxb.qb_node_owner_document(results.items[0]), @constCast(text));
return beam.make(@as([*]const u8, @ptrCast(text))[0..len], opts).v;
}
pub fn do_dom_attr(dd: *DocumentData, selector: []const u8, attr_name: []const u8, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const opts = .{ .env = env };
const root = lxb.qb_doc_as_node(dd.doc) orelse return beam.make_into_atom("nil", opts).v;
const list_sel = lxb.qb_css_selectors_parse(dd.css_parser, to_lxb(selector), selector.len);
if (lxb.qb_css_parser_status(dd.css_parser) != 0 or list_sel == null)
return beam.make_into_atom("nil", opts).v;
defer lxb.qb_css_selector_list_destroy(dd.css_parser, list_sel);
var results = std.ArrayList(*lxb.lxb_dom_node_t){};
var sctx = SelectorCtx{ .results = &results, .find_one = true };
_ = lxb.qb_selectors_find(dd.selectors, root, list_sel, selector_callback, @ptrCast(&sctx));
defer results.deinit(types.gpa);
if (results.items.len == 0) return beam.make_into_atom("nil", opts).v;
const elem = lxb.qb_node_as_element(results.items[0]) orelse return beam.make_into_atom("nil", opts).v;
var val_len: usize = 0;
const val = lxb.qb_element_get_attribute(elem, to_lxb(attr_name), attr_name.len, &val_len);
if (val == null) return beam.make_into_atom("nil", opts).v;
return beam.make(@as([*]const u8, @ptrCast(val))[0..val_len], opts).v;
}
pub fn do_dom_html(dd: *DocumentData, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const opts = .{ .env = env };
const node = lxb.qb_doc_as_node(dd.doc) orelse return beam.make(@as([]const u8, ""), opts).v;
var buf = std.ArrayList(u8){};
defer buf.deinit(types.gpa);
_ = lxb.qb_serialize_tree(node, serialize_callback, @ptrCast(&buf));
return beam.make(buf.items, opts).v;
}
fn node_to_floki_term(dd: *DocumentData, node: *lxb.lxb_dom_node_t, env: ?*e.ErlNifEnv) e.ErlNifTerm {
const opts = .{ .env = env };
const node_type = lxb.qb_node_type(node);
if (node_type == lxb.QB_NODE_TYPE_TEXT) {
var len: usize = 0;
const text = lxb.qb_node_text_content(node, &len);
if (text == null) return beam.make(@as([]const u8, ""), opts).v;
defer lxb.qb_dom_document_destroy_text(lxb.qb_node_owner_document(node), @constCast(text));
return beam.make(@as([*]const u8, @ptrCast(text))[0..len], opts).v;
}
if (node_type != lxb.QB_NODE_TYPE_ELEMENT) return beam.make_into_atom("nil", opts).v;
const elem = lxb.qb_node_as_element(node) orelse return beam.make_into_atom("nil", opts).v;
// Tag name
var name_len: usize = 0;
const name_ptr = lxb.qb_element_qualified_name(elem, &name_len);
const tag_term = if (name_ptr != null)
beam.make(@as([*]const u8, @ptrCast(name_ptr))[0..name_len], opts)
else
beam.make(@as([]const u8, ""), opts);
// Attributes — list of {name, value} tuples
const attrs_term = node_attrs_to_list(dd, elem, env);
// Children — recursive
var children_list = beam.make_empty_list(opts);
var child_count: usize = 0;
var counter: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (counter) |c| {
child_count += 1;
counter = lxb.qb_node_next(c);
}
if (child_count > 0) {
const child_terms = types.gpa.alloc(e.ErlNifTerm, child_count) catch return beam.make_into_atom("nil", opts).v;
defer types.gpa.free(child_terms);
var idx: usize = 0;
var child: ?*lxb.lxb_dom_node_t = lxb.qb_node_first_child(node);
while (child) |c| {
child_terms[idx] = node_to_floki_term(dd, c, env);
idx += 1;
child = lxb.qb_node_next(c);
}
var i: usize = child_count;
while (i > 0) {
i -= 1;
children_list = beam.make_list_cell(beam.term{ .v = child_terms[i] }, children_list, opts);
}
}
// {tag_name, attrs, children}
return beam.make(.{ tag_term, beam.term{ .v = attrs_term }, children_list }, opts).v;
}
fn node_attrs_to_list(dd: *DocumentData, elem: *lxb.lxb_dom_element_t, env: ?*e.ErlNifEnv) e.ErlNifTerm {
_ = dd;
const opts = .{ .env = env };
// Use the bridge to iterate attributes
var attr_count: usize = 0;
var attr: ?*lxb.lxb_dom_attr_t = lxb.qb_element_first_attr(elem);
while (attr != null) {
attr_count += 1;
attr = lxb.qb_attr_next(attr);
}
if (attr_count == 0) return beam.make_empty_list(opts).v;
const terms = types.gpa.alloc(e.ErlNifTerm, attr_count) catch return beam.make_empty_list(opts).v;
defer types.gpa.free(terms);
var idx: usize = 0;
attr = lxb.qb_element_first_attr(elem);
while (attr) |a| {
var name_len: usize = 0;
var val_len: usize = 0;
const a_name = lxb.qb_attr_name(a, &name_len);
const a_val = lxb.qb_attr_value(a, &val_len);
const name_term = if (a_name != null)
beam.make(@as([*]const u8, @ptrCast(a_name))[0..name_len], opts)
else
beam.make(@as([]const u8, ""), opts);
const val_term = if (a_val != null)
beam.make(@as([*]const u8, @ptrCast(a_val))[0..val_len], opts)
else
beam.make(@as([]const u8, ""), opts);
terms[idx] = beam.make(.{ name_term, val_term }, opts).v;
idx += 1;
attr = lxb.qb_attr_next(a);
}
var result = beam.make_empty_list(opts);
var i: usize = attr_count;
while (i > 0) {
i -= 1;
result = beam.make_list_cell(beam.term{ .v = terms[i] }, result, opts);
}
return result.v;
}