1#![doc = include_str!("../README.md")]
2#![warn(
3 clippy::pedantic,
4 clippy::doc_markdown,
5 clippy::redundant_closure,
6 clippy::explicit_iter_loop,
7 clippy::match_same_arms,
8 clippy::needless_borrow,
9 clippy::print_stdout,
10 clippy::arithmetic_side_effects,
11 clippy::cast_possible_truncation,
12 clippy::unwrap_used,
13 clippy::map_unwrap_or,
14 clippy::trivially_copy_pass_by_ref,
15 clippy::needless_pass_by_value,
16 missing_docs,
17 missing_debug_implementations,
18 trivial_casts,
19 trivial_numeric_casts,
20 unreachable_pub,
21 unused_extern_crates,
22 unused_import_braces,
23 unused_qualifications,
24 variant_size_differences,
25 rust_2018_idioms,
26 rust_2018_compatibility,
27 rust_2021_compatibility
28)]
29#![allow(clippy::module_name_repetitions)]
30pub mod error;
31mod html;
32mod parser;
33mod resolver;
34
35pub use error::InlineError;
36#[cfg(feature = "stylesheet-cache")]
37use lru::{DefaultHasher, LruCache};
38use selectors::context::SelectorCaches;
39use smallvec::SmallVec;
40use std::{borrow::Cow, fmt::Formatter, io::Write, iter::Peekable, ops::Range, sync::Arc};
41
42#[cfg(feature = "async")]
43pub use async_trait::async_trait;
44use html::{Document, InliningMode, NodeData, NodeId, Precedence};
45use parser::{LayerId, Layers, Rule, Span};
46pub use resolver::{DefaultStylesheetResolver, StylesheetResolver};
47use rustc_hash::FxHashMap;
48pub use url::{ParseError, Url};
49
50#[cfg(feature = "stylesheet-cache")]
52pub type StylesheetCache<S = DefaultHasher> = LruCache<String, String, S>;
53
54#[allow(clippy::struct_excessive_bools)]
56pub struct InlineOptions<'a> {
57 pub inline_style_tags: bool,
62 pub keep_style_tags: bool,
64 pub keep_link_tags: bool,
66 pub keep_at_rules: bool,
68 pub minify_css: bool,
70 pub base_url: Option<Url>,
72 pub load_remote_stylesheets: bool,
74 #[cfg(feature = "stylesheet-cache")]
76 pub cache: Option<std::sync::Mutex<StylesheetCache>>,
77 pub extra_css: Option<Cow<'a, str>>,
82 pub preallocate_node_capacity: usize,
85 pub resolver: Arc<dyn StylesheetResolver>,
87 pub remove_inlined_selectors: bool,
89 pub apply_width_attributes: bool,
94 pub apply_height_attributes: bool,
99 pub resolve_css_variables: bool,
104}
105
106impl std::fmt::Debug for InlineOptions<'_> {
107 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108 let mut debug = f.debug_struct("InlineOptions");
109 debug
110 .field("inline_style_tags", &self.inline_style_tags)
111 .field("keep_style_tags", &self.keep_style_tags)
112 .field("keep_link_tags", &self.keep_link_tags)
113 .field("base_url", &self.base_url)
114 .field("load_remote_stylesheets", &self.load_remote_stylesheets);
115 #[cfg(feature = "stylesheet-cache")]
116 {
117 debug.field("cache", &self.cache);
118 }
119 debug
120 .field("extra_css", &self.extra_css)
121 .field("preallocate_node_capacity", &self.preallocate_node_capacity)
122 .field("remove_inlined_selectors", &self.remove_inlined_selectors)
123 .field("apply_width_attributes", &self.apply_width_attributes)
124 .field("apply_height_attributes", &self.apply_height_attributes)
125 .field("resolve_css_variables", &self.resolve_css_variables)
126 .finish_non_exhaustive()
127 }
128}
129
130#[derive(Debug)]
131struct CssChunk {
132 range: Range<usize>,
133 style_node: Option<NodeId>,
136}
137
138type SelectorList<'i> = SmallVec<[&'i str; 2]>;
139
140#[derive(Debug)]
141struct SelectorUsage<'i> {
142 selector: &'i str,
143 declarations: (usize, usize),
144 rule_id: usize,
145 chunk_index: usize,
146 matched: bool,
147}
148
149#[derive(Debug, Default)]
150struct RuleRemainder<'i> {
151 selectors: SelectorList<'i>,
152 declarations: (usize, usize),
153 offset: usize,
155}
156
157#[derive(Debug, Default)]
158struct SelectorCleanupState<'i> {
159 chunks: Vec<CssChunk>,
160 usages: Vec<SelectorUsage<'i>>,
161}
162
163impl<'i> SelectorCleanupState<'i> {
164 fn record_usage(&mut self, usage: SelectorUsage<'i>) {
165 self.usages.push(usage);
166 }
167}
168
169struct LayerBlock<'i> {
171 range: Range<usize>,
172 prelude: &'i str,
173 layer: LayerId,
174 created: bool,
175}
176
177struct KeptCss<'a, 'i> {
180 source: &'i str,
181 layers: &'a Layers<'i>,
182 kept: Vec<bool>,
184}
185
186impl<'a, 'i> KeptCss<'a, 'i> {
187 fn new(
190 source: &'i str,
191 layers: &'a Layers<'i>,
192 spans: &[Span<'i>],
193 rules: &[Rule<'i>],
194 cleanup: Option<&SelectorCleanupState<'i>>,
195 verbatim_styles: bool,
196 ) -> Self {
197 let mut kept = vec![false; layers.len()];
198 let mut mark = |mut id: LayerId| {
199 while id != 0 && !kept[id] {
200 kept[id] = true;
201 id = layers.parent(id);
202 }
203 };
204 for span in spans {
205 if let Span::AtRule { layer, .. } = span {
206 mark(*layer);
207 }
208 }
209 if let Some(state) = cleanup {
210 for usage in state.usages.iter().filter(|usage| !usage.matched) {
211 mark(rules[usage.rule_id].layer);
212 }
213 }
214 if verbatim_styles {
215 for rule in rules {
216 mark(rule.layer);
217 }
218 }
219 KeptCss {
220 source,
221 layers,
222 kept,
223 }
224 }
225
226 fn is_kept(&self, mut layer: LayerId) -> bool {
228 while layer != 0 {
229 if self.kept[layer] {
230 return true;
231 }
232 layer = self.layers.parent(layer);
233 }
234 false
235 }
236
237 fn render(
240 &self,
241 spans: &[Span<'i>],
242 range: &Range<usize>,
243 mut items: Vec<(usize, String)>,
244 sep: &str,
245 ) -> Vec<String> {
246 let mut blocks = Vec::new();
247 for span in spans.iter().filter(|span| range.contains(&span.start())) {
248 match span {
249 Span::AtRule { range: at_rule, .. } => items.push((
251 at_rule.start,
252 self.source[at_rule.start..at_rule.end.min(range.end)]
253 .trim()
254 .to_string(),
255 )),
256 Span::LayerStatement {
257 start,
258 prelude,
259 layers,
260 } => {
261 if layers.iter().any(|&layer| self.is_kept(layer)) {
262 items.push((*start, format!("@layer {};", prelude.trim())));
263 }
264 }
265 Span::LayerBlock {
266 range: block,
267 prelude,
268 layer,
269 created,
270 } => blocks.push(LayerBlock {
271 range: block.start..block.end.min(range.end),
272 prelude,
273 layer: *layer,
274 created: *created,
275 }),
276 }
277 }
278 items.retain(|(_, text)| !text.is_empty());
279 items.sort_by_key(|(offset, _)| *offset);
280 blocks.sort_by_key(|block| block.range.start);
281 self.render_tree(&mut items.into_iter().peekable(), &blocks, range.end, sep)
282 }
283
284 #[allow(clippy::arithmetic_side_effects)]
286 fn render_tree(
287 &self,
288 items: &mut Peekable<std::vec::IntoIter<(usize, String)>>,
289 blocks: &[LayerBlock<'i>],
290 end: usize,
291 sep: &str,
292 ) -> Vec<String> {
293 let mut out = Vec::new();
294 let mut i = 0;
295 while let Some(block) = blocks.get(i) {
296 while let Some((_, text)) = items.next_if(|(offset, _)| *offset < block.range.start) {
297 out.push(text);
298 }
299 let mut j = i + 1;
300 while blocks
301 .get(j)
302 .is_some_and(|child| child.range.start < block.range.end)
303 {
304 j += 1;
305 }
306 let inner = self.render_tree(items, &blocks[i + 1..j], block.range.end, sep);
307 let prelude = block.prelude.trim();
308 if inner.is_empty() {
309 if block.created && !prelude.is_empty() && self.is_kept(block.layer) {
311 out.push(format!("@layer {prelude};"));
312 }
313 } else {
314 let inner = inner.join(sep);
315 if prelude.is_empty() {
316 out.push(format!("@layer {{{sep}{inner}{sep}}}"));
317 } else {
318 out.push(format!("@layer {prelude} {{{sep}{inner}{sep}}}"));
319 }
320 }
321 i = j;
322 }
323 while let Some((_, text)) = items.next_if(|(offset, _)| *offset < end) {
324 out.push(text);
325 }
326 out
327 }
328}
329
330fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
332 chunks
333 .iter()
334 .position(|chunk| chunk.range.contains(&offset))
335}
336
337#[allow(clippy::arithmetic_side_effects)]
339fn compute_rule_chunk_indices(
340 rules: &[Rule<'_>],
341 source: &str,
342 chunks: &[CssChunk],
343) -> Vec<Option<usize>> {
344 let source_start = source.as_ptr() as usize;
345 let source_end = source_start.saturating_add(source.len());
346
347 rules
348 .iter()
349 .map(|rule| {
350 let sel_start = rule.selectors.as_ptr() as usize;
351 if sel_start >= source_start && sel_start < source_end {
353 let offset = sel_start.wrapping_sub(source_start);
354 find_chunk_index(chunks, offset)
355 } else {
356 None
357 }
358 })
359 .collect()
360}
361
362struct CssBuffer {
363 raw: String,
364 chunks: Option<Vec<CssChunk>>,
365}
366
367impl CssBuffer {
368 fn new(track_chunks: bool) -> Self {
369 CssBuffer {
370 raw: String::new(),
371 chunks: track_chunks.then(Vec::new),
372 }
373 }
374
375 fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
376 if content.is_empty() {
377 return;
378 }
379 let start = self.raw.len();
380 self.raw.push_str(content);
381 if append_newline {
382 self.raw.push('\n');
383 }
384 if let Some(chunks) = &mut self.chunks {
385 let end = self.raw.len();
386 chunks.push(CssChunk {
387 range: start..end,
388 style_node,
389 });
390 }
391 }
392
393 fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
394 (self.raw, self.chunks)
395 }
396}
397
398fn apply_selector_cleanup<'i>(
400 state: &SelectorCleanupState<'i>,
401 document: &mut Document,
402 requested_keep_style_tags: bool,
403 declarations: &[parser::Declaration<'i>],
404 kept: &KeptCss<'_, 'i>,
405 spans: &[Span<'i>],
406) -> bool {
407 let source = kept.source;
408 let source_start = source.as_ptr() as usize;
409 let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
410 (0..state.chunks.len()).map(|_| Vec::new()).collect();
411 let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
412
413 for usage in &state.usages {
414 if usage.matched {
415 continue;
416 }
417 let trimmed = usage.selector.trim();
418 if trimmed.is_empty() {
419 continue;
420 }
421 let key = (usage.chunk_index, usage.rule_id);
422 let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
423 let idx = chunk_remainders[usage.chunk_index].len();
424 chunk_remainders[usage.chunk_index].push(RuleRemainder {
425 selectors: SelectorList::new(),
426 declarations: usage.declarations,
427 offset: (trimmed.as_ptr() as usize).wrapping_sub(source_start),
428 });
429 idx
430 });
431 chunk_remainders[usage.chunk_index][*entry_index]
432 .selectors
433 .push(trimmed);
434 }
435
436 let mut keeps_content = false;
437 for (idx, chunk) in state.chunks.iter().enumerate() {
438 let Some(node_id) = chunk.style_node else {
439 continue;
440 };
441 let mut items: Vec<(usize, String)> = Vec::new();
443 for remainder in &chunk_remainders[idx] {
444 let mut text = String::new();
445 append_rule(&mut text, remainder, declarations);
446 items.push((remainder.offset, text.trim().to_string()));
447 }
448 let rules = kept.render(spans, &chunk.range, items, "\n");
449 if rules.is_empty() {
450 handle_empty_remainder(document, chunk, requested_keep_style_tags);
451 continue;
452 }
453 keeps_content = true;
454 overwrite_style_node(document, node_id, &rules.join("\n"));
455 }
456 keeps_content
457}
458
459fn handle_empty_remainder(
460 document: &mut Document,
461 chunk: &CssChunk,
462 requested_keep_style_tags: bool,
463) {
464 if let Some(node_id) = chunk.style_node {
465 if requested_keep_style_tags {
466 overwrite_style_node(document, node_id, "");
467 } else {
468 document.detach_node(node_id);
469 }
470 }
471}
472
473fn append_rule<'i>(
474 buffer: &mut String,
475 remainder: &RuleRemainder<'i>,
476 declarations: &[parser::Declaration<'i>],
477) {
478 let (start, end) = remainder.declarations;
479 if start >= end || end > declarations.len() {
480 return;
481 }
482 let mut selectors_iter = remainder.selectors.iter().peekable();
483 while let Some(selector) = selectors_iter.next() {
484 buffer.push_str(selector);
485 if selectors_iter.peek().is_some() {
486 buffer.push_str(", ");
487 }
488 }
489 buffer.push_str(" {");
490 for (name, value) in &declarations[start..end] {
491 buffer.push(' ');
492 buffer.push_str(name);
493 buffer.push(':');
494 buffer.push(' ');
495 let value_trimmed = value.trim();
496 buffer.push_str(value_trimmed);
497 if !value_trimmed.ends_with(';') {
498 buffer.push(';');
499 }
500 }
501 buffer.push_str(" }\n");
502}
503
504fn overwrite_style_node(document: &mut Document, node_id: NodeId, new_css: &str) {
505 let new_css = new_css.trim();
506 if let Some(text_node_id) = document[node_id].first_child {
507 if let NodeData::Text { text } = &mut document[text_node_id].data {
508 text.clear();
509 text.push_slice(new_css);
510 }
511 }
512}
513
514impl<'a> InlineOptions<'a> {
515 #[must_use]
517 pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
518 self.inline_style_tags = inline_style_tags;
519 self
520 }
521
522 #[must_use]
524 pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
525 self.keep_style_tags = keep_style_tags;
526 self
527 }
528
529 #[must_use]
531 pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
532 self.keep_link_tags = keep_link_tags;
533 self
534 }
535
536 #[must_use]
538 pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
539 self.keep_at_rules = keep_at_rules;
540 self
541 }
542
543 #[must_use]
545 pub fn minify_css(mut self, minify_css: bool) -> Self {
546 self.minify_css = minify_css;
547 self
548 }
549
550 #[must_use]
552 pub fn base_url(mut self, base_url: Option<Url>) -> Self {
553 self.base_url = base_url;
554 self
555 }
556
557 #[must_use]
559 pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
560 self.load_remote_stylesheets = load_remote_stylesheets;
561 self
562 }
563
564 #[must_use]
566 #[cfg(feature = "stylesheet-cache")]
567 pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
568 if let Some(cache) = cache.into() {
569 self.cache = Some(std::sync::Mutex::new(cache));
570 } else {
571 self.cache = None;
572 }
573 self
574 }
575
576 #[must_use]
578 pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
579 self.extra_css = extra_css;
580 self
581 }
582
583 #[must_use]
585 pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
586 self.preallocate_node_capacity = preallocate_node_capacity;
587 self
588 }
589
590 #[must_use]
592 pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
593 self.resolver = resolver;
594 self
595 }
596
597 #[must_use]
599 pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
600 self.remove_inlined_selectors = enabled;
601 self
602 }
603
604 #[must_use]
609 pub fn apply_width_attributes(mut self, apply: bool) -> Self {
610 self.apply_width_attributes = apply;
611 self
612 }
613
614 #[must_use]
619 pub fn apply_height_attributes(mut self, apply: bool) -> Self {
620 self.apply_height_attributes = apply;
621 self
622 }
623
624 #[must_use]
627 pub fn resolve_css_variables(mut self, resolve: bool) -> Self {
628 self.resolve_css_variables = resolve;
629 self
630 }
631
632 #[must_use]
634 pub const fn build(self) -> CSSInliner<'a> {
635 CSSInliner::new(self)
636 }
637}
638
639impl Default for InlineOptions<'_> {
640 #[inline]
641 fn default() -> Self {
642 InlineOptions {
643 inline_style_tags: true,
644 keep_style_tags: false,
645 keep_link_tags: false,
646 keep_at_rules: false,
647 minify_css: false,
648 base_url: None,
649 load_remote_stylesheets: true,
650 #[cfg(feature = "stylesheet-cache")]
651 cache: None,
652 extra_css: None,
653 preallocate_node_capacity: 32,
654 resolver: Arc::new(DefaultStylesheetResolver),
655 remove_inlined_selectors: false,
656 apply_width_attributes: false,
657 apply_height_attributes: false,
658 resolve_css_variables: false,
659 }
660 }
661}
662
663pub type Result<T> = std::result::Result<T, InlineError>;
665
666#[derive(Debug)]
668pub struct CSSInliner<'a> {
669 options: InlineOptions<'a>,
670}
671
672const GROWTH_COEFFICIENT: f64 = 1.5;
673const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
675
676fn allocate_output_buffer(html: &str) -> Vec<u8> {
677 #[allow(
679 clippy::cast_precision_loss,
680 clippy::cast_sign_loss,
681 clippy::cast_possible_truncation
682 )]
683 Vec::with_capacity(
684 (html.len() as f64 * GROWTH_COEFFICIENT)
685 .min(usize::MAX as f64)
686 .round() as usize,
687 )
688}
689
690impl<'a> CSSInliner<'a> {
691 #[must_use]
693 #[inline]
694 pub const fn new(options: InlineOptions<'a>) -> Self {
695 CSSInliner { options }
696 }
697
698 #[must_use]
717 #[inline]
718 pub fn options() -> InlineOptions<'a> {
719 InlineOptions::default()
720 }
721
722 #[inline]
738 pub fn inline(&self, html: &str) -> Result<String> {
739 let mut out = allocate_output_buffer(html);
740 self.inline_to(html, &mut out)?;
741 Ok(String::from_utf8_lossy(&out).to_string())
742 }
743
744 #[inline]
760 pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
761 self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
762 self.options.resolver.retrieve(url)
763 })
764 }
765
766 pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
789 let mut out = allocate_output_buffer(html);
790 self.inline_fragment_to(html, css, &mut out)?;
791 Ok(String::from_utf8_lossy(&out).to_string())
792 }
793
794 pub fn inline_fragment_to<W: Write>(
811 &self,
812 html: &str,
813 css: &str,
814 target: &mut W,
815 ) -> Result<()> {
816 self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
817 self.options.resolver.retrieve(url)
818 })
819 }
820
821 #[cfg(feature = "async")]
832 pub async fn inline_async(&self, html: &str) -> Result<String> {
833 let mut out = allocate_output_buffer(html);
834 self.inline_to_async(html, &mut out).await?;
835 Ok(String::from_utf8_lossy(&out).to_string())
836 }
837
838 #[cfg(feature = "async")]
848 pub async fn inline_to_async<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
849 let stylesheets = self.fetch_stylesheets(html, InliningMode::Document).await?;
850 self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
851 Ok(stylesheets[url].clone())
853 })
854 }
855
856 #[cfg(feature = "async")]
866 pub async fn inline_fragment_async(&self, html: &str, css: &str) -> Result<String> {
867 let mut out = allocate_output_buffer(html);
868 self.inline_fragment_to_async(html, css, &mut out).await?;
869 Ok(String::from_utf8_lossy(&out).to_string())
870 }
871
872 #[cfg(feature = "async")]
882 pub async fn inline_fragment_to_async<W: Write>(
883 &self,
884 html: &str,
885 css: &str,
886 target: &mut W,
887 ) -> Result<()> {
888 let stylesheets = self.fetch_stylesheets(html, InliningMode::Fragment).await?;
889 self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
890 Ok(stylesheets[url].clone())
892 })
893 }
894
895 #[cfg(feature = "async")]
896 async fn fetch_stylesheets(
897 &self,
898 html: &str,
899 mode: InliningMode,
900 ) -> Result<FxHashMap<String, String>> {
901 let mut stylesheets = FxHashMap::default();
902 if !self.options.load_remote_stylesheets {
903 return Ok(stylesheets);
904 }
905 let mut missing = Vec::new();
906 {
907 let document = Document::parse_with_options(
909 html.as_bytes(),
910 self.options.preallocate_node_capacity,
911 mode,
912 );
913 let mut urls = document
914 .stylesheets()
915 .map(|href| self.get_full_url(href).into_owned())
916 .collect::<Vec<_>>();
917 urls.sort_unstable();
918 urls.dedup();
919 for url in urls {
920 #[cfg(feature = "stylesheet-cache")]
922 if let Some(lock) = self.options.cache.as_ref() {
923 let mut cache = lock.lock().expect("Cache lock is poisoned");
924 if let Some(css) = cache.get(&url) {
925 stylesheets.insert(url, css.clone());
926 continue;
927 }
928 }
929 missing.push(url);
930 }
931 }
932 let fetched = futures_util::future::try_join_all(
933 missing
934 .iter()
935 .map(|url| self.options.resolver.retrieve_async(url)),
936 )
937 .await?;
938 stylesheets.extend(missing.into_iter().zip(fetched));
939 Ok(stylesheets)
940 }
941
942 #[allow(clippy::too_many_lines)]
943 fn inline_to_impl<W: Write>(
944 &self,
945 html: &str,
946 css: Option<&str>,
947 target: &mut W,
948 mode: InliningMode,
949 retrieve: &dyn Fn(&str) -> Result<String>,
950 ) -> Result<()> {
951 let mut document = Document::parse_with_options(
952 html.as_bytes(),
953 self.options.preallocate_node_capacity,
954 mode,
955 );
956 let track_selector_cleanup =
965 self.options.remove_inlined_selectors && self.options.inline_style_tags;
966 let mut size_estimate: usize = if self.options.inline_style_tags {
967 document
968 .styles()
969 .map(|(_, s)| {
970 s.len().saturating_add(1)
972 })
973 .sum()
974 } else {
975 0
976 };
977 if let Some(extra_css) = &self.options.extra_css {
978 size_estimate = size_estimate.saturating_add(extra_css.len());
979 }
980 if let Some(css) = css {
981 size_estimate = size_estimate.saturating_add(css.len());
982 }
983 let mut css_buffer = CssBuffer::new(track_selector_cleanup);
984 css_buffer.raw.reserve(size_estimate);
985 if self.options.inline_style_tags || self.options.keep_at_rules {
986 for (node_id, style) in document.styles() {
987 let style_node = track_selector_cleanup.then_some(node_id);
988 css_buffer.push(style_node, style, true);
989 }
990 }
991 if self.options.load_remote_stylesheets {
992 let mut links = document.stylesheets().collect::<Vec<&str>>();
993 links.sort_unstable();
994 links.dedup();
995 for href in &links {
996 let url = self.get_full_url(href);
997 #[cfg(feature = "stylesheet-cache")]
998 if let Some(lock) = self.options.cache.as_ref() {
999 let mut cache = lock.lock().expect("Cache lock is poisoned");
1000 if let Some(cached) = cache.get(url.as_ref()) {
1001 css_buffer.push(None, cached, true);
1002 continue;
1003 }
1004 }
1005
1006 let css = retrieve(url.as_ref())?;
1007 css_buffer.push(None, &css, true);
1008
1009 #[cfg(feature = "stylesheet-cache")]
1010 if let Some(lock) = self.options.cache.as_ref() {
1011 let mut cache = lock.lock().expect("Cache lock is poisoned");
1012 cache.put(url.into_owned(), css);
1013 }
1014 }
1015 }
1016 if let Some(extra_css) = &self.options.extra_css {
1017 css_buffer.push(None, extra_css, false);
1018 }
1019 if let Some(css) = css {
1020 css_buffer.push(None, css, false);
1021 }
1022 let (raw_styles, css_chunks) = css_buffer.into_parts();
1023 let mut selector_cleanup_state = if track_selector_cleanup {
1024 Some(SelectorCleanupState::default())
1025 } else {
1026 None
1027 };
1028 if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
1029 state.chunks = chunks;
1030 }
1031 #[allow(
1033 clippy::cast_precision_loss,
1034 clippy::cast_sign_loss,
1035 clippy::cast_possible_truncation
1036 )]
1037 let mut declarations = Vec::with_capacity(
1038 ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
1039 .min(usize::MAX as f64)
1040 .round() as usize)
1041 .max(16),
1042 );
1043 let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
1044 let mut spans = (self.options.keep_at_rules || track_selector_cleanup).then(Vec::new);
1047 let mut rule_parser = parser::RuleParser::new(
1048 &raw_styles,
1049 &mut declarations,
1050 &mut rule_list,
1051 spans.as_mut(),
1052 );
1053 if !raw_styles.is_empty() {
1054 let mut parser = cssparser::Parser::new(&raw_styles);
1055 let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_parser);
1056 parser::collect_rules(&mut items);
1057 }
1058 let layers = rule_parser.into_layers();
1059 let spans = spans.unwrap_or_default();
1060 if self.options.keep_at_rules && !self.options.inline_style_tags {
1061 rule_list.clear();
1063 }
1064 let ranks = layers.ranks();
1065 let last_rank = u32::try_from(layers.len().saturating_sub(1)).unwrap_or(u32::MAX);
1067 let reversed = |precedence: Precedence| {
1068 Precedence(last_rank.saturating_sub(precedence.0), precedence.1)
1069 };
1070 let rule_chunk_indices = selector_cleanup_state
1072 .as_ref()
1073 .map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
1074 .unwrap_or_default();
1075 let mut styles: Vec<Option<html::ElementStyleMap<'_>>> = vec![None; document.nodes.len()];
1077 let mut caches = SelectorCaches::default();
1079 for (rule_id, rule) in rule_list.iter().enumerate() {
1080 let (start, end) = rule.declarations;
1081 let rank = ranks[rule.layer];
1082 for selector in rule.selectors.split(',') {
1085 let mut matched_any = false;
1086 if !document.anchor_exists(selector) {
1089 if let Some(state) = selector_cleanup_state.as_mut() {
1090 if let Some(chunk_index) =
1091 rule_chunk_indices.get(rule_id).copied().flatten()
1092 {
1093 state.record_usage(SelectorUsage {
1094 selector,
1095 declarations: (start, end),
1096 rule_id,
1097 chunk_index,
1098 matched: false,
1099 });
1100 }
1101 }
1102 continue;
1103 }
1104 if let Ok(matching_elements) = document.select(selector, &mut caches) {
1105 let precedence = Precedence(rank, matching_elements.specificity());
1106 for matching_element in matching_elements {
1107 matched_any = true;
1108 let element_styles = styles[matching_element.node_id.get()]
1109 .get_or_insert_with(SmallVec::new);
1110 for (name, value) in &declarations[start..end] {
1113 let prop_name = name.as_ref();
1114 if let Some(idx) =
1116 element_styles.iter().position(|(n, _, _)| *n == prop_name)
1117 {
1118 let entry = &mut element_styles[idx];
1119 let new_important = value.trim_end().ends_with("!important");
1120 let old_important = entry.2.trim_end().ends_with("!important");
1121 match (new_important, old_important) {
1122 (false, false) => {
1124 if entry.1 <= precedence {
1125 entry.1 = precedence;
1126 entry.2 = *value;
1127 }
1128 }
1129 (true, true) => {
1131 if reversed(entry.1) <= reversed(precedence) {
1132 entry.1 = precedence;
1133 entry.2 = *value;
1134 }
1135 }
1136 (true, false) => {
1138 entry.1 = precedence;
1139 entry.2 = *value;
1140 }
1141 (false, true) => {}
1144 }
1145 } else {
1146 element_styles.push((prop_name, precedence, *value));
1147 }
1148 }
1149 }
1150 }
1151 if let Some(state) = selector_cleanup_state.as_mut() {
1152 if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
1153 state.record_usage(SelectorUsage {
1154 selector,
1155 declarations: (start, end),
1156 rule_id,
1157 chunk_index,
1158 matched: matched_any,
1159 });
1160 }
1161 }
1162 }
1165 }
1166 let mut keep_style_tags = self.options.keep_style_tags;
1167 let mut at_rules = None;
1168 if self.options.keep_at_rules || selector_cleanup_state.is_some() {
1169 let kept = KeptCss::new(
1170 &raw_styles,
1171 &layers,
1172 &spans,
1173 &rule_list,
1174 selector_cleanup_state.as_ref(),
1175 self.options.keep_style_tags && !track_selector_cleanup,
1176 );
1177 if self.options.keep_at_rules {
1178 let rules = kept.render(&spans, &(0..raw_styles.len()), Vec::new(), " ");
1179 at_rules = (!rules.is_empty()).then(|| format!("{} ", rules.join(" ")));
1180 }
1181 if let Some(state) = selector_cleanup_state.as_ref() {
1182 keep_style_tags |= apply_selector_cleanup(
1183 state,
1184 &mut document,
1185 self.options.keep_style_tags,
1186 &declarations,
1187 &kept,
1188 &spans,
1189 );
1190 }
1191 }
1192 document.serialize(
1193 target,
1194 styles,
1195 keep_style_tags,
1196 self.options.keep_link_tags,
1197 self.options.minify_css,
1198 at_rules.as_ref(),
1199 mode,
1200 self.options.apply_width_attributes,
1201 self.options.apply_height_attributes,
1202 self.options.resolve_css_variables,
1203 )?;
1204 Ok(())
1205 }
1206
1207 fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
1208 if Url::parse(href).is_ok() {
1210 return Cow::Borrowed(href);
1211 }
1212 if let Some(base_url) = &self.options.base_url {
1213 if href.starts_with("//") {
1215 return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
1216 }
1217 if let Ok(new_url) = base_url.join(href) {
1219 return Cow::Owned(new_url.into());
1220 }
1221 }
1222 Cow::Borrowed(href)
1224 }
1225}
1226
1227impl Default for CSSInliner<'_> {
1228 #[inline]
1229 fn default() -> Self {
1230 CSSInliner::new(InlineOptions::default())
1231 }
1232}
1233
1234#[inline]
1249pub fn inline(html: &str) -> Result<String> {
1250 CSSInliner::default().inline(html)
1251}
1252
1253#[inline]
1268pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
1269 CSSInliner::default().inline_to(html, target)
1270}
1271
1272#[inline]
1289pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
1290 CSSInliner::default().inline_fragment(html, css)
1291}
1292
1293#[inline]
1308pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
1309 CSSInliner::default().inline_fragment_to(html, css, target)
1310}
1311
1312#[cfg(feature = "async")]
1318pub async fn inline_async(html: &str) -> Result<String> {
1319 CSSInliner::default().inline_async(html).await
1320}
1321
1322#[cfg(feature = "async")]
1328pub async fn inline_to_async<W: Write>(html: &str, target: &mut W) -> Result<()> {
1329 CSSInliner::default().inline_to_async(html, target).await
1330}
1331
1332#[cfg(feature = "async")]
1338pub async fn inline_fragment_async(html: &str, css: &str) -> Result<String> {
1339 CSSInliner::default().inline_fragment_async(html, css).await
1340}
1341
1342#[cfg(feature = "async")]
1348pub async fn inline_fragment_to_async<W: Write>(
1349 html: &str,
1350 css: &str,
1351 target: &mut W,
1352) -> Result<()> {
1353 CSSInliner::default()
1354 .inline_fragment_to_async(html, css, target)
1355 .await
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360 use crate::{CSSInliner, InlineOptions};
1361
1362 #[test]
1363 fn test_inliner_sync_send() {
1364 fn assert_send<T: Send + Sync>() {}
1365 assert_send::<CSSInliner<'_>>();
1366 assert_send::<InlineOptions<'_>>();
1367 }
1368}