askama-minify 0.3.1

A procedural macro for minifying Askama templates at compile time
Documentation
use super::css::minify_css;
use super::js::minify_js;
use super::template::try_push_askama_template;
use super::util::trim_trailing_whitespace;

pub(crate) fn minify_html(content: &str) -> String {
    let mut result = String::with_capacity(content.len());
    let mut chars = content.chars().peekable();
    let mut in_tag = false;
    let mut in_script = false;
    let mut in_style = false;
    let mut in_pre = false;
    let mut in_textarea = false;
    let mut attr_quote = None;
    let mut last_was_space = false;
    let mut tag_name = String::new();
    let mut script_content = String::new();
    let mut style_content = String::new();

    while let Some(ch) = chars.next() {
        if !in_tag && (in_script || in_style) {
            if ch == '<' {
                let is_closing_tag = (in_script && starts_with_closing_tag(&chars, "script"))
                    || (in_style && starts_with_closing_tag(&chars, "style"));

                if !is_closing_tag && in_script {
                    script_content.push(ch);
                    last_was_space = false;
                    continue;
                } else if !is_closing_tag {
                    style_content.push(ch);
                    last_was_space = false;
                    continue;
                }
            } else {
                let target = if in_script {
                    &mut script_content
                } else {
                    &mut style_content
                };

                if try_push_askama_template(ch, &mut chars, target).is_some() {
                    last_was_space = false;
                    continue;
                }

                target.push(ch);
                last_was_space = false;
                continue;
            }
        }

        if try_push_askama_template(ch, &mut chars, &mut result).is_some() {
            last_was_space = false;
            continue;
        }

        if !in_script && !in_style && ch == '<' && starts_with_html_comment(&chars) {
            chars.next();
            chars.next();
            chars.next();

            let mut comment_end = String::new();
            for c in chars.by_ref() {
                comment_end.push(c);
                if comment_end.ends_with("-->") {
                    break;
                }

                if comment_end.len() > 3 {
                    let keep_from = comment_end.len() - 3;
                    comment_end.drain(..keep_from);
                }
            }
            last_was_space = result.ends_with(' ');
            continue;
        }

        if in_tag {
            if let Some(quote) = attr_quote {
                result.push(ch);
                if ch == quote {
                    attr_quote = None;
                }
                last_was_space = false;
                continue;
            }

            if ch == '"' || ch == '\'' {
                attr_quote = Some(ch);
                result.push(ch);
                last_was_space = false;
                continue;
            }

            if ch == '>' {
                in_tag = false;
                result.push(ch);
                last_was_space = false;
                continue;
            }

            if ch.is_whitespace() {
                if !last_was_space {
                    result.push(' ');
                    last_was_space = true;
                }
            } else {
                result.push(ch);
                last_was_space = false;
            }
            continue;
        }

        if ch == '<' {
            in_tag = true;
            attr_quote = None;
            tag_name.clear();
            result.push(ch);
            last_was_space = false;

            while let Some(&next_ch) = chars.peek() {
                if next_ch.is_whitespace() || next_ch == '>' {
                    break;
                }
                if next_ch == '/' && !tag_name.is_empty() {
                    break;
                }
                tag_name.push(chars.next().unwrap());
            }

            if tag_name.eq_ignore_ascii_case("/script") {
                result.pop();
                if !script_content.is_empty() {
                    let minified = minify_js(&script_content);
                    result.push_str(&minified);
                }
                script_content.clear();
                in_script = false;
                result.push('<');
            } else if tag_name.eq_ignore_ascii_case("/style") {
                result.pop();
                if !style_content.is_empty() {
                    let minified = minify_css(&style_content);
                    result.push_str(&minified);
                }
                style_content.clear();
                in_style = false;
                result.push('<');
            } else if tag_name.eq_ignore_ascii_case("/pre") {
                in_pre = false;
            } else if tag_name.eq_ignore_ascii_case("/textarea") {
                in_textarea = false;
            }

            result.push_str(&tag_name);

            if tag_name.eq_ignore_ascii_case("script") {
                in_script = true;
                script_content.clear();
            } else if tag_name.eq_ignore_ascii_case("style") {
                in_style = true;
                style_content.clear();
            } else if tag_name.eq_ignore_ascii_case("pre") {
                in_pre = true;
            } else if tag_name.eq_ignore_ascii_case("textarea") {
                in_textarea = true;
            }
            continue;
        }

        if in_pre || in_textarea {
            result.push(ch);
            last_was_space = false;
        } else if ch.is_whitespace() {
            if !last_was_space && !result.is_empty() {
                result.push(' ');
                last_was_space = true;
            }
        } else {
            result.push(ch);
            last_was_space = false;
        }
    }

    trim_trailing_whitespace(&mut result);
    result
}

fn starts_with_html_comment(chars: &std::iter::Peekable<std::str::Chars<'_>>) -> bool {
    let mut lookahead = chars.clone();

    matches!(
        (lookahead.next(), lookahead.next(), lookahead.next()),
        (Some('!'), Some('-'), Some('-'))
    )
}

fn starts_with_closing_tag(chars: &std::iter::Peekable<std::str::Chars<'_>>, tag: &str) -> bool {
    let mut lookahead = chars.clone();

    if lookahead.next() != Some('/') {
        return false;
    }

    for expected in tag.chars() {
        let Some(actual) = lookahead.next() else {
            return false;
        };

        if !actual.eq_ignore_ascii_case(&expected) {
            return false;
        }
    }

    matches!(lookahead.peek(), Some(ch) if ch.is_whitespace() || *ch == '>')
}