Source code

Revision control

Copy as Markdown

Other Tools

/* 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::Notation;
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. && !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: &[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 let Some(value) = value.strip_prefix("--") {
dest.write_str("--")?;
serialize_name(value, 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'\x01'..=b'\x1F').contains(&b) || 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 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 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,
/// type.
WhiteSpace,
/// 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,
Number,
/// type.
Dimension,
/// type.
Percentage,
/// `<bad-url-token>` type.
UrlOrBadUrl,
Function,
Ident,
/// The `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram) type.
CDC,
/// The `|=`
DashMatch,
/// The `*=`
/// 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.
///
///
/// `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,
}
}
}