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.
This commit is contained in:
Karl Seguin
2026-06-22 18:26:31 +08:00
parent f61d16aa49
commit 23e72cf573
4 changed files with 32 additions and 10 deletions

View File

@@ -11,6 +11,10 @@
<p id=d4><span>Hello</span>
<span>World</span></p>
<style>.ihide { display: none }</style>
<div id=d5>a<span class=ihide>X</span>b<span style="display:none">Y</span>c</div>
<div id=d6>before<div class=ihide>deep <span>nested</span></div>after</div>
<script id=innerHTML>
const d1 = $('#d1');
testing.expectEqual('hello <em>world</em>', d1.innerHTML);
@@ -175,4 +179,10 @@
testing.expectEqual("This is a\ntext", d2.innerText);
testing.expectEqual("Hello World", $('#d3').innerText);
testing.expectEqual("Hello World", $('#d4').innerText);
// display:none subtrees are excluded from innerText, whether the rule comes
// from an inline style or a stylesheet (the common real-world case: content
// hidden via a CSS class). A hidden block does not contribute a line break.
testing.expectEqual("abc", $('#d5').innerText);
testing.expectEqual("beforeafter", $('#d6').innerText);
</script>

View File

@@ -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();
}

View File

@@ -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 {

View File

@@ -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);