/src/serenity/Userland/Libraries/LibWeb/CSS/SelectorEngine.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2024, 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/CSS/Keyword.h> |
9 | | #include <LibWeb/CSS/Parser/Parser.h> |
10 | | #include <LibWeb/CSS/SelectorEngine.h> |
11 | | #include <LibWeb/CSS/StyleProperties.h> |
12 | | #include <LibWeb/DOM/Attr.h> |
13 | | #include <LibWeb/DOM/Document.h> |
14 | | #include <LibWeb/DOM/Element.h> |
15 | | #include <LibWeb/DOM/NamedNodeMap.h> |
16 | | #include <LibWeb/DOM/Text.h> |
17 | | #include <LibWeb/HTML/AttributeNames.h> |
18 | | #include <LibWeb/HTML/HTMLAnchorElement.h> |
19 | | #include <LibWeb/HTML/HTMLAreaElement.h> |
20 | | #include <LibWeb/HTML/HTMLButtonElement.h> |
21 | | #include <LibWeb/HTML/HTMLDetailsElement.h> |
22 | | #include <LibWeb/HTML/HTMLDialogElement.h> |
23 | | #include <LibWeb/HTML/HTMLFieldSetElement.h> |
24 | | #include <LibWeb/HTML/HTMLHtmlElement.h> |
25 | | #include <LibWeb/HTML/HTMLInputElement.h> |
26 | | #include <LibWeb/HTML/HTMLMediaElement.h> |
27 | | #include <LibWeb/HTML/HTMLOptGroupElement.h> |
28 | | #include <LibWeb/HTML/HTMLOptionElement.h> |
29 | | #include <LibWeb/HTML/HTMLProgressElement.h> |
30 | | #include <LibWeb/HTML/HTMLSelectElement.h> |
31 | | #include <LibWeb/HTML/HTMLTextAreaElement.h> |
32 | | #include <LibWeb/Infra/Strings.h> |
33 | | #include <LibWeb/Namespace.h> |
34 | | |
35 | | namespace Web::SelectorEngine { |
36 | | |
37 | | // Upward traversal for descendant (' ') and immediate child combinator ('>') |
38 | | // If we're starting inside a shadow tree, traversal stops at the nearest shadow host. |
39 | | // This is an implementation detail of the :host selector. Otherwise we would just traverse up to the document root. |
40 | | static inline JS::GCPtr<DOM::Node const> traverse_up(JS::GCPtr<DOM::Node const> node, JS::GCPtr<DOM::Element const> shadow_host) |
41 | 0 | { |
42 | 0 | if (!node) |
43 | 0 | return nullptr; |
44 | | |
45 | 0 | if (shadow_host) { |
46 | | // NOTE: We only traverse up to the shadow host, not beyond. |
47 | 0 | if (node == shadow_host) |
48 | 0 | return nullptr; |
49 | | |
50 | 0 | return node->parent_or_shadow_host_element(); |
51 | 0 | } |
52 | 0 | return node->parent(); |
53 | 0 | } |
54 | | |
55 | | // https://drafts.csswg.org/selectors-4/#the-lang-pseudo |
56 | | static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector<FlyString> const& languages) |
57 | 0 | { |
58 | 0 | FlyString element_language; |
59 | 0 | for (auto const* e = &element; e; e = e->parent_element()) { |
60 | 0 | auto lang = e->attribute(HTML::AttributeNames::lang); |
61 | 0 | if (lang.has_value()) { |
62 | 0 | element_language = lang.release_value(); |
63 | 0 | break; |
64 | 0 | } |
65 | 0 | } |
66 | 0 | if (element_language.is_empty()) |
67 | 0 | return false; |
68 | | |
69 | | // FIXME: This is ad-hoc. Implement a proper language range matching algorithm as recommended by BCP47. |
70 | 0 | for (auto const& language : languages) { |
71 | 0 | if (language.is_empty()) |
72 | 0 | continue; |
73 | 0 | if (language == "*"sv) |
74 | 0 | return true; |
75 | 0 | if (!element_language.to_string().contains('-') && Infra::is_ascii_case_insensitive_match(element_language, language)) |
76 | 0 | return true; |
77 | 0 | auto parts = element_language.to_string().split_limit('-', 2).release_value_but_fixme_should_propagate_errors(); |
78 | 0 | if (Infra::is_ascii_case_insensitive_match(parts[0], language)) |
79 | 0 | return true; |
80 | 0 | } |
81 | 0 | return false; |
82 | 0 | } |
83 | | |
84 | | // https://drafts.csswg.org/selectors-4/#relational |
85 | | static inline bool matches_has_pseudo_class(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& anchor, JS::GCPtr<DOM::Element const> shadow_host) |
86 | 0 | { |
87 | 0 | switch (selector.compound_selectors()[0].combinator) { |
88 | | // Shouldn't be possible because we've parsed relative selectors, which always have a combinator, implicitly or explicitly. |
89 | 0 | case CSS::Selector::Combinator::None: |
90 | 0 | VERIFY_NOT_REACHED(); |
91 | 0 | case CSS::Selector::Combinator::Descendant: { |
92 | 0 | bool has = false; |
93 | 0 | anchor.for_each_in_subtree([&](auto const& descendant) { |
94 | 0 | if (!descendant.is_element()) |
95 | 0 | return TraversalDecision::Continue; |
96 | 0 | auto const& descendant_element = static_cast<DOM::Element const&>(descendant); |
97 | 0 | if (matches(selector, style_sheet_for_rule, descendant_element, shadow_host, {}, {}, SelectorKind::Relative)) { |
98 | 0 | has = true; |
99 | 0 | return TraversalDecision::Break; |
100 | 0 | } |
101 | 0 | return TraversalDecision::Continue; |
102 | 0 | }); |
103 | 0 | return has; |
104 | 0 | } |
105 | 0 | case CSS::Selector::Combinator::ImmediateChild: { |
106 | 0 | bool has = false; |
107 | 0 | anchor.for_each_child([&](DOM::Node const& child) { |
108 | 0 | if (!child.is_element()) |
109 | 0 | return IterationDecision::Continue; |
110 | 0 | auto const& child_element = static_cast<DOM::Element const&>(child); |
111 | 0 | if (matches(selector, style_sheet_for_rule, child_element, shadow_host, {}, {}, SelectorKind::Relative)) { |
112 | 0 | has = true; |
113 | 0 | return IterationDecision::Break; |
114 | 0 | } |
115 | 0 | return IterationDecision::Continue; |
116 | 0 | }); |
117 | 0 | return has; |
118 | 0 | } |
119 | 0 | case CSS::Selector::Combinator::NextSibling: |
120 | 0 | return anchor.next_element_sibling() != nullptr && matches(selector, style_sheet_for_rule, *anchor.next_element_sibling(), shadow_host, {}, {}, SelectorKind::Relative); |
121 | 0 | case CSS::Selector::Combinator::SubsequentSibling: { |
122 | 0 | for (auto* sibling = anchor.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { |
123 | 0 | if (matches(selector, style_sheet_for_rule, *sibling, shadow_host, {}, {}, SelectorKind::Relative)) |
124 | 0 | return true; |
125 | 0 | } |
126 | 0 | return false; |
127 | 0 | } |
128 | 0 | case CSS::Selector::Combinator::Column: |
129 | 0 | TODO(); |
130 | 0 | } |
131 | 0 | return false; |
132 | 0 | } |
133 | | |
134 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-link |
135 | | static inline bool matches_link_pseudo_class(DOM::Element const& element) |
136 | 0 | { |
137 | | // All a elements that have an href attribute, and all area elements that have an href attribute, must match one of :link and :visited. |
138 | 0 | if (!is<HTML::HTMLAnchorElement>(element) && !is<HTML::HTMLAreaElement>(element)) |
139 | 0 | return false; |
140 | 0 | return element.has_attribute(HTML::AttributeNames::href); |
141 | 0 | } |
142 | | |
143 | | bool matches_hover_pseudo_class(DOM::Element const& element) |
144 | 0 | { |
145 | 0 | auto* hovered_node = element.document().hovered_node(); |
146 | 0 | if (!hovered_node) |
147 | 0 | return false; |
148 | 0 | if (&element == hovered_node) |
149 | 0 | return true; |
150 | 0 | return element.is_shadow_including_ancestor_of(*hovered_node); |
151 | 0 | } |
152 | | |
153 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-checked |
154 | | static inline bool matches_checked_pseudo_class(DOM::Element const& element) |
155 | 0 | { |
156 | | // The :checked pseudo-class must match any element falling into one of the following categories: |
157 | | // - input elements whose type attribute is in the Checkbox state and whose checkedness state is true |
158 | | // - input elements whose type attribute is in the Radio Button state and whose checkedness state is true |
159 | 0 | if (is<HTML::HTMLInputElement>(element)) { |
160 | 0 | auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element); |
161 | 0 | switch (input_element.type_state()) { |
162 | 0 | case HTML::HTMLInputElement::TypeAttributeState::Checkbox: |
163 | 0 | case HTML::HTMLInputElement::TypeAttributeState::RadioButton: |
164 | 0 | return static_cast<HTML::HTMLInputElement const&>(element).checked(); |
165 | 0 | default: |
166 | 0 | return false; |
167 | 0 | } |
168 | 0 | } |
169 | | |
170 | | // - option elements whose selectedness is true |
171 | 0 | if (is<HTML::HTMLOptionElement>(element)) { |
172 | 0 | return static_cast<HTML::HTMLOptionElement const&>(element).selected(); |
173 | 0 | } |
174 | 0 | return false; |
175 | 0 | } |
176 | | |
177 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-indeterminate |
178 | | static inline bool matches_indeterminate_pseudo_class(DOM::Element const& element) |
179 | 0 | { |
180 | | // The :indeterminate pseudo-class must match any element falling into one of the following categories: |
181 | | // - input elements whose type attribute is in the Checkbox state and whose indeterminate IDL attribute is set to true |
182 | | // FIXME: - input elements whose type attribute is in the Radio Button state and whose radio button group contains no input elements whose checkedness state is true. |
183 | 0 | if (is<HTML::HTMLInputElement>(element)) { |
184 | 0 | auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element); |
185 | 0 | switch (input_element.type_state()) { |
186 | 0 | case HTML::HTMLInputElement::TypeAttributeState::Checkbox: |
187 | 0 | return input_element.indeterminate(); |
188 | 0 | default: |
189 | 0 | return false; |
190 | 0 | } |
191 | 0 | } |
192 | | // - progress elements with no value content attribute |
193 | 0 | if (is<HTML::HTMLProgressElement>(element)) { |
194 | 0 | return !element.has_attribute(HTML::AttributeNames::value); |
195 | 0 | } |
196 | 0 | return false; |
197 | 0 | } |
198 | | |
199 | | static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element) |
200 | 0 | { |
201 | | // FIXME: Check the attribute's namespace, once we support that in DOM::Element! |
202 | |
|
203 | 0 | auto const& attribute_name = attribute.qualified_name.name.name; |
204 | |
|
205 | 0 | auto const* attr = element.namespace_uri() == Namespace::HTML ? element.attributes()->get_attribute_with_lowercase_qualified_name(attribute_name) |
206 | 0 | : element.attributes()->get_attribute(attribute_name); |
207 | |
|
208 | 0 | if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) { |
209 | | // Early way out in case of an attribute existence selector. |
210 | 0 | return attr != nullptr; |
211 | 0 | } |
212 | | |
213 | 0 | if (!attr) |
214 | 0 | return false; |
215 | | |
216 | 0 | auto case_sensitivity = [&](CSS::Selector::SimpleSelector::Attribute::CaseType case_type) { |
217 | 0 | switch (case_type) { |
218 | 0 | case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch: |
219 | 0 | return CaseSensitivity::CaseInsensitive; |
220 | 0 | case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch: |
221 | 0 | return CaseSensitivity::CaseSensitive; |
222 | 0 | case CSS::Selector::SimpleSelector::Attribute::CaseType::DefaultMatch: |
223 | | // See: https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors |
224 | 0 | if (element.document().is_html_document() |
225 | 0 | && element.namespace_uri() == Namespace::HTML |
226 | 0 | && attribute_name.is_one_of( |
227 | 0 | HTML::AttributeNames::accept, HTML::AttributeNames::accept_charset, HTML::AttributeNames::align, |
228 | 0 | HTML::AttributeNames::alink, HTML::AttributeNames::axis, HTML::AttributeNames::bgcolor, HTML::AttributeNames::charset, |
229 | 0 | HTML::AttributeNames::checked, HTML::AttributeNames::clear, HTML::AttributeNames::codetype, HTML::AttributeNames::color, |
230 | 0 | HTML::AttributeNames::compact, HTML::AttributeNames::declare, HTML::AttributeNames::defer, HTML::AttributeNames::dir, |
231 | 0 | HTML::AttributeNames::direction, HTML::AttributeNames::disabled, HTML::AttributeNames::enctype, HTML::AttributeNames::face, |
232 | 0 | HTML::AttributeNames::frame, HTML::AttributeNames::hreflang, HTML::AttributeNames::http_equiv, HTML::AttributeNames::lang, |
233 | 0 | HTML::AttributeNames::language, HTML::AttributeNames::link, HTML::AttributeNames::media, HTML::AttributeNames::method, |
234 | 0 | HTML::AttributeNames::multiple, HTML::AttributeNames::nohref, HTML::AttributeNames::noresize, HTML::AttributeNames::noshade, |
235 | 0 | HTML::AttributeNames::nowrap, HTML::AttributeNames::readonly, HTML::AttributeNames::rel, HTML::AttributeNames::rev, |
236 | 0 | HTML::AttributeNames::rules, HTML::AttributeNames::scope, HTML::AttributeNames::scrolling, HTML::AttributeNames::selected, |
237 | 0 | HTML::AttributeNames::shape, HTML::AttributeNames::target, HTML::AttributeNames::text, HTML::AttributeNames::type, |
238 | 0 | HTML::AttributeNames::valign, HTML::AttributeNames::valuetype, HTML::AttributeNames::vlink)) { |
239 | 0 | return CaseSensitivity::CaseInsensitive; |
240 | 0 | } |
241 | | |
242 | 0 | return CaseSensitivity::CaseSensitive; |
243 | 0 | } |
244 | 0 | VERIFY_NOT_REACHED(); |
245 | 0 | }(attribute.case_type); |
246 | 0 | auto case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive; |
247 | |
|
248 | 0 | switch (attribute.match_type) { |
249 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch: |
250 | 0 | return case_insensitive_match |
251 | 0 | ? Infra::is_ascii_case_insensitive_match(attr->value(), attribute.value) |
252 | 0 | : attr->value() == attribute.value; |
253 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: { |
254 | 0 | if (attribute.value.is_empty()) { |
255 | | // This selector is always false is match value is empty. |
256 | 0 | return false; |
257 | 0 | } |
258 | 0 | auto const& attribute_value = attr->value(); |
259 | 0 | auto const view = attribute_value.bytes_as_string_view().split_view(' '); |
260 | 0 | auto const size = view.size(); |
261 | 0 | for (size_t i = 0; i < size; ++i) { |
262 | 0 | auto const value = view.at(i); |
263 | 0 | if (case_insensitive_match |
264 | 0 | ? Infra::is_ascii_case_insensitive_match(value, attribute.value) |
265 | 0 | : value == attribute.value) { |
266 | 0 | return true; |
267 | 0 | } |
268 | 0 | } |
269 | 0 | return false; |
270 | 0 | } |
271 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString: |
272 | 0 | return !attribute.value.is_empty() |
273 | 0 | && attr->value().contains(attribute.value, case_sensitivity); |
274 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: { |
275 | 0 | auto const& element_attr_value = attr->value(); |
276 | 0 | if (element_attr_value.is_empty()) { |
277 | | // If the attribute value on element is empty, the selector is true |
278 | | // if the match value is also empty and false otherwise. |
279 | 0 | return attribute.value.is_empty(); |
280 | 0 | } |
281 | 0 | if (attribute.value.is_empty()) { |
282 | 0 | return false; |
283 | 0 | } |
284 | 0 | auto segments = element_attr_value.bytes_as_string_view().split_view('-'); |
285 | 0 | return case_insensitive_match |
286 | 0 | ? Infra::is_ascii_case_insensitive_match(segments.first(), attribute.value) |
287 | 0 | : segments.first() == attribute.value; |
288 | 0 | } |
289 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString: |
290 | 0 | return !attribute.value.is_empty() |
291 | 0 | && attr->value().bytes_as_string_view().starts_with(attribute.value, case_sensitivity); |
292 | 0 | case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString: |
293 | 0 | return !attribute.value.is_empty() |
294 | 0 | && attr->value().bytes_as_string_view().ends_with(attribute.value, case_sensitivity); |
295 | 0 | default: |
296 | 0 | break; |
297 | 0 | } |
298 | | |
299 | 0 | return false; |
300 | 0 | } |
301 | | |
302 | | static inline DOM::Element const* previous_sibling_with_same_tag_name(DOM::Element const& element) |
303 | 0 | { |
304 | 0 | for (auto const* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { |
305 | 0 | if (sibling->tag_name() == element.tag_name()) |
306 | 0 | return sibling; |
307 | 0 | } |
308 | 0 | return nullptr; |
309 | 0 | } |
310 | | |
311 | | static inline DOM::Element const* next_sibling_with_same_tag_name(DOM::Element const& element) |
312 | 0 | { |
313 | 0 | for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) { |
314 | 0 | if (sibling->tag_name() == element.tag_name()) |
315 | 0 | return sibling; |
316 | 0 | } |
317 | 0 | return nullptr; |
318 | 0 | } |
319 | | |
320 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-read-write |
321 | | static bool matches_read_write_pseudo_class(DOM::Element const& element) |
322 | 0 | { |
323 | | // The :read-write pseudo-class must match any element falling into one of the following categories, |
324 | | // which for the purposes of Selectors are thus considered user-alterable: [SELECTORS] |
325 | | // - input elements to which the readonly attribute applies, and that are mutable |
326 | | // (i.e. that do not have the readonly attribute specified and that are not disabled) |
327 | 0 | if (is<HTML::HTMLInputElement>(element)) { |
328 | 0 | auto& input_element = static_cast<HTML::HTMLInputElement const&>(element); |
329 | 0 | if (input_element.has_attribute(HTML::AttributeNames::readonly)) |
330 | 0 | return false; |
331 | 0 | if (!input_element.enabled()) |
332 | 0 | return false; |
333 | 0 | return true; |
334 | 0 | } |
335 | | // - textarea elements that do not have a readonly attribute, and that are not disabled |
336 | 0 | if (is<HTML::HTMLTextAreaElement>(element)) { |
337 | 0 | auto& input_element = static_cast<HTML::HTMLTextAreaElement const&>(element); |
338 | 0 | if (input_element.has_attribute(HTML::AttributeNames::readonly)) |
339 | 0 | return false; |
340 | 0 | if (!input_element.enabled()) |
341 | 0 | return false; |
342 | 0 | return true; |
343 | 0 | } |
344 | | // - elements that are editing hosts or editable and are neither input elements nor textarea elements |
345 | 0 | return element.is_editable(); |
346 | 0 | } |
347 | | |
348 | | // https://www.w3.org/TR/selectors-4/#open-state |
349 | | static bool matches_open_state_pseudo_class(DOM::Element const& element, bool open) |
350 | 0 | { |
351 | | // The :open pseudo-class represents an element that has both “open” and “closed” states, |
352 | | // and which is currently in the “open” state. |
353 | | // The :closed pseudo-class represents an element that has both “open” and “closed” states, |
354 | | // and which is currently in the closed state. |
355 | | |
356 | | // NOTE: Spec specifically suggests supporting <details>, <dialog>, and <select>. |
357 | | // There may be others we want to treat as open or closed. |
358 | 0 | if (is<HTML::HTMLDetailsElement>(element) || is<HTML::HTMLDialogElement>(element)) |
359 | 0 | return open == element.has_attribute(HTML::AttributeNames::open); |
360 | 0 | if (is<HTML::HTMLSelectElement>(element)) |
361 | 0 | return open == static_cast<HTML::HTMLSelectElement const&>(element).is_open(); |
362 | | |
363 | 0 | return false; |
364 | 0 | } |
365 | | |
366 | | // https://drafts.csswg.org/css-scoping/#host-selector |
367 | | static inline bool matches_host_pseudo_class(JS::NonnullGCPtr<DOM::Element const> element, JS::GCPtr<DOM::Element const> shadow_host, CSS::SelectorList const& argument_selector_list, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule) |
368 | 0 | { |
369 | | // When evaluated in the context of a shadow tree, it matches the shadow tree’s shadow host if the shadow host, |
370 | | // in its normal context, matches the selector argument. In any other context, it matches nothing. |
371 | 0 | if (!shadow_host || element != shadow_host) |
372 | 0 | return false; |
373 | | |
374 | | // NOTE: There's either 0 or 1 argument selector, since the syntax is :host or :host(<compound-selector>) |
375 | 0 | if (!argument_selector_list.is_empty()) |
376 | 0 | return matches(argument_selector_list.first(), style_sheet_for_rule, element, nullptr); |
377 | | |
378 | 0 | return true; |
379 | 0 | } |
380 | | |
381 | | static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host, JS::GCPtr<DOM::ParentNode const> scope, SelectorKind selector_kind) |
382 | 0 | { |
383 | 0 | switch (pseudo_class.type) { |
384 | 0 | case CSS::PseudoClass::Link: |
385 | 0 | case CSS::PseudoClass::AnyLink: |
386 | | // NOTE: AnyLink should match whether the link is visited or not, so if we ever start matching |
387 | | // :visited, we'll need to handle these differently. |
388 | 0 | return matches_link_pseudo_class(element); |
389 | 0 | case CSS::PseudoClass::LocalLink: { |
390 | | // The :local-link pseudo-class allows authors to style hyperlinks based on the users current location |
391 | | // within a site. It represents an element that is the source anchor of a hyperlink whose target’s |
392 | | // absolute URL matches the element’s own document URL. If the hyperlink’s target includes a fragment |
393 | | // URL, then the fragment URL of the current URL must also match; if it does not, then the fragment |
394 | | // URL portion of the current URL is not taken into account in the comparison. |
395 | 0 | if (!matches_link_pseudo_class(element)) |
396 | 0 | return false; |
397 | 0 | auto document_url = element.document().url(); |
398 | 0 | URL::URL target_url = element.document().parse_url(element.attribute(HTML::AttributeNames::href).value_or({})); |
399 | 0 | if (target_url.fragment().has_value()) |
400 | 0 | return document_url.equals(target_url, URL::ExcludeFragment::No); |
401 | 0 | return document_url.equals(target_url, URL::ExcludeFragment::Yes); |
402 | 0 | } |
403 | 0 | case CSS::PseudoClass::Visited: |
404 | | // FIXME: Maybe match this selector sometimes? |
405 | 0 | return false; |
406 | 0 | case CSS::PseudoClass::Active: |
407 | 0 | return element.is_active(); |
408 | 0 | case CSS::PseudoClass::Hover: |
409 | 0 | return matches_hover_pseudo_class(element); |
410 | 0 | case CSS::PseudoClass::Focus: |
411 | 0 | return element.is_focused(); |
412 | 0 | case CSS::PseudoClass::FocusVisible: |
413 | | // FIXME: We should only apply this when a visible focus is useful. Decide when that is! |
414 | 0 | return element.is_focused(); |
415 | 0 | case CSS::PseudoClass::FocusWithin: { |
416 | 0 | auto* focused_element = element.document().focused_element(); |
417 | 0 | return focused_element && element.is_inclusive_ancestor_of(*focused_element); |
418 | 0 | } |
419 | 0 | case CSS::PseudoClass::FirstChild: |
420 | 0 | return !element.previous_element_sibling(); |
421 | 0 | case CSS::PseudoClass::LastChild: |
422 | 0 | return !element.next_element_sibling(); |
423 | 0 | case CSS::PseudoClass::OnlyChild: |
424 | 0 | return !(element.previous_element_sibling() || element.next_element_sibling()); |
425 | 0 | case CSS::PseudoClass::Empty: { |
426 | 0 | if (!element.has_children()) |
427 | 0 | return true; |
428 | 0 | if (element.first_child_of_type<DOM::Element>()) |
429 | 0 | return false; |
430 | | // NOTE: CSS Selectors level 4 changed ":empty" to also match whitespace-only text nodes. |
431 | | // However, none of the major browser supports this yet, so let's just hang back until they do. |
432 | 0 | bool has_nonempty_text_child = false; |
433 | 0 | element.for_each_child_of_type<DOM::Text>([&](auto const& text_child) { |
434 | 0 | if (!text_child.data().is_empty()) { |
435 | 0 | has_nonempty_text_child = true; |
436 | 0 | return IterationDecision::Break; |
437 | 0 | } |
438 | 0 | return IterationDecision::Continue; |
439 | 0 | }); |
440 | 0 | return !has_nonempty_text_child; |
441 | 0 | } |
442 | 0 | case CSS::PseudoClass::Root: |
443 | 0 | return is<HTML::HTMLHtmlElement>(element); |
444 | 0 | case CSS::PseudoClass::Host: |
445 | 0 | return matches_host_pseudo_class(element, shadow_host, pseudo_class.argument_selector_list, style_sheet_for_rule); |
446 | 0 | case CSS::PseudoClass::Scope: |
447 | 0 | return scope ? &element == scope : is<HTML::HTMLHtmlElement>(element); |
448 | 0 | case CSS::PseudoClass::FirstOfType: |
449 | 0 | return !previous_sibling_with_same_tag_name(element); |
450 | 0 | case CSS::PseudoClass::LastOfType: |
451 | 0 | return !next_sibling_with_same_tag_name(element); |
452 | 0 | case CSS::PseudoClass::OnlyOfType: |
453 | 0 | return !previous_sibling_with_same_tag_name(element) && !next_sibling_with_same_tag_name(element); |
454 | 0 | case CSS::PseudoClass::Lang: |
455 | 0 | return matches_lang_pseudo_class(element, pseudo_class.languages); |
456 | 0 | case CSS::PseudoClass::Disabled: |
457 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-disabled |
458 | | // The :disabled pseudo-class must match any element that is actually disabled. |
459 | 0 | return element.is_actually_disabled(); |
460 | 0 | case CSS::PseudoClass::Enabled: |
461 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-enabled |
462 | | // The :enabled pseudo-class must match any button, input, select, textarea, optgroup, option, fieldset element, or form-associated custom element that is not actually disabled. |
463 | 0 | return (is<HTML::HTMLButtonElement>(element) || is<HTML::HTMLInputElement>(element) || is<HTML::HTMLSelectElement>(element) || is<HTML::HTMLTextAreaElement>(element) || is<HTML::HTMLOptGroupElement>(element) || is<HTML::HTMLOptionElement>(element) || is<HTML::HTMLFieldSetElement>(element)) |
464 | 0 | && !element.is_actually_disabled(); |
465 | 0 | case CSS::PseudoClass::Checked: |
466 | 0 | return matches_checked_pseudo_class(element); |
467 | 0 | case CSS::PseudoClass::Indeterminate: |
468 | 0 | return matches_indeterminate_pseudo_class(element); |
469 | 0 | case CSS::PseudoClass::Defined: |
470 | 0 | return element.is_defined(); |
471 | 0 | case CSS::PseudoClass::Has: |
472 | | // :has() cannot be nested in a :has() |
473 | 0 | if (selector_kind == SelectorKind::Relative) |
474 | 0 | return false; |
475 | | // These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector) |
476 | 0 | for (auto& selector : pseudo_class.argument_selector_list) { |
477 | 0 | if (matches_has_pseudo_class(selector, style_sheet_for_rule, element, shadow_host)) |
478 | 0 | return true; |
479 | 0 | } |
480 | 0 | return false; |
481 | 0 | case CSS::PseudoClass::Is: |
482 | 0 | case CSS::PseudoClass::Where: |
483 | 0 | for (auto& selector : pseudo_class.argument_selector_list) { |
484 | 0 | if (matches(selector, style_sheet_for_rule, element, shadow_host)) |
485 | 0 | return true; |
486 | 0 | } |
487 | 0 | return false; |
488 | 0 | case CSS::PseudoClass::Not: |
489 | 0 | for (auto& selector : pseudo_class.argument_selector_list) { |
490 | 0 | if (matches(selector, style_sheet_for_rule, element, shadow_host)) |
491 | 0 | return false; |
492 | 0 | } |
493 | 0 | return true; |
494 | 0 | case CSS::PseudoClass::NthChild: |
495 | 0 | case CSS::PseudoClass::NthLastChild: |
496 | 0 | case CSS::PseudoClass::NthOfType: |
497 | 0 | case CSS::PseudoClass::NthLastOfType: { |
498 | 0 | auto const step_size = pseudo_class.nth_child_pattern.step_size; |
499 | 0 | auto const offset = pseudo_class.nth_child_pattern.offset; |
500 | 0 | if (step_size == 0 && offset == 0) |
501 | 0 | return false; // "If both a and b are equal to zero, the pseudo-class represents no element in the document tree." |
502 | | |
503 | 0 | auto const* parent = element.parent_element(); |
504 | 0 | if (!parent) |
505 | 0 | return false; |
506 | | |
507 | 0 | auto matches_selector_list = [&style_sheet_for_rule, shadow_host](CSS::SelectorList const& list, DOM::Element const& element) { |
508 | 0 | if (list.is_empty()) |
509 | 0 | return true; |
510 | 0 | for (auto const& child_selector : list) { |
511 | 0 | if (matches(child_selector, style_sheet_for_rule, element, shadow_host)) { |
512 | 0 | return true; |
513 | 0 | } |
514 | 0 | } |
515 | 0 | return false; |
516 | 0 | }; |
517 | |
|
518 | 0 | int index = 1; |
519 | 0 | switch (pseudo_class.type) { |
520 | 0 | case CSS::PseudoClass::NthChild: { |
521 | 0 | if (!matches_selector_list(pseudo_class.argument_selector_list, element)) |
522 | 0 | return false; |
523 | 0 | for (auto* child = parent->first_child_of_type<DOM::Element>(); child && child != &element; child = child->next_element_sibling()) { |
524 | 0 | if (matches_selector_list(pseudo_class.argument_selector_list, *child)) |
525 | 0 | ++index; |
526 | 0 | } |
527 | 0 | break; |
528 | 0 | } |
529 | 0 | case CSS::PseudoClass::NthLastChild: { |
530 | 0 | if (!matches_selector_list(pseudo_class.argument_selector_list, element)) |
531 | 0 | return false; |
532 | 0 | for (auto* child = parent->last_child_of_type<DOM::Element>(); child && child != &element; child = child->previous_element_sibling()) { |
533 | 0 | if (matches_selector_list(pseudo_class.argument_selector_list, *child)) |
534 | 0 | ++index; |
535 | 0 | } |
536 | 0 | break; |
537 | 0 | } |
538 | 0 | case CSS::PseudoClass::NthOfType: { |
539 | 0 | for (auto* child = previous_sibling_with_same_tag_name(element); child; child = previous_sibling_with_same_tag_name(*child)) |
540 | 0 | ++index; |
541 | 0 | break; |
542 | 0 | } |
543 | 0 | case CSS::PseudoClass::NthLastOfType: { |
544 | 0 | for (auto* child = next_sibling_with_same_tag_name(element); child; child = next_sibling_with_same_tag_name(*child)) |
545 | 0 | ++index; |
546 | 0 | break; |
547 | 0 | } |
548 | 0 | default: |
549 | 0 | VERIFY_NOT_REACHED(); |
550 | 0 | } |
551 | | |
552 | | // When "step_size == -1", selector represents first "offset" elements in document tree. |
553 | 0 | if (step_size == -1) |
554 | 0 | return !(offset <= 0 || index > offset); |
555 | | |
556 | | // When "step_size == 1", selector represents last "offset" elements in document tree. |
557 | 0 | if (step_size == 1) |
558 | 0 | return !(offset < 0 || index < offset); |
559 | | |
560 | | // When "step_size == 0", selector picks only the "offset" element. |
561 | 0 | if (step_size == 0) |
562 | 0 | return index == offset; |
563 | | |
564 | | // If both are negative, nothing can match. |
565 | 0 | if (step_size < 0 && offset < 0) |
566 | 0 | return false; |
567 | | |
568 | | // Like "a % b", but handles negative integers correctly. |
569 | 0 | auto const canonical_modulo = [](int a, int b) -> int { |
570 | 0 | int c = a % b; |
571 | 0 | if ((c < 0 && b > 0) || (c > 0 && b < 0)) { |
572 | 0 | c += b; |
573 | 0 | } |
574 | 0 | return c; |
575 | 0 | }; |
576 | | |
577 | | // When "step_size < 0", we start at "offset" and count backwards. |
578 | 0 | if (step_size < 0) |
579 | 0 | return index <= offset && canonical_modulo(index - offset, -step_size) == 0; |
580 | | |
581 | | // Otherwise, we start at "offset" and count forwards. |
582 | 0 | return index >= offset && canonical_modulo(index - offset, step_size) == 0; |
583 | 0 | } |
584 | 0 | case CSS::PseudoClass::Playing: { |
585 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
586 | 0 | return false; |
587 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
588 | 0 | return !media_element.paused(); |
589 | 0 | } |
590 | 0 | case CSS::PseudoClass::Paused: { |
591 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
592 | 0 | return false; |
593 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
594 | 0 | return media_element.paused(); |
595 | 0 | } |
596 | 0 | case CSS::PseudoClass::Seeking: { |
597 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
598 | 0 | return false; |
599 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
600 | 0 | return media_element.seeking(); |
601 | 0 | } |
602 | 0 | case CSS::PseudoClass::Muted: { |
603 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
604 | 0 | return false; |
605 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
606 | 0 | return media_element.muted(); |
607 | 0 | } |
608 | 0 | case CSS::PseudoClass::VolumeLocked: { |
609 | | // FIXME: Currently we don't allow the user to specify an override volume, so this is always false. |
610 | | // Once we do, implement this! |
611 | 0 | return false; |
612 | 0 | } |
613 | 0 | case CSS::PseudoClass::Buffering: { |
614 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
615 | 0 | return false; |
616 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
617 | 0 | return media_element.blocked(); |
618 | 0 | } |
619 | 0 | case CSS::PseudoClass::Stalled: { |
620 | 0 | if (!is<HTML::HTMLMediaElement>(element)) |
621 | 0 | return false; |
622 | 0 | auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element); |
623 | 0 | return media_element.stalled(); |
624 | 0 | } |
625 | 0 | case CSS::PseudoClass::Target: |
626 | 0 | return element.is_target(); |
627 | 0 | case CSS::PseudoClass::TargetWithin: { |
628 | 0 | auto* target_element = element.document().target_element(); |
629 | 0 | if (!target_element) |
630 | 0 | return false; |
631 | 0 | return element.is_inclusive_ancestor_of(*target_element); |
632 | 0 | } |
633 | 0 | case CSS::PseudoClass::Dir: { |
634 | | // "Values other than ltr and rtl are not invalid, but do not match anything." |
635 | | // - https://www.w3.org/TR/selectors-4/#the-dir-pseudo |
636 | 0 | if (!first_is_one_of(pseudo_class.keyword, CSS::Keyword::Ltr, CSS::Keyword::Rtl)) |
637 | 0 | return false; |
638 | 0 | switch (element.directionality()) { |
639 | 0 | case DOM::Element::Directionality::Ltr: |
640 | 0 | return pseudo_class.keyword == CSS::Keyword::Ltr; |
641 | 0 | case DOM::Element::Directionality::Rtl: |
642 | 0 | return pseudo_class.keyword == CSS::Keyword::Rtl; |
643 | 0 | } |
644 | 0 | VERIFY_NOT_REACHED(); |
645 | 0 | } |
646 | 0 | case CSS::PseudoClass::ReadOnly: |
647 | 0 | return !matches_read_write_pseudo_class(element); |
648 | 0 | case CSS::PseudoClass::ReadWrite: |
649 | 0 | return matches_read_write_pseudo_class(element); |
650 | 0 | case CSS::PseudoClass::PlaceholderShown: { |
651 | | // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-placeholder-shown |
652 | | // The :placeholder-shown pseudo-class must match any element falling into one of the following categories: |
653 | | // - input elements that have a placeholder attribute whose value is currently being presented to the user. |
654 | 0 | if (is<HTML::HTMLInputElement>(element) && element.has_attribute(HTML::AttributeNames::placeholder)) { |
655 | 0 | auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element); |
656 | 0 | return input_element.placeholder_element() && input_element.placeholder_value().has_value(); |
657 | 0 | } |
658 | | // - FIXME: textarea elements that have a placeholder attribute whose value is currently being presented to the user. |
659 | 0 | return false; |
660 | 0 | } |
661 | 0 | case CSS::PseudoClass::Open: |
662 | 0 | return matches_open_state_pseudo_class(element, pseudo_class.type == CSS::PseudoClass::Open); |
663 | 0 | case CSS::PseudoClass::Modal: { |
664 | | // https://drafts.csswg.org/selectors/#modal-state |
665 | 0 | if (is<HTML::HTMLDialogElement>(element)) { |
666 | 0 | auto const& dialog_element = static_cast<HTML::HTMLDialogElement const&>(element); |
667 | 0 | return dialog_element.is_modal(); |
668 | 0 | } |
669 | | // FIXME: fullscreen elements are also modal. |
670 | 0 | return false; |
671 | 0 | } |
672 | 0 | } |
673 | | |
674 | 0 | return false; |
675 | 0 | } |
676 | | |
677 | | static ALWAYS_INLINE bool matches_namespace( |
678 | | CSS::Selector::SimpleSelector::QualifiedName const& qualified_name, |
679 | | DOM::Element const& element, |
680 | | Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule) |
681 | 0 | { |
682 | 0 | switch (qualified_name.namespace_type) { |
683 | 0 | case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: |
684 | | // "if no default namespace has been declared for selectors, this is equivalent to *|E." |
685 | 0 | if (!style_sheet_for_rule.has_value() || !style_sheet_for_rule->default_namespace_rule()) |
686 | 0 | return true; |
687 | | // "Otherwise it is equivalent to ns|E where ns is the default namespace." |
688 | 0 | return element.namespace_uri() == style_sheet_for_rule->default_namespace_rule()->namespace_uri(); |
689 | 0 | case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: |
690 | | // "elements with name E without a namespace" |
691 | 0 | return !element.namespace_uri().has_value(); |
692 | 0 | case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: |
693 | | // "elements with name E in any namespace, including those without a namespace" |
694 | 0 | return true; |
695 | 0 | case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: |
696 | | // "elements with name E in namespace ns" |
697 | | // Unrecognized namespace prefixes are invalid, so don't match. |
698 | | // (We can't detect this at parse time, since a namespace rule may be inserted later.) |
699 | | // So, if we don't have a context to look up namespaces from, we fail to match. |
700 | 0 | if (!style_sheet_for_rule.has_value()) |
701 | 0 | return false; |
702 | | |
703 | 0 | auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); |
704 | 0 | return selector_namespace.has_value() && selector_namespace.value() == element.namespace_uri(); |
705 | 0 | } |
706 | 0 | VERIFY_NOT_REACHED(); |
707 | 0 | } |
708 | | |
709 | | static inline bool matches(CSS::Selector::SimpleSelector const& component, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host, JS::GCPtr<DOM::ParentNode const> scope, SelectorKind selector_kind) |
710 | 0 | { |
711 | 0 | switch (component.type) { |
712 | 0 | case CSS::Selector::SimpleSelector::Type::Universal: |
713 | 0 | case CSS::Selector::SimpleSelector::Type::TagName: { |
714 | 0 | auto const& qualified_name = component.qualified_name(); |
715 | | |
716 | | // Reject if the tag name doesn't match |
717 | 0 | if (component.type == CSS::Selector::SimpleSelector::Type::TagName) { |
718 | | // See https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors |
719 | 0 | if (element.document().document_type() == DOM::Document::Type::HTML) { |
720 | 0 | if (qualified_name.name.lowercase_name != element.local_name()) |
721 | 0 | return false; |
722 | 0 | } else if (!Infra::is_ascii_case_insensitive_match(qualified_name.name.name, element.local_name())) { |
723 | 0 | return false; |
724 | 0 | } |
725 | 0 | } |
726 | | |
727 | 0 | return matches_namespace(qualified_name, element, style_sheet_for_rule); |
728 | 0 | } |
729 | 0 | case CSS::Selector::SimpleSelector::Type::Id: |
730 | 0 | return component.name() == element.id(); |
731 | 0 | case CSS::Selector::SimpleSelector::Type::Class: { |
732 | | // Class selectors are matched case insensitively in quirks mode. |
733 | | // See: https://drafts.csswg.org/selectors-4/#class-html |
734 | 0 | auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; |
735 | 0 | return element.has_class(component.name(), case_sensitivity); |
736 | 0 | } |
737 | 0 | case CSS::Selector::SimpleSelector::Type::Attribute: |
738 | 0 | return matches_attribute(component.attribute(), style_sheet_for_rule, element); |
739 | 0 | case CSS::Selector::SimpleSelector::Type::PseudoClass: |
740 | 0 | return matches_pseudo_class(component.pseudo_class(), style_sheet_for_rule, element, shadow_host, scope, selector_kind); |
741 | 0 | case CSS::Selector::SimpleSelector::Type::PseudoElement: |
742 | | // Pseudo-element matching/not-matching is handled in the top level matches(). |
743 | 0 | return true; |
744 | 0 | case CSS::Selector::SimpleSelector::Type::Nesting: |
745 | | // We should only try to match selectors that have been absolutized! |
746 | 0 | VERIFY_NOT_REACHED(); |
747 | 0 | } |
748 | 0 | VERIFY_NOT_REACHED(); |
749 | 0 | } |
750 | | |
751 | | static inline bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, int component_list_index, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host, JS::GCPtr<DOM::ParentNode const> scope, SelectorKind selector_kind) |
752 | 0 | { |
753 | 0 | auto& compound_selector = selector.compound_selectors()[component_list_index]; |
754 | 0 | for (auto& simple_selector : compound_selector.simple_selectors) { |
755 | 0 | if (!matches(simple_selector, style_sheet_for_rule, element, shadow_host, scope, selector_kind)) { |
756 | 0 | return false; |
757 | 0 | } |
758 | 0 | } |
759 | | // Always matches because we assume that element is already relative to its anchor |
760 | 0 | if (selector_kind == SelectorKind::Relative && component_list_index == 0) |
761 | 0 | return true; |
762 | 0 | switch (compound_selector.combinator) { |
763 | 0 | case CSS::Selector::Combinator::None: |
764 | 0 | VERIFY(selector_kind != SelectorKind::Relative); |
765 | 0 | return true; |
766 | 0 | case CSS::Selector::Combinator::Descendant: |
767 | 0 | VERIFY(component_list_index != 0); |
768 | 0 | for (auto ancestor = traverse_up(element, shadow_host); ancestor; ancestor = traverse_up(ancestor, shadow_host)) { |
769 | 0 | if (!is<DOM::Element>(*ancestor)) |
770 | 0 | continue; |
771 | 0 | if (matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), shadow_host, scope, selector_kind)) |
772 | 0 | return true; |
773 | 0 | } |
774 | 0 | return false; |
775 | 0 | case CSS::Selector::Combinator::ImmediateChild: { |
776 | 0 | VERIFY(component_list_index != 0); |
777 | 0 | auto parent = traverse_up(element, shadow_host); |
778 | 0 | if (!parent || !parent->is_element()) |
779 | 0 | return false; |
780 | 0 | return matches(selector, style_sheet_for_rule, component_list_index - 1, static_cast<DOM::Element const&>(*parent), shadow_host, scope, selector_kind); |
781 | 0 | } |
782 | 0 | case CSS::Selector::Combinator::NextSibling: |
783 | 0 | VERIFY(component_list_index != 0); |
784 | 0 | if (auto* sibling = element.previous_element_sibling()) |
785 | 0 | return matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, shadow_host, scope, selector_kind); |
786 | 0 | return false; |
787 | 0 | case CSS::Selector::Combinator::SubsequentSibling: |
788 | 0 | VERIFY(component_list_index != 0); |
789 | 0 | for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) { |
790 | 0 | if (matches(selector, style_sheet_for_rule, component_list_index - 1, *sibling, shadow_host, scope, selector_kind)) |
791 | 0 | return true; |
792 | 0 | } |
793 | 0 | return false; |
794 | 0 | case CSS::Selector::Combinator::Column: |
795 | 0 | TODO(); |
796 | 0 | } |
797 | 0 | VERIFY_NOT_REACHED(); |
798 | 0 | } |
799 | | |
800 | | bool matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host, Optional<CSS::Selector::PseudoElement::Type> pseudo_element, JS::GCPtr<DOM::ParentNode const> scope, SelectorKind selector_kind) |
801 | 0 | { |
802 | 0 | VERIFY(!selector.compound_selectors().is_empty()); |
803 | 0 | if (pseudo_element.has_value() && selector.pseudo_element().has_value() && selector.pseudo_element().value().type() != pseudo_element) |
804 | 0 | return false; |
805 | 0 | if (!pseudo_element.has_value() && selector.pseudo_element().has_value()) |
806 | 0 | return false; |
807 | 0 | return matches(selector, style_sheet_for_rule, selector.compound_selectors().size() - 1, element, shadow_host, scope, selector_kind); |
808 | 0 | } |
809 | | |
810 | | static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host) |
811 | 0 | { |
812 | 0 | switch (simple_selector.type) { |
813 | 0 | case CSS::Selector::SimpleSelector::Type::Universal: |
814 | 0 | return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule); |
815 | 0 | case CSS::Selector::SimpleSelector::Type::TagName: |
816 | 0 | if (element.document().document_type() == DOM::Document::Type::HTML) { |
817 | 0 | if (simple_selector.qualified_name().name.lowercase_name != element.local_name()) |
818 | 0 | return false; |
819 | 0 | } else if (!Infra::is_ascii_case_insensitive_match(simple_selector.qualified_name().name.name, element.local_name())) { |
820 | 0 | return false; |
821 | 0 | } |
822 | 0 | return matches_namespace(simple_selector.qualified_name(), element, style_sheet_for_rule); |
823 | 0 | case CSS::Selector::SimpleSelector::Type::Class: { |
824 | | // Class selectors are matched case insensitively in quirks mode. |
825 | | // See: https://drafts.csswg.org/selectors-4/#class-html |
826 | 0 | auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; |
827 | 0 | return element.has_class(simple_selector.name(), case_sensitivity); |
828 | 0 | } |
829 | 0 | case CSS::Selector::SimpleSelector::Type::Id: |
830 | 0 | return simple_selector.name() == element.id(); |
831 | 0 | case CSS::Selector::SimpleSelector::Type::Attribute: |
832 | 0 | return matches_attribute(simple_selector.attribute(), style_sheet_for_rule, element); |
833 | 0 | case CSS::Selector::SimpleSelector::Type::PseudoClass: |
834 | 0 | return matches_pseudo_class(simple_selector.pseudo_class(), style_sheet_for_rule, element, shadow_host, nullptr, SelectorKind::Normal); |
835 | 0 | default: |
836 | 0 | VERIFY_NOT_REACHED(); |
837 | 0 | } |
838 | 0 | } |
839 | | |
840 | | static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element, JS::GCPtr<DOM::Element const> shadow_host) |
841 | 0 | { |
842 | 0 | for (auto const& simple_selector : compound_selector.simple_selectors) { |
843 | 0 | if (!fast_matches_simple_selector(simple_selector, style_sheet_for_rule, element, shadow_host)) |
844 | 0 | return false; |
845 | 0 | } |
846 | 0 | return true; |
847 | 0 | } |
848 | | |
849 | | bool fast_matches(CSS::Selector const& selector, Optional<CSS::CSSStyleSheet const&> style_sheet_for_rule, DOM::Element const& element_to_match, JS::GCPtr<DOM::Element const> shadow_host) |
850 | 0 | { |
851 | 0 | DOM::Element const* current = &element_to_match; |
852 | |
|
853 | 0 | ssize_t compound_selector_index = selector.compound_selectors().size() - 1; |
854 | |
|
855 | 0 | if (!fast_matches_compound_selector(selector.compound_selectors().last(), style_sheet_for_rule, *current, shadow_host)) |
856 | 0 | return false; |
857 | | |
858 | | // NOTE: If we fail after following a child combinator, we may need to backtrack |
859 | | // to the last matched descendant. We store the state here. |
860 | 0 | struct { |
861 | 0 | JS::GCPtr<DOM::Element const> element; |
862 | 0 | ssize_t compound_selector_index = 0; |
863 | 0 | } backtrack_state; |
864 | |
|
865 | 0 | for (;;) { |
866 | | // NOTE: There should always be a leftmost compound selector without combinator that kicks us out of this loop. |
867 | 0 | VERIFY(compound_selector_index >= 0); |
868 | | |
869 | 0 | auto const* compound_selector = &selector.compound_selectors()[compound_selector_index]; |
870 | |
|
871 | 0 | switch (compound_selector->combinator) { |
872 | 0 | case CSS::Selector::Combinator::None: |
873 | 0 | return true; |
874 | 0 | case CSS::Selector::Combinator::Descendant: |
875 | 0 | backtrack_state = { current->parent_element(), compound_selector_index }; |
876 | 0 | compound_selector = &selector.compound_selectors()[--compound_selector_index]; |
877 | 0 | for (current = current->parent_element(); current; current = current->parent_element()) { |
878 | 0 | if (fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current, shadow_host)) |
879 | 0 | break; |
880 | 0 | } |
881 | 0 | if (!current) |
882 | 0 | return false; |
883 | 0 | break; |
884 | 0 | case CSS::Selector::Combinator::ImmediateChild: |
885 | 0 | compound_selector = &selector.compound_selectors()[--compound_selector_index]; |
886 | 0 | current = current->parent_element(); |
887 | 0 | if (!current) |
888 | 0 | return false; |
889 | 0 | if (!fast_matches_compound_selector(*compound_selector, style_sheet_for_rule, *current, shadow_host)) { |
890 | 0 | if (backtrack_state.element) { |
891 | 0 | current = backtrack_state.element; |
892 | 0 | compound_selector_index = backtrack_state.compound_selector_index; |
893 | 0 | continue; |
894 | 0 | } |
895 | 0 | return false; |
896 | 0 | } |
897 | 0 | break; |
898 | 0 | default: |
899 | 0 | VERIFY_NOT_REACHED(); |
900 | 0 | } |
901 | 0 | } |
902 | 0 | } |
903 | | |
904 | | bool can_use_fast_matches(CSS::Selector const& selector) |
905 | 0 | { |
906 | 0 | for (auto const& compound_selector : selector.compound_selectors()) { |
907 | 0 | if (compound_selector.combinator != CSS::Selector::Combinator::None |
908 | 0 | && compound_selector.combinator != CSS::Selector::Combinator::Descendant |
909 | 0 | && compound_selector.combinator != CSS::Selector::Combinator::ImmediateChild) { |
910 | 0 | return false; |
911 | 0 | } |
912 | | |
913 | 0 | for (auto const& simple_selector : compound_selector.simple_selectors) { |
914 | 0 | if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoClass) { |
915 | 0 | auto const pseudo_class = simple_selector.pseudo_class().type; |
916 | 0 | if (pseudo_class != CSS::PseudoClass::FirstChild |
917 | 0 | && pseudo_class != CSS::PseudoClass::LastChild |
918 | 0 | && pseudo_class != CSS::PseudoClass::OnlyChild |
919 | 0 | && pseudo_class != CSS::PseudoClass::Hover |
920 | 0 | && pseudo_class != CSS::PseudoClass::Active |
921 | 0 | && pseudo_class != CSS::PseudoClass::Focus |
922 | 0 | && pseudo_class != CSS::PseudoClass::FocusVisible |
923 | 0 | && pseudo_class != CSS::PseudoClass::FocusWithin |
924 | 0 | && pseudo_class != CSS::PseudoClass::Link |
925 | 0 | && pseudo_class != CSS::PseudoClass::AnyLink |
926 | 0 | && pseudo_class != CSS::PseudoClass::Visited |
927 | 0 | && pseudo_class != CSS::PseudoClass::LocalLink |
928 | 0 | && pseudo_class != CSS::PseudoClass::Empty |
929 | 0 | && pseudo_class != CSS::PseudoClass::Root |
930 | 0 | && pseudo_class != CSS::PseudoClass::Enabled |
931 | 0 | && pseudo_class != CSS::PseudoClass::Disabled |
932 | 0 | && pseudo_class != CSS::PseudoClass::Checked) { |
933 | 0 | return false; |
934 | 0 | } |
935 | 0 | } else if (simple_selector.type != CSS::Selector::SimpleSelector::Type::TagName |
936 | 0 | && simple_selector.type != CSS::Selector::SimpleSelector::Type::Universal |
937 | 0 | && simple_selector.type != CSS::Selector::SimpleSelector::Type::Class |
938 | 0 | && simple_selector.type != CSS::Selector::SimpleSelector::Type::Id |
939 | 0 | && simple_selector.type != CSS::Selector::SimpleSelector::Type::Attribute) { |
940 | 0 | return false; |
941 | 0 | } |
942 | 0 | } |
943 | 0 | } |
944 | | |
945 | 0 | return true; |
946 | 0 | } |
947 | | |
948 | | } |