/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! The [`@counter-style`][counter-style] at-rule. //! //! [counter-style]: https://drafts.csswg.org/css-counter-styles/ use crate::derives::*; use crate::error_reporting::ContextualParseError; use crate::parser::{Parse, ParserContext}; use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; use crate::values::specified::Integer; use crate::values::{AtomString, CustomIdent}; use crate::Atom; use cssparser::{ ascii_case_insensitive_phf_map, match_ignore_ascii_case, CowRcStr, Parser, RuleBodyParser, SourceLocation, Token, }; use std::fmt::{self, Write}; use std::mem; use std::num::Wrapping; use style_traits::{ Comma, CssStringWriter, CssWriter, KeywordsCollectFn, OneOrMoreSeparated, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, }; pub use crate::properties::counter_style::{DescriptorId, DescriptorParser, Descriptors}; /// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type #[allow(missing_docs)] #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum SymbolsType { Cyclic, Numeric, Alphabetic, Symbolic, Fixed, } /// /// /// Note that 'none' is not a valid name, but we include this (along with String) for space /// efficiency when storing list-style-type. #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum CounterStyle { /// The 'none' value. None, /// `` Name(CustomIdent), /// `symbols()` #[css(function)] Symbols { /// The , or symbolic if not specified. #[css(skip_if = "is_symbolic")] ty: SymbolsType, /// The actual symbols. symbols: Symbols, }, /// A single string value, useful for ``. String(AtomString), } #[inline] fn is_symbolic(symbols_type: &SymbolsType) -> bool { *symbols_type == SymbolsType::Symbolic } impl CounterStyle { /// disc value pub fn disc() -> Self { CounterStyle::Name(CustomIdent(atom!("disc"))) } /// decimal value pub fn decimal() -> Self { CounterStyle::Name(CustomIdent(atom!("decimal"))) } /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`) #[inline] pub fn is_bullet(&self) -> bool { match self { CounterStyle::Name(CustomIdent(ref name)) => { name == &atom!("disc") || name == &atom!("circle") || name == &atom!("square") || name == &atom!("disclosure-closed") || name == &atom!("disclosure-open") }, _ => false, } } } bitflags! { #[derive(Clone, Copy)] /// Flags to control parsing of counter styles. pub struct CounterStyleParsingFlags: u8 { /// Whether `none` is allowed. const ALLOW_NONE = 1 << 0; /// Whether a bare string is allowed. const ALLOW_STRING = 1 << 1; } } impl CounterStyle { /// Parse a counter style, and optionally none|string (for list-style-type). pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, flags: CounterStyleParsingFlags, ) -> Result> { use self::CounterStyleParsingFlags as Flags; let location = input.current_source_location(); match input.next()? { Token::QuotedString(ref string) if flags.intersects(Flags::ALLOW_STRING) => { Ok(Self::String(AtomString::from(string.as_ref()))) }, Token::Ident(ref ident) => { if flags.intersects(Flags::ALLOW_NONE) && ident.eq_ignore_ascii_case("none") { return Ok(Self::None); } Ok(Self::Name(counter_style_name_from_ident(ident, location)?)) }, Token::Function(ref name) if name.eq_ignore_ascii_case("symbols") => { input.parse_nested_block(|input| { let symbols_type = input .try_parse(SymbolsType::parse) .unwrap_or(SymbolsType::Symbolic); let symbols = Symbols::parse(context, input)?; // There must be at least two symbols for alphabetic or // numeric system. if (symbols_type == SymbolsType::Alphabetic || symbols_type == SymbolsType::Numeric) && symbols.0.len() < 2 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } // Identifier is not allowed in symbols() function. if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Self::Symbols { ty: symbols_type, symbols, }) }) }, t => Err(location.new_unexpected_token_error(t.clone())), } } } impl SpecifiedValueInfo for CounterStyle { fn collect_completion_keywords(f: KeywordsCollectFn) { // XXX The best approach for implementing this is probably // having a CounterStyleName type wrapping CustomIdent, and // put the predefined list for that type in counter_style mod. // But that's a non-trivial change itself, so we use a simpler // approach here. macro_rules! predefined { ($($name:expr,)+) => { f(&["symbols", "none", $($name,)+]) } } include!("predefined.rs"); } } fn parse_counter_style_name<'i>(input: &mut Parser<'i, '_>) -> Result> { let location = input.current_source_location(); let ident = input.expect_ident()?; counter_style_name_from_ident(ident, location) } /// This allows the reserved counter style names "decimal" and "disc". fn counter_style_name_from_ident<'i>( ident: &CowRcStr<'i>, location: SourceLocation, ) -> Result> { macro_rules! predefined { ($($name: tt,)+) => {{ ascii_case_insensitive_phf_map! { predefined -> Atom = { $( $name => atom!($name), )+ } } // This effectively performs case normalization only on predefined names. if let Some(lower_case) = predefined::get(&ident) { Ok(CustomIdent(lower_case.clone())) } else { // none is always an invalid value. CustomIdent::from_ident(location, ident, &["none"]) } }} } include!("predefined.rs") } fn is_valid_name_definition(ident: &CustomIdent) -> bool { ident.0 != atom!("decimal") && ident.0 != atom!("disc") && ident.0 != atom!("circle") && ident.0 != atom!("square") && ident.0 != atom!("disclosure-closed") && ident.0 != atom!("disclosure-open") } /// Parse the prelude of an @counter-style rule pub fn parse_counter_style_name_definition<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { parse_counter_style_name(input).and_then(|ident| { if !is_valid_name_definition(&ident) { Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(ident) } }) } /// A @counter-style rule #[derive(Clone, Debug, ToShmem)] pub struct CounterStyleRule { name: CustomIdent, generation: Wrapping, descriptors: Descriptors, /// The parser location of the @counter-style rule. pub source_location: SourceLocation, } /// Parse the body (inside `{}`) of an @counter-style rule pub fn parse_counter_style_body<'i, 't>( name: CustomIdent, context: &ParserContext, input: &mut Parser<'i, 't>, location: SourceLocation, ) -> Result> { let start = input.current_source_location(); let mut rule = CounterStyleRule::empty(name, location); { let mut parser = DescriptorParser { context, descriptors: &mut rule.descriptors, }; let mut iter = RuleBodyParser::new(input, &mut parser); while let Some(declaration) = iter.next() { if let Err((error, slice)) = declaration { let location = error.location; let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration( slice, error, ); context.log_css_error(location, error) } } } let error = match *rule.resolved_system() { ref system @ System::Cyclic | ref system @ System::Fixed { .. } | ref system @ System::Symbolic | ref system @ System::Alphabetic | ref system @ System::Numeric if rule.descriptors.symbols.is_none() => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleWithoutSymbols( system, )) }, ref system @ System::Alphabetic | ref system @ System::Numeric if rule.descriptors.symbols.as_ref().unwrap().0.len() < 2 => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols( system, )) }, System::Additive if rule.descriptors.additive_symbols.is_none() => { Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) }, System::Extends(_) if rule.descriptors.symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) }, System::Extends(_) if rule.descriptors.additive_symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) }, _ => None, }; if let Some(error) = error { context.log_css_error(start, error); Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(rule) } } impl ToCssWithGuard for CounterStyleRule { fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { dest.write_str("@counter-style ")?; self.name.to_css(&mut CssWriter::new(dest))?; dest.write_str(" { ")?; self.descriptors.to_css(&mut CssWriter::new(dest))?; dest.write_char('}') } } // Implements the special checkers for some setters. // See impl CounterStyleRule { fn empty(name: CustomIdent, source_location: SourceLocation) -> Self { Self { name: name, generation: Wrapping(0), descriptors: Descriptors::default(), source_location, } } /// Expose the descriptors as read-only, since we rely on updating our generation when they /// change, and some setters need extra validation. pub fn descriptors(&self) -> &Descriptors { &self.descriptors } /// Set a descriptor, with the relevant validation. returns whether the descriptor changed. pub fn set_descriptor<'i>( &mut self, id: DescriptorId, context: &ParserContext, input: &mut Parser<'i, '_>, ) -> Result> { // Some descriptors need a couple extra checks, deal with them specially. // TODO(emilio): Remove these, see https://github.com/w3c/csswg-drafts/issues/5717 if id == DescriptorId::AdditiveSymbols && matches!(*self.resolved_system(), System::Extends(..)) { // No additive symbols should be set for extends system. return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let changed = match id { DescriptorId::System => { let system = input.parse_entirely(|i| System::parse(context, i))?; if !self.check_system(&system) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let new = Some(system); if self.descriptors.system == new { return Ok(false); } self.descriptors.system = new; true }, DescriptorId::Symbols => { let symbols = input.parse_entirely(|i| Symbols::parse(context, i))?; if !self.check_symbols(&symbols) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } let new = Some(symbols); if self.descriptors.symbols == new { return Ok(false); } self.descriptors.symbols = new; true }, _ => self.descriptors.set(id, context, input)?, }; if changed { self.generation += Wrapping(1); } Ok(changed) } /// Check that the system is effectively not changed. Only params of system descriptor is /// changeable. fn check_system(&self, value: &System) -> bool { mem::discriminant(self.resolved_system()) == mem::discriminant(value) } fn check_symbols(&self, value: &Symbols) -> bool { match *self.resolved_system() { // These two systems require at least two symbols. System::Numeric | System::Alphabetic => value.0.len() >= 2, // No symbols should be set for extends system. System::Extends(_) => false, _ => true, } } /// Get the name of the counter style rule. pub fn name(&self) -> &CustomIdent { &self.name } /// Set the name of the counter style rule. Caller must ensure that /// the name is valid. pub fn set_name(&mut self, name: CustomIdent) { debug_assert!(is_valid_name_definition(&name)); self.name = name; } /// Get the current generation of the counter style rule. pub fn generation(&self) -> u32 { self.generation.0 } /// Get the system of this counter style rule, default to /// `symbolic` if not specified. pub fn resolved_system(&self) -> &System { match self.descriptors.system { Some(ref system) => system, None => &System::Symbolic, } } } /// #[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] pub enum System { /// 'cyclic' Cyclic, /// 'numeric' Numeric, /// 'alphabetic' Alphabetic, /// 'symbolic' Symbolic, /// 'additive' Additive, /// 'fixed ?' Fixed { /// '?' first_symbol_value: Option, }, /// 'extends ' Extends(CustomIdent), } impl Parse for System { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { try_match_ident_ignore_ascii_case! { input, "cyclic" => Ok(System::Cyclic), "numeric" => Ok(System::Numeric), "alphabetic" => Ok(System::Alphabetic), "symbolic" => Ok(System::Symbolic), "additive" => Ok(System::Additive), "fixed" => { let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok(); Ok(System::Fixed { first_symbol_value }) }, "extends" => { let other = parse_counter_style_name(input)?; Ok(System::Extends(other)) }, } } } impl ToCss for System { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match *self { System::Cyclic => dest.write_str("cyclic"), System::Numeric => dest.write_str("numeric"), System::Alphabetic => dest.write_str("alphabetic"), System::Symbolic => dest.write_str("symbolic"), System::Additive => dest.write_str("additive"), System::Fixed { first_symbol_value } => { if let Some(value) = first_symbol_value { dest.write_str("fixed ")?; value.to_css(dest) } else { dest.write_str("fixed") } }, System::Extends(ref other) => { dest.write_str("extends ")?; other.to_css(dest) }, } } } /// #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, )] #[repr(u8)] pub enum Symbol { /// String(crate::OwnedStr), /// Ident(CustomIdent), // Not implemented: // /// // Image(Image), } impl Parse for Symbol { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); match *input.next()? { Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())), Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)), ref t => Err(location.new_unexpected_token_error(t.clone())), } } } impl Symbol { /// Returns whether this symbol is allowed in symbols() function. pub fn is_allowed_in_symbols(&self) -> bool { match self { // Identifier is not allowed. &Symbol::Ident(_) => false, _ => true, } } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Negative(pub Symbol, pub Option); impl Parse for Negative { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(Negative( Symbol::parse(context, input)?, input.try_parse(|input| Symbol::parse(context, input)).ok(), )) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct CounterRange { /// The start of the range. pub start: CounterBound, /// The end of the range. pub end: CounterBound, } /// /// /// Empty represents 'auto' #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] #[css(comma)] pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice); /// A bound found in `CounterRanges`. #[derive(Clone, Copy, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub enum CounterBound { /// An integer bound. Integer(Integer), /// The infinite bound. Infinite, } impl Parse for CounterRanges { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if input .try_parse(|input| input.expect_ident_matching("auto")) .is_ok() { return Ok(CounterRanges(Default::default())); } let ranges = input.parse_comma_separated(|input| { let start = parse_bound(context, input)?; let end = parse_bound(context, input)?; if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) { if start > end { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } } Ok(CounterRange { start, end }) })?; Ok(CounterRanges(ranges.into())) } } fn parse_bound<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) { return Ok(CounterBound::Integer(integer)); } input.expect_ident_matching("infinite")?; Ok(CounterBound::Infinite) } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Pad(pub Integer, pub Symbol); impl Parse for Pad { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let pad_with = input.try_parse(|input| Symbol::parse(context, input)); let min_length = Integer::parse_non_negative(context, input)?; let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; Ok(Pad(min_length, pad_with)) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct Fallback(pub CustomIdent); impl Parse for Fallback { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(Fallback(parse_counter_style_name(input)?)) } } /// #[derive( Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, )] #[repr(C)] pub struct Symbols( #[css(iterable)] #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice, ); impl Parse for Symbols { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut symbols = smallvec::SmallVec::<[_; 5]>::new(); while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) { symbols.push(s); } if symbols.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(Symbols(crate::ArcSlice::from_iter(symbols.drain(..)))) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] #[css(comma)] pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice); impl Parse for AdditiveSymbols { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let tuples = Vec::::parse(context, input)?; // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 if tuples .windows(2) .any(|window| window[0].weight <= window[1].weight) { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } Ok(AdditiveSymbols(tuples.into())) } } /// && #[derive(Clone, Debug, MallocSizeOf, ToCss, ToShmem, PartialEq)] pub struct AdditiveTuple { /// pub weight: Integer, /// pub symbol: Symbol, } impl OneOrMoreSeparated for AdditiveTuple { type S = Comma; } impl Parse for AdditiveTuple { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let symbol = input.try_parse(|input| Symbol::parse(context, input)); let weight = Integer::parse_non_negative(context, input)?; let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; Ok(Self { weight, symbol }) } } /// #[derive(Clone, Debug, MallocSizeOf, ToCss, PartialEq, ToShmem)] pub enum SpeakAs { /// auto Auto, /// bullets Bullets, /// numbers Numbers, /// words Words, // /// spell-out, not supported, see bug 1024178 // SpellOut, /// Other(CustomIdent), } impl Parse for SpeakAs { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let mut is_spell_out = false; let result = input.try_parse(|input| { let ident = input.expect_ident().map_err(|_| ())?; match_ignore_ascii_case! { &*ident, "auto" => Ok(SpeakAs::Auto), "bullets" => Ok(SpeakAs::Bullets), "numbers" => Ok(SpeakAs::Numbers), "words" => Ok(SpeakAs::Words), "spell-out" => { is_spell_out = true; Err(()) }, _ => Err(()), } }); if is_spell_out { // spell-out is not supported, but don’t parse it as a . // See bug 1024178. return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?))) } }