Skip to main content

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