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}
145
146#[derive(Debug, Default)]
147struct SelectorCleanupState<'i> {
148 chunks: Vec<CssChunk>,
149 usages: Vec<SelectorUsage<'i>>,
150}
151
152impl<'i> SelectorCleanupState<'i> {
153 fn record_usage(&mut self, usage: SelectorUsage<'i>) {
154 self.usages.push(usage);
155 }
156
157 fn has_unmatched(&self) -> bool {
158 self.usages.iter().any(|usage| !usage.matched)
159 }
160}
161
162fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
164 chunks
165 .iter()
166 .position(|chunk| chunk.range.contains(&offset))
167}
168
169#[allow(clippy::arithmetic_side_effects)]
171fn compute_rule_chunk_indices(
172 rules: &[(&str, (usize, usize))],
173 source: &str,
174 chunks: &[CssChunk],
175) -> Vec<Option<usize>> {
176 let source_start = source.as_ptr() as usize;
177 let source_end = source_start.saturating_add(source.len());
178
179 rules
180 .iter()
181 .map(|(selectors, _)| {
182 let sel_start = selectors.as_ptr() as usize;
183 if sel_start >= source_start && sel_start < source_end {
185 let offset = sel_start.wrapping_sub(source_start);
186 find_chunk_index(chunks, offset)
187 } else {
188 None
189 }
190 })
191 .collect()
192}
193
194struct CssBuffer {
195 raw: String,
196 chunks: Option<Vec<CssChunk>>,
197}
198
199impl CssBuffer {
200 fn new(track_chunks: bool) -> Self {
201 CssBuffer {
202 raw: String::new(),
203 chunks: track_chunks.then(Vec::new),
204 }
205 }
206
207 fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
208 if content.is_empty() {
209 return;
210 }
211 let start = self.raw.len();
212 self.raw.push_str(content);
213 if append_newline {
214 self.raw.push('\n');
215 }
216 if let Some(chunks) = &mut self.chunks {
217 let end = self.raw.len();
218 chunks.push(CssChunk {
219 range: start..end,
220 style_node,
221 });
222 }
223 }
224
225 fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
226 (self.raw, self.chunks)
227 }
228}
229
230fn apply_selector_cleanup<'i>(
231 state: &SelectorCleanupState<'i>,
232 document: &mut Document,
233 requested_keep_style_tags: bool,
234 declarations: &[parser::Declaration<'i>],
235) {
236 if state.usages.is_empty() || state.chunks.is_empty() {
237 return;
238 }
239 rewrite_style_blocks(state, document, requested_keep_style_tags, declarations);
240}
241
242fn rewrite_style_blocks<'i>(
243 state: &SelectorCleanupState<'i>,
244 document: &mut Document,
245 requested_keep_style_tags: bool,
246 declarations: &[parser::Declaration<'i>],
247) {
248 let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
249 (0..state.chunks.len()).map(|_| Vec::new()).collect();
250 let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
251
252 for usage in &state.usages {
253 if usage.matched {
254 continue;
255 }
256 let trimmed = usage.selector.trim();
257 if trimmed.is_empty() {
258 continue;
259 }
260 let key = (usage.chunk_index, usage.rule_id);
261 let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
262 let idx = chunk_remainders[usage.chunk_index].len();
263 chunk_remainders[usage.chunk_index].push(RuleRemainder {
264 selectors: SelectorList::new(),
265 declarations: usage.declarations,
266 });
267 idx
268 });
269 chunk_remainders[usage.chunk_index][*entry_index]
270 .selectors
271 .push(trimmed);
272 }
273
274 for (idx, chunk) in state.chunks.iter().enumerate() {
275 let rules = &chunk_remainders[idx];
276 if rules.is_empty() {
277 handle_empty_remainder(document, chunk, requested_keep_style_tags);
278 continue;
279 }
280 let mut buffer = String::new();
281 for remainder in rules {
282 append_rule(&mut buffer, remainder, declarations);
283 }
284 if buffer.trim().is_empty() {
285 handle_empty_remainder(document, chunk, requested_keep_style_tags);
286 continue;
287 }
288 if let Some(node_id) = chunk.style_node {
289 overwrite_style_node(document, node_id, buffer.trim_end());
290 }
291 }
292}
293
294fn handle_empty_remainder(
295 document: &mut Document,
296 chunk: &CssChunk,
297 requested_keep_style_tags: bool,
298) {
299 if let Some(node_id) = chunk.style_node {
300 if requested_keep_style_tags {
301 overwrite_style_node(document, node_id, "");
302 } else {
303 document.detach_node(node_id);
304 }
305 }
306}
307
308fn append_rule<'i>(
309 buffer: &mut String,
310 remainder: &RuleRemainder<'i>,
311 declarations: &[parser::Declaration<'i>],
312) {
313 let (start, end) = remainder.declarations;
314 if start >= end || end > declarations.len() {
315 return;
316 }
317 let mut selectors_iter = remainder.selectors.iter().peekable();
318 while let Some(selector) = selectors_iter.next() {
319 buffer.push_str(selector);
320 if selectors_iter.peek().is_some() {
321 buffer.push_str(", ");
322 }
323 }
324 buffer.push_str(" {");
325 for (name, value) in &declarations[start..end] {
326 buffer.push(' ');
327 buffer.push_str(name);
328 buffer.push(':');
329 buffer.push(' ');
330 let value_trimmed = value.trim();
331 buffer.push_str(value_trimmed);
332 if !value_trimmed.ends_with(';') {
333 buffer.push(';');
334 }
335 }
336 buffer.push_str(" }\n");
337}
338
339fn overwrite_style_node(document: &mut Document, node_id: NodeId, new_css: &str) {
340 let new_css = new_css.trim();
341 if let Some(text_node_id) = document[node_id].first_child {
342 if let NodeData::Text { text } = &mut document[text_node_id].data {
343 text.clear();
344 text.push_slice(new_css);
345 }
346 }
347}
348
349impl<'a> InlineOptions<'a> {
350 #[must_use]
352 pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
353 self.inline_style_tags = inline_style_tags;
354 self
355 }
356
357 #[must_use]
359 pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
360 self.keep_style_tags = keep_style_tags;
361 self
362 }
363
364 #[must_use]
366 pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
367 self.keep_link_tags = keep_link_tags;
368 self
369 }
370
371 #[must_use]
373 pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
374 self.keep_at_rules = keep_at_rules;
375 self
376 }
377
378 #[must_use]
380 pub fn minify_css(mut self, minify_css: bool) -> Self {
381 self.minify_css = minify_css;
382 self
383 }
384
385 #[must_use]
387 pub fn base_url(mut self, base_url: Option<Url>) -> Self {
388 self.base_url = base_url;
389 self
390 }
391
392 #[must_use]
394 pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
395 self.load_remote_stylesheets = load_remote_stylesheets;
396 self
397 }
398
399 #[must_use]
401 #[cfg(feature = "stylesheet-cache")]
402 pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
403 if let Some(cache) = cache.into() {
404 self.cache = Some(std::sync::Mutex::new(cache));
405 } else {
406 self.cache = None;
407 }
408 self
409 }
410
411 #[must_use]
413 pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
414 self.extra_css = extra_css;
415 self
416 }
417
418 #[must_use]
420 pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
421 self.preallocate_node_capacity = preallocate_node_capacity;
422 self
423 }
424
425 #[must_use]
427 pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
428 self.resolver = resolver;
429 self
430 }
431
432 #[must_use]
434 pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
435 self.remove_inlined_selectors = enabled;
436 self
437 }
438
439 #[must_use]
444 pub fn apply_width_attributes(mut self, apply: bool) -> Self {
445 self.apply_width_attributes = apply;
446 self
447 }
448
449 #[must_use]
454 pub fn apply_height_attributes(mut self, apply: bool) -> Self {
455 self.apply_height_attributes = apply;
456 self
457 }
458
459 #[must_use]
461 pub const fn build(self) -> CSSInliner<'a> {
462 CSSInliner::new(self)
463 }
464}
465
466impl Default for InlineOptions<'_> {
467 #[inline]
468 fn default() -> Self {
469 InlineOptions {
470 inline_style_tags: true,
471 keep_style_tags: false,
472 keep_link_tags: false,
473 keep_at_rules: false,
474 minify_css: false,
475 base_url: None,
476 load_remote_stylesheets: true,
477 #[cfg(feature = "stylesheet-cache")]
478 cache: None,
479 extra_css: None,
480 preallocate_node_capacity: 32,
481 resolver: Arc::new(DefaultStylesheetResolver),
482 remove_inlined_selectors: false,
483 apply_width_attributes: false,
484 apply_height_attributes: false,
485 }
486 }
487}
488
489pub type Result<T> = std::result::Result<T, InlineError>;
491
492#[derive(Debug)]
494pub struct CSSInliner<'a> {
495 options: InlineOptions<'a>,
496}
497
498const GROWTH_COEFFICIENT: f64 = 1.5;
499const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
501
502fn allocate_output_buffer(html: &str) -> Vec<u8> {
503 #[allow(
505 clippy::cast_precision_loss,
506 clippy::cast_sign_loss,
507 clippy::cast_possible_truncation
508 )]
509 Vec::with_capacity(
510 (html.len() as f64 * GROWTH_COEFFICIENT)
511 .min(usize::MAX as f64)
512 .round() as usize,
513 )
514}
515
516impl<'a> CSSInliner<'a> {
517 #[must_use]
519 #[inline]
520 pub const fn new(options: InlineOptions<'a>) -> Self {
521 CSSInliner { options }
522 }
523
524 #[must_use]
543 #[inline]
544 pub fn options() -> InlineOptions<'a> {
545 InlineOptions::default()
546 }
547
548 #[inline]
564 pub fn inline(&self, html: &str) -> Result<String> {
565 let mut out = allocate_output_buffer(html);
566 self.inline_to(html, &mut out)?;
567 Ok(String::from_utf8_lossy(&out).to_string())
568 }
569
570 #[inline]
586 pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
587 self.inline_to_impl(html, None, target, InliningMode::Document)
588 }
589
590 pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
605 let mut out = allocate_output_buffer(html);
606 self.inline_fragment_to(html, css, &mut out)?;
607 Ok(String::from_utf8_lossy(&out).to_string())
608 }
609
610 pub fn inline_fragment_to<W: Write>(
625 &self,
626 html: &str,
627 css: &str,
628 target: &mut W,
629 ) -> Result<()> {
630 self.inline_to_impl(html, Some(css), target, InliningMode::Fragment)
631 }
632
633 #[allow(clippy::too_many_lines)]
634 fn inline_to_impl<W: Write>(
635 &self,
636 html: &str,
637 css: Option<&str>,
638 target: &mut W,
639 mode: InliningMode,
640 ) -> Result<()> {
641 let mut document = Document::parse_with_options(
642 html.as_bytes(),
643 self.options.preallocate_node_capacity,
644 mode,
645 );
646 let track_selector_cleanup = self.options.remove_inlined_selectors;
654 let mut size_estimate: usize = if self.options.inline_style_tags {
655 document
656 .styles()
657 .map(|(_, s)| {
658 s.len().saturating_add(1)
660 })
661 .sum()
662 } else {
663 0
664 };
665 if let Some(extra_css) = &self.options.extra_css {
666 size_estimate = size_estimate.saturating_add(extra_css.len());
667 }
668 if let Some(css) = css {
669 size_estimate = size_estimate.saturating_add(css.len());
670 }
671 let mut css_buffer = CssBuffer::new(track_selector_cleanup);
672 css_buffer.raw.reserve(size_estimate);
673 if self.options.inline_style_tags || self.options.keep_at_rules {
674 for (node_id, style) in document.styles() {
675 let style_node = track_selector_cleanup.then_some(node_id);
676 css_buffer.push(style_node, style, true);
677 }
678 }
679 if self.options.load_remote_stylesheets {
680 let mut links = document.stylesheets().collect::<Vec<&str>>();
681 links.sort_unstable();
682 links.dedup();
683 for href in &links {
684 let url = self.get_full_url(href);
685 #[cfg(feature = "stylesheet-cache")]
686 if let Some(lock) = self.options.cache.as_ref() {
687 let mut cache = lock.lock().expect("Cache lock is poisoned");
688 if let Some(cached) = cache.get(url.as_ref()) {
689 css_buffer.push(None, cached, true);
690 continue;
691 }
692 }
693
694 let css = self.options.resolver.retrieve(url.as_ref())?;
695 css_buffer.push(None, &css, true);
696
697 #[cfg(feature = "stylesheet-cache")]
698 if let Some(lock) = self.options.cache.as_ref() {
699 let mut cache = lock.lock().expect("Cache lock is poisoned");
700 cache.put(url.into_owned(), css);
701 }
702 }
703 }
704 if let Some(extra_css) = &self.options.extra_css {
705 css_buffer.push(None, extra_css, false);
706 }
707 if let Some(css) = css {
708 css_buffer.push(None, css, false);
709 }
710 let (raw_styles, css_chunks) = css_buffer.into_parts();
711 let mut selector_cleanup_state = if track_selector_cleanup {
712 Some(SelectorCleanupState::default())
713 } else {
714 None
715 };
716 if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
717 state.chunks = chunks;
718 }
719 let mut parse_input = cssparser::ParserInput::new(&raw_styles);
720 let mut parser = cssparser::Parser::new(&mut parse_input);
721 #[allow(
723 clippy::cast_precision_loss,
724 clippy::cast_sign_loss,
725 clippy::cast_possible_truncation
726 )]
727 let mut declarations = Vec::with_capacity(
728 ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
729 .min(usize::MAX as f64)
730 .round() as usize)
731 .max(16),
732 );
733 let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
734 let at_rules = if self.options.keep_at_rules {
735 let mut at_rules = String::new();
736 for rule in cssparser::StyleSheetParser::new(
737 &mut parser,
738 &mut parser::AtRuleFilteringParser::new(&mut declarations, &mut at_rules),
739 )
740 .flatten()
741 {
742 if self.options.inline_style_tags {
743 rule_list.push(rule);
744 }
745 }
746 Some(at_rules)
747 } else if !raw_styles.is_empty() {
748 for rule in cssparser::StyleSheetParser::new(
750 &mut parser,
751 &mut parser::CSSRuleListParser::new(&mut declarations),
752 )
753 .flatten()
754 {
755 rule_list.push(rule);
756 }
757 None
758 } else {
759 None
760 };
761 let rule_chunk_indices = selector_cleanup_state
763 .as_ref()
764 .map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
765 .unwrap_or_default();
766 let mut styles: Vec<Option<SmallVec<[_; 4]>>> = vec![None; document.nodes.len()];
768 let mut caches = SelectorCaches::default();
770 for (rule_id, (selectors, (start, end))) in rule_list.iter().enumerate() {
771 for selector in selectors.split(',') {
774 let mut matched_any = false;
775 if !document.anchor_exists(selector) {
778 if let Some(state) = selector_cleanup_state.as_mut() {
779 if let Some(chunk_index) =
780 rule_chunk_indices.get(rule_id).copied().flatten()
781 {
782 state.record_usage(SelectorUsage {
783 selector,
784 declarations: (*start, *end),
785 rule_id,
786 chunk_index,
787 matched: false,
788 });
789 }
790 }
791 continue;
792 }
793 if let Ok(matching_elements) = document.select(selector, &mut caches) {
794 let specificity = matching_elements.specificity();
795 for matching_element in matching_elements {
796 matched_any = true;
797 let element_styles = styles[matching_element.node_id.get()]
798 .get_or_insert_with(SmallVec::new);
799 for (name, value) in &declarations[*start..*end] {
802 let prop_name = name.as_ref();
803 if let Some(idx) =
805 element_styles.iter().position(|(n, _, _)| *n == prop_name)
806 {
807 let entry: &mut (&str, Specificity, &str) =
808 &mut element_styles[idx];
809 let new_important = value.trim_end().ends_with("!important");
810 let old_important = entry.2.trim_end().ends_with("!important");
811 match (new_important, old_important) {
812 (false, false) | (true, true) => {
814 if entry.1 <= specificity {
815 entry.1 = specificity;
816 entry.2 = *value;
817 }
818 }
819 (true, false) => {
821 entry.1 = specificity;
822 entry.2 = *value;
823 }
824 (false, true) => {}
827 }
828 } else {
829 element_styles.push((prop_name, specificity, *value));
830 }
831 }
832 }
833 }
834 if let Some(state) = selector_cleanup_state.as_mut() {
835 if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
836 state.record_usage(SelectorUsage {
837 selector,
838 declarations: (*start, *end),
839 rule_id,
840 chunk_index,
841 matched: matched_any,
842 });
843 }
844 }
845 }
848 }
849 let cleanup_requires_css = selector_cleanup_state
850 .as_ref()
851 .is_some_and(SelectorCleanupState::has_unmatched);
852 let keep_style_tags = self.options.keep_style_tags || cleanup_requires_css;
853 if let Some(state) = selector_cleanup_state.as_ref() {
854 apply_selector_cleanup(
855 state,
856 &mut document,
857 self.options.keep_style_tags,
858 &declarations,
859 );
860 }
861 document.serialize(
862 target,
863 styles,
864 keep_style_tags,
865 self.options.keep_link_tags,
866 self.options.minify_css,
867 at_rules.as_ref(),
868 mode,
869 self.options.apply_width_attributes,
870 self.options.apply_height_attributes,
871 )?;
872 Ok(())
873 }
874
875 fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
876 if Url::parse(href).is_ok() {
878 return Cow::Borrowed(href);
879 }
880 if let Some(base_url) = &self.options.base_url {
881 if href.starts_with("//") {
883 return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
884 }
885 if let Ok(new_url) = base_url.join(href) {
887 return Cow::Owned(new_url.into());
888 }
889 }
890 Cow::Borrowed(href)
892 }
893}
894
895impl Default for CSSInliner<'_> {
896 #[inline]
897 fn default() -> Self {
898 CSSInliner::new(InlineOptions::default())
899 }
900}
901
902#[inline]
917pub fn inline(html: &str) -> Result<String> {
918 CSSInliner::default().inline(html)
919}
920
921#[inline]
936pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
937 CSSInliner::default().inline_to(html, target)
938}
939
940#[inline]
955pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
956 CSSInliner::default().inline_fragment(html, css)
957}
958
959#[inline]
974pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
975 CSSInliner::default().inline_fragment_to(html, css, target)
976}
977
978#[cfg(test)]
979mod tests {
980 use crate::{CSSInliner, InlineOptions};
981
982 #[test]
983 fn test_inliner_sync_send() {
984 fn assert_send<T: Send + Sync>() {}
985 assert_send::<CSSInliner<'_>>();
986 assert_send::<InlineOptions<'_>>();
987 }
988}