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