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