layout/
lists.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use style::counter_style::{CounterStyle, Symbol, SymbolsType};
6use style::properties::longhands::list_style_type::computed_value::T as ListStyleType;
7use style::values::computed::Image;
8use style::values::generics::counters::Content;
9use stylo_atoms::atom;
10
11use crate::context::LayoutContext;
12use crate::dom_traversal::{
13    NodeAndStyleInfo, PseudoElementContentItem, generate_pseudo_element_content,
14};
15use crate::replaced::ReplacedContents;
16
17/// <https://drafts.csswg.org/css-lists/#content-property>
18pub(crate) fn make_marker<'dom>(
19    context: &LayoutContext,
20    info: &NodeAndStyleInfo<'dom>,
21) -> Option<(NodeAndStyleInfo<'dom>, Vec<PseudoElementContentItem>)> {
22    let marker_info =
23        info.with_pseudo_element(context, style::selector_parser::PseudoElement::Marker)?;
24    let style = &marker_info.style;
25    let list_style = style.get_list();
26
27    // https://drafts.csswg.org/css-lists/#marker-image
28    let marker_image = || match &list_style.list_style_image {
29        Image::Url(url) => Some(vec![
30            PseudoElementContentItem::Replaced(ReplacedContents::from_image_url(
31                marker_info.node,
32                context,
33                url,
34            )?),
35            PseudoElementContentItem::Text(" ".into()),
36        ]),
37        // XXX: Non-None image types unimplemented.
38        Image::ImageSet(..) |
39        Image::Gradient(..) |
40        Image::CrossFade(..) |
41        Image::PaintWorklet(..) |
42        Image::None => None,
43        Image::LightDark(..) => unreachable!("light-dark() should be disabled"),
44    };
45
46    let content = match &marker_info.style.get_counters().content {
47        Content::Items(_) => generate_pseudo_element_content(&marker_info, context),
48        Content::None => return None,
49        Content::Normal => marker_image().or_else(|| {
50            Some(vec![PseudoElementContentItem::Text(marker_string(
51                &list_style.list_style_type,
52            )?)])
53        })?,
54    };
55
56    Some((marker_info, content))
57}
58
59fn symbol_to_string(symbol: &Symbol) -> &str {
60    match symbol {
61        Symbol::String(string) => string,
62        Symbol::Ident(ident) => &ident.0,
63    }
64}
65
66/// <https://drafts.csswg.org/css-counter-styles-3/#generate-a-counter>
67pub(crate) fn generate_counter_representation(counter_style: &CounterStyle) -> &str {
68    // TODO: Most counter styles produce different results depending on the counter value.
69    // Since we don't support counter properties yet, assume a value of 0 for now.
70    match counter_style {
71        CounterStyle::None | CounterStyle::String(_) => unreachable!("Invalid counter style"),
72        CounterStyle::Name(name) => match name.0 {
73            atom!("disc") => "\u{2022}",            /* "•" */
74            atom!("circle") => "\u{25E6}",          /* "◦" */
75            atom!("square") => "\u{25AA}",          /* "▪" */
76            atom!("disclosure-open") => "\u{25BE}", /* "▾" */
77            // TODO: Use U+25C2 "◂" depending on the direction.
78            atom!("disclosure-closed") => "\u{25B8}", /* "▸" */
79            atom!("decimal-leading-zero") => "00",
80            atom!("arabic-indic") => "\u{660}", /* "٠" */
81            atom!("bengali") => "\u{9E6}",      /* "০" */
82            atom!("cambodian") | atom!("khmer") => "\u{17E0}", /* "០" */
83            atom!("devanagari") => "\u{966}",   /* "०" */
84            atom!("gujarati") => "\u{AE6}",     /* "૦" */
85            atom!("gurmukhi") => "\u{A66}",     /* "੦" */
86            atom!("kannada") => "\u{CE6}",      /* "೦" */
87            atom!("lao") => "\u{ED0}",          /* "໐" */
88            atom!("malayalam") => "\u{D66}",    /* "൦" */
89            atom!("mongolian") => "\u{1810}",   /* "᠐" */
90            atom!("myanmar") => "\u{1040}",     /* "၀" */
91            atom!("oriya") => "\u{B66}",        /* "୦" */
92            atom!("persian") => "\u{6F0}",      /* "۰" */
93            atom!("tamil") => "\u{BE6}",        /* "௦" */
94            atom!("telugu") => "\u{C66}",       /* "౦" */
95            atom!("thai") => "\u{E50}",         /* "๐" */
96            atom!("tibetan") => "\u{F20}",      /* "༠" */
97            atom!("cjk-decimal") |
98            atom!("cjk-earthly-branch") |
99            atom!("cjk-heavenly-stem") |
100            atom!("japanese-informal") => "\u{3007}", /* "〇" */
101            atom!("korean-hangul-formal") => "\u{C601}", /* "영" */
102            atom!("korean-hanja-informal") |
103            atom!("korean-hanja-formal") |
104            atom!("japanese-formal") |
105            atom!("simp-chinese-informal") |
106            atom!("simp-chinese-formal") |
107            atom!("trad-chinese-informal") |
108            atom!("trad-chinese-formal") |
109            atom!("cjk-ideographic") => "\u{96F6}", /* "零" */
110            // Fall back to decimal.
111            _ => "0",
112        },
113        CounterStyle::Symbols { ty, symbols } => match ty {
114            // For numeric, use the first symbol, which represents the value 0.
115            SymbolsType::Numeric => {
116                symbol_to_string(symbols.0.first().expect("symbols() should have symbols"))
117            },
118            // For cyclic, the first symbol represents the value 1. However, it loops back,
119            // so the last symbol represents the value 0.
120            SymbolsType::Cyclic => {
121                symbol_to_string(symbols.0.last().expect("symbols() should have symbols"))
122            },
123            // For the others, the first symbol represents the value 1, and 0 is out of range.
124            // Therefore, fall back to `decimal`.
125            SymbolsType::Alphabetic | SymbolsType::Symbolic | SymbolsType::Fixed => "0",
126        },
127    }
128}
129
130/// <https://drafts.csswg.org/css-lists/#marker-string>
131pub(crate) fn marker_string(list_style_type: &ListStyleType) -> Option<String> {
132    let suffix = match &list_style_type.0 {
133        CounterStyle::None => return None,
134        CounterStyle::String(string) => return Some(string.to_string()),
135        CounterStyle::Name(name) => match name.0 {
136            atom!("disc") |
137            atom!("circle") |
138            atom!("square") |
139            atom!("disclosure-open") |
140            atom!("disclosure-closed") => " ",
141            atom!("hiragana") |
142            atom!("hiragana-iroha") |
143            atom!("katakana") |
144            atom!("katakana-iroha") |
145            atom!("cjk-decimal") |
146            atom!("cjk-earthly-branch") |
147            atom!("cjk-heavenly-stem") |
148            atom!("japanese-informal") |
149            atom!("japanese-formal") |
150            atom!("simp-chinese-informal") |
151            atom!("simp-chinese-formal") |
152            atom!("trad-chinese-informal") |
153            atom!("trad-chinese-formal") |
154            atom!("cjk-ideographic") => "\u{3001}", /* "、" */
155            atom!("korean-hangul-formal") |
156            atom!("korean-hanja-informal") |
157            atom!("korean-hanja-formal") => ", ",
158            atom!("ethiopic-numeric") => "/ ",
159            _ => ". ",
160        },
161        CounterStyle::Symbols { .. } => " ",
162    };
163    Some(generate_counter_representation(&list_style_type.0).to_string() + suffix)
164}