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 find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
186 chunks
187 .iter()
188 .position(|chunk| chunk.range.contains(&offset))
189}
190
191#[allow(clippy::arithmetic_side_effects)]
193fn compute_rule_chunk_indices(
194 rules: &[(&str, (usize, usize))],
195 source: &str,
196 chunks: &[CssChunk],
197) -> Vec<Option<usize>> {
198 let source_start = source.as_ptr() as usize;
199 let source_end = source_start.saturating_add(source.len());
200
201 rules
202 .iter()
203 .map(|(selectors, _)| {
204 let sel_start = selectors.as_ptr() as usize;
205 if sel_start >= source_start && sel_start < source_end {
207 let offset = sel_start.wrapping_sub(source_start);
208 find_chunk_index(chunks, offset)
209 } else {
210 None
211 }
212 })
213 .collect()
214}
215
216struct CssBuffer {
217 raw: String,
218 chunks: Option<Vec<CssChunk>>,
219}
220
221impl CssBuffer {
222 fn new(track_chunks: bool) -> Self {
223 CssBuffer {
224 raw: String::new(),
225 chunks: track_chunks.then(Vec::new),
226 }
227 }
228
229 fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
230 if content.is_empty() {
231 return;
232 }
233 let start = self.raw.len();
234 self.raw.push_str(content);
235 if append_newline {
236 self.raw.push('\n');
237 }
238 if let Some(chunks) = &mut self.chunks {
239 let end = self.raw.len();
240 chunks.push(CssChunk {
241 range: start..end,
242 style_node,
243 });
244 }
245 }
246
247 fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
248 (self.raw, self.chunks)
249 }
250}
251
252fn apply_selector_cleanup<'i>(
253 state: &SelectorCleanupState<'i>,
254 document: &mut Document,
255 requested_keep_style_tags: bool,
256 declarations: &[parser::Declaration<'i>],
257 source: &'i str,
258) {
259 if state.chunks.is_empty() {
260 return;
261 }
262 rewrite_style_blocks(
263 state,
264 document,
265 requested_keep_style_tags,
266 declarations,
267 source,
268 );
269}
270
271fn rewrite_style_blocks<'i>(
272 state: &SelectorCleanupState<'i>,
273 document: &mut Document,
274 requested_keep_style_tags: bool,
275 declarations: &[parser::Declaration<'i>],
276 source: &'i str,
277) {
278 let source_start = source.as_ptr() as usize;
279 let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
280 (0..state.chunks.len()).map(|_| Vec::new()).collect();
281 let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
282
283 for usage in &state.usages {
284 if usage.matched {
285 continue;
286 }
287 let trimmed = usage.selector.trim();
288 if trimmed.is_empty() {
289 continue;
290 }
291 let key = (usage.chunk_index, usage.rule_id);
292 let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
293 let idx = chunk_remainders[usage.chunk_index].len();
294 chunk_remainders[usage.chunk_index].push(RuleRemainder {
295 selectors: SelectorList::new(),
296 declarations: usage.declarations,
297 offset: (trimmed.as_ptr() as usize).wrapping_sub(source_start),
298 });
299 idx
300 });
301 chunk_remainders[usage.chunk_index][*entry_index]
302 .selectors
303 .push(trimmed);
304 }
305
306 for (idx, chunk) in state.chunks.iter().enumerate() {
307 let mut items: Vec<(usize, String)> = Vec::new();
309 for remainder in &chunk_remainders[idx] {
310 let mut text = String::new();
311 append_rule(&mut text, remainder, declarations);
312 items.push((remainder.offset, text.trim().to_string()));
313 }
314 for range in &state.chunk_at_rules[idx] {
315 items.push((range.start, source[range.clone()].trim().to_string()));
316 }
317 items.retain(|(_, text)| !text.is_empty());
318 if items.is_empty() {
319 handle_empty_remainder(document, chunk, requested_keep_style_tags);
320 continue;
321 }
322 items.sort_by_key(|(offset, _)| *offset);
323 let buffer = items
324 .into_iter()
325 .map(|(_, text)| text)
326 .collect::<Vec<_>>()
327 .join("\n");
328 if let Some(node_id) = chunk.style_node {
329 overwrite_style_node(document, node_id, &buffer);
330 }
331 }
332}
333
334fn handle_empty_remainder(
335 document: &mut Document,
336 chunk: &CssChunk,
337 requested_keep_style_tags: bool,
338) {
339 if let Some(node_id) = chunk.style_node {
340 if requested_keep_style_tags {
341 overwrite_style_node(document, node_id, "");
342 } else {
343 document.detach_node(node_id);
344 }
345 }
346}
347
348fn append_rule<'i>(
349 buffer: &mut String,
350 remainder: &RuleRemainder<'i>,
351 declarations: &[parser::Declaration<'i>],
352) {
353 let (start, end) = remainder.declarations;
354 if start >= end || end > declarations.len() {
355 return;
356 }
357 let mut selectors_iter = remainder.selectors.iter().peekable();
358 while let Some(selector) = selectors_iter.next() {
359 buffer.push_str(selector);
360 if selectors_iter.peek().is_some() {
361 buffer.push_str(", ");
362 }
363 }
364 buffer.push_str(" {");
365 for (name, value) in &declarations[start..end] {
366 buffer.push(' ');
367 buffer.push_str(name);
368 buffer.push(':');
369 buffer.push(' ');
370 let value_trimmed = value.trim();
371 buffer.push_str(value_trimmed);
372 if !value_trimmed.ends_with(';') {
373 buffer.push(';');
374 }
375 }
376 buffer.push_str(" }\n");
377}
378
379fn overwrite_style_node(document: &mut Document, node_id: NodeId, new_css: &str) {
380 let new_css = new_css.trim();
381 if let Some(text_node_id) = document[node_id].first_child {
382 if let NodeData::Text { text } = &mut document[text_node_id].data {
383 text.clear();
384 text.push_slice(new_css);
385 }
386 }
387}
388
389impl<'a> InlineOptions<'a> {
390 #[must_use]
392 pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
393 self.inline_style_tags = inline_style_tags;
394 self
395 }
396
397 #[must_use]
399 pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
400 self.keep_style_tags = keep_style_tags;
401 self
402 }
403
404 #[must_use]
406 pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
407 self.keep_link_tags = keep_link_tags;
408 self
409 }
410
411 #[must_use]
413 pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
414 self.keep_at_rules = keep_at_rules;
415 self
416 }
417
418 #[must_use]
420 pub fn minify_css(mut self, minify_css: bool) -> Self {
421 self.minify_css = minify_css;
422 self
423 }
424
425 #[must_use]
427 pub fn base_url(mut self, base_url: Option<Url>) -> Self {
428 self.base_url = base_url;
429 self
430 }
431
432 #[must_use]
434 pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
435 self.load_remote_stylesheets = load_remote_stylesheets;
436 self
437 }
438
439 #[must_use]
441 #[cfg(feature = "stylesheet-cache")]
442 pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
443 if let Some(cache) = cache.into() {
444 self.cache = Some(std::sync::Mutex::new(cache));
445 } else {
446 self.cache = None;
447 }
448 self
449 }
450
451 #[must_use]
453 pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
454 self.extra_css = extra_css;
455 self
456 }
457
458 #[must_use]
460 pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
461 self.preallocate_node_capacity = preallocate_node_capacity;
462 self
463 }
464
465 #[must_use]
467 pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
468 self.resolver = resolver;
469 self
470 }
471
472 #[must_use]
474 pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
475 self.remove_inlined_selectors = enabled;
476 self
477 }
478
479 #[must_use]
484 pub fn apply_width_attributes(mut self, apply: bool) -> Self {
485 self.apply_width_attributes = apply;
486 self
487 }
488
489 #[must_use]
494 pub fn apply_height_attributes(mut self, apply: bool) -> Self {
495 self.apply_height_attributes = apply;
496 self
497 }
498
499 #[must_use]
501 pub const fn build(self) -> CSSInliner<'a> {
502 CSSInliner::new(self)
503 }
504}
505
506impl Default for InlineOptions<'_> {
507 #[inline]
508 fn default() -> Self {
509 InlineOptions {
510 inline_style_tags: true,
511 keep_style_tags: false,
512 keep_link_tags: false,
513 keep_at_rules: false,
514 minify_css: false,
515 base_url: None,
516 load_remote_stylesheets: true,
517 #[cfg(feature = "stylesheet-cache")]
518 cache: None,
519 extra_css: None,
520 preallocate_node_capacity: 32,
521 resolver: Arc::new(DefaultStylesheetResolver),
522 remove_inlined_selectors: false,
523 apply_width_attributes: false,
524 apply_height_attributes: false,
525 }
526 }
527}
528
529pub type Result<T> = std::result::Result<T, InlineError>;
531
532#[derive(Debug)]
534pub struct CSSInliner<'a> {
535 options: InlineOptions<'a>,
536}
537
538const GROWTH_COEFFICIENT: f64 = 1.5;
539const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
541
542fn allocate_output_buffer(html: &str) -> Vec<u8> {
543 #[allow(
545 clippy::cast_precision_loss,
546 clippy::cast_sign_loss,
547 clippy::cast_possible_truncation
548 )]
549 Vec::with_capacity(
550 (html.len() as f64 * GROWTH_COEFFICIENT)
551 .min(usize::MAX as f64)
552 .round() as usize,
553 )
554}
555
556impl<'a> CSSInliner<'a> {
557 #[must_use]
559 #[inline]
560 pub const fn new(options: InlineOptions<'a>) -> Self {
561 CSSInliner { options }
562 }
563
564 #[must_use]
583 #[inline]
584 pub fn options() -> InlineOptions<'a> {
585 InlineOptions::default()
586 }
587
588 #[inline]
604 pub fn inline(&self, html: &str) -> Result<String> {
605 let mut out = allocate_output_buffer(html);
606 self.inline_to(html, &mut out)?;
607 Ok(String::from_utf8_lossy(&out).to_string())
608 }
609
610 #[inline]
626 pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
627 self.inline_to_impl(html, None, target, InliningMode::Document)
628 }
629
630 pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
653 let mut out = allocate_output_buffer(html);
654 self.inline_fragment_to(html, css, &mut out)?;
655 Ok(String::from_utf8_lossy(&out).to_string())
656 }
657
658 pub fn inline_fragment_to<W: Write>(
675 &self,
676 html: &str,
677 css: &str,
678 target: &mut W,
679 ) -> Result<()> {
680 self.inline_to_impl(html, Some(css), target, InliningMode::Fragment)
681 }
682
683 #[allow(clippy::too_many_lines)]
684 fn inline_to_impl<W: Write>(
685 &self,
686 html: &str,
687 css: Option<&str>,
688 target: &mut W,
689 mode: InliningMode,
690 ) -> Result<()> {
691 let mut document = Document::parse_with_options(
692 html.as_bytes(),
693 self.options.preallocate_node_capacity,
694 mode,
695 );
696 let track_selector_cleanup =
705 self.options.remove_inlined_selectors && self.options.inline_style_tags;
706 let mut size_estimate: usize = if self.options.inline_style_tags {
707 document
708 .styles()
709 .map(|(_, s)| {
710 s.len().saturating_add(1)
712 })
713 .sum()
714 } else {
715 0
716 };
717 if let Some(extra_css) = &self.options.extra_css {
718 size_estimate = size_estimate.saturating_add(extra_css.len());
719 }
720 if let Some(css) = css {
721 size_estimate = size_estimate.saturating_add(css.len());
722 }
723 let mut css_buffer = CssBuffer::new(track_selector_cleanup);
724 css_buffer.raw.reserve(size_estimate);
725 if self.options.inline_style_tags || self.options.keep_at_rules {
726 for (node_id, style) in document.styles() {
727 let style_node = track_selector_cleanup.then_some(node_id);
728 css_buffer.push(style_node, style, true);
729 }
730 }
731 if self.options.load_remote_stylesheets {
732 let mut links = document.stylesheets().collect::<Vec<&str>>();
733 links.sort_unstable();
734 links.dedup();
735 for href in &links {
736 let url = self.get_full_url(href);
737 #[cfg(feature = "stylesheet-cache")]
738 if let Some(lock) = self.options.cache.as_ref() {
739 let mut cache = lock.lock().expect("Cache lock is poisoned");
740 if let Some(cached) = cache.get(url.as_ref()) {
741 css_buffer.push(None, cached, true);
742 continue;
743 }
744 }
745
746 let css = self.options.resolver.retrieve(url.as_ref())?;
747 css_buffer.push(None, &css, true);
748
749 #[cfg(feature = "stylesheet-cache")]
750 if let Some(lock) = self.options.cache.as_ref() {
751 let mut cache = lock.lock().expect("Cache lock is poisoned");
752 cache.put(url.into_owned(), css);
753 }
754 }
755 }
756 if let Some(extra_css) = &self.options.extra_css {
757 css_buffer.push(None, extra_css, false);
758 }
759 if let Some(css) = css {
760 css_buffer.push(None, css, false);
761 }
762 let (raw_styles, css_chunks) = css_buffer.into_parts();
763 let mut selector_cleanup_state = if track_selector_cleanup {
764 Some(SelectorCleanupState::default())
765 } else {
766 None
767 };
768 if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
769 state.chunk_at_rules = vec![Vec::new(); chunks.len()];
770 state.chunks = chunks;
771 }
772 let mut parser = cssparser::Parser::new(&raw_styles);
773 #[allow(
775 clippy::cast_precision_loss,
776 clippy::cast_sign_loss,
777 clippy::cast_possible_truncation
778 )]
779 let mut declarations = Vec::with_capacity(
780 ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
781 .min(usize::MAX as f64)
782 .round() as usize)
783 .max(16),
784 );
785 let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
786 let at_rules = if self.options.keep_at_rules {
787 let mut at_rules = String::new();
788 for rule in cssparser::StyleSheetParser::new(
789 &mut parser,
790 &mut parser::AtRuleFilteringParser::new(&mut declarations, &mut at_rules),
791 )
792 .flatten()
793 {
794 if self.options.inline_style_tags {
795 rule_list.push(rule);
796 }
797 }
798 Some(at_rules)
799 } else if !raw_styles.is_empty() {
800 let mut rule_list_parser = parser::CSSRuleListParser::new(&mut declarations);
804 let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_list_parser);
805 let mut rejected = None;
809 loop {
810 if let Some(rule) = rejected.take() {
811 items.input.skip_whitespace();
812 let end = items.input.position().byte_index();
813 if let Some(state) = selector_cleanup_state.as_mut() {
814 state.record_at_rule(&raw_styles, rule, end);
815 }
816 }
817 match items.next() {
818 None => break,
819 Some(Ok(rule)) => rule_list.push(rule),
820 Some(Err((_, rule, _))) if selector_cleanup_state.is_some() => {
821 rejected = Some(rule);
822 }
823 Some(Err(_)) => {}
824 }
825 }
826 None
827 } else {
828 None
829 };
830 let rule_chunk_indices = selector_cleanup_state
832 .as_ref()
833 .map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
834 .unwrap_or_default();
835 let mut styles: Vec<Option<SmallVec<[_; 4]>>> = vec![None; document.nodes.len()];
837 let mut caches = SelectorCaches::default();
839 for (rule_id, (selectors, (start, end))) in rule_list.iter().enumerate() {
840 for selector in selectors.split(',') {
843 let mut matched_any = false;
844 if !document.anchor_exists(selector) {
847 if let Some(state) = selector_cleanup_state.as_mut() {
848 if let Some(chunk_index) =
849 rule_chunk_indices.get(rule_id).copied().flatten()
850 {
851 state.record_usage(SelectorUsage {
852 selector,
853 declarations: (*start, *end),
854 rule_id,
855 chunk_index,
856 matched: false,
857 });
858 }
859 }
860 continue;
861 }
862 if let Ok(matching_elements) = document.select(selector, &mut caches) {
863 let specificity = matching_elements.specificity();
864 for matching_element in matching_elements {
865 matched_any = true;
866 let element_styles = styles[matching_element.node_id.get()]
867 .get_or_insert_with(SmallVec::new);
868 for (name, value) in &declarations[*start..*end] {
871 let prop_name = name.as_ref();
872 if let Some(idx) =
874 element_styles.iter().position(|(n, _, _)| *n == prop_name)
875 {
876 let entry: &mut (&str, Specificity, &str) =
877 &mut element_styles[idx];
878 let new_important = value.trim_end().ends_with("!important");
879 let old_important = entry.2.trim_end().ends_with("!important");
880 match (new_important, old_important) {
881 (false, false) | (true, true) => {
883 if entry.1 <= specificity {
884 entry.1 = specificity;
885 entry.2 = *value;
886 }
887 }
888 (true, false) => {
890 entry.1 = specificity;
891 entry.2 = *value;
892 }
893 (false, true) => {}
896 }
897 } else {
898 element_styles.push((prop_name, specificity, *value));
899 }
900 }
901 }
902 }
903 if let Some(state) = selector_cleanup_state.as_mut() {
904 if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
905 state.record_usage(SelectorUsage {
906 selector,
907 declarations: (*start, *end),
908 rule_id,
909 chunk_index,
910 matched: matched_any,
911 });
912 }
913 }
914 }
917 }
918 let cleanup_requires_css = selector_cleanup_state
919 .as_ref()
920 .is_some_and(SelectorCleanupState::needs_css);
921 let keep_style_tags = self.options.keep_style_tags || cleanup_requires_css;
922 if let Some(state) = selector_cleanup_state.as_ref() {
923 apply_selector_cleanup(
924 state,
925 &mut document,
926 self.options.keep_style_tags,
927 &declarations,
928 &raw_styles,
929 );
930 }
931 document.serialize(
932 target,
933 styles,
934 keep_style_tags,
935 self.options.keep_link_tags,
936 self.options.minify_css,
937 at_rules.as_ref(),
938 mode,
939 self.options.apply_width_attributes,
940 self.options.apply_height_attributes,
941 )?;
942 Ok(())
943 }
944
945 fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
946 if Url::parse(href).is_ok() {
948 return Cow::Borrowed(href);
949 }
950 if let Some(base_url) = &self.options.base_url {
951 if href.starts_with("//") {
953 return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
954 }
955 if let Ok(new_url) = base_url.join(href) {
957 return Cow::Owned(new_url.into());
958 }
959 }
960 Cow::Borrowed(href)
962 }
963}
964
965impl Default for CSSInliner<'_> {
966 #[inline]
967 fn default() -> Self {
968 CSSInliner::new(InlineOptions::default())
969 }
970}
971
972#[inline]
987pub fn inline(html: &str) -> Result<String> {
988 CSSInliner::default().inline(html)
989}
990
991#[inline]
1006pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
1007 CSSInliner::default().inline_to(html, target)
1008}
1009
1010#[inline]
1027pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
1028 CSSInliner::default().inline_fragment(html, css)
1029}
1030
1031#[inline]
1046pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
1047 CSSInliner::default().inline_fragment_to(html, css, target)
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use crate::{CSSInliner, InlineOptions};
1053
1054 #[test]
1055 fn test_inliner_sync_send() {
1056 fn assert_send<T: Send + Sync>() {}
1057 assert_send::<CSSInliner<'_>>();
1058 assert_send::<InlineOptions<'_>>();
1059 }
1060}