bim 0.1.0

A content-first static site generator for blogs, digital gardens, and knowledge bases
Documentation
use crate::content::NavItem;
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use std::path::Path;

pub fn parse_nav(content: &str) -> Vec<NavItem> {
    let mut items = Vec::new();
    let parser = Parser::new(content);

    let mut in_link = false;
    let mut current_url = String::new();
    let mut current_label = String::new();

    for event in parser {
        match event {
            Event::Start(Tag::Link { dest_url, .. }) => {
                in_link = true;
                current_url = dest_url.to_string();
            }
            Event::End(TagEnd::Link) => {
                if in_link && !current_label.is_empty() {
                    items.push(NavItem {
                        label: current_label.trim().to_string(),
                        url: current_url.clone(),
                    });
                }
                in_link = false;
                current_label.clear();
                current_url.clear();
            }
            Event::Text(text) if in_link => {
                current_label.push_str(&text);
            }
            _ => {}
        }
    }

    items
}

pub fn render_nav_html(items: &[NavItem]) -> String {
    if items.is_empty() {
        return String::new();
    }

    let mut html = String::from("<nav>\n  <ul>\n");

    for item in items {
        html.push_str(&format!(
            "    <li><a href=\"{}\">{}</a></li>\n",
            html_escape::encode_double_quoted_attribute(&item.url),
            html_escape::encode_text(&item.label)
        ));
    }

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

pub fn load_nav(source_dir: &Path) -> (Vec<NavItem>, String) {
    let nav_path = source_dir.join("nav.md");

    if nav_path.exists() {
        match std::fs::read_to_string(&nav_path) {
            Ok(content) => {
                let items = parse_nav(&content);
                let html = render_nav_html(&items);
                return (items, html);
            }
            Err(e) => {
                tracing::warn!("Failed to read nav.md: {}", e);
            }
        }
    }

    (Vec::new(), String::new())
}

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

    #[test]
    fn test_parse_nav() {
        let content = r#"
- [Home](/)
- [Blog](/blog/)
- [Notes](/notes/)
"#;
        let items = parse_nav(content);
        assert_eq!(items.len(), 3);
        assert_eq!(items[0].label, "Home");
        assert_eq!(items[0].url, "/");
        assert_eq!(items[1].label, "Blog");
        assert_eq!(items[1].url, "/blog/");
    }

    #[test]
    fn test_render_nav_html() {
        let items = vec![
            NavItem {
                label: "Home".to_string(),
                url: "/".to_string(),
            },
            NavItem {
                label: "Blog".to_string(),
                url: "/blog/".to_string(),
            },
        ];

        let html = render_nav_html(&items);
        assert!(html.contains("<nav>"));
        assert!(html.contains("href=\"/\""));
        assert!(html.contains("Home"));
    }
}