askama-minify 0.3.4

A procedural macro for minifying Askama templates at compile time
Documentation
use super::template::try_push_askama_template;
use super::util::{is_html_whitespace as is_css_whitespace, trim_trailing_space};
use std::borrow::Cow;

#[cfg(feature = "advanced-css")]
use super::template::contains_askama_template;
#[cfg(feature = "advanced-css")]
use super::util::contains_end_tag;
#[cfg(feature = "advanced-css")]
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet};

pub(super) fn minify_css(css_code: &str) -> Cow<'_, str> {
    #[cfg(feature = "advanced-css")]
    {
        if !contains_askama_template(css_code) {
            let stylesheet = StyleSheet::parse(css_code, ParserOptions::default());

            if let Ok(mut sheet) = stylesheet {
                if sheet.minify(MinifyOptions::default()).is_err() {
                    return Cow::Borrowed(css_code);
                }
                let result = sheet.to_css(PrinterOptions {
                    minify: true,
                    ..PrinterOptions::default()
                });

                if let Ok(output) = result {
                    if contains_end_tag(&output.code, "style") {
                        return Cow::Borrowed(css_code);
                    }
                    return Cow::Owned(output.code);
                }
            }
        }
    }

    minify_css_conservative(css_code)
}

fn minify_css_conservative(css_code: &str) -> Cow<'_, str> {
    if css_code.contains("--") {
        return Cow::Borrowed(css_code);
    }

    let mut result = String::with_capacity(css_code.len());
    let mut chars = css_code.chars();
    let mut in_string = false;
    let mut string_char = '\0';
    let mut escaped = false;
    let mut last_was_space = false;
    let mut last_significant_char = None;

    while let Some(ch) = chars.next() {
        if let Some(last_ch) = try_push_askama_template(ch, &mut chars, &mut result) {
            last_significant_char = Some(last_ch);
            last_was_space = false;
            continue;
        }

        if in_string {
            result.push(ch);

            if escaped {
                escaped = false;
            } else if ch == '\\' {
                escaped = true;
            } else if ch == string_char {
                in_string = false;
            }

            if !is_css_whitespace(ch) {
                last_significant_char = Some(ch);
            }
            last_was_space = false;
            continue;
        }

        if ch == '"' || ch == '\'' {
            in_string = true;
            string_char = ch;
            result.push(ch);
            last_significant_char = Some(ch);
            last_was_space = false;
            continue;
        }

        if ch == '/' && chars.clone().next() == Some('*') {
            chars.next();

            while let Some(comment_ch) = chars.next() {
                if comment_ch == '*' && chars.clone().next() == Some('/') {
                    chars.next();
                    break;
                }
            }

            // An empty comment separates tokens without adding a descendant combinator.
            if !result.is_empty()
                && !last_was_space
                && !css_space_is_redundant_after(last_significant_char)
                && chars
                    .clone()
                    .next()
                    .is_some_and(|ch| !is_css_whitespace(ch))
            {
                result.push_str("/**/");
            }
            continue;
        }

        if is_css_whitespace(ch) {
            if !css_space_is_redundant_after(last_significant_char)
                && !last_was_space
                && !result.is_empty()
            {
                result.push(' ');
                last_was_space = true;
            }
            continue;
        }

        if matches!(ch, '{' | '}' | ';' | ',') {
            trim_trailing_space(&mut result);
            if ch == '}' && result.ends_with(';') {
                result.pop();
            }
            result.push(ch);
            last_significant_char = Some(ch);
            last_was_space = false;
            continue;
        }

        result.push(ch);
        last_significant_char = Some(ch);
        last_was_space = false;
    }

    trim_trailing_space(&mut result);
    Cow::Owned(result)
}

fn css_space_is_redundant_after(previous: Option<char>) -> bool {
    matches!(previous, Some('{' | ':' | ';' | ',' | '('))
}