use crate::GuffError;
use grass::{
Options,
OutputStyle,
};
use lightningcss::{
stylesheet::{
MinifyOptions,
ParserFlags,
ParserOptions,
PrinterOptions,
StyleSheet,
},
targets::{
Browsers,
Features,
Targets,
},
};
use std::path::Path;
use trimothy::TrimMut;
#[derive(Debug)]
pub struct Css<'a> {
path: &'a str,
css: String,
}
impl From<&str> for Css<'_> {
fn from(src: &str) -> Self { Self::from(src.to_owned()) }
}
impl From<String> for Css<'_> {
fn from(css: String) -> Self {
Self {
path: "stylesheet.css",
css,
}
}
}
impl<'a> TryFrom<&'a Path> for Css<'a> {
type Error = GuffError;
fn try_from(src: &'a Path) -> Result<Self, Self::Error> {
let path: &str = src.as_os_str().to_str().ok_or(GuffError::PathUtf8)?;
let css: String = match StyleKind::try_from(path.as_bytes())? {
StyleKind::Scss => grass::from_path(
path,
&Options::default()
.style(OutputStyle::Expanded)
.quiet(true)
)?,
StyleKind::Css => std::fs::read_to_string(path)
.map_err(|_| GuffError::SourceInvalid)?
};
Ok(Self { path, css })
}
}
impl Css<'_> {
fn prepare(&mut self) {
self.css.retain(|c| c != '\u{feff}');
self.css.trim_mut();
}
pub fn minified(mut self, browsers: Option<Browsers>) -> Result<String, GuffError> {
self.prepare();
let Self { path, css } = self;
if css.is_empty() { Ok(css) }
else {
let mut stylesheet = StyleSheet::parse(&css, ParserOptions {
filename: path.to_owned(),
css_modules: None,
source_index: 0,
error_recovery: false,
warnings: None,
flags: ParserFlags::NESTING,
})?;
stylesheet.minify(MinifyOptions {
targets: Targets {
browsers,
include: Features::MediaRangeSyntax,
exclude: Features::empty(),
},
..MinifyOptions::default()
})?;
let out = stylesheet.to_css(PrinterOptions {
minify: true,
targets: Targets {
browsers,
include: Features::MediaRangeSyntax,
exclude: Features::empty(),
},
..PrinterOptions::default()
})?;
Ok(out.code)
}
}
pub fn take(mut self) -> Result<String, GuffError> {
self.prepare();
let Self { path, css } = self;
if ! css.is_empty() {
StyleSheet::parse(&css, ParserOptions {
filename: path.to_owned(),
css_modules: None,
source_index: 0,
error_recovery: false,
warnings: None,
flags: ParserFlags::NESTING,
})?;
}
Ok(css)
}
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
enum StyleKind {
Css,
Scss,
}
impl TryFrom<&[u8]> for StyleKind {
type Error = GuffError;
fn try_from(src: &[u8]) -> Result<Self, Self::Error> {
if let [src @ .., b's' | b'S', b's' | b'S'] = src {
match src {
[.., 0..=46 | 48..=91 | 93..=255, b'.', b'c' | b'C'] => Ok(Self::Css),
[.., 0..=46 | 48..=91 | 93..=255, b'.', b's' | b'S', b'a' | b'c' | b'A' | b'C'] => Ok(Self::Scss),
_ => Err(GuffError::PathExt),
}
}
else { Err(GuffError::PathExt) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_stylekind() {
for (file, expected) in [
("/foo/bar.css", Some(StyleKind::Css)),
("/foo/bar.sass", Some(StyleKind::Scss)),
("/foo/bar.scss", Some(StyleKind::Scss)),
("a.css", Some(StyleKind::Css)),
("a.sass", Some(StyleKind::Scss)),
("a.scss", Some(StyleKind::Scss)),
(".css", None),
(".sass", None),
(".scss", None),
("/foo/.css", None),
("/foo/.sass", None),
("/foo/.scss", None),
("/foo/bar.jpeg", None),
] {
assert_eq!(StyleKind::try_from(file.as_bytes()).ok(), expected);
let file = file.to_uppercase();
assert_eq!(StyleKind::try_from(file.as_bytes()).ok(), expected);
}
}
}