Move HTML-specific behavior from Element to HTMLElement.

This commit is contained in:
Karl Seguin
2025-12-24 15:23:02 +08:00
parent c0704f822b
commit 0fcb316837
2 changed files with 143 additions and 105 deletions

View File

@@ -290,78 +290,25 @@ pub fn getLocalName(self: *Element) []const u8 {
return name; return name;
} }
// innerText represents the **rendered** text content of a node and its // Wrapper methods that delegate to Html implementations
// descendants.
pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
var state = innerTextState{}; const he = self.is(Html) orelse return error.NotHtmlElement;
return try self._getInnerText(writer, &state); return he.getInnerText(writer);
}
const innerTextState = struct {
pre_w: bool = false,
trim_left: bool = true,
};
fn _getInnerText(self: *Element, writer: *std.Io.Writer, state: *innerTextState) !void {
var it = self.asNode().childrenIterator();
while (it.next()) |child| {
switch (child._type) {
.element => |e| switch (e._type) {
.html => |he| switch (he._type) {
.br => {
try writer.writeByte('\n');
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.script, .style, .template => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
else => try e._getInnerText(writer, state), // TODO check if elt is hidden.
},
.svg => {},
},
.cdata => |c| switch (c._type) {
.comment => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.text => {
if (state.pre_w) try writer.writeByte(' ');
state.pre_w = try c.render(writer, .{ .trim_left = state.trim_left });
// if we had a pre space, trim left next one.
state.trim_left = state.pre_w;
},
// CDATA sections should not be used within HTML. They are
// considered comments and are not displayed.
.cdata_section => {},
// Processing instructions are not displayed in innerText
.processing_instruction => {},
},
.document => {},
.document_type => {},
.document_fragment => {},
.attribute => |attr| try writer.writeAll(attr._value),
}
}
} }
pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void { pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void {
const parent = self.asNode(); const he = self.is(Html) orelse return error.NotHtmlElement;
return he.setInnerText(text, page);
// Remove all existing children
page.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
} }
// Fast path: skip if text is empty pub fn insertAdjacentHTML(
if (text.len == 0) { self: *Element,
return; position: []const u8,
} html_or_xml: []const u8,
page: *Page,
// Create and append text node ) !void {
const text_node = try page.createTextNode(text); const he = self.is(Html) orelse return error.NotHtmlElement;
try page.appendNode(parent, text_node, .{ .child_already_connected = false }); return he.insertAdjacentHTML(position, html_or_xml, page);
} }
pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void { pub fn getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
@@ -504,44 +451,6 @@ pub fn attachShadow(self: *Element, mode_str: []const u8, page: *Page) !*ShadowR
return shadow_root; return shadow_root;
} }
pub fn insertAdjacentHTML(
self: *Element,
position: []const u8,
/// TODO: Add support for XML parsing.
html_or_xml: []const u8,
page: *Page,
) !void {
// Create a new HTMLDocument.
const doc = try page._factory.document(@import("HTMLDocument.zig"){
._proto = undefined,
});
const doc_node = doc.asNode();
const Parser = @import("../parser/Parser.zig");
var parser = Parser.init(page.call_arena, doc_node, page);
parser.parse(html_or_xml);
// Check if there's parsing error.
if (parser.err) |_| return error.Invalid;
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html_node = doc_node.firstChild();
std.debug.assert(maybe_html_node != null);
const html_node = maybe_html_node orelse return;
const maybe_body_node = html_node.lastChild();
std.debug.assert(maybe_body_node != null);
const body = maybe_body_node orelse return;
const target_node, const prev_node = try self.asNode().findAdjacentNodes(position);
var iter = body.childrenIterator();
while (iter.next()) |child_node| {
_ = try target_node.insertBefore(child_node, prev_node, page);
}
}
pub fn insertAdjacentElement( pub fn insertAdjacentElement(
self: *Element, self: *Element,
position: []const u8, position: []const u8,

View File

@@ -61,6 +61,8 @@ pub const Unknown = @import("html/Unknown.zig");
const HtmlElement = @This(); const HtmlElement = @This();
const std = @import("std");
_type: Type, _type: Type,
_proto: *Element, _proto: *Element,
@@ -166,6 +168,124 @@ pub fn className(self: *const HtmlElement) []const u8 {
}; };
} }
pub fn asElement(self: *HtmlElement) *Element {
return self._proto;
}
// innerText represents the **rendered** text content of a node and its
// descendants.
pub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer) !void {
var state = innerTextState{};
return try self._getInnerText(writer, &state);
}
const innerTextState = struct {
pre_w: bool = false,
trim_left: bool = true,
};
fn _getInnerText(self: *HtmlElement, writer: *std.Io.Writer, state: *innerTextState) !void {
var it = self.asElement().asNode().childrenIterator();
while (it.next()) |child| {
switch (child._type) {
.element => |e| switch (e._type) {
.html => |he| switch (he._type) {
.br => {
try writer.writeByte('\n');
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.script, .style, .template => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
else => try he._getInnerText(writer, state), // TODO check if elt is hidden.
},
.svg => {},
},
.cdata => |c| switch (c._type) {
.comment => {
state.pre_w = false; // prevent a next pre space.
state.trim_left = true;
},
.text => {
if (state.pre_w) try writer.writeByte(' ');
state.pre_w = try c.render(writer, .{ .trim_left = state.trim_left });
// if we had a pre space, trim left next one.
state.trim_left = state.pre_w;
},
// CDATA sections should not be used within HTML. They are
// considered comments and are not displayed.
.cdata_section => {},
// Processing instructions are not displayed in innerText
.processing_instruction => {},
},
.document => {},
.document_type => {},
.document_fragment => {},
.attribute => |attr| try writer.writeAll(attr._value),
}
}
}
pub fn setInnerText(self: *HtmlElement, text: []const u8, page: *Page) !void {
const parent = self.asElement().asNode();
// Remove all existing children
page.domChanged();
var it = parent.childrenIterator();
while (it.next()) |child| {
page.removeNode(parent, child, .{ .will_be_reconnected = false });
}
// Fast path: skip if text is empty
if (text.len == 0) {
return;
}
// Create and append text node
const text_node = try page.createTextNode(text);
try page.appendNode(parent, text_node, .{ .child_already_connected = false });
}
pub fn insertAdjacentHTML(
self: *HtmlElement,
position: []const u8,
/// TODO: Add support for XML parsing.
html_or_xml: []const u8,
page: *Page,
) !void {
// Create a new HTMLDocument.
const doc = try page._factory.document(@import("../HTMLDocument.zig"){
._proto = undefined,
});
const doc_node = doc.asNode();
const Parser = @import("../../parser/Parser.zig");
var parser = Parser.init(page.call_arena, doc_node, page);
parser.parse(html_or_xml);
// Check if there's parsing error.
if (parser.err) |_| return error.Invalid;
// We always get it wrapped like so:
// <html><head></head><body>{ ... }</body></html>
// None of the following can be null.
const maybe_html_node = doc_node.firstChild();
std.debug.assert(maybe_html_node != null);
const html_node = maybe_html_node orelse return;
const maybe_body_node = html_node.lastChild();
std.debug.assert(maybe_body_node != null);
const body = maybe_body_node orelse return;
const target_node, const prev_node = try self.asElement().asNode().findAdjacentNodes(position);
var iter = body.childrenIterator();
while (iter.next()) |child_node| {
_ = try target_node.insertBefore(child_node, prev_node, page);
}
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(HtmlElement); pub const bridge = js.Bridge(HtmlElement);
@@ -176,6 +296,15 @@ pub const JsApi = struct {
}; };
pub const constructor = bridge.constructor(HtmlElement.construct, .{}); pub const constructor = bridge.constructor(HtmlElement.construct, .{});
pub const innerText = bridge.accessor(_innerText, HtmlElement.setInnerText, .{});
fn _innerText(self: *HtmlElement, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerText(&buf.writer);
return buf.written();
}
pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true });
}; };
pub const Build = struct { pub const Build = struct {