bim 0.1.0

A content-first static site generator for blogs, digital gardens, and knowledge bases
Documentation
use crate::content::Snippet;
use crate::date::extract_date;
use crate::git::GitRepo;
use std::path::Path;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;

pub fn process_snippet(path: &Path, git_repo: Option<&GitRepo>) -> anyhow::Result<Snippet> {
    let code = std::fs::read_to_string(path)?;
    let mut snippet = Snippet::new(path.to_path_buf());

    // Extract language from file extension
    snippet.language = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("text")
        .to_string();

    // Extract title from filename (without extension)
    let filename = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("untitled");
    snippet.title = filename.replace(['_', '-'], " ");

    // Generate URL slug
    let slug = slug::slugify(filename);

    // Get the relative path within snippets directory
    snippet.url = format!("/snippets/{}/", slug);

    // Store raw code
    snippet.code = code.clone();

    // Generate syntax-highlighted HTML
    snippet.html_content = highlight_code(&code, &snippet.language);

    // Extract date
    snippet.date = extract_date(path, git_repo);

    Ok(snippet)
}

pub fn highlight_code(code: &str, language: &str) -> String {
    let ss = SyntaxSet::load_defaults_newlines();
    let ts = ThemeSet::load_defaults();

    let syntax = ss
        .find_syntax_by_extension(language)
        .or_else(|| ss.find_syntax_by_name(language))
        .unwrap_or_else(|| ss.find_syntax_plain_text());

    let theme = &ts.themes["base16-ocean.dark"];

    match highlighted_html_for_string(code, &ss, syntax, theme) {
        Ok(html) => html,
        Err(_) => {
            // Fallback to plain code block
            format!(
                "<pre><code class=\"language-{}\">{}</code></pre>",
                html_escape::encode_text(language),
                html_escape::encode_text(code)
            )
        }
    }
}

pub fn get_language_name(extension: &str) -> &'static str {
    match extension {
        "rs" => "Rust",
        "py" => "Python",
        "js" => "JavaScript",
        "ts" => "TypeScript",
        "go" => "Go",
        "rb" => "Ruby",
        "java" => "Java",
        "c" => "C",
        "cpp" | "cc" | "cxx" => "C++",
        "h" | "hpp" => "C/C++ Header",
        "sh" | "bash" => "Bash",
        "zsh" => "Zsh",
        "fish" => "Fish",
        "sql" => "SQL",
        "html" => "HTML",
        "css" => "CSS",
        "scss" | "sass" => "SCSS",
        "json" => "JSON",
        "yaml" | "yml" => "YAML",
        "toml" => "TOML",
        "xml" => "XML",
        "md" => "Markdown",
        "php" => "PHP",
        "swift" => "Swift",
        "kt" | "kts" => "Kotlin",
        "lua" => "Lua",
        "r" => "R",
        "ex" | "exs" => "Elixir",
        "erl" => "Erlang",
        "hs" => "Haskell",
        "ml" | "mli" => "OCaml",
        "clj" | "cljs" => "Clojure",
        "scala" => "Scala",
        "pl" | "pm" => "Perl",
        "vim" => "Vim Script",
        "dockerfile" => "Dockerfile",
        "tf" => "Terraform",
        "nix" => "Nix",
        _ => "Code",
    }
}

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

    #[test]
    fn test_highlight_code() {
        let code = "fn main() { println!(\"Hello\"); }";
        let html = highlight_code(code, "rs");
        assert!(html.contains("pre"));
    }

    #[test]
    fn test_get_language_name() {
        assert_eq!(get_language_name("rs"), "Rust");
        assert_eq!(get_language_name("py"), "Python");
        assert_eq!(get_language_name("unknown"), "Code");
    }
}