Merge pull request #3269 from lightpanda-io/markdown-hidden-content

markdown: skip hidden elements
This commit is contained in:
Karl Seguin authored and GitHub committed 2026-08-26 13:54:49 +08:00
commit 2fbfbea955
12 files changed
+242 -63

No files matched your search

+1 -1
View File
@@ -131,7 +131,7 @@ fn walk(
if (tag == .datalist or tag == .option or tag == .optgroup) return;
// Check visibility using the engine's checkVisibility which handles CSS display: none
if (!el.checkVisibilityCached(ctx.visibility_cache, self.frame)) {
if (!el.checkVisibilityCached(ctx.visibility_cache, self.frame, .scan)) {
return;
}
+111 -29
View File
@@ -33,7 +33,6 @@ const SelectorList = @import("webapi/selector/List.zig");
const CSSStyleRule = @import("webapi/css/CSSStyleRule.zig");
const CSSStyleSheet = @import("webapi/css/CSSStyleSheet.zig");
const CSSStyleProperties = @import("webapi/css/CSSStyleProperties.zig");
const CSSStyleProperty = @import("webapi/css/CSSStyleDeclaration.zig").Property;
const log = lp.log;
const String = lp.String;
@@ -573,7 +572,7 @@ fn rebuildIfDirty(self: *StyleManager) !void {
// Check if an element is hidden based on options.
// By default only checks display:none.
// Walks up the tree to check ancestors.
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions) bool {
pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, options: CheckVisibilityOptions, comptime access: InlineAccess) bool {
self.rebuildIfDirty() catch return false;
var current: ?*Element = el;
@@ -590,7 +589,7 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt
}
}
const hidden = self.isElementHidden(elem, options);
const hidden = self.isElementHidden(elem, options, access);
// Store in cache
if (cache) |c| {
@@ -611,18 +610,18 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt
/// Computed display:none for a single element (own property, no ancestor walk).
/// Honors the UA stylesheet rules per HTML Rendering §15.3.1 "Hidden elements"
/// via `isElementHidden`.
pub fn hasDisplayNone(self: *StyleManager, el: *Element) bool {
pub fn hasDisplayNone(self: *StyleManager, el: *Element, comptime access: InlineAccess) bool {
self.rebuildIfDirty() catch return false;
return self.isElementHidden(el, .{});
return self.isElementHidden(el, .{}, access);
}
/// Computed display:none coming only from inline style or an author stylesheet
/// rule — the UA stylesheet's hidden elements (<head>, <script>, [hidden], …)
/// are NOT counted, so document scaffolding is preserved. Used by the HTML
/// dump's "invisible" strip mode.
pub fn hasAuthorDisplayNone(self: *StyleManager, el: *Element) bool {
pub fn hasAuthorDisplayNone(self: *StyleManager, el: *Element, comptime access: InlineAccess) bool {
self.rebuildIfDirty() catch return false;
return self.isElementHidden(el, .{ .ua_display_none = false });
return self.isElementHidden(el, .{ .ua_display_none = false }, access);
}
/// Centralizes UA-stylesheet display:none truth so `getComputedStyle().display`
@@ -644,6 +643,9 @@ fn matchesUaDisplayNoneRule(el: *Element) bool {
}
}
// dialog:not([open]) { display: none }
if (tag == .dialog and !el.hasAttributeSafe(comptime .wrap("open"))) return true;
// details:not([open]) > *:not(summary) { display: none }
if (tag != .summary) {
if (el.parentElement()) |parent| {
@@ -664,7 +666,7 @@ pub fn hasVisibilityHiddenInherited(self: *StyleManager, el: *Element) bool {
self.rebuildIfDirty() catch return false;
var current: ?*Element = el;
while (current) |elem| {
if (self.isElementHidden(elem, .{ .check_display = false, .check_visibility = true })) {
if (self.isElementHidden(elem, .{ .check_display = false, .check_visibility = true }, .materialize)) {
return true;
}
current = elem.parentElement();
@@ -673,7 +675,7 @@ pub fn hasVisibilityHiddenInherited(self: *StyleManager, el: *Element) bool {
}
/// Check if a single element (not ancestors) is hidden.
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions) bool {
fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOptions, comptime access: InlineAccess) bool {
// Track best match per property (value + priority)
// Initialize priority to INLINE_PRIORITY for properties we don't care about - this makes
// the loop naturally skip them since no stylesheet rule can have priority >= INLINE_PRIORITY
@@ -688,8 +690,8 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
// Check inline styles FIRST - they use INLINE_PRIORITY so no stylesheet can beat them
if (options.check_display) {
if (getInlineStyleProperty(el, comptime .wrap("display"), self.frame)) |property| {
if (property._value.eqlSliceIgnoreCase("none")) {
if (inlineValue(el, comptime .wrap("display"), access, self.frame)) |value| {
if (std.ascii.eqlIgnoreCase(value, "none")) {
return true; // Early exit for hiding value
}
display_none = false;
@@ -701,8 +703,8 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
}
if (options.check_visibility) {
if (getInlineStyleProperty(el, comptime .wrap("visibility"), self.frame)) |property| {
if (property._value.eqlSliceIgnoreCase("hidden") or property._value.eqlSliceIgnoreCase("collapse")) {
if (inlineValue(el, comptime .wrap("visibility"), access, self.frame)) |value| {
if (std.ascii.eqlIgnoreCase(value, "hidden") or std.ascii.eqlIgnoreCase(value, "collapse")) {
return true;
}
visibility_hidden = false;
@@ -716,8 +718,8 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
}
if (options.check_opacity) {
if (getInlineStyleProperty(el, comptime .wrap("opacity"), self.frame)) |property| {
if (property._value.eqlSliceIgnoreCase("0")) {
if (inlineValue(el, comptime .wrap("opacity"), access, self.frame)) |value| {
if (std.ascii.eqlIgnoreCase(value, "0")) {
return true;
}
opacity_zero = false;
@@ -871,8 +873,8 @@ fn elementHasPointerEventsNone(self: *StyleManager, el: *Element) bool {
const frame = self.frame;
// Check inline style first
if (getInlineStyleProperty(el, .wrap("pointer-events"), frame)) |property| {
if (property._value.eqlSliceIgnoreCase("none")) {
if (inlineValue(el, .wrap("pointer-events"), .materialize, frame)) |value| {
if (std.ascii.eqlIgnoreCase(value, "none")) {
return true;
}
return false;
@@ -1191,22 +1193,67 @@ const CheckVisibilityOptions = struct {
// its field max, so a real rule can never pack to all-ones.
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
/// How a probe reads an element's inline `style=` attribute.
pub const InlineAccess = enum {
/// Parse the attribute into the element's CSSStyleProperties (the object
/// `el.style` hands out) and keep it, so repeat probes on the same element
/// cost a list lookup. Layout reads that object directly (getElementAxis,
/// horizontalPosition), so every JS-reachable path must materialize.
materialize,
/// Fold the attribute text on each call; allocates nothing. For
/// Zig-initiated tooling that walks the whole document once per turn
/// (markdown, dump, tree), where pinning a parsed declaration list in the
/// page arena for every inline-styled element outlives its use. An
/// object JS already created is still read.
scan,
};
// `frame` is the StyleManager's frame, which callers guarantee is el's owner
// frame (el.ownerFrame) — the map where the materialized style lives.
fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty {
fn inlineValue(el: *Element, property_name: String, comptime access: InlineAccess, frame: *Frame) ?[]const u8 {
if (!el._flags.has_inline_style) {
// Neither a style object nor a style attribute; skip both lookups.
return null;
}
const style = el.getStyle(frame) orelse blk: {
// No JS-set style object and no style attribute -> nothing inline to read.
if (el.getAttributeSafe(comptime .wrap("style")) == null) return null;
break :blk el.getOrCreateStyle(frame) catch |err| {
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
return null;
};
};
return style.asCSSStyleDeclaration().findProperty(property_name);
if (el.getStyle(frame)) |style| {
return styleValue(style, property_name);
}
// No JS-set style object and no style attribute -> nothing inline to read.
const attr = el.getAttributeSafe(comptime .wrap("style")) orelse return null;
switch (access) {
.materialize => {
const style = el.getOrCreateStyle(frame) catch |err| {
log.err(.browser, "StyleManager getOrCreateStyle", .{ .err = err });
return null;
};
return styleValue(style, property_name);
},
.scan => return scanInlineValue(attr, property_name.str()),
}
}
fn styleValue(style: *CSSStyleProperties, property_name: String) ?[]const u8 {
const property = style.asCSSStyleDeclaration().findProperty(property_name) orelse return null;
return property._value.str();
}
// Must agree with CSSStyleDeclaration.applyDeclarations, which is what the
// materialized object was built from.
fn scanInlineValue(attr: []const u8, property_name: []const u8) ?[]const u8 {
var value: ?[]const u8 = null;
var important = false;
var it = CssParser.parseDeclarationsList(attr);
while (it.next()) |declaration| {
if (std.ascii.eqlIgnoreCase(declaration.name, property_name) == false) {
continue;
}
if (important and declaration.important == false) {
continue;
}
value = declaration.value;
important = declaration.important;
}
return value;
}
/// Resolved value of an element's inline `style=` declaration for `property_name`,
@@ -1214,8 +1261,7 @@ fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*
/// inline style (the same source `el.style` exposes), so `getComputedStyle` and
/// `el.style` agree on inline values instead of resolving them independently.
pub fn inlineStyleValue(self: *StyleManager, el: *Element, property_name: String) ?[]const u8 {
const property = getInlineStyleProperty(el, property_name, self.frame) orelse return null;
return property._value.str();
return inlineValue(el, property_name, .materialize, self.frame);
}
/// Bounds computedFontSize's ancestor recursion (the parent walk and
@@ -1437,3 +1483,39 @@ test "StyleManager: packed priority bounds" {
// Real layer ranks fit the 12 rank bits below the unlayered sentinel.
try testing.expect(MAX_LAYERS < UNLAYERED_RANK);
}
test "StyleManager: inlineValue: scan matches materialize" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
const div = try frame.window._document.createElement("div", null, frame);
try Frame.parse.htmlAsChildren(frame, div.asNode(),
\\<i style="display:none"></i>
\\<i style="color:red; DISPLAY : none !important ; display:block"></i>
\\<i style="display:block;display:none"></i>
\\<i style="display: none; display: /* c */ block"></i>
\\<i style="visibility:hidden"></i>
\\<i style="display:"></i>
\\<i></i>
);
const expected = [_]?[]const u8{ "none", "none", "none", "block", null, null, null };
var i: usize = 0;
var child = div.asNode().firstChild();
while (child) |node| : (child = node.nextSibling()) {
const el = node.is(Element) orelse continue;
const scanned = inlineValue(el, comptime .wrap("display"), .scan, frame);
// scanning never creates the style object
try testing.expectEqual(null, el.getStyle(frame));
const materialized = inlineValue(el, comptime .wrap("display"), .materialize, frame);
if (expected[i]) |value| {
try testing.expectEqual(value, scanned);
try testing.expectEqual(value, materialized);
} else {
try testing.expectEqual(null, scanned);
try testing.expectEqual(null, materialized);
}
i += 1;
}
try testing.expectEqual(expected.len, i);
}
+1 -1
View File
@@ -384,7 +384,7 @@ fn shouldStripElement(el: *Node.Element, opts: Opts, frame: *Frame) bool {
if (std.mem.eql(u8, tag_name, "iframe")) return true;
}
if (opts.strip.invisible and frame._style_manager.hasAuthorDisplayNone(el)) {
if (opts.strip.invisible and frame._style_manager.hasAuthorDisplayNone(el, .scan)) {
return true;
}
+84 -10
View File
@@ -65,7 +65,7 @@ fn isLayoutBlock(tag: Element.Tag) bool {
};
}
pub fn isStandaloneAnchor(el: *Element) bool {
pub fn isStandaloneAnchor(el: *Element, frame: *Frame) bool {
const node = el.asNode();
const parent = node.parentNode() orelse return false;
const parent_el = parent.is(Element) orelse return false;
@@ -76,7 +76,7 @@ pub fn isStandaloneAnchor(el: *Element) bool {
while (prev) |p| : (prev = p.previousSibling()) {
if (isSignificantText(p)) return false;
if (p.is(Element)) |pe| {
if (isVisibleElement(pe)) break;
if (isVisibleElement(pe, frame)) break;
}
}
@@ -84,7 +84,7 @@ pub fn isStandaloneAnchor(el: *Element) bool {
while (next) |n| : (next = n.nextSibling()) {
if (isSignificantText(n)) return false;
if (n.is(Element)) |ne| {
if (isVisibleElement(ne)) break;
if (isVisibleElement(ne, frame)) break;
}
}
@@ -96,9 +96,16 @@ fn isSignificantText(node: *Node) bool {
return !isAllWhitespace(text.ownData());
}
fn isVisibleElement(el: *Element) bool {
// Own state only; the dump root is exempt so a scoped dump of a hidden
// subtree still renders it.
fn isVisibleElement(el: *Element, frame: *Frame) bool {
const tag = el.getTag();
return !tag.isMetadata() and tag != .svg;
if (tag.isMetadata() or tag == .svg) return false;
if (frame._style_manager.hasDisplayNone(el, .scan)) return false;
if (el.getAttributeSafe(comptime .wrap("aria-hidden"))) |v| {
if (std.ascii.eqlIgnoreCase(v, "true")) return false;
}
return true;
}
fn getAnchorLabel(el: *Element) ?[]const u8 {
@@ -110,7 +117,7 @@ pub const ContentInfo = struct {
has_block: bool,
};
pub fn analyzeContent(root: *Node) ContentInfo {
pub fn analyzeContent(root: *Node, frame: *Frame) ContentInfo {
var result: ContentInfo = .{ .has_visible = false, .has_block = false };
var tw = TreeWalker.FullExcludeSelf.init(root, .{});
while (tw.next()) |node| {
@@ -118,7 +125,7 @@ pub fn analyzeContent(root: *Node) ContentInfo {
result.has_visible = true;
if (result.has_block) return result;
} else if (node.is(Element)) |el| {
if (!isVisibleElement(el)) {
if (!isVisibleElement(el, frame)) {
tw.skipChildren();
} else {
const tag = el.getTag();
@@ -140,6 +147,7 @@ const Context = struct {
state: State,
writer: *std.Io.Writer,
frame: *Frame,
root: *Node,
// When there's a slot-attribute, we skip rendering, unless this flag has
// bet set to true.
@@ -203,7 +211,7 @@ const Context = struct {
const tag = el.getTag();
if (!isVisibleElement(el)) return;
if (el.asNode() != self.root and !isVisibleElement(el, self.frame)) return;
if (!force_slot) {
if (el.getAttributeSafe(comptime .wrap("slot")) != null) {
@@ -326,7 +334,7 @@ const Context = struct {
},
.anchor => {
const frame = self.frame;
const info = analyzeContent(el.asNode());
const info = analyzeContent(el.asNode(), frame);
const label = getAnchorLabel(el);
const href_raw = el.getAttributeSafe(comptime .wrap("href"));
@@ -348,7 +356,7 @@ const Context = struct {
return;
}
const standalone = isStandaloneAnchor(el);
const standalone = isStandaloneAnchor(el, frame);
if (standalone) {
if (!self.state.last_char_was_newline) try self.writer.writeByte('\n');
}
@@ -515,6 +523,7 @@ pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !voi
.state = .{},
.writer = &lw.writer,
.frame = frame,
.root = node,
};
ctx.render(node) catch |err| switch (err) {
error.WriteFailed => {
@@ -533,6 +542,7 @@ pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !voi
.state = .{},
.writer = writer,
.frame = frame,
.root = node,
};
try ctx.render(node);
if (!ctx.state.last_char_was_newline) {
@@ -783,6 +793,70 @@ test "browser.markdown: anchor fallback label" {
, "[](http://localhost/no-label)\n");
}
test "browser.markdown: hidden elements are skipped" {
try testMarkdownHTML(
\\<p>before</p>
\\<p style="display:none">inline</p>
\\<div hidden><p>attribute</p></div>
\\<p aria-hidden="true">aria</p>
\\<span aria-hidden="TRUE">aria caps</span>
\\<p aria-hidden="false">aria false</p>
\\<details><summary>Summary</summary><p>collapsed</p></details><dialog><p>closed dialog</p></dialog><p>after</p>
,
\\
\\before
\\
\\aria false
\\Summary
\\
\\after
\\
);
}
test "browser.markdown: stylesheet display:none is skipped" {
var page = try testing.pageTest("dump.html", .{});
defer page.close();
const frame = page.frame().?;
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
try dump(frame.window._document.asNode(), .{}, &aw.writer, frame);
try testing.expectString(
\\
\\# Title
\\![]()
\\
\\visible & well
\\
, aw.written());
}
test "browser.markdown: anchor with only hidden content falls back to label" {
try testMarkdownHTML(
\\<a href="/x" aria-label="Label"><span hidden>secret</span></a>
, "[Label](http://localhost/x)\n");
}
test "browser.markdown: scoped dump of a hidden subtree still renders it" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
frame.url = "http://localhost/";
const doc = frame.window._document;
const div = try doc.createElement("div", null, frame);
try Frame.parse.htmlAsChildren(frame, div.asNode(),
\\<div id="modal" style="display:none"><p>dialog text</p><p hidden>nested hidden</p></div>
);
const modal = div.asNode().firstChild().?;
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
defer aw.deinit();
try dump(modal, .{}, &aw.writer, frame);
try testing.expectString("\ndialog text\n", aw.written());
}
test "browser.markdown: max_bytes leaves output untouched when under cap" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
+2 -2
View File
@@ -556,13 +556,13 @@ const Builder = struct {
.anchor => {
const href = el.getAttributeSafe(comptime .wrap("href"));
const label = el.getAttributeSafe(comptime .wrap("aria-label")) orelse el.getAttributeSafe(comptime .wrap("title"));
const info = markdown.analyzeContent(el.asNode());
const info = markdown.analyzeContent(el.asNode(), self.frame);
if (!info.has_visible and label == null) return;
// Same split as markdown: an anchor wrapping blocks, or one
// sitting among element-only siblings (nav bars, post lists),
// gets its own tight block instead of flowing inline.
const standalone = info.has_block or markdown.isStandaloneAnchor(el);
const standalone = info.has_block or markdown.isStandaloneAnchor(el, self.frame);
if (standalone) {
try self.closeBlock();
self.tight += 1;
@@ -271,6 +271,29 @@
}
</script>
<script id="ua_closed_dialog_is_hidden">
{
// dialog:not([open]) { display: none }
const dialog = document.createElement('dialog');
const child = document.createElement('p');
dialog.appendChild(child);
document.body.appendChild(dialog);
testing.expectEqual(false, dialog.checkVisibility());
testing.expectEqual(false, child.checkVisibility());
testing.expectEqual('none', window.getComputedStyle(dialog).display);
dialog.show();
testing.expectEqual(true, dialog.checkVisibility());
testing.expectEqual(true, child.checkVisibility());
dialog.close();
testing.expectEqual(false, dialog.checkVisibility());
dialog.remove();
}
</script>
<script id="hidden_attribute_propagates_through_check_visibility">
{
// [hidden] { display: none } applies to the element itself...
+1 -1
View File
@@ -916,7 +916,7 @@ fn elementFromPointImpl(self: *Document, x: f64, y: f64, ignore_x: bool, frame:
preorder_index += 1;
if (node.is(Element)) |element| {
if (element.checkVisibilityCached(&visibility_cache, frame)) {
if (element.checkVisibilityCached(&visibility_cache, frame, .materialize)) {
if (y >= pos and y <= pos + element.boxAxis(frame, .height)) {
if (ignore_x) {
topmost = element;
+14 -14
View File
@@ -1128,7 +1128,7 @@ pub fn focus(self: *Element, frame: *Frame) !void {
// Per HTML spec §6.4.4, an element must be "being rendered" (not
// display:none on self or any ancestor) to be focusable.
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return;
}
@@ -1329,8 +1329,8 @@ pub fn hasPointerEventsNone(self: *Element, cache: ?*PointerEventsCache, frame:
return self.ownerFrame(frame)._style_manager.hasPointerEventsNone(self, cache);
}
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, frame: *Frame) bool {
return !self.ownerFrame(frame)._style_manager.isHidden(self, cache, .{});
pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, frame: *Frame, comptime access: StyleManager.InlineAccess) bool {
return !self.ownerFrame(frame)._style_manager.isHidden(self, cache, .{}, access);
}
// The element's own display:none only, no ancestor walk. For a child or
@@ -1338,7 +1338,7 @@ pub fn checkVisibilityCached(self: *Element, cache: ?*VisibilityCache, frame: *F
// answer: they share the visible ancestor chain — and the owner frame, which
// the caller resolves once rather than per element.
fn isVisibleSelf(self: *Element, style_manager: *StyleManager) bool {
return !style_manager.hasDisplayNone(self);
return !style_manager.hasDisplayNone(self, .materialize);
}
const CheckVisibilityOpts = struct {
@@ -1352,7 +1352,7 @@ pub fn checkVisibility(self: *Element, opts_: ?CheckVisibilityOpts, frame: *Fram
return !self.ownerFrame(frame)._style_manager.isHidden(self, null, .{
.check_opacity = opts.checkOpacity or opts.opacityProperty,
.check_visibility = opts.visibilityProperty or opts.checkVisibilityCSS,
});
}, .materialize);
}
pub const Axis = enum {
@@ -1398,14 +1398,14 @@ pub fn getElementAxis(self: *Element, frame: *Frame, comptime axis: Axis) Axis.S
// width / height treshold is reached. If the size isn't explicit, we fallback
// to the content size.
pub fn getClientWidth(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
return self.boxAxis(frame, .width);
}
pub fn getClientHeight(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
return self.boxAxis(frame, .height);
@@ -1436,7 +1436,7 @@ pub fn getBoundingClientRect(self: *Element, frame: *Frame) !*DOMRect {
// getBoundingClientRect, getClientRects, and IntersectionObserver. A DOMRect is
// only materialized at the JS boundary.
pub fn boundingClientRectValues(self: *Element, frame: *Frame) DOMRect.Data {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return .{};
}
return self.boundingClientRectValuesForVisible(frame);
@@ -1453,7 +1453,7 @@ pub fn boundingClientRectValuesForVisible(self: *Element, frame: *Frame) DOMRect
}
pub fn getClientRects(self: *Element, frame: *Frame) ![]*DOMRect {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return &.{};
}
const rects = try frame.local_arena.alloc(*DOMRect, 1);
@@ -1505,7 +1505,7 @@ pub fn setScrollLeft(self: *Element, value: i32, frame: *Frame) !void {
}
pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
@@ -1522,7 +1522,7 @@ pub fn getScrollHeight(self: *Element, frame: *Frame) f64 {
}
pub fn getScrollWidth(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
@@ -1590,21 +1590,21 @@ pub fn getOffsetWidth(self: *Element, frame: *Frame) f64 {
}
pub fn getOffsetTop(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
return calculateDocumentPosition(self.asNode());
}
pub fn getOffsetLeft(self: *Element, frame: *Frame) f64 {
if (!self.checkVisibilityCached(null, frame)) {
if (!self.checkVisibilityCached(null, frame, .materialize)) {
return 0.0;
}
return self.horizontalPosition(frame);
}
pub fn getOffsetParent(self: *Element, frame: *Frame) ?*Element {
if (!self.asNode().isConnected() or !self.checkVisibilityCached(null, frame)) {
if (!self.asNode().isConnected() or !self.checkVisibilityCached(null, frame, .materialize)) {
return null;
}
+1 -1
View File
@@ -162,7 +162,7 @@ pub fn deliverEntries(self: *ResizeObserver, frame: *Frame) !void {
obs.connected = connected;
const width, const height = blk: {
if (!connected or !target.checkVisibilityCached(&visibility_cache, frame)) {
if (!connected or !target.checkVisibilityCached(&visibility_cache, frame, .materialize)) {
break :blk .{ 0, 0 };
}
break :blk .{
@@ -84,7 +84,7 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
if (self._element) |element| {
const style_manager = &element.ownerFrame(frame)._style_manager;
if (wrapped.eql(comptime .wrap("display"))) {
if (style_manager.hasDisplayNone(element)) return "none";
if (style_manager.hasDisplayNone(element, .materialize)) return "none";
} else if (wrapped.eql(comptime .wrap("visibility"))) {
if (style_manager.hasVisibilityHiddenInherited(element)) return "hidden";
}
@@ -122,7 +122,7 @@ pub fn getPropertyValue(self: *const CSSStyleDeclaration, property_name: []const
}
fn resolvedDimension(element: *Element, dimension: enum { width, height }, frame: *Frame) []const u8 {
if (!element.checkVisibilityCached(null, frame)) {
if (!element.checkVisibilityCached(null, frame, .materialize)) {
return "auto";
}
const value = switch (dimension) {
+1 -1
View File
@@ -1612,7 +1612,7 @@ fn handleChildElement(
// is hidden through its parent. If you can el.innerText on an element, the
// visibility of el.parent doesn't matter. So we only care about visibility
// on the element itself and then on each child. This is much simpler too.
if (state.frame._style_manager.hasDisplayNone(he.asElement())) {
if (state.frame._style_manager.hasDisplayNone(he.asElement(), .materialize)) {
return;
}
+1 -1
View File
@@ -1300,7 +1300,7 @@ fn isHidden(elt: *DOMNode.Element, frame: *Frame, cache: *DOMNode.Element.Visibi
// CSS display:none and visibility:hidden (both inherited from ancestors via
// style computation). Matches Chromium's AX tree which prunes both.
if (frame._style_manager.isHidden(elt, cache, .{ .check_visibility = true })) {
if (frame._style_manager.isHidden(elt, cache, .{ .check_visibility = true }, .scan)) {
return true;
}