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