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