bim 0.1.0

A content-first static site generator for blogs, digital gardens, and knowledge bases
Documentation
use crate::collections::Collection;
use crate::content::Site;
use regex::Regex;
use std::sync::LazyLock;

static RECENT_DIRECTIVE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\{\{recent\s+(\w+)\s+(\d+)\}\}").unwrap());

pub fn process_directives(content: &str, site: &Site) -> String {
    let mut result = content.to_string();

    // Process {{recent collection count}} directives
    result = RECENT_DIRECTIVE
        .replace_all(&result, |caps: &regex::Captures| {
            let collection_name = &caps[1];
            let count: usize = caps[2].parse().unwrap_or(5);

            let collection = match collection_name {
                "blog" => Collection::Blog,
                "notes" => Collection::Notes,
                "snippets" => Collection::Snippets,
                "talks" => Collection::Talks,
                "projects" => Collection::Projects,
                "recipes" => Collection::Recipes,
                _ => return format!("<!-- Unknown collection: {} -->", collection_name),
            };

            generate_recent_list(site, collection, count)
        })
        .to_string();

    result
}

fn generate_recent_list(site: &Site, collection: Collection, count: usize) -> String {
    let pages = site.recent_pages(collection, count);

    if pages.is_empty() {
        return String::new();
    }

    let mut html = String::from("<ul class=\"recent-list\">\n");

    for page in pages {
        let date_str = page
            .date
            .map(|d| d.format("%Y-%m-%d").to_string())
            .unwrap_or_default();

        html.push_str(&format!(
            "  <li><a href=\"{}\">{}</a>",
            html_escape::encode_double_quoted_attribute(&page.url),
            html_escape::encode_text(&page.title)
        ));

        if !date_str.is_empty() {
            html.push_str(&format!(
                " <time datetime=\"{}\">{}</time>",
                date_str, date_str
            ));
        }

        html.push_str("</li>\n");
    }

    html.push_str("</ul>");
    html
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_directive_regex() {
        let content = "{{recent blog 5}}";
        assert!(RECENT_DIRECTIVE.is_match(content));

        let caps = RECENT_DIRECTIVE.captures(content).unwrap();
        assert_eq!(&caps[1], "blog");
        assert_eq!(&caps[2], "5");
    }

    #[test]
    fn test_process_unknown_collection() {
        let site = Site::new();
        let result = process_directives("{{recent unknown 3}}", &site);
        assert!(result.contains("Unknown collection"));
    }
}