/* 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/. */ use crate::match_byte; use dtoa_short::{self, Notation}; use itoa; use std::fmt::{self, Write}; use std::str; use super::Token; /// Trait for things the can serialize themselves in CSS syntax. pub trait ToCss { /// Serialize `self` in CSS syntax, writing to `dest`. fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write; /// Serialize `self` in CSS syntax and return a string. /// /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) #[inline] fn to_css_string(&self) -> String { let mut s = String::new(); self.to_css(&mut s).unwrap(); s } } #[inline] fn write_numeric(value: f32, int_value: Option, has_sign: bool, dest: &mut W) -> fmt::Result where W: fmt::Write, { // `value.value >= 0` is true for negative 0. if has_sign && value.is_sign_positive() { dest.write_str("+")?; } let notation = if value == 0.0 && value.is_sign_negative() { // Negative zero. Work around #20596. dest.write_str("-0")?; Notation { decimal_point: false, scientific: false, } } else { dtoa_short::write(dest, value)? }; if int_value.is_none() && value.fract() == 0. { if !notation.decimal_point && !notation.scientific { dest.write_str(".0")?; } } Ok(()) } impl<'a> ToCss for Token<'a> { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { match *self { Token::Ident(ref value) => serialize_identifier(&**value, dest)?, Token::AtKeyword(ref value) => { dest.write_str("@")?; serialize_identifier(&**value, dest)?; } Token::Hash(ref value) => { dest.write_str("#")?; serialize_name(value, dest)?; } Token::IDHash(ref value) => { dest.write_str("#")?; serialize_identifier(&**value, dest)?; } Token::QuotedString(ref value) => serialize_string(&**value, dest)?, Token::UnquotedUrl(ref value) => { dest.write_str("url(")?; serialize_unquoted_url(&**value, dest)?; dest.write_str(")")?; } Token::Delim(value) => dest.write_char(value)?, Token::Number { value, int_value, has_sign, } => write_numeric(value, int_value, has_sign, dest)?, Token::Percentage { unit_value, int_value, has_sign, } => { write_numeric(unit_value * 100., int_value, has_sign, dest)?; dest.write_str("%")?; } Token::Dimension { value, int_value, has_sign, ref unit, } => { write_numeric(value, int_value, has_sign, dest)?; // Disambiguate with scientific notation. let unit = &**unit; // TODO(emilio): This doesn't handle e.g. 100E1m, which gets us // an unit of "E1m"... if unit == "e" || unit == "E" || unit.starts_with("e-") || unit.starts_with("E-") { dest.write_str("\\65 ")?; serialize_name(&unit[1..], dest)?; } else { serialize_identifier(unit, dest)?; } } Token::WhiteSpace(content) => dest.write_str(content)?, Token::Comment(content) => { dest.write_str("/*")?; dest.write_str(content)?; dest.write_str("*/")? } Token::Colon => dest.write_str(":")?, Token::Semicolon => dest.write_str(";")?, Token::Comma => dest.write_str(",")?, Token::IncludeMatch => dest.write_str("~=")?, Token::DashMatch => dest.write_str("|=")?, Token::PrefixMatch => dest.write_str("^=")?, Token::SuffixMatch => dest.write_str("$=")?, Token::SubstringMatch => dest.write_str("*=")?, Token::CDO => dest.write_str("")?, Token::Function(ref name) => { serialize_identifier(&**name, dest)?; dest.write_str("(")?; } Token::ParenthesisBlock => dest.write_str("(")?, Token::SquareBracketBlock => dest.write_str("[")?, Token::CurlyBracketBlock => dest.write_str("{")?, Token::BadUrl(ref contents) => { dest.write_str("url(")?; dest.write_str(contents)?; dest.write_char(')')?; } Token::BadString(ref value) => { // During tokenization, an unescaped newline after a quote causes // the token to be a BadString instead of a QuotedString. // The BadString token ends just before the newline // (which is in a separate WhiteSpace token), // and therefore does not have a closing quote. dest.write_char('"')?; CssStringWriter::new(dest).write_str(value)?; } Token::CloseParenthesis => dest.write_str(")")?, Token::CloseSquareBracket => dest.write_str("]")?, Token::CloseCurlyBracket => dest.write_str("}")?, } Ok(()) } } fn hex_escape(ascii_byte: u8, dest: &mut W) -> fmt::Result where W: fmt::Write, { static HEX_DIGITS: &'static [u8; 16] = b"0123456789abcdef"; let b3; let b4; let bytes = if ascii_byte > 0x0F { let high = (ascii_byte >> 4) as usize; let low = (ascii_byte & 0x0F) as usize; b4 = [b'\\', HEX_DIGITS[high], HEX_DIGITS[low], b' ']; &b4[..] } else { b3 = [b'\\', HEX_DIGITS[ascii_byte as usize], b' ']; &b3[..] }; dest.write_str(unsafe { str::from_utf8_unchecked(&bytes) }) } fn char_escape(ascii_byte: u8, dest: &mut W) -> fmt::Result where W: fmt::Write, { let bytes = [b'\\', ascii_byte]; dest.write_str(unsafe { str::from_utf8_unchecked(&bytes) }) } /// Write a CSS identifier, escaping characters as necessary. pub fn serialize_identifier(mut value: &str, dest: &mut W) -> fmt::Result where W: fmt::Write, { if value.is_empty() { return Ok(()); } if value.starts_with("--") { dest.write_str("--")?; serialize_name(&value[2..], dest) } else if value == "-" { dest.write_str("\\-") } else { if value.as_bytes()[0] == b'-' { dest.write_str("-")?; value = &value[1..]; } if let digit @ b'0'..=b'9' = value.as_bytes()[0] { hex_escape(digit, dest)?; value = &value[1..]; } serialize_name(value, dest) } } /// Write a CSS name, like a custom property name. /// /// You should only use this when you know what you're doing, when in doubt, /// consider using `serialize_identifier`. pub fn serialize_name(value: &str, dest: &mut W) -> fmt::Result where W: fmt::Write, { let mut chunk_start = 0; for (i, b) in value.bytes().enumerate() { let escaped = match_byte! { b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' => continue, b'\0' => Some("\u{FFFD}"), b => { if !b.is_ascii() { continue; } None }, }; dest.write_str(&value[chunk_start..i])?; if let Some(escaped) = escaped { dest.write_str(escaped)?; } else if (b >= b'\x01' && b <= b'\x1F') || b == b'\x7F' { hex_escape(b, dest)?; } else { char_escape(b, dest)?; } chunk_start = i + 1; } dest.write_str(&value[chunk_start..]) } fn serialize_unquoted_url(value: &str, dest: &mut W) -> fmt::Result where W: fmt::Write, { let mut chunk_start = 0; for (i, b) in value.bytes().enumerate() { let hex = match_byte! { b, b'\0'..=b' ' | b'\x7F' => true, b'(' | b')' | b'"' | b'\'' | b'\\' => false, _ => continue, }; dest.write_str(&value[chunk_start..i])?; if hex { hex_escape(b, dest)?; } else { char_escape(b, dest)?; } chunk_start = i + 1; } dest.write_str(&value[chunk_start..]) } /// Write a double-quoted CSS string token, escaping content as necessary. pub fn serialize_string(value: &str, dest: &mut W) -> fmt::Result where W: fmt::Write, { dest.write_str("\"")?; CssStringWriter::new(dest).write_str(value)?; dest.write_str("\"")?; Ok(()) } /// A `fmt::Write` adapter that escapes text for writing as a double-quoted CSS string. /// Quotes are not included. /// /// Typical usage: /// /// ```{rust,ignore} /// fn write_foo(foo: &Foo, dest: &mut W) -> fmt::Result where W: fmt::Write { /// dest.write_str("\"")?; /// { /// let mut string_dest = CssStringWriter::new(dest); /// // Write into string_dest... /// } /// dest.write_str("\"")?; /// Ok(()) /// } /// ``` pub struct CssStringWriter<'a, W> { inner: &'a mut W, } impl<'a, W> CssStringWriter<'a, W> where W: fmt::Write, { /// Wrap a text writer to create a `CssStringWriter`. pub fn new(inner: &'a mut W) -> CssStringWriter<'a, W> { CssStringWriter { inner } } } impl<'a, W> fmt::Write for CssStringWriter<'a, W> where W: fmt::Write, { fn write_str(&mut self, s: &str) -> fmt::Result { let mut chunk_start = 0; for (i, b) in s.bytes().enumerate() { let escaped = match_byte! { b, b'"' => Some("\\\""), b'\\' => Some("\\\\"), b'\0' => Some("\u{FFFD}"), b'\x01'..=b'\x1F' | b'\x7F' => None, _ => continue, }; self.inner.write_str(&s[chunk_start..i])?; match escaped { Some(x) => self.inner.write_str(x)?, None => hex_escape(b, self.inner)?, }; chunk_start = i + 1; } self.inner.write_str(&s[chunk_start..]) } } macro_rules! impl_tocss_for_int { ($T: ty) => { impl<'a> ToCss for $T { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { let mut buf = itoa::Buffer::new(); dest.write_str(buf.format(*self)) } } }; } impl_tocss_for_int!(i8); impl_tocss_for_int!(u8); impl_tocss_for_int!(i16); impl_tocss_for_int!(u16); impl_tocss_for_int!(i32); impl_tocss_for_int!(u32); impl_tocss_for_int!(i64); impl_tocss_for_int!(u64); macro_rules! impl_tocss_for_float { ($T: ty) => { impl<'a> ToCss for $T { fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write, { dtoa_short::write(dest, *self).map(|_| ()) } } }; } impl_tocss_for_float!(f32); impl_tocss_for_float!(f64); /// A category of token. See the `needs_separator_when_before` method. #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub enum TokenSerializationType { /// No token serialization type. #[default] Nothing, /// The [``](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram) /// type. WhiteSpace, /// The [``](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram) /// type, the "[``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with /// the type flag set to 'unrestricted'" type, or the /// "[``](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type /// flag set to 'id'" type. AtKeywordOrHash, /// The [``](https://drafts.csswg.org/css-syntax/#number-token-diagram) type. Number, /// The [``](https://drafts.csswg.org/css-syntax/#dimension-token-diagram) /// type. Dimension, /// The [``](https://drafts.csswg.org/css-syntax/#percentage-token-diagram) /// type. Percentage, /// The [``](https://drafts.csswg.org/css-syntax/#url-token-diagram) or /// `` type. UrlOrBadUrl, /// The [``](https://drafts.csswg.org/css-syntax/#function-token-diagram) type. Function, /// The [``](https://drafts.csswg.org/css-syntax/#ident-token-diagram) type. Ident, /// The `-->` [``](https://drafts.csswg.org/css-syntax/#CDC-token-diagram) type. CDC, /// The `|=` /// [``](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram) type. DashMatch, /// The `*=` /// [``](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram) /// type. SubstringMatch, /// The `<(-token>` type. OpenParen, /// The `#` `` type. DelimHash, /// The `@` `` type. DelimAt, /// The `.` or `+` `` type. DelimDotOrPlus, /// The `-` `` type. DelimMinus, /// The `?` `` type. DelimQuestion, /// The `$`, `^`, or `~` `` type. DelimAssorted, /// The `=` `` type. DelimEquals, /// The `|` `` type. DelimBar, /// The `/` `` type. DelimSlash, /// The `*` `` type. DelimAsterisk, /// The `%` `` type. DelimPercent, /// A type indicating any other token. Other, } impl TokenSerializationType { /// Return a value that represents the absence of a token, e.g. before the start of the input. #[deprecated( since = "0.32.1", note = "use TokenSerializationType::Nothing or TokenSerializationType::default() instead" )] pub fn nothing() -> TokenSerializationType { Default::default() } /// If this value is `TokenSerializationType::Nothing`, set it to the given value instead. pub fn set_if_nothing(&mut self, new_value: TokenSerializationType) { if matches!(self, TokenSerializationType::Nothing) { *self = new_value } } /// Return true if, when a token of category `self` is serialized just before /// a token of category `other` with no whitespace in between, /// an empty comment `/**/` needs to be inserted between them /// so that they are not re-parsed as a single token. /// /// See https://drafts.csswg.org/css-syntax/#serialization /// /// See https://github.com/w3c/csswg-drafts/issues/4088 for the /// `DelimPercent` bits. pub fn needs_separator_when_before(self, other: TokenSerializationType) -> bool { use self::TokenSerializationType::*; match self { Ident => matches!( other, Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension | CDC | OpenParen ), AtKeywordOrHash | Dimension => matches!( other, Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension | CDC ), DelimHash | DelimMinus => matches!( other, Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension ), Number => matches!( other, Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | DelimPercent | Dimension ), DelimAt => matches!(other, Ident | Function | UrlOrBadUrl | DelimMinus), DelimDotOrPlus => matches!(other, Number | Percentage | Dimension), DelimAssorted | DelimAsterisk => matches!(other, DelimEquals), DelimBar => matches!(other, DelimEquals | DelimBar | DashMatch), DelimSlash => matches!(other, DelimAsterisk | SubstringMatch), Nothing | WhiteSpace | Percentage | UrlOrBadUrl | Function | CDC | OpenParen | DashMatch | SubstringMatch | DelimQuestion | DelimEquals | DelimPercent | Other => { false } } } } impl<'a> Token<'a> { /// Categorize a token into a type that determines when `/**/` needs to be inserted /// between two tokens when serialized next to each other without whitespace in between. /// /// See the `TokenSerializationType::needs_separator_when_before` method. pub fn serialization_type(&self) -> TokenSerializationType { use self::TokenSerializationType::*; match self { Token::Ident(_) => Ident, Token::AtKeyword(_) | Token::Hash(_) | Token::IDHash(_) => AtKeywordOrHash, Token::UnquotedUrl(_) | Token::BadUrl(_) => UrlOrBadUrl, Token::Delim('#') => DelimHash, Token::Delim('@') => DelimAt, Token::Delim('.') | Token::Delim('+') => DelimDotOrPlus, Token::Delim('-') => DelimMinus, Token::Delim('?') => DelimQuestion, Token::Delim('$') | Token::Delim('^') | Token::Delim('~') => DelimAssorted, Token::Delim('%') => DelimPercent, Token::Delim('=') => DelimEquals, Token::Delim('|') => DelimBar, Token::Delim('/') => DelimSlash, Token::Delim('*') => DelimAsterisk, Token::Number { .. } => Number, Token::Percentage { .. } => Percentage, Token::Dimension { .. } => Dimension, Token::WhiteSpace(_) => WhiteSpace, Token::Comment(_) => DelimSlash, Token::DashMatch => DashMatch, Token::SubstringMatch => SubstringMatch, Token::CDC => CDC, Token::Function(_) => Function, Token::ParenthesisBlock => OpenParen, Token::SquareBracketBlock | Token::CurlyBracketBlock | Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket | Token::QuotedString(_) | Token::BadString(_) | Token::Delim(_) | Token::Colon | Token::Semicolon | Token::Comma | Token::CDO | Token::IncludeMatch | Token::PrefixMatch | Token::SuffixMatch => Other, } } }