mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -258,6 +258,10 @@ fn installNewActivePage(self: *Session, frame_id: u32) !*Frame {
|
||||
// the pointer on Frame is just returned as a convenience
|
||||
pub fn createPage(self: *Session) !*Frame {
|
||||
lp.assert(self._active == null, "Session.createPage - page not null", .{});
|
||||
|
||||
// Drain any pending Page deinits now, while we're at a known-safe point
|
||||
self.processQueuedDestroyed();
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.browser, "create page", .{});
|
||||
}
|
||||
|
||||
@@ -89,7 +89,13 @@ fn parseSheet(self: *StyleManager, sheet: *CSSStyleSheet) !void {
|
||||
const text = try style.asNode().getTextContentAlloc(self.arena);
|
||||
var it = CssParser.parseStylesheet(text);
|
||||
while (it.next()) |parsed_rule| {
|
||||
try self.addRawRule(parsed_rule.selector, parsed_rule.block);
|
||||
// StyleManager only filters on regular style rules (display,
|
||||
// visibility, opacity). At-rules don't carry top-level
|
||||
// declarations relevant here -- skip them.
|
||||
switch (parsed_rule) {
|
||||
.style => |s| try self.addRawRule(s.selector, s.block),
|
||||
.at_rule => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,11 +294,36 @@ fn isBang(token: Tokenizer.Token) bool {
|
||||
};
|
||||
}
|
||||
|
||||
pub const Rule = struct {
|
||||
pub const StyleRule = struct {
|
||||
selector: []const u8,
|
||||
block: []const u8,
|
||||
};
|
||||
|
||||
/// An at-rule (`@keyframes`, `@media`, `@supports`, `@font-face`, etc.).
|
||||
///
|
||||
/// We don't apply at-rules to the page (the CSS engine doesn't process them
|
||||
/// yet), but we do surface them so that JS-side reads via `cssRules` see
|
||||
/// what was inserted. CSS-in-JS libraries (styled-components, emotion,
|
||||
/// Stitches, Mantine) deduplicate their stylesheets by reading back
|
||||
/// `cssRules` after `insertRule` -- if the rule is missing they fall back to
|
||||
/// per-render `<style>` element injection, which leaks unboundedly. See
|
||||
/// lightpanda-io/browser#2459.
|
||||
pub const AtRule = struct {
|
||||
/// At-keyword without the leading `@` (e.g., `"keyframes"`, `"media"`,
|
||||
/// `"-webkit-keyframes"`). Borrowed from the input slice; copy if you
|
||||
/// need to outlive the input.
|
||||
keyword: []const u8,
|
||||
/// Full at-rule source span starting at `@` and ending after the closing
|
||||
/// brace (block at-rules) or semicolon (statement at-rules). Borrowed
|
||||
/// from the input slice.
|
||||
text: []const u8,
|
||||
};
|
||||
|
||||
pub const Rule = union(enum) {
|
||||
style: StyleRule,
|
||||
at_rule: AtRule,
|
||||
};
|
||||
|
||||
pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||
return RulesIterator.init(input);
|
||||
}
|
||||
@@ -306,7 +331,6 @@ pub fn parseStylesheet(input: []const u8) RulesIterator {
|
||||
pub const RulesIterator = struct {
|
||||
input: []const u8,
|
||||
stream: TokenStream,
|
||||
has_skipped_at_rule: bool = false,
|
||||
|
||||
pub fn init(input: []const u8) RulesIterator {
|
||||
return .{
|
||||
@@ -352,18 +376,14 @@ pub const RulesIterator = struct {
|
||||
var selector = self.input[selector_start.?..selector_end.?];
|
||||
selector = std.mem.trim(u8, selector, &std.ascii.whitespace);
|
||||
|
||||
return .{
|
||||
return .{ .style = .{
|
||||
.selector = selector,
|
||||
.block = self.input[block_start..block_end],
|
||||
};
|
||||
} };
|
||||
}
|
||||
|
||||
if (peeked.token == .at_keyword) {
|
||||
self.has_skipped_at_rule = true;
|
||||
self.skipAtRule();
|
||||
selector_start = null;
|
||||
selector_end = null;
|
||||
continue;
|
||||
return .{ .at_rule = self.consumeAtRule() };
|
||||
}
|
||||
|
||||
if (selector_start == null and (isWhitespaceOrComment(peeked.token) or isSemicolon(peeked.token))) {
|
||||
@@ -395,29 +415,48 @@ pub const RulesIterator = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn skipAtRule(self: *RulesIterator) void {
|
||||
_ = self.stream.next(); // consume @keyword
|
||||
/// Consume a full at-rule (statement or block form) and return its
|
||||
/// keyword (without `@`) and full source span. Mirrors `skipAtRule`'s
|
||||
/// termination logic but records spans instead of discarding.
|
||||
fn consumeAtRule(self: *RulesIterator) AtRule {
|
||||
const at_span = self.stream.next() orelse unreachable; // caller peeked an at_keyword
|
||||
const start = at_span.start;
|
||||
const keyword = switch (at_span.token) {
|
||||
.at_keyword => |name| name,
|
||||
else => "",
|
||||
};
|
||||
var end = at_span.end;
|
||||
var depth: usize = 0;
|
||||
var saw_block = false;
|
||||
|
||||
while (true) {
|
||||
const peeked = self.stream.peek() orelse return;
|
||||
const peeked = self.stream.peek() orelse break;
|
||||
if (!saw_block and isSemicolon(peeked.token) and depth == 0) {
|
||||
_ = self.stream.next();
|
||||
return;
|
||||
const semi = self.stream.next() orelse break;
|
||||
end = semi.end;
|
||||
break;
|
||||
}
|
||||
|
||||
const span = self.stream.next() orelse return;
|
||||
if (isWhitespaceOrComment(span.token)) continue;
|
||||
const span = self.stream.next() orelse break;
|
||||
if (isWhitespaceOrComment(span.token)) {
|
||||
end = span.end;
|
||||
continue;
|
||||
}
|
||||
|
||||
end = span.end;
|
||||
if (span.token == .curly_bracket_block) {
|
||||
depth += 1;
|
||||
saw_block = true;
|
||||
} else if (span.token == .close_curly_bracket) {
|
||||
if (depth > 0) depth -= 1;
|
||||
if (saw_block and depth == 0) return;
|
||||
if (saw_block and depth == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.keyword = keyword,
|
||||
.text = self.input[start..end],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -426,8 +465,8 @@ const testing = std.testing;
|
||||
test "RulesIterator: single rule" {
|
||||
var it = RulesIterator.init(".test { color: red; }");
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||
try testing.expectEqualStrings(".test", rule.style.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.style.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
@@ -435,31 +474,47 @@ test "RulesIterator: multiple rules" {
|
||||
var it = RulesIterator.init("h1 { margin: 0; } p { padding: 10px; }");
|
||||
|
||||
var rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("h1", rule.selector);
|
||||
try testing.expectEqualStrings(" margin: 0; ", rule.block);
|
||||
try testing.expectEqualStrings("h1", rule.style.selector);
|
||||
try testing.expectEqualStrings(" margin: 0; ", rule.style.block);
|
||||
|
||||
rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("p", rule.selector);
|
||||
try testing.expectEqualStrings(" padding: 10px; ", rule.block);
|
||||
try testing.expectEqualStrings("p", rule.style.selector);
|
||||
try testing.expectEqualStrings(" padding: 10px; ", rule.style.block);
|
||||
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: skips at-rules without block" {
|
||||
test "RulesIterator: surfaces statement at-rules" {
|
||||
var it = RulesIterator.init("@import url('style.css'); .test { color: red; }");
|
||||
|
||||
const at = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("import", at.at_rule.keyword);
|
||||
try testing.expectEqualStrings("@import url('style.css');", at.at_rule.text);
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.block);
|
||||
try testing.expectEqualStrings(".test", rule.style.selector);
|
||||
try testing.expectEqualStrings(" color: red; ", rule.style.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: skips at-rules with block" {
|
||||
test "RulesIterator: surfaces block at-rules" {
|
||||
var it = RulesIterator.init("@media screen { .test { color: blue; } } .test2 { color: green; }");
|
||||
|
||||
const at = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("media", at.at_rule.keyword);
|
||||
try testing.expectEqualStrings("@media screen { .test { color: blue; } }", at.at_rule.text);
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test2", rule.selector);
|
||||
try testing.expectEqualStrings(" color: green; ", rule.block);
|
||||
try testing.expectEqualStrings(".test2", rule.style.selector);
|
||||
try testing.expectEqualStrings(" color: green; ", rule.style.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: surfaces vendor-prefixed at-rules" {
|
||||
var it = RulesIterator.init("@-webkit-keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }");
|
||||
|
||||
const at = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("-webkit-keyframes", at.at_rule.keyword);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
@@ -467,17 +522,17 @@ test "RulesIterator: comments and whitespace" {
|
||||
var it = RulesIterator.init(" /* comment */ .test /* comment */ { /* comment */ color: red; } \n\t");
|
||||
|
||||
const rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings(".test", rule.selector);
|
||||
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.block);
|
||||
try testing.expectEqualStrings(".test", rule.style.selector);
|
||||
try testing.expectEqualStrings(" /* comment */ color: red; ", rule.style.block);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
test "RulesIterator: top-level semicolons" {
|
||||
var it = RulesIterator.init("*{}; ; p{}");
|
||||
var rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("*", rule.selector);
|
||||
try testing.expectEqualStrings("*", rule.style.selector);
|
||||
|
||||
rule = it.next() orelse return error.MissingRule;
|
||||
try testing.expectEqualStrings("p", rule.selector);
|
||||
try testing.expectEqualStrings("p", rule.style.selector);
|
||||
try testing.expectEqual(@as(?Rule, null), it.next());
|
||||
}
|
||||
|
||||
@@ -555,3 +555,126 @@
|
||||
testing.expectTrue(cssText.includes('}'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_keyframes_surfaces">
|
||||
{
|
||||
// Regression test for #2459: at-rules must surface in cssRules so that
|
||||
// CSS-in-JS libraries (styled-components, emotion, ...) don't fall back
|
||||
// to per-render <style> element injection. Before the fix insertRule
|
||||
// silently swallowed at-rules and returned a fake index, leaving
|
||||
// cssRules.length at 0.
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
const idx = sheet.insertRule('@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }');
|
||||
testing.expectEqual(0, idx);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
|
||||
const rule = sheet.cssRules[0];
|
||||
testing.expectEqual(CSSRule.KEYFRAMES_RULE, rule.type);
|
||||
testing.expectTrue(rule.cssText.includes('@keyframes'));
|
||||
testing.expectTrue(rule.cssText.includes('spin'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_media_surfaces">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
const idx = sheet.insertRule('@media screen and (min-width: 600px) { .test { color: red; } }');
|
||||
testing.expectEqual(0, idx);
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.MEDIA_RULE, sheet.cssRules[0].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_atrule_mixed_with_style">
|
||||
{
|
||||
// CSS-in-JS dedup paths read back cssRules.length and per-index types
|
||||
// after insertion; both must be correct when style and at-rules are
|
||||
// interleaved.
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
sheet.insertRule('.first { color: red; }', 0);
|
||||
sheet.insertRule('@keyframes spin { from {} to {} }', 1);
|
||||
sheet.insertRule('.last { color: blue; }', 2);
|
||||
|
||||
testing.expectEqual(3, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[0].type);
|
||||
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[1].type);
|
||||
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[2].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_vendor_prefixed_keyframes">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
sheet.insertRule('@-webkit-keyframes spin { from {} to {} }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[0].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_supports_surfaces">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
sheet.insertRule('@supports (display: grid) { .grid { display: grid; } }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.SUPPORTS_RULE, sheet.cssRules[0].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_font_face_surfaces">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
sheet.insertRule("@font-face { font-family: 'Test'; src: url('test.woff2'); }");
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.FONT_FACE_RULE, sheet.cssRules[0].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_replaceSync_preserves_atrules">
|
||||
{
|
||||
// replaceSync goes through the same parser path; at-rules in the
|
||||
// replacement source should also surface in cssRules.
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync('.a { color: red; } @keyframes spin { from {} to {} } .b { color: blue; }');
|
||||
|
||||
testing.expectEqual(3, sheet.cssRules.length);
|
||||
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[0].type);
|
||||
testing.expectEqual(CSSRule.KEYFRAMES_RULE, sheet.cssRules[1].type);
|
||||
testing.expectEqual(CSSRule.STYLE_RULE, sheet.cssRules[2].type);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_atrule_repeated_no_growth">
|
||||
{
|
||||
// The CSS-in-JS dedup pattern: insert a hash-keyed at-rule, read back
|
||||
// cssRules to check whether it's already there, skip if so. Before the
|
||||
// fix the read-back was always empty, so the library re-inserted every
|
||||
// render -- and (separately) created a fresh <style> element each time.
|
||||
// After the fix the read-back finds the rule and the library can dedup.
|
||||
const sheet = new CSSStyleSheet();
|
||||
|
||||
sheet.insertRule('@keyframes spin { from {} to {} }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
|
||||
// Library checks cssRules and finds the rule -- doesn't re-insert.
|
||||
const exists = Array.from(sheet.cssRules).some(
|
||||
r => r.type === CSSRule.KEYFRAMES_RULE && r.cssText.includes('spin')
|
||||
);
|
||||
testing.expectTrue(exists);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CSSStyleSheet_insertRule_unknown_at_rule_typed_as_zero">
|
||||
{
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.insertRule('@layer utilities { .x { color: red; } }');
|
||||
testing.expectEqual(1, sheet.cssRules.length);
|
||||
testing.expectEqual(0, sheet.cssRules[0].type);
|
||||
testing.expectTrue(sheet.cssRules[0].cssText.includes('@layer'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,9 +23,16 @@ pub const Type = union(enum) {
|
||||
font_feature_values: void,
|
||||
viewport: void,
|
||||
region_style: void,
|
||||
unknown: void,
|
||||
};
|
||||
|
||||
_type: Type,
|
||||
/// Original source text for at-rules (`@keyframes`, `@media`, ...). Empty
|
||||
/// for `.style` because `CSSStyleRule.getCssText` constructs its own
|
||||
/// serialization from selector + declarations. The bridge dispatches the
|
||||
/// most-derived `cssText` accessor (CSSStyleRule's), so this field only
|
||||
/// surfaces for opaque at-rule placeholders. See lightpanda-io/browser#2459.
|
||||
_text: []const u8 = "",
|
||||
|
||||
pub fn as(self: *CSSRule, comptime T: type) *T {
|
||||
return self.is(T).?;
|
||||
@@ -44,12 +51,26 @@ pub fn init(rule_type: Type, frame: *Frame) !*CSSRule {
|
||||
});
|
||||
}
|
||||
|
||||
/// Construct an at-rule placeholder with stored source text. Used when an
|
||||
/// `@keyframes` / `@media` / etc. lands via `insertRule` or `replaceSync`
|
||||
/// so that JS-side reads via `cssRules` see the rule (matching length and
|
||||
/// `cssText`) without the CSS engine actually applying it.
|
||||
pub fn initAtRule(rule_type: Type, text: []const u8, frame: *Frame) !*CSSRule {
|
||||
return frame._factory.create(CSSRule{
|
||||
._type = rule_type,
|
||||
._text = try frame.dupeString(text),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getType(self: *const CSSRule) u16 {
|
||||
if (self._type == .unknown) {
|
||||
return 0;
|
||||
}
|
||||
return @as(u16, @intFromEnum(std.meta.activeTag(self._type))) + 1;
|
||||
}
|
||||
|
||||
pub fn getCssText(_: *const CSSRule, _: *Frame) []const u8 {
|
||||
return "";
|
||||
pub fn getCssText(self: *const CSSRule, _: *Frame) []const u8 {
|
||||
return self._text;
|
||||
}
|
||||
|
||||
pub fn getParentRule(_: *const CSSRule) ?*CSSRule {
|
||||
@@ -70,22 +91,27 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const STYLE_RULE = 1;
|
||||
pub const CHARSET_RULE = 2;
|
||||
pub const IMPORT_RULE = 3;
|
||||
pub const MEDIA_RULE = 4;
|
||||
pub const FONT_FACE_RULE = 5;
|
||||
pub const PAGE_RULE = 6;
|
||||
pub const KEYFRAMES_RULE = 7;
|
||||
pub const KEYFRAME_RULE = 8;
|
||||
pub const MARGIN_RULE = 9;
|
||||
pub const NAMESPACE_RULE = 10;
|
||||
pub const COUNTER_STYLE_RULE = 11;
|
||||
pub const SUPPORTS_RULE = 12;
|
||||
pub const DOCUMENT_RULE = 13;
|
||||
pub const FONT_FEATURE_VALUES_RULE = 14;
|
||||
pub const VIEWPORT_RULE = 15;
|
||||
pub const REGION_STYLE_RULE = 16;
|
||||
// Spec rule-type constants. Wrapped with `bridge.property(.template)` so
|
||||
// they're exposed on the JS-side `CSSRule` constructor (e.g.
|
||||
// `CSSRule.KEYFRAMES_RULE === 7`). Without the wrapping these are plain
|
||||
// Zig declarations that never reach JS, and code reading them gets
|
||||
// `undefined`.
|
||||
pub const STYLE_RULE = bridge.property(1, .{ .template = true });
|
||||
pub const CHARSET_RULE = bridge.property(2, .{ .template = true });
|
||||
pub const IMPORT_RULE = bridge.property(3, .{ .template = true });
|
||||
pub const MEDIA_RULE = bridge.property(4, .{ .template = true });
|
||||
pub const FONT_FACE_RULE = bridge.property(5, .{ .template = true });
|
||||
pub const PAGE_RULE = bridge.property(6, .{ .template = true });
|
||||
pub const KEYFRAMES_RULE = bridge.property(7, .{ .template = true });
|
||||
pub const KEYFRAME_RULE = bridge.property(8, .{ .template = true });
|
||||
pub const MARGIN_RULE = bridge.property(9, .{ .template = true });
|
||||
pub const NAMESPACE_RULE = bridge.property(10, .{ .template = true });
|
||||
pub const COUNTER_STYLE_RULE = bridge.property(11, .{ .template = true });
|
||||
pub const SUPPORTS_RULE = bridge.property(12, .{ .template = true });
|
||||
pub const DOCUMENT_RULE = bridge.property(13, .{ .template = true });
|
||||
pub const FONT_FEATURE_VALUES_RULE = bridge.property(14, .{ .template = true });
|
||||
pub const VIEWPORT_RULE = bridge.property(15, .{ .template = true });
|
||||
pub const REGION_STYLE_RULE = bridge.property(16, .{ .template = true });
|
||||
|
||||
pub const @"type" = bridge.accessor(CSSRule.getType, null, .{});
|
||||
pub const cssText = bridge.accessor(CSSRule.getCssText, null, .{});
|
||||
|
||||
@@ -81,26 +81,26 @@ pub fn getOwnerRule(self: *const CSSStyleSheet) ?*CSSRule {
|
||||
pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, frame: *Frame) !u32 {
|
||||
const requested_index = maybe_index orelse 0;
|
||||
var it = Parser.parseStylesheet(rule);
|
||||
const parsed_rule = it.next() orelse {
|
||||
if (it.has_skipped_at_rule) {
|
||||
log.debug(.not_implemented, "CSSStyleSheet.insertRule", .{});
|
||||
// Lightpanda currently skips at-rules (e.g., @keyframes, @media) in its
|
||||
// CSS parser. To prevent JS apps (like Expo/Reanimated) from crashing
|
||||
// during initialization, we simulate a successful insertion by returning
|
||||
// the requested index.
|
||||
return requested_index;
|
||||
}
|
||||
return error.SyntaxError;
|
||||
};
|
||||
const parsed_rule = it.next() orelse return error.SyntaxError;
|
||||
|
||||
if (it.next() != null) return error.SyntaxError;
|
||||
|
||||
const style_rule = try CSSStyleRule.init(frame);
|
||||
try style_rule.setSelectorText(parsed_rule.selector, frame);
|
||||
const inserted: *CSSRule = switch (parsed_rule) {
|
||||
.style => |s| blk: {
|
||||
const style_rule = try CSSStyleRule.init(frame);
|
||||
try style_rule.setSelectorText(s.selector, frame);
|
||||
|
||||
const style_props = try style_rule.getStyle(frame);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(parsed_rule.block, frame);
|
||||
const style_props = try style_rule.getStyle(frame);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(s.block, frame);
|
||||
break :blk style_rule._proto;
|
||||
},
|
||||
// Opaque placeholder for at-rules. The CSS engine doesn't apply
|
||||
// these (`@keyframes`, `@media`, ...) but JS-side reads must see
|
||||
// them via `cssRules` so CSS-in-JS libraries don't fall back to
|
||||
// per-render `<style>` injection. See #2459.
|
||||
.at_rule => |a| try CSSRule.initAtRule(atRuleTypeFor(a.keyword), a.text, frame),
|
||||
};
|
||||
|
||||
const rules = try self.getCssRules(frame);
|
||||
|
||||
@@ -113,7 +113,7 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
|
||||
if (index != requested_index) {
|
||||
log.debug(.not_implemented, "insertRule clamped index", .{});
|
||||
}
|
||||
try rules.insert(index, style_rule._proto, frame);
|
||||
try rules.insert(index, inserted, frame);
|
||||
|
||||
// Notify StyleManager that rules have changed
|
||||
frame._style_manager.sheetModified();
|
||||
@@ -121,6 +121,35 @@ pub fn insertRule(self: *CSSStyleSheet, rule: []const u8, maybe_index: ?u32, fra
|
||||
return index;
|
||||
}
|
||||
|
||||
/// Map an at-rule keyword (without `@`) to the matching `CSSRule.Type`
|
||||
/// variant. Vendor prefixes are stripped before matching
|
||||
/// (`-webkit-keyframes` -> `keyframes`). Unrecognized keywords fall back
|
||||
/// to `.unknown`: which maps to 0 for rule.type.
|
||||
fn atRuleTypeFor(keyword_with_prefix: []const u8) CSSRule.Type {
|
||||
var keyword = keyword_with_prefix;
|
||||
inline for (.{ "-webkit-", "-moz-", "-ms-", "-o-" }) |prefix| {
|
||||
if (std.ascii.startsWithIgnoreCase(keyword, prefix)) {
|
||||
keyword = keyword[prefix.len..];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const eql = std.ascii.eqlIgnoreCase;
|
||||
if (eql(keyword, "media")) return .media;
|
||||
if (eql(keyword, "keyframes")) return .keyframes;
|
||||
if (eql(keyword, "supports")) return .supports;
|
||||
if (eql(keyword, "font-face")) return .font_face;
|
||||
if (eql(keyword, "import")) return .import;
|
||||
if (eql(keyword, "charset")) return .charset;
|
||||
if (eql(keyword, "namespace")) return .namespace;
|
||||
if (eql(keyword, "page")) return .frame;
|
||||
if (eql(keyword, "counter-style")) return .counter_style;
|
||||
if (eql(keyword, "font-feature-values")) return .font_feature_values;
|
||||
if (eql(keyword, "viewport")) return .viewport;
|
||||
if (eql(keyword, "document")) return .document;
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
pub fn deleteRule(self: *CSSStyleSheet, index: u32, frame: *Frame) !void {
|
||||
const rules = try self.getCssRules(frame);
|
||||
try rules.remove(index);
|
||||
@@ -141,14 +170,20 @@ pub fn replaceSync(self: *CSSStyleSheet, text: []const u8, frame: *Frame) CSSErr
|
||||
var it = Parser.parseStylesheet(text);
|
||||
var index: u32 = 0;
|
||||
while (it.next()) |parsed_rule| {
|
||||
const style_rule = try CSSStyleRule.init(frame);
|
||||
try style_rule.setSelectorText(parsed_rule.selector, frame);
|
||||
const inserted: *CSSRule = switch (parsed_rule) {
|
||||
.style => |s| blk: {
|
||||
const style_rule = try CSSStyleRule.init(frame);
|
||||
try style_rule.setSelectorText(s.selector, frame);
|
||||
|
||||
const style_props = try style_rule.getStyle(frame);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(parsed_rule.block, frame);
|
||||
const style_props = try style_rule.getStyle(frame);
|
||||
const style = style_props.asCSSStyleDeclaration();
|
||||
try style.setCssText(s.block, frame);
|
||||
break :blk style_rule._proto;
|
||||
},
|
||||
.at_rule => |a| try CSSRule.initAtRule(atRuleTypeFor(a.keyword), a.text, frame),
|
||||
};
|
||||
|
||||
try rules.insert(index, style_rule._proto, frame);
|
||||
try rules.insert(index, inserted, frame);
|
||||
index += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
|
||||
switch (action) {
|
||||
.enable => return enable(cmd),
|
||||
.disable => return disable(cmd),
|
||||
.setCacheDisabled => return cmd.sendResult(null, .{}),
|
||||
.setCacheDisabled => return setCacheDisabled(cmd),
|
||||
.setUserAgentOverride => return @import("emulation.zig").setUserAgentOverride(cmd),
|
||||
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
|
||||
.deleteCookies => return deleteCookies(cmd),
|
||||
@@ -82,6 +82,17 @@ fn disable(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn setCacheDisabled(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
cacheDisabled: bool,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const client = &bc.cdp.browser.http_client;
|
||||
client.cache_layer.disabled = params.cacheDisabled;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn setExtraHTTPHeaders(cmd: *CDP.Command) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
headers: std.json.ArrayHashMap([]const u8),
|
||||
@@ -734,3 +745,42 @@ test "cdp.Network: canClearBrowserCache" {
|
||||
// Cache is disabled in standard tests for now.
|
||||
try ctx.expectSentResult(.{ .result = false }, .{ .id = 1 });
|
||||
}
|
||||
|
||||
test "cdp.Network: setCacheDisabled disables cache" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-CD1" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
.method = "Network.setCacheDisabled",
|
||||
.params = .{ .cacheDisabled = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
|
||||
const client = ctx.cdp().browser.http_client;
|
||||
try testing.expectEqual(true, client.cache_layer.disabled);
|
||||
}
|
||||
|
||||
test "cdp.Network: setCacheDisabled re-enables cache" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-CD2" });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 1,
|
||||
.method = "Network.setCacheDisabled",
|
||||
.params = .{ .cacheDisabled = true },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 1 });
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 2,
|
||||
.method = "Network.setCacheDisabled",
|
||||
.params = .{ .cacheDisabled = false },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 2 });
|
||||
|
||||
const client = ctx.cdp().browser.http_client;
|
||||
try testing.expectEqual(false, client.cache_layer.disabled);
|
||||
}
|
||||
|
||||
@@ -85,52 +85,55 @@ pub fn fetch(app: *App, browser: *Browser, url: [:0]const u8, opts: FetchOpts) !
|
||||
// Stash scripts user want to inject.
|
||||
session.inject_scripts = opts.inject_script.items;
|
||||
|
||||
const frame = try session.createPage();
|
||||
{
|
||||
const frame = try session.createPage();
|
||||
// frame isn't safe to use after navigate, it can be swapped out
|
||||
|
||||
// // Comment this out to get a profile of the JS code in v8/profile.json.
|
||||
// // You can open this in Chrome's profiler.
|
||||
// // I've seen it generate invalid JSON, but I'm not sure why. It
|
||||
// // happens rarely, and I manually fix the file.
|
||||
// frame.js.startCpuProfiler();
|
||||
// defer {
|
||||
// if (frame.js.stopCpuProfiler()) |profile| {
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/cpu_profile.json",
|
||||
// .data = profile,
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "profile write error", .{ .err = err });
|
||||
// };
|
||||
// } else |err| {
|
||||
// log.err(.app, "profile error", .{ .err = err });
|
||||
// }
|
||||
// }
|
||||
// // Comment this out to get a profile of the JS code in v8/profile.json.
|
||||
// // You can open this in Chrome's profiler.
|
||||
// // I've seen it generate invalid JSON, but I'm not sure why. It
|
||||
// // happens rarely, and I manually fix the file.
|
||||
// frame.js.startCpuProfiler();
|
||||
// defer {
|
||||
// if (frame.js.stopCpuProfiler()) |profile| {
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/cpu_profile.json",
|
||||
// .data = profile,
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "profile write error", .{ .err = err });
|
||||
// };
|
||||
// } else |err| {
|
||||
// log.err(.app, "profile error", .{ .err = err });
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Comment this out to get a heap V8 heap profil
|
||||
// frame.js.startHeapProfiler();
|
||||
// defer {
|
||||
// if (frame.js.stopHeapProfiler()) |profile| {
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/allocating.heapprofile",
|
||||
// .data = profile.@"0",
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "allocating write error", .{ .err = err });
|
||||
// };
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/snapshot.heapsnapshot",
|
||||
// .data = profile.@"1",
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "heapsnapshot write error", .{ .err = err });
|
||||
// };
|
||||
// } else |err| {
|
||||
// log.err(.app, "profile error", .{ .err = err });
|
||||
// }
|
||||
// }
|
||||
// // Comment this out to get a heap V8 heap profil
|
||||
// frame.js.startHeapProfiler();
|
||||
// defer {
|
||||
// if (frame.js.stopHeapProfiler()) |profile| {
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/allocating.heapprofile",
|
||||
// .data = profile.@"0",
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "allocating write error", .{ .err = err });
|
||||
// };
|
||||
// std.fs.cwd().writeFile(.{
|
||||
// .sub_path = ".lp-cache/snapshot.heapsnapshot",
|
||||
// .data = profile.@"1",
|
||||
// }) catch |err| {
|
||||
// log.err(.app, "heapsnapshot write error", .{ .err = err });
|
||||
// };
|
||||
// } else |err| {
|
||||
// log.err(.app, "profile error", .{ .err = err });
|
||||
// }
|
||||
// }
|
||||
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, url, "UTF-8");
|
||||
_ = try frame.navigate(encoded_url, .{
|
||||
.reason = .address_bar,
|
||||
.kind = .{ .push = null },
|
||||
});
|
||||
const encoded_url = try URL.ensureEncoded(frame.call_arena, url, "UTF-8");
|
||||
_ = try frame.navigate(encoded_url, .{
|
||||
.reason = .address_bar,
|
||||
.kind = .{ .push = null },
|
||||
});
|
||||
}
|
||||
var runner = try session.runner(.{});
|
||||
|
||||
var timer = try std.time.Timer.start();
|
||||
@@ -159,7 +162,11 @@ pub fn fetch(app: *App, browser: *Browser, url: [:0]const u8, opts: FetchOpts) !
|
||||
}
|
||||
|
||||
const writer = opts.writer orelse return;
|
||||
if (opts.dump_mode) |mode| {
|
||||
if (opts.dump_mode) |mode| blk: {
|
||||
const frame = session.currentFrame() orelse {
|
||||
try writer.writeAll("Frame closed. Please open a bug report including the URL\n");
|
||||
break :blk;
|
||||
};
|
||||
switch (mode) {
|
||||
.html => try dump.root(frame.window._document, opts.dump, writer, frame),
|
||||
.markdown => try markdown.dump(frame.window._document.asNode(), .{}, writer, frame),
|
||||
|
||||
@@ -36,6 +36,7 @@ const log = lp.log;
|
||||
const CacheLayer = @This();
|
||||
|
||||
next: Layer = undefined,
|
||||
disabled: bool = false,
|
||||
|
||||
pub fn layer(self: *CacheLayer) Layer {
|
||||
return .{
|
||||
@@ -50,7 +51,7 @@ fn request(ptr: *anyopaque, transfer: *Transfer) anyerror!void {
|
||||
const self: *CacheLayer = @ptrCast(@alignCast(ptr));
|
||||
const req = &transfer.req;
|
||||
|
||||
if (req.params.method != .GET) {
|
||||
if (self.disabled or req.params.method != .GET) {
|
||||
return self.next.request(transfer);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user