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#[cfg(feature = "stylesheet-cache")]
52pub type StylesheetCache<S = DefaultHasher> = LruCache<String, String, S>;
53
54#[allow(clippy::struct_excessive_bools)]
56pub struct InlineOptions<'a> {
57 pub inline_style_tags: bool,
62 pub keep_style_tags: bool,
64 pub keep_link_tags: bool,
66 pub keep_at_rules: bool,
68 pub minify_css: bool,
70 pub base_url: Option<Url>,
72 pub load_remote_stylesheets: bool,
74 #[cfg(feature = "stylesheet-cache")]
76 pub cache: Option<std::sync::Mutex<StylesheetCache>>,
77 pub extra_css: Option<Cow<'a, str>>,
82 pub preallocate_node_capacity: usize,
85 pub resolver: Arc<dyn StylesheetResolver>,
87 pub remove_inlined_selectors: bool,
89 pub apply_width_attributes: bool,
94 pub apply_height_attributes: bool,
99 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 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: 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
169struct LayerBlock<'i> {
171 range: Range<usize>,
172 prelude: &'i str,
173 layer: LayerId,
174 created: bool,
175}
176
177struct KeptCss<'a, 'i> {
180 source: &'i str,
181 layers: &'a Layers<'i>,
182 kept: Vec<bool>,
184}
185
186impl<'a, 'i> KeptCss<'a, 'i> {
187 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 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 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 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 #[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 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
333fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
335 chunks
336 .iter()
337 .position(|chunk| chunk.range.contains(&offset))
338}
339
340#[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 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
401fn 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 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 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
520fn 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 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 #[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 #[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 #[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 #[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 #[must_use]
568 pub fn minify_css(mut self, minify_css: bool) -> Self {
569 self.minify_css = minify_css;
570 self
571 }
572
573 #[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 #[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 #[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 #[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 #[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 #[must_use]
615 pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
616 self.resolver = resolver;
617 self
618 }
619
620 #[must_use]
622 pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
623 self.remove_inlined_selectors = enabled;
624 self
625 }
626
627 #[must_use]
632 pub fn apply_width_attributes(mut self, apply: bool) -> Self {
633 self.apply_width_attributes = apply;
634 self
635 }
636
637 #[must_use]
642 pub fn apply_height_attributes(mut self, apply: bool) -> Self {
643 self.apply_height_attributes = apply;
644 self
645 }
646
647 #[must_use]
650 pub fn resolve_css_variables(mut self, resolve: bool) -> Self {
651 self.resolve_css_variables = resolve;
652 self
653 }
654
655 #[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
686pub type Result<T> = std::result::Result<T, InlineError>;
688
689#[derive(Debug)]
691pub struct CSSInliner<'a> {
692 options: InlineOptions<'a>,
693}
694
695const GROWTH_COEFFICIENT: f64 = 1.5;
696const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
698
699fn allocate_output_buffer(html: &str) -> Vec<u8> {
700 #[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
713fn 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 #[must_use]
722 #[inline]
723 pub const fn new(options: InlineOptions<'a>) -> Self {
724 CSSInliner { options }
725 }
726
727 #[must_use]
746 #[inline]
747 pub fn options() -> InlineOptions<'a> {
748 InlineOptions::default()
749 }
750
751 #[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]
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 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 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 #[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 #[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 Ok(stylesheets[url].clone())
882 })
883 }
884
885 #[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 #[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 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 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 #[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 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 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 #[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 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 rule_list.clear();
1087 }
1088 let ranks = layers.ranks();
1089 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 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 let mut styles: Vec<Option<html::ElementStyleMap<'_>>> = vec![None; document.nodes.len()];
1101 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 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 for (name, value) in &declarations[start..end] {
1135 let prop_name = name.as_ref();
1136 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 (false, false) => {
1146 if entry.1 <= precedence {
1147 entry.1 = precedence;
1148 entry.2 = *value;
1149 }
1150 }
1151 (true, true) => {
1153 if reversed(entry.1) <= reversed(precedence) {
1154 entry.1 = precedence;
1155 entry.2 = *value;
1156 }
1157 }
1158 (true, false) => {
1160 entry.1 = precedence;
1161 entry.2 = *value;
1162 }
1163 (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 }
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 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 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 if Url::parse(href).is_ok() {
1260 return Cow::Borrowed(href);
1261 }
1262 if let Some(base_url) = &self.options.base_url {
1263 if href.starts_with("//") {
1265 return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
1266 }
1267 if let Ok(new_url) = base_url.join(href) {
1269 return Cow::Owned(new_url.into());
1270 }
1271 }
1272 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#[inline]
1299pub fn inline(html: &str) -> Result<String> {
1300 CSSInliner::default().inline(html)
1301}
1302
1303#[inline]
1318pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
1319 CSSInliner::default().inline_to(html, target)
1320}
1321
1322#[inline]
1339pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
1340 CSSInliner::default().inline_fragment(html, css)
1341}
1342
1343#[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#[cfg(feature = "async")]
1368pub async fn inline_async(html: &str) -> Result<String> {
1369 CSSInliner::default().inline_async(html).await
1370}
1371
1372#[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#[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#[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}