Skip to main content

style/counter_style/
mod.rs

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