diff --git a/src/browser/tests/element/inner.html b/src/browser/tests/element/inner.html
index c9bb08946..ada3f7c17 100644
--- a/src/browser/tests/element/inner.html
+++ b/src/browser/tests/element/inner.html
@@ -1,6 +1,12 @@
hello world
+
+
+
+ This is a
+ text
+
diff --git a/src/browser/tests/node/text_content.html b/src/browser/tests/node/text_content.html
index d6250320e..fc6a0de9a 100644
--- a/src/browser/tests/node/text_content.html
+++ b/src/browser/tests/node/text_content.html
@@ -1,6 +1,12 @@
+
+
+
+ This is a
+ text
+
diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig
index f02d51cad..b1c78b038 100644
--- a/src/browser/webapi/CData.zig
+++ b/src/browser/webapi/CData.zig
@@ -60,6 +60,58 @@ pub fn getData(self: *const CData) []const u8 {
return self._data;
}
+pub const RenderOpts = struct {
+ trim_left: bool = true,
+ trim_right: bool = true,
+};
+// Replace successives whitespaces with one withespace.
+// Trims left and right according to the options.
+pub fn render(self: *const CData, writer: *std.io.Writer, opts: RenderOpts) !void {
+ var start: usize = 0;
+ var prev_w: ?bool = null;
+ var is_w: bool = undefined;
+ const s = self._data;
+
+ for (s, 0..) |c, i| {
+ is_w = std.ascii.isWhitespace(c);
+
+ // Detect the first char type.
+ if (prev_w == null) {
+ prev_w = is_w;
+ }
+ // The current char is the same kind of char, the chunk continues.
+ if (prev_w.? == is_w) {
+ continue;
+ }
+
+ // Starting here, the chunk changed.
+ if (is_w) {
+ // We have a chunk of non-whitespaces, we write it as it.
+ try writer.writeAll(s[start..i]);
+ } else {
+ // We have a chunk of whitespaces, replace with one space,
+ // depending the position.
+ if (start > 0 or !opts.trim_left) {
+ try writer.writeByte(' ');
+ }
+ }
+ // Start the new chunk.
+ prev_w = is_w;
+ start = i;
+ }
+ // Write the reminder chunk.
+ if (is_w) {
+ // Last chunk is whitespaces.
+ // If the string contains only whitespaces, don't write it.
+ if (start > 0 and opts.trim_right == false) {
+ try writer.writeByte(' ');
+ }
+ } else {
+ // last chunk is non whitespaces.
+ try writer.writeAll(s[start..]);
+ }
+}
+
pub fn setData(self: *CData, value: ?[]const u8, page: *Page) !void {
const old_value = self._data;
@@ -223,3 +275,44 @@ const testing = @import("../../testing.zig");
test "WebApi: CData" {
try testing.htmlRunner("cdata", .{});
}
+
+test "WebApi: CData.render" {
+ const allocator = std.testing.allocator;
+
+ const TestCase = struct {
+ value: []const u8,
+ expected: []const u8,
+ opts: RenderOpts = .{},
+ };
+
+ const test_cases = [_]TestCase{
+ .{ .value = " ", .expected = "" },
+ .{ .value = " ", .expected = "", .opts = .{ .trim_left = false, .trim_right = false } },
+ .{ .value = "foo bar", .expected = "foo bar" },
+ .{ .value = "foo bar", .expected = "foo bar" },
+ .{ .value = " foo bar", .expected = "foo bar" },
+ .{ .value = "foo bar ", .expected = "foo bar" },
+ .{ .value = " foo bar ", .expected = "foo bar" },
+ .{ .value = "foo\n\tbar", .expected = "foo bar" },
+ .{ .value = "\tfoo bar baz \t\n yeah\r\n", .expected = "foo bar baz yeah" },
+ .{ .value = " foo bar", .expected = " foo bar", .opts = .{ .trim_left = false } },
+ .{ .value = "foo bar ", .expected = "foo bar ", .opts = .{ .trim_right = false } },
+ .{ .value = " foo bar ", .expected = " foo bar ", .opts = .{ .trim_left = false, .trim_right = false } },
+ };
+
+ var buffer = std.io.Writer.Allocating.init(allocator);
+ defer buffer.deinit();
+ for (test_cases) |test_case| {
+ buffer.clearRetainingCapacity();
+
+ const cdata = CData{
+ ._type = .{ .text = undefined },
+ ._proto = undefined,
+ ._data = test_case.value,
+ };
+
+ try cdata.render(&buffer.writer, test_case.opts);
+
+ try std.testing.expectEqualStrings(test_case.expected, buffer.written());
+ }
+}
diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig
index 758bb1f24..4d50051dd 100644
--- a/src/browser/webapi/Element.zig
+++ b/src/browser/webapi/Element.zig
@@ -223,10 +223,29 @@ pub fn getNamespaceURI(self: *const Element) []const u8 {
return self._namespace.toUri();
}
+// innerText represents the **rendered** text content of a node and its
+// descendants.
pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void {
var it = self.asNode().childrenIterator();
while (it.next()) |child| {
- try child.getTextContent(writer);
+ switch (child._type) {
+ .element => |e| switch (e._type) {
+ .html => |he| switch (he._type) {
+ .br => try writer.writeByte('\n'),
+ .script, .style, .template => continue,
+ else => try e.getInnerText(writer), // TODO check if elt is hidden.
+ },
+ .svg => {},
+ },
+ .cdata => |c| switch (c._type) {
+ .comment => continue,
+ .text => try c.render(writer, .{ .trim_right = false, .trim_left = false }),
+ },
+ .document => {},
+ .document_type => {},
+ .document_fragment => {},
+ .attribute => |attr| try writer.writeAll(attr._value),
+ }
}
}
diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig
index 596dc5549..ef6de4a7d 100644
--- a/src/browser/webapi/Node.zig
+++ b/src/browser/webapi/Node.zig
@@ -171,7 +171,16 @@ pub fn childNodes(self: *const Node, page: *Page) !*collections.ChildNodes {
pub fn getTextContent(self: *Node, writer: *std.Io.Writer) error{WriteFailed}!void {
switch (self._type) {
- .element => |el| return el.getInnerText(writer),
+ .element => {
+ var it = self.childrenIterator();
+ while (it.next()) |child| {
+ // ignore comments and TODO processing instructions.
+ if (child.is(CData.Comment) != null) {
+ continue;
+ }
+ try child.getTextContent(writer);
+ }
+ },
.cdata => |c| try writer.writeAll(c.getData()),
.document => {},
.document_type => {},
@@ -719,7 +728,7 @@ pub const JsApi = struct {
switch (self._type) {
.element => |el| {
var buf = std.Io.Writer.Allocating.init(page.call_arena);
- try el.getInnerText(&buf.writer);
+ try el.asNode().getTextContent(&buf.writer);
return buf.written();
},
.cdata => |cdata| return cdata.getData(),