use crate::commands::{load_quill, read_document, read_fonts, render_date};
use crate::errors::{CliError, Result};
use crate::output::{derive_output_path, page_output_path, write_file, write_stdout};
use clap::Parser;
use quillmark::{
CalendarDate, Diagnostic, Document, Engine, OutputFormat, Quill, RenderError,
RenderOptions, RenderResult, Severity,
};
use std::path::{Path, PathBuf};
#[derive(Parser)]
pub struct RenderArgs {
#[arg(value_name = "QUILL_PATH")]
quill: PathBuf,
#[arg(value_name = "MARKDOWN_FILE")]
markdown_file: Option<PathBuf>,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(short, long, value_name = "FORMAT")]
format: Option<String>,
#[arg(long, value_name = "PPI")]
ppi: Option<f32>,
#[arg(long)]
stdout: bool,
#[arg(long)]
quiet: bool,
#[arg(long, value_name = "DATA_FILE")]
output_data: Option<PathBuf>,
#[arg(long, value_name = "YYYY-MM-DD")]
today: Option<CalendarDate>,
#[arg(long = "font", value_name = "FONT_FILE")]
fonts: Vec<PathBuf>,
}
pub fn execute(args: RenderArgs) -> Result<()> {
let today = render_date(args.today);
let quill = load_quill(&args.quill)?;
let (parsed, parse_warnings) = read_document(&quill, args.markdown_file.as_deref())?;
let markdown_path_for_output = args.markdown_file.clone();
let output_format = resolve_format(
args.format.as_deref(),
args.output.as_deref().filter(|_| !args.stdout),
)?;
let mut options = RenderOptions::default().with_output_format(output_format);
options.ppi = args.ppi;
let engine = Engine::with_fonts(read_fonts(&args.fonts)?)?;
let rendered = render(&engine, &quill, &parsed, today, &options, args.output_data.as_deref());
let mut result = match rendered {
Ok(result) => result,
Err(e) => {
let (e, carried) = split_warnings(e);
if !args.quiet {
let unclaimed = quill
.validate(&parsed)
.into_iter()
.filter(|d| d.severity == Severity::Warning);
let warnings: Vec<_> = parse_warnings
.into_iter()
.chain(unclaimed)
.chain(carried)
.collect();
crate::errors::print_warnings(&warnings);
}
return Err(e);
}
};
result.warnings.splice(0..0, parse_warnings);
if !args.quiet {
crate::errors::print_warnings(&result.warnings);
}
if result.artifacts.is_empty() {
return Err(CliError::InvalidArgument(
"No artifacts produced from rendering".to_string(),
));
}
if args.stdout {
if result.artifacts.len() > 1 {
return Err(CliError::InvalidArgument(format!(
"{} renders {} pages, one artifact each, and --stdout carries one; \
drop --stdout to write the pages as files",
output_format,
result.artifacts.len()
)));
}
write_stdout(&result.artifacts[0].bytes)?;
} else {
let output_path = args.output.unwrap_or_else(|| {
if let Some(ref path) = markdown_path_for_output {
derive_output_path(path, output_format.as_str())
} else {
PathBuf::from(format!("example.{}", output_format))
}
});
if let [artifact] = result.artifacts.as_slice() {
write_file(&output_path, &artifact.bytes, !args.quiet)?;
} else {
for (i, artifact) in result.artifacts.iter().enumerate() {
let path = page_output_path(&output_path, i + 1);
write_file(&path, &artifact.bytes, !args.quiet)?;
}
}
}
Ok(())
}
fn split_warnings(err: CliError) -> (CliError, Vec<Diagnostic>) {
let CliError::Render(err) = err else {
return (err, Vec::new());
};
let (errors, warnings): (Vec<_>, Vec<_>) = err
.into_diagnostics()
.into_iter()
.partition(|d| d.severity == Severity::Error);
(CliError::Render(RenderError::new(errors)), warnings)
}
fn render(
engine: &Engine,
quill: &Quill,
parsed: &Document,
today: CalendarDate,
options: &RenderOptions,
output_data: Option<&Path>,
) -> Result<RenderResult> {
if let Some(data_path) = output_data {
let json_data = quill.compile_data(parsed, today).map_err(CliError::Render)?;
let f = std::fs::File::create(data_path).map_err(|e| {
CliError::Io(std::io::Error::new(
e.kind(),
format!(
"Failed to create data output file '{}': {}",
data_path.display(),
e
),
))
})?;
serde_json::to_writer_pretty(f, &json_data).map_err(|e| {
CliError::Io(std::io::Error::other(format!(
"Failed to write JSON data: {}",
e
)))
})?;
}
Ok(engine.render(quill, parsed, today, options)?)
}
fn resolve_format(flag: Option<&str>, output: Option<&Path>) -> Result<OutputFormat> {
let flag = flag
.map(str::parse::<OutputFormat>)
.transpose()
.map_err(|e| CliError::InvalidArgument(e.to_string()))?;
let named = output.and_then(|path| {
let format = path.extension()?.to_str()?.parse::<OutputFormat>().ok()?;
Some((format, path))
});
match (flag, named) {
(Some(flag), Some((named, path))) if flag != named => {
Err(CliError::InvalidArgument(format!(
"-f {flag} disagrees with -o {}, which names {named}; \
drop -f to write {named}, or give -o a .{flag} extension",
path.display()
)))
}
(flag, named) => Ok(flag
.or(named.map(|(format, _)| format))
.unwrap_or(OutputFormat::Pdf)),
}
}