Files
servo/components/layout/lists.rs
niya d29ef8dcd0 layout: Display generated content for ::marker (#43515)
Implements a fix for #43036

- Renders content for `::marker`
- If content is not present render marker_image/marker_string

Here's a small webpage I wrote to test if my implementation works :) 

<img width="1136" height="880" alt="Screenshot 2026-03-21 at 4 50 43 PM"
src="https://github.com/user-attachments/assets/91ccbfd9-2c18-446d-bac5-d559f483b08c"
/>

# Testing
The existing WPT ones should be sufficient, the tests need to be
updated.


## (WPT) Stable Unexpected Results that are failing:

| Test  | Issue  | Remark |
|--------|--------|--------|
| /css/css-lists/marker-dynamic-content-change.html | #43120 | |
| /css/css-lists/marker-quotes.html | #30365 | Test File: Ordered List
showing '0' for all `li` |
| /css/css-pseudo/marker-content-001.html | #30365 | Ref file: Ordered
List showing '0' for all `li` |
| /css/css-pseudo/marker-content-001b.html | #30365 | Ref file: Ordered
List showing '0' for all `li` |
| /css/css-pseudo/marker-content-001c.html | #30365 | Ref file: Ordered
List showing '0' for all `li` |
| /css/css-pseudo/marker-content-020.html | | Test File: JavaScript
dynamically toggles the `no-marker` class, but it doesn't update the DOM
when called from inside `addEventListener()` and
`requestAnimationFrame()` functions. |
| /css/selectors/has-style-sharing-pseudo-007.html | #25133 | Test file
uses `has` to remove `content` from a `li`; `has` pseudo-class has not
been implemented yet |
| /css/selectors/has-style-sharing-pseudo-008.html | #25133 | Test file
uses `has` to remove `content` from a `li`; `has` pseudo-class has not
been implemented yet |

---------

Signed-off-by: Niya Gupta <niyabits@gmail.com>
Signed-off-by: niya <niyabits@gmail.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
2026-03-25 07:24:05 +00:00

165 lines
7.1 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use style::counter_style::{CounterStyle, Symbol, SymbolsType};
use style::properties::longhands::list_style_type::computed_value::T as ListStyleType;
use style::values::computed::Image;
use style::values::generics::counters::Content;
use stylo_atoms::atom;
use crate::context::LayoutContext;
use crate::dom_traversal::{
NodeAndStyleInfo, PseudoElementContentItem, generate_pseudo_element_content,
};
use crate::replaced::ReplacedContents;
/// <https://drafts.csswg.org/css-lists/#content-property>
pub(crate) fn make_marker<'dom>(
context: &LayoutContext,
info: &NodeAndStyleInfo<'dom>,
) -> Option<(NodeAndStyleInfo<'dom>, Vec<PseudoElementContentItem>)> {
let marker_info =
info.with_pseudo_element(context, style::selector_parser::PseudoElement::Marker)?;
let style = &marker_info.style;
let list_style = style.get_list();
// https://drafts.csswg.org/css-lists/#marker-image
let marker_image = || match &list_style.list_style_image {
Image::Url(url) => Some(vec![
PseudoElementContentItem::Replaced(ReplacedContents::from_image_url(
marker_info.node,
context,
url,
)?),
PseudoElementContentItem::Text(" ".into()),
]),
// XXX: Non-None image types unimplemented.
Image::ImageSet(..) |
Image::Gradient(..) |
Image::CrossFade(..) |
Image::PaintWorklet(..) |
Image::None => None,
Image::LightDark(..) => unreachable!("light-dark() should be disabled"),
};
let content = match &marker_info.style.get_counters().content {
Content::Items(_) => generate_pseudo_element_content(&marker_info, context),
Content::None => return None,
Content::Normal => marker_image().or_else(|| {
Some(vec![PseudoElementContentItem::Text(marker_string(
&list_style.list_style_type,
)?)])
})?,
};
Some((marker_info, content))
}
fn symbol_to_string(symbol: &Symbol) -> &str {
match symbol {
Symbol::String(string) => string,
Symbol::Ident(ident) => &ident.0,
}
}
/// <https://drafts.csswg.org/css-counter-styles-3/#generate-a-counter>
pub(crate) fn generate_counter_representation(counter_style: &CounterStyle) -> &str {
// TODO: Most counter styles produce different results depending on the counter value.
// Since we don't support counter properties yet, assume a value of 0 for now.
match counter_style {
CounterStyle::None | CounterStyle::String(_) => unreachable!("Invalid counter style"),
CounterStyle::Name(name) => match name.0 {
atom!("disc") => "\u{2022}", /* "•" */
atom!("circle") => "\u{25E6}", /* "◦" */
atom!("square") => "\u{25AA}", /* "▪" */
atom!("disclosure-open") => "\u{25BE}", /* "▾" */
// TODO: Use U+25C2 "◂" depending on the direction.
atom!("disclosure-closed") => "\u{25B8}", /* "▸" */
atom!("decimal-leading-zero") => "00",
atom!("arabic-indic") => "\u{660}", /* "٠" */
atom!("bengali") => "\u{9E6}", /* "" */
atom!("cambodian") | atom!("khmer") => "\u{17E0}", /* "០" */
atom!("devanagari") => "\u{966}", /* "" */
atom!("gujarati") => "\u{AE6}", /* "" */
atom!("gurmukhi") => "\u{A66}", /* "" */
atom!("kannada") => "\u{CE6}", /* "" */
atom!("lao") => "\u{ED0}", /* "" */
atom!("malayalam") => "\u{D66}", /* "" */
atom!("mongolian") => "\u{1810}", /* "᠐" */
atom!("myanmar") => "\u{1040}", /* "" */
atom!("oriya") => "\u{B66}", /* "" */
atom!("persian") => "\u{6F0}", /* "۰" */
atom!("tamil") => "\u{BE6}", /* "" */
atom!("telugu") => "\u{C66}", /* "" */
atom!("thai") => "\u{E50}", /* "" */
atom!("tibetan") => "\u{F20}", /* "༠" */
atom!("cjk-decimal") |
atom!("cjk-earthly-branch") |
atom!("cjk-heavenly-stem") |
atom!("japanese-informal") => "\u{3007}", /* "" */
atom!("korean-hangul-formal") => "\u{C601}", /* "영" */
atom!("korean-hanja-informal") |
atom!("korean-hanja-formal") |
atom!("japanese-formal") |
atom!("simp-chinese-informal") |
atom!("simp-chinese-formal") |
atom!("trad-chinese-informal") |
atom!("trad-chinese-formal") |
atom!("cjk-ideographic") => "\u{96F6}", /* "零" */
// Fall back to decimal.
_ => "0",
},
CounterStyle::Symbols { ty, symbols } => match ty {
// For numeric, use the first symbol, which represents the value 0.
SymbolsType::Numeric => {
symbol_to_string(symbols.0.first().expect("symbols() should have symbols"))
},
// For cyclic, the first symbol represents the value 1. However, it loops back,
// so the last symbol represents the value 0.
SymbolsType::Cyclic => {
symbol_to_string(symbols.0.last().expect("symbols() should have symbols"))
},
// For the others, the first symbol represents the value 1, and 0 is out of range.
// Therefore, fall back to `decimal`.
SymbolsType::Alphabetic | SymbolsType::Symbolic | SymbolsType::Fixed => "0",
},
}
}
/// <https://drafts.csswg.org/css-lists/#marker-string>
pub(crate) fn marker_string(list_style_type: &ListStyleType) -> Option<String> {
let suffix = match &list_style_type.0 {
CounterStyle::None => return None,
CounterStyle::String(string) => return Some(string.to_string()),
CounterStyle::Name(name) => match name.0 {
atom!("disc") |
atom!("circle") |
atom!("square") |
atom!("disclosure-open") |
atom!("disclosure-closed") => " ",
atom!("hiragana") |
atom!("hiragana-iroha") |
atom!("katakana") |
atom!("katakana-iroha") |
atom!("cjk-decimal") |
atom!("cjk-earthly-branch") |
atom!("cjk-heavenly-stem") |
atom!("japanese-informal") |
atom!("japanese-formal") |
atom!("simp-chinese-informal") |
atom!("simp-chinese-formal") |
atom!("trad-chinese-informal") |
atom!("trad-chinese-formal") |
atom!("cjk-ideographic") => "\u{3001}", /* "、" */
atom!("korean-hangul-formal") |
atom!("korean-hanja-informal") |
atom!("korean-hanja-formal") => ", ",
atom!("ethiopic-numeric") => "/ ",
_ => ". ",
},
CounterStyle::Symbols { .. } => " ",
};
Some(generate_counter_representation(&list_style_type.0).to_string() + suffix)
}