diff options
Diffstat (limited to 'third_party/rust/cssparser/src/serializer.rs')
-rw-r--r-- | third_party/rust/cssparser/src/serializer.rs | 593 |
1 files changed, 593 insertions, 0 deletions
diff --git a/third_party/rust/cssparser/src/serializer.rs b/third_party/rust/cssparser/src/serializer.rs new file mode 100644 index 0000000000..09c224022d --- /dev/null +++ b/third_party/rust/cssparser/src/serializer.rs @@ -0,0 +1,593 @@ +/* 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<W>(&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<W>(value: f32, int_value: Option<i32>, 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<W>(&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::CDC => 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<W>(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<W>(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<W>(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<W>(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<W>(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<W>(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<W>(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<W>(&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<W>(&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 [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram) + /// type. + WhiteSpace, + + /// The [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram) + /// type, the "[`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with + /// the type flag set to 'unrestricted'" type, or the + /// "[`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type + /// flag set to 'id'" type. + AtKeywordOrHash, + + /// The [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram) type. + Number, + + /// The [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram) + /// type. + Dimension, + + /// The [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram) + /// type. + Percentage, + + /// The [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram) or + /// `<bad-url-token>` type. + UrlOrBadUrl, + + /// The [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram) type. + Function, + + /// The [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram) type. + Ident, + + /// The `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram) type. + CDC, + + /// The `|=` + /// [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram) type. + DashMatch, + + /// The `*=` + /// [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram) + /// type. + SubstringMatch, + + /// The `<(-token>` type. + OpenParen, + + /// The `#` `<delim-token>` type. + DelimHash, + + /// The `@` `<delim-token>` type. + DelimAt, + + /// The `.` or `+` `<delim-token>` type. + DelimDotOrPlus, + + /// The `-` `<delim-token>` type. + DelimMinus, + + /// The `?` `<delim-token>` type. + DelimQuestion, + + /// The `$`, `^`, or `~` `<delim-token>` type. + DelimAssorted, + + /// The `=` `<delim-token>` type. + DelimEquals, + + /// The `|` `<delim-token>` type. + DelimBar, + + /// The `/` `<delim-token>` type. + DelimSlash, + + /// The `*` `<delim-token>` type. + DelimAsterisk, + + /// The `%` `<delim-token>` 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, + } + } +} |