LibWeb: Add support for trees of pseudo-elements

This is needed for CSS view transitions.
This commit is contained in:
Psychpsyo 2025-03-25 18:13:18 +01:00 committed by Sam Atkins
commit 31301ef08b
Notes: github-actions[bot] 2025-05-13 11:39:50 +00:00
5 changed files with 88 additions and 25 deletions

View file

@ -169,8 +169,9 @@ Each entry has the following properties:
|----------------------|----------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `alias-for` | No | Nothing | Use to specify that this should be treated as an alias for the named pseudo-element. |
| `function-syntax` | No | Nothing | Syntax for the function arguments if this is a function-type pseudo-element. Copied directly from the spec. |
| `is-generated` | No | `false` | Whether this is a [generated pseudo-element.](https://drafts.csswg.org/css-pseudo-4/#generated-content) |
| `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element.](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element) |
| `is-generated` | No | `false` | Whether this is a [generated pseudo-element](https://drafts.csswg.org/css-pseudo-4/#generated-content). |
| `is-allowed-in-has` | No | `false` | Whether this is a [`:has`-allowed pseudo-element](https://drafts.csswg.org/selectors/#has-allowed-pseudo-element). |
| `is-pseudo-root` | No | `false` | Whether this is a [pseudo-element root](https://drafts.csswg.org/css-view-transitions/#pseudo-element-root). |
| `property-whitelist` | No | Nothing | Some pseudo-elements only permit certain properties. If so, name them in an array here. Some special values are allowed here for categories of properties - see below. |
| `spec` | No | Nothing | Link to the spec definition, for reference. Not used in generated code. |
| `type` | No | `"identifier"` | What type of pseudo-element is this. Either "identifier", "function", or "both". |
@ -181,6 +182,7 @@ The generated code provides:
- `Optional<PseudoElement> aliased_pseudo_element_from_string(StringView)` is similar, but returns the `PseudoElement` this name is an alias for
- `StringView pseudo_element_name(PseudoElement)` to convert a `PseudoElement` back into a string
- `bool is_has_allowed_pseudo_element(PseudoElement)` returns whether the pseudo-element is valid inside `:has()`
- `bool is_pseudo_element_root(PseudoElement)` returns whether the pseudo-element is a [pseudo-element root](https://drafts.csswg.org/css-view-transitions/#pseudo-element-root)
- `bool pseudo_element_supports_property(PseudoElement, PropertyID)` returns whether the property can be applied to this pseudo-element
- A `GeneratedPseudoElement` enum listing only the pseudo-elements that are [generated content](https://drafts.csswg.org/css-pseudo-4/#generated-content)
- `Optional<GeneratedPseudoElement> to_generated_pseudo_element(PseudoElement)` for converting from `PseudoElement` to `GeneratedPseudoElement`. Returns nothing if it's not a generated pseudo-element

View file

@ -114,7 +114,8 @@
"spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-track"
},
"view-transition": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition"
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition",
"is-pseudo-root": true
},
"view-transition-group": {
"spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition-group",

View file

@ -87,6 +87,9 @@
namespace Web::DOM {
GC_DEFINE_ALLOCATOR(Element::PseudoElement);
GC_DEFINE_ALLOCATOR(Element::PseudoElementTreeNode);
Element::Element(Document& document, DOM::QualifiedName qualified_name)
: ParentNode(document, NodeType::ELEMENT_NODE)
, m_qualified_name(move(qualified_name))
@ -117,9 +120,7 @@ void Element::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_computed_properties);
if (m_pseudo_element_data) {
for (auto& pseudo_element : *m_pseudo_element_data) {
visitor.visit(pseudo_element.cascaded_properties);
visitor.visit(pseudo_element.computed_properties);
visitor.visit(pseudo_element.layout_node);
visitor.visit(pseudo_element.value);
}
}
if (m_registered_intersection_observers) {
@ -128,6 +129,15 @@ void Element::visit_edges(Cell::Visitor& visitor)
}
}
void Element::PseudoElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(cascaded_properties);
visitor.visit(computed_properties);
visitor.visit(layout_node);
}
// https://dom.spec.whatwg.org/#dom-element-getattribute
Optional<String> Element::get_attribute(FlyString const& name) const
{
@ -1337,9 +1347,9 @@ bool Element::affected_by_pseudo_class(CSS::PseudoClass pseudo_class) const
}
if (m_pseudo_element_data) {
for (auto& pseudo_element : *m_pseudo_element_data) {
if (!pseudo_element.computed_properties)
if (!pseudo_element.value->computed_properties)
continue;
if (pseudo_element.computed_properties->has_attempted_match_against_pseudo_class(pseudo_class))
if (pseudo_element.value->computed_properties->has_attempted_match_against_pseudo_class(pseudo_class))
return true;
}
}
@ -1506,7 +1516,7 @@ bool Element::has_pseudo_elements() const
{
if (m_pseudo_element_data) {
for (auto& pseudo_element : *m_pseudo_element_data) {
if (pseudo_element.layout_node)
if (pseudo_element.value->layout_node)
return true;
}
}
@ -1517,7 +1527,7 @@ void Element::clear_pseudo_element_nodes(Badge<Layout::TreeBuilder>)
{
if (m_pseudo_element_data) {
for (auto& pseudo_element : *m_pseudo_element_data) {
pseudo_element.layout_node = nullptr;
pseudo_element.value->layout_node = nullptr;
}
}
}
@ -1526,15 +1536,12 @@ void Element::serialize_pseudo_elements_as_json(JsonArraySerializer<StringBuilde
{
if (!m_pseudo_element_data)
return;
for (size_t i = 0; i < m_pseudo_element_data->size(); ++i) {
auto& pseudo_element = (*m_pseudo_element_data)[i].layout_node;
if (!pseudo_element)
continue;
for (auto& pseudo_element : m_pseudo_element_data->keys()) {
auto object = MUST(children_array.add_object());
MUST(object.add("name"sv, MUST(String::formatted("::{}", CSS::pseudo_element_name(static_cast<CSS::PseudoElement>(i))))));
MUST(object.add("name"sv, MUST(String::formatted("::{}", CSS::pseudo_element_name(pseudo_element)))));
MUST(object.add("type"sv, "pseudo-element"));
MUST(object.add("parent-id"sv, unique_id().value()));
MUST(object.add("pseudo-element"sv, i));
MUST(object.add("pseudo-element"sv, to_underlying(pseudo_element)));
MUST(object.finish());
}
}
@ -2857,9 +2864,8 @@ void Element::set_pseudo_element_computed_properties(CSS::PseudoElement pseudo_e
if (!m_pseudo_element_data && !style)
return;
if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(pseudo_element)) {
if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(pseudo_element))
return;
}
ensure_pseudo_element(pseudo_element).computed_properties = style;
}
@ -2881,7 +2887,11 @@ Optional<Element::PseudoElement&> Element::get_pseudo_element(CSS::PseudoElement
return {};
}
return m_pseudo_element_data->at(to_underlying(type));
auto pseudo_element = m_pseudo_element_data->get(type);
if (!pseudo_element.has_value())
return {};
return *(pseudo_element.value());
}
Element::PseudoElement& Element::ensure_pseudo_element(CSS::PseudoElement type) const
@ -2891,7 +2901,15 @@ Element::PseudoElement& Element::ensure_pseudo_element(CSS::PseudoElement type)
VERIFY(CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(type));
return m_pseudo_element_data->at(to_underlying(type));
if (!m_pseudo_element_data->get(type).has_value()) {
if (is_pseudo_element_root(type)) {
m_pseudo_element_data->set(type, heap().allocate<PseudoElementTreeNode>());
} else {
m_pseudo_element_data->set(type, heap().allocate<PseudoElement>());
}
}
return *(m_pseudo_element_data->get(type).value());
}
void Element::set_custom_properties(Optional<CSS::PseudoElement> pseudo_element, HashMap<FlyString, CSS::StyleProperty> custom_properties)

View file

@ -527,15 +527,26 @@ private:
GC::Ptr<CSS::ComputedProperties> m_computed_properties;
HashMap<FlyString, CSS::StyleProperty> m_custom_properties;
struct PseudoElement {
struct PseudoElement : public JS::Cell {
GC_CELL(PseudoElement, JS::Cell);
GC_DECLARE_ALLOCATOR(PseudoElement);
GC::Ptr<Layout::NodeWithStyle> layout_node;
GC::Ptr<CSS::CascadedProperties> cascaded_properties;
GC::Ptr<CSS::ComputedProperties> computed_properties;
HashMap<FlyString, CSS::StyleProperty> custom_properties;
private:
virtual void visit_edges(JS::Cell::Visitor&) override;
};
// TODO: CSS::Selector::PseudoElement includes a lot of pseudo-elements that exist in shadow trees,
// and so we don't want to include data for them here.
using PseudoElementData = Array<PseudoElement, to_underlying(CSS::PseudoElement::KnownPseudoElementCount)>;
// https://drafts.csswg.org/css-view-transitions/#pseudo-element-tree
struct PseudoElementTreeNode
: public PseudoElement
, TreeNode<PseudoElementTreeNode> {
GC_CELL(PseudoElementTreeNode, PseudoElement);
GC_DECLARE_ALLOCATOR(PseudoElementTreeNode);
};
using PseudoElementData = HashMap<CSS::PseudoElement, GC::Ref<PseudoElement>>;
mutable OwnPtr<PseudoElementData> m_pseudo_element_data;
Optional<PseudoElement&> get_pseudo_element(CSS::PseudoElement) const;
PseudoElement& ensure_pseudo_element(CSS::PseudoElement) const;
@ -618,7 +629,10 @@ inline bool Element::has_pseudo_element(CSS::PseudoElement type) const
return false;
if (!CSS::Selector::PseudoElementSelector::is_known_pseudo_element_type(type))
return false;
return m_pseudo_element_data->at(to_underlying(type)).layout_node;
auto pseudo_element = m_pseudo_element_data->get(type);
if (!pseudo_element.has_value())
return false;
return pseudo_element.value()->layout_node;
}
WebIDL::ExceptionOr<QualifiedName> validate_and_extract(JS::Realm&, Optional<FlyString> namespace_, FlyString const& qualified_name);

View file

@ -86,6 +86,7 @@ Optional<PseudoElement> aliased_pseudo_element_from_string(StringView);
StringView pseudo_element_name(PseudoElement);
bool is_has_allowed_pseudo_element(PseudoElement);
bool is_pseudo_element_root(PseudoElement);
bool pseudo_element_supports_property(PseudoElement, PropertyID);
struct PseudoElementMetadata {
@ -234,6 +235,33 @@ bool is_has_allowed_pseudo_element(PseudoElement pseudo_element)
}
}
bool is_pseudo_element_root(PseudoElement pseudo_element)
{
switch (pseudo_element) {
)~~~");
pseudo_elements_data.for_each_member([&](auto& name, JsonValue const& value) {
auto& pseudo_element = value.as_object();
if (pseudo_element.has("alias-for"sv))
return;
if (!pseudo_element.get_bool("is-pseudo-root"sv).value_or(false))
return;
auto member_generator = generator.fork();
member_generator.set("name:titlecase", title_casify(name));
member_generator.append(R"~~~(
case PseudoElement::@name:titlecase@:
return true;
)~~~");
});
generator.append(R"~~~(
default:
return false;
}
}
bool pseudo_element_supports_property(PseudoElement pseudo_element, PropertyID property_id)
{
switch (pseudo_element) {