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