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