@@ -35,14 +35,24 @@ mod resolver;
35
35
36
36
pub use error:: InlineError ;
37
37
use indexmap:: IndexMap ;
38
- use std:: { borrow:: Cow , fmt:: Formatter , hash:: BuildHasherDefault , io:: Write , sync:: Arc } ;
38
+ use lru:: { DefaultHasher , LruCache } ;
39
+ use std:: {
40
+ borrow:: Cow ,
41
+ fmt:: Formatter ,
42
+ hash:: BuildHasherDefault ,
43
+ io:: Write ,
44
+ sync:: { Arc , Mutex } ,
45
+ } ;
39
46
40
47
use crate :: html:: ElementStyleMap ;
41
48
use hasher:: BuildNoHashHasher ;
42
49
use html:: Document ;
43
50
pub use resolver:: { DefaultStylesheetResolver , StylesheetResolver } ;
44
51
pub use url:: { ParseError , Url } ;
45
52
53
+ /// An LRU Cache for external stylesheets.
54
+ pub type StylesheetCache < S = DefaultHasher > = LruCache < String , String , S > ;
55
+
46
56
/// Configuration options for CSS inlining process.
47
57
#[ allow( clippy:: struct_excessive_bools) ]
48
58
pub struct InlineOptions < ' a > {
@@ -59,6 +69,8 @@ pub struct InlineOptions<'a> {
59
69
pub base_url : Option < Url > ,
60
70
/// Whether remote stylesheets should be loaded or not.
61
71
pub load_remote_stylesheets : bool ,
72
+ /// External stylesheet cache.
73
+ pub cache : Option < Mutex < StylesheetCache > > ,
62
74
// The point of using `Cow` here is Python bindings, where it is problematic to pass a reference
63
75
// without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in
64
76
// Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side
@@ -79,6 +91,7 @@ impl<'a> std::fmt::Debug for InlineOptions<'a> {
79
91
. field ( "keep_link_tags" , & self . keep_link_tags )
80
92
. field ( "base_url" , & self . base_url )
81
93
. field ( "load_remote_stylesheets" , & self . load_remote_stylesheets )
94
+ . field ( "cache" , & self . cache )
82
95
. field ( "extra_css" , & self . extra_css )
83
96
. field ( "preallocate_node_capacity" , & self . preallocate_node_capacity )
84
97
. finish_non_exhaustive ( )
@@ -121,6 +134,17 @@ impl<'a> InlineOptions<'a> {
121
134
self
122
135
}
123
136
137
+ /// Set external stylesheet cache size.
138
+ #[ must_use]
139
+ pub fn cache ( mut self , cache : impl Into < Option < StylesheetCache > > ) -> Self {
140
+ if let Some ( cache) = cache. into ( ) {
141
+ self . cache = Some ( Mutex :: new ( cache) ) ;
142
+ } else {
143
+ self . cache = None ;
144
+ }
145
+ self
146
+ }
147
+
124
148
/// Set additional CSS to inline.
125
149
#[ must_use]
126
150
pub fn extra_css ( mut self , extra_css : Option < Cow < ' a , str > > ) -> Self {
@@ -158,6 +182,7 @@ impl Default for InlineOptions<'_> {
158
182
keep_link_tags : false ,
159
183
base_url : None ,
160
184
load_remote_stylesheets : true ,
185
+ cache : None ,
161
186
extra_css : None ,
162
187
preallocate_node_capacity : 32 ,
163
188
resolver : Arc :: new ( DefaultStylesheetResolver ) ,
@@ -247,7 +272,13 @@ impl<'a> CSSInliner<'a> {
247
272
/// - Remote stylesheet is not available;
248
273
/// - IO errors;
249
274
/// - Internal CSS selector parsing error;
275
+ ///
276
+ /// # Panics
277
+ ///
278
+ /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
279
+ /// using the same inliner panicked while resolving external stylesheets.
250
280
#[ inline]
281
+ #[ allow( clippy:: too_many_lines) ]
251
282
pub fn inline_to < W : Write > ( & self , html : & str , target : & mut W ) -> Result < ( ) > {
252
283
let document =
253
284
Document :: parse_with_options ( html. as_bytes ( ) , self . options . preallocate_node_capacity ) ;
@@ -285,9 +316,23 @@ impl<'a> CSSInliner<'a> {
285
316
links. dedup ( ) ;
286
317
for href in & links {
287
318
let url = self . get_full_url ( href) ;
319
+ if let Some ( lock) = self . options . cache . as_ref ( ) {
320
+ let mut cache = lock. lock ( ) . expect ( "Cache lock is poisoned" ) ;
321
+ if let Some ( cached) = cache. get ( url. as_ref ( ) ) {
322
+ raw_styles. push_str ( cached) ;
323
+ raw_styles. push ( '\n' ) ;
324
+ continue ;
325
+ }
326
+ }
327
+
288
328
let css = self . options . resolver . retrieve ( url. as_ref ( ) ) ?;
289
329
raw_styles. push_str ( & css) ;
290
330
raw_styles. push ( '\n' ) ;
331
+
332
+ if let Some ( lock) = self . options . cache . as_ref ( ) {
333
+ let mut cache = lock. lock ( ) . expect ( "Cache lock is poisoned" ) ;
334
+ cache. put ( url. into_owned ( ) , css) ;
335
+ }
291
336
}
292
337
}
293
338
if let Some ( extra_css) = & self . options . extra_css {
@@ -415,3 +460,15 @@ pub fn inline(html: &str) -> Result<String> {
415
460
pub fn inline_to < W : Write > ( html : & str , target : & mut W ) -> Result < ( ) > {
416
461
CSSInliner :: default ( ) . inline_to ( html, target)
417
462
}
463
+
464
+ #[ cfg( test) ]
465
+ mod tests {
466
+ use crate :: { CSSInliner , InlineOptions } ;
467
+
468
+ #[ test]
469
+ fn test_inliner_sync_send ( ) {
470
+ fn assert_send < T : Send + Sync > ( ) { }
471
+ assert_send :: < CSSInliner < ' _ > > ( ) ;
472
+ assert_send :: < InlineOptions < ' _ > > ( ) ;
473
+ }
474
+ }
0 commit comments