1use std::{path::Path, sync::OnceLock};
2
3use clap::{ArgAction, ValueHint, builder::ValueParser};
4use tinymist_std::{bail, error::prelude::Result};
5
6pub use tinymist_world::args::{CompileFontArgs, CompilePackageArgs};
7use tinymist_world::args::{PdfExportArgs, PngExportArgs, parse_input_pair};
8
9use crate::PROJECT_ROUTE_USER_ACTION_PRIORITY;
10use crate::model::*;
11
12#[derive(Debug, Clone, clap::Subcommand)]
14#[clap(rename_all = "kebab-case")]
15pub enum DocCommands {
16 New(DocNewArgs),
18 Configure(DocConfigureArgs),
20}
21
22#[derive(Debug, Clone, clap::Parser)]
24pub struct DocNewArgs {
25 #[clap(flatten)]
27 pub id: DocIdArgs,
28 #[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
32 pub root: Option<String>,
33 #[clap(
35 long = "input",
36 value_name = "key=value",
37 action = ArgAction::Append,
38 value_parser = ValueParser::new(parse_input_pair),
39 )]
40 pub inputs: Vec<(String, String)>,
41 #[clap(flatten)]
43 pub font: CompileFontArgs,
44 #[clap(flatten)]
46 pub package: CompilePackageArgs,
47}
48
49impl DocNewArgs {
50 pub fn to_input(&self, ctx: CtxPath) -> ProjectInput {
52 let id: Id = self.id.id(ctx);
53
54 let root = self
55 .root
56 .as_ref()
57 .map(|root| ResourcePath::from_user_sys(Path::new(root), ctx));
58 let main = ResourcePath::from_user_sys(Path::new(&self.id.input), ctx);
59
60 let font_paths = self
61 .font
62 .font_paths
63 .iter()
64 .map(|p| ResourcePath::from_user_sys(p, ctx))
65 .collect::<Vec<_>>();
66
67 let package_path = self
68 .package
69 .package_path
70 .as_ref()
71 .map(|p| ResourcePath::from_user_sys(p, ctx));
72
73 let package_cache_path = self
74 .package
75 .package_cache_path
76 .as_ref()
77 .map(|p| ResourcePath::from_user_sys(p, ctx));
78
79 ProjectInput {
80 id: id.clone(),
81 lock_dir: Some(ctx.1.to_path_buf()),
82 root,
83 main,
84 inputs: self.inputs.clone(),
85 font_paths,
86 system_fonts: !self.font.ignore_system_fonts,
87 package_path,
88 package_cache_path,
89 }
90 }
91}
92
93#[derive(Debug, Clone, clap::Parser)]
97pub struct DocIdArgs {
98 #[clap(long = "name")]
100 pub name: Option<String>,
101 #[clap(value_hint = ValueHint::FilePath)]
103 pub input: String,
104}
105
106impl DocIdArgs {
107 pub fn id(&self, ctx: CtxPath) -> Id {
109 if let Some(id) = &self.name {
110 Id::new(id.clone())
111 } else {
112 (&ResourcePath::from_user_sys(Path::new(&self.input), ctx)).into()
113 }
114 }
115}
116
117#[derive(Debug, Clone, clap::Parser)]
119pub struct DocConfigureArgs {
120 #[clap(flatten)]
122 pub id: DocIdArgs,
123 #[clap(long = "priority", default_value_t = PROJECT_ROUTE_USER_ACTION_PRIORITY)]
126 pub priority: u32,
127}
128
129#[derive(Debug, Clone, clap::Parser)]
131pub struct TaskCompileArgs {
132 #[clap(flatten)]
134 pub declare: DocNewArgs,
135
136 #[arg(long = "when")]
138 pub when: Option<TaskWhen>,
139
140 #[clap(value_hint = ValueHint::FilePath)]
149 pub output: Option<String>,
150
151 #[arg(long = "format", short = 'f')]
154 pub format: Option<OutputFormat>,
155
156 #[arg(long = "pages", value_delimiter = ',')]
167 pub pages: Option<Vec<Pages>>,
168
169 #[clap(flatten)]
171 pub pdf: PdfExportArgs,
172
173 #[clap(flatten)]
175 pub png: PngExportArgs,
176
177 #[clap(skip)]
179 pub output_format: OnceLock<Result<OutputFormat>>,
180}
181
182impl TaskCompileArgs {
183 pub fn to_task(self, doc_id: Id, cwd: &Path) -> Result<ApplyProjectTask> {
185 let new_task_id = self.declare.id.name.map(Id::new);
186 let task_id = new_task_id.unwrap_or(doc_id.clone());
187
188 let output_format = if let Some(specified) = self.format {
189 specified
190 } else if let Some(output) = &self.output {
191 let output = Path::new(output);
192
193 match output.extension() {
194 Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
195 Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
196 Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
197 Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
198 _ => bail!(
199 "could not infer output format for path {output:?}.\n\
200 consider providing the format manually with `--format/-f`",
201 ),
202 }
203 } else {
204 OutputFormat::Pdf
205 };
206
207 let output = self.output.as_ref().map(|output| {
208 let output = Path::new(output);
209 let output = if output.is_absolute() {
210 output.to_path_buf()
211 } else {
212 cwd.join(output)
213 };
214
215 PathPattern::new(&output.with_extension("").to_string_lossy())
216 });
217
218 let when = self.when.unwrap_or(TaskWhen::Never);
219
220 let mut transforms = vec![];
221
222 if let Some(pages) = &self.pages {
223 transforms.push(ExportTransform::Pages {
224 ranges: pages.clone(),
225 });
226 }
227
228 let export = ExportTask {
229 when,
230 output,
231 transform: transforms,
232 };
233
234 let config = match output_format {
235 OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
236 export,
237 pages: self.pages.clone(),
238 pdf_standards: self.pdf.standard.clone(),
239 no_pdf_tags: self.pdf.no_tags,
240 creation_timestamp: None,
241 }),
242 OutputFormat::Png => ProjectTask::ExportPng(ExportPngTask {
243 export,
244 pages: self.pages.clone(),
245 page_number_template: None,
246 merge: None,
247 ppi: self.png.ppi.try_into().unwrap(),
248 fill: None,
249 }),
250 OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask {
251 export,
252 pages: self.pages.clone(),
253 page_number_template: None,
254 merge: None,
255 }),
256 OutputFormat::Html => ProjectTask::ExportHtml(ExportHtmlTask { export }),
257 };
258
259 Ok(ApplyProjectTask {
260 id: task_id.clone(),
261 document: doc_id,
262 task: config,
263 })
264 }
265}