From a5f38235213c92d438eb907c1d84b77b36be7e8f Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 8 Jul 2021 22:05:23 +0800 Subject: [PATCH] add compressed output (#33) --- src/lib.rs | 8 +- src/main.rs | 15 ++- src/output.rs | 258 ++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 232 insertions(+), 49 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 71a96f9..19d247d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,7 @@ mod utils; mod value; #[non_exhaustive] -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum OutputStyle { /// The default style, this mode writes each /// selector and declaration on its own line. @@ -313,7 +313,7 @@ pub fn from_path(p: &str, options: &Options) -> Result { Css::from_stmts(stmts, false, options.allows_charset) .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))? - .pretty_print(&map) + .pretty_print(&map, options.style) .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages)) } @@ -359,7 +359,7 @@ pub fn from_string(p: String, options: &Options) -> Result { Css::from_stmts(stmts, false, options.allows_charset) .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))? - .pretty_print(&map) + .pretty_print(&map, options.style) .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages)) } @@ -396,6 +396,6 @@ pub fn from_string(p: String) -> std::result::Result { Ok(Css::from_stmts(stmts, false, true) .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())? - .pretty_print(&map) + .pretty_print(&map, options.style) .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?) } diff --git a/src/main.rs b/src/main.rs index 404214d..7a83465 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,9 @@ use std::{ use clap::{arg_enum, App, AppSettings, Arg}; #[cfg(not(feature = "wasm"))] -use grass::{from_path, from_string, Options}; +use grass::{from_path, from_string, Options, OutputStyle}; +// TODO remove this arg_enum! { #[derive(PartialEq, Debug)] pub enum Style { @@ -58,11 +59,10 @@ fn main() -> std::io::Result<()> { ) .arg( Arg::with_name("STYLE") - .short("s") // this is required for compatibility with ruby sass - .short("t") + .short("t") // FIXME change this to short_alias later + .short("s") .long("style") - .hidden(true) .help("Minified or expanded output") .default_value("expanded") .case_insensitive(true) @@ -183,8 +183,15 @@ fn main() -> std::io::Result<()> { .values_of("LOAD_PATH") .map_or_else(Vec::new, |vals| vals.map(Path::new).collect()); + let style = match matches.value_of("STYLE").unwrap() { + "expanded" => OutputStyle::Expanded, + "compressed" => OutputStyle::Compressed, + _ => unreachable!(), + }; + let options = &Options::default() .load_paths(&load_paths) + .style(style) .quiet(matches.is_present("QUIET")) .unicode_error_messages(!matches.is_present("NO_UNICODE")) .allows_charset(!matches.is_present("NO_CHARSET")); diff --git a/src/output.rs b/src/output.rs index 30fabd0..bbf16b4 100644 --- a/src/output.rs +++ b/src/output.rs @@ -11,8 +11,9 @@ use crate::{ }, error::SassResult, parse::Stmt, - selector::Selector, + selector::{ComplexSelector, ComplexSelectorComponent, Selector}, style::Style, + OutputStyle, }; #[derive(Debug, Clone)] @@ -233,35 +234,224 @@ impl Css { Ok(self) } - pub fn pretty_print(mut self, map: &CodeMap) -> SassResult { - let mut string = Vec::new(); + pub fn pretty_print(self, map: &CodeMap, style: OutputStyle) -> SassResult { + let mut buf = Vec::new(); let allows_charset = self.allows_charset; - self._inner_pretty_print(&mut string, map, 0)?; - if allows_charset && string.iter().any(|s| !s.is_ascii()) { - return Ok(format!("@charset \"UTF-8\";\n{}", unsafe { - String::from_utf8_unchecked(string) - })); + match style { + OutputStyle::Compressed => { + CompressedFormatter::default().write_css(&mut buf, self, map)?; + } + OutputStyle::Expanded => ExpandedFormatter::default().write_css(&mut buf, self, map)?, } - Ok(unsafe { String::from_utf8_unchecked(string) }) + // TODO: check for this before writing + let show_charset = allows_charset && buf.iter().any(|s| !s.is_ascii()); + let out = unsafe { String::from_utf8_unchecked(buf) }; + Ok(if show_charset { + match style { + OutputStyle::Compressed => format!("@charset \"UTF-8\";{}", out), + OutputStyle::Expanded => format!("@charset \"UTF-8\";\n{}", out), + } + } else { + out + }) + } +} + +trait Formatter { + fn write_css(&mut self, buf: &mut Vec, css: Css, map: &CodeMap) -> SassResult<()>; +} + +#[derive(Debug, Default)] +struct CompressedFormatter {} + +impl Formatter for CompressedFormatter { + fn write_css(&mut self, buf: &mut Vec, mut css: Css, map: &CodeMap) -> SassResult<()> { + for block in mem::take(&mut css.blocks) { + match block { + Toplevel::RuleSet(selector, styles) => { + if styles.is_empty() { + continue; + } + + let mut complexes = selector.0.components.iter().filter(|c| !c.is_invisible()); + if let Some(complex) = complexes.next() { + self.write_complex(buf, complex)?; + } + for complex in complexes { + write!(buf, ",")?; + self.write_complex(buf, complex)?; + } + + write!(buf, "{{")?; + self.write_block_entry(buf, &styles)?; + write!(buf, "}}")?; + } + Toplevel::KeyframesRuleSet(selectors, styles) => { + if styles.is_empty() { + continue; + } + + let mut selectors = selectors.iter(); + if let Some(selector) = selectors.next() { + match selector { + KeyframesSelector::To => write!(buf, "to")?, + KeyframesSelector::From => write!(buf, "from")?, + KeyframesSelector::Percent(p) => write!(buf, "{}%", p)?, + } + } + for selector in selectors { + match selector { + KeyframesSelector::To => write!(buf, ",to")?, + KeyframesSelector::From => write!(buf, ",from")?, + KeyframesSelector::Percent(p) => write!(buf, ",{}%", p)?, + } + } + + write!(buf, "{{")?; + self.write_block_entry(buf, &styles)?; + write!(buf, "}}")?; + } + Toplevel::MultilineComment(s) => { + write!(buf, "/*{}*/", s)?; + } + Toplevel::Import(s) => { + write!(buf, "@import {};", s)?; + } + Toplevel::UnknownAtRule(u) => { + let ToplevelUnknownAtRule { params, name, body } = *u; + + if params.is_empty() { + write!(buf, "@{}", name)?; + } else { + write!(buf, "@{} {}", name, params)?; + } + + if body.is_empty() { + write!(buf, ";")?; + continue; + } + + write!(buf, "{{")?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; + write!(buf, "}}")?; + } + Toplevel::Keyframes(k) => { + let Keyframes { rule, name, body } = *k; + + write!(buf, "@{}", rule)?; + + if !name.is_empty() { + write!(buf, " {}", name)?; + } + + if body.is_empty() { + write!(buf, "{{}}")?; + continue; + } + + write!(buf, "{{")?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; + write!(buf, "}}")?; + } + Toplevel::Supports { params, body } => { + if params.is_empty() { + write!(buf, "@supports")?; + } else { + write!(buf, "@supports {}", params)?; + } + + if body.is_empty() { + write!(buf, ";")?; + continue; + } + + write!(buf, "{{")?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; + write!(buf, "}}")?; + } + Toplevel::Media { query, body } => { + if body.is_empty() { + continue; + } + + write!(buf, "@media {}{{", query)?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; + write!(buf, "}}")?; + } + Toplevel::Style(style) => { + let value = style.value.node.to_css_string(style.value.span)?; + write!(buf, "{}:{};", style.property, value)?; + } + Toplevel::Newline => {} + } + } + Ok(()) + } +} + +// this could be a trait implemented on value itself +#[allow(clippy::unused_self)] +impl CompressedFormatter { + fn write_complex(&self, buf: &mut Vec, complex: &ComplexSelector) -> SassResult<()> { + let mut was_compound = false; + for component in &complex.components { + match component { + ComplexSelectorComponent::Compound(c) if was_compound => write!(buf, " {}", c)?, + ComplexSelectorComponent::Compound(c) => write!(buf, "{}", c)?, + ComplexSelectorComponent::Combinator(c) => write!(buf, "{}", c)?, + } + was_compound = matches!(component, ComplexSelectorComponent::Compound(_)); + } + Ok(()) } - fn _inner_pretty_print( - &mut self, - buf: &mut Vec, - map: &CodeMap, - nesting: usize, - ) -> SassResult<()> { + fn write_block_entry(&self, buf: &mut Vec, styles: &[BlockEntry]) -> SassResult<()> { + let mut styles = styles.iter(); + if let Some(style) = styles.next() { + match style { + BlockEntry::Style(s) => { + let value = s.value.node.to_css_string(s.value.span)?; + write!(buf, "{}:{}", s.property, value)?; + } + BlockEntry::MultilineComment(s) => write!(buf, "/*{}*/", s)?, + } + } + for style in styles { + match style { + BlockEntry::Style(s) => { + let value = s.value.node.to_css_string(s.value.span)?; + write!(buf, ";{}:{}", s.property, value)?; + } + BlockEntry::MultilineComment(s) => write!(buf, "/*{}*/", s)?, + } + } + Ok(()) + } +} + +#[derive(Debug, Default)] +struct ExpandedFormatter { + nesting: usize, +} + +impl Formatter for ExpandedFormatter { + fn write_css(&mut self, buf: &mut Vec, mut css: Css, map: &CodeMap) -> SassResult<()> { let mut has_written = false; - let padding = vec![' '; nesting * 2].iter().collect::(); + let padding = " ".repeat(self.nesting); let mut should_emit_newline = false; - for block in mem::take(&mut self.blocks) { + self.nesting += 1; + for block in mem::take(&mut css.blocks) { match block { Toplevel::RuleSet(selector, styles) => { if styles.is_empty() { continue; } has_written = true; - if should_emit_newline && !self.in_at_rule { + if should_emit_newline && !css.in_at_rule { should_emit_newline = false; writeln!(buf)?; } @@ -319,12 +509,8 @@ impl Css { } writeln!(buf, " {{")?; - - Css::from_stmts(body, true, self.allows_charset)?._inner_pretty_print( - buf, - map, - nesting + 1, - )?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; writeln!(buf, "{}}}", padding)?; } Toplevel::Keyframes(k) => { @@ -346,12 +532,8 @@ impl Css { } writeln!(buf, " {{")?; - - Css::from_stmts(body, true, self.allows_charset)?._inner_pretty_print( - buf, - map, - nesting + 1, - )?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; writeln!(buf, "{}}}", padding)?; } Toplevel::Supports { params, body } => { @@ -372,12 +554,8 @@ impl Css { } writeln!(buf, " {{")?; - - Css::from_stmts(body, true, self.allows_charset)?._inner_pretty_print( - buf, - map, - nesting + 1, - )?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; writeln!(buf, "{}}}", padding)?; } Toplevel::Media { query, body } => { @@ -386,11 +564,8 @@ impl Css { } writeln!(buf, "{}@media {} {{", padding, query)?; - Css::from_stmts(body, true, self.allows_charset)?._inner_pretty_print( - buf, - map, - nesting + 1, - )?; + let css = Css::from_stmts(body, true, css.allows_charset)?; + self.write_css(buf, css, map)?; writeln!(buf, "{}}}", padding)?; } Toplevel::Style(s) => { @@ -404,6 +579,7 @@ impl Css { } } } + self.nesting -= 1; Ok(()) } }