@@ -92,11 +92,23 @@ pub enum KeyframesStrategy {
9292}
9393
9494#[ derive( Debug , Clone ) ]
95- pub enum SelectorMatcher {
95+ pub enum Matcher {
9696 String ( String ) ,
9797 Regex ( Regex ) ,
9898}
99- impl Serialize for SelectorMatcher {
99+ impl Matcher {
100+ pub fn matches ( & self , value : & str ) -> bool {
101+ match self {
102+ Matcher :: Regex ( regex) => regex. is_match ( value) ,
103+ Matcher :: String ( exp) => exp == value,
104+ }
105+ }
106+ }
107+
108+ #[ deprecated( note = "Use `Matcher` instead." ) ]
109+ pub use Matcher as SelectorMatcher ;
110+
111+ impl Serialize for Matcher {
100112 fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
101113 where
102114 S : serde:: Serializer ,
@@ -107,7 +119,7 @@ impl Serialize for SelectorMatcher {
107119 } )
108120 }
109121}
110- impl < ' de > Deserialize < ' de > for SelectorMatcher {
122+ impl < ' de > Deserialize < ' de > for Matcher {
111123 fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
112124 where
113125 D : serde:: Deserializer < ' de > ,
@@ -149,7 +161,7 @@ pub struct CrittersOptions {
149161 /// Remove inlined rules from the external stylesheet
150162 #[ clap( long) ]
151163 pub prune_source : bool ,
152- /// Merged inlined stylesheets into a single `<style>` tag
164+ /// Merge inlined stylesheets into a single `<style>` tag
153165 #[ clap( long, action = clap:: ArgAction :: Set , default_value_t = true ) ]
154166 pub merge_stylesheets : bool ,
155167 /// Glob for matching other stylesheets to be used while looking for critical CSS.
@@ -181,7 +193,13 @@ pub struct CrittersOptions {
181193 /// Provide a list of selectors that should be included in the critical CSS.
182194 #[ clap( skip) ]
183195 #[ cfg_attr( feature = "typegen" , ts( as = "Vec<String>" ) ) ]
184- pub allow_rules : Vec < SelectorMatcher > ,
196+ pub allow_rules : Vec < Matcher > ,
197+ /// List of external stylesheets that should be inlined without an external
198+ /// stylesheet reference. Links to these stylesheets will be removed, and
199+ /// only the matched selectors will be preserved.
200+ #[ clap( skip) ]
201+ #[ cfg_attr( feature = "typegen" , ts( as = "Vec<String>" ) ) ]
202+ pub exclude_external : Vec < Matcher > ,
185203}
186204
187205/// Statistics resulting from `Critters::process_dir`.
@@ -215,6 +233,7 @@ impl default::Default for CrittersOptions {
215233 keyframes : Default :: default ( ) ,
216234 compress : true ,
217235 allow_rules : Default :: default ( ) ,
236+ exclude_external : Default :: default ( ) ,
218237 }
219238 }
220239}
@@ -487,10 +506,12 @@ impl Critters {
487506 }
488507
489508 // allow rules
490- if self . options . allow_rules . iter ( ) . any ( |exp| match exp {
491- SelectorMatcher :: Regex ( regex) => regex. is_match ( & selector) ,
492- SelectorMatcher :: String ( exp) => exp == & selector,
493- } ) {
509+ if self
510+ . options
511+ . allow_rules
512+ . iter ( )
513+ . any ( |m| m. matches ( & selector) )
514+ {
494515 return true ;
495516 }
496517
@@ -712,6 +733,16 @@ impl Critters {
712733
713734 // TODO: inline threshold?
714735
736+ if self
737+ . options
738+ . exclude_external
739+ . iter ( )
740+ . any ( |m| m. matches ( & href) )
741+ {
742+ link. detach ( ) ;
743+ return Ok ( Some ( style) ) ;
744+ }
745+
715746 let body = dom
716747 . select_first ( "body" )
717748 . map_err ( |_| anyhow:: Error :: msg ( "Failed to locate document body" ) ) ?;
@@ -1030,6 +1061,44 @@ mod tests {
10301061 ) ;
10311062 }
10321063
1064+ #[ test]
1065+ fn external_stylesheet_exclude ( ) {
1066+ let tmp_dir = create_test_folder ( & [ ( "external.css" , BASIC_CSS ) ] ) ;
1067+
1068+ let html = construct_html (
1069+ r#"<link rel="stylesheet" href="external.css" />"# ,
1070+ r#"<div class="critical">Hello world</div>"# ,
1071+ ) ;
1072+
1073+ let critters = Critters :: new ( CrittersOptions {
1074+ path : tmp_dir,
1075+ external : true ,
1076+ preload : PreloadStrategy :: BodyPreload ,
1077+ exclude_external : vec ! [ Matcher :: Regex ( Regex :: new( "external\\ .css$" ) . unwrap( ) ) ] ,
1078+ ..Default :: default ( )
1079+ } ) ;
1080+
1081+ let processed = critters
1082+ . process ( & html)
1083+ . expect ( "Failed to inline critical css" ) ;
1084+
1085+ let parser = html:: parse_html ( ) ;
1086+ let dom = parser. one ( processed) ;
1087+
1088+ dom. select_first ( "link[rel=preload]" )
1089+ . expect_err ( "Unexpected preload link." ) ;
1090+
1091+ let stylesheet = dom
1092+ . select_first ( "style" )
1093+ . expect ( "Failed to locate inline stylesheet" )
1094+ . text_contents ( ) ;
1095+ assert ! ( stylesheet. contains( ".critical" ) ) ;
1096+ assert ! ( !stylesheet. contains( ".non-critical" ) ) ;
1097+
1098+ dom. select_first ( "link[rel=stylesheet]" )
1099+ . expect_err ( "Unexpected external stylesheet link." ) ;
1100+ }
1101+
10331102 #[ test]
10341103 fn additional_stylesheets ( ) {
10351104 let tmp_dir = create_test_folder ( & [ (
@@ -1251,7 +1320,7 @@ mod tests {
12511320 #[ test]
12521321 fn allow_rules_string ( ) {
12531322 let critters = Critters :: new ( CrittersOptions {
1254- allow_rules : vec ! [ SelectorMatcher :: String ( ".non-critical" . to_string( ) ) ] ,
1323+ allow_rules : vec ! [ Matcher :: String ( ".non-critical" . to_string( ) ) ] ,
12551324 ..Default :: default ( )
12561325 } ) ;
12571326
@@ -1268,7 +1337,7 @@ mod tests {
12681337 #[ test]
12691338 fn allow_rules_regex ( ) {
12701339 let critters = Critters :: new ( CrittersOptions {
1271- allow_rules : vec ! [ SelectorMatcher :: Regex ( Regex :: new( "^.non" ) . unwrap( ) ) ] ,
1340+ allow_rules : vec ! [ Matcher :: Regex ( Regex :: new( "^.non" ) . unwrap( ) ) ] ,
12721341 ..Default :: default ( )
12731342 } ) ;
12741343
0 commit comments