Previously, and according to the spec, `a::part(foo)::before` would be a single CompoundSelector, even though it matches against 3 different targets. This meant some awkward swapping of targets in the middle of matching, and in particular it made `::part()` and `::slotted()` quite hacky, requiring them to track extra data on the MatchContext to then use later. This was scattered around and difficult to follow. Partly inspired by Gecko, this commit instead introduces an invisible PseudoElement combinator. After parsing a selector, we find any CompoundSelectors that contain a pseudo-element and split them up, so that each CompoundSelector only has a single target in the end. Where the pseudo-element was at the start of a CompoundSelector, we insert an invisible universal selector before it to represent its originating element. So now, a CompoundSelector deals with one target, and switching targets is done at the combinator. The one inconsistency is that we match the target of ::slotted() and ::part() in pseudo_element_transition_target(), instead of before then when processing the SimpleSelector. This is to avoid repeating the same computations twice. No outward-facing behaviour changes, though the invalidation metrics have changed.
279 lines
9.3 KiB
C++
279 lines
9.3 KiB
C++
/*
|
|
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <AK/FlyString.h>
|
|
#include <AK/RefCounted.h>
|
|
#include <AK/String.h>
|
|
#include <AK/Vector.h>
|
|
#include <LibWeb/CSS/Keyword.h>
|
|
#include <LibWeb/CSS/Parser/ComponentValue.h>
|
|
#include <LibWeb/CSS/PseudoClass.h>
|
|
#include <LibWeb/CSS/PseudoClassBitmap.h>
|
|
#include <LibWeb/CSS/PseudoElement.h>
|
|
|
|
namespace Web::CSS {
|
|
|
|
using SelectorList = Vector<NonnullRefPtr<class Selector>>;
|
|
|
|
// This is a <complex-selector> in the spec. https://www.w3.org/TR/selectors-4/#complex
|
|
class Selector : public RefCounted<Selector> {
|
|
public:
|
|
class PseudoElementSelector {
|
|
public:
|
|
struct PTNameSelector {
|
|
bool is_universal { false };
|
|
FlyString value {};
|
|
};
|
|
using IdentList = Vector<FlyString>;
|
|
|
|
using Value = Variant<Empty, PTNameSelector, NonnullRefPtr<Selector>, IdentList>;
|
|
|
|
explicit PseudoElementSelector(PseudoElement type, Value value = {})
|
|
: m_type(type)
|
|
, m_value(move(value))
|
|
{
|
|
VERIFY(is_known_pseudo_element_type(type));
|
|
}
|
|
|
|
PseudoElementSelector(PseudoElement type, String name, Value value = {})
|
|
: m_type(type)
|
|
, m_name(move(name))
|
|
, m_value(move(value))
|
|
{
|
|
}
|
|
|
|
bool operator==(PseudoElementSelector const&) const = default;
|
|
|
|
[[nodiscard]] static bool is_known_pseudo_element_type(PseudoElement type)
|
|
{
|
|
return to_underlying(type) < to_underlying(PseudoElement::KnownPseudoElementCount);
|
|
}
|
|
|
|
String serialize() const;
|
|
|
|
PseudoElement type() const { return m_type; }
|
|
|
|
PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); }
|
|
|
|
// NOTE: This can't (currently) be a CompoundSelector due to cyclic dependencies.
|
|
Selector const& compound_selector() const { return m_value.get<NonnullRefPtr<Selector>>(); }
|
|
|
|
IdentList const& ident_list() const { return m_value.get<IdentList>(); }
|
|
|
|
private:
|
|
PseudoElement m_type;
|
|
String m_name;
|
|
Value m_value;
|
|
};
|
|
|
|
struct SimpleSelector {
|
|
enum class Type : u8 {
|
|
Universal,
|
|
TagName,
|
|
Id,
|
|
Class,
|
|
Attribute,
|
|
PseudoClass,
|
|
PseudoElement,
|
|
Nesting,
|
|
Invalid,
|
|
};
|
|
|
|
struct ANPlusBPattern {
|
|
int step_size { 0 }; // "A"
|
|
int offset = { 0 }; // "B"
|
|
|
|
bool matches(int index) const;
|
|
String serialize() const;
|
|
};
|
|
|
|
struct PseudoClassSelector {
|
|
PseudoClass type;
|
|
|
|
// Used for the :nth-*() pseudo-classes
|
|
ANPlusBPattern an_plus_b_pattern {};
|
|
|
|
// FIXME: This would make more sense as part of SelectorList but that's currently a `using`
|
|
bool is_forgiving { false };
|
|
SelectorList argument_selector_list {};
|
|
|
|
// Used for :lang(en-gb,dk)
|
|
Vector<FlyString> languages {};
|
|
|
|
// Used by :dir()
|
|
struct Ident {
|
|
Keyword keyword;
|
|
FlyString string_value;
|
|
};
|
|
Optional<Ident> ident {};
|
|
|
|
// Used by :heading()
|
|
Vector<i64> levels {};
|
|
};
|
|
|
|
struct Name {
|
|
Name(FlyString n)
|
|
: name(move(n))
|
|
, lowercase_name(name.to_ascii_lowercase())
|
|
{
|
|
}
|
|
|
|
FlyString name;
|
|
FlyString lowercase_name;
|
|
};
|
|
|
|
// Equivalent to `<wq-name>`
|
|
// https://www.w3.org/TR/selectors-4/#typedef-wq-name
|
|
struct QualifiedName {
|
|
enum class NamespaceType {
|
|
Default, // `E`
|
|
None, // `|E`
|
|
Any, // `*|E`
|
|
Named, // `ns|E`
|
|
};
|
|
NamespaceType namespace_type { NamespaceType::Default };
|
|
FlyString namespace_ {};
|
|
Name name;
|
|
};
|
|
|
|
struct Attribute {
|
|
enum class MatchType {
|
|
HasAttribute,
|
|
ExactValueMatch,
|
|
ContainsWord, // [att~=val]
|
|
ContainsString, // [att*=val]
|
|
StartsWithSegment, // [att|=val]
|
|
StartsWithString, // [att^=val]
|
|
EndsWithString, // [att$=val]
|
|
};
|
|
enum class CaseType {
|
|
DefaultMatch,
|
|
CaseSensitiveMatch,
|
|
CaseInsensitiveMatch,
|
|
};
|
|
MatchType match_type;
|
|
QualifiedName qualified_name;
|
|
String value {};
|
|
CaseType case_type;
|
|
};
|
|
|
|
struct Invalid {
|
|
Vector<Parser::ComponentValue> component_values;
|
|
};
|
|
|
|
Type type;
|
|
Variant<Empty, Attribute, PseudoClassSelector, PseudoElementSelector, Name, QualifiedName, Invalid> value {};
|
|
|
|
Attribute const& attribute() const { return value.get<Attribute>(); }
|
|
Attribute& attribute() { return value.get<Attribute>(); }
|
|
PseudoClassSelector const& pseudo_class() const { return value.get<PseudoClassSelector>(); }
|
|
PseudoClassSelector& pseudo_class() { return value.get<PseudoClassSelector>(); }
|
|
PseudoElementSelector const& pseudo_element() const { return value.get<PseudoElementSelector>(); }
|
|
PseudoElementSelector& pseudo_element() { return value.get<PseudoElementSelector>(); }
|
|
|
|
FlyString const& name() const { return value.get<Name>().name; }
|
|
FlyString& name() { return value.get<Name>().name; }
|
|
FlyString const& lowercase_name() const { return value.get<Name>().lowercase_name; }
|
|
FlyString& lowercase_name() { return value.get<Name>().lowercase_name; }
|
|
QualifiedName const& qualified_name() const { return value.get<QualifiedName>(); }
|
|
QualifiedName& qualified_name() { return value.get<QualifiedName>(); }
|
|
|
|
String serialize() const;
|
|
|
|
Optional<SimpleSelector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
|
};
|
|
|
|
enum class Combinator {
|
|
None,
|
|
ImmediateChild, // >
|
|
Descendant, // <whitespace>
|
|
NextSibling, // +
|
|
SubsequentSibling, // ~
|
|
Column, // ||
|
|
PseudoElement, // Internal-only transition to a different AbstractElement
|
|
};
|
|
|
|
struct CompoundSelector {
|
|
// Spec-wise, the <combinator> is not part of a <compound-selector>,
|
|
// but it is more understandable to put them together.
|
|
Combinator combinator { Combinator::None };
|
|
bool is_implicit_universal_anchor { false };
|
|
Vector<SimpleSelector> simple_selectors;
|
|
|
|
Optional<CompoundSelector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
|
};
|
|
|
|
static NonnullRefPtr<Selector> create(Vector<CompoundSelector>&& compound_selectors)
|
|
{
|
|
return adopt_ref(*new Selector(move(compound_selectors)));
|
|
}
|
|
|
|
~Selector() = default;
|
|
|
|
Vector<CompoundSelector> const& compound_selectors() const { return m_compound_selectors; }
|
|
Optional<PseudoElement> target_pseudo_element() const { return m_target_pseudo_element; }
|
|
bool contains_pseudo_element_transition() const { return m_contains_pseudo_element_transition; }
|
|
NonnullRefPtr<Selector> relative_to(SimpleSelector const&) const;
|
|
bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; }
|
|
bool contains_pseudo_class(PseudoClass pseudo_class) const { return m_contained_pseudo_classes.get(pseudo_class); }
|
|
bool contains_unknown_webkit_pseudo_element() const;
|
|
RefPtr<Selector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
|
u32 specificity() const;
|
|
String serialize() const;
|
|
|
|
auto const& ancestor_hashes() const { return m_ancestor_hashes; }
|
|
|
|
bool can_use_fast_matches() const { return m_can_use_fast_matches; }
|
|
bool can_use_ancestor_filter() const { return m_can_use_ancestor_filter; }
|
|
|
|
size_t sibling_invalidation_distance() const;
|
|
|
|
bool is_slotted() const { return m_contains_slotted_pseudo_element; }
|
|
bool has_part_pseudo_element() const { return m_contains_part_pseudo_element; }
|
|
|
|
private:
|
|
explicit Selector(Vector<CompoundSelector>&&);
|
|
|
|
Vector<CompoundSelector> m_compound_selectors;
|
|
mutable Optional<u32> m_specificity;
|
|
Optional<PseudoElement> m_target_pseudo_element;
|
|
mutable Optional<size_t> m_sibling_invalidation_distance;
|
|
bool m_can_use_fast_matches { false };
|
|
bool m_can_use_ancestor_filter { false };
|
|
bool m_contains_the_nesting_selector { false };
|
|
bool m_contains_pseudo_element_transition { false };
|
|
bool m_contains_slotted_pseudo_element { false };
|
|
bool m_contains_part_pseudo_element { false };
|
|
|
|
PseudoClassBitmap m_contained_pseudo_classes;
|
|
|
|
void collect_ancestor_hashes();
|
|
|
|
Array<u32, 8> m_ancestor_hashes;
|
|
};
|
|
|
|
bool is_legacy_single_colon_pseudo_element(PseudoElement);
|
|
|
|
String serialize_a_group_of_selectors(SelectorList const& selectors);
|
|
|
|
SelectorList adapt_nested_relative_selector_list(SelectorList const&);
|
|
|
|
}
|
|
|
|
namespace AK {
|
|
|
|
template<>
|
|
struct Formatter<Web::CSS::Selector> : Formatter<StringView> {
|
|
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::Selector const& selector)
|
|
{
|
|
return Formatter<StringView>::format(builder, selector.serialize());
|
|
}
|
|
};
|
|
|
|
}
|