2020-01-18 09:38:21 +01:00
|
|
|
/*
|
2025-04-17 13:39:30 +02:00
|
|
|
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
|
2025-03-20 16:56:46 +00:00
|
|
|
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
|
2020-01-18 09:38:21 +01:00
|
|
|
*
|
2021-04-22 01:24:48 -07:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-01-18 09:38:21 +01:00
|
|
|
*/
|
|
|
|
|
2019-06-20 23:25:25 +02:00
|
|
|
#pragma once
|
|
|
|
|
2023-02-18 15:42:55 +00:00
|
|
|
#include <AK/FlyString.h>
|
2021-07-12 17:30:40 +01:00
|
|
|
#include <AK/RefCounted.h>
|
2023-02-18 15:42:55 +00:00
|
|
|
#include <AK/String.h>
|
2019-06-20 23:25:25 +02:00
|
|
|
#include <AK/Vector.h>
|
2024-08-14 14:06:03 +01:00
|
|
|
#include <LibWeb/CSS/Keyword.h>
|
2024-11-13 15:49:43 +00:00
|
|
|
#include <LibWeb/CSS/Parser/ComponentValue.h>
|
2023-08-11 21:26:04 +01:00
|
|
|
#include <LibWeb/CSS/PseudoClass.h>
|
2025-04-17 13:39:30 +02:00
|
|
|
#include <LibWeb/CSS/PseudoClassBitmap.h>
|
2025-03-19 14:58:22 +00:00
|
|
|
#include <LibWeb/CSS/PseudoElement.h>
|
2019-06-20 23:25:25 +02:00
|
|
|
|
2020-07-26 20:01:35 +02:00
|
|
|
namespace Web::CSS {
|
2020-03-07 10:27:02 +01:00
|
|
|
|
2023-03-06 14:17:01 +01:00
|
|
|
using SelectorList = Vector<NonnullRefPtr<class Selector>>;
|
2021-07-23 15:24:33 +01:00
|
|
|
|
|
|
|
// This is a <complex-selector> in the spec. https://www.w3.org/TR/selectors-4/#complex
|
2021-07-12 17:30:40 +01:00
|
|
|
class Selector : public RefCounted<Selector> {
|
2019-06-20 23:25:25 +02:00
|
|
|
public:
|
2025-03-20 16:56:46 +00:00
|
|
|
class PseudoElementSelector {
|
2023-12-10 21:00:03 +13:00
|
|
|
public:
|
2025-03-24 13:56:24 +00:00
|
|
|
struct PTNameSelector {
|
|
|
|
bool is_universal { false };
|
|
|
|
FlyString value {};
|
|
|
|
};
|
|
|
|
|
2025-07-12 16:15:06 +12:00
|
|
|
using Value = Variant<Empty, PTNameSelector, NonnullRefPtr<Selector>>;
|
2025-03-24 13:56:24 +00:00
|
|
|
|
|
|
|
explicit PseudoElementSelector(PseudoElement type, Value value = {})
|
2023-12-10 21:00:03 +13:00
|
|
|
: m_type(type)
|
2025-03-24 13:56:24 +00:00
|
|
|
, m_value(move(value))
|
2023-12-10 22:06:55 +13:00
|
|
|
{
|
2024-12-19 19:15:02 +01:00
|
|
|
VERIFY(is_known_pseudo_element_type(type));
|
2023-12-10 22:06:55 +13:00
|
|
|
}
|
|
|
|
|
2025-03-24 13:56:24 +00:00
|
|
|
PseudoElementSelector(PseudoElement type, String name, Value value = {})
|
2023-12-10 22:06:55 +13:00
|
|
|
: m_type(type)
|
|
|
|
, m_name(move(name))
|
2025-03-24 13:56:24 +00:00
|
|
|
, m_value(move(value))
|
2023-12-10 21:00:03 +13:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2025-03-20 16:56:46 +00:00
|
|
|
bool operator==(PseudoElementSelector const&) const = default;
|
2023-12-10 21:00:03 +13:00
|
|
|
|
2025-03-20 16:56:46 +00:00
|
|
|
[[nodiscard]] static bool is_known_pseudo_element_type(PseudoElement type)
|
2024-12-19 19:15:02 +01:00
|
|
|
{
|
2025-03-20 16:56:46 +00:00
|
|
|
return to_underlying(type) < to_underlying(PseudoElement::KnownPseudoElementCount);
|
2024-12-19 19:15:02 +01:00
|
|
|
}
|
|
|
|
|
2025-03-24 13:56:24 +00:00
|
|
|
String serialize() const;
|
2023-12-10 21:00:03 +13:00
|
|
|
|
2025-03-20 16:56:46 +00:00
|
|
|
PseudoElement type() const { return m_type; }
|
2023-12-10 21:00:03 +13:00
|
|
|
|
2025-03-24 13:56:24 +00:00
|
|
|
PTNameSelector const& pt_name_selector() const { return m_value.get<PTNameSelector>(); }
|
|
|
|
|
2025-07-12 16:15:06 +12:00
|
|
|
// NOTE: This can't (currently) be a CompoundSelector due to cyclic dependencies.
|
|
|
|
Selector const& compound_selector() const { return m_value.get<NonnullRefPtr<Selector>>(); }
|
|
|
|
|
2023-12-10 21:00:03 +13:00
|
|
|
private:
|
2025-03-20 16:56:46 +00:00
|
|
|
PseudoElement m_type;
|
2023-12-10 22:06:55 +13:00
|
|
|
String m_name;
|
2025-07-12 16:15:06 +12:00
|
|
|
Value m_value;
|
2022-02-24 15:13:20 +00:00
|
|
|
};
|
|
|
|
|
2019-11-27 20:37:36 +01:00
|
|
|
struct SimpleSelector {
|
2024-10-15 12:00:29 +01:00
|
|
|
enum class Type : u8 {
|
2019-11-19 18:22:12 +01:00
|
|
|
Universal,
|
2019-06-29 17:32:32 +02:00
|
|
|
TagName,
|
|
|
|
Id,
|
2019-10-06 09:28:10 +02:00
|
|
|
Class,
|
2021-07-12 14:58:03 +01:00
|
|
|
Attribute,
|
2021-07-12 16:18:00 +01:00
|
|
|
PseudoClass,
|
2021-07-12 16:34:18 +01:00
|
|
|
PseudoElement,
|
2024-10-15 12:00:29 +01:00
|
|
|
Nesting,
|
2024-11-13 15:49:43 +00:00
|
|
|
Invalid,
|
2019-06-29 17:32:32 +02:00
|
|
|
};
|
2019-10-06 09:28:10 +02:00
|
|
|
|
2021-07-24 21:22:44 +01:00
|
|
|
struct ANPlusBPattern {
|
|
|
|
int step_size { 0 }; // "A"
|
|
|
|
int offset = { 0 }; // "B"
|
2021-07-12 16:18:00 +01:00
|
|
|
|
2022-05-07 23:26:58 +02:00
|
|
|
// https://www.w3.org/TR/css-syntax-3/#serializing-anb
|
2023-08-22 12:45:29 +01:00
|
|
|
String serialize() const
|
2021-07-24 21:22:44 +01:00
|
|
|
{
|
2022-05-07 23:26:58 +02:00
|
|
|
// 1. If A is zero, return the serialization of B.
|
|
|
|
if (step_size == 0) {
|
2024-10-14 10:05:01 +02:00
|
|
|
return String::number(offset);
|
2022-05-07 23:26:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 2. Otherwise, let result initially be an empty string.
|
|
|
|
StringBuilder result;
|
|
|
|
|
|
|
|
// 3.
|
|
|
|
// - A is 1: Append "n" to result.
|
|
|
|
if (step_size == 1)
|
2023-08-22 12:45:29 +01:00
|
|
|
result.append('n');
|
2022-05-07 23:26:58 +02:00
|
|
|
// - A is -1: Append "-n" to result.
|
|
|
|
else if (step_size == -1)
|
2023-08-22 12:45:29 +01:00
|
|
|
result.append("-n"sv);
|
2022-05-07 23:26:58 +02:00
|
|
|
// - A is non-zero: Serialize A and append it to result, then append "n" to result.
|
|
|
|
else if (step_size != 0)
|
2023-08-22 12:45:29 +01:00
|
|
|
result.appendff("{}n", step_size);
|
2022-05-07 23:26:58 +02:00
|
|
|
|
|
|
|
// 4.
|
|
|
|
// - B is greater than zero: Append "+" to result, then append the serialization of B to result.
|
|
|
|
if (offset > 0)
|
2023-08-22 12:45:29 +01:00
|
|
|
result.appendff("+{}", offset);
|
2022-05-07 23:26:58 +02:00
|
|
|
// - B is less than zero: Append the serialization of B to result.
|
|
|
|
if (offset < 0)
|
2023-08-22 12:45:29 +01:00
|
|
|
result.appendff("{}", offset);
|
2022-05-07 23:26:58 +02:00
|
|
|
|
|
|
|
// 5. Return result.
|
2023-08-22 12:45:29 +01:00
|
|
|
return MUST(result.to_string());
|
2021-07-24 21:22:44 +01:00
|
|
|
}
|
2021-07-12 16:18:00 +01:00
|
|
|
};
|
|
|
|
|
2023-08-11 21:26:04 +01:00
|
|
|
struct PseudoClassSelector {
|
|
|
|
PseudoClass type;
|
2021-07-12 16:18:00 +01:00
|
|
|
|
2025-08-12 10:28:05 +01:00
|
|
|
// Used for the :nth-*() pseudo-classes, and :heading()
|
|
|
|
Vector<ANPlusBPattern> an_plus_b_patterns {};
|
2021-07-12 16:18:00 +01:00
|
|
|
|
2024-11-14 12:48:14 +00:00
|
|
|
// FIXME: This would make more sense as part of SelectorList but that's currently a `using`
|
|
|
|
bool is_forgiving { false };
|
2022-03-17 15:28:42 +00:00
|
|
|
SelectorList argument_selector_list {};
|
2022-03-20 12:39:11 +01:00
|
|
|
|
|
|
|
// Used for :lang(en-gb,dk)
|
2023-02-18 15:42:55 +00:00
|
|
|
Vector<FlyString> languages {};
|
2023-08-12 18:11:50 +01:00
|
|
|
|
|
|
|
// Used by :dir()
|
2025-05-16 14:57:18 +01:00
|
|
|
struct Ident {
|
|
|
|
Keyword keyword;
|
|
|
|
FlyString string_value;
|
|
|
|
};
|
|
|
|
Optional<Ident> ident {};
|
2019-10-13 21:54:26 +02:00
|
|
|
};
|
2019-11-21 20:07:43 +01:00
|
|
|
|
2023-08-07 16:48:44 +01:00
|
|
|
struct Name {
|
|
|
|
Name(FlyString n)
|
|
|
|
: name(move(n))
|
|
|
|
, lowercase_name(name.to_string().to_lowercase().release_value_but_fixme_should_propagate_errors())
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2021-07-12 14:58:03 +01:00
|
|
|
struct Attribute {
|
|
|
|
enum class MatchType {
|
|
|
|
HasAttribute,
|
|
|
|
ExactValueMatch,
|
|
|
|
ContainsWord, // [att~=val]
|
|
|
|
ContainsString, // [att*=val]
|
|
|
|
StartsWithSegment, // [att|=val]
|
|
|
|
StartsWithString, // [att^=val]
|
|
|
|
EndsWithString, // [att$=val]
|
|
|
|
};
|
2022-03-29 18:01:36 +02:00
|
|
|
enum class CaseType {
|
|
|
|
DefaultMatch,
|
|
|
|
CaseSensitiveMatch,
|
|
|
|
CaseInsensitiveMatch,
|
|
|
|
};
|
2022-03-22 12:58:36 +00:00
|
|
|
MatchType match_type;
|
2023-08-08 16:19:20 +01:00
|
|
|
QualifiedName qualified_name;
|
2023-02-18 15:42:55 +00:00
|
|
|
String value {};
|
2022-03-29 18:01:36 +02:00
|
|
|
CaseType case_type;
|
2019-11-21 20:07:43 +01:00
|
|
|
};
|
2022-03-21 15:43:59 +00:00
|
|
|
|
2024-11-13 15:49:43 +00:00
|
|
|
struct Invalid {
|
|
|
|
Vector<Parser::ComponentValue> component_values;
|
|
|
|
};
|
|
|
|
|
2022-03-22 12:58:36 +00:00
|
|
|
Type type;
|
2025-03-20 16:56:46 +00:00
|
|
|
Variant<Empty, Attribute, PseudoClassSelector, PseudoElementSelector, Name, QualifiedName, Invalid> value {};
|
2022-03-21 15:43:59 +00:00
|
|
|
|
|
|
|
Attribute const& attribute() const { return value.get<Attribute>(); }
|
|
|
|
Attribute& attribute() { return value.get<Attribute>(); }
|
2023-08-11 21:26:04 +01:00
|
|
|
PseudoClassSelector const& pseudo_class() const { return value.get<PseudoClassSelector>(); }
|
|
|
|
PseudoClassSelector& pseudo_class() { return value.get<PseudoClassSelector>(); }
|
2025-03-20 16:56:46 +00:00
|
|
|
PseudoElementSelector const& pseudo_element() const { return value.get<PseudoElementSelector>(); }
|
|
|
|
PseudoElementSelector& pseudo_element() { return value.get<PseudoElementSelector>(); }
|
2022-09-15 13:51:30 +02:00
|
|
|
|
2023-02-18 15:42:55 +00:00
|
|
|
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; }
|
2023-08-08 15:11:48 +01:00
|
|
|
QualifiedName const& qualified_name() const { return value.get<QualifiedName>(); }
|
|
|
|
QualifiedName& qualified_name() { return value.get<QualifiedName>(); }
|
2021-10-15 11:30:01 +01:00
|
|
|
|
2023-08-22 12:45:29 +01:00
|
|
|
String serialize() const;
|
2024-10-17 12:26:37 +01:00
|
|
|
|
2024-11-14 12:48:50 +00:00
|
|
|
Optional<SimpleSelector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
2019-06-20 23:25:25 +02:00
|
|
|
};
|
|
|
|
|
2021-07-23 15:24:33 +01:00
|
|
|
enum class Combinator {
|
|
|
|
None,
|
|
|
|
ImmediateChild, // >
|
|
|
|
Descendant, // <whitespace>
|
|
|
|
NextSibling, // +
|
|
|
|
SubsequentSibling, // ~
|
|
|
|
Column, // ||
|
|
|
|
};
|
2019-11-27 20:37:36 +01:00
|
|
|
|
2021-07-23 15:24:33 +01:00
|
|
|
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 };
|
|
|
|
Vector<SimpleSelector> simple_selectors;
|
2024-10-17 12:26:37 +01:00
|
|
|
|
2024-11-14 12:48:50 +00:00
|
|
|
Optional<CompoundSelector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
2019-11-27 20:37:36 +01:00
|
|
|
};
|
|
|
|
|
2021-07-23 15:24:33 +01:00
|
|
|
static NonnullRefPtr<Selector> create(Vector<CompoundSelector>&& compound_selectors)
|
2021-07-12 17:30:40 +01:00
|
|
|
{
|
2021-07-23 15:24:33 +01:00
|
|
|
return adopt_ref(*new Selector(move(compound_selectors)));
|
2021-07-12 17:30:40 +01:00
|
|
|
}
|
|
|
|
|
2022-03-14 13:21:51 -06:00
|
|
|
~Selector() = default;
|
2019-06-22 09:27:39 +02:00
|
|
|
|
2021-07-23 15:24:33 +01:00
|
|
|
Vector<CompoundSelector> const& compound_selectors() const { return m_compound_selectors; }
|
2025-03-20 16:56:46 +00:00
|
|
|
Optional<PseudoElementSelector> const& pseudo_element() const { return m_pseudo_element; }
|
2024-10-17 12:26:37 +01:00
|
|
|
NonnullRefPtr<Selector> relative_to(SimpleSelector const&) const;
|
|
|
|
bool contains_the_nesting_selector() const { return m_contains_the_nesting_selector; }
|
2025-04-17 13:39:30 +02:00
|
|
|
bool contains_pseudo_class(PseudoClass pseudo_class) const { return m_contained_pseudo_classes.get(pseudo_class); }
|
2025-03-14 14:37:24 +00:00
|
|
|
bool contains_unknown_webkit_pseudo_element() const;
|
2024-11-14 12:48:50 +00:00
|
|
|
RefPtr<Selector> absolutized(SimpleSelector const& selector_for_nesting) const;
|
2020-06-25 16:43:49 +02:00
|
|
|
u32 specificity() const;
|
2023-08-22 12:45:29 +01:00
|
|
|
String serialize() const;
|
2019-06-29 17:32:32 +02:00
|
|
|
|
LibWeb: Use an ancestor filter to quickly reject many CSS selectors
Given a selector like `.foo .bar #baz`, we know that elements with
the class names `foo` and `bar` must be present in the ancestor chain of
the candidate element, or the selector cannot match.
By keeping track of the current ancestor chain during style computation,
and which strings are used in tag names and attribute names, we can do
a quick check before evaluating the selector itself, to see if all the
required ancestors are present.
The way this works:
1. CSS::Selector now has a cache of up to 8 strings that must be present
in the ancestor chain of a matching element. Note that we actually
store string *hashes*, not the strings themselves.
2. When Document performs a recursive style update, we now push and pop
elements to the ancestor chain stack as they are entered and exited.
3. When entering/exiting an ancestor, StyleComputer collects all the
relevant string hashes from that ancestor element and updates a
counting bloom filter.
4. Before evaluating a selector, we first check if any of the hashes
required by the selector are definitely missing from the ancestor
filter. If so, it cannot be a match, and we reject it immediately.
5. Otherwise, we carry on and evaluate the selector as usual.
I originally tried doing this with a HashMap, but we ended up losing
a huge chunk of the time saved to HashMap instead. As it turns out,
a simple counting bloom filter is way better at handling this.
The cost is a flat 8KB per StyleComputer, and since it's a bloom filter,
false positives are a thing.
This is extremely efficient, and allows us to quickly reject the
majority of selectors on many huge websites.
Some example rejection rates:
- https://amazon.com: 77%
- https://github.com/SerenityOS/serenity: 61%
- https://nytimes.com: 57%
- https://store.steampowered.com: 55%
- https://en.wikipedia.org: 45%
- https://youtube.com: 32%
- https://shopify.com: 25%
This also yields a chunky 37% speedup on StyleBench. :^)
2024-03-22 13:50:33 +01:00
|
|
|
auto const& ancestor_hashes() const { return m_ancestor_hashes; }
|
|
|
|
|
2025-02-02 20:35:29 +01:00
|
|
|
bool can_use_fast_matches() const { return m_can_use_fast_matches; }
|
2025-02-20 16:25:29 +01:00
|
|
|
bool can_use_ancestor_filter() const { return m_can_use_ancestor_filter; }
|
2025-02-02 20:35:29 +01:00
|
|
|
|
2025-03-10 16:16:08 +01:00
|
|
|
size_t sibling_invalidation_distance() const;
|
|
|
|
|
2019-06-20 23:25:25 +02:00
|
|
|
private:
|
2021-07-23 15:24:33 +01:00
|
|
|
explicit Selector(Vector<CompoundSelector>&&);
|
2021-07-12 17:30:40 +01:00
|
|
|
|
2021-07-23 15:24:33 +01:00
|
|
|
Vector<CompoundSelector> m_compound_selectors;
|
2022-02-05 17:40:18 +02:00
|
|
|
mutable Optional<u32> m_specificity;
|
2025-03-20 16:56:46 +00:00
|
|
|
Optional<Selector::PseudoElementSelector> m_pseudo_element;
|
2025-03-10 16:16:08 +01:00
|
|
|
mutable Optional<size_t> m_sibling_invalidation_distance;
|
2025-02-02 20:35:29 +01:00
|
|
|
bool m_can_use_fast_matches { false };
|
2025-02-20 16:25:29 +01:00
|
|
|
bool m_can_use_ancestor_filter { false };
|
2024-10-17 12:26:37 +01:00
|
|
|
bool m_contains_the_nesting_selector { false };
|
2025-04-17 13:39:30 +02:00
|
|
|
|
|
|
|
PseudoClassBitmap m_contained_pseudo_classes;
|
LibWeb: Use an ancestor filter to quickly reject many CSS selectors
Given a selector like `.foo .bar #baz`, we know that elements with
the class names `foo` and `bar` must be present in the ancestor chain of
the candidate element, or the selector cannot match.
By keeping track of the current ancestor chain during style computation,
and which strings are used in tag names and attribute names, we can do
a quick check before evaluating the selector itself, to see if all the
required ancestors are present.
The way this works:
1. CSS::Selector now has a cache of up to 8 strings that must be present
in the ancestor chain of a matching element. Note that we actually
store string *hashes*, not the strings themselves.
2. When Document performs a recursive style update, we now push and pop
elements to the ancestor chain stack as they are entered and exited.
3. When entering/exiting an ancestor, StyleComputer collects all the
relevant string hashes from that ancestor element and updates a
counting bloom filter.
4. Before evaluating a selector, we first check if any of the hashes
required by the selector are definitely missing from the ancestor
filter. If so, it cannot be a match, and we reject it immediately.
5. Otherwise, we carry on and evaluate the selector as usual.
I originally tried doing this with a HashMap, but we ended up losing
a huge chunk of the time saved to HashMap instead. As it turns out,
a simple counting bloom filter is way better at handling this.
The cost is a flat 8KB per StyleComputer, and since it's a bloom filter,
false positives are a thing.
This is extremely efficient, and allows us to quickly reject the
majority of selectors on many huge websites.
Some example rejection rates:
- https://amazon.com: 77%
- https://github.com/SerenityOS/serenity: 61%
- https://nytimes.com: 57%
- https://store.steampowered.com: 55%
- https://en.wikipedia.org: 45%
- https://youtube.com: 32%
- https://shopify.com: 25%
This also yields a chunky 37% speedup on StyleBench. :^)
2024-03-22 13:50:33 +01:00
|
|
|
|
|
|
|
void collect_ancestor_hashes();
|
|
|
|
|
|
|
|
Array<u32, 8> m_ancestor_hashes;
|
2019-06-20 23:25:25 +02:00
|
|
|
};
|
2020-03-07 10:27:02 +01:00
|
|
|
|
2024-10-17 12:01:13 +01:00
|
|
|
String serialize_a_group_of_selectors(SelectorList const& selectors);
|
2021-10-15 11:30:01 +01:00
|
|
|
|
2024-11-08 17:50:38 +00:00
|
|
|
SelectorList adapt_nested_relative_selector_list(SelectorList const&);
|
|
|
|
|
2020-03-07 10:27:02 +01:00
|
|
|
}
|
2021-10-15 11:57:13 +01:00
|
|
|
|
|
|
|
namespace AK {
|
|
|
|
|
|
|
|
template<>
|
|
|
|
struct Formatter<Web::CSS::Selector> : Formatter<StringView> {
|
2021-11-16 01:15:21 +01:00
|
|
|
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::Selector const& selector)
|
2021-10-15 11:57:13 +01:00
|
|
|
{
|
2023-08-22 12:45:29 +01:00
|
|
|
return Formatter<StringView>::format(builder, selector.serialize());
|
2021-10-15 11:57:13 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|