use std::path::Path;
use crate::assemble;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::project::ProjectLayout;
use crate::store::Store;
use crate::store::hierarchy::Hierarchy;
use crate::store::node::{Node, NodeKind};
use crate::typst_compile;
pub fn run(project: &Path, book_name: Option<&str>, compile: bool) -> Result<()> {
let layout = ProjectLayout::new(project);
layout.require_initialized()?;
let cfg = Config::load(&layout.config_path())?;
let store = Store::open(layout.clone(), &cfg)?;
let h = Hierarchy::load(&store)?;
let book = resolve_user_book(&h, book_name)?;
eprintln!("Assembling `{}` (slug: {})…", book.title, book.slug);
let mut progress = |done: usize, total: usize, file: &Path| {
eprintln!(" [{done}/{total}] {}", file.display());
};
let report = assemble::assemble_book(&store, &layout, &cfg, &book, &mut progress)
.map_err(|e| Error::Store(format!("assemble: {e:#}")))?;
eprintln!(
"Assembly OK · root: {} ({} files)",
report.root_typ.display(),
report.files_written,
);
if !compile {
return Ok(());
}
let mut handle = typst_compile::spawn_with_config(&cfg, &report.root_typ)
.map_err(|e| Error::Store(format!("typst spawn: {e:#}")))?;
loop {
match handle.try_wait() {
Ok(Some(_)) => break,
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(80)),
Err(e) => {
return Err(Error::Store(format!("typst try_wait: {e}")));
}
}
}
let outcome = typst_compile::finish(handle)
.map_err(|e| Error::Store(format!("typst finish: {e:#}")))?;
if outcome.success {
println!("PDF: {}", outcome.pdf_path.display());
Ok(())
} else {
let body = if outcome.stderr.trim().is_empty() {
outcome.stdout.clone()
} else {
outcome.stderr.clone()
};
Err(Error::Store(format!(
"typst compile failed:\n{body}"
)))
}
}
fn resolve_user_book<'a>(
h: &'a Hierarchy,
book_name: Option<&str>,
) -> Result<Node> {
let user_books: Vec<&Node> = h
.children_of(None)
.into_iter()
.filter(|n| n.kind == NodeKind::Book && n.system_tag.is_none())
.collect();
match book_name {
Some(name) => {
let needle = name.trim().to_ascii_lowercase();
let pick = user_books.iter().copied().find(|b| {
b.title.to_ascii_lowercase() == needle
|| b.slug.to_ascii_lowercase() == needle
});
match pick {
Some(book) => Ok(book.clone()),
None => {
let listing = user_books
.iter()
.map(|b| format!("`{}` (slug: {})", b.title, b.slug))
.collect::<Vec<_>>()
.join(", ");
let listing = if listing.is_empty() {
"no user books in this project".into()
} else {
listing
};
Err(Error::Store(format!(
"build: no book matches `--book-name {name}`. Available: {listing}"
)))
}
}
}
None => match user_books.as_slice() {
[book] => Ok((*book).clone()),
[] => Err(Error::Store(
"build: project has no user books — add one with `inkhaven add book <title>`".into(),
)),
_ => {
let listing = user_books
.iter()
.map(|b| format!("`{}`", b.title))
.collect::<Vec<_>>()
.join(", ");
Err(Error::Store(format!(
"build: project has {n} user books — pass --book-name <name>. Available: {listing}",
n = user_books.len(),
)))
}
},
}
}