bim 0.1.0

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

static TAG_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[#([^\]]+)\]").unwrap());

static CONTEXT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[@([^\]]+)\]").unwrap());

static LINK_REGEX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"\[([^\]]*)\]\(([^)]+)\)").unwrap());

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Tag {
    pub name: String,
    pub slug: String,
    pub url: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Context {
    pub name: String,
    pub slug: String,
    pub url: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InternalLink {
    pub text: String,
    pub url: String,
}

impl Tag {
    pub fn new(name: &str) -> Self {
        let slug = slug::slugify(name);
        Self {
            name: name.to_string(),
            slug: slug.clone(),
            url: format!("/tags/{}/", slug),
        }
    }
}

impl Context {
    pub fn new(name: &str) -> Self {
        let slug = slug::slugify(name);
        Self {
            name: name.to_string(),
            slug: slug.clone(),
            url: format!("/contexts/{}/", slug),
        }
    }
}

pub fn extract_tags(content: &str) -> Vec<Tag> {
    TAG_REGEX
        .captures_iter(content)
        .map(|cap| Tag::new(&cap[1]))
        .collect()
}

pub fn extract_contexts(content: &str) -> Vec<Context> {
    CONTEXT_REGEX
        .captures_iter(content)
        .map(|cap| Context::new(&cap[1]))
        .collect()
}

pub fn extract_internal_links(content: &str) -> Vec<InternalLink> {
    LINK_REGEX
        .captures_iter(content)
        .filter_map(|cap| {
            let url = &cap[2];
            // Only internal links (starting with / or relative)
            if url.starts_with("http://")
                || url.starts_with("https://")
                || url.starts_with("mailto:")
            {
                None
            } else {
                Some(InternalLink {
                    text: cap[1].to_string(),
                    url: url.to_string(),
                })
            }
        })
        .collect()
}

pub fn transform_tags_to_links(content: &str) -> String {
    TAG_REGEX
        .replace_all(content, |caps: &regex::Captures| {
            let tag = Tag::new(&caps[1]);
            format!("[#{}]({})", tag.name, tag.url)
        })
        .to_string()
}

pub fn transform_contexts_to_links(content: &str) -> String {
    CONTEXT_REGEX
        .replace_all(content, |caps: &regex::Captures| {
            let ctx = Context::new(&caps[1]);
            format!("[@{}]({})", ctx.name, ctx.url)
        })
        .to_string()
}

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

    #[test]
    fn test_extract_tags() {
        let content = "This is about [#rust systems programming] and [#web development].";
        let tags = extract_tags(content);
        assert_eq!(tags.len(), 2);
        assert_eq!(tags[0].name, "rust systems programming");
        assert_eq!(tags[0].slug, "rust-systems-programming");
        assert_eq!(tags[0].url, "/tags/rust-systems-programming/");
    }

    #[test]
    fn test_extract_contexts() {
        let content = "This relates to [@linux networking] setup.";
        let contexts = extract_contexts(content);
        assert_eq!(contexts.len(), 1);
        assert_eq!(contexts[0].name, "linux networking");
        assert_eq!(contexts[0].url, "/contexts/linux-networking/");
    }

    #[test]
    fn test_extract_internal_links() {
        let content = "See [my post](/blog/my-post/) and [external](https://example.com).";
        let links = extract_internal_links(content);
        assert_eq!(links.len(), 1);
        assert_eq!(links[0].url, "/blog/my-post/");
    }

    #[test]
    fn test_transform_tags() {
        let content = "Using [#rust].";
        let transformed = transform_tags_to_links(content);
        assert_eq!(transformed, "Using [#rust](/tags/rust/).");
    }
}