Skip to main content

css_inline/
lib.rs

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::{contains_var, CssVariables, 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/// An LRU Cache for external stylesheets.
51#[cfg(feature = "stylesheet-cache")]
52pub type StylesheetCache<S = DefaultHasher> = LruCache<String, String, S>;
53
54/// Configuration options for CSS inlining process.
55#[allow(clippy::struct_excessive_bools)]
56pub struct InlineOptions<'a> {
57    /// Whether to inline CSS from "style" tags.
58    ///
59    /// Sometimes HTML may include a lot of boilerplate styles, that are not applicable in every
60    /// scenario and it is useful to ignore them and use `extra_css` instead.
61    pub inline_style_tags: bool,
62    /// Keep "style" tags after inlining.
63    pub keep_style_tags: bool,
64    /// Keep "link" tags after inlining.
65    pub keep_link_tags: bool,
66    /// Keep "at-rules" after inlining.
67    pub keep_at_rules: bool,
68    /// Remove trailing semicolons and spaces between properties and values.
69    pub minify_css: bool,
70    /// Used for loading external stylesheets via relative URLs.
71    pub base_url: Option<Url>,
72    /// Whether remote stylesheets should be loaded or not.
73    pub load_remote_stylesheets: bool,
74    /// External stylesheet cache.
75    #[cfg(feature = "stylesheet-cache")]
76    pub cache: Option<std::sync::Mutex<StylesheetCache>>,
77    // The point of using `Cow` here is Python bindings, where it is problematic to pass a reference
78    // without dealing with memory leaks & unsafe. With `Cow` we can use moved values as `String` in
79    // Python wrapper for `CSSInliner` and `&str` in Rust & simple functions on the Python side
80    /// Additional CSS to inline.
81    pub extra_css: Option<Cow<'a, str>>,
82    /// Pre-allocate capacity for HTML nodes during parsing.
83    /// It can improve performance when you have an estimate of the number of nodes in your HTML document.
84    pub preallocate_node_capacity: usize,
85    /// A way to resolve stylesheets from various sources.
86    pub resolver: Arc<dyn StylesheetResolver>,
87    /// Remove selectors that were successfully inlined from inline `<style>` blocks.
88    pub remove_inlined_selectors: bool,
89    /// Apply `width` HTML attributes from CSS `width` properties on supported elements.
90    ///
91    /// This is useful for email compatibility with clients like Outlook that ignore CSS width.
92    /// Supported elements: `table`, `td`, `th`, `img`.
93    pub apply_width_attributes: bool,
94    /// Apply `height` HTML attributes from CSS `height` properties on supported elements.
95    ///
96    /// This is useful for email compatibility with clients like Outlook that ignore CSS height.
97    /// Supported elements: `table`, `td`, `th`, `img`.
98    pub apply_height_attributes: bool,
99    /// Substitute `var()` references with the values of the custom properties they refer to
100    /// and drop the `--*` declarations from the inlined styles.
101    ///
102    /// Email clients do not support custom properties; this makes the output work without them.
103    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    /// The style node this chunk came from, if any.
134    /// `None` for linked stylesheets, extra CSS, or fragment CSS.
135    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 of the first selector in the combined CSS.
154    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
169/// A kept `@layer` block; the range covers its contents.
170struct LayerBlock<'i> {
171    range: Range<usize>,
172    prelude: &'i str,
173    layer: LayerId,
174    created: bool,
175}
176
177/// Renders the `@`-rules and leftover style rules that stay in the output, wrapped in their
178/// `@layer` blocks.
179struct KeptCss<'a, 'i> {
180    source: &'i str,
181    layers: &'a Layers<'i>,
182    /// Layers whose rules stay in the output, with their ancestors.
183    kept: Vec<bool>,
184}
185
186impl<'a, 'i> KeptCss<'a, 'i> {
187    /// Rules stay in the output as kept `@`-rules, as selector cleanup leftovers, or verbatim
188    /// when the `<style>` tags are kept as they are.
189    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    /// Whether `layer` or an ancestor has rules in the output, so its position matters.
227    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    /// Render the spans within `range` of the source together with `items` (offset, text),
238    /// `sep`-separated within `@layer` blocks. `@`-rules starting before `at_rules_from` are
239    /// skipped, while the layer structure around them stays.
240    fn render(
241        &self,
242        spans: &[Span<'i>],
243        range: &Range<usize>,
244        at_rules_from: usize,
245        mut items: Vec<(usize, String)>,
246        sep: &str,
247    ) -> Vec<String> {
248        let mut blocks = Vec::new();
249        for span in spans.iter().filter(|span| range.contains(&span.start())) {
250            match span {
251                Span::AtRule { range: at_rule, .. } if at_rule.start < at_rules_from => {}
252                // An unterminated `@`-rule runs into the following blocks; it ends with its own.
253                Span::AtRule { range: at_rule, .. } => items.push((
254                    at_rule.start,
255                    self.source[at_rule.start..at_rule.end.min(range.end)]
256                        .trim()
257                        .to_string(),
258                )),
259                Span::LayerStatement {
260                    start,
261                    prelude,
262                    layers,
263                } => {
264                    if layers.iter().any(|&layer| self.is_kept(layer)) {
265                        items.push((*start, format!("@layer {};", prelude.trim())));
266                    }
267                }
268                Span::LayerBlock {
269                    range: block,
270                    prelude,
271                    layer,
272                    created,
273                } => blocks.push(LayerBlock {
274                    range: block.start..block.end.min(range.end),
275                    prelude,
276                    layer: *layer,
277                    created: *created,
278                }),
279            }
280        }
281        items.retain(|(_, text)| !text.is_empty());
282        items.sort_by_key(|(offset, _)| *offset);
283        blocks.sort_by_key(|block| block.range.start);
284        self.render_tree(&mut items.into_iter().peekable(), &blocks, range.end, sep)
285    }
286
287    /// Blocks are sorted by start and nest, so the blocks starting inside one are its children.
288    #[allow(clippy::arithmetic_side_effects)]
289    fn render_tree(
290        &self,
291        items: &mut Peekable<std::vec::IntoIter<(usize, String)>>,
292        blocks: &[LayerBlock<'i>],
293        end: usize,
294        sep: &str,
295    ) -> Vec<String> {
296        let mut out = Vec::new();
297        let mut i = 0;
298        while let Some(block) = blocks.get(i) {
299            while let Some((_, text)) = items.next_if(|(offset, _)| *offset < block.range.start) {
300                out.push(text);
301            }
302            let mut j = i + 1;
303            while blocks
304                .get(j)
305                .is_some_and(|child| child.range.start < block.range.end)
306            {
307                j += 1;
308            }
309            let inner = self.render_tree(items, &blocks[i + 1..j], block.range.end, sep);
310            let prelude = block.prelude.trim();
311            if inner.is_empty() {
312                // The block declared the layer; a statement keeps its position.
313                if block.created && !prelude.is_empty() && self.is_kept(block.layer) {
314                    out.push(format!("@layer {prelude};"));
315                }
316            } else {
317                let inner = inner.join(sep);
318                if prelude.is_empty() {
319                    out.push(format!("@layer {{{sep}{inner}{sep}}}"));
320                } else {
321                    out.push(format!("@layer {prelude} {{{sep}{inner}{sep}}}"));
322                }
323            }
324            i = j;
325        }
326        while let Some((_, text)) = items.next_if(|(offset, _)| *offset < end) {
327            out.push(text);
328        }
329        out
330    }
331}
332
333/// Find which chunk contains the given byte offset.
334fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
335    chunks
336        .iter()
337        .position(|chunk| chunk.range.contains(&offset))
338}
339
340/// Compute chunk indices for all rules based on where their selectors point in the source.
341#[allow(clippy::arithmetic_side_effects)]
342fn compute_rule_chunk_indices(
343    rules: &[Rule<'_>],
344    source: &str,
345    chunks: &[CssChunk],
346) -> Vec<Option<usize>> {
347    let source_start = source.as_ptr() as usize;
348    let source_end = source_start.saturating_add(source.len());
349
350    rules
351        .iter()
352        .map(|rule| {
353            let sel_start = rule.selectors.as_ptr() as usize;
354            // Check if selectors slice is within source bounds
355            if sel_start >= source_start && sel_start < source_end {
356                let offset = sel_start.wrapping_sub(source_start);
357                find_chunk_index(chunks, offset)
358            } else {
359                None
360            }
361        })
362        .collect()
363}
364
365struct CssBuffer {
366    raw: String,
367    chunks: Option<Vec<CssChunk>>,
368}
369
370impl CssBuffer {
371    fn new(track_chunks: bool) -> Self {
372        CssBuffer {
373            raw: String::new(),
374            chunks: track_chunks.then(Vec::new),
375        }
376    }
377
378    fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
379        if content.is_empty() {
380            return;
381        }
382        let start = self.raw.len();
383        self.raw.push_str(content);
384        if append_newline {
385            self.raw.push('\n');
386        }
387        if let Some(chunks) = &mut self.chunks {
388            let end = self.raw.len();
389            chunks.push(CssChunk {
390                range: start..end,
391                style_node,
392            });
393        }
394    }
395
396    fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
397        (self.raw, self.chunks)
398    }
399}
400
401/// Rewrite the `<style>` blocks to what remains after inlining; whether any of them keeps content.
402fn apply_selector_cleanup<'i>(
403    state: &SelectorCleanupState<'i>,
404    document: &mut Document,
405    requested_keep_style_tags: bool,
406    declarations: &[parser::Declaration<'i>],
407    kept: &KeptCss<'_, 'i>,
408    spans: &[Span<'i>],
409) -> bool {
410    let source = kept.source;
411    let source_start = source.as_ptr() as usize;
412    let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
413        (0..state.chunks.len()).map(|_| Vec::new()).collect();
414    let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
415
416    for usage in &state.usages {
417        if usage.matched {
418            continue;
419        }
420        let trimmed = usage.selector.trim();
421        if trimmed.is_empty() {
422            continue;
423        }
424        let key = (usage.chunk_index, usage.rule_id);
425        let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
426            let idx = chunk_remainders[usage.chunk_index].len();
427            chunk_remainders[usage.chunk_index].push(RuleRemainder {
428                selectors: SelectorList::new(),
429                declarations: usage.declarations,
430                offset: (trimmed.as_ptr() as usize).wrapping_sub(source_start),
431            });
432            idx
433        });
434        chunk_remainders[usage.chunk_index][*entry_index]
435            .selectors
436            .push(trimmed);
437    }
438
439    let mut keeps_content = false;
440    for (idx, chunk) in state.chunks.iter().enumerate() {
441        let Some(node_id) = chunk.style_node else {
442            continue;
443        };
444        // Remainders and `@`-rules, keyed by their offset so the block keeps source order.
445        let mut items: Vec<(usize, String)> = Vec::with_capacity(chunk_remainders[idx].len());
446        for remainder in &chunk_remainders[idx] {
447            let mut text = String::new();
448            append_rule(&mut text, remainder, declarations);
449            items.push((remainder.offset, text));
450        }
451        let rules = kept.render(spans, &chunk.range, 0, items, "\n");
452        if rules.is_empty() {
453            handle_empty_remainder(document, chunk, requested_keep_style_tags);
454            continue;
455        }
456        keeps_content = true;
457        overwrite_style_node(document, node_id, &rules);
458    }
459    keeps_content
460}
461
462fn handle_empty_remainder(
463    document: &mut Document,
464    chunk: &CssChunk,
465    requested_keep_style_tags: bool,
466) {
467    if let Some(node_id) = chunk.style_node {
468        if requested_keep_style_tags {
469            overwrite_style_node(document, node_id, &[]);
470        } else {
471            document.detach_node(node_id);
472        }
473    }
474}
475
476fn append_rule<'i>(
477    buffer: &mut String,
478    remainder: &RuleRemainder<'i>,
479    declarations: &[parser::Declaration<'i>],
480) {
481    let (start, end) = remainder.declarations;
482    if start >= end || end > declarations.len() {
483        return;
484    }
485    let declarations = &declarations[start..end];
486    // `selector, ` per selector, ` name: value;` per declaration and the braces.
487    let size = remainder
488        .selectors
489        .iter()
490        .map(|selector| selector.len().saturating_add(2))
491        .chain(
492            declarations
493                .iter()
494                .map(|(name, value)| name.len().saturating_add(value.len()).saturating_add(4)),
495        )
496        .fold(2, usize::saturating_add);
497    buffer.reserve(size);
498    let mut selectors_iter = remainder.selectors.iter().peekable();
499    while let Some(selector) = selectors_iter.next() {
500        buffer.push_str(selector);
501        if selectors_iter.peek().is_some() {
502            buffer.push_str(", ");
503        }
504    }
505    buffer.push_str(" {");
506    for (name, value) in declarations {
507        buffer.push(' ');
508        buffer.push_str(name);
509        buffer.push(':');
510        buffer.push(' ');
511        let value_trimmed = value.trim();
512        buffer.push_str(value_trimmed);
513        if !value_trimmed.ends_with(';') {
514            buffer.push(';');
515        }
516    }
517    buffer.push_str(" }");
518}
519
520/// Replace the text of a `<style>` node with `rules` joined by newlines.
521fn overwrite_style_node(document: &mut Document, node_id: NodeId, rules: &[String]) {
522    if let Some(text_node_id) = document[node_id].first_child {
523        if let NodeData::Text { text } = &mut document[text_node_id].data {
524            text.clear();
525            for (idx, rule) in rules.iter().enumerate() {
526                // Every rendered rule is non-empty and trimmed, so the joined text needs no trim.
527                debug_assert!(!rule.is_empty() && rule.trim().len() == rule.len());
528                if idx != 0 {
529                    text.push_char('\n');
530                }
531                text.push_slice(rule);
532            }
533        }
534    }
535}
536
537impl<'a> InlineOptions<'a> {
538    /// Override whether "style" tags should be inlined.
539    #[must_use]
540    pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
541        self.inline_style_tags = inline_style_tags;
542        self
543    }
544
545    /// Override whether "style" tags should be kept after processing.
546    #[must_use]
547    pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
548        self.keep_style_tags = keep_style_tags;
549        self
550    }
551
552    /// Override whether "link" tags should be kept after processing.
553    #[must_use]
554    pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
555        self.keep_link_tags = keep_link_tags;
556        self
557    }
558
559    /// Override whether "at-rules" should be kept after processing.
560    #[must_use]
561    pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
562        self.keep_at_rules = keep_at_rules;
563        self
564    }
565
566    /// Override whether trailing semicolons and spaces between properties and values should be removed.
567    #[must_use]
568    pub fn minify_css(mut self, minify_css: bool) -> Self {
569        self.minify_css = minify_css;
570        self
571    }
572
573    /// Set base URL that will be used for loading external stylesheets via relative URLs.
574    #[must_use]
575    pub fn base_url(mut self, base_url: Option<Url>) -> Self {
576        self.base_url = base_url;
577        self
578    }
579
580    /// Override whether remote stylesheets should be loaded.
581    #[must_use]
582    pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
583        self.load_remote_stylesheets = load_remote_stylesheets;
584        self
585    }
586
587    /// Set external stylesheet cache.
588    #[must_use]
589    #[cfg(feature = "stylesheet-cache")]
590    pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
591        if let Some(cache) = cache.into() {
592            self.cache = Some(std::sync::Mutex::new(cache));
593        } else {
594            self.cache = None;
595        }
596        self
597    }
598
599    /// Set additional CSS to inline.
600    #[must_use]
601    pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
602        self.extra_css = extra_css;
603        self
604    }
605
606    /// Set the initial node capacity for HTML tree.
607    #[must_use]
608    pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
609        self.preallocate_node_capacity = preallocate_node_capacity;
610        self
611    }
612
613    /// Set the way to resolve stylesheets from various sources.
614    #[must_use]
615    pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
616        self.resolver = resolver;
617        self
618    }
619
620    /// Remove selectors that were successfully inlined from inline `<style>` blocks.
621    #[must_use]
622    pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
623        self.remove_inlined_selectors = enabled;
624        self
625    }
626
627    /// Apply `width` HTML attributes from CSS `width` properties on supported elements.
628    ///
629    /// This is useful for email compatibility with clients like Outlook that ignore CSS width.
630    /// Supported elements: `table`, `td`, `th`, `img`.
631    #[must_use]
632    pub fn apply_width_attributes(mut self, apply: bool) -> Self {
633        self.apply_width_attributes = apply;
634        self
635    }
636
637    /// Apply `height` HTML attributes from CSS `height` properties on supported elements.
638    ///
639    /// This is useful for email compatibility with clients like Outlook that ignore CSS height.
640    /// Supported elements: `table`, `td`, `th`, `img`.
641    #[must_use]
642    pub fn apply_height_attributes(mut self, apply: bool) -> Self {
643        self.apply_height_attributes = apply;
644        self
645    }
646
647    /// Substitute `var()` references with the values of the custom properties they refer to
648    /// and drop the `--*` declarations from the inlined styles.
649    #[must_use]
650    pub fn resolve_css_variables(mut self, resolve: bool) -> Self {
651        self.resolve_css_variables = resolve;
652        self
653    }
654
655    /// Create a new `CSSInliner` instance from this options.
656    #[must_use]
657    pub const fn build(self) -> CSSInliner<'a> {
658        CSSInliner::new(self)
659    }
660}
661
662impl Default for InlineOptions<'_> {
663    #[inline]
664    fn default() -> Self {
665        InlineOptions {
666            inline_style_tags: true,
667            keep_style_tags: false,
668            keep_link_tags: false,
669            keep_at_rules: false,
670            minify_css: false,
671            base_url: None,
672            load_remote_stylesheets: true,
673            #[cfg(feature = "stylesheet-cache")]
674            cache: None,
675            extra_css: None,
676            preallocate_node_capacity: 32,
677            resolver: Arc::new(DefaultStylesheetResolver),
678            remove_inlined_selectors: false,
679            apply_width_attributes: false,
680            apply_height_attributes: false,
681            resolve_css_variables: false,
682        }
683    }
684}
685
686/// A specialized `Result` type for CSS inlining operations.
687pub type Result<T> = std::result::Result<T, InlineError>;
688
689/// Customizable CSS inliner.
690#[derive(Debug)]
691pub struct CSSInliner<'a> {
692    options: InlineOptions<'a>,
693}
694
695const GROWTH_COEFFICIENT: f64 = 1.5;
696// A rough coefficient to calculate the number of individual declarations based on the total CSS size.
697const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
698
699fn allocate_output_buffer(html: &str) -> Vec<u8> {
700    // Allocating more memory than the input HTML, as the inlined version is usually bigger
701    #[allow(
702        clippy::cast_precision_loss,
703        clippy::cast_sign_loss,
704        clippy::cast_possible_truncation
705    )]
706    Vec::with_capacity(
707        (html.len() as f64 * GROWTH_COEFFICIENT)
708            .min(usize::MAX as f64)
709            .round() as usize,
710    )
711}
712
713/// Reuse the output buffer instead of copying it into a new `String`.
714fn into_string(out: Vec<u8>) -> String {
715    String::from_utf8(out)
716        .unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned())
717}
718
719impl<'a> CSSInliner<'a> {
720    /// Create a new `CSSInliner` instance with given options.
721    #[must_use]
722    #[inline]
723    pub const fn new(options: InlineOptions<'a>) -> Self {
724        CSSInliner { options }
725    }
726
727    /// Return a default `InlineOptions` that can fully configure the CSS inliner.
728    ///
729    /// # Examples
730    ///
731    /// Get default `InlineOptions`, then change base url
732    ///
733    /// ```rust
734    /// use css_inline::{CSSInliner, Url};
735    /// # use url::ParseError;
736    /// # fn run() -> Result<(), ParseError> {
737    /// let url = Url::parse("https://api.example.com")?;
738    /// let inliner = CSSInliner::options()
739    ///     .base_url(Some(url))
740    ///     .build();
741    /// # Ok(())
742    /// # }
743    /// # run().unwrap();
744    /// ```
745    #[must_use]
746    #[inline]
747    pub fn options() -> InlineOptions<'a> {
748        InlineOptions::default()
749    }
750
751    /// Inline CSS styles from <style> tags to matching elements in the HTML tree and return a
752    /// string.
753    ///
754    /// # Errors
755    ///
756    /// Inlining might fail for the following reasons:
757    ///   - Missing stylesheet file;
758    ///   - Remote stylesheet is not available;
759    ///   - IO errors;
760    ///   - Internal CSS selector parsing error;
761    ///
762    /// # Panics
763    ///
764    /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
765    /// using the same inliner panicked while resolving external stylesheets.
766    #[inline]
767    pub fn inline(&self, html: &str) -> Result<String> {
768        let mut out = allocate_output_buffer(html);
769        self.inline_to(html, &mut out)?;
770        Ok(into_string(out))
771    }
772
773    /// Inline CSS & write the result to a generic writer. Use it if you want to write
774    /// the inlined document to a file.
775    ///
776    /// # Errors
777    ///
778    /// Inlining might fail for the following reasons:
779    ///   - Missing stylesheet file;
780    ///   - Remote stylesheet is not available;
781    ///   - IO errors;
782    ///   - Internal CSS selector parsing error;
783    ///
784    /// # Panics
785    ///
786    /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
787    /// using the same inliner panicked while resolving external stylesheets.
788    #[inline]
789    pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
790        self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
791            self.options.resolver.retrieve(url)
792        })
793    }
794
795    /// Inline CSS into an HTML fragment.
796    ///
797    /// Unlike [`inline`](CSSInliner::inline), this method does not wrap the output in `<html>` and
798    /// `<body>` tags. Structural tags (`<html>`, `<head>`, `<body>`) are stripped from the output;
799    /// only their contents are preserved. CSS from `<style>` tags within the input is also
800    /// processed and inlined.
801    ///
802    /// If you need to preserve the full HTML document structure, use [`inline`](CSSInliner::inline)
803    /// instead.
804    ///
805    /// # Errors
806    ///
807    /// Inlining might fail for the following reasons:
808    ///   - Missing stylesheet file;
809    ///   - Remote stylesheet is not available;
810    ///   - IO errors;
811    ///   - Internal CSS selector parsing error;
812    ///
813    /// # Panics
814    ///
815    /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
816    /// using the same inliner panicked while resolving external stylesheets.
817    pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
818        let mut out = allocate_output_buffer(html);
819        self.inline_fragment_to(html, css, &mut out)?;
820        Ok(into_string(out))
821    }
822
823    /// Inline CSS into an HTML fragment and write the result to a generic writer.
824    ///
825    /// See [`inline_fragment`](CSSInliner::inline_fragment) for details on fragment handling.
826    ///
827    /// # Errors
828    ///
829    /// Inlining might fail for the following reasons:
830    ///   - Missing stylesheet file;
831    ///   - Remote stylesheet is not available;
832    ///   - IO errors;
833    ///   - Internal CSS selector parsing error;
834    ///
835    /// # Panics
836    ///
837    /// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
838    /// using the same inliner panicked while resolving external stylesheets.
839    pub fn inline_fragment_to<W: Write>(
840        &self,
841        html: &str,
842        css: &str,
843        target: &mut W,
844    ) -> Result<()> {
845        self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
846            self.options.resolver.retrieve(url)
847        })
848    }
849
850    /// Non-blocking version of [`CSSInliner::inline`]. External stylesheets are fetched concurrently
851    /// via [`StylesheetResolver::retrieve_async`].
852    ///
853    /// # Errors
854    ///
855    /// Same as [`CSSInliner::inline`].
856    ///
857    /// # Panics
858    ///
859    /// If the external stylesheet cache lock is poisoned.
860    #[cfg(feature = "async")]
861    pub async fn inline_async(&self, html: &str) -> Result<String> {
862        let mut out = allocate_output_buffer(html);
863        self.inline_to_async(html, &mut out).await?;
864        Ok(into_string(out))
865    }
866
867    /// Non-blocking version of [`CSSInliner::inline_to`].
868    ///
869    /// # Errors
870    ///
871    /// Same as [`CSSInliner::inline_to`].
872    ///
873    /// # Panics
874    ///
875    /// If the external stylesheet cache lock is poisoned.
876    #[cfg(feature = "async")]
877    pub async fn inline_to_async<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
878        let stylesheets = self.fetch_stylesheets(html, InliningMode::Document).await?;
879        self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
880            // `fetch_stylesheets` collects the same URLs from the same HTML
881            Ok(stylesheets[url].clone())
882        })
883    }
884
885    /// Non-blocking version of [`CSSInliner::inline_fragment`].
886    ///
887    /// # Errors
888    ///
889    /// Same as [`CSSInliner::inline_fragment`].
890    ///
891    /// # Panics
892    ///
893    /// If the external stylesheet cache lock is poisoned.
894    #[cfg(feature = "async")]
895    pub async fn inline_fragment_async(&self, html: &str, css: &str) -> Result<String> {
896        let mut out = allocate_output_buffer(html);
897        self.inline_fragment_to_async(html, css, &mut out).await?;
898        Ok(into_string(out))
899    }
900
901    /// Non-blocking version of [`CSSInliner::inline_fragment_to`].
902    ///
903    /// # Errors
904    ///
905    /// Same as [`CSSInliner::inline_fragment_to`].
906    ///
907    /// # Panics
908    ///
909    /// If the external stylesheet cache lock is poisoned.
910    #[cfg(feature = "async")]
911    pub async fn inline_fragment_to_async<W: Write>(
912        &self,
913        html: &str,
914        css: &str,
915        target: &mut W,
916    ) -> Result<()> {
917        let stylesheets = self.fetch_stylesheets(html, InliningMode::Fragment).await?;
918        self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
919            // `fetch_stylesheets` collects the same URLs from the same HTML
920            Ok(stylesheets[url].clone())
921        })
922    }
923
924    #[cfg(feature = "async")]
925    async fn fetch_stylesheets(
926        &self,
927        html: &str,
928        mode: InliningMode,
929    ) -> Result<FxHashMap<String, String>> {
930        let mut stylesheets = FxHashMap::default();
931        if !self.options.load_remote_stylesheets {
932            return Ok(stylesheets);
933        }
934        let mut missing = Vec::new();
935        {
936            // Dropped before any `.await`, as `Document` is not `Send`
937            let document =
938                Document::parse_with_options(html, self.options.preallocate_node_capacity, mode);
939            let mut urls = document
940                .stylesheets()
941                .map(|href| self.get_full_url(href).into_owned())
942                .collect::<Vec<_>>();
943            urls.sort_unstable();
944            urls.dedup();
945            for url in urls {
946                // Copied, so an eviction before `inline_to_impl` reads the cache does not lose it
947                #[cfg(feature = "stylesheet-cache")]
948                if let Some(lock) = self.options.cache.as_ref() {
949                    let mut cache = lock.lock().expect("Cache lock is poisoned");
950                    if let Some(css) = cache.get(&url) {
951                        stylesheets.insert(url, css.clone());
952                        continue;
953                    }
954                }
955                missing.push(url);
956            }
957        }
958        let fetched = futures_util::future::try_join_all(
959            missing
960                .iter()
961                .map(|url| self.options.resolver.retrieve_async(url)),
962        )
963        .await?;
964        stylesheets.extend(missing.into_iter().zip(fetched));
965        Ok(stylesheets)
966    }
967
968    #[allow(clippy::too_many_lines)]
969    fn inline_to_impl<W: Write>(
970        &self,
971        html: &str,
972        css: Option<&str>,
973        target: &mut W,
974        mode: InliningMode,
975        retrieve: &dyn Fn(&str) -> Result<String>,
976    ) -> Result<()> {
977        let mut document =
978            Document::parse_with_options(html, self.options.preallocate_node_capacity, mode);
979        // CSS rules may overlap, and the final set of rules applied to an element depend on
980        // selectors' specificity - selectors with higher specificity have more priority.
981        // Inlining happens in two major steps:
982        //   1. All available styles are mapped to respective elements together with their
983        //      selector's specificity. When two rules overlap on the same declaration, then
984        //      the one with higher specificity replaces another.
985        //   2. Resulting styles are merged into existing "style" tags.
986        // Without `inline_style_tags` no `<style>` rule is inlined, so none is removed.
987        let track_selector_cleanup =
988            self.options.remove_inlined_selectors && self.options.inline_style_tags;
989        let mut size_estimate: usize = if self.options.inline_style_tags {
990            document
991                .styles()
992                .map(|(_, s)| {
993                    // Add 1 to account for the extra `\n` char we add between styles
994                    s.len().saturating_add(1)
995                })
996                .sum()
997        } else {
998            0
999        };
1000        if let Some(extra_css) = &self.options.extra_css {
1001            size_estimate = size_estimate.saturating_add(extra_css.len());
1002        }
1003        if let Some(css) = css {
1004            size_estimate = size_estimate.saturating_add(css.len());
1005        }
1006        let mut css_buffer = CssBuffer::new(track_selector_cleanup);
1007        css_buffer.raw.reserve(size_estimate);
1008        if self.options.inline_style_tags || self.options.keep_at_rules {
1009            for (node_id, style) in document.styles() {
1010                let style_node = track_selector_cleanup.then_some(node_id);
1011                css_buffer.push(style_node, style, true);
1012            }
1013        }
1014        let styles_end = css_buffer.raw.len();
1015        if self.options.load_remote_stylesheets {
1016            let mut links = document.stylesheets().collect::<Vec<&str>>();
1017            links.sort_unstable();
1018            links.dedup();
1019            for href in &links {
1020                let url = self.get_full_url(href);
1021                #[cfg(feature = "stylesheet-cache")]
1022                if let Some(lock) = self.options.cache.as_ref() {
1023                    let mut cache = lock.lock().expect("Cache lock is poisoned");
1024                    if let Some(cached) = cache.get(url.as_ref()) {
1025                        css_buffer.push(None, cached, true);
1026                        continue;
1027                    }
1028                }
1029
1030                let css = retrieve(url.as_ref())?;
1031                css_buffer.push(None, &css, true);
1032
1033                #[cfg(feature = "stylesheet-cache")]
1034                if let Some(lock) = self.options.cache.as_ref() {
1035                    let mut cache = lock.lock().expect("Cache lock is poisoned");
1036                    cache.put(url.into_owned(), css);
1037                }
1038            }
1039        }
1040        if let Some(extra_css) = &self.options.extra_css {
1041            css_buffer.push(None, extra_css, false);
1042        }
1043        if let Some(css) = css {
1044            css_buffer.push(None, css, false);
1045        }
1046        let (raw_styles, css_chunks) = css_buffer.into_parts();
1047        let mut selector_cleanup_state = if track_selector_cleanup {
1048            Some(SelectorCleanupState::default())
1049        } else {
1050            None
1051        };
1052        if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
1053            state.chunks = chunks;
1054        }
1055        // Allocating some memory for all the parsed declarations
1056        #[allow(
1057            clippy::cast_precision_loss,
1058            clippy::cast_sign_loss,
1059            clippy::cast_possible_truncation
1060        )]
1061        let mut declarations = Vec::with_capacity(
1062            ((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
1063                .min(usize::MAX as f64)
1064                .round() as usize)
1065                .max(16),
1066        );
1067        let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
1068        // `@`-rules are not inlined. `keep_at_rules` moves them to a new `<style>` and selector
1069        // cleanup keeps them in the rewritten ones, so both need their spans.
1070        let mut spans = (self.options.keep_at_rules || track_selector_cleanup).then(Vec::new);
1071        let mut rule_parser = parser::RuleParser::new(
1072            &raw_styles,
1073            &mut declarations,
1074            &mut rule_list,
1075            spans.as_mut(),
1076        );
1077        if !raw_styles.is_empty() {
1078            let mut parser = cssparser::Parser::new(&raw_styles);
1079            let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_parser);
1080            parser::collect_rules(&mut items);
1081        }
1082        let layers = rule_parser.into_layers();
1083        let spans = spans.unwrap_or_default();
1084        if self.options.keep_at_rules && !self.options.inline_style_tags {
1085            // The styles were collected only for their `@`-rules.
1086            rule_list.clear();
1087        }
1088        let ranks = layers.ranks();
1089        // Important declarations cascade in reverse layer order.
1090        let last_rank = u32::try_from(layers.len().saturating_sub(1)).unwrap_or(u32::MAX);
1091        let reversed = |precedence: Precedence| {
1092            Precedence(last_rank.saturating_sub(precedence.0), precedence.1)
1093        };
1094        // Compute chunk indices for all rules once, before processing
1095        let rule_chunk_indices = selector_cleanup_state
1096            .as_ref()
1097            .map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
1098            .unwrap_or_default();
1099        // Vec indexed by NodeId for O(1) access instead of hash lookups
1100        let mut styles: Vec<Option<html::ElementStyleMap<'_>>> = vec![None; document.nodes.len()];
1101        // This cache is unused but required in the `selectors` API
1102        let mut caches = SelectorCaches::default();
1103        for (rule_id, rule) in rule_list.iter().enumerate() {
1104            let (start, end) = rule.declarations;
1105            let rank = ranks[rule.layer];
1106            for selector in parser::split_selector_list(rule.selectors) {
1107                let mut matched_any = false;
1108                // Skip selectors naming a class, ID, or tag absent from the document
1109                // without compiling them
1110                if !document.may_match(selector) {
1111                    if let Some(state) = selector_cleanup_state.as_mut() {
1112                        if let Some(chunk_index) =
1113                            rule_chunk_indices.get(rule_id).copied().flatten()
1114                        {
1115                            state.record_usage(SelectorUsage {
1116                                selector,
1117                                declarations: (start, end),
1118                                rule_id,
1119                                chunk_index,
1120                                matched: false,
1121                            });
1122                        }
1123                    }
1124                    continue;
1125                }
1126                if let Ok(matching_elements) = document.select(selector, &mut caches) {
1127                    let precedence = Precedence(rank, matching_elements.specificity());
1128                    for matching_element in matching_elements {
1129                        matched_any = true;
1130                        let element_styles = styles[matching_element.node_id.get()]
1131                            .get_or_insert_with(SmallVec::new);
1132                        // Iterate over pairs of property name & value
1133                        // Example: `padding`, `0`
1134                        for (name, value) in &declarations[start..end] {
1135                            let prop_name = name.as_ref();
1136                            // Linear search for existing property
1137                            if let Some(idx) =
1138                                element_styles.iter().position(|(n, _, _)| *n == prop_name)
1139                            {
1140                                let entry = &mut element_styles[idx];
1141                                let new_important = parser::strip_important(value).is_some();
1142                                let old_important = parser::strip_important(entry.2).is_some();
1143                                match (new_important, old_important) {
1144                                    // Both normal; the later layer, then the higher specificity wins.
1145                                    (false, false) => {
1146                                        if entry.1 <= precedence {
1147                                            entry.1 = precedence;
1148                                            entry.2 = *value;
1149                                        }
1150                                    }
1151                                    // Both important; the earlier layer, then the higher specificity wins.
1152                                    (true, true) => {
1153                                        if reversed(entry.1) <= reversed(precedence) {
1154                                            entry.1 = precedence;
1155                                            entry.2 = *value;
1156                                        }
1157                                    }
1158                                    // Only the new value is important; it wins.
1159                                    (true, false) => {
1160                                        entry.1 = precedence;
1161                                        entry.2 = *value;
1162                                    }
1163                                    // The old value is important and the new one is not; keep
1164                                    // the old value.
1165                                    (false, true) => {}
1166                                }
1167                            } else {
1168                                element_styles.push((prop_name, precedence, *value));
1169                            }
1170                        }
1171                    }
1172                }
1173                if let Some(state) = selector_cleanup_state.as_mut() {
1174                    if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
1175                        state.record_usage(SelectorUsage {
1176                            selector,
1177                            declarations: (start, end),
1178                            rule_id,
1179                            chunk_index,
1180                            matched: matched_any,
1181                        });
1182                    }
1183                }
1184                // Ignore not parsable selectors. E.g. there is no parser for @media queries
1185                // Which means that they will fall into this category and will be ignored
1186            }
1187        }
1188        let mut keep_style_tags = self.options.keep_style_tags;
1189        let mut at_rules = None;
1190        if self.options.keep_at_rules || selector_cleanup_state.is_some() {
1191            let kept = KeptCss::new(
1192                &raw_styles,
1193                &layers,
1194                &spans,
1195                &rule_list,
1196                selector_cleanup_state.as_ref(),
1197                self.options.keep_style_tags && !track_selector_cleanup,
1198            );
1199            if self.options.keep_at_rules {
1200                // `<style>` `@`-rules stay in their blocks when the blocks are kept or rewritten;
1201                // only the rest (remote stylesheets, `extra_css`) goes to the new `<style>`, with
1202                // the layer order declared before it.
1203                let at_rules_from = if self.options.keep_style_tags || track_selector_cleanup {
1204                    styles_end
1205                } else {
1206                    0
1207                };
1208                let has_at_rules = at_rules_from == 0
1209                    || spans.iter().any(|span| {
1210                        !matches!(span, Span::LayerBlock { .. }) && span.start() >= at_rules_from
1211                    });
1212                if has_at_rules {
1213                    let rules = kept.render(
1214                        &spans,
1215                        &(0..raw_styles.len()),
1216                        at_rules_from,
1217                        Vec::new(),
1218                        " ",
1219                    );
1220                    at_rules = (!rules.is_empty()).then(|| format!("{} ", rules.join(" ")));
1221                }
1222            }
1223            if let Some(state) = selector_cleanup_state.as_ref() {
1224                keep_style_tags |= apply_selector_cleanup(
1225                    state,
1226                    &mut document,
1227                    self.options.keep_style_tags,
1228                    &declarations,
1229                    &kept,
1230                    &spans,
1231                );
1232            }
1233        }
1234        document.serialize(
1235            target,
1236            styles,
1237            keep_style_tags,
1238            self.options.keep_link_tags,
1239            self.options.minify_css,
1240            at_rules.as_ref(),
1241            mode,
1242            self.options.apply_width_attributes,
1243            self.options.apply_height_attributes,
1244            // Inlined values are slices of `raw_styles`; attributes are checked once decoded.
1245            if !self.options.resolve_css_variables {
1246                CssVariables::Keep
1247            } else if contains_var(raw_styles.as_bytes()) || document.has_var_in_style_attributes()
1248            {
1249                CssVariables::Resolve
1250            } else {
1251                CssVariables::Drop
1252            },
1253        )?;
1254        Ok(())
1255    }
1256
1257    fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
1258        // Valid absolute URL
1259        if Url::parse(href).is_ok() {
1260            return Cow::Borrowed(href);
1261        }
1262        if let Some(base_url) = &self.options.base_url {
1263            // Use the same scheme as the base URL
1264            if href.starts_with("//") {
1265                return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
1266            }
1267            // Not a URL, then it is a relative URL
1268            if let Ok(new_url) = base_url.join(href) {
1269                return Cow::Owned(new_url.into());
1270            }
1271        }
1272        // If it is not a valid URL and there is no base URL specified, we assume a local path
1273        Cow::Borrowed(href)
1274    }
1275}
1276
1277impl Default for CSSInliner<'_> {
1278    #[inline]
1279    fn default() -> Self {
1280        CSSInliner::new(InlineOptions::default())
1281    }
1282}
1283
1284/// Shortcut for inlining CSS with default parameters.
1285///
1286/// # Errors
1287///
1288/// Inlining might fail for the following reasons:
1289///   - Missing stylesheet file;
1290///   - Remote stylesheet is not available;
1291///   - IO errors;
1292///   - Internal CSS selector parsing error;
1293///
1294/// # Panics
1295///
1296/// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
1297/// using the same inliner panicked while resolving external stylesheets.
1298#[inline]
1299pub fn inline(html: &str) -> Result<String> {
1300    CSSInliner::default().inline(html)
1301}
1302
1303/// Shortcut for inlining CSS with default parameters and writing the output to a generic writer.
1304///
1305/// # Errors
1306///
1307/// Inlining might fail for the following reasons:
1308///   - Missing stylesheet file;
1309///   - Remote stylesheet is not available;
1310///   - IO errors;
1311///   - Internal CSS selector parsing error;
1312///
1313/// # Panics
1314///
1315/// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
1316/// using the same inliner panicked while resolving external stylesheets.
1317#[inline]
1318pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
1319    CSSInliner::default().inline_to(html, target)
1320}
1321
1322/// Shortcut for inlining CSS into an HTML fragment with default parameters.
1323///
1324/// See [`CSSInliner::inline_fragment`] for details on fragment handling.
1325///
1326/// # Errors
1327///
1328/// Inlining might fail for the following reasons:
1329///   - Missing stylesheet file;
1330///   - Remote stylesheet is not available;
1331///   - IO errors;
1332///   - Internal CSS selector parsing error;
1333///
1334/// # Panics
1335///
1336/// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
1337/// using the same inliner panicked while resolving external stylesheets.
1338#[inline]
1339pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
1340    CSSInliner::default().inline_fragment(html, css)
1341}
1342
1343/// Shortcut for inlining CSS into an HTML fragment with default parameters and writing the output to a generic writer.
1344///
1345/// # Errors
1346///
1347/// Inlining might fail for the following reasons:
1348///   - Missing stylesheet file;
1349///   - Remote stylesheet is not available;
1350///   - IO errors;
1351///   - Internal CSS selector parsing error;
1352///
1353/// # Panics
1354///
1355/// This function may panic if external stylesheet cache lock is poisoned, i.e. another thread
1356/// using the same inliner panicked while resolving external stylesheets.
1357#[inline]
1358pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
1359    CSSInliner::default().inline_fragment_to(html, css, target)
1360}
1361
1362/// Shortcut for non-blocking inlining with default parameters. See [`CSSInliner::inline_async`].
1363///
1364/// # Errors
1365///
1366/// Same as [`CSSInliner::inline`].
1367#[cfg(feature = "async")]
1368pub async fn inline_async(html: &str) -> Result<String> {
1369    CSSInliner::default().inline_async(html).await
1370}
1371
1372/// Shortcut for non-blocking inlining with default parameters, writing the output to a generic writer.
1373///
1374/// # Errors
1375///
1376/// Same as [`CSSInliner::inline`].
1377#[cfg(feature = "async")]
1378pub async fn inline_to_async<W: Write>(html: &str, target: &mut W) -> Result<()> {
1379    CSSInliner::default().inline_to_async(html, target).await
1380}
1381
1382/// Shortcut for non-blocking inlining into an HTML fragment with default parameters.
1383///
1384/// # Errors
1385///
1386/// Same as [`CSSInliner::inline_fragment`].
1387#[cfg(feature = "async")]
1388pub async fn inline_fragment_async(html: &str, css: &str) -> Result<String> {
1389    CSSInliner::default().inline_fragment_async(html, css).await
1390}
1391
1392/// Shortcut for non-blocking inlining into an HTML fragment with default parameters, writing the output to a generic writer.
1393///
1394/// # Errors
1395///
1396/// Same as [`CSSInliner::inline_fragment`].
1397#[cfg(feature = "async")]
1398pub async fn inline_fragment_to_async<W: Write>(
1399    html: &str,
1400    css: &str,
1401    target: &mut W,
1402) -> Result<()> {
1403    CSSInliner::default()
1404        .inline_fragment_to_async(html, css, target)
1405        .await
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410    use crate::{CSSInliner, InlineOptions};
1411
1412    #[test]
1413    fn test_inliner_sync_send() {
1414        fn assert_send<T: Send + Sync>() {}
1415        assert_send::<CSSInliner<'_>>();
1416        assert_send::<InlineOptions<'_>>();
1417    }
1418}