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