use std::{path::Path, sync::OnceLock};
use clap::{ArgAction, ValueHint, builder::ValueParser};
use tinymist_std::{bail, error::prelude::Result};
pub use tinymist_world::args::{CompileFontArgs, CompilePackageArgs};
use tinymist_world::args::{PdfExportArgs, PngExportArgs, parse_input_pair};
use crate::PROJECT_ROUTE_USER_ACTION_PRIORITY;
use crate::model::*;
#[derive(Debug, Clone, clap::Subcommand)]
#[clap(rename_all = "kebab-case")]
pub enum DocCommands {
New(DocNewArgs),
Configure(DocConfigureArgs),
}
#[derive(Debug, Clone, clap::Parser)]
pub struct DocNewArgs {
#[clap(flatten)]
pub id: DocIdArgs,
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
pub root: Option<String>,
#[clap(
long = "input",
value_name = "key=value",
action = ArgAction::Append,
value_parser = ValueParser::new(parse_input_pair),
)]
pub inputs: Vec<(String, String)>,
#[clap(flatten)]
pub font: CompileFontArgs,
#[clap(flatten)]
pub package: CompilePackageArgs,
}
impl DocNewArgs {
pub fn to_input(&self, ctx: CtxPath) -> ProjectInput {
let id: Id = self.id.id(ctx);
let root = self
.root
.as_ref()
.map(|root| ResourcePath::from_user_sys(Path::new(root), ctx));
let main = ResourcePath::from_user_sys(Path::new(&self.id.input), ctx);
let font_paths = self
.font
.font_paths
.iter()
.map(|p| ResourcePath::from_user_sys(p, ctx))
.collect::<Vec<_>>();
let package_path = self
.package
.package_path
.as_ref()
.map(|p| ResourcePath::from_user_sys(p, ctx));
let package_cache_path = self
.package
.package_cache_path
.as_ref()
.map(|p| ResourcePath::from_user_sys(p, ctx));
ProjectInput {
id: id.clone(),
lock_dir: Some(ctx.1.to_path_buf()),
root,
main,
inputs: self.inputs.clone(),
font_paths,
system_fonts: !self.font.ignore_system_fonts,
package_path,
package_cache_path,
}
}
}
#[derive(Debug, Clone, clap::Parser)]
pub struct DocIdArgs {
#[clap(long = "name")]
pub name: Option<String>,
#[clap(value_hint = ValueHint::FilePath)]
pub input: String,
}
impl DocIdArgs {
pub fn id(&self, ctx: CtxPath) -> Id {
if let Some(id) = &self.name {
Id::new(id.clone())
} else {
(&ResourcePath::from_user_sys(Path::new(&self.input), ctx)).into()
}
}
}
#[derive(Debug, Clone, clap::Parser)]
pub struct DocConfigureArgs {
#[clap(flatten)]
pub id: DocIdArgs,
#[clap(long = "priority", default_value_t = PROJECT_ROUTE_USER_ACTION_PRIORITY)]
pub priority: u32,
}
#[derive(Debug, Clone, clap::Parser)]
pub struct TaskCompileArgs {
#[clap(flatten)]
pub declare: DocNewArgs,
#[arg(long = "when")]
pub when: Option<TaskWhen>,
#[clap(value_hint = ValueHint::FilePath)]
pub output: Option<String>,
#[arg(long = "format", short = 'f')]
pub format: Option<OutputFormat>,
#[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<Pages>>,
#[clap(flatten)]
pub pdf: PdfExportArgs,
#[clap(flatten)]
pub png: PngExportArgs,
#[clap(skip)]
pub output_format: OnceLock<Result<OutputFormat>>,
}
impl TaskCompileArgs {
pub fn to_task(self, doc_id: Id, cwd: &Path) -> Result<ApplyProjectTask> {
let new_task_id = self.declare.id.name.map(Id::new);
let task_id = new_task_id.unwrap_or(doc_id.clone());
let output_format = if let Some(specified) = self.format {
specified
} else if let Some(output) = &self.output {
let output = Path::new(output);
match output.extension() {
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
_ => bail!(
"could not infer output format for path {output:?}.\n\
consider providing the format manually with `--format/-f`",
),
}
} else {
OutputFormat::Pdf
};
let output = self.output.as_ref().map(|output| {
let output = Path::new(output);
let output = if output.is_absolute() {
output.to_path_buf()
} else {
cwd.join(output)
};
PathPattern::new(&output.with_extension("").to_string_lossy())
});
let when = self.when.unwrap_or(TaskWhen::Never);
let mut transforms = vec![];
if let Some(pages) = &self.pages {
transforms.push(ExportTransform::Pages {
ranges: pages.clone(),
});
}
let export = ExportTask {
when,
output,
transform: transforms,
};
let config = match output_format {
OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
export,
pages: self.pages.clone(),
pdf_standards: self.pdf.standard.clone(),
no_pdf_tags: self.pdf.no_tags,
creation_timestamp: None,
}),
OutputFormat::Png => ProjectTask::ExportPng(ExportPngTask {
export,
pages: self.pages.clone(),
page_number_template: None,
merge: None,
ppi: self.png.ppi.try_into().unwrap(),
fill: None,
}),
OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask {
export,
pages: self.pages.clone(),
page_number_template: None,
merge: None,
}),
OutputFormat::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
OutputFormat::Bundle => ProjectTask::ExportBundle(ExportBundleTask {
export,
pages: self.pages.clone(),
pdf_standards: self.pdf.standard.clone(),
no_pdf_tags: self.pdf.no_tags,
creation_timestamp: None,
ppi: self.png.ppi.try_into().unwrap(),
}),
};
Ok(ApplyProjectTask {
id: task_id.clone(),
document: doc_id,
task: config,
})
}
}