pub(crate) mod config; mod pandoc; use self::config::Config; use self::pandoc::into_pandoc; use crate::types::*; use anyhow::{ensure, Context, Result}; use rayon::Scope; use thiserror::Error; use std::collections::BTreeMap; use std::io::Write; use std::path::Path; const DEFAULT_CSS: &[u8] = include_bytes!("../../res/style.css"); pub(crate) fn generate( base_dir: &Path, toplevel_entity: Entity, config: &Config, ) -> Result<()> { let md_output_dir = base_dir.join("markdown"); let html_output_dir = base_dir.join("html"); std::fs::create_dir_all(&md_output_dir) .context("Failed to create the markdown output directory")?; std::fs::create_dir_all(&html_output_dir) .context("Failed to create the HTML output directory")?; let mut css_tempfile = tempfile::Builder::new() .prefix("style") .suffix(".css") .tempfile()?; css_tempfile.write_all(DEFAULT_CSS)?; let css_path = css_tempfile.path(); debug!("Generated temporary file with CSS at: {:?}", css_path); rayon::scope(|scope| { generate_recursively( // TODO: a bit hacky? EntityId(String::from("index")), toplevel_entity, scope, &md_output_dir, &html_output_dir, css_path, config, ); }); Ok(()) } fn generate_recursively<'a>( id: EntityId, entity: Entity, pool: &Scope<'a>, md_output_dir: &'a Path, html_output_dir: &'a Path, css_path: &'a Path, config: &'a Config, ) { pool.spawn(move |pool| { trace!("Trying to generate {}", id.0); let leftovers = generate_single( &id, entity, &md_output_dir, &html_output_dir, &css_path, config, ) .unwrap(); for (id, entity) in leftovers { generate_recursively( id, entity, pool, md_output_dir, html_output_dir, css_path, config, ); } }); } fn generate_single( id: &EntityId, entity: Entity, md_output_dir: impl AsRef, html_output_dir: impl AsRef, css_path: impl AsRef, config: &Config, ) -> Result> { use std::io::prelude::*; use std::process::{Command, Stdio}; let md_output_file = md_output_dir.as_ref().join(&id.0); let html_output_file = html_output_dir.as_ref().join(&id.0); let mut command = Command::new("pandoc"); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .args(&[ "--from=json", // standalone keeps the sent metadat "--standalone", "--to", &config.from, "--output", md_output_file .to_str() .context("Entity name is not valid UTF-8")?, ]); debug!("Launching command: {:?}", command); let mut pandoc = command .spawn() .context("Failed to execute Pandoc command")?; let (pandoc_ast, leftovers) = into_pandoc(entity, config); if log_enabled!(log::Level::Trace) { let json = serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?; trace!("Sending json: {}", json); write!( pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), "{}", &json, ) .context("Failed to convert Pandoc AST to JSON")?; } else { serde_json::to_writer( pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), &pandoc_ast, ) .context("Failed to convert Pandoc AST to JSON")?; } let command_str = format!("{:?}", pandoc); let output = pandoc .wait_with_output() .expect("Pandoc command wasn't running"); ensure!( output.status.success(), CommandError { command: format!("{:?}", command_str), status: output.status, stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), } ); let mut command = Command::new("pandoc"); command .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .args(&[ "--from", &config.from, "--to", &config.to, "--css", css_path .as_ref() .to_str() .context("CSS path is not valid UTF-8")?, "--standalone", "--self-contained", md_output_file .to_str() .context("Entity name is not valid UTF-8")?, "--output", html_output_file .to_str() .context("Entity name is not valid UTF-8")?, ]); for filter in &config.pandoc_filters { command.args(&["--filter", filter]); } let command_str = format!("{:?}", command); debug!("Launching command: {}", command_str); let output = command .output() .context("Failed to execute Pandoc command")?; std::io::stderr().lock().write_all(&output.stderr)?; ensure!( output.status.success(), CommandError { command: command_str, status: output.status, stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), } ); Ok(leftovers) } #[derive(Debug, Clone, Error)] #[error( "Command {} returned status {:?} and stderr {:?}", command, status, stderr )] struct CommandError { command: String, status: std::process::ExitStatus, stderr: String, }