Merge pull request #2796 from lightpanda-io/strip-mode-invisible

cli: add strip-mode invisible
This commit is contained in:
Karl Seguin
2026-06-24 06:53:17 +08:00
committed by GitHub
4 changed files with 90 additions and 9 deletions

View File

@@ -326,6 +326,15 @@ pub fn hasDisplayNone(self: *StyleManager, el: *Element) bool {
return self.isElementHidden(el, .{});
}
/// 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 {
self.rebuildIfDirty() catch return false;
return self.isElementHidden(el, .{ .ua_display_none = false });
}
/// Centralizes UA-stylesheet display:none truth so `getComputedStyle().display`
/// (via `hasDisplayNone`) and `el.checkVisibility()` (via `isHidden`) agree.
/// Spec: HTML Rendering §15.3.1 "Hidden elements".
@@ -523,8 +532,10 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
// element — per CSS Cascade §6.1 any normal-origin author rule beats UA
// origin regardless of specificity, so `.x { display: flex }` on a
// `<div class="x" hidden>` must report visible.
if (options.check_display and display_priority == 0) {
if (matchesUaDisplayNoneRule(el)) display_none = true;
if (options.check_display and options.ua_display_none and display_priority == 0) {
if (matchesUaDisplayNoneRule(el)) {
display_none = true;
}
}
return (display_none orelse false) or (visibility_hidden orelse false) or (opacity_zero orelse false);
@@ -846,6 +857,7 @@ const CheckVisibilityOptions = struct {
check_display: bool = true,
check_visibility: bool = false,
check_opacity: bool = false,
ua_display_none: bool = true,
};
// Inline styles always win over stylesheets - use max u64 as sentinel

View File

@@ -30,10 +30,11 @@ pub const Opts = struct {
strip: Opts.Strip = .{},
shadow: Opts.Shadow = .rendered,
pub const Strip = packed struct(u3) {
pub const Strip = packed struct(u4) {
js: bool = false,
ui: bool = false,
css: bool = false,
invisible: bool = false,
};
pub const Shadow = enum {
@@ -99,7 +100,7 @@ fn _deep(node: *Node, opts: Opts, comptime force_slot: bool, writer: *std.Io.Wri
}
},
.element => |el| {
if (shouldStripElement(el, opts)) {
if (shouldStripElement(el, opts, frame)) {
return;
}
@@ -265,7 +266,12 @@ fn isVoidElement(el: *const Node.Element) bool {
};
}
fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
fn shouldStripElement(el: *Node.Element, opts: Opts, frame: *Frame) bool {
// Fast path: with no strip flags set (every innerHTML/outerHTML call)
if (@as(u4, @bitCast(opts.strip)) == 0) {
return false;
}
const tag_name = el.getTagNameDump();
if (opts.strip.js) {
@@ -306,6 +312,10 @@ fn shouldStripElement(el: *const Node.Element, opts: Opts) bool {
if (std.mem.eql(u8, tag_name, "iframe")) return true;
}
if (opts.strip.invisible and frame._style_manager.hasAuthorDisplayNone(el)) {
return true;
}
return false;
}
@@ -358,3 +368,60 @@ fn writeEscapedByte(input: []const u8, index: usize, writer: *std.Io.Writer) ![]
}
return input[index + 1 ..];
}
const testing = @import("../testing.zig");
// A fresh page per assertion: `with_base` mutates the document (it inserts a
// <base> element), so reusing one frame across opts would leak that mutation
// into later dumps.
fn expectDump(opts: Opts, expected: []const u8) !void {
var frame = try testing.pageTest("dump.html", .{});
defer testing.reset();
defer frame._session.removePage();
var aw: std.Io.Writer.Allocating = .init(testing.arena_allocator);
try root(frame.window._document, opts, &aw.writer, frame);
try testing.expectString(expected, aw.written());
}
test "dump: default dumps the whole document" {
try expectDump(.{},
\\<!DOCTYPE html>
\\<html><head><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible &amp; well</p></body></html>
);
}
test "dump: with_base injects a <base> element" {
try expectDump(.{ .with_base = true },
\\<!DOCTYPE html>
\\<html><head><base base="http://127.0.0.1:9582/src/browser/tests/dump.html"></base><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible &amp; well</p></body></html>
);
}
test "dump: strip.js removes script and noscript" {
try expectDump(.{ .strip = .{ .js = true } },
\\<!DOCTYPE html>
\\<html><head><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><p>visible &amp; well</p></body></html>
);
}
test "dump: strip.css removes style and stylesheet links" {
try expectDump(.{ .strip = .{ .css = true } },
\\<!DOCTYPE html>
\\<html><head><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible &amp; well</p></body></html>
);
}
test "dump: strip.ui removes css plus visual elements" {
try expectDump(.{ .strip = .{ .ui = true } },
\\<!DOCTYPE html>
\\<html><head><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><noscript>nojs</noscript><p>visible &amp; well</p></body></html>
);
}
test "dump: strip.invisible removes author display:none elements" {
try expectDump(.{ .strip = .{ .invisible = true } },
\\<!DOCTYPE html>
\\<html><head><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><img><svg></svg><noscript>nojs</noscript><p>visible &amp; well</p></body></html>
);
}

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html><head><style>.hidden{display:none}</style><link rel="stylesheet" href="data:text/css,"><script>var a=1;</script></head><body><h1>Title</h1><p class="hidden">secret</p><img><svg></svg><noscript>nojs</noscript><p>visible &amp; well</p></body></html>

View File

@@ -63,10 +63,11 @@
\\ Comma-separated list of tag groups to remove from dump.
\\ Defaults to no-strip.
\\ Allowed values:
\\ js script and link[as=script, rel=preload].
\\ ui Includes img, picture, video, CSS and SVG.
\\ css Includes style and link[rel=stylesheet].
\\ full Strip everything.
\\ js Script and link[as=script, rel=preload].
\\ ui Includes img, picture, video, CSS and SVG.
\\ css Includes style and link[rel=stylesheet].
\\ invisible Best-effort (e.g. display:none) hidden elements
\\ full Strip everything.
\\ --json
\\ Capture and print the status of the fetch in a JSON string and output
\\ it. When used with --dump <MODE> this will wrap the dumped content