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