Element.setInnerText

This commit is contained in:
Karl Seguin
2025-12-03 08:52:51 +08:00
parent 568a4428ba
commit c0da6994da
3 changed files with 87 additions and 2 deletions

View File

@@ -64,7 +64,7 @@ pub fn root(opts: RootOpts, writer: *std.Io.Writer, page: *Page) !void {
pub fn deep(node: *Node, opts: Opts, writer: *std.Io.Writer, page: *Page) error{WriteFailed}!void {
switch (node._type) {
.cdata => |cd| try writer.writeAll(cd.getData()),
.cdata => |cd| try writeEscapedText(cd.getData(), writer),
.element => |el| {
if (shouldStripElement(el, opts)) {
return;
@@ -211,3 +211,35 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
return false;
}
fn writeEscapedText(text: []const u8, writer: *std.Io.Writer) !void {
// Fast path: if no special characters, write directly
const first_special = std.mem.indexOfAny(u8, text, "&<>") orelse {
return writer.writeAll(text);
};
try writer.writeAll(text[0..first_special]);
try writer.writeAll(switch (text[first_special]) {
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
else => unreachable,
});
// Process remaining text
var remaining = text[first_special + 1 ..];
while (std.mem.indexOfAny(u8, remaining, "&<>")) |offset| {
try writer.writeAll(remaining[0..offset]);
try writer.writeAll(switch (remaining[offset]) {
'&' => "&amp;",
'<' => "&lt;",
'>' => "&gt;",
else => unreachable,
});
remaining = remaining[offset + 1 ..];
}
if (remaining.len > 0) {
try writer.writeAll(remaining);
}
}

View File

@@ -129,3 +129,36 @@
d1.innerHTML = '<div><br><hr><img src="x.png"></div>';
testing.expectEqual('<div><br><hr><img src="x.png"></div>', d1.innerHTML);
</script>
<script id=innerText>
// Get innerText from element
d1.innerHTML = 'hello <em>world</em>';
testing.expectEqual('hello world', d1.innerText);
// Set innerText - should replace children with text node
d1.innerText = 'only text';
testing.expectEqual('only text', d1.innerText);
testing.expectEqual('only text', d1.innerHTML);
// innerText does NOT parse HTML (unlike innerHTML)
d1.innerText = 'hello <div>world</div><b>!!</b>';
testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerText);
console.warn(d1.innerHTML);
testing.expectEqual('hello &lt;div&gt;world&lt;/div&gt;&lt;b&gt;!!&lt;/b&gt;', d1.innerHTML);
// Setting empty string clears children
d1.innerText = '';
testing.expectEqual('', d1.innerText);
testing.expectEqual('', d1.innerHTML);
// innerText with nested elements
d1.innerHTML = '<div>hello <span>beautiful</span> world</div>';
testing.expectEqual('hello beautiful world', d1.innerText);
// Setting innerText removes all previous children including elements
d1.innerHTML = '<div><p><a id=link2>hi</a></p></div>';
testing.expectEqual('hi', d1.innerText);
d1.innerText = 'new content';
testing.expectEqual('new content', d1.innerText);
testing.expectEqual(null, $('#link2'));
</script>

View File

@@ -229,6 +229,26 @@ pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
}
}
pub fn setInnerText(self: *Element, text: []const u8, page: *Page) !void {
const parent = self.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 getOuterHTML(self: *Element, writer: *std.Io.Writer, page: *Page) !void {
const dump = @import("../dump.zig");
return dump.deep(self.asNode(), .{ .shadow = .skip }, writer, page);
@@ -913,7 +933,7 @@ pub const JsApi = struct {
}
pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{});
pub const innerText = bridge.accessor(_innerText, null, .{});
pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{});
fn _innerText(self: *Element, page: *const Page) ![]const u8 {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
try self.getInnerText(&buf.writer);