mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-21 12:05:40 -04:00
Merge pull request #3269 from lightpanda-io/markdown-hidden-content
markdown: skip hidden elements
This commit is contained in:
12 files changed
+242
-63
No files matched your search
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user