LibWeb/CSS: Add support for content to the ::marker pseudo-element

A ::marker pseudo-element is created for list item nodes (nodes
with display:list-item).

Before:
    - The content of the ::marker element is created magically from
    the value of the ordinal (for <ol>) or from a template (for <ul>).
    The style `content` is ignored for ::marker pseudo-elements.

After:
    - If a "list item node" has CSS `content` specified for its ::marker
    pseudo-element, use this to layout the pseudo-element,
    https://drafts.csswg.org/css-lists-3/#content-property
    - Otherwise, layout the list item node as before.
This commit is contained in:
Manuel Zahariev 2025-08-13 17:41:04 -07:00 committed by Sam Atkins
commit 9d77221c4d
Notes: github-actions[bot] 2025-10-10 11:03:28 +00:00
2 changed files with 56 additions and 19 deletions

View file

@ -7,6 +7,8 @@
#include <AK/TemporaryChange.h> #include <AK/TemporaryChange.h>
#include <LibWeb/CSS/Length.h> #include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h> #include <LibWeb/DOM/Node.h>
#include <LibWeb/Dump.h> #include <LibWeb/Dump.h>
#include <LibWeb/HTML/BrowsingContext.h> #include <LibWeb/HTML/BrowsingContext.h>
@ -804,11 +806,17 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
resolve_used_height_if_treated_as_auto(box, available_space_for_height_resolution); resolve_used_height_if_treated_as_auto(box, available_space_for_height_resolution);
} }
// This monster basically means: "a ListItemBox that does not have specified content in the ::marker pseudo-element".
// This happens for ::marker with content 'normal'.
// FIXME: We currently so not support ListItemBox-es generated by pseudo-elements. We will need to, eventually.
ListItemBox const* li_box = as_if<ListItemBox>(box);
bool is_list_item_box_without_css_content = li_box && (!(box.dom_node() && box.dom_node()->is_element() && as_if<DOM::Element const>(box.dom_node())->computed_properties(CSS::PseudoElement::Marker)->property(CSS::PropertyID::Content).is_content()));
// Before we insert the children of a list item we need to know the location of the marker. // Before we insert the children of a list item we need to know the location of the marker.
// If we do not do this then left-floating elements inside the list item will push the marker to the right, // If we do not do this then left-floating elements inside the list item will push the marker to the right,
// in some cases even causing it to overlap with the non-floating content of the list. // in some cases even causing it to overlap with the non-floating content of the list.
CSSPixels left_space_before_children_formatted; CSSPixels left_space_before_children_formatted;
if (auto const* li_box = as_if<ListItemBox>(box)) { if (is_list_item_box_without_css_content) {
// We need to ensure that our height and width are final before we calculate our left offset. // We need to ensure that our height and width are final before we calculate our left offset.
// Otherwise, the y at which we calculate the intrusion by floats might be incorrect. // Otherwise, the y at which we calculate the intrusion by floats might be incorrect.
ensure_sizes_correct_for_left_offset_calculation(*li_box); ensure_sizes_correct_for_left_offset_calculation(*li_box);
@ -868,8 +876,10 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
compute_inset(box, content_box_rect(block_container_state).size()); compute_inset(box, content_box_rect(block_container_state).size());
// Now that our children are formatted we place the ListItemBox with the left space we remembered. // Now that our children are formatted we place the ListItemBox with the left space we remembered.
if (auto const* li_box = as_if<ListItemBox>(box)) if (is_list_item_box_without_css_content)
// The marker pseudo-element will be created from a ListItemMarkerBox
layout_list_item_marker(*li_box, left_space_before_children_formatted); layout_list_item_marker(*li_box, left_space_before_children_formatted);
// Otherwise, it will be a dealt with as a generic pseudo-element with the content of the ::marker pseudo-element.
bottom_of_lowest_margin_box = max(bottom_of_lowest_margin_box, box_state.offset.y() + box_state.content_height() + box_state.margin_box_bottom()); bottom_of_lowest_margin_box = max(bottom_of_lowest_margin_box, box_state.offset.y() + box_state.content_height() + box_state.margin_box_bottom());

View file

@ -4,6 +4,7 @@
* Copyright (c) 2022, MacDue <macdue@dueutil.tech> * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org> * Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2025, Aziz B. Yesilyurt <abyesilyurt@gmail.com> * Copyright (c) 2025, Aziz B. Yesilyurt <abyesilyurt@gmail.com>
* Copyright (c) 2025, Manuel Zahariev <manuel@duck.com>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -11,6 +12,9 @@
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/TemporaryChange.h> #include <AK/TemporaryChange.h>
#include <LibWeb/CSS/ComputedProperties.h> #include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/ComputedValues.h>
#include <LibWeb/CSS/Enums.h>
#include <LibWeb/CSS/PseudoElement.h>
#include <LibWeb/CSS/StyleComputer.h> #include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/DisplayStyleValue.h> #include <LibWeb/CSS/StyleValues/DisplayStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h> #include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
@ -269,6 +273,8 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Ps
auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_style->content(element_reference, initial_quote_nesting_level); auto [pseudo_element_content, final_quote_nesting_level] = pseudo_element_style->content(element_reference, initial_quote_nesting_level);
m_quote_nesting_level = final_quote_nesting_level; m_quote_nesting_level = final_quote_nesting_level;
auto pseudo_element_display = pseudo_element_style->display(); auto pseudo_element_display = pseudo_element_style->display();
Optional<String> content_from_counter_style;
// ::before and ::after only exist if they have content. `content: normal` computes to `none` for them. // ::before and ::after only exist if they have content. `content: normal` computes to `none` for them.
// We also don't create them if they are `display: none`. // We also don't create them if they are `display: none`.
if (first_is_one_of(pseudo_element, CSS::PseudoElement::Before, CSS::PseudoElement::After) if (first_is_one_of(pseudo_element, CSS::PseudoElement::Before, CSS::PseudoElement::After)
@ -277,14 +283,39 @@ void TreeBuilder::create_pseudo_element_if_needed(DOM::Element& element, CSS::Ps
|| pseudo_element_content.type == CSS::ContentData::Type::None)) || pseudo_element_content.type == CSS::ContentData::Type::None))
return; return;
// For ::marker with content or display 'none' -- do nothing.
if (pseudo_element == CSS::PseudoElement::Marker
&& (pseudo_element_display.is_none() || pseudo_element_content.type == CSS::ContentData::Type::None))
return;
// For ::marker with content 'normal', create the marker pseudo-element from a ListItemMarkerBox
// FIXME: This + ListItemBox + ListItemMarkerBox will disappear once ::marker pseudo-elements with 'normal' content
// are rendered using the special list-item counter.
// See: https://github.com/LadybirdBrowser/ladybird/issues/4782
if (pseudo_element == CSS::PseudoElement::Marker && pseudo_element_content.type == CSS::ContentData::Type::Normal)
if (auto* list_box = as_if<ListItemBox>(*element.layout_node())) {
auto list_item_marker = document.heap().allocate<ListItemMarkerBox>(
document,
list_box->computed_values().list_style_type(),
list_box->computed_values().list_style_position(),
element,
*pseudo_element_style);
list_box->set_marker(list_item_marker);
element.set_computed_properties(CSS::PseudoElement::Marker, pseudo_element_style);
element.set_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker);
list_box->prepend_child(*list_item_marker);
return;
}
auto pseudo_element_node = DOM::Element::create_layout_node_for_display_type(document, pseudo_element_display, *pseudo_element_style, nullptr); auto pseudo_element_node = DOM::Element::create_layout_node_for_display_type(document, pseudo_element_display, *pseudo_element_style, nullptr);
if (!pseudo_element_node) if (!pseudo_element_node)
return; return;
auto& style_computer = document.style_computer();
// FIXME: This code actually computes style for element::marker, and shouldn't for element::pseudo::marker // FIXME: This code actually computes style for element::marker, and shouldn't for element::pseudo::marker
if (is<ListItemBox>(*pseudo_element_node)) { if (is<ListItemBox>(*pseudo_element_node)) {
auto& style_computer = document.style_computer();
auto marker_style = style_computer.compute_style({ element, CSS::PseudoElement::Marker }); auto marker_style = style_computer.compute_style({ element, CSS::PseudoElement::Marker });
auto list_item_marker = document.heap().allocate<ListItemMarkerBox>( auto list_item_marker = document.heap().allocate<ListItemMarkerBox>(
document, document,
@ -777,27 +808,13 @@ void TreeBuilder::update_layout_tree_before_children(DOM::Node& dom_node, GC::Re
auto& element = static_cast<DOM::Element&>(dom_node); auto& element = static_cast<DOM::Element&>(dom_node);
push_parent(as<NodeWithStyle>(*layout_node)); push_parent(as<NodeWithStyle>(*layout_node));
create_pseudo_element_if_needed(element, CSS::PseudoElement::Before, AppendOrPrepend::Prepend); create_pseudo_element_if_needed(element, CSS::PseudoElement::Before, AppendOrPrepend::Prepend);
pop_parent(); pop_parent();
} }
} }
void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node, TreeBuilder::Context& context, bool element_has_content_visibility_hidden) void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref<Layout::Node> layout_node, TreeBuilder::Context& context, bool element_has_content_visibility_hidden)
{ {
auto& document = dom_node.document();
auto& style_computer = document.style_computer();
if (is<ListItemBox>(*layout_node)) {
auto& element = static_cast<DOM::Element&>(dom_node);
DOM::AbstractElement list_marker_pseudo { element, CSS::PseudoElement::Marker };
auto marker_style = style_computer.compute_style(list_marker_pseudo);
auto list_item_marker = document.heap().allocate<ListItemMarkerBox>(document, layout_node->computed_values().list_style_type(), layout_node->computed_values().list_style_position(), element, marker_style);
static_cast<ListItemBox&>(*layout_node).set_marker(list_item_marker);
element.set_computed_properties(CSS::PseudoElement::Marker, marker_style);
element.set_pseudo_element_node({}, CSS::PseudoElement::Marker, list_item_marker);
layout_node->prepend_child(*list_item_marker);
CSS::resolve_counters(list_marker_pseudo);
}
if (is<SVG::SVGGraphicsElement>(dom_node)) { if (is<SVG::SVGGraphicsElement>(dom_node)) {
auto& graphics_element = static_cast<SVG::SVGGraphicsElement&>(dom_node); auto& graphics_element = static_cast<SVG::SVGGraphicsElement&>(dom_node);
// Create the layout tree for the SVG mask/clip paths as a child of the masked element. // Create the layout tree for the SVG mask/clip paths as a child of the masked element.
@ -820,6 +837,16 @@ void TreeBuilder::update_layout_tree_after_children(DOM::Node& dom_node, GC::Ref
if (is<DOM::Element>(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) { if (is<DOM::Element>(dom_node) && layout_node->can_have_children() && !element_has_content_visibility_hidden) {
auto& element = static_cast<DOM::Element&>(dom_node); auto& element = static_cast<DOM::Element&>(dom_node);
push_parent(as<NodeWithStyle>(*layout_node)); push_parent(as<NodeWithStyle>(*layout_node));
// https://drafts.csswg.org/css-lists-3/#marker-pseudo
// The marker box is generated by the ::marker pseudo-element of a list item as the list items first child,
// before the ::before pseudo-element (if it exists on the element). It is filled with content as defined
// in §3.2 Generating Marker Contents.
// NOTE: This happens in update_layout_tree_after_children (and not in ..._before_...), since potential
// block container wrapper children are created after update_layout_tree_before_children.
if (layout_node->is_list_item_box())
create_pseudo_element_if_needed(element, CSS::PseudoElement::Marker, AppendOrPrepend::Prepend);
create_pseudo_element_if_needed(element, CSS::PseudoElement::After, AppendOrPrepend::Append); create_pseudo_element_if_needed(element, CSS::PseudoElement::After, AppendOrPrepend::Append);
pop_parent(); pop_parent();
} }