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