Skip to main content

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::Image(..) |
41        Image::CrossFade(..) |
42        Image::PaintWorklet(..) |
43        Image::None => None,
44        Image::LightDark(..) => unreachable!("light-dark() should be disabled"),
45    };
46
47    let content = match &marker_info.style.get_counters().content {
48        Content::Items(_) => generate_pseudo_element_content(&marker_info, context),
49        Content::None => return None,
50        Content::Normal => marker_image().or_else(|| {
51            Some(vec![PseudoElementContentItem::Text(marker_string(
52                &list_style.list_style_type,
53            )?)])
54        })?,
55    };
56
57    Some((marker_info, content))
58}
59
60fn symbol_to_string(symbol: &Symbol) -> &str {
61    match symbol {
62        Symbol::String(string) => string,
63        Symbol::Ident(ident) => &ident.0,
64    }
65}
66
67/// <https://drafts.csswg.org/css-counter-styles-3/#generate-a-counter>
68pub(crate) fn generate_counter_representation(counter_style: &CounterStyle) -> &str {
69    // TODO: Most counter styles produce different results depending on the counter value.
70    // Since we don't support counter properties yet, assume a value of 0 for now.
71    match counter_style {
72        CounterStyle::None | CounterStyle::String(_) => unreachable!("Invalid counter style"),
73        CounterStyle::Name(name) => match name.0 {
74            atom!("disc") => "\u{2022}",            /* "•" */
75            atom!("circle") => "\u{25E6}",          /* "◦" */
76            atom!("square") => "\u{25AA}",          /* "▪" */
77            atom!("disclosure-open") => "\u{25BE}", /* "▾" */
78            // TODO: Use U+25C2 "◂" depending on the direction.
79            atom!("disclosure-closed") => "\u{25B8}", /* "▸" */
80            atom!("decimal-leading-zero") => "00",
81            atom!("arabic-indic") => "\u{660}", /* "٠" */
82            atom!("bengali") => "\u{9E6}",      /* "০" */
83            atom!("cambodian") | atom!("khmer") => "\u{17E0}", /* "០" */
84            atom!("devanagari") => "\u{966}",   /* "०" */
85            atom!("gujarati") => "\u{AE6}",     /* "૦" */
86            atom!("gurmukhi") => "\u{A66}",     /* "੦" */
87            atom!("kannada") => "\u{CE6}",      /* "೦" */
88            atom!("lao") => "\u{ED0}",          /* "໐" */
89            atom!("malayalam") => "\u{D66}",    /* "൦" */
90            atom!("mongolian") => "\u{1810}",   /* "᠐" */
91            atom!("myanmar") => "\u{1040}",     /* "၀" */
92            atom!("oriya") => "\u{B66}",        /* "୦" */
93            atom!("persian") => "\u{6F0}",      /* "۰" */
94            atom!("tamil") => "\u{BE6}",        /* "௦" */
95            atom!("telugu") => "\u{C66}",       /* "౦" */
96            atom!("thai") => "\u{E50}",         /* "๐" */
97            atom!("tibetan") => "\u{F20}",      /* "༠" */
98            atom!("cjk-decimal") |
99            atom!("cjk-earthly-branch") |
100            atom!("cjk-heavenly-stem") |
101            atom!("japanese-informal") => "\u{3007}", /* "〇" */
102            atom!("korean-hangul-formal") => "\u{C601}", /* "영" */
103            atom!("korean-hanja-informal") |
104            atom!("korean-hanja-formal") |
105            atom!("japanese-formal") |
106            atom!("simp-chinese-informal") |
107            atom!("simp-chinese-formal") |
108            atom!("trad-chinese-informal") |
109            atom!("trad-chinese-formal") |
110            atom!("cjk-ideographic") => "\u{96F6}", /* "零" */
111            // Fall back to decimal.
112            _ => "0",
113        },
114        CounterStyle::Symbols { ty, symbols } => match ty {
115            // For numeric, use the first symbol, which represents the value 0.
116            SymbolsType::Numeric => {
117                symbol_to_string(symbols.0.first().expect("symbols() should have symbols"))
118            },
119            // For cyclic, the first symbol represents the value 1. However, it loops back,
120            // so the last symbol represents the value 0.
121            SymbolsType::Cyclic => {
122                symbol_to_string(symbols.0.last().expect("symbols() should have symbols"))
123            },
124            // For the others, the first symbol represents the value 1, and 0 is out of range.
125            // Therefore, fall back to `decimal`.
126            SymbolsType::Alphabetic | SymbolsType::Symbolic | SymbolsType::Fixed => "0",
127        },
128    }
129}
130
131/// <https://drafts.csswg.org/css-lists/#marker-string>
132pub(crate) fn marker_string(list_style_type: &ListStyleType) -> Option<String> {
133    let suffix = match &list_style_type.0 {
134        CounterStyle::None => return None,
135        CounterStyle::String(string) => return Some(string.to_string()),
136        CounterStyle::Name(name) => match name.0 {
137            atom!("disc") |
138            atom!("circle") |
139            atom!("square") |
140            atom!("disclosure-open") |
141            atom!("disclosure-closed") => " ",
142            atom!("hiragana") |
143            atom!("hiragana-iroha") |
144            atom!("katakana") |
145            atom!("katakana-iroha") |
146            atom!("cjk-decimal") |
147            atom!("cjk-earthly-branch") |
148            atom!("cjk-heavenly-stem") |
149            atom!("japanese-informal") |
150            atom!("japanese-formal") |
151            atom!("simp-chinese-informal") |
152            atom!("simp-chinese-formal") |
153            atom!("trad-chinese-informal") |
154            atom!("trad-chinese-formal") |
155            atom!("cjk-ideographic") => "\u{3001}", /* "、" */
156            atom!("korean-hangul-formal") |
157            atom!("korean-hanja-informal") |
158            atom!("korean-hanja-formal") => ", ",
159            atom!("ethiopic-numeric") => "/ ",
160            _ => ". ",
161        },
162        CounterStyle::Symbols { .. } => " ",
163    };
164    Some(generate_counter_representation(&list_style_type.0).to_string() + suffix)
165}