lariv-rs 0.1.0

Compile-time plugin web application framework built on Axum, SeaORM, Maud, and HTMX
Documentation
//! Theme asset inject/strip.

use crate::grapesjs::GrapesJsTheme;

fn theme_head_html(theme_id: &str, theme: &GrapesJsTheme) -> String {
    if theme_id.is_empty() {
        return String::new();
    }
    let escaped_id = html_escape(theme_id);
    let mut b = String::new();
    for src in &theme.scripts {
        let src = src.trim();
        if src.is_empty() {
            continue;
        }
        b.push_str("<script src=\"");
        b.push_str(&html_escape(src));
        b.push_str("\" data-lariv-theme=\"");
        b.push_str(&escaped_id);
        b.push_str("\"></script>\n");
    }
    for href in &theme.stylesheets {
        let href = href.trim();
        if href.is_empty() {
            continue;
        }
        b.push_str("<link rel=\"stylesheet\" href=\"");
        b.push_str(&html_escape(href));
        b.push_str("\" data-lariv-theme=\"");
        b.push_str(&escaped_id);
        b.push_str("\">\n");
    }
    let css = theme.css.trim();
    if !css.is_empty() {
        b.push_str("<style");
        if let Some(css_type) = theme.css_type.as_deref().filter(|s| !s.is_empty()) {
            b.push_str(" type=\"");
            b.push_str(&html_escape(css_type));
            b.push_str("\"");
        }
        b.push_str(" data-lariv-theme=\"");
        b.push_str(&escaped_id);
        b.push_str("\">\n");
        b.push_str(css);
        b.push_str("\n</style>\n");
    }
    b
}

fn theme_body_js_html(theme_id: &str, theme: &GrapesJsTheme) -> String {
    if theme_id.is_empty() {
        return String::new();
    }
    let js = theme.js.trim();
    if js.is_empty() {
        return String::new();
    }
    let mut b = String::new();
    b.push_str("<script data-lariv-theme=\"");
    b.push_str(&html_escape(theme_id));
    b.push_str("\">\n");
    b.push_str(js);
    b.push_str("\n</script>\n");
    b
}

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

fn strip_lariv_theme_assets(html_doc: &str) -> String {
    let mut out = html_doc.to_string();
    loop {
        let lower = out.to_lowercase();
        if let Some((start, end)) =
            find_tagged_block(&lower, "<style", "data-lariv-theme", "</style>")
        {
            out.replace_range(start..end, "");
            continue;
        }
        if let Some((start, end)) = find_link_tag(&lower, "data-lariv-theme") {
            out.replace_range(start..end, "");
            continue;
        }
        if let Some((start, end)) = find_script_tag(&lower, "data-lariv-theme") {
            out.replace_range(start..end, "");
            continue;
        }
        break;
    }
    out
}

fn find_tagged_block(lower: &str, open: &str, marker: &str, close: &str) -> Option<(usize, usize)> {
    let mut search = 0;
    while let Some(rel) = lower[search..].find(open) {
        let start = search + rel;
        let after_open = start + open.len();
        let gt = lower[after_open..].find('>')?;
        let tag_end = after_open + gt + 1;
        if lower[start..tag_end].contains(marker)
            && let Some(crel) = lower[tag_end..].find(close)
        {
            let mut end = tag_end + crel + close.len();
            if lower.as_bytes().get(end) == Some(&b'\n') {
                end += 1;
            }
            return Some((start, end));
        }
        search = tag_end;
    }
    None
}

fn find_script_tag(lower: &str, marker: &str) -> Option<(usize, usize)> {
    let mut search = 0;
    while let Some(rel) = lower[search..].find("<script") {
        let start = search + rel;
        let after_open = start + 7;
        let close = lower[after_open..].find("</script>")?;
        let mut end = after_open + close + 9;
        let tag_end = lower[after_open..]
            .find('>')
            .map(|gt| after_open + gt + 1)?;
        if lower[start..tag_end.min(end)].contains(marker) {
            if lower.as_bytes().get(end) == Some(&b'\n') {
                end += 1;
            }
            return Some((start, end));
        }
        search = end;
    }
    None
}

fn find_link_tag(lower: &str, marker: &str) -> Option<(usize, usize)> {
    let mut search = 0;
    while let Some(rel) = lower[search..].find("<link") {
        let start = search + rel;
        let after_open = start + 5;
        let gt = lower[after_open..].find('>')?;
        let mut end = after_open + gt + 1;
        if lower[start..end].contains(marker) {
            if lower.as_bytes().get(end) == Some(&b'\n') {
                end += 1;
            }
            return Some((start, end));
        }
        search = end;
    }
    None
}

/// Insert or replace theme stylesheets/CSS/JS into an HTML document.
pub fn inject_theme_assets(
    html_doc: &str,
    theme_id: &str,
    theme: Option<&GrapesJsTheme>,
) -> String {
    let html_doc = strip_lariv_theme_assets(html_doc);
    let Some(theme) = theme else {
        return html_doc;
    };
    let head_block = theme_head_html(theme_id, theme);
    let body_js = theme_body_js_html(theme_id, theme);
    if head_block.is_empty() && body_js.is_empty() {
        return html_doc;
    }

    let lower = html_doc.to_lowercase();
    let mut out = if !head_block.is_empty() {
        if let Some(idx) = lower.find("</head>") {
            format!("{}{}{}", &html_doc[..idx], head_block, &html_doc[idx..])
        } else if let Some(idx) = lower.find("<body") {
            format!(
                "{}<head>\n{}</head>\n{}",
                &html_doc[..idx],
                head_block,
                &html_doc[idx..]
            )
        } else if lower.contains("<html") {
            format!("{head_block}{html_doc}")
        } else {
            format!(
                "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n{head_block}</head>\n<body>\n{html_doc}\n</body>\n</html>\n"
            )
        }
    } else {
        html_doc
    };

    if !body_js.is_empty() {
        let lower = out.to_lowercase();
        if let Some(idx) = lower.rfind("</body>") {
            out = format!("{}{}{}", &out[..idx], body_js, &out[idx..]);
        } else {
            out.push_str(&body_js);
        }
    }
    out
}