Skip to main content

style/counter_style/
mod.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//! The [`@counter-style`][counter-style] at-rule.
6//!
7//! [counter-style]: https://drafts.csswg.org/css-counter-styles/
8
9use crate::derives::*;
10use crate::error_reporting::ContextualParseError;
11use crate::parser::{Parse, ParserContext};
12use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard};
13use crate::values::specified::Integer;
14use crate::values::{AtomString, CustomIdent};
15use crate::Atom;
16use cssparser::{
17    ascii_case_insensitive_phf_map, match_ignore_ascii_case, CowRcStr, Parser, RuleBodyParser,
18    SourceLocation, Token,
19};
20use std::fmt::{self, Write};
21use std::mem;
22use std::num::Wrapping;
23use style_traits::{
24    Comma, CssStringWriter, CssWriter, KeywordsCollectFn, OneOrMoreSeparated, ParseError,
25    SpecifiedValueInfo, StyleParseErrorKind, ToCss,
26};
27
28pub use crate::properties::counter_style::{DescriptorId, DescriptorParser, Descriptors};
29
30/// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type
31#[allow(missing_docs)]
32#[derive(
33    Clone,
34    Copy,
35    Debug,
36    Deserialize,
37    Eq,
38    MallocSizeOf,
39    Parse,
40    PartialEq,
41    Serialize,
42    ToComputedValue,
43    ToCss,
44    ToResolvedValue,
45    ToShmem,
46)]
47#[repr(u8)]
48pub enum SymbolsType {
49    Cyclic,
50    Numeric,
51    Alphabetic,
52    Symbolic,
53    Fixed,
54}
55
56/// <https://drafts.csswg.org/css-counter-styles/#typedef-counter-style>
57///
58/// Note that 'none' is not a valid name, but we include this (along with String) for space
59/// efficiency when storing list-style-type.
60#[derive(
61    Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
62)]
63#[repr(u8)]
64pub enum CounterStyle {
65    /// The 'none' value.
66    None,
67    /// `<counter-style-name>`
68    Name(CustomIdent),
69    /// `symbols()`
70    #[css(function)]
71    Symbols {
72        /// The <symbols-type>, or symbolic if not specified.
73        #[css(skip_if = "is_symbolic")]
74        ty: SymbolsType,
75        /// The actual symbols.
76        symbols: Symbols,
77    },
78    /// A single string value, useful for `<list-style-type>`.
79    String(AtomString),
80}
81
82#[inline]
83fn is_symbolic(symbols_type: &SymbolsType) -> bool {
84    *symbols_type == SymbolsType::Symbolic
85}
86
87impl CounterStyle {
88    /// disc value
89    pub fn disc() -> Self {
90        CounterStyle::Name(CustomIdent(atom!("disc")))
91    }
92
93    /// decimal value
94    pub fn decimal() -> Self {
95        CounterStyle::Name(CustomIdent(atom!("decimal")))
96    }
97
98    /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`)
99    #[inline]
100    pub fn is_bullet(&self) -> bool {
101        match self {
102            CounterStyle::Name(CustomIdent(name)) => {
103                name == &atom!("disc")
104                    || name == &atom!("circle")
105                    || name == &atom!("square")
106                    || name == &atom!("disclosure-closed")
107                    || name == &atom!("disclosure-open")
108            },
109            _ => false,
110        }
111    }
112
113    /// Returns whether this is a particular name.
114    pub fn is_name(&self, name: &Atom) -> bool {
115        match *self {
116            CounterStyle::Name(CustomIdent(ref n)) => n == name,
117            _ => false,
118        }
119    }
120}
121
122bitflags! {
123    #[derive(Clone, Copy)]
124    /// Flags to control parsing of counter styles.
125    pub struct CounterStyleParsingFlags: u8 {
126        /// Whether `none` is allowed.
127        const ALLOW_NONE = 1 << 0;
128        /// Whether a bare string is allowed.
129        const ALLOW_STRING = 1 << 1;
130    }
131}
132
133impl CounterStyle {
134    /// Parse a counter style, and optionally none|string (for list-style-type).
135    pub fn parse(
136        context: &ParserContext,
137        input: &mut Parser,
138        flags: CounterStyleParsingFlags,
139    ) -> Result<Self, ParseError> {
140        use self::CounterStyleParsingFlags as Flags;
141        match input.next()? {
142            Token::QuotedString(string) if flags.intersects(Flags::ALLOW_STRING) => {
143                Ok(Self::String(AtomString::from(string.as_ref())))
144            },
145            Token::Ident(ident) => {
146                if flags.intersects(Flags::ALLOW_NONE) && ident.eq_ignore_ascii_case("none") {
147                    return Ok(Self::None);
148                }
149                Ok(Self::Name(counter_style_name_from_ident(ident)?))
150            },
151            Token::Function(name) if name.eq_ignore_ascii_case("symbols") => {
152                input.parse_nested_block(|input| {
153                    let symbols_type = input
154                        .try_parse(SymbolsType::parse)
155                        .unwrap_or(SymbolsType::Symbolic);
156                    let symbols = Symbols::parse(context, input)?;
157                    // There must be at least two symbols for alphabetic or
158                    // numeric system.
159                    if (symbols_type == SymbolsType::Alphabetic
160                        || symbols_type == SymbolsType::Numeric)
161                        && symbols.0.len() < 2
162                    {
163                        return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
164                    }
165                    // Identifier is not allowed in symbols() function.
166                    if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) {
167                        return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
168                    }
169                    Ok(Self::Symbols {
170                        ty: symbols_type,
171                        symbols,
172                    })
173                })
174            },
175            _ => Err(ParseError::unexpected_token()),
176        }
177    }
178}
179
180impl SpecifiedValueInfo for CounterStyle {
181    fn collect_completion_keywords(f: KeywordsCollectFn) {
182        // XXX The best approach for implementing this is probably
183        // having a CounterStyleName type wrapping CustomIdent, and
184        // put the predefined list for that type in counter_style mod.
185        // But that's a non-trivial change itself, so we use a simpler
186        // approach here.
187        macro_rules! predefined {
188            ($($name:expr,)+) => {
189                f(&["symbols", "none", $($name,)+])
190            }
191        }
192        include!("predefined.rs");
193    }
194}
195
196fn parse_counter_style_name(input: &mut Parser) -> Result<CustomIdent, ParseError> {
197    let ident = input.expect_ident()?;
198    counter_style_name_from_ident(ident)
199}
200
201/// This allows the reserved counter style names "decimal" and "disc".
202fn counter_style_name_from_ident<'i>(ident: &CowRcStr<'i>) -> Result<CustomIdent, ParseError> {
203    macro_rules! predefined {
204        ($($name: tt,)+) => {{
205            ascii_case_insensitive_phf_map! {
206                predefined -> Atom = {
207                    $(
208                        $name => atom!($name),
209                    )+
210                }
211            }
212
213            // This effectively performs case normalization only on predefined names.
214            if let Some(lower_case) = predefined::get(&ident) {
215                Ok(CustomIdent(lower_case.clone()))
216            } else {
217                // none is always an invalid <counter-style> value.
218                CustomIdent::from_ident(ident, &["none"])
219            }
220        }}
221    }
222    include!("predefined.rs")
223}
224
225fn is_valid_name_definition(ident: &CustomIdent) -> bool {
226    ident.0 != atom!("decimal")
227        && ident.0 != atom!("disc")
228        && ident.0 != atom!("circle")
229        && ident.0 != atom!("square")
230        && ident.0 != atom!("disclosure-closed")
231        && ident.0 != atom!("disclosure-open")
232}
233
234/// Parse the prelude of an @counter-style rule
235pub fn parse_counter_style_name_definition(input: &mut Parser) -> Result<CustomIdent, ParseError> {
236    parse_counter_style_name(input).and_then(|ident| {
237        if !is_valid_name_definition(&ident) {
238            Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
239        } else {
240            Ok(ident)
241        }
242    })
243}
244
245/// A @counter-style rule
246#[derive(Clone, Debug, ToShmem)]
247pub struct CounterStyleRule {
248    name: CustomIdent,
249    generation: Wrapping<u32>,
250    descriptors: Descriptors,
251    /// The parser location of the @counter-style rule.
252    pub source_location: SourceLocation,
253}
254
255/// Parse the body (inside `{}`) of an @counter-style rule
256pub fn parse_counter_style_body(
257    name: CustomIdent,
258    context: &ParserContext,
259    input: &mut Parser,
260    location: SourceLocation,
261) -> Result<CounterStyleRule, ParseError> {
262    let start = input.current_source_location();
263    let mut rule = CounterStyleRule::empty(name, location);
264    {
265        let mut parser = DescriptorParser {
266            context,
267            descriptors: &mut rule.descriptors,
268        };
269        let iter = RuleBodyParser::new(input, &mut parser);
270        for declaration in iter {
271            if let Err((error, slice, location)) = declaration {
272                let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(
273                    slice, error,
274                );
275                context.log_css_error(location, error)
276            }
277        }
278    }
279    let error = match *rule.resolved_system() {
280        ref system @ System::Cyclic
281        | ref system @ System::Fixed { .. }
282        | ref system @ System::Symbolic
283        | ref system @ System::Alphabetic
284        | ref system @ System::Numeric
285            if rule.descriptors.symbols.is_none() =>
286        {
287            let system = system.to_css_string();
288            Some(ContextualParseError::InvalidCounterStyleWithoutSymbols(
289                system,
290            ))
291        },
292        ref system @ System::Alphabetic | ref system @ System::Numeric
293            if rule.descriptors.symbols.as_ref().unwrap().0.len() < 2 =>
294        {
295            let system = system.to_css_string();
296            Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols(
297                system,
298            ))
299        },
300        System::Additive if rule.descriptors.additive_symbols.is_none() => {
301            Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols)
302        },
303        System::Extends(_) if rule.descriptors.symbols.is_some() => {
304            Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols)
305        },
306        System::Extends(_) if rule.descriptors.additive_symbols.is_some() => {
307            Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols)
308        },
309        _ => None,
310    };
311    if let Some(error) = error {
312        context.log_css_error(start, error);
313        Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
314    } else {
315        Ok(rule)
316    }
317}
318
319impl ToCssWithGuard for CounterStyleRule {
320    fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
321        dest.write_str("@counter-style ")?;
322        self.name.to_css(&mut CssWriter::new(dest))?;
323        dest.write_str(" { ")?;
324        self.descriptors.to_css(&mut CssWriter::new(dest))?;
325        dest.write_char('}')
326    }
327}
328
329// Implements the special checkers for some setters.
330// See <https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface>
331impl CounterStyleRule {
332    fn empty(name: CustomIdent, source_location: SourceLocation) -> Self {
333        Self {
334            name,
335            generation: Wrapping(0),
336            descriptors: Descriptors::default(),
337            source_location,
338        }
339    }
340
341    /// Expose the descriptors as read-only, since we rely on updating our generation when they
342    /// change, and some setters need extra validation.
343    pub fn descriptors(&self) -> &Descriptors {
344        &self.descriptors
345    }
346
347    /// Set a descriptor, with the relevant validation. returns whether the descriptor changed.
348    pub fn set_descriptor(
349        &mut self,
350        id: DescriptorId,
351        context: &ParserContext,
352        input: &mut Parser,
353    ) -> Result<bool, ParseError> {
354        // Some descriptors need a couple extra checks, deal with them specially.
355        // TODO(emilio): Remove these, see https://github.com/w3c/csswg-drafts/issues/5717
356        if id == DescriptorId::AdditiveSymbols
357            && matches!(*self.resolved_system(), System::Extends(..))
358        {
359            // No additive symbols should be set for extends system.
360            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
361        }
362        let changed = match id {
363            DescriptorId::System => {
364                let system = input.parse_entirely(|i| System::parse(context, i))?;
365                if !self.check_system(&system) {
366                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
367                }
368                let new = Some(system);
369                if self.descriptors.system == new {
370                    return Ok(false);
371                }
372                self.descriptors.system = new;
373                true
374            },
375            DescriptorId::Symbols => {
376                let symbols = input.parse_entirely(|i| Symbols::parse(context, i))?;
377                if !self.check_symbols(&symbols) {
378                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
379                }
380                let new = Some(symbols);
381                if self.descriptors.symbols == new {
382                    return Ok(false);
383                }
384                self.descriptors.symbols = new;
385                true
386            },
387            _ => self.descriptors.set(id, context, input)?,
388        };
389        if changed {
390            self.generation += Wrapping(1);
391        }
392        Ok(changed)
393    }
394
395    /// Check that the system is effectively not changed. Only params of system descriptor is
396    /// changeable.
397    fn check_system(&self, value: &System) -> bool {
398        mem::discriminant(self.resolved_system()) == mem::discriminant(value)
399    }
400
401    fn check_symbols(&self, value: &Symbols) -> bool {
402        match *self.resolved_system() {
403            // These two systems require at least two symbols.
404            System::Numeric | System::Alphabetic => value.0.len() >= 2,
405            // No symbols should be set for extends system.
406            System::Extends(_) => false,
407            _ => true,
408        }
409    }
410
411    /// Get the name of the counter style rule.
412    pub fn name(&self) -> &CustomIdent {
413        &self.name
414    }
415
416    /// Set the name of the counter style rule. Caller must ensure that
417    /// the name is valid.
418    pub fn set_name(&mut self, name: CustomIdent) {
419        debug_assert!(is_valid_name_definition(&name));
420        self.name = name;
421    }
422
423    /// Get the current generation of the counter style rule.
424    pub fn generation(&self) -> u32 {
425        self.generation.0
426    }
427
428    /// Get the system of this counter style rule, default to
429    /// `symbolic` if not specified.
430    pub fn resolved_system(&self) -> &System {
431        match self.descriptors.system {
432            Some(ref system) => system,
433            None => &System::Symbolic,
434        }
435    }
436}
437
438/// <https://drafts.csswg.org/css-counter-styles/#counter-style-system>
439#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
440pub enum System {
441    /// 'cyclic'
442    Cyclic,
443    /// 'numeric'
444    Numeric,
445    /// 'alphabetic'
446    Alphabetic,
447    /// 'symbolic'
448    Symbolic,
449    /// 'additive'
450    Additive,
451    /// 'fixed <integer>?'
452    Fixed {
453        /// '<integer>?'
454        first_symbol_value: Option<Integer>,
455    },
456    /// 'extends <counter-style-name>'
457    Extends(CustomIdent),
458}
459
460impl Parse for System {
461    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
462        try_match_ident_ignore_ascii_case! { input,
463            "cyclic" => Ok(System::Cyclic),
464            "numeric" => Ok(System::Numeric),
465            "alphabetic" => Ok(System::Alphabetic),
466            "symbolic" => Ok(System::Symbolic),
467            "additive" => Ok(System::Additive),
468            "fixed" => {
469                let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok();
470                Ok(System::Fixed { first_symbol_value })
471            },
472            "extends" => {
473                let other = parse_counter_style_name(input)?;
474                Ok(System::Extends(other))
475            },
476        }
477    }
478}
479
480impl ToCss for System {
481    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
482    where
483        W: Write,
484    {
485        match self {
486            System::Cyclic => dest.write_str("cyclic"),
487            System::Numeric => dest.write_str("numeric"),
488            System::Alphabetic => dest.write_str("alphabetic"),
489            System::Symbolic => dest.write_str("symbolic"),
490            System::Additive => dest.write_str("additive"),
491            System::Fixed { first_symbol_value } => {
492                if let Some(value) = first_symbol_value {
493                    dest.write_str("fixed ")?;
494                    value.to_css(dest)
495                } else {
496                    dest.write_str("fixed")
497                }
498            },
499            System::Extends(other) => {
500                dest.write_str("extends ")?;
501                other.to_css(dest)
502            },
503        }
504    }
505}
506
507/// <https://drafts.csswg.org/css-counter-styles/#typedef-symbol>
508#[derive(
509    Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem,
510)]
511#[repr(u8)]
512pub enum Symbol {
513    /// <string>
514    String(crate::OwnedStr),
515    /// <custom-ident>
516    Ident(CustomIdent),
517    // Not implemented:
518    // /// <image>
519    // Image(Image),
520}
521
522impl Parse for Symbol {
523    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
524        match *input.next()? {
525            Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())),
526            Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(s, &[])?)),
527            _ => Err(ParseError::unexpected_token()),
528        }
529    }
530}
531
532impl Symbol {
533    /// Returns whether this symbol is allowed in symbols() function.
534    pub fn is_allowed_in_symbols(&self) -> bool {
535        match self {
536            // Identifier is not allowed.
537            &Symbol::Ident(_) => false,
538            _ => true,
539        }
540    }
541}
542
543/// <https://drafts.csswg.org/css-counter-styles/#counter-style-negative>
544#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
545pub struct Negative(pub Symbol, pub Option<Symbol>);
546
547impl Parse for Negative {
548    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
549        Ok(Negative(
550            Symbol::parse(context, input)?,
551            input.try_parse(|input| Symbol::parse(context, input)).ok(),
552        ))
553    }
554}
555
556/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range>
557#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
558pub struct CounterRange {
559    /// The start of the range.
560    pub start: CounterBound,
561    /// The end of the range.
562    pub end: CounterBound,
563}
564
565/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range>
566///
567/// Empty represents 'auto'
568#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
569#[css(comma)]
570pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice<CounterRange>);
571
572/// A bound found in `CounterRanges`.
573#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
574pub enum CounterBound {
575    /// An integer bound.
576    Integer(Integer),
577    /// The infinite bound.
578    Infinite,
579}
580
581impl Parse for CounterRanges {
582    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
583        if input
584            .try_parse(|input| input.expect_ident_matching("auto"))
585            .is_ok()
586        {
587            return Ok(CounterRanges(Default::default()));
588        }
589
590        let ranges = input.parse_comma_separated(|input| {
591            let start = parse_bound(context, input)?;
592            let end = parse_bound(context, input)?;
593            if let (CounterBound::Integer(s), CounterBound::Integer(e)) = (&start, &end) {
594                // Rejects calc expressions that cannot be resolved at parse time,
595                // since @counter-style descriptors require concrete values.
596                let s = s.resolve();
597                let e = e.resolve();
598                if s.is_none() || e.is_none() || s.unwrap() > e.unwrap() {
599                    return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
600                }
601            }
602            Ok(CounterRange { start, end })
603        })?;
604
605        Ok(CounterRanges(ranges.into()))
606    }
607}
608
609fn parse_bound(context: &ParserContext, input: &mut Parser) -> Result<CounterBound, ParseError> {
610    if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) {
611        return Ok(CounterBound::Integer(integer));
612    }
613    input.expect_ident_matching("infinite")?;
614    Ok(CounterBound::Infinite)
615}
616
617/// <https://drafts.csswg.org/css-counter-styles/#counter-style-pad>
618#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
619pub struct Pad(pub Integer, pub Symbol);
620
621impl Parse for Pad {
622    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
623        let pad_with = input.try_parse(|input| Symbol::parse(context, input));
624        let min_length = Integer::parse_non_negative(context, input)?;
625        // Rejects calc expressions that cannot be resolved at parse time,
626        // since @counter-style descriptors require concrete values.
627        if min_length.resolve().is_none() {
628            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
629        }
630        let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?;
631        Ok(Pad(min_length, pad_with))
632    }
633}
634
635/// <https://drafts.csswg.org/css-counter-styles/#counter-style-fallback>
636#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
637pub struct Fallback(pub CustomIdent);
638
639impl Parse for Fallback {
640    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
641        Ok(Fallback(parse_counter_style_name(input)?))
642    }
643}
644
645/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols>
646#[derive(
647    Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem,
648)]
649#[repr(C)]
650pub struct Symbols(
651    #[css(iterable)]
652    #[ignore_malloc_size_of = "Arc"]
653    pub crate::ArcSlice<Symbol>,
654);
655
656impl Parse for Symbols {
657    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
658        let mut symbols = smallvec::SmallVec::<[_; 5]>::new();
659        while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) {
660            symbols.push(s);
661        }
662        if symbols.is_empty() {
663            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
664        }
665        Ok(Symbols(crate::ArcSlice::from_iter(symbols.drain(..))))
666    }
667}
668
669/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols>
670#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
671#[css(comma)]
672pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice<AdditiveTuple>);
673
674impl Parse for AdditiveSymbols {
675    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
676        let tuples = Vec::<AdditiveTuple>::parse(context, input)?;
677        if tuples.iter().any(|t| t.weight.resolve().is_none()) {
678            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
679        }
680        // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220
681        if tuples
682            .windows(2)
683            .any(|window| window[0].weight.get() <= window[1].weight.get())
684        {
685            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
686        }
687        Ok(AdditiveSymbols(tuples.into()))
688    }
689}
690
691/// <integer> && <symbol>
692#[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)]
693pub struct AdditiveTuple {
694    /// <integer>
695    pub weight: Integer,
696    /// <symbol>
697    pub symbol: Symbol,
698}
699
700impl OneOrMoreSeparated for AdditiveTuple {
701    type S = Comma;
702}
703
704impl Parse for AdditiveTuple {
705    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
706        let symbol = input.try_parse(|input| Symbol::parse(context, input));
707        let weight = Integer::parse_non_negative(context, input)?;
708        let symbol = symbol.or_else(|_| Symbol::parse(context, input))?;
709        Ok(Self { weight, symbol })
710    }
711}
712
713/// <https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as>
714#[derive(Clone, Debug, MallocSizeOf, ToCss, PartialEq, ToShmem)]
715pub enum SpeakAs {
716    /// auto
717    Auto,
718    /// bullets
719    Bullets,
720    /// numbers
721    Numbers,
722    /// words
723    Words,
724    // /// spell-out, not supported, see bug 1024178
725    // SpellOut,
726    /// <counter-style-name>
727    Other(CustomIdent),
728}
729
730impl Parse for SpeakAs {
731    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
732        let mut is_spell_out = false;
733        let result = input.try_parse(|input| {
734            let ident = input.expect_ident().map_err(|_| ())?;
735            match_ignore_ascii_case! { ident,
736                "auto" => Ok(SpeakAs::Auto),
737                "bullets" => Ok(SpeakAs::Bullets),
738                "numbers" => Ok(SpeakAs::Numbers),
739                "words" => Ok(SpeakAs::Words),
740                "spell-out" => {
741                    is_spell_out = true;
742                    Err(())
743                },
744                _ => Err(()),
745            }
746        });
747        if is_spell_out {
748            // spell-out is not supported, but don’t parse it as a <counter-style-name>.
749            // See bug 1024178.
750            return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
751        }
752        result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?)))
753    }
754}