#![warn(missing_docs)]
use nargo_types::{Error, Result, Span};
use notify::{self, Watcher};
use oak_core::{ParseSession, parse_one_pass};
use oak_css::{CssLanguage, CssParser};
use oak_scss::{ScssLanguage, ScssParser};
use oak_stylus::{StylusLanguage, StylusParser};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PreprocessorType {
Css,
Scss,
Less,
Stylus,
}
use std::collections::HashMap;
#[derive(Default)]
pub struct StyleProcessor {
pub minify: bool,
pub remove_unused: bool,
pub preprocessor: Option<PreprocessorType>,
cache: HashMap<(String, PreprocessorType), String>,
minify_cache: HashMap<String, String>,
}
impl StyleProcessor {
pub fn new() -> Self {
Self { minify: false, remove_unused: false, preprocessor: None, cache: HashMap::new(), minify_cache: HashMap::new() }
}
pub fn with_minify(mut self, minify: bool) -> Self {
self.minify = minify;
self
}
pub fn with_remove_unused(mut self, remove_unused: bool) -> Self {
self.remove_unused = remove_unused;
self
}
pub fn with_preprocessor(mut self, preprocessor: PreprocessorType) -> Self {
self.preprocessor = Some(preprocessor);
self
}
pub fn process(&mut self, css: &str, used_selectors: Option<&Vec<String>>) -> Result<String> {
let mut processed = css.to_string();
if let Some(preprocessor) = self.preprocessor.clone() {
processed = self.process_preprocessor(&processed, &preprocessor)?;
}
if self.remove_unused && used_selectors.is_some() {
processed = self.remove_unused_styles(&processed, used_selectors.unwrap())?;
}
if self.minify {
processed = self.minify_css(&processed);
}
else {
processed = format!("/* Processed by Nargo Style Processor */\n{}", processed);
}
Ok(processed)
}
fn process_preprocessor(&mut self, code: &str, preprocessor: &PreprocessorType) -> Result<String> {
let cache_key = (code.to_string(), preprocessor.clone());
Self::get_or_compute(&mut self.cache, cache_key, || {
match preprocessor {
PreprocessorType::Css => {
let language = CssLanguage::default();
let parser = CssParser::new(&language);
let mut session = ParseSession::new(1024);
let result = parse_one_pass(&parser, code, &mut session);
result.result.map_err(|e| Error::external_error("oak-css".to_string(), e.to_string(), Span::default()))?;
Ok(code.to_string())
}
PreprocessorType::Scss => {
let language = ScssLanguage::default();
let parser = ScssParser::new(&language);
let mut session = ParseSession::new(1024);
let result = parse_one_pass(&parser, code, &mut session);
result.result.map_err(|e| Error::external_error("oak-scss".to_string(), e.to_string(), Span::default()))?;
Ok(code.to_string())
}
PreprocessorType::Less => Ok(code.to_string()),
PreprocessorType::Stylus => {
let language = StylusLanguage::default();
let parser = StylusParser::new(&language);
let mut session = ParseSession::new(1024);
let result = parse_one_pass(&parser, code, &mut session);
result.result.map_err(|e| Error::external_error("oak-stylus".to_string(), e.to_string(), Span::default()))?;
Ok(code.to_string())
}
}
})
}
fn get_or_compute<K, V, F>(cache: &mut HashMap<K, V>, key: K, compute: F) -> Result<V>
where
K: std::hash::Hash + Eq,
V: Clone,
F: FnOnce() -> Result<V>,
{
if let Some(cached) = cache.get(&key) {
Ok(cached.clone())
}
else {
let value = compute()?;
cache.insert(key, value.clone());
Ok(value)
}
}
fn remove_unused_styles(&self, css: &str, used_selectors: &Vec<String>) -> Result<String> {
let mut result = String::new();
let mut remaining = css.to_string();
while !remaining.is_empty() {
if let Some(start_idx) = remaining.find('{') {
let selector = remaining[..start_idx].trim().to_string();
let mut brace_count = 1;
let mut end_idx = start_idx + 1;
while end_idx < remaining.len() && brace_count > 0 {
if remaining.chars().nth(end_idx) == Some('{') {
brace_count += 1;
}
else if remaining.chars().nth(end_idx) == Some('}') {
brace_count -= 1;
}
end_idx += 1;
}
let rule = remaining[..end_idx].to_string();
if self.is_selector_used(&selector, used_selectors) {
result.push_str(&rule);
result.push_str("\n");
}
remaining = remaining[end_idx..].trim_start().to_string();
}
else {
result.push_str(&remaining);
break;
}
}
Ok(result)
}
fn is_selector_used(&self, selector: &str, used_selectors: &Vec<String>) -> bool {
let selectors = selector.split(',').map(|s| s.trim());
for s in selectors {
if used_selectors.contains(&s.to_string()) {
return true;
}
}
false
}
fn minify_css(&mut self, css: &str) -> String {
if let Some(cached) = self.minify_cache.get(css) {
return cached.clone();
}
let minified = self.perform_minification(css);
self.minify_cache.insert(css.to_string(), minified.clone());
minified
}
fn perform_minification(&self, css: &str) -> String {
let mut result = String::with_capacity(css.len());
let mut in_comment = false;
let mut in_string = false;
let mut string_delimiter = '"';
let mut chars = css.chars();
while let Some(c) = chars.next() {
if in_comment {
if c == '*' {
if let Some(next) = chars.next() {
if next == '/' {
in_comment = false;
}
}
}
continue;
}
if in_string {
result.push(c);
if c == string_delimiter {
let is_escaped = self.is_escaped(&result);
if !is_escaped {
in_string = false;
}
}
continue;
}
match c {
'"' | '\'' => {
result.push(c);
in_string = true;
string_delimiter = c;
}
'/' => {
if let Some(next) = chars.next() {
if next == '*' {
in_comment = true;
}
else {
result.push('/');
result.push(next);
}
}
else {
result.push('/');
}
}
' ' | '\t' | '\n' | '\r' => {
}
'#' => {
result.push('#');
let mut hex_chars = String::new();
for _ in 0..6 {
if let Some(next) = chars.next() {
if next.is_ascii_hexdigit() {
hex_chars.push(next);
}
else {
result.push_str(&hex_chars);
result.push(next);
break;
}
}
else {
result.push_str(&hex_chars);
break;
}
}
if hex_chars.len() == 6 {
if let Some(compressed) = self.compress_color(&hex_chars) {
result.push_str(&compressed);
}
else {
result.push_str(&hex_chars);
}
}
else if !hex_chars.is_empty() {
result.push_str(&hex_chars);
}
}
_ => {
result.push(c);
}
}
}
let trimmed = self.remove_unnecessary_semicolons(&result);
trimmed
}
fn is_escaped(&self, result: &String) -> bool {
let mut backslash_count = 0;
for ch in result.chars().rev() {
if ch == '\\' {
backslash_count += 1;
}
else {
break;
}
}
backslash_count % 2 == 1
}
fn compress_color(&self, color: &str) -> Option<String> {
if color.len() != 6 {
return None;
}
let chars: Vec<char> = color.chars().collect();
if chars[0] == chars[1] && chars[2] == chars[3] && chars[4] == chars[5] { Some(format!("{}{}{}", chars[0], chars[2], chars[4])) } else { None }
}
fn remove_unnecessary_semicolons(&self, css: &str) -> String {
let mut result = String::with_capacity(css.len());
let mut chars = css.chars().peekable();
while let Some(c) = chars.next() {
result.push(c);
}
result
}
}
pub fn process(css: &str) -> Result<String> {
let mut processor = StyleProcessor::new();
processor.process(css, None)
}
pub fn process_with_used_selectors(css: &str, used_selectors: &Vec<String>) -> Result<String> {
let mut processor = StyleProcessor::new().with_remove_unused(true);
processor.process(css, Some(used_selectors))
}
pub fn minify(css: &str) -> Result<String> {
let mut processor = StyleProcessor::new().with_minify(true);
processor.process(css, None)
}
pub fn optimize(css: &str, used_selectors: &Vec<String>) -> Result<String> {
let mut processor = StyleProcessor::new().with_remove_unused(true).with_minify(true);
processor.process(css, Some(used_selectors))
}
pub fn process_with_preprocessor(css: &str, preprocessor: PreprocessorType) -> Result<String> {
let mut processor = StyleProcessor::new().with_preprocessor(preprocessor);
processor.process(css, None)
}
pub fn watch(processor: &StyleProcessor, file_path: &str, output_path: &str, callback: Option<fn()>) -> Result<()> {
let minify = processor.minify;
let remove_unused = processor.remove_unused;
let preprocessor = processor.preprocessor.clone();
let file_path_str = file_path.to_string();
let file_path_closure = file_path_str.clone();
let output_path = output_path.to_string();
match notify::recommended_watcher(move |res: std::result::Result<notify::Event, notify::Error>| {
match res {
Ok(event) => {
if let notify::EventKind::Modify(_) = event.kind {
if let Ok(css) = std::fs::read_to_string(&file_path_closure) {
let mut processor = StyleProcessor::new().with_minify(minify).with_remove_unused(remove_unused).with_preprocessor(preprocessor.clone().unwrap_or(PreprocessorType::Css));
if let Ok(processed) = processor.process(&css, None) {
if std::fs::write(&output_path, processed).is_ok() {
println!("Style updated: {}", output_path);
if let Some(cb) = callback {
cb();
}
}
}
}
}
}
Err(e) => println!("watch error: {:?}", e),
}
}) {
Ok(mut watcher) => {
if let Err(err) = watcher.watch(Path::new(&file_path_str), notify::RecursiveMode::NonRecursive) {
return Err(Error::external_error("notify".to_string(), err.to_string(), Span::default()));
}
Ok(())
}
Err(err) => Err(Error::external_error("notify".to_string(), err.to_string(), Span::default())),
}
}