diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index 4adb987f8..f67affde4 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -139,7 +139,7 @@ fn walk( if (html_el.getHidden()) return; } } else if (node.is(CData.Text)) |text_node| { - const text = text_node.getWholeText(); + const text = text_node.ownData(); if (isAllWhitespace(text)) { return; } @@ -414,7 +414,7 @@ const JsonVisitor = struct { try self.jw.objectField("nodeType"); try self.jw.write(3); try self.jw.objectField("nodeValue"); - try self.jw.write(text_node.getWholeText()); + try self.jw.write(text_node.ownData()); } else { try self.jw.objectField("nodeType"); try self.jw.write(9); @@ -472,7 +472,7 @@ const TextVisitor = struct { name_to_print = n; } } else if (node.is(CData.Text)) |text_node| { - const trimmed = std.mem.trim(u8, text_node.getWholeText(), " \t\r\n"); + const trimmed = std.mem.trim(u8, text_node.ownData(), " \t\r\n"); if (trimmed.len > 0) { name_to_print = trimmed; } diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index 45bd7d7b7..8c9c51e31 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -448,7 +448,7 @@ pub fn getTextContent(node: *Node, arena: Allocator) !?[]const u8 { } if (child.is(Node.CData)) |cdata| { if (cdata.is(Node.CData.Text)) |text| { - const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace); + const content = std.mem.trim(u8, text.ownData(), &std.ascii.whitespace); if (content.len > 0) { if (single_chunk == null and arr.items.len == 0) { single_chunk = content; diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index b56d25bb3..48fbf495d 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -142,7 +142,7 @@ fn isStandaloneAnchor(el: *Element) bool { fn isSignificantText(node: *Node) bool { const text = node.is(Node.CData.Text) orelse return false; - return !isAllWhitespace(text.getWholeText()); + return !isAllWhitespace(text.ownData()); } fn isVisibleElement(el: *Element) bool { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index c5d2d3a26..e412a0df0 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1175,11 +1175,11 @@ fn _normalize(self: *Node, allocator: Allocator, buffer: *std.ArrayList(u8), fra if (next_node) |next| { if (next.is(CData.Text)) |_| { - try buffer.appendSlice(allocator, text_node.getWholeText()); + try buffer.appendSlice(allocator, text_node.ownData()); while (next_node) |node_to_merge| { const next_text_node = node_to_merge.is(CData.Text) orelse break; - try buffer.appendSlice(allocator, next_text_node.getWholeText()); + try buffer.appendSlice(allocator, next_text_node.ownData()); const to_remove = node_to_merge; next_node = node_to_merge.nextSibling(); diff --git a/src/browser/webapi/cdata/Text.zig b/src/browser/webapi/cdata/Text.zig index 6617339de..81cb7c6ad 100644 --- a/src/browser/webapi/cdata/Text.zig +++ b/src/browser/webapi/cdata/Text.zig @@ -16,8 +16,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); + const js = @import("../../js/js.zig"); const Frame = @import("../../Frame.zig"); +const Node = @import("../Node.zig"); const CData = @import("../CData.zig"); const Slot = @import("../element/html/Slot.zig"); const slotting = @import("../element/slotting.zig"); @@ -31,10 +34,41 @@ pub fn init(str: ?js.NullableString, frame: *Frame) !*Text { return node.as(Text); } -pub fn getWholeText(self: *Text) []const u8 { +// This Text node's own data (getWholeText below spans adjacent Text nodes). +pub fn ownData(self: *const Text) []const u8 { return self._proto._data.str(); } +// The concatenated data of the contiguous exclusive Text nodes (adjacent +// Text siblings on both sides of this one), in tree order. +pub fn getWholeText(self: *Text, frame: *Frame) ![]const u8 { + const node = self._proto.asNode(); + + var first = node; + while (first.previousSibling()) |prev| { + if (!isExclusiveTextNode(prev)) break; + first = prev; + } + + // Common case: no adjacent text nodes, return our data directly. + const has_next_text = if (node.nextSibling()) |next| isExclusiveTextNode(next) else false; + if (first == node and !has_next_text) { + return self._proto._data.str(); + } + + var buf: std.ArrayList(u8) = .empty; + var current: ?*Node = first; + while (current) |cur| : (current = cur.nextSibling()) { + if (!isExclusiveTextNode(cur)) break; + try buf.appendSlice(frame.call_arena, cur._type.cdata._data.str()); + } + return buf.items; +} + +fn isExclusiveTextNode(node: *const Node) bool { + return node._type == .cdata and node._type.cdata._type == .text; +} + pub fn getAssignedSlot(self: *Text, frame: *Frame) ?*Slot { return slotting.findSlot(self._proto.asNode(), true, frame); } diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index 6256b691e..319126148 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -90,7 +90,7 @@ pub fn getDefaultValue(self: *const TextArea) []const u8 { const node = self.asConstNode(); if (node.firstChild()) |child| { if (child.is(Node.CData.Text)) |txt| { - return txt.getWholeText(); + return txt.ownData(); } } return ""; diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index dd301cf1c..2da6c98b7 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -989,7 +989,7 @@ fn writeName( }, .cdata => |cd| switch (cd._type) { .text => |*text| { - try writeString(text.getWholeText(), w); + try writeString(text.ownData(), w); return .contents; }, else => null, @@ -1109,7 +1109,7 @@ fn writeAccessibleNameFallback(node: *DOMNode, writer: *std.Io.Writer, frame: *F switch (child._type) { .cdata => |cd| switch (cd._type) { .text => |*text| { - const content = std.mem.trim(u8, text.getWholeText(), &std.ascii.whitespace); + const content = std.mem.trim(u8, text.ownData(), &std.ascii.whitespace); if (content.len > 0) { try writer.writeAll(content); try writer.writeByte(' ');