diff --git a/Cargo.toml b/Cargo.toml index 04a43803..253c6c37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ license = "MPL-2.0" [dependencies] encoding = "0.2" +text_writer = "0.1.1" diff --git a/src/ast.rs b/src/ast.rs index 750bb760..4c32fb0e 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -69,19 +69,6 @@ pub enum ComponentValue { } -impl ComponentValue { - pub fn to_css(&self) -> String { - let mut css = String::new(); - self.to_css_push(&mut css); - css - } - - pub fn to_css_push(&self, css: &mut String) { - ::serializer::to_css_push(self, css) - } -} - - #[deriving(PartialEq)] pub struct Declaration { pub location: SourceLocation, diff --git a/src/color.rs b/src/color.rs index 33c853fb..5872269f 100644 --- a/src/color.rs +++ b/src/color.rs @@ -6,8 +6,11 @@ use std::ascii::AsciiExt; use std::fmt; use std::num::{Float, FloatMath}; +use text_writer::{mod, TextWriter}; + use ast::{ComponentValue, SkipWhitespaceIterable}; use ast::ComponentValue::{Number, Percentage, Function, Ident, Hash, IDHash, Comma}; +use serializer::ToCss; #[deriving(Clone, Copy, PartialEq)] @@ -20,14 +23,19 @@ pub struct RGBA { pub alpha: f32, } -impl fmt::Show for RGBA { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl ToCss for RGBA { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { if self.alpha == 1f32 { - write!(f, "rgb({}, {}, {})", (self.red * 255.).round(), (self.green * 255.).round(), + write!(dest, "rgb({}, {}, {})", + (self.red * 255.).round(), + (self.green * 255.).round(), (self.blue * 255.).round()) } else { - write!(f, "rgba({}, {}, {}, {})", (self.red * 255.).round(), (self.green * 255.).round(), - (self.blue * 255.).round(), self.alpha) + write!(dest, "rgba({}, {}, {}, {})", + (self.red * 255.).round(), + (self.green * 255.).round(), + (self.blue * 255.).round(), + self.alpha) } } } @@ -38,15 +46,23 @@ pub enum Color { RGBA(RGBA), } -impl fmt::Show for Color { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl ToCss for Color { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { match self { - &Color::CurrentColor => write!(f, "currentColor"), - &Color::RGBA(c) => write!(f, "{}", c), + &Color::CurrentColor => dest.write_str("currentColor"), + &Color::RGBA(rgba) => rgba.to_css(dest), } } } +impl fmt::Show for RGBA { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_to_css(f) } +} + +impl fmt::Show for Color { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_to_css(f) } +} + /// Return `Err(())` on invalid or unsupported value (not a color). impl Color { pub fn parse(component_value: &ComponentValue) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 2e626242..66ce3d66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,8 @@ #![feature(globs, macro_rules)] -extern crate encoding; // https://github.com/lifthrasiir/rust-encoding +extern crate encoding; +extern crate text_writer; #[cfg(test)] extern crate test; @@ -24,7 +25,7 @@ pub use parser::{parse_stylesheet_rules, StylesheetParser, pub use from_bytes::{decode_stylesheet_bytes, parse_stylesheet_rules_from_bytes}; pub use color::{RGBA, Color}; pub use nth::parse_nth; -pub use serializer::{ToCss, serialize_identifier, serialize_string}; +pub use serializer::{ToCss, CssStringWriter, serialize_identifier, serialize_string}; pub mod ast; mod tokenizer; diff --git a/src/serializer.rs b/src/serializer.rs index b61016ba..ba911edf 100644 --- a/src/serializer.rs +++ b/src/serializer.rs @@ -2,223 +2,393 @@ * 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 std::fmt; + +use text_writer::{mod, TextWriter}; + use ast::*; use ast::ComponentValue::*; -pub fn to_css_push(component_value: &ComponentValue, css: &mut String) { - match *component_value { - Ident(ref value) => serialize_identifier(value.as_slice(), css), - AtKeyword(ref value) => { - css.push('@'); - serialize_identifier(value.as_slice(), css); - }, - Hash(ref value) => { - css.push('#'); - for c in value.as_slice().chars() { - serialize_char(c, css, /* is_identifier_start = */ false); - } - }, - IDHash(ref value) => { - css.push('#'); - serialize_identifier(value.as_slice(), css); - } - QuotedString(ref value) => serialize_string(value.as_slice(), css), - URL(ref value) => { - css.push_str("url("); - serialize_string(value.as_slice(), css); - css.push(')'); - }, - Delim(value) => css.push(value), - - Number(ref value) => css.push_str(value.representation.as_slice()), - Percentage(ref value) => { - css.push_str(value.representation.as_slice()); - css.push('%'); - }, - Dimension(ref value, ref unit) => { - css.push_str(value.representation.as_slice()); - // Disambiguate with scientific notation. - let unit = unit.as_slice(); - if unit == "e" || unit == "E" || unit.starts_with("e-") || unit.starts_with("E-") { - css.push_str("\\65 "); - for c in unit.slice_from(1).chars() { - serialize_char(c, css, /* is_identifier_start = */ false); +pub trait ToCss for Sized? { + /// Serialize `self` in CSS syntax, writing to `dest`. + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter; + + /// 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 + } + + /// Serialize `self` in CSS syntax and return a result compatible with `std::fmt::Show`. + /// + /// Typical usage is, for a `Foo` that implements `ToCss`: + /// + /// ```{rust,ignore} + /// use std::fmt; + /// impl fmt::Show for Foo { + /// #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_to_css(f) } + /// } + /// ``` + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + #[inline] + fn fmt_to_css(&self, dest: &mut W) -> fmt::Result where W: TextWriter { + self.to_css(dest).map_err(|_| fmt::Error) + } +} + + +impl ToCss for ComponentValue { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match self { + &Ident(ref value) => try!(serialize_identifier(value.as_slice(), dest)), + &AtKeyword(ref value) => { + try!(dest.write_char('@')); + try!(serialize_identifier(value.as_slice(), dest)); + }, + &Hash(ref value) => { + try!(dest.write_char('#')); + for c in value.as_slice().chars() { + try!(serialize_char(c, dest, /* is_identifier_start = */ false)); } - } else { - serialize_identifier(unit, css) + }, + &IDHash(ref value) => { + try!(dest.write_char('#')); + try!(serialize_identifier(value.as_slice(), dest)); } - }, + &QuotedString(ref value) => try!(serialize_string(value.as_slice(), dest)), + &URL(ref value) => { + try!(dest.write_str("url(")); + try!(serialize_string(value.as_slice(), dest)); + try!(dest.write_char(')')); + }, + &Delim(value) => try!(dest.write_char(value)), - UnicodeRange(start, end) => { - css.push_str(format!("U+{:X}", start).as_slice()); - if end != start { - css.push_str(format!("-{:X}", end).as_slice()); + &Number(ref value) => try!(dest.write_str(value.representation.as_slice())), + &Percentage(ref value) => { + try!(dest.write_str(value.representation.as_slice())); + try!(dest.write_char('%')); + }, + &Dimension(ref value, ref unit) => { + try!(dest.write_str(value.representation.as_slice())); + // Disambiguate with scientific notation. + let unit = unit.as_slice(); + if unit == "e" || unit == "E" || unit.starts_with("e-") || unit.starts_with("E-") { + try!(dest.write_str("\\65 ")); + for c in unit.slice_from(1).chars() { + try!(serialize_char(c, dest, /* is_identifier_start = */ false)); + } + } else { + try!(serialize_identifier(unit, dest)); + } + }, + + &UnicodeRange(start, end) => { + try!(dest.write_str(format!("U+{:X}", start).as_slice())); + if end != start { + try!(dest.write_str(format!("-{:X}", end).as_slice())); + } } + + &WhiteSpace => try!(dest.write_char(' ')), + &Colon => try!(dest.write_char(':')), + &Semicolon => try!(dest.write_char(';')), + &Comma => try!(dest.write_char(',')), + &IncludeMatch => try!(dest.write_str("~=")), + &DashMatch => try!(dest.write_str("|=")), + &PrefixMatch => try!(dest.write_str("^=")), + &SuffixMatch => try!(dest.write_str("$=")), + &SubstringMatch => try!(dest.write_str("*=")), + &Column => try!(dest.write_str("||")), + &CDO => try!(dest.write_str("")), + + &Function(ref name, ref arguments) => { + try!(serialize_identifier(name.as_slice(), dest)); + try!(dest.write_char('(')); + try!(arguments.to_css(dest)); + try!(dest.write_char(')')); + }, + &ParenthesisBlock(ref content) => { + try!(dest.write_char('(')); + try!(content.to_css(dest)); + try!(dest.write_char(')')); + }, + &SquareBracketBlock(ref content) => { + try!(dest.write_char('[')); + try!(content.to_css(dest)); + try!(dest.write_char(']')); + }, + &CurlyBracketBlock(ref content) => { + try!(dest.write_char('{')); + try!(content.to_css(dest)); + try!(dest.write_char('}')); + }, + + &BadURL => try!(dest.write_str("url()")), + &BadString => try!(dest.write_str("\"\n")), + &CloseParenthesis => try!(dest.write_char(')')), + &CloseSquareBracket => try!(dest.write_char(']')), + &CloseCurlyBracket => try!(dest.write_char('}')), } + Ok(()) + } +} + - WhiteSpace => css.push(' '), - Colon => css.push(':'), - Semicolon => css.push(';'), - Comma => css.push(','), - IncludeMatch => css.push_str("~="), - DashMatch => css.push_str("|="), - PrefixMatch => css.push_str("^="), - SuffixMatch => css.push_str("$="), - SubstringMatch => css.push_str("*="), - Column => css.push_str("||"), - CDO => css.push_str(""), - - Function(ref name, ref arguments) => { - serialize_identifier(name.as_slice(), css); - css.push('('); - arguments.iter().to_css_push(css); - css.push(')'); - }, - ParenthesisBlock(ref content) => { - css.push('('); - content.iter().to_css_push(css); - css.push(')'); - }, - SquareBracketBlock(ref content) => { - css.push('['); - content.iter().to_css_push(css); - css.push(']'); - }, - CurlyBracketBlock(ref content) => { - css.push('{'); - content.iter().map(|t| match *t { (ref c, _) => c }).to_css_push(css); - css.push('}'); - }, - - BadURL => css.push_str("url()"), - BadString => css.push_str("\"\n"), - CloseParenthesis => css.push(')'), - CloseSquareBracket => css.push(']'), - CloseCurlyBracket => css.push('}'), - } -} - - -pub fn serialize_identifier(value: &str, css: &mut String) { +pub fn serialize_identifier(value: &str, dest: &mut W) -> text_writer::Result +where W:TextWriter { // TODO: avoid decoding/re-encoding UTF-8? let mut iter = value.chars(); let mut c = iter.next().unwrap(); if c == '-' { c = match iter.next() { - None => { css.push_str("\\-"); return }, - Some(c) => { css.push('-'); c }, + None => return dest.write_str("\\-"), + Some(c) => { try!(dest.write_char('-')); c }, } }; - serialize_char(c, css, /* is_identifier_start = */ true); + try!(serialize_char(c, dest, /* is_identifier_start = */ true)); for c in iter { - serialize_char(c, css, /* is_identifier_start = */ false); + try!(serialize_char(c, dest, /* is_identifier_start = */ false)); } + Ok(()) } #[inline] -fn serialize_char(c: char, css: &mut String, is_identifier_start: bool) { +fn serialize_char(c: char, dest: &mut W, is_identifier_start: bool) -> text_writer::Result +where W: TextWriter { match c { - '0'...'9' if is_identifier_start => css.push_str(format!("\\3{} ", c).as_slice()), - '-' if is_identifier_start => css.push_str("\\-"), - '0'...'9' | 'A'...'Z' | 'a'...'z' | '_' | '-' => css.push(c), - _ if c > '\x7F' => css.push(c), - '\n' => css.push_str("\\A "), - '\r' => css.push_str("\\D "), - '\x0C' => css.push_str("\\C "), - _ => { css.push('\\'); css.push(c) }, + '0'...'9' if is_identifier_start => try!(dest.write_str(format!("\\3{} ", c).as_slice())), + '-' if is_identifier_start => try!(dest.write_str("\\-")), + '0'...'9' | 'A'...'Z' | 'a'...'z' | '_' | '-' => try!(dest.write_char(c)), + _ if c > '\x7F' => try!(dest.write_char(c)), + '\n' => try!(dest.write_str("\\A ")), + '\r' => try!(dest.write_str("\\D ")), + '\x0C' => try!(dest.write_str("\\C ")), + _ => { try!(dest.write_char('\\')); try!(dest.write_char(c)) }, + }; + Ok(()) +} + + +pub fn serialize_string(value: &str, dest: &mut W) -> text_writer::Result +where W: TextWriter { + try!(dest.write_char('"')); + try!(CssStringWriter::new(dest).write_str(value)); + try!(dest.write_char('"')); + Ok(()) +} + + +/// A `TextWriter` adaptor that escapes text for writing as a CSS string. +/// Quotes are not included. +/// +/// Typical usage: +/// +/// ```{rust,ignore} +/// fn write_foo(foo: &Foo, dest: &mut W) -> text_writer::Result where W: TextWriter { +/// try!(dest.write_char('"')); +/// { +/// let mut string_dest = CssStringWriter::new(dest); +/// // Write into string_dest... +/// } +/// try!(dest.write_char('"')); +/// Ok(()) +/// } +/// ``` +pub struct CssStringWriter<'a, W: 'a> { + inner: &'a mut W, +} + +impl<'a, W> CssStringWriter<'a, W> where W: TextWriter { + pub fn new(inner: &'a mut W) -> CssStringWriter<'a, W> { + CssStringWriter { inner: inner } } } +impl<'a, W> TextWriter for CssStringWriter<'a, W> where W: TextWriter { + fn write_str(&mut self, s: &str) -> text_writer::Result { + // TODO: avoid decoding/re-encoding UTF-8? + for c in s.chars() { + try!(self.write_char(c)) + } + Ok(()) + } -pub fn serialize_string(value: &str, css: &mut String) { - css.push('"'); - // TODO: avoid decoding/re-encoding UTF-8? - for c in value.chars() { + fn write_char(&mut self, c: char) -> text_writer::Result { match c { - '"' => css.push_str("\\\""), - '\\' => css.push_str("\\\\"), - '\n' => css.push_str("\\A "), - '\r' => css.push_str("\\D "), - '\x0C' => css.push_str("\\C "), - _ => css.push(c), + '"' => self.inner.write_str("\\\""), + '\\' => self.inner.write_str("\\\\"), + '\n' => self.inner.write_str("\\A "), + '\r' => self.inner.write_str("\\D "), + '\x0C' => self.inner.write_str("\\C "), + _ => self.inner.write_char(c), } } - css.push('"'); } -pub trait ToCss { - fn to_css(&mut self) -> String { - let mut css = String::new(); - self.to_css_push(&mut css); - css +impl<'a> ToCss for [ComponentValue] { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + component_values_to_css(self.iter(), dest) } - - fn to_css_push(&mut self, css: &mut String); } +impl<'a> ToCss for [Node] { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + let component_values = self.iter().map(|n| match n { &(ref c, _) => c }); + component_values_to_css(component_values, dest) + } +} -impl<'a, I: Iterator<&'a ComponentValue>> ToCss for I { - fn to_css_push(&mut self, css: &mut String) { - let mut previous = match self.next() { - None => return, - Some(first) => { first.to_css_push(css); first } - }; - macro_rules! matches( - ($value:expr, $($pattern:pat)|+) => ( - match $value { $($pattern)|+ => true, _ => false } - ); +fn component_values_to_css<'a, I, W>(mut iter: I, dest: &mut W) -> text_writer::Result +where I: Iterator<&'a ComponentValue>, W: TextWriter { + let mut previous = match iter.next() { + None => return Ok(()), + Some(first) => { try!(first.to_css(dest)); first } + }; + macro_rules! matches( + ($value:expr, $($pattern:pat)|+) => ( + match $value { $($pattern)|+ => true, _ => false } ); - // This does not borrow-check: for component_value in self { - loop { match self.next() { None => break, Some(component_value) => { - let (a, b) = (previous, component_value); - if ( - matches!(*a, Ident(..) | AtKeyword(..) | Hash(..) | IDHash(..) | - Dimension(..) | Delim('#') | Delim('-') | Number(..)) && - matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..) | - Number(..) | Percentage(..) | Dimension(..) | UnicodeRange(..)) - ) || ( - matches!(*a, Ident(..)) && - matches!(*b, ParenthesisBlock(..)) - ) || ( - matches!(*a, Ident(..) | AtKeyword(..) | Hash(..) | IDHash(..) | Dimension(..)) && - matches!(*b, Delim('-') | CDC) - ) || ( - matches!(*a, Delim('#') | Delim('-') | Number(..) | Delim('@')) && - matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..)) - ) || ( - matches!(*a, Delim('@')) && - matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..) | - UnicodeRange(..) | Delim('-')) - ) || ( - matches!(*a, UnicodeRange(..) | Delim('.') | Delim('+')) && - matches!(*b, Number(..) | Percentage(..) | Dimension(..)) - ) || ( - matches!(*a, UnicodeRange(..)) && - matches!(*b, Ident(..) | Function(..) | Delim('?')) - ) || (match (a, b) { (&Delim(a), &Delim(b)) => matches!((a, b), - ('#', '-') | - ('$', '=') | - ('*', '=') | - ('^', '=') | - ('~', '=') | - ('|', '=') | - ('|', '|') | - ('/', '*') - ), _ => false }) { - css.push_str("/**/") - } - // Skip whitespace when '\n' was previously written at the previous iteration. - if !matches!((previous, component_value), (&Delim('\\'), &WhiteSpace)) { - component_value.to_css_push(css); + ); + // This does not borrow-check: for component_value in iter { + loop { match iter.next() { None => break, Some(component_value) => { + let (a, b) = (previous, component_value); + if ( + matches!(*a, Ident(..) | AtKeyword(..) | Hash(..) | IDHash(..) | + Dimension(..) | Delim('#') | Delim('-') | Number(..)) && + matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..) | + Number(..) | Percentage(..) | Dimension(..) | UnicodeRange(..)) + ) || ( + matches!(*a, Ident(..)) && + matches!(*b, ParenthesisBlock(..)) + ) || ( + matches!(*a, Ident(..) | AtKeyword(..) | Hash(..) | IDHash(..) | Dimension(..)) && + matches!(*b, Delim('-') | CDC) + ) || ( + matches!(*a, Delim('#') | Delim('-') | Number(..) | Delim('@')) && + matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..)) + ) || ( + matches!(*a, Delim('@')) && + matches!(*b, Ident(..) | Function(..) | URL(..) | BadURL(..) | + UnicodeRange(..) | Delim('-')) + ) || ( + matches!(*a, UnicodeRange(..) | Delim('.') | Delim('+')) && + matches!(*b, Number(..) | Percentage(..) | Dimension(..)) + ) || ( + matches!(*a, UnicodeRange(..)) && + matches!(*b, Ident(..) | Function(..) | Delim('?')) + ) || (match (a, b) { (&Delim(a), &Delim(b)) => matches!((a, b), + ('#', '-') | + ('$', '=') | + ('*', '=') | + ('^', '=') | + ('~', '=') | + ('|', '=') | + ('|', '|') | + ('/', '*') + ), _ => false }) { + try!(dest.write_str("/**/")); + } + // Skip whitespace when '\n' was previously written at the previous iteration. + if !matches!((previous, component_value), (&Delim('\\'), &WhiteSpace)) { + try!(component_value.to_css(dest)); + } + if component_value == &Delim('\\') { + try!(dest.write_char('\n')); + } + previous = component_value; + }}} + Ok(()) +} + + +impl ToCss for Declaration { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + try!(dest.write_str(self.name.as_slice())); + try!(dest.write_char(':')); + try!(self.value.to_css(dest)); + Ok(()) + } +} + + +impl ToCss for [Declaration] { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + for declaration in self.iter() { + try!(declaration.to_css(dest)); + try!(dest.write_char(';')); + } + Ok(()) + } +} + + +impl ToCss for QualifiedRule { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + try!(self.prelude.to_css(dest)); + try!(dest.write_char('{')); + try!(self.block.to_css(dest)); + try!(dest.write_char('}')); + Ok(()) + } +} + + +impl ToCss for AtRule { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + try!(dest.write_char('@')); + try!(dest.write_str(self.name.as_slice())); + try!(self.prelude.to_css(dest)); + match self.block { + Some(ref block) => { + try!(dest.write_char('{')); + try!(block.to_css(dest)); + try!(dest.write_char('}')); } - if component_value == &Delim('\\') { - css.push('\n'); + None => try!(dest.write_char(';')) + } + Ok(()) + } +} + + +impl ToCss for DeclarationListItem { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match self { + &DeclarationListItem::Declaration(ref declaration) => declaration.to_css(dest), + &DeclarationListItem::AtRule(ref at_rule) => at_rule.to_css(dest), + } + } +} + + +impl ToCss for [DeclarationListItem] { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + for item in self.iter() { + try!(item.to_css(dest)); + match item { + &DeclarationListItem::AtRule(_) => {} + &DeclarationListItem::Declaration(_) => try!(dest.write_char(';')) } - previous = component_value; - }}} + } + Ok(()) + } +} + + +impl ToCss for Rule { + fn to_css(&self, dest: &mut W) -> text_writer::Result where W: TextWriter { + match self { + &Rule::QualifiedRule(ref rule) => rule.to_css(dest), + &Rule::AtRule(ref rule) => rule.to_css(dest), + } } } diff --git a/src/tests.rs b/src/tests.rs index 379397c8..6e50a81d 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -273,7 +273,7 @@ fn nth() { fn serializer() { run_json_tests(include_str!("css-parsing-tests/component_value_list.json"), |input| { let component_values = tokenize(input).map(|(c, _)| c).collect::>(); - let serialized = component_values.iter().to_css(); + let serialized = component_values.to_css_string(); tokenize(serialized.as_slice()).map(|(c, _)| c).collect::>() }); }