bookyard 0.1.1

Build and locally edit a bookshelf for multiple mdBook projects.
use std::{
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, bail};

pub fn run(
    source: PathBuf,
    folder: String,
    title: Option<String>,
    id: Option<String>,
    tags: Vec<String>,
    force: bool,
) -> anyhow::Result<()> {
    let root = crate::commands::root_dir()?;
    let target = root.join(&source);
    let title = title.unwrap_or_else(|| crate::commands::register::title_from_path(&source));

    create_mdbook_skeleton(&target, &title, force)
        .with_context(|| format!("failed to create mdBook at {}", source.display()))?;
    let registered =
        crate::commands::register::register_book(source, folder, Some(title), id, tags)?;

    println!("created {}", target.display());
    println!("added {}: {}", registered.id, registered.title);
    Ok(())
}

fn create_mdbook_skeleton(path: &Path, title: &str, force: bool) -> anyhow::Result<()> {
    if path.exists() {
        if !path.is_dir() {
            bail!("{} already exists and is not a directory", path.display());
        }
        if !force && !is_empty_dir(path)? {
            bail!(
                "{} already exists and is not empty; pass --force to write generated files",
                path.display()
            );
        }
    }

    let src_dir = path.join("src");
    fs::create_dir_all(&src_dir)
        .with_context(|| format!("failed to create {}", src_dir.display()))?;
    write_generated(
        &path.join("book.toml"),
        &format!("[book]\ntitle = {title:?}\n"),
        force,
    )?;
    write_generated(
        &src_dir.join("SUMMARY.md"),
        "# Summary\n\n- [Introduction](./index.md)\n",
        force,
    )?;
    write_generated(&src_dir.join("index.md"), &format!("# {title}\n"), force)?;
    Ok(())
}

fn is_empty_dir(path: &Path) -> anyhow::Result<bool> {
    Ok(fs::read_dir(path)
        .with_context(|| format!("failed to read {}", path.display()))?
        .next()
        .is_none())
}

fn write_generated(path: &Path, contents: &str, force: bool) -> anyhow::Result<()> {
    if path.exists() && !force {
        bail!("{} already exists; pass --force to replace it", path.display());
    }
    fs::write(path, contents).with_context(|| format!("failed to write {}", path.display()))
}