dioxus-docs-kit-build 0.4.0

Build-time helper for dioxus-docs-kit: generates content maps from _nav.json
Documentation
use serde::Deserialize;
use std::env;
use std::fs;
use std::path::Path;

#[derive(Deserialize)]
struct NavConfig {
    groups: Vec<NavGroup>,
}

#[derive(Deserialize)]
struct NavGroup {
    pages: Vec<String>,
}

/// Generates `doc_content_generated.rs` in `OUT_DIR` from a `_nav.json` file.
///
/// Call this from your `build.rs`:
///
/// ```rust,ignore
/// fn main() {
///     dioxus_docs_kit_build::generate_content_map("docs/_nav.json");
/// }
/// ```
///
/// The generated file is an expression that returns a `HashMap<&'static str, &'static str>`
/// and is intended to be used with `include!()`.
///
/// The docs directory is inferred from the parent of `nav_json_path`
/// (e.g. `"docs/_nav.json"` → `"docs"`).
pub fn generate_content_map(nav_json_path: &str) {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    println!("cargo:rerun-if-changed={nav_json_path}");

    let json = fs::read_to_string(nav_json_path)
        .unwrap_or_else(|e| panic!("Failed to read {nav_json_path}: {e}"));
    let nav: NavConfig = serde_json::from_str(&json)
        .unwrap_or_else(|e| panic!("Failed to parse {nav_json_path}: {e}"));

    // Infer docs directory from nav path parent (e.g. "docs/_nav.json" → "docs")
    let docs_dir = Path::new(nav_json_path)
        .parent()
        .and_then(|p| p.to_str())
        .unwrap_or("docs");

    let mut code = String::from("// Auto-generated by dioxus-docs-kit-build — do not edit\n{\n");
    code.push_str("    let mut map = std::collections::HashMap::new();\n");

    for group in &nav.groups {
        for page in &group.pages {
            let mdx_path = format!("{docs_dir}/{page}.mdx");
            let full_path = format!("{manifest_dir}/{mdx_path}");

            if !Path::new(&full_path).exists() {
                println!(
                    "cargo:warning=Skipping \"{page}\": {mdx_path} not found (OpenAPI endpoints don't need .mdx files)"
                );
                continue;
            }

            println!("cargo:rerun-if-changed={mdx_path}");

            // Use absolute path so include_str! works from OUT_DIR
            code.push_str(&format!(
                "    map.insert(\"{page}\", include_str!(\"{full_path}\"));\n"
            ));
        }
    }

    code.push_str("    map\n}\n");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("doc_content_generated.rs");
    fs::write(&dest, code).expect("Failed to write generated file");
}

// ============================================================================
// Blog content map generation
// ============================================================================

#[derive(Deserialize)]
struct BlogManifest {
    posts: Vec<String>,
}

/// Generates `blog_content_generated.rs` in `OUT_DIR` from a `_blog.json` file.
///
/// Call this from your `build.rs`:
///
/// ```rust,ignore
/// fn main() {
///     dioxus_docs_kit_build::generate_blog_content_map("blog/_blog.json");
/// }
/// ```
///
/// The generated file is an expression that returns a `HashMap<&'static str, &'static str>`
/// and is intended to be used with `include!()`.
///
/// The blog directory is inferred from the parent of `manifest_path`
/// (e.g. `"blog/_blog.json"` → `"blog"`).
///
/// The manifest JSON itself is embedded under the key `"__manifest__"` so the
/// runtime library can parse author definitions and other metadata.
pub fn generate_blog_content_map(manifest_path: &str) {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    println!("cargo:rerun-if-changed={manifest_path}");

    let json = fs::read_to_string(manifest_path)
        .unwrap_or_else(|e| panic!("Failed to read {manifest_path}: {e}"));
    let manifest: BlogManifest = serde_json::from_str(&json)
        .unwrap_or_else(|e| panic!("Failed to parse {manifest_path}: {e}"));

    // Infer blog directory from manifest path parent
    let blog_dir = Path::new(manifest_path)
        .parent()
        .and_then(|p| p.to_str())
        .unwrap_or("blog");

    let mut code = String::from("// Auto-generated by dioxus-docs-kit-build — do not edit\n{\n");
    code.push_str("    let mut map = std::collections::HashMap::new();\n");

    // Embed the manifest JSON itself
    let manifest_full_path = format!("{manifest_dir}/{manifest_path}");
    code.push_str(&format!(
        "    map.insert(\"__manifest__\", include_str!(\"{manifest_full_path}\"));\n"
    ));

    for slug in &manifest.posts {
        let mdx_path = format!("{blog_dir}/{slug}.mdx");
        let full_path = format!("{manifest_dir}/{mdx_path}");

        if !Path::new(&full_path).exists() {
            println!("cargo:warning=Skipping \"{slug}\": {mdx_path} not found");
            continue;
        }

        println!("cargo:rerun-if-changed={mdx_path}");

        code.push_str(&format!(
            "    map.insert(\"{slug}\", include_str!(\"{full_path}\"));\n"
        ));
    }

    code.push_str("    map\n}\n");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("blog_content_generated.rs");
    fs::write(&dest, code).expect("Failed to write generated file");
}