/* 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 http://mozilla.org/MPL/2.0/. */ //! The [`@counter-style`][counter-style] at-rule. //! //! [counter-style]: https://drafts.csswg.org/css-counter-styles/ use Atom; use cssparser::{AtRuleParser, DeclarationListParser, DeclarationParser}; use cssparser::{Parser, Token, serialize_identifier, CowRcStr}; use error_reporting::{ContextualParseError, ParseErrorReporter}; #[cfg(feature = "gecko")] use gecko::rules::CounterStyleDescriptors; #[cfg(feature = "gecko")] use gecko_bindings::structs::nsCSSCounterDesc; use parser::{ParserContext, ParserErrorContext, Parse}; use selectors::parser::SelectorParseErrorKind; use shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; use std::ascii::AsciiExt; use std::borrow::Cow; use std::fmt; use std::ops::Range; use style_traits::{Comma, OneOrMoreSeparated, ParseError, StyleParseErrorKind, ToCss}; use values::CustomIdent; /// Parse the prelude of an @counter-style rule pub fn parse_counter_style_name<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { macro_rules! predefined { ($($name: expr,)+) => { { ascii_case_insensitive_phf_map! { // FIXME: use static atoms https://github.com/rust-lang/rust/issues/33156 predefined -> &'static str = { $( $name => $name, )+ } } let location = input.current_source_location(); let ident = input.expect_ident()?; if let Some(&lower_cased) = predefined(&ident) { Ok(CustomIdent(Atom::from(lower_cased))) } else { // https://github.com/w3c/csswg-drafts/issues/1295 excludes "none" CustomIdent::from_ident(location, ident, &["none"]) } } } } include!("predefined.rs") } /// Parse the body (inside `{}`) of an @counter-style rule pub fn parse_counter_style_body<'i, 't, R>(name: CustomIdent, context: &ParserContext, error_context: &ParserErrorContext, input: &mut Parser<'i, 't>) -> Result> where R: ParseErrorReporter { let start = input.current_source_location(); let mut rule = CounterStyleRuleData::empty(name); { let parser = CounterStyleRuleParser { context: context, rule: &mut rule, }; let mut iter = DeclarationListParser::new(input, 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(error_context, location, error) } } } let error = match *rule.system() { ref system @ System::Cyclic | ref system @ System::Fixed { .. } | ref system @ System::Symbolic | ref system @ System::Alphabetic | ref system @ System::Numeric if rule.symbols.is_none() => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleWithoutSymbols(system)) } ref system @ System::Alphabetic | ref system @ System::Numeric if rule.symbols().unwrap().0.len() < 2 => { let system = system.to_css_string(); Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols(system)) } System::Additive if rule.additive_symbols.is_none() => { Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) } System::Extends(_) if rule.symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) } System::Extends(_) if rule.additive_symbols.is_some() => { Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) } _ => None }; if let Some(error) = error { context.log_css_error(error_context, start, error); Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { Ok(rule) } } struct CounterStyleRuleParser<'a, 'b: 'a> { context: &'a ParserContext<'b>, rule: &'a mut CounterStyleRuleData, } /// Default methods reject all at rules. impl<'a, 'b, 'i> AtRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { type PreludeNoBlock = (); type PreludeBlock = (); type AtRule = (); type Error = StyleParseErrorKind<'i>; } macro_rules! accessor { (#[$doc: meta] $name: tt $ident: ident: $ty: ty = !) => { #[$doc] pub fn $ident(&self) -> Option<&$ty> { self.$ident.as_ref() } }; (#[$doc: meta] $name: tt $ident: ident: $ty: ty = $initial: expr) => { #[$doc] pub fn $ident(&self) -> Cow<$ty> { if let Some(ref value) = self.$ident { Cow::Borrowed(value) } else { Cow::Owned($initial) } } } } macro_rules! counter_style_descriptors { ( $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty = $initial: tt )+ ) => { /// An @counter-style rule #[derive(Clone, Debug)] pub struct CounterStyleRuleData { name: CustomIdent, $( #[$doc] $ident: Option<$ty>, )+ } impl CounterStyleRuleData { fn empty(name: CustomIdent) -> Self { CounterStyleRuleData { name: name, $( $ident: None, )+ } } /// Get the name of the counter style rule. pub fn name(&self) -> &CustomIdent { &self.name } $( accessor!(#[$doc] $name $ident: $ty = $initial); )+ /// Convert to Gecko types #[cfg(feature = "gecko")] pub fn set_descriptors(self, descriptors: &mut CounterStyleDescriptors) { $( if let Some(value) = self.$ident { descriptors[nsCSSCounterDesc::$gecko_ident as usize].set_from(value) } )* } } impl<'a, 'b, 'i> DeclarationParser<'i> for CounterStyleRuleParser<'a, 'b> { type Declaration = (); type Error = StyleParseErrorKind<'i>; fn parse_value<'t>(&mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { match_ignore_ascii_case! { &*name, $( $name => { // DeclarationParser also calls parse_entirely // so we’d normally not need to, // but in this case we do because we set the value as a side effect // rather than returning it. let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; self.rule.$ident = Some(value) } )* _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) } Ok(()) } } impl ToCssWithGuard for CounterStyleRuleData { fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut W) -> fmt::Result where W: fmt::Write { dest.write_str("@counter-style ")?; self.name.to_css(dest)?; dest.write_str(" {\n")?; $( if let Some(ref value) = self.$ident { dest.write_str(concat!(" ", $name, ": "))?; ToCss::to_css(value, dest)?; dest.write_str(";\n")?; } )+ dest.write_str("}") } } } } counter_style_descriptors! { /// "system" system / eCSSCounterDesc_System: System = { System::Symbolic } /// "negative" negative / eCSSCounterDesc_Negative: Negative = { Negative(Symbol::String("-".to_owned()), None) } /// "prefix" prefix / eCSSCounterDesc_Prefix: Symbol = { Symbol::String("".to_owned()) } /// "suffix" suffix / eCSSCounterDesc_Suffix: Symbol = { Symbol::String(". ".to_owned()) } /// "range" range / eCSSCounterDesc_Range: Ranges = { Ranges(Vec::new()) // Empty Vec represents 'auto' } /// "pad" pad / eCSSCounterDesc_Pad: Pad = { Pad(0, Symbol::String("".to_owned())) } /// "fallback" fallback / eCSSCounterDesc_Fallback: Fallback = { // FIXME https://bugzilla.mozilla.org/show_bug.cgi?id=1359323 use atom!() Fallback(CustomIdent(Atom::from("decimal"))) } /// "symbols" symbols / eCSSCounterDesc_Symbols: Symbols = ! /// "additive-symbols" additive_symbols / eCSSCounterDesc_AdditiveSymbols: AdditiveSymbols = ! /// "speak-as" speak_as / eCSSCounterDesc_SpeakAs: SpeakAs = { SpeakAs::Auto } } /// #[derive(Clone, Debug)] 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(|i| i.expect_integer()).ok(); Ok(System::Fixed { first_symbol_value: 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 W) -> fmt::Result where W: fmt::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) } } } } /// #[cfg_attr(feature = "gecko", derive(MallocSizeOf))] #[derive(Clone, Debug, Eq, PartialEq, ToComputedValue)] pub enum Symbol { /// String(String), /// Ident(String), // 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() { Ok(&Token::QuotedString(ref s)) => Ok(Symbol::String(s.as_ref().to_owned())), Ok(&Token::Ident(ref s)) => Ok(Symbol::Ident(s.as_ref().to_owned())), Ok(t) => Err(location.new_unexpected_token_error(t.clone())), Err(e) => Err(e.into()), } } } impl ToCss for Symbol { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { match *self { Symbol::String(ref s) => s.to_css(dest), Symbol::Ident(ref s) => serialize_identifier(s, dest), } } } 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, ToCss)] 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(|input| Symbol::parse(context, input)).ok(), )) } } /// /// /// Empty Vec represents 'auto' #[derive(Clone, Debug)] pub struct Ranges(pub Vec>>); impl Parse for Ranges { fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>) -> Result> { if input.try(|input| input.expect_ident_matching("auto")).is_ok() { Ok(Ranges(Vec::new())) } else { input.parse_comma_separated(|input| { let opt_start = parse_bound(input)?; let opt_end = parse_bound(input)?; if let (Some(start), Some(end)) = (opt_start, opt_end) { if start > end { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } } Ok(opt_start..opt_end) }).map(Ranges) } } } fn parse_bound<'i, 't>(input: &mut Parser<'i, 't>) -> Result, ParseError<'i>> { let location = input.current_source_location(); match input.next() { Ok(&Token::Number { int_value: Some(v), .. }) => Ok(Some(v)), Ok(&Token::Ident(ref ident)) if ident.eq_ignore_ascii_case("infinite") => Ok(None), Ok(t) => Err(location.new_unexpected_token_error(t.clone())), Err(e) => Err(e.into()), } } impl ToCss for Ranges { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { let mut iter = self.0.iter(); if let Some(first) = iter.next() { range_to_css(first, dest)?; for item in iter { dest.write_str(", ")?; range_to_css(item, dest)?; } Ok(()) } else { dest.write_str("auto") } } } fn range_to_css(range: &Range>, dest: &mut W) -> fmt::Result where W: fmt::Write { bound_to_css(range.start, dest)?; dest.write_char(' ')?; bound_to_css(range.end, dest) } fn bound_to_css(range: Option, dest: &mut W) -> fmt::Result where W: fmt::Write { if let Some(finite) = range { finite.to_css(dest) } else { dest.write_str("infinite") } } /// #[derive(Clone, Debug, ToCss)] pub struct Pad(pub u32, pub Symbol); impl Parse for Pad { fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Result> { let pad_with = input.try(|input| Symbol::parse(context, input)); let min_length = input.expect_integer()?; if min_length < 0 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; Ok(Pad(min_length as u32, pad_with)) } } /// #[derive(Clone, Debug, ToCss)] pub struct Fallback(pub CustomIdent); impl Parse for Fallback { fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>) -> Result> { parse_counter_style_name(input).map(Fallback) } } /// #[cfg_attr(feature = "gecko", derive(MallocSizeOf))] #[derive(Clone, Debug, Eq, PartialEq, ToComputedValue)] pub struct Symbols(pub Vec); impl Parse for Symbols { fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Result> { let mut symbols = Vec::new(); loop { if let Ok(s) = input.try(|input| Symbol::parse(context, input)) { symbols.push(s) } else { if symbols.is_empty() { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } else { return Ok(Symbols(symbols)) } } } } } impl ToCss for Symbols { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { let mut iter = self.0.iter(); let first = iter.next().expect("expected at least one symbol"); first.to_css(dest)?; for item in iter { dest.write_char(' ')?; item.to_css(dest)?; } Ok(()) } } /// #[derive(Clone, Debug, ToCss)] pub struct AdditiveSymbols(pub Vec); 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)) } } /// && #[derive(Clone, Debug, ToCss)] pub struct AdditiveTuple { /// pub weight: u32, /// 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(|input| Symbol::parse(context, input)); let weight = input.expect_integer()?; if weight < 0 { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; Ok(AdditiveTuple { weight: weight as u32, symbol: symbol, }) } } /// #[derive(Clone, Debug, ToCss)] 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(|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)?)) }) } }