/src/serenity/Userland/Libraries/LibWeb/CSS/CSSStyleRule.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <LibWeb/Bindings/CSSStyleRulePrototype.h> |
9 | | #include <LibWeb/Bindings/Intrinsics.h> |
10 | | #include <LibWeb/CSS/CSSRuleList.h> |
11 | | #include <LibWeb/CSS/CSSStyleRule.h> |
12 | | #include <LibWeb/CSS/Parser/Parser.h> |
13 | | #include <LibWeb/CSS/StyleComputer.h> |
14 | | |
15 | | namespace Web::CSS { |
16 | | |
17 | | JS_DEFINE_ALLOCATOR(CSSStyleRule); |
18 | | |
19 | | JS::NonnullGCPtr<CSSStyleRule> CSSStyleRule::create(JS::Realm& realm, SelectorList&& selectors, PropertyOwningCSSStyleDeclaration& declaration, CSSRuleList& nested_rules) |
20 | 0 | { |
21 | 0 | return realm.heap().allocate<CSSStyleRule>(realm, realm, move(selectors), declaration, nested_rules); |
22 | 0 | } |
23 | | |
24 | | CSSStyleRule::CSSStyleRule(JS::Realm& realm, SelectorList&& selectors, PropertyOwningCSSStyleDeclaration& declaration, CSSRuleList& nested_rules) |
25 | 0 | : CSSGroupingRule(realm, nested_rules) |
26 | 0 | , m_selectors(move(selectors)) |
27 | 0 | , m_declaration(declaration) |
28 | 0 | { |
29 | 0 | m_declaration->set_parent_rule(*this); |
30 | 0 | } |
31 | | |
32 | | void CSSStyleRule::initialize(JS::Realm& realm) |
33 | 0 | { |
34 | 0 | Base::initialize(realm); |
35 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSStyleRule); |
36 | 0 | } |
37 | | |
38 | | void CSSStyleRule::visit_edges(Cell::Visitor& visitor) |
39 | 0 | { |
40 | 0 | Base::visit_edges(visitor); |
41 | 0 | visitor.visit(m_declaration); |
42 | 0 | } |
43 | | |
44 | | // https://drafts.csswg.org/cssom-1/#dom-cssstylerule-style |
45 | | CSSStyleDeclaration* CSSStyleRule::style() |
46 | 0 | { |
47 | 0 | return m_declaration; |
48 | 0 | } |
49 | | |
50 | | // https://drafts.csswg.org/cssom-1/#serialize-a-css-rule |
51 | | String CSSStyleRule::serialized() const |
52 | 0 | { |
53 | 0 | StringBuilder builder; |
54 | | |
55 | | // 1. Let s initially be the result of performing serialize a group of selectors on the rule’s associated selectors, |
56 | | // followed by the string " {", i.e., a single SPACE (U+0020), followed by LEFT CURLY BRACKET (U+007B). |
57 | 0 | builder.append(serialize_a_group_of_selectors(selectors())); |
58 | 0 | builder.append(" {"sv); |
59 | | |
60 | | // 2. Let decls be the result of performing serialize a CSS declaration block on the rule’s associated declarations, |
61 | | // or null if there are no such declarations. |
62 | 0 | auto decls = declaration().length() > 0 ? declaration().serialized() : Optional<String>(); |
63 | | |
64 | | // 3. Let rules be the result of performing serialize a CSS rule on each rule in the rule’s cssRules list, |
65 | | // or null if there are no such rules. |
66 | 0 | Vector<String> rules; |
67 | 0 | for (auto& rule : css_rules()) { |
68 | 0 | rules.append(rule->serialized()); |
69 | 0 | } |
70 | | |
71 | | // 4. If decls and rules are both null, append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)) and return s. |
72 | 0 | if (!decls.has_value() && rules.is_empty()) { |
73 | 0 | builder.append(" }"sv); |
74 | 0 | return builder.to_string_without_validation(); |
75 | 0 | } |
76 | | |
77 | | // 5. If rules is null: |
78 | 0 | if (rules.is_empty()) { |
79 | | // 1. Append a single SPACE (U+0020) to s |
80 | 0 | builder.append(' '); |
81 | | // 2. Append decls to s |
82 | 0 | builder.append(*decls); |
83 | | // 3. Append " }" to s (i.e. a single SPACE (U+0020) followed by RIGHT CURLY BRACKET (U+007D)). |
84 | 0 | builder.append(" }"sv); |
85 | | // 4. Return s. |
86 | 0 | return builder.to_string_without_validation(); |
87 | 0 | } |
88 | | |
89 | | // 6. Otherwise: |
90 | 0 | else { |
91 | | // 1. If decls is not null, prepend it to rules. |
92 | 0 | if (decls.has_value()) |
93 | 0 | rules.prepend(decls.value()); |
94 | | |
95 | | // 2. For each rule in rules: |
96 | 0 | for (auto& rule : rules) { |
97 | | // * If rule is the empty string, do nothing. |
98 | 0 | if (rule.is_empty()) |
99 | 0 | continue; |
100 | | |
101 | | // * Otherwise: |
102 | | // 1. Append a newline followed by two spaces to s. |
103 | | // 2. Append rule to s. |
104 | 0 | builder.appendff("\n {}", rule); |
105 | 0 | } |
106 | | |
107 | | // 3. Append a newline followed by RIGHT CURLY BRACKET (U+007D) to s. |
108 | 0 | builder.append("\n}"sv); |
109 | | |
110 | | // 4. Return s. |
111 | 0 | return builder.to_string_without_validation(); |
112 | 0 | } |
113 | 0 | } |
114 | | |
115 | | // https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext |
116 | | String CSSStyleRule::selector_text() const |
117 | 0 | { |
118 | | // The selectorText attribute, on getting, must return the result of serializing the associated group of selectors. |
119 | 0 | return serialize_a_group_of_selectors(selectors()); |
120 | 0 | } |
121 | | |
122 | | // https://drafts.csswg.org/cssom-1/#dom-cssstylerule-selectortext |
123 | | void CSSStyleRule::set_selector_text(StringView selector_text) |
124 | 0 | { |
125 | 0 | clear_caches(); |
126 | | |
127 | | // 1. Run the parse a group of selectors algorithm on the given value. |
128 | 0 | auto parsed_selectors = parse_selector(Parser::ParsingContext { realm() }, selector_text); |
129 | | |
130 | | // 2. If the algorithm returns a non-null value replace the associated group of selectors with the returned value. |
131 | 0 | if (parsed_selectors.has_value()) { |
132 | 0 | m_selectors = parsed_selectors.release_value(); |
133 | 0 | if (auto* sheet = parent_style_sheet()) { |
134 | 0 | if (auto style_sheet_list = sheet->style_sheet_list()) { |
135 | 0 | style_sheet_list->document().style_computer().invalidate_rule_cache(); |
136 | 0 | style_sheet_list->document_or_shadow_root().invalidate_style(DOM::StyleInvalidationReason::SetSelectorText); |
137 | 0 | } |
138 | 0 | } |
139 | 0 | } |
140 | | |
141 | | // 3. Otherwise, if the algorithm returns a null value, do nothing. |
142 | 0 | } |
143 | | |
144 | | SelectorList const& CSSStyleRule::absolutized_selectors() const |
145 | 0 | { |
146 | 0 | if (m_cached_absolutized_selectors.has_value()) |
147 | 0 | return m_cached_absolutized_selectors.value(); |
148 | | |
149 | | // Replace all occurrences of `&` with the nearest ancestor style rule's selector list wrapped in `:is(...)`, |
150 | | // or if we have no such ancestor, with `:scope`. |
151 | | |
152 | | // If we don't have any nesting selectors, we can just use our selectors as they are. |
153 | 0 | bool has_any_nesting = false; |
154 | 0 | for (auto const& selector : selectors()) { |
155 | 0 | if (selector->contains_the_nesting_selector()) { |
156 | 0 | has_any_nesting = true; |
157 | 0 | break; |
158 | 0 | } |
159 | 0 | } |
160 | |
|
161 | 0 | if (!has_any_nesting) { |
162 | 0 | m_cached_absolutized_selectors = m_selectors; |
163 | 0 | return m_cached_absolutized_selectors.value(); |
164 | 0 | } |
165 | | |
166 | | // Otherwise, build up a new list of selectors with the `&` replaced. |
167 | | |
168 | | // First, figure out what we should replace `&` with. |
169 | | // "When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. |
170 | | // When used in any other context, it represents the same elements as :scope in that context (unless otherwise defined)." |
171 | | // https://drafts.csswg.org/css-nesting-1/#nest-selector |
172 | 0 | CSSStyleRule const* parent_style_rule = nullptr; |
173 | 0 | for (auto* parent = parent_rule(); parent; parent = parent->parent_rule()) { |
174 | 0 | if (parent->type() == CSSStyleRule::Type::Style) { |
175 | 0 | parent_style_rule = static_cast<CSSStyleRule const*>(parent); |
176 | 0 | break; |
177 | 0 | } |
178 | 0 | } |
179 | 0 | Selector::SimpleSelector parent_selector; |
180 | 0 | if (parent_style_rule) { |
181 | | // TODO: If there's only 1, we don't have to use `:is()` for it |
182 | 0 | parent_selector = { |
183 | 0 | .type = Selector::SimpleSelector::Type::PseudoClass, |
184 | 0 | .value = Selector::SimpleSelector::PseudoClassSelector { |
185 | 0 | .type = PseudoClass::Is, |
186 | 0 | .argument_selector_list = parent_style_rule->absolutized_selectors(), |
187 | 0 | }, |
188 | 0 | }; |
189 | 0 | } else { |
190 | 0 | parent_selector = { |
191 | 0 | .type = Selector::SimpleSelector::Type::PseudoClass, |
192 | 0 | .value = Selector::SimpleSelector::PseudoClassSelector { .type = PseudoClass::Scope }, |
193 | 0 | }; |
194 | 0 | } |
195 | |
|
196 | 0 | SelectorList absolutized_selectors; |
197 | 0 | for (auto const& selector : selectors()) |
198 | 0 | absolutized_selectors.append(selector->absolutized(parent_selector)); |
199 | |
|
200 | 0 | m_cached_absolutized_selectors = move(absolutized_selectors); |
201 | 0 | return m_cached_absolutized_selectors.value(); |
202 | 0 | } |
203 | | |
204 | | void CSSStyleRule::clear_caches() |
205 | 0 | { |
206 | 0 | Base::clear_caches(); |
207 | 0 | m_cached_absolutized_selectors.clear(); |
208 | 0 | } |
209 | | |
210 | | } |