bookyard 0.1.1

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

use anyhow::Context;
use bookyard_core::{BookyardConfig, build_catalog};

const SHELF_CSS: &str = include_str!("../../assets/shelf/style.css");

pub fn write_shelf(output_dir: &Path, config: &BookyardConfig) -> anyhow::Result<()> {
    fs::create_dir_all(output_dir.join("assets"))
        .with_context(|| format!("failed to create {}", output_dir.join("assets").display()))?;
    fs::write(output_dir.join("assets/style.css"), SHELF_CSS)
        .with_context(|| "failed to write shelf stylesheet")?;
    fs::write(output_dir.join("index.html"), render_shelf(config))
        .with_context(|| "failed to write shelf index")?;
    let catalog = build_catalog(config);
    fs::write(
        output_dir.join("catalog.json"),
        serde_json::to_string_pretty(&catalog)?,
    )
    .with_context(|| "failed to write shelf catalog")?;
    Ok(())
}

pub fn render_shelf(config: &BookyardConfig) -> String {
    let catalog = build_catalog(config);
    let mut books = String::new();
    for book in catalog.books {
        let folders = if book.folders.is_empty() {
            "Unsorted".to_owned()
        } else {
            book.folders.join(", ")
        };
        let tags = if book.tags.is_empty() {
            String::new()
        } else {
            format!(
                "<div class=\"tags\">{}</div>",
                book.tags
                    .iter()
                    .map(|tag| format!("<span>{}</span>", escape_html(tag)))
                    .collect::<Vec<_>>()
                    .join("")
            )
        };
        books.push_str(&format!(
            "<article class=\"book-card\"><a href=\"{}\"><h2>{}</h2></a><p>{}</p>{}</article>",
            escape_html(&book.href),
            escape_html(&book.title),
            escape_html(&folders),
            tags,
        ));
    }

    format!(
        r#"<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{title}</title>
    <link rel="stylesheet" href="assets/style.css">
  </head>
  <body>
    <main>
      <header>
        <p class="eyebrow">Bookyard</p>
        <h1>{title}</h1>
      </header>
      <section class="grid">{books}</section>
    </main>
  </body>
</html>
"#,
        title = escape_html(&config.workspace.title),
        books = books,
    )
}

fn escape_html(input: &str) -> String {
    input
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}