From 23e72cf573fff6b7958a7071df1c7be1de4bcef7 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 22 Jun 2026 18:26:31 +0800 Subject: [PATCH] webapi: innerText uses StyleManager's visibility innerText now uses the StyleManager's visibility check (isHidden) to skip rendering children which are hidden. Should allow us to close https://github.com/lightpanda-io/browser/pull/2741 and https://github.com/lightpanda-io/browser/issues/2734 Note that ~1000 WPT test still fail because of other style-driven behavior, most significantly is `white-space`. The isHidden change only fixes a couple WPT cases, but I think, for end users, it's the more important change (and it's also the simpler for us to implement). Having wrong spacing is usually less of a problem than showing the wrong content. --- src/browser/tests/element/inner.html | 10 ++++++++++ src/browser/webapi/Element.zig | 8 ++++---- src/browser/webapi/element/Html.zig | 20 ++++++++++++++++---- src/cdp/AXNode.zig | 4 ++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/browser/tests/element/inner.html b/src/browser/tests/element/inner.html index 7c59ea14d..f8088357f 100644 --- a/src/browser/tests/element/inner.html +++ b/src/browser/tests/element/inner.html @@ -11,6 +11,10 @@

Hello World

+ +
aXbYc
+
before
deep nested
after
+ diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 42f00be7b..4f2d62db1 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -453,9 +453,9 @@ pub fn getLocalName(self: *Element) []const u8 { } // Wrapper methods that delegate to Html implementations -pub fn getInnerText(self: *Element, writer: *std.Io.Writer) !void { +pub fn getInnerText(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void { const he = self.is(Html) orelse return error.NotHtmlElement; - return he.getInnerText(writer); + return he.getInnerText(writer, frame); } pub fn setInnerText(self: *Element, text: []const u8, frame: *Frame) !void { @@ -1913,9 +1913,9 @@ pub const JsApi = struct { pub const namespaceURI = bridge.accessor(Element.getNamespaceURI, null, .{}); pub const innerText = bridge.accessor(_innerText, Element.setInnerText, .{ .ce_reactions = true }); - fn _innerText(self: *Element, frame: *const Frame) ![]const u8 { + fn _innerText(self: *Element, frame: *Frame) ![]const u8 { var buf = std.Io.Writer.Allocating.init(frame.call_arena); - try self.getInnerText(&buf.writer); + try self.getInnerText(&buf.writer, frame); return buf.written(); } diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 6549384ab..4571d71cb 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -216,13 +216,14 @@ pub fn asEventTarget(self: *HtmlElement) *@import("../EventTarget.zig") { return self._proto._proto._proto; } -pub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer) !void { +pub fn getInnerText(self: *HtmlElement, writer: *std.Io.Writer, frame: *Frame) !void { const tag = self.asElement().getTag(); switch (innerTextDisplay(self, tag)) { .skip, .replaced => return, else => {}, } - var state = InnerTextState{ .writer = writer, .preserve = tag == .pre }; + + var state = InnerTextState{ .writer = writer, .frame = frame, .preserve = tag == .pre }; try self.collectInnerText(&state); } @@ -1326,6 +1327,13 @@ pub fn reflectEnumerated( const InnerTextState = struct { writer: *std.Io.Writer, + + // Needed to reach the StyleManager for CSS-driven visibility (display:none). + frame: *Frame, + + // Tree-local visibility cache shared across the whole walk (see getInnerText). + cache: Element.VisibilityCache = .{}, + // number of line breaks we've accumulated for the block. Emitted lazily that // leading/trailing breaks aren't written and so that we can emit the max // requested, which can change as we render children. @@ -1450,6 +1458,10 @@ fn handleChildElement( return; } + if (state.frame._style_manager.isHidden(he.asElement(), &state.cache, .{})) { + return; + } + switch (innerTextDisplay(he, tag)) { // Not rendered: skip the subtree entirely (script/style/metadata/...). .skip => {}, @@ -1632,9 +1644,9 @@ pub const JsApi = struct { pub const constructor = bridge.constructor(HtmlElement.construct, .{ .new_target = true }); pub const innerText = bridge.accessor(_innerText, _setInnerText, .{ .ce_reactions = true }); - fn _innerText(self: *HtmlElement, frame: *const Frame) ![]const u8 { + fn _innerText(self: *HtmlElement, frame: *Frame) ![]const u8 { var buf = std.Io.Writer.Allocating.init(frame.call_arena); - try self.getInnerText(&buf.writer); + try self.getInnerText(&buf.writer, frame); return buf.written(); } fn _setInnerText(self: *HtmlElement, value: js.Value, frame: *Frame) !void { diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 23b2c2bfe..d74073228 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -1011,7 +1011,7 @@ fn writeName( if (doc.getElementById(trimmed_id, frame)) |referenced_el| { // Get the text content of the referenced element - try referenced_el.getInnerText(&buf.writer); + try referenced_el.getInnerText(&buf.writer, frame); try buf.writer.writeByte(' '); has_content = true; } @@ -1262,7 +1262,7 @@ fn writeLabelInnerText( w: anytype, ) !bool { var buf: std.Io.Writer.Allocating = .init(scratchAllocator(temp_arena, frame)); - try label_el.getInnerText(&buf.writer); + try label_el.getInnerText(&buf.writer, frame); const text = std.mem.trim(u8, buf.written(), &std.ascii.whitespace); if (text.len == 0) return false; try writeString(text, w);