Skip to content

Commit fc573db

Browse files
feat: configure the output path of pdf files
1 parent 988b09a commit fc573db

File tree

7 files changed

+168
-36
lines changed

7 files changed

+168
-36
lines changed

crates/tinymist/src/actor.rs

+8-7
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,25 @@ impl TypstLanguageServer {
2222
let (doc_tx, doc_rx) = watch::channel(None);
2323
let (render_tx, _) = broadcast::channel(10);
2424

25+
let roots = self.roots.clone();
26+
let root_dir = roots.first().cloned().unwrap_or_default();
2527
// Run the PDF export actor before preparing cluster to avoid loss of events
2628
tokio::spawn(
2729
PdfExportActor::new(
2830
doc_rx.clone(),
2931
render_tx.subscribe(),
30-
Some(PdfExportConfig {
31-
path: entry
32-
.as_ref()
33-
.map(|e| e.clone().with_extension("pdf").into()),
32+
PdfExportConfig {
33+
substitute_pattern: self.config.output_path.clone(),
34+
root: root_dir.clone().into(),
35+
path: entry.clone().map(From::from),
3436
mode: self.config.export_pdf,
35-
}),
37+
},
3638
)
3739
.run(),
3840
);
3941

40-
let roots = self.roots.clone();
4142
let opts = CompileOpts {
42-
root_dir: roots.first().cloned().unwrap_or_default(),
43+
root_dir,
4344
// todo: font paths
4445
// font_paths: arguments.font_paths.clone(),
4546
with_embedded_fonts: typst_assets::fonts().map(Cow::Borrowed).collect(),

crates/tinymist/src/actor/render.rs

+97-10
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,28 @@ use tokio::sync::{
1212
watch,
1313
};
1414
use typst::foundations::Smart;
15-
use typst_ts_core::{ImmutPath, TypstDocument};
15+
use typst_ts_core::{path::PathClean, ImmutPath, TypstDocument};
1616

1717
use crate::ExportPdfMode;
1818

1919
#[derive(Debug, Clone)]
2020
pub enum RenderActorRequest {
2121
OnTyped,
2222
OnSaved(PathBuf),
23-
ChangeExportPath(Option<ImmutPath>),
23+
ChangeExportPath(PdfPathVars),
2424
ChangeConfig(PdfExportConfig),
2525
}
2626

27+
#[derive(Debug, Clone)]
28+
pub struct PdfPathVars {
29+
pub root: ImmutPath,
30+
pub path: Option<ImmutPath>,
31+
}
32+
2733
#[derive(Debug, Clone)]
2834
pub struct PdfExportConfig {
35+
pub substitute_pattern: String,
36+
pub root: ImmutPath,
2937
pub path: Option<ImmutPath>,
3038
pub mode: ExportPdfMode,
3139
}
@@ -34,6 +42,8 @@ pub struct PdfExportActor {
3442
render_rx: broadcast::Receiver<RenderActorRequest>,
3543
document: watch::Receiver<Option<Arc<TypstDocument>>>,
3644

45+
pub substitute_pattern: String,
46+
pub root: ImmutPath,
3747
pub path: Option<ImmutPath>,
3848
pub mode: ExportPdfMode,
3949
}
@@ -42,13 +52,15 @@ impl PdfExportActor {
4252
pub fn new(
4353
document: watch::Receiver<Option<Arc<TypstDocument>>>,
4454
render_rx: broadcast::Receiver<RenderActorRequest>,
45-
config: Option<PdfExportConfig>,
55+
config: PdfExportConfig,
4656
) -> Self {
4757
Self {
4858
render_rx,
4959
document,
50-
path: config.as_ref().and_then(|c| c.path.clone()),
51-
mode: config.map(|c| c.mode).unwrap_or(ExportPdfMode::Auto),
60+
substitute_pattern: config.substitute_pattern,
61+
root: config.root,
62+
path: config.path,
63+
mode: config.mode,
5264
}
5365
}
5466

@@ -72,11 +84,14 @@ impl PdfExportActor {
7284
info!("PdfRenderActor: received request: {req:?}", req = req);
7385
match req {
7486
RenderActorRequest::ChangeConfig(cfg) => {
87+
self.substitute_pattern = cfg.substitute_pattern;
88+
self.root = cfg.root;
7589
self.path = cfg.path;
7690
self.mode = cfg.mode;
7791
}
7892
RenderActorRequest::ChangeExportPath(cfg) => {
79-
self.path = cfg;
93+
self.root = cfg.root;
94+
self.path = cfg.path;
8095
}
8196
_ => {
8297
self.check_mode_and_export(req).await;
@@ -99,7 +114,10 @@ impl PdfExportActor {
99114
_ => unreachable!(),
100115
};
101116

102-
info!("PdfRenderActor: check path {:?}", self.path);
117+
info!(
118+
"PdfRenderActor: check path {:?} with output directory {}",
119+
self.path, self.substitute_pattern
120+
);
103121
if let Some(path) = self.path.as_ref() {
104122
if (get_mode(self.mode) == eq_mode) || validate_document(&req, self.mode, &document) {
105123
let Err(err) = self.export_pdf(&document, path).await else {
@@ -135,15 +153,84 @@ impl PdfExportActor {
135153
}
136154

137155
async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<()> {
156+
let Some(to) = substitute_path(&self.substitute_pattern, &self.root, path) else {
157+
return Err(anyhow::anyhow!("failed to substitute path"));
158+
};
159+
if to.is_relative() {
160+
return Err(anyhow::anyhow!("path is relative: {to:?}"));
161+
}
162+
if to.is_dir() {
163+
return Err(anyhow::anyhow!("path is a directory: {to:?}"));
164+
}
165+
166+
let to = to.with_extension("pdf");
167+
info!("exporting PDF {path:?} to {to:?}");
168+
169+
if let Some(e) = to.parent() {
170+
if !e.exists() {
171+
std::fs::create_dir_all(e).context("failed to create directory")?;
172+
}
173+
}
174+
138175
// todo: Some(pdf_uri.as_str())
139176
// todo: timestamp world.now()
140-
info!("exporting PDF {path}", path = path.display());
141-
142177
let data = typst_pdf::pdf(doc, Smart::Auto, None);
143178

144-
std::fs::write(path, data).context("failed to export PDF")?;
179+
std::fs::write(to, data).context("failed to export PDF")?;
145180

146181
info!("PDF export complete");
147182
Ok(())
148183
}
149184
}
185+
186+
#[comemo::memoize]
187+
fn substitute_path(substitute_pattern: &str, root: &Path, path: &Path) -> Option<ImmutPath> {
188+
if substitute_pattern.is_empty() {
189+
return Some(path.to_path_buf().clean().into());
190+
}
191+
192+
let path = path.strip_prefix(root).ok()?;
193+
let dir = path.parent();
194+
let file_name = path.file_name().unwrap_or_default();
195+
196+
let w = root.to_string_lossy();
197+
let f = file_name.to_string_lossy();
198+
199+
// replace all $root
200+
let mut path = substitute_pattern.replace("$root", &w);
201+
if let Some(dir) = dir {
202+
let d = dir.to_string_lossy();
203+
path = path.replace("$dir", &d);
204+
}
205+
path = path.replace("$name", &f);
206+
207+
Some(PathBuf::from(path).clean().into())
208+
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use super::*;
213+
214+
#[test]
215+
fn test_substitute_path() {
216+
let root = Path::new("/root");
217+
let path = Path::new("/root/dir1/dir2/file.txt");
218+
219+
assert_eq!(
220+
substitute_path("/substitute/$dir/$name", root, path),
221+
Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
222+
);
223+
assert_eq!(
224+
substitute_path("/substitute/$dir/../$name", root, path),
225+
Some(PathBuf::from("/substitute/dir1/file.txt").into())
226+
);
227+
assert_eq!(
228+
substitute_path("/substitute/$name", root, path),
229+
Some(PathBuf::from("/substitute/file.txt").into())
230+
);
231+
assert_eq!(
232+
substitute_path("/substitute/target/$dir/$name", root, path),
233+
Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
234+
);
235+
}
236+
}

crates/tinymist/src/actor/typst.rs

+18-10
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use typst_ts_core::{
3737

3838
use super::compile::CompileClient as TsCompileClient;
3939
use super::{compile::CompileActor as CompileActorInner, render::PdfExportConfig};
40-
use crate::actor::render::RenderActorRequest;
40+
use crate::actor::render::{PdfPathVars, RenderActorRequest};
4141
use crate::ConstConfig;
4242

4343
type CompileService<H> = CompileActorInner<Reporter<CompileExporter<CompileDriver>, H>>;
@@ -80,14 +80,15 @@ pub fn create_server(
8080
inner: driver,
8181
cb: handler.clone(),
8282
};
83-
let driver = CompileActorInner::new(driver, root).with_watch(true);
83+
let driver = CompileActorInner::new(driver, root.clone()).with_watch(true);
8484

8585
let (server, client) = driver.split();
8686

8787
current_runtime.spawn(server.spawn());
8888

8989
let this = CompileActor::new(
9090
diag_group,
91+
root.into(),
9192
cfg.position_encoding,
9293
handler,
9394
client,
@@ -297,6 +298,7 @@ pub struct CompileActor {
297298
diag_group: String,
298299
position_encoding: PositionEncoding,
299300
handler: CompileHandler,
301+
root: ImmutPath,
300302
entry: Arc<Mutex<Option<ImmutPath>>>,
301303
pub inner: CompileClient<CompileHandler>,
302304
render_tx: broadcast::Sender<RenderActorRequest>,
@@ -383,9 +385,10 @@ impl CompileActor {
383385
);
384386

385387
self.render_tx
386-
.send(RenderActorRequest::ChangeExportPath(Some(
387-
next.with_extension("pdf").into(),
388-
)))
388+
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
389+
root: self.root.clone(),
390+
path: Some(next.clone()),
391+
}))
389392
.unwrap();
390393

391394
// todo
@@ -402,9 +405,10 @@ impl CompileActor {
402405

403406
if res.is_err() {
404407
self.render_tx
405-
.send(RenderActorRequest::ChangeExportPath(
406-
prev.clone().map(|e| e.with_extension("pdf").into()),
407-
))
408+
.send(RenderActorRequest::ChangeExportPath(PdfPathVars {
409+
root: self.root.clone(),
410+
path: prev.clone(),
411+
}))
408412
.unwrap();
409413

410414
let mut entry = entry.lock();
@@ -423,16 +427,18 @@ impl CompileActor {
423427
Ok(())
424428
}
425429

426-
pub(crate) fn change_export_pdf(&self, export_pdf: crate::ExportPdfMode) {
430+
pub(crate) fn change_export_pdf(&self, config: PdfExportConfig) {
427431
let entry = self.entry.lock();
428432
let path = entry
429433
.as_ref()
430434
.map(|e| e.clone().with_extension("pdf").into());
431435
let _ = self
432436
.render_tx
433437
.send(RenderActorRequest::ChangeConfig(PdfExportConfig {
438+
substitute_pattern: config.substitute_pattern,
439+
root: self.root.clone(),
434440
path,
435-
mode: export_pdf,
441+
mode: config.mode,
436442
}))
437443
.unwrap();
438444
}
@@ -526,13 +532,15 @@ impl CompileHost for CompileActor {}
526532
impl CompileActor {
527533
fn new(
528534
diag_group: String,
535+
root: ImmutPath,
529536
position_encoding: PositionEncoding,
530537
handler: CompileHandler,
531538
inner: CompileClient<CompileHandler>,
532539
render_tx: broadcast::Sender<RenderActorRequest>,
533540
) -> Self {
534541
Self {
535542
diag_group,
543+
root,
536544
position_encoding,
537545
handler,
538546
entry: Arc::new(Mutex::new(None)),

crates/tinymist/src/init.rs

+10
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ pub enum SemanticTokensMode {
143143
type Listener<T> = Box<dyn FnMut(&T) -> anyhow::Result<()>>;
144144

145145
const CONFIG_ITEMS: &[&str] = &[
146+
"outputPath",
146147
"exportPdf",
147148
"rootPath",
148149
"semanticTokens",
@@ -152,6 +153,8 @@ const CONFIG_ITEMS: &[&str] = &[
152153
/// The user configuration read from the editor.
153154
#[derive(Default)]
154155
pub struct Config {
156+
/// The output directory for PDF export.
157+
pub output_path: String,
155158
/// The mode of PDF export.
156159
pub export_pdf: ExportPdfMode,
157160
/// Specifies the root path of the project manually.
@@ -210,6 +213,12 @@ impl Config {
210213
/// # Errors
211214
/// Errors if the update is invalid.
212215
pub fn update_by_map(&mut self, update: &Map<String, JsonValue>) -> anyhow::Result<()> {
216+
if let Some(JsonValue::String(output_path)) = update.get("outputPath") {
217+
self.output_path = output_path.to_owned();
218+
} else {
219+
self.output_path = String::new();
220+
}
221+
213222
let export_pdf = update
214223
.get("exportPdf")
215224
.map(ExportPdfMode::deserialize)
@@ -218,6 +227,7 @@ impl Config {
218227
self.export_pdf = export_pdf;
219228
}
220229

230+
// todo: it doesn't respect the root path
221231
let root_path = update.get("rootPath");
222232
if let Some(root_path) = root_path {
223233
if root_path.is_null() {

crates/tinymist/src/lib.rs

+14-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ use typst::util::Deferred;
6262

6363
pub type MaySyncResult<'a> = Result<JsonValue, BoxFuture<'a, JsonValue>>;
6464

65+
use crate::actor::render::PdfExportConfig;
6566
use crate::init::*;
6667

6768
// Enforces drop order
@@ -793,17 +794,27 @@ impl TypstLanguageServer {
793794
}
794795

795796
fn on_changed_configuration(&mut self, values: Map<String, JsonValue>) -> LspResult<()> {
797+
let output_directory = self.config.output_path.clone();
796798
let export_pdf = self.config.export_pdf;
797799
match self.config.update_by_map(&values) {
798800
Ok(()) => {
799801
info!("new settings applied");
800802

801-
if export_pdf != self.config.export_pdf {
802-
self.primary().change_export_pdf(self.config.export_pdf);
803+
if output_directory != self.config.output_path
804+
|| export_pdf != self.config.export_pdf
805+
{
806+
let config = PdfExportConfig {
807+
substitute_pattern: self.config.output_path.clone(),
808+
mode: self.config.export_pdf,
809+
root: Path::new("").into(),
810+
path: None,
811+
};
812+
813+
self.primary().change_export_pdf(config.clone());
803814
{
804815
let m = self.main.lock();
805816
if let Some(main) = m.as_ref() {
806-
main.wait().change_export_pdf(self.config.export_pdf);
817+
main.wait().change_export_pdf(config);
807818
}
808819
}
809820
}

0 commit comments

Comments
 (0)