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, ops::Range, sync::Arc};
41
42use html::{Document, InliningMode, NodeData, NodeId, Specificity};
43pub use resolver::{DefaultStylesheetResolver, StylesheetResolver};
44use rustc_hash::FxHashMap;
45pub use url::{ParseError, Url};
46
47#[cfg(feature = "stylesheet-cache")]
49pub type StylesheetCache<S = DefaultHasher> = LruCache<String, String, S>;
50
51#[allow(clippy::struct_excessive_bools)]
53pub struct InlineOptions<'a> {
54 pub inline_style_tags: bool,
59 pub keep_style_tags: bool,
61 pub keep_link_tags: bool,
63 pub keep_at_rules: bool,
65 pub minify_css: bool,
67 pub base_url: Option<Url>,
69 pub load_remote_stylesheets: bool,
71 #[cfg(feature = "stylesheet-cache")]
73 pub cache: Option<std::sync::Mutex<StylesheetCache>>,
74 pub extra_css: Option<Cow<'a, str>>,
79 pub preallocate_node_capacity: usize,
82 pub resolver: Arc<dyn StylesheetResolver>,
84 pub remove_inlined_selectors: bool,
86 pub apply_width_attributes: bool,
91 pub apply_height_attributes: bool,
96}
97
98impl std::fmt::Debug for InlineOptions<'_> {
99 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
100 let mut debug = f.debug_struct("InlineOptions");
101 debug
102 .field("inline_style_tags", &self.inline_style_tags)
103 .field("keep_style_tags", &self.keep_style_tags)
104 .field("keep_link_tags", &self.keep_link_tags)
105 .field("base_url", &self.base_url)
106 .field("load_remote_stylesheets", &self.load_remote_stylesheets);
107 #[cfg(feature = "stylesheet-cache")]
108 {
109 debug.field("cache", &self.cache);
110 }
111 debug
112 .field("extra_css", &self.extra_css)
113 .field("preallocate_node_capacity", &self.preallocate_node_capacity)
114 .field("remove_inlined_selectors", &self.remove_inlined_selectors)
115 .field("apply_width_attributes", &self.apply_width_attributes)
116 .field("apply_height_attributes", &self.apply_height_attributes)
117 .finish_non_exhaustive()
118 }
119}
120
121#[derive(Debug)]
122struct CssChunk {
123 range: Range<usize>,
124 style_node: Option<NodeId>,
127}
128
129type SelectorList<'i> = SmallVec<[&'i str; 2]>;
130
131#[derive(Debug)]
132struct SelectorUsage<'i> {
133 selector: &'i str,
134 declarations: (usize, usize),
135 rule_id: usize,
136 chunk_index: usize,
137 matched: bool,
138}
139
140#[derive(Debug, Default)]
141struct RuleRemainder<'i> {
142 selectors: SelectorList<'i>,
143 declarations: (usize, usize),
144 offset: usize,
146}
147
148#[derive(Debug, Default)]
149struct SelectorCleanupState<'i> {
150 chunks: Vec<CssChunk>,
151 usages: Vec<SelectorUsage<'i>>,
152 chunk_at_rules: Vec<Vec<Range<usize>>>,
154}
155
156impl<'i> SelectorCleanupState<'i> {
157 fn record_usage(&mut self, usage: SelectorUsage<'i>) {
158 self.usages.push(usage);
159 }
160
161 fn record_at_rule(&mut self, source: &str, rule: &str, end: usize) {
164 if !rule.starts_with('@') {
165 return;
166 }
167 let start = (rule.as_ptr() as usize).wrapping_sub(source.as_ptr() as usize);
168 if let Some(idx) = find_chunk_index(&self.chunks, start) {
169 let chunk = &self.chunks[idx];
170 if chunk.style_node.is_some() {
171 self.chunk_at_rules[idx].push(start..end.min(chunk.range.end));
173 }
174 }
175 }
176
177 fn needs_css(&self) -> bool {
179 self.usages.iter().any(|usage| !usage.matched)
180 || self.chunk_at_rules.iter().any(|ranges| !ranges.is_empty())
181 }
182}
183
184fn collect_rules<'i, P>(
185 items: &mut cssparser::StyleSheetParser<'i, '_, P>,
186 source: &'i str,
187 mut rules: Option<&mut Vec<parser::QualifiedRule<'i>>>,
188 mut state: Option<&mut SelectorCleanupState<'i>>,
189) where
190 P: cssparser::QualifiedRuleParser<'i, QualifiedRule = parser::QualifiedRule<'i>, Error = ()>
191 + cssparser::AtRuleParser<'i, AtRule = parser::QualifiedRule<'i>, Error = ()>,
192{
193 let mut rejected = None;
197 loop {
198 if let Some(rule) = rejected.take() {
199 items.input.skip_whitespace();
200 let end = items.input.position().byte_index();
201 if let Some(state) = state.as_mut() {
202 state.record_at_rule(source, rule, end);
203 }
204 }
205 match items.next() {
206 None => break,
207 Some(Ok(rule)) => {
208 if let Some(rules) = rules.as_mut() {
209 rules.push(rule);
210 }
211 }
212 Some(Err((_, rule, _))) if state.is_some() => rejected = Some(rule),
213 Some(Err(_)) => {}
214 }
215 }
216}
217
218fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
220 chunks
221 .iter()
222 .position(|chunk| chunk.range.contains(&offset))
223}
224
225#[allow(clippy::arithmetic_side_effects)]
227fn compute_rule_chunk_indices(
228 rules: &[(&str, (usize, usize))],
229 source: &str,
230 chunks: &[CssChunk],
231) -> Vec<Option<usize>> {
232 let source_start = source.as_ptr() as usize;
233 let source_end = source_start.saturating_add(source.len());
234
235 rules
236 .iter()
237 .map(|(selectors, _)| {
238 let sel_start = selectors.as_ptr() as usize;
239 if sel_start >= source_start && sel_start < source_end {
241 let offset = sel_start.wrapping_sub(source_start);
242 find_chunk_index(chunks, offset)
243 } else {
244 None
245 }
246 })
247 .collect()
248}
249
250struct CssBuffer {
251 raw: String,
252 chunks: Option<Vec<CssChunk>>,
253}
254
255impl CssBuffer {
256 fn new(track_chunks: bool) -> Self {
257 CssBuffer {
258 raw: String::new(),
259 chunks: track_chunks.then(Vec::new),
260 }
261 }
262
263 fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
264 if content.is_empty() {
265 return;
266 }
267 let start = self.raw.len();
268 self.raw.push_str(content);
269 if append_newline {
270 self.raw.push('\n');
271 }
272 if let Some(chunks) = &mut self.chunks {
273 let end = self.raw.len();
274 chunks.push(CssChunk {
275 range: start..end,
276 style_node,
277 });
278 }
279 }
280
281 fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
282 (self.raw, self.chunks)
283 }
284}
285
286fn apply_selector_cleanup<'i>(
287 state: &SelectorCleanupState<'i>,
288 document: &mut Document,
289 requested_keep_style_tags: bool,
290 declarations: &[parser::Declaration<'i>],
291 source: &'i str,
292) {
293 if state.chunks.is_empty() {
294 return;
295 }
296 rewrite_style_blocks(
297 state,
298 document,
299 requested_keep_style_tags,
300 declarations,
301 source,
302 );
303}
304
305fn rewrite_style_blocks<'i>(
306 state: &SelectorCleanupState<'i>,
307 document: &mut Document,
308 requested_keep_style_tags: bool,
309 declarations: &[parser::Declaration<'i>],
310 source: &'i str,
311) {
312 let source_start = source.as_ptr() as usize;
313 let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
314 (0..state.chunks.len()).map(|_| Vec::new()).collect();
315 let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
316
317 for usage in &state.usages {
318 if usage.matched {
319 continue;
320 }
321 let trimmed = usage.selector.trim();
322 if trimmed.is_empty() {
323 continue;
324 }
325 let key = (usage.chunk_index, usage.rule_id);
326 let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
327 let idx = chunk_remainders[usage.chunk_index].len();
328 chunk_remainders[usage.chunk_index].push(RuleRemainder {
329 selectors: SelectorList::new(),
330 declarations: usage.declarations,
331 offset: (trimmed.as_ptr() as usize).wrapping_sub(source_start),
332 });
333 idx
334 });
335 chunk_remainders[usage.chunk_index][*entry_index]
336 .selectors
337 .push(trimmed);
338 }
339
340 for (idx, chunk) in state.chunks.iter().enumerate() {
341 let mut items: Vec<(usize, String)> = Vec::new();
343 for remainder in &chunk_remainders[idx] {
344 let mut text = String::new();
345 append_rule(&mut text, remainder, declarations);
346 items.push((remainder.offset, text.trim().to_string()));
347 }
348 for range in &state.chunk_at_rules[idx] {
349 items.push((range.start, source[range.clone()].trim().to_string()));
350 }
351 items.retain(|(_, text)| !text.is_empty());
352 if items.is_empty() {
353 handle_empty_remainder(document, chunk, requested_keep_style_tags);
354 continue;
355 }
356 items.sort_by_key(|(offset, _)| *offset);
357 let buffer = items
358 .into_iter()
359 .map(|(_, text)| text)
360 .collect::<Vec<_>>()
361 .join("\n");
362 if let Some(node_id) = chunk.style_node {
363 overwrite_style_node(document, node_id, &buffer);
364 }
365 }
366}
367
368fn handle_empty_remainder(
369 document: &mut Document,
370 chunk: &CssChunk,
371 requested_keep_style_tags: bool,
372) {
373 if let Some(node_id) = chunk.style_node {
374 if requested_keep_style_tags {
375 overwrite_style_node(document, node_id, "");
376 } else {
377 document.detach_node(node_id);
378 }
379 }
380}
381
382fn append_rule<'i>(
383 buffer: &mut String,
384 remainder: &RuleRemainder<'i>,
385 declarations: &[parser::Declaration<'i>],
386) {
387 let (start, end) = remainder.declarations;
388 if start >= end || end > declarations.len() {
389 return;
390 }
391 let mut selectors_iter = remainder.selectors.iter().peekable();
392 while let Some(selector) = selectors_iter.next() {
393 buffer.push_str(selector);
394 if selectors_iter.peek().is_some() {
395 buffer.push_str(", ");
396 }
397 }
398 buffer.push_str(" {");
399 for (name, value) in &declarations[start..end] {
400 buffer.push(' ');
401 buffer.push_str(name);
402 buffer.push(':');
403 buffer.push(' ');
404 let value_trimmed = value.trim();
405 buffer.push_str(value_trimmed);
406 if !value_trimmed.ends_with(';') {
407 buffer.push(';');
408 }
409 }
410 buffer.push_str(" }\n");
411}
412
413fn overwrite_style_node(document: &mut Document, node_id: NodeId, new_css: &str) {
414 let new_css = new_css.trim();
415 if let Some(text_node_id) = document[node_id].first_child {
416 if let NodeData::Text { text } = &mut document[text_node_id].data {
417 text.clear();
418 text.push_slice(new_css);
419 }
420 }
421}
422
423impl<'a> InlineOptions<'a> {
424 #[must_use]
426 pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
427 self.inline_style_tags = inline_style_tags;
428 self
429 }
430
431 #[must_use]
433 pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
434 self.keep_style_tags = keep_style_tags;
435 self
436 }
437
438 #[must_use]
440 pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
441 self.keep_link_tags = keep_link_tags;
442 self
443 }
444
445 #[must_use]
447 pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
448 self.keep_at_rules = keep_at_rules;
449 self
450 }
451
452 #[must_use]
454 pub fn minify_css(mut self, minify_css: bool) -> Self {
455 self.minify_css = minify_css;
456 self
457 }
458
459 #[must_use]
461 pub fn base_url(mut self, base_url: Option<Url>) -> Self {
462 self.base_url = base_url;
463 self
464 }
465
466 #[must_use]
468 pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
469 self.load_remote_stylesheets = load_remote_stylesheets;
470 self
471 }
472
473 #[must_use]
475 #[cfg(feature = "stylesheet-cache")]
476 pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
477 if let Some(cache) = cache.into() {
478 self.cache = Some(std::sync::Mutex::new(cache));
479 } else {
480 self.cache = None;
481 }
482 self
483 }
484
485 #[must_use]
487 pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
488 self.extra_css = extra_css;
489 self
490 }
491
492 #[must_use]
494 pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
495 self.preallocate_node_capacity = preallocate_node_capacity;
496 self
497 }
498
499 #[must_use]
501 pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
502 self.resolver = resolver;
503 self
504 }
505
506 #[must_use]
508 pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
509 self.remove_inlined_selectors = enabled;
510 self
511 }
512
513 #[must_use]
518 pub fn apply_width_attributes(mut self, apply: bool) -> Self {
519 self.apply_width_attributes = apply;
520 self
521 }
522
523 #[must_use]
528 pub fn apply_height_attributes(mut self, apply: bool) -> Self {
529 self.apply_height_attributes = apply;
530 self
531 }
532
533 #[must_use]
535 pub const fn build(self) -> CSSInliner<'a> {
536 CSSInliner::new(self)
537 }
538}
539
540impl Default for InlineOptions<'_> {
541 #[inline]
542 fn default() -> Self {
543 InlineOptions {
544 inline_style_tags: true,
545 keep_style_tags: false,
546 keep_link_tags: false,
547 keep_at_rules: false,
548 minify_css: false,
549 base_url: None,
550 load_remote_stylesheets: true,
551 #[cfg(feature = "stylesheet-cache")]
552 cache: None,
553 extra_css: None,
554 preallocate_node_capacity: 32,
555 resolver: Arc::new(DefaultStylesheetResolver),
556 remove_inlined_selectors: false,
557 apply_width_attributes: false,
558 apply_height_attributes: false,
559 }
560 }
561}
562
563pub type Result<T> = std::result::Result<T, InlineError>;
565
566#[derive(Debug)]
568pub struct CSSInliner<'a> {
569 options: InlineOptions<'a>,
570}
571
572const GROWTH_COEFFICIENT: f64 = 1.5;
573const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
575
576fn allocate_output_buffer(html: &str) -> Vec<u8> {
577 #[allow(
579 clippy::cast_precision_loss,
580 clippy::cast_sign_loss,
581 clippy::cast_possible_truncation
582 )]
583 Vec::with_capacity(
584 (html.len() as f64 * GROWTH_COEFFICIENT)
585 .min(usize::MAX as f64)
586 .round() as usize,
587 )
588}
589
590impl<'a> CSSInliner<'a> {
591 #[must_use]
593 #[inline]
594 pub const fn new(options: InlineOptions<'a>) -> Self {
595 CSSInliner { options }
596 }
597
598 #[must_use]
617 #[inline]
618 pub fn options() -> InlineOptions<'a> {
619 InlineOptions::default()
620 }
621
622 #[inline]
638 pub fn inline(&self, html: &str) -> Result<String> {
639 let mut out = allocate_output_buffer(html);
640 self.inline_to(html, &mut out)?;
641 Ok(String::from_utf8_lossy(&out).to_string())
642 }
643
644 #[inline]
660 pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
661 self.inline_to_impl(html, None, target, InliningMode::Document)
662 }
663
664 pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
687 let mut out = allocate_output_buffer(html);
688 self.inline_fragment_to(html, css, &mut out)?;
689 Ok(String::from_utf8_lossy(&out).to_string())
690 }
691
692 pub fn inline_fragment_to<W: Write>(
709 &self,
710 html: &str,
711 css: &str,
712 target: &mut W,
713 ) -> Result<()> {
714 self.inline_to_impl(html, Some(css), target, InliningMode::Fragment)
715 }
716
717 #[allow(clippy::too_many_lines)]
718 fn inline_to_impl<W: Write>(
719 &self,
720 html: &str,
721 css: Option<&str>,
722 target: &mut W,
723 mode: InliningMode,
724 ) -> Result<()> {
725 let mut document = Document::parse_with_options(
726 html.as_bytes(),
727 self.options.preallocate_node_capacity,
728 mode,
729 );
730 let track_selector_cleanup =
739 self.options.remove_inlined_selectors && self.options.inline_style_tags;
740 let mut size_estimate: usize = if self.options.inline_style_tags {
741 document
742 .styles()
743 .map(|(_, s)| {
744 s.len().saturating_add(1)
746 })
747 .sum()
748 } else {
749 0
750 };
751 if let Some(extra_css) = &self.options.extra_css {
752 size_estimate = size_estimate.saturating_add(extra_css.len());
753 }
754 if let Some(css) = css {
755 size_estimate = size_estimate.saturating_add(css.len());
756 }
757 let mut css_buffer = CssBuffer::new(track_selector_cleanup);
758 css_buffer.raw.reserve(size_estimate);
759 if self.options.inline_style_tags || self.options.keep_at_rules {
760 for (node_id, style) in document.styles() {
761 let style_node = track_selector_cleanup.then_some(node_id);
762 css_buffer.push(style_node, style, true);
763 }
764 }
765 if self.options.load_remote_stylesheets {
766 let mut links = document.stylesheets().collect::<Vec<&str>>();
767 links.sort_unstable();
768 links.dedup();
769 for href in &links {
770 let url = self.get_full_url(href);
771 #[cfg(feature = "stylesheet-cache")]
772 if let Some(lock) = self.options.cache.as_ref() {
773 let mut cache = lock.lock().expect("Cache lock is poisoned");
774 if let Some(cached) = cache.get(url.as_ref()) {
775 css_buffer.push(None, cached, true);
776 continue;
777 }
778 }
779
780 let css = self.options.resolver.retrieve(url.as_ref())?;
781 css_buffer.push(None, &css, true);
782
783 #[cfg(feature = "stylesheet-cache")]
784 if let Some(lock) = self.options.cache.as_ref() {
785 let mut cache = lock.lock().expect("Cache lock is poisoned");
786 cache.put(url.into_owned(), css);
787 }
788 }
789 }
790 if let Some(extra_css) = &self.options.extra_css {
791 css_buffer.push(None, extra_css, false);
792 }
793 if let Some(css) = css {
794 css_buffer.push(None, css, false);
795 }
796 let (raw_styles, css_chunks) = css_buffer.into_parts();
797 let mut selector_cleanup_state = if track_selector_cleanup {
798 Some(SelectorCleanupState::default())
799 } else {
800 None
801 };
802 if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
803 state.chunk_at_rules = vec![Vec::new(); chunks.len()];
804 state.chunks = chunks;
805 }
806 let mut parser = cssparser::Parser::new(&raw_styles);
807 #[allow(
809 clippy::cast_precision_loss,
810 clippy::cast_sign_loss,
811 clippy::cast_possible_truncation
812 )]
813 let mut declarations = Vec::with_capacity(
814 ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
815 .min(usize::MAX as f64)
816 .round() as usize)
817 .max(16),
818 );
819 let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
820 let at_rules = if self.options.keep_at_rules {
823 let mut at_rules = String::new();
824 let mut rule_parser =
825 parser::AtRuleFilteringParser::new(&mut declarations, &mut at_rules);
826 let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_parser);
827 let rules = self.options.inline_style_tags.then_some(&mut rule_list);
828 collect_rules(
829 &mut items,
830 &raw_styles,
831 rules,
832 selector_cleanup_state.as_mut(),
833 );
834 Some(at_rules)
835 } else if !raw_styles.is_empty() {
836 let mut rule_parser = parser::CSSRuleListParser::new(&mut declarations);
838 let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_parser);
839 collect_rules(
840 &mut items,
841 &raw_styles,
842 Some(&mut rule_list),
843 selector_cleanup_state.as_mut(),
844 );
845 None
846 } else {
847 None
848 };
849 let rule_chunk_indices = selector_cleanup_state
851 .as_ref()
852 .map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
853 .unwrap_or_default();
854 let mut styles: Vec<Option<SmallVec<[_; 4]>>> = vec![None; document.nodes.len()];
856 let mut caches = SelectorCaches::default();
858 for (rule_id, (selectors, (start, end))) in rule_list.iter().enumerate() {
859 for selector in selectors.split(',') {
862 let mut matched_any = false;
863 if !document.anchor_exists(selector) {
866 if let Some(state) = selector_cleanup_state.as_mut() {
867 if let Some(chunk_index) =
868 rule_chunk_indices.get(rule_id).copied().flatten()
869 {
870 state.record_usage(SelectorUsage {
871 selector,
872 declarations: (*start, *end),
873 rule_id,
874 chunk_index,
875 matched: false,
876 });
877 }
878 }
879 continue;
880 }
881 if let Ok(matching_elements) = document.select(selector, &mut caches) {
882 let specificity = matching_elements.specificity();
883 for matching_element in matching_elements {
884 matched_any = true;
885 let element_styles = styles[matching_element.node_id.get()]
886 .get_or_insert_with(SmallVec::new);
887 for (name, value) in &declarations[*start..*end] {
890 let prop_name = name.as_ref();
891 if let Some(idx) =
893 element_styles.iter().position(|(n, _, _)| *n == prop_name)
894 {
895 let entry: &mut (&str, Specificity, &str) =
896 &mut element_styles[idx];
897 let new_important = value.trim_end().ends_with("!important");
898 let old_important = entry.2.trim_end().ends_with("!important");
899 match (new_important, old_important) {
900 (false, false) | (true, true) => {
902 if entry.1 <= specificity {
903 entry.1 = specificity;
904 entry.2 = *value;
905 }
906 }
907 (true, false) => {
909 entry.1 = specificity;
910 entry.2 = *value;
911 }
912 (false, true) => {}
915 }
916 } else {
917 element_styles.push((prop_name, specificity, *value));
918 }
919 }
920 }
921 }
922 if let Some(state) = selector_cleanup_state.as_mut() {
923 if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
924 state.record_usage(SelectorUsage {
925 selector,
926 declarations: (*start, *end),
927 rule_id,
928 chunk_index,
929 matched: matched_any,
930 });
931 }
932 }
933 }
936 }
937 let cleanup_requires_css = selector_cleanup_state
938 .as_ref()
939 .is_some_and(SelectorCleanupState::needs_css);
940 let keep_style_tags = self.options.keep_style_tags || cleanup_requires_css;
941 if let Some(state) = selector_cleanup_state.as_ref() {
942 apply_selector_cleanup(
943 state,
944 &mut document,
945 self.options.keep_style_tags,
946 &declarations,
947 &raw_styles,
948 );
949 }
950 document.serialize(
951 target,
952 styles,
953 keep_style_tags,
954 self.options.keep_link_tags,
955 self.options.minify_css,
956 at_rules.as_ref(),
957 mode,
958 self.options.apply_width_attributes,
959 self.options.apply_height_attributes,
960 )?;
961 Ok(())
962 }
963
964 fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
965 if Url::parse(href).is_ok() {
967 return Cow::Borrowed(href);
968 }
969 if let Some(base_url) = &self.options.base_url {
970 if href.starts_with("//") {
972 return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
973 }
974 if let Ok(new_url) = base_url.join(href) {
976 return Cow::Owned(new_url.into());
977 }
978 }
979 Cow::Borrowed(href)
981 }
982}
983
984impl Default for CSSInliner<'_> {
985 #[inline]
986 fn default() -> Self {
987 CSSInliner::new(InlineOptions::default())
988 }
989}
990
991#[inline]
1006pub fn inline(html: &str) -> Result<String> {
1007 CSSInliner::default().inline(html)
1008}
1009
1010#[inline]
1025pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
1026 CSSInliner::default().inline_to(html, target)
1027}
1028
1029#[inline]
1046pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
1047 CSSInliner::default().inline_fragment(html, css)
1048}
1049
1050#[inline]
1065pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
1066 CSSInliner::default().inline_fragment_to(html, css, target)
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use crate::{CSSInliner, InlineOptions};
1072
1073 #[test]
1074 fn test_inliner_sync_send() {
1075 fn assert_send<T: Send + Sync>() {}
1076 assert_send::<CSSInliner<'_>>();
1077 assert_send::<InlineOptions<'_>>();
1078 }
1079}