@@ -5,40 +5,16 @@ mod formatting;
5
5
6
6
pub use error:: OgImageError ;
7
7
8
- use crate :: formatting:: { format_bytes , format_number } ;
8
+ use crate :: formatting:: { serialize_bytes , serialize_number , serialize_optional_number } ;
9
9
use bytes:: Bytes ;
10
10
use crates_io_env_vars:: var;
11
- use minijinja:: { Environment , context} ;
12
11
use serde:: Serialize ;
13
12
use std:: collections:: HashMap ;
14
13
use std:: path:: PathBuf ;
15
- use std:: sync:: LazyLock ;
16
14
use tempfile:: NamedTempFile ;
17
15
use tokio:: fs;
18
16
use tokio:: process:: Command ;
19
17
20
- static TEMPLATE_ENV : LazyLock < Environment < ' _ > > = LazyLock :: new ( || {
21
- let mut env = Environment :: new ( ) ;
22
-
23
- // Add custom filter for escaping Typst special characters
24
- env. add_filter ( "typst_escape" , |value : String | -> String {
25
- value
26
- . replace ( '\\' , "\\ \\ " ) // Escape backslashes first
27
- . replace ( '"' , "\\ \" " ) // Escape double quotes
28
- // Note: No need to escape # characters when inside double-quoted strings
29
- } ) ;
30
-
31
- // Add custom filter for formatting byte sizes
32
- env. add_filter ( "format_bytes" , format_bytes) ;
33
-
34
- // Add custom filter for formatting numbers with k/M suffixes
35
- env. add_filter ( "format_number" , format_number) ;
36
-
37
- let template_str = include_str ! ( "../templates/og-image.typ.j2" ) ;
38
- env. add_template ( "og-image.typ" , template_str) . unwrap ( ) ;
39
- env
40
- } ) ;
41
-
42
18
/// Data structure containing information needed to generate an OpenGraph image
43
19
/// for a crates.io crate.
44
20
#[ derive( Debug , Clone , Serialize ) ]
@@ -56,10 +32,13 @@ pub struct OgImageData<'a> {
56
32
/// Author information
57
33
pub authors : & ' a [ OgImageAuthorData < ' a > ] ,
58
34
/// Source lines of code count (optional)
35
+ #[ serde( serialize_with = "serialize_optional_number" ) ]
59
36
pub lines_of_code : Option < u32 > ,
60
37
/// Package size in bytes
38
+ #[ serde( serialize_with = "serialize_bytes" ) ]
61
39
pub crate_size : u32 ,
62
40
/// Total number of releases
41
+ #[ serde( serialize_with = "serialize_number" ) ]
63
42
pub releases : u32 ,
64
43
}
65
44
@@ -187,20 +166,6 @@ impl OgImageGenerator {
187
166
Ok ( avatar_map)
188
167
}
189
168
190
- /// Generates the Typst template content from the provided data.
191
- ///
192
- /// This private method renders the Jinja2 template with the provided data
193
- /// and returns the resulting Typst markup as a string.
194
- fn generate_template (
195
- & self ,
196
- data : & OgImageData < ' _ > ,
197
- avatar_map : & HashMap < & str , String > ,
198
- ) -> Result < String , OgImageError > {
199
- let template = TEMPLATE_ENV . get_template ( "og-image.typ" ) ?;
200
- let rendered = template. render ( context ! { data, avatar_map } ) ?;
201
- Ok ( rendered)
202
- }
203
-
204
169
/// Generates an OpenGraph image using the provided data.
205
170
///
206
171
/// This method creates a temporary directory with all the necessary files
@@ -258,19 +223,30 @@ impl OgImageGenerator {
258
223
// Process avatars - download URLs and copy assets
259
224
let avatar_map = self . process_avatars ( & data, & assets_dir) . await ?;
260
225
261
- // Create og-image.typ file using minijinja template
262
- let rendered = self . generate_template ( & data , & avatar_map ) ? ;
226
+ // Copy the static Typst template file
227
+ let template_content = include_str ! ( "../templates/og-image.typ" ) ;
263
228
let typ_file_path = temp_dir. path ( ) . join ( "og-image.typ" ) ;
264
- fs:: write ( & typ_file_path, rendered ) . await ?;
229
+ fs:: write ( & typ_file_path, template_content ) . await ?;
265
230
266
231
// Create a named temp file for the output PNG
267
232
let output_file = NamedTempFile :: new ( ) . map_err ( OgImageError :: TempFileError ) ?;
268
233
269
- // Run typst compile command
234
+ // Serialize data and avatar_map to JSON
235
+ let json_data = serde_json:: to_string ( & data) ;
236
+ let json_data = json_data. map_err ( OgImageError :: JsonSerializationError ) ?;
237
+
238
+ let json_avatar_map = serde_json:: to_string ( & avatar_map) ;
239
+ let json_avatar_map = json_avatar_map. map_err ( OgImageError :: JsonSerializationError ) ?;
240
+
241
+ // Run typst compile command with input data
270
242
let output = Command :: new ( & self . typst_binary_path )
271
243
. arg ( "compile" )
272
244
. arg ( "--format" )
273
245
. arg ( "png" )
246
+ . arg ( "--input" )
247
+ . arg ( format ! ( "data={}" , json_data) )
248
+ . arg ( "--input" )
249
+ . arg ( format ! ( "avatar_map={}" , json_avatar_map) )
274
250
. arg ( & typ_file_path)
275
251
. arg ( output_file. path ( ) )
276
252
. output ( )
@@ -313,22 +289,6 @@ mod tests {
313
289
OgImageAuthorData :: new ( name, Some ( "test-avatar" ) )
314
290
}
315
291
316
- fn create_standard_test_data ( ) -> OgImageData < ' static > {
317
- static AUTHORS : & [ OgImageAuthorData < ' _ > ] = & [ author_with_avatar ( "alice" ) , author ( "bob" ) ] ;
318
-
319
- OgImageData {
320
- name : "example-crate" ,
321
- version : "v2.1.0" ,
322
- description : "A comprehensive example crate showcasing various OpenGraph features" ,
323
- license : "MIT OR Apache-2.0" ,
324
- tags : & [ "web" , "api" , "async" , "json" , "http" ] ,
325
- authors : AUTHORS ,
326
- lines_of_code : Some ( 5500 ) ,
327
- crate_size : 128000 ,
328
- releases : 15 ,
329
- }
330
- }
331
-
332
292
fn create_minimal_test_data ( ) -> OgImageData < ' static > {
333
293
static AUTHORS : & [ OgImageAuthorData < ' _ > ] = & [ author ( "author" ) ] ;
334
294
@@ -428,12 +388,6 @@ mod tests {
428
388
. is_err ( )
429
389
}
430
390
431
- fn generate_template ( data : OgImageData < ' _ > , avatar_map : HashMap < & str , String > ) -> String {
432
- OgImageGenerator :: default ( )
433
- . generate_template ( & data, & avatar_map)
434
- . expect ( "Failed to generate template" )
435
- }
436
-
437
391
async fn generate_image ( data : OgImageData < ' _ > ) -> Option < Vec < u8 > > {
438
392
if skip_if_typst_unavailable ( ) {
439
393
return None ;
@@ -449,33 +403,6 @@ mod tests {
449
403
Some ( std:: fs:: read ( temp_file. path ( ) ) . expect ( "Failed to read generated image" ) )
450
404
}
451
405
452
- #[ test]
453
- fn test_generate_template_snapshot ( ) {
454
- let data = create_standard_test_data ( ) ;
455
- let avatar_map = HashMap :: from ( [ ( "test-avatar" , "avatar_0.png" . to_string ( ) ) ] ) ;
456
-
457
- let template_content = generate_template ( data, avatar_map) ;
458
- insta:: assert_snapshot!( "generated_template.typ" , template_content) ;
459
- }
460
-
461
- #[ test]
462
- fn test_generate_template_minimal_snapshot ( ) {
463
- let data = create_minimal_test_data ( ) ;
464
- let avatar_map = HashMap :: new ( ) ;
465
-
466
- let template_content = generate_template ( data, avatar_map) ;
467
- insta:: assert_snapshot!( "generated_template_minimal.typ" , template_content) ;
468
- }
469
-
470
- #[ test]
471
- fn test_generate_template_escaping_snapshot ( ) {
472
- let data = create_escaping_test_data ( ) ;
473
- let avatar_map = HashMap :: from ( [ ( "test-avatar" , "avatar_0.png" . to_string ( ) ) ] ) ;
474
-
475
- let template_content = generate_template ( data, avatar_map) ;
476
- insta:: assert_snapshot!( "generated_template_escaping.typ" , template_content) ;
477
- }
478
-
479
406
#[ tokio:: test]
480
407
async fn test_generate_og_image_snapshot ( ) {
481
408
let data = create_simple_test_data ( ) ;
0 commit comments