@@ -12,20 +12,28 @@ use tokio::sync::{
12
12
watch,
13
13
} ;
14
14
use typst:: foundations:: Smart ;
15
- use typst_ts_core:: { ImmutPath , TypstDocument } ;
15
+ use typst_ts_core:: { path :: PathClean , ImmutPath , TypstDocument } ;
16
16
17
17
use crate :: ExportPdfMode ;
18
18
19
19
#[ derive( Debug , Clone ) ]
20
20
pub enum RenderActorRequest {
21
21
OnTyped ,
22
22
OnSaved ( PathBuf ) ,
23
- ChangeExportPath ( Option < ImmutPath > ) ,
23
+ ChangeExportPath ( PdfPathVars ) ,
24
24
ChangeConfig ( PdfExportConfig ) ,
25
25
}
26
26
27
+ #[ derive( Debug , Clone ) ]
28
+ pub struct PdfPathVars {
29
+ pub root : ImmutPath ,
30
+ pub path : Option < ImmutPath > ,
31
+ }
32
+
27
33
#[ derive( Debug , Clone ) ]
28
34
pub struct PdfExportConfig {
35
+ pub substitute_pattern : String ,
36
+ pub root : ImmutPath ,
29
37
pub path : Option < ImmutPath > ,
30
38
pub mode : ExportPdfMode ,
31
39
}
@@ -34,6 +42,8 @@ pub struct PdfExportActor {
34
42
render_rx : broadcast:: Receiver < RenderActorRequest > ,
35
43
document : watch:: Receiver < Option < Arc < TypstDocument > > > ,
36
44
45
+ pub substitute_pattern : String ,
46
+ pub root : ImmutPath ,
37
47
pub path : Option < ImmutPath > ,
38
48
pub mode : ExportPdfMode ,
39
49
}
@@ -42,13 +52,15 @@ impl PdfExportActor {
42
52
pub fn new (
43
53
document : watch:: Receiver < Option < Arc < TypstDocument > > > ,
44
54
render_rx : broadcast:: Receiver < RenderActorRequest > ,
45
- config : Option < PdfExportConfig > ,
55
+ config : PdfExportConfig ,
46
56
) -> Self {
47
57
Self {
48
58
render_rx,
49
59
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 ,
52
64
}
53
65
}
54
66
@@ -72,11 +84,14 @@ impl PdfExportActor {
72
84
info!( "PdfRenderActor: received request: {req:?}" , req = req) ;
73
85
match req {
74
86
RenderActorRequest :: ChangeConfig ( cfg) => {
87
+ self . substitute_pattern = cfg. substitute_pattern;
88
+ self . root = cfg. root;
75
89
self . path = cfg. path;
76
90
self . mode = cfg. mode;
77
91
}
78
92
RenderActorRequest :: ChangeExportPath ( cfg) => {
79
- self . path = cfg;
93
+ self . root = cfg. root;
94
+ self . path = cfg. path;
80
95
}
81
96
_ => {
82
97
self . check_mode_and_export( req) . await ;
@@ -99,7 +114,10 @@ impl PdfExportActor {
99
114
_ => unreachable ! ( ) ,
100
115
} ;
101
116
102
- info ! ( "PdfRenderActor: check path {:?}" , self . path) ;
117
+ info ! (
118
+ "PdfRenderActor: check path {:?} with output directory {}" ,
119
+ self . path, self . substitute_pattern
120
+ ) ;
103
121
if let Some ( path) = self . path . as_ref ( ) {
104
122
if ( get_mode ( self . mode ) == eq_mode) || validate_document ( & req, self . mode , & document) {
105
123
let Err ( err) = self . export_pdf ( & document, path) . await else {
@@ -135,15 +153,84 @@ impl PdfExportActor {
135
153
}
136
154
137
155
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
+
138
175
// todo: Some(pdf_uri.as_str())
139
176
// todo: timestamp world.now()
140
- info ! ( "exporting PDF {path}" , path = path. display( ) ) ;
141
-
142
177
let data = typst_pdf:: pdf ( doc, Smart :: Auto , None ) ;
143
178
144
- std:: fs:: write ( path , data) . context ( "failed to export PDF" ) ?;
179
+ std:: fs:: write ( to , data) . context ( "failed to export PDF" ) ?;
145
180
146
181
info ! ( "PDF export complete" ) ;
147
182
Ok ( ( ) )
148
183
}
149
184
}
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
+ }
0 commit comments