@@ -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,18 @@ 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. debug_struct ( "InlineOptions" ) ;
86
+ debug
77
87
. field ( "inline_style_tags" , & self . inline_style_tags )
78
88
. field ( "keep_style_tags" , & self . keep_style_tags )
79
89
. field ( "keep_link_tags" , & self . keep_link_tags )
80
90
. field ( "base_url" , & self . base_url )
81
- . field ( "load_remote_stylesheets" , & self . load_remote_stylesheets )
91
+ . field ( "load_remote_stylesheets" , & self . load_remote_stylesheets ) ;
92
+ #[ cfg( feature = "stylesheet-cache" ) ]
93
+ {
94
+ debug. field ( "cache" , & self . cache ) ;
95
+ }
96
+ debug
82
97
. field ( "extra_css" , & self . extra_css )
83
98
. field ( "preallocate_node_capacity" , & self . preallocate_node_capacity )
84
99
. finish_non_exhaustive ( )
@@ -121,6 +136,18 @@ impl<'a> InlineOptions<'a> {
121
136
self
122
137
}
123
138
139
+ /// Set external stylesheet cache.
140
+ #[ must_use]
141
+ #[ cfg( feature = "stylesheet-cache" ) ]
142
+ pub fn cache ( mut self , cache : impl Into < Option < StylesheetCache > > ) -> Self {
143
+ if let Some ( cache) = cache. into ( ) {
144
+ self . cache = Some ( std:: sync:: Mutex :: new ( cache) ) ;
145
+ } else {
146
+ self . cache = None ;
147
+ }
148
+ self
149
+ }
150
+
124
151
/// Set additional CSS to inline.
125
152
#[ must_use]
126
153
pub fn extra_css ( mut self , extra_css : Option < Cow < ' a , str > > ) -> Self {
@@ -158,6 +185,8 @@ impl Default for InlineOptions<'_> {
158
185
keep_link_tags : false ,
159
186
base_url : None ,
160
187
load_remote_stylesheets : true ,
188
+ #[ cfg( feature = "stylesheet-cache" ) ]
189
+ cache : None ,
161
190
extra_css : None ,
162
191
preallocate_node_capacity : 32 ,
163
192
resolver : Arc :: new ( DefaultStylesheetResolver ) ,
@@ -247,7 +276,13 @@ impl<'a> CSSInliner<'a> {
247
276
/// - Remote stylesheet is not available;
248
277
/// - IO errors;
249
278
/// - Internal CSS selector parsing error;
279
+ ///
280
+ /// # Panics
281
+ ///
282
+ /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
283
+ /// using the same inliner panicked while resolving external stylesheets.
250
284
#[ inline]
285
+ #[ allow( clippy:: too_many_lines) ]
251
286
pub fn inline_to < W : Write > ( & self , html : & str , target : & mut W ) -> Result < ( ) > {
252
287
let document =
253
288
Document :: parse_with_options ( html. as_bytes ( ) , self . options . preallocate_node_capacity ) ;
@@ -285,9 +320,25 @@ impl<'a> CSSInliner<'a> {
285
320
links. dedup ( ) ;
286
321
for href in & links {
287
322
let url = self . get_full_url ( href) ;
323
+ #[ cfg( feature = "stylesheet-cache" ) ]
324
+ if let Some ( lock) = self . options . cache . as_ref ( ) {
325
+ let mut cache = lock. lock ( ) . expect ( "Cache lock is poisoned" ) ;
326
+ if let Some ( cached) = cache. get ( url. as_ref ( ) ) {
327
+ raw_styles. push_str ( cached) ;
328
+ raw_styles. push ( '\n' ) ;
329
+ continue ;
330
+ }
331
+ }
332
+
288
333
let css = self . options . resolver . retrieve ( url. as_ref ( ) ) ?;
289
334
raw_styles. push_str ( & css) ;
290
335
raw_styles. push ( '\n' ) ;
336
+
337
+ #[ cfg( feature = "stylesheet-cache" ) ]
338
+ if let Some ( lock) = self . options . cache . as_ref ( ) {
339
+ let mut cache = lock. lock ( ) . expect ( "Cache lock is poisoned" ) ;
340
+ cache. put ( url. into_owned ( ) , css) ;
341
+ }
291
342
}
292
343
}
293
344
if let Some ( extra_css) = & self . options . extra_css {
@@ -415,3 +466,15 @@ pub fn inline(html: &str) -> Result<String> {
415
466
pub fn inline_to < W : Write > ( html : & str , target : & mut W ) -> Result < ( ) > {
416
467
CSSInliner :: default ( ) . inline_to ( html, target)
417
468
}
469
+
470
+ #[ cfg( test) ]
471
+ mod tests {
472
+ use crate :: { CSSInliner , InlineOptions } ;
473
+
474
+ #[ test]
475
+ fn test_inliner_sync_send ( ) {
476
+ fn assert_send < T : Send + Sync > ( ) { }
477
+ assert_send :: < CSSInliner < ' _ > > ( ) ;
478
+ assert_send :: < InlineOptions < ' _ > > ( ) ;
479
+ }
480
+ }
0 commit comments