style/values/specified/
counters.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
5//! Specified types for counter properties.
6
7use crate::counter_style::CounterStyle;
8use crate::parser::{Parse, ParserContext};
9use crate::values::generics::counters as generics;
10use crate::values::generics::counters::CounterPair;
11use crate::values::specified::image::Image;
12use crate::values::specified::Attr;
13use crate::values::specified::Integer;
14use crate::values::CustomIdent;
15use cssparser::{match_ignore_ascii_case, Parser, Token};
16use selectors::parser::SelectorParseErrorKind;
17use style_traits::{ParseError, StyleParseErrorKind};
18
19#[derive(PartialEq)]
20enum CounterType {
21    Increment,
22    Set,
23    Reset,
24}
25
26impl CounterType {
27    fn default_value(&self) -> i32 {
28        match *self {
29            Self::Increment => 1,
30            Self::Reset | Self::Set => 0,
31        }
32    }
33}
34
35/// A specified value for the `counter-increment` property.
36pub type CounterIncrement = generics::GenericCounterIncrement<Integer>;
37
38impl Parse for CounterIncrement {
39    fn parse<'i, 't>(
40        context: &ParserContext,
41        input: &mut Parser<'i, 't>,
42    ) -> Result<Self, ParseError<'i>> {
43        Ok(Self::new(parse_counters(
44            context,
45            input,
46            CounterType::Increment,
47        )?))
48    }
49}
50
51/// A specified value for the `counter-set` property.
52pub type CounterSet = generics::GenericCounterSet<Integer>;
53
54impl Parse for CounterSet {
55    fn parse<'i, 't>(
56        context: &ParserContext,
57        input: &mut Parser<'i, 't>,
58    ) -> Result<Self, ParseError<'i>> {
59        Ok(Self::new(parse_counters(context, input, CounterType::Set)?))
60    }
61}
62
63/// A specified value for the `counter-reset` property.
64pub type CounterReset = generics::GenericCounterReset<Integer>;
65
66impl Parse for CounterReset {
67    fn parse<'i, 't>(
68        context: &ParserContext,
69        input: &mut Parser<'i, 't>,
70    ) -> Result<Self, ParseError<'i>> {
71        Ok(Self::new(parse_counters(
72            context,
73            input,
74            CounterType::Reset,
75        )?))
76    }
77}
78
79fn parse_counters<'i, 't>(
80    context: &ParserContext,
81    input: &mut Parser<'i, 't>,
82    counter_type: CounterType,
83) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> {
84    if input
85        .try_parse(|input| input.expect_ident_matching("none"))
86        .is_ok()
87    {
88        return Ok(vec![]);
89    }
90
91    let mut counters = Vec::new();
92    loop {
93        let location = input.current_source_location();
94        let (name, is_reversed) = match input.next() {
95            Ok(&Token::Ident(ref ident)) => {
96                (CustomIdent::from_ident(location, ident, &["none"])?, false)
97            },
98            Ok(&Token::Function(ref name))
99                if counter_type == CounterType::Reset && name.eq_ignore_ascii_case("reversed") =>
100            {
101                input
102                    .parse_nested_block(|input| Ok((CustomIdent::parse(input, &["none"])?, true)))?
103            },
104            Ok(t) => {
105                let t = t.clone();
106                return Err(location.new_unexpected_token_error(t));
107            },
108            Err(_) => break,
109        };
110
111        let value = match input.try_parse(|input| Integer::parse(context, input)) {
112            Ok(start) => {
113                if start.value() == i32::min_value() {
114                    // The spec says that values must be clamped to the valid range,
115                    // and we reserve i32::min_value() as an internal magic value.
116                    // https://drafts.csswg.org/css-lists/#auto-numbering
117                    Integer::new(i32::min_value() + 1)
118                } else {
119                    start
120                }
121            },
122            _ => Integer::new(if is_reversed {
123                i32::min_value()
124            } else {
125                counter_type.default_value()
126            }),
127        };
128        counters.push(CounterPair {
129            name,
130            value,
131            is_reversed,
132        });
133    }
134
135    if !counters.is_empty() {
136        Ok(counters)
137    } else {
138        Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
139    }
140}
141
142/// The specified value for the `content` property.
143pub type Content = generics::GenericContent<Image>;
144
145/// The specified value for a content item in the `content` property.
146pub type ContentItem = generics::GenericContentItem<Image>;
147
148impl Content {
149    fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle {
150        use crate::counter_style::CounterStyleParsingFlags;
151        input
152            .try_parse(|input| {
153                input.expect_comma()?;
154                CounterStyle::parse(context, input, CounterStyleParsingFlags::empty())
155            })
156            .unwrap_or_else(|_| CounterStyle::decimal())
157    }
158}
159
160impl Parse for Content {
161    // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote |
162    // no-close-quote ]+
163    // TODO: <uri>, attr(<identifier>)
164    #[cfg_attr(feature = "servo", allow(unused_mut))]
165    fn parse<'i, 't>(
166        context: &ParserContext,
167        input: &mut Parser<'i, 't>,
168    ) -> Result<Self, ParseError<'i>> {
169        if input
170            .try_parse(|input| input.expect_ident_matching("normal"))
171            .is_ok()
172        {
173            return Ok(generics::Content::Normal);
174        }
175        if input
176            .try_parse(|input| input.expect_ident_matching("none"))
177            .is_ok()
178        {
179            return Ok(generics::Content::None);
180        }
181
182        let mut items = thin_vec::ThinVec::new();
183        let mut alt_start = None;
184        loop {
185            if alt_start.is_none() {
186                if let Ok(image) = input.try_parse(|i| Image::parse_forbid_none(context, i)) {
187                    items.push(generics::ContentItem::Image(image));
188                    continue;
189                }
190            }
191            let Ok(t) = input.next() else { break };
192            match *t {
193                Token::QuotedString(ref value) => {
194                    items.push(generics::ContentItem::String(
195                        value.as_ref().to_owned().into(),
196                    ));
197                },
198                Token::Function(ref name) => {
199                    // FIXME(emilio): counter() / counters() should be valid per spec past
200                    // the alt marker, but it's likely non-trivial to support and other
201                    // browsers don't support it either, so restricting it for now.
202                    let result = match_ignore_ascii_case! { &name,
203                        "counter" if alt_start.is_none() => input.parse_nested_block(|input| {
204                            let name = CustomIdent::parse(input, &[])?;
205                            let style = Content::parse_counter_style(context, input);
206                            Ok(generics::ContentItem::Counter(name, style))
207                        }),
208                        "counters" if alt_start.is_none() => input.parse_nested_block(|input| {
209                            let name = CustomIdent::parse(input, &[])?;
210                            input.expect_comma()?;
211                            let separator = input.expect_string()?.as_ref().to_owned().into();
212                            let style = Content::parse_counter_style(context, input);
213                            Ok(generics::ContentItem::Counters(name, separator, style))
214                        }),
215                        "attr" if !static_prefs::pref!("layout.css.attr.enabled") => input.parse_nested_block(|input| {
216                            Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?))
217                        }),
218                        _ => {
219                            use style_traits::StyleParseErrorKind;
220                            let name = name.clone();
221                            return Err(input.new_custom_error(
222                                StyleParseErrorKind::UnexpectedFunction(name),
223                            ))
224                        }
225                    }?;
226                    items.push(result);
227                },
228                Token::Ident(ref ident) if alt_start.is_none() => {
229                    items.push(match_ignore_ascii_case! { &ident,
230                        "open-quote" => generics::ContentItem::OpenQuote,
231                        "close-quote" => generics::ContentItem::CloseQuote,
232                        "no-open-quote" => generics::ContentItem::NoOpenQuote,
233                        "no-close-quote" => generics::ContentItem::NoCloseQuote,
234                        #[cfg(feature = "gecko")]
235                        "-moz-alt-content" if context.in_ua_sheet() => {
236                            generics::ContentItem::MozAltContent
237                        },
238                        #[cfg(feature = "gecko")]
239                        "-moz-label-content" if context.chrome_rules_enabled() => {
240                            generics::ContentItem::MozLabelContent
241                        },
242                        _ =>{
243                            let ident = ident.clone();
244                            return Err(input.new_custom_error(
245                                SelectorParseErrorKind::UnexpectedIdent(ident)
246                            ));
247                        }
248                    });
249                },
250                Token::Delim('/')
251                    if alt_start.is_none()
252                        && !items.is_empty()
253                        && static_prefs::pref!("layout.css.content.alt-text.enabled") =>
254                {
255                    alt_start = Some(items.len());
256                },
257                ref t => {
258                    let t = t.clone();
259                    return Err(input.new_unexpected_token_error(t));
260                },
261            }
262        }
263        if items.is_empty() {
264            return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
265        }
266        let alt_start = alt_start.unwrap_or(items.len());
267        Ok(generics::Content::Items(generics::GenericContentItems {
268            items,
269            alt_start,
270        }))
271    }
272}