Skip to content

Commit 8ceb30e

Browse files
feat: add exclude_external option (#14)
* feat: add exclude_external option * test: add integration tests for exclude_external
1 parent 9ac0461 commit 8ceb30e

File tree

3 files changed

+890
-12
lines changed

3 files changed

+890
-12
lines changed

src/lib.rs

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)