diff --git a/Tests/LibWeb/Text/expected/css/layer-rule.txt b/Tests/LibWeb/Text/expected/css/layer-rule.txt
new file mode 100644
index 00000000000..2bddcdef567
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/css/layer-rule.txt
@@ -0,0 +1,86 @@
+ PASS: A1 Anonymous layers (first target)
+PASS: A1 Anonymous layers (second target)
+PASS: A2 Anonymous layers (first target)
+PASS: A2 Anonymous layers (second target)
+PASS: A3 Anonymous layers (first target)
+PASS: A3 Anonymous layers (second target)
+PASS: A4 Anonymous layers (first target)
+PASS: A4 Anonymous layers (second target)
+PASS: A5 Anonymous layers (first target)
+PASS: A5 Anonymous layers (second target)
+PASS: A6 Anonymous layers (first target)
+PASS: A6 Anonymous layers (second target)
+PASS: A7 Anonymous layers (first target)
+PASS: A7 Anonymous layers (second target)
+PASS: A8 Anonymous layers (first target)
+PASS: A8 Anonymous layers (second target)
+PASS: A9 Anonymous layers (first target)
+PASS: A9 Anonymous layers (second target)
+PASS: B1 Named layers (first target)
+PASS: B1 Named layers (second target)
+PASS: B2 Named layers (first target)
+PASS: B2 Named layers (second target)
+PASS: B3 Named layers (first target)
+PASS: B3 Named layers (second target)
+PASS: B4 Named layers (first target)
+PASS: B4 Named layers (second target)
+PASS: B5 Named layers (first target)
+PASS: B5 Named layers (second target)
+PASS: B6 Named layers (first target)
+PASS: B6 Named layers (second target)
+PASS: B7 Named layers (first target)
+PASS: B7 Named layers (second target)
+PASS: B8 Named layers (first target)
+PASS: B8 Named layers (second target)
+PASS: B9 Named layers (first target)
+PASS: B9 Named layers (second target)
+PASS: B10 Named layers (first target)
+PASS: B10 Named layers (second target)
+PASS: C1 Named layers shorthand (first target)
+PASS: C1 Named layers shorthand (second target)
+PASS: C2 Named layers shorthand (first target)
+PASS: C2 Named layers shorthand (second target)
+PASS: C3 Named layers shorthand (first target)
+PASS: C3 Named layers shorthand (second target)
+PASS: C4 Named layers shorthand (first target)
+PASS: C4 Named layers shorthand (second target)
+PASS: C5 Named layers shorthand (first target)
+PASS: C5 Named layers shorthand (second target)
+PASS: D1 Mixed named and anonymous layers (first target)
+PASS: D1 Mixed named and anonymous layers (second target)
+PASS: D2 Mixed named and anonymous layers (first target)
+PASS: D2 Mixed named and anonymous layers (second target)
+PASS: D3 Mixed named and anonymous layers (first target)
+PASS: D3 Mixed named and anonymous layers (second target)
+PASS: D4 Mixed named and anonymous layers (first target)
+PASS: D4 Mixed named and anonymous layers (second target)
+PASS: D5 Mixed named and anonymous layers (first target)
+PASS: D5 Mixed named and anonymous layers (second target)
+PASS: E1 Statement syntax (first target)
+PASS: E1 Statement syntax (second target)
+PASS: E2 Statement syntax (first target)
+PASS: E2 Statement syntax (second target)
+PASS: E3 Statement syntax (first target)
+PASS: E3 Statement syntax (second target)
+PASS: E4 Statement syntax (first target)
+PASS: E4 Statement syntax (second target)
+PASS: E5 Statement syntax (first target)
+PASS: E5 Statement syntax (second target)
+PASS: I.A1 Unlayered !important style (first target)
+PASS: I.A1 Unlayered !important style (second target)
+PASS: I.B1 Same specificity, layered !important first (first target)
+PASS: I.B1 Same specificity, layered !important first (second target)
+PASS: I.C1 Same specificity, layered !important second (first target)
+PASS: I.C1 Same specificity, layered !important second (second target)
+PASS: I.D1 Same specificity, all !important (first target)
+PASS: I.D1 Same specificity, all !important (second target)
+PASS: I.D2 Same specificity, all !important (first target)
+PASS: I.D2 Same specificity, all !important (second target)
+PASS: I.D3 Same specificity, all !important (first target)
+PASS: I.D3 Same specificity, all !important (second target)
+PASS: I.D4 Same specificity, all !important (first target)
+PASS: I.D4 Same specificity, all !important (second target)
+PASS: I.E1 Different specificity, all !important (first target)
+PASS: I.E1 Different specificity, all !important (second target)
+PASS: I.E2 Different specificity, all !important (first target)
+PASS: I.E2 Different specificity, all !important (second target)
diff --git a/Tests/LibWeb/Text/input/css/layer-rule.html b/Tests/LibWeb/Text/input/css/layer-rule.html
new file mode 100644
index 00000000000..5eb3243238e
--- /dev/null
+++ b/Tests/LibWeb/Text/input/css/layer-rule.html
@@ -0,0 +1,590 @@
+
+
+
+
+
+
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
index 43cb7ad681d..c81b55514e4 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
@@ -46,6 +46,11 @@ CSSStyleDeclaration* CSSStyleRule::style()
return m_declaration;
}
+FlyString CSSStyleRule::qualified_layer_name() const
+{
+ return parent_layer_internal_qualified_name();
+}
+
// https://www.w3.org/TR/cssom/#serialize-a-css-rule
String CSSStyleRule::serialized() const
{
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
index ab46be00a52..934db81de4a 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleRule.h
@@ -33,6 +33,8 @@ public:
CSSStyleDeclaration* style();
+ FlyString qualified_layer_name() const;
+
private:
CSSStyleRule(JS::Realm&, Vector>&&, PropertyOwningCSSStyleDeclaration&);
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
index 33eec786286..554eff120a0 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
@@ -29,6 +29,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -98,6 +100,7 @@ StyleComputer::StyleComputer(DOM::Document& document)
, m_default_font_metrics(16, Platform::FontPlugin::the().default_font().pixel_metrics())
, m_root_element_font_metrics(m_default_font_metrics)
{
+ m_qualified_layer_names_in_order.append({});
}
StyleComputer::~StyleComputer() = default;
@@ -322,6 +325,13 @@ StyleComputer::RuleCache const& StyleComputer::rule_cache_for_cascade_origin(Cas
return true;
}
+[[nodiscard]] static bool filter_layer(FlyString const& qualified_layer_name, MatchingRule const& rule)
+{
+ if (rule.rule && rule.rule->qualified_layer_name() != qualified_layer_name)
+ return false;
+ return true;
+}
+
bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector) const
{
for (u32 hash : selector.ancestor_hashes()) {
@@ -333,7 +343,7 @@ bool StyleComputer::should_reject_with_ancestor_filter(Selector const& selector)
return false;
}
-Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element) const
+Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element, FlyString const& qualified_layer_name) const
{
auto const& root_node = element.root();
auto shadow_root = is(root_node) ? static_cast(&root_node) : nullptr;
@@ -351,12 +361,12 @@ Vector StyleComputer::collect_matching_rules(DOM::Element const& e
rules_to_run.grow_capacity(rules_to_run.size() + rules.size());
if (pseudo_element.has_value()) {
for (auto const& rule : rules) {
- if (rule.contains_pseudo_element && filter_namespace_rule(element, rule))
+ if (rule.contains_pseudo_element && filter_namespace_rule(element, rule) && filter_layer(qualified_layer_name, rule))
rules_to_run.unchecked_append(rule);
}
} else {
for (auto const& rule : rules) {
- if (!rule.contains_pseudo_element && filter_namespace_rule(element, rule))
+ if (!rule.contains_pseudo_element && filter_namespace_rule(element, rule) && filter_layer(qualified_layer_name, rule))
rules_to_run.unchecked_append(rule);
}
}
@@ -1737,6 +1747,7 @@ static void apply_dimension_attribute(StyleProperties& style, DOM::Element const
}
// https://www.w3.org/TR/css-cascade/#cascading
+// https://drafts.csswg.org/css-cascade-5/#layering
void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
{
// First, we collect all the CSS rules whose selectors match `element`:
@@ -1745,8 +1756,16 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element
sort_matching_rules(matching_rule_set.user_agent_rules);
matching_rule_set.user_rules = collect_matching_rules(element, CascadeOrigin::User, pseudo_element);
sort_matching_rules(matching_rule_set.user_rules);
- matching_rule_set.author_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element);
- sort_matching_rules(matching_rule_set.author_rules);
+ // @layer-ed author rules
+ for (auto const& layer_name : m_qualified_layer_names_in_order) {
+ auto layer_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element, layer_name);
+ sort_matching_rules(layer_rules);
+ matching_rule_set.author_rules.append({ layer_name, layer_rules });
+ }
+ // Un-@layer-ed author rules
+ auto unlayered_author_rules = collect_matching_rules(element, CascadeOrigin::Author, pseudo_element);
+ sort_matching_rules(unlayered_author_rules);
+ matching_rule_set.author_rules.append({ {}, unlayered_author_rules });
if (mode == ComputeStyleMode::CreatePseudoElementStyleIfNeeded) {
VERIFY(pseudo_element.has_value());
@@ -1758,7 +1777,10 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element
}
// Then we resolve all the CSS custom properties ("variables") for this element:
- cascade_custom_properties(element, pseudo_element, matching_rule_set.author_rules);
+ // FIXME: Also resolve !important custom properties, in a second cascade.
+ for (auto& layer : matching_rule_set.author_rules) {
+ cascade_custom_properties(element, pseudo_element, layer.rules);
+ }
// Then we apply the declarations from the matched rules in cascade order:
@@ -1789,8 +1811,10 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element
}
}
- // Normal author declarations
- cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No);
+ // Normal author declarations, ordered by @layer, with un-@layer-ed rules last
+ for (auto const& layer : matching_rule_set.author_rules) {
+ cascade_declarations(style, element, pseudo_element, layer.rules, CascadeOrigin::Author, Important::No);
+ }
// Animation declarations [css-animations-2]
auto animation_name = [&]() -> Optional {
@@ -1854,8 +1878,10 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element
}
}
- // Important author declarations
- cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::Yes);
+ // Important author declarations, with un-@layer-ed rules first, followed by each @layer in reverse order.
+ for (auto const& layer : matching_rule_set.author_rules.in_reverse()) {
+ cascade_declarations(style, element, pseudo_element, layer.rules, CascadeOrigin::Author, Important::Yes);
+ }
// Important user declarations
cascade_declarations(style, element, pseudo_element, matching_rule_set.user_rules, CascadeOrigin::User, Important::Yes);
@@ -2742,12 +2768,82 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca
return rule_cache;
}
+struct LayerNode {
+ OrderedHashMap children {};
+};
+
+static void flatten_layer_names_tree(Vector& layer_names, StringView const& parent_qualified_name, FlyString const& name, LayerNode const& node)
+{
+ FlyString qualified_name = parent_qualified_name.is_empty() ? name : MUST(String::formatted("{}.{}", parent_qualified_name, name));
+
+ for (auto const& item : node.children)
+ flatten_layer_names_tree(layer_names, qualified_name, item.key, item.value);
+
+ layer_names.append(qualified_name);
+}
+
+void StyleComputer::build_qualified_layer_names_cache()
+{
+ LayerNode root;
+
+ auto insert_layer_name = [&](FlyString const& internal_qualified_name) {
+ auto* node = &root;
+ internal_qualified_name.bytes_as_string_view()
+ .for_each_split_view('.', SplitBehavior::Nothing, [&](StringView part) {
+ auto local_name = MUST(FlyString::from_utf8(part));
+ node = &node->children.ensure(local_name);
+ });
+ };
+
+ // Walk all style sheets, identifying when we first see a @layer name, and add its qualified name to the list.
+ // TODO: Separate the light and shadow-dom layers.
+ for_each_stylesheet(CascadeOrigin::Author, [&](auto& sheet, JS::GCPtr) {
+ // NOTE: Postorder so that a @layer block is iterated after its children,
+ // because we want those children to occur before it in the list.
+ sheet.for_each_effective_rule(TraversalOrder::Postorder, [&](auto& rule) {
+ switch (rule.type()) {
+ case CSSRule::Type::Import:
+ // TODO: Handle `layer(foo)` in import rules once we implement that.
+ break;
+ case CSSRule::Type::LayerBlock: {
+ auto& layer_block = static_cast(rule);
+ insert_layer_name(layer_block.internal_qualified_name({}));
+ break;
+ }
+ case CSSRule::Type::LayerStatement: {
+ auto& layer_statement = static_cast(rule);
+ auto qualified_names = layer_statement.internal_qualified_name_list({});
+ for (auto& name : qualified_names)
+ insert_layer_name(name);
+ break;
+ }
+
+ // Ignore everything else
+ case CSSRule::Type::Style:
+ case CSSRule::Type::Media:
+ case CSSRule::Type::FontFace:
+ case CSSRule::Type::Keyframes:
+ case CSSRule::Type::Keyframe:
+ case CSSRule::Type::Namespace:
+ case CSSRule::Type::Supports:
+ break;
+ }
+ });
+ });
+
+ // Now, produce a flat list of qualified names to use later
+ m_qualified_layer_names_in_order.clear();
+ flatten_layer_names_tree(m_qualified_layer_names_in_order, ""sv, {}, root);
+}
+
void StyleComputer::build_rule_cache()
{
if (auto user_style_source = document().page().user_style(); user_style_source.has_value()) {
m_user_style_sheet = JS::make_handle(parse_css_stylesheet(CSS::Parser::ParsingContext(document()), user_style_source.value()));
}
+ build_qualified_layer_names_cache();
+
m_author_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::Author);
m_user_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::User);
m_user_agent_rule_cache = make_rule_cache_for_cascade_origin(CascadeOrigin::UserAgent);
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
index 1142b6b4cae..8398ebaad72 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
@@ -133,7 +133,7 @@ public:
NonnullRefPtr compute_style(DOM::Element&, Optional = {}) const;
RefPtr compute_pseudo_element_style_if_needed(DOM::Element&, Optional) const;
- Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional) const;
+ Vector collect_matching_rules(DOM::Element const&, CascadeOrigin, Optional, FlyString const& qualified_layer_name = {}) const;
void invalidate_rule_cache();
@@ -188,10 +188,18 @@ private:
[[nodiscard]] Length::FontMetrics calculate_root_element_font_metrics(StyleProperties const&) const;
+ Vector m_qualified_layer_names_in_order;
+ void build_qualified_layer_names_cache();
+
+ struct LayerMatchingRules {
+ FlyString qualified_layer_name;
+ Vector rules;
+ };
+
struct MatchingRuleSet {
Vector user_agent_rules;
Vector user_rules;
- Vector author_rules;
+ Vector author_rules;
};
void cascade_declarations(StyleProperties&, DOM::Element&, Optional, Vector const&, CascadeOrigin, Important) const;