#![doc = include_str!("../README.md")]
#![warn(
clippy::pedantic,
clippy::doc_markdown,
clippy::redundant_closure,
clippy::explicit_iter_loop,
clippy::match_same_arms,
clippy::needless_borrow,
clippy::print_stdout,
clippy::arithmetic_side_effects,
clippy::cast_possible_truncation,
clippy::unwrap_used,
clippy::map_unwrap_or,
clippy::trivially_copy_pass_by_ref,
clippy::needless_pass_by_value,
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unreachable_pub,
unused_extern_crates,
unused_import_braces,
unused_qualifications,
variant_size_differences,
rust_2018_idioms,
rust_2018_compatibility,
rust_2021_compatibility
)]
#![allow(clippy::module_name_repetitions)]
pub mod error;
mod html;
mod parser;
mod resolver;
pub use error::InlineError;
#[cfg(feature = "stylesheet-cache")]
use lru::{DefaultHasher, LruCache};
use selectors::context::SelectorCaches;
use smallvec::SmallVec;
use std::{borrow::Cow, fmt::Formatter, io::Write, iter::Peekable, ops::Range, sync::Arc};
#[cfg(feature = "async")]
pub use async_trait::async_trait;
use html::{contains_var, CssVariables, Document, InliningMode, NodeData, NodeId, Precedence};
use parser::{LayerId, Layers, Rule, Span};
pub use resolver::{DefaultStylesheetResolver, StylesheetResolver};
use rustc_hash::FxHashMap;
pub use url::{ParseError, Url};
#[cfg(feature = "stylesheet-cache")]
pub type StylesheetCache<S = DefaultHasher> = LruCache<String, String, S>;
#[allow(clippy::struct_excessive_bools)]
pub struct InlineOptions<'a> {
pub inline_style_tags: bool,
pub keep_style_tags: bool,
pub keep_link_tags: bool,
pub keep_at_rules: bool,
pub minify_css: bool,
pub base_url: Option<Url>,
pub load_remote_stylesheets: bool,
#[cfg(feature = "stylesheet-cache")]
pub cache: Option<std::sync::Mutex<StylesheetCache>>,
pub extra_css: Option<Cow<'a, str>>,
pub preallocate_node_capacity: usize,
pub resolver: Arc<dyn StylesheetResolver>,
pub remove_inlined_selectors: bool,
pub apply_width_attributes: bool,
pub apply_height_attributes: bool,
pub resolve_css_variables: bool,
}
impl std::fmt::Debug for InlineOptions<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("InlineOptions");
debug
.field("inline_style_tags", &self.inline_style_tags)
.field("keep_style_tags", &self.keep_style_tags)
.field("keep_link_tags", &self.keep_link_tags)
.field("base_url", &self.base_url)
.field("load_remote_stylesheets", &self.load_remote_stylesheets);
#[cfg(feature = "stylesheet-cache")]
{
debug.field("cache", &self.cache);
}
debug
.field("extra_css", &self.extra_css)
.field("preallocate_node_capacity", &self.preallocate_node_capacity)
.field("remove_inlined_selectors", &self.remove_inlined_selectors)
.field("apply_width_attributes", &self.apply_width_attributes)
.field("apply_height_attributes", &self.apply_height_attributes)
.field("resolve_css_variables", &self.resolve_css_variables)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
struct CssChunk {
range: Range<usize>,
style_node: Option<NodeId>,
}
type SelectorList<'i> = SmallVec<[&'i str; 2]>;
#[derive(Debug)]
struct SelectorUsage<'i> {
selector: &'i str,
declarations: (usize, usize),
rule_id: usize,
chunk_index: usize,
matched: bool,
}
#[derive(Debug, Default)]
struct RuleRemainder<'i> {
selectors: SelectorList<'i>,
declarations: (usize, usize),
offset: usize,
}
#[derive(Debug, Default)]
struct SelectorCleanupState<'i> {
chunks: Vec<CssChunk>,
usages: Vec<SelectorUsage<'i>>,
}
impl<'i> SelectorCleanupState<'i> {
fn record_usage(&mut self, usage: SelectorUsage<'i>) {
self.usages.push(usage);
}
}
struct LayerBlock<'i> {
range: Range<usize>,
prelude: &'i str,
layer: LayerId,
created: bool,
}
struct KeptCss<'a, 'i> {
source: &'i str,
layers: &'a Layers<'i>,
kept: Vec<bool>,
}
impl<'a, 'i> KeptCss<'a, 'i> {
fn new(
source: &'i str,
layers: &'a Layers<'i>,
spans: &[Span<'i>],
rules: &[Rule<'i>],
cleanup: Option<&SelectorCleanupState<'i>>,
verbatim_styles: bool,
) -> Self {
let mut kept = vec![false; layers.len()];
let mut mark = |mut id: LayerId| {
while id != 0 && !kept[id] {
kept[id] = true;
id = layers.parent(id);
}
};
for span in spans {
if let Span::AtRule { layer, .. } = span {
mark(*layer);
}
}
if let Some(state) = cleanup {
for usage in state.usages.iter().filter(|usage| !usage.matched) {
mark(rules[usage.rule_id].layer);
}
}
if verbatim_styles {
for rule in rules {
mark(rule.layer);
}
}
KeptCss {
source,
layers,
kept,
}
}
fn is_kept(&self, mut layer: LayerId) -> bool {
while layer != 0 {
if self.kept[layer] {
return true;
}
layer = self.layers.parent(layer);
}
false
}
fn render(
&self,
spans: &[Span<'i>],
range: &Range<usize>,
at_rules_from: usize,
mut items: Vec<(usize, String)>,
sep: &str,
) -> Vec<String> {
let mut blocks = Vec::new();
for span in spans.iter().filter(|span| range.contains(&span.start())) {
match span {
Span::AtRule { range: at_rule, .. } if at_rule.start < at_rules_from => {}
Span::AtRule { range: at_rule, .. } => items.push((
at_rule.start,
self.source[at_rule.start..at_rule.end.min(range.end)]
.trim()
.to_string(),
)),
Span::LayerStatement {
start,
prelude,
layers,
} => {
if layers.iter().any(|&layer| self.is_kept(layer)) {
items.push((*start, format!("@layer {};", prelude.trim())));
}
}
Span::LayerBlock {
range: block,
prelude,
layer,
created,
} => blocks.push(LayerBlock {
range: block.start..block.end.min(range.end),
prelude,
layer: *layer,
created: *created,
}),
}
}
items.retain(|(_, text)| !text.is_empty());
items.sort_by_key(|(offset, _)| *offset);
blocks.sort_by_key(|block| block.range.start);
self.render_tree(&mut items.into_iter().peekable(), &blocks, range.end, sep)
}
#[allow(clippy::arithmetic_side_effects)]
fn render_tree(
&self,
items: &mut Peekable<std::vec::IntoIter<(usize, String)>>,
blocks: &[LayerBlock<'i>],
end: usize,
sep: &str,
) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while let Some(block) = blocks.get(i) {
while let Some((_, text)) = items.next_if(|(offset, _)| *offset < block.range.start) {
out.push(text);
}
let mut j = i + 1;
while blocks
.get(j)
.is_some_and(|child| child.range.start < block.range.end)
{
j += 1;
}
let inner = self.render_tree(items, &blocks[i + 1..j], block.range.end, sep);
let prelude = block.prelude.trim();
if inner.is_empty() {
if block.created && !prelude.is_empty() && self.is_kept(block.layer) {
out.push(format!("@layer {prelude};"));
}
} else {
let inner = inner.join(sep);
if prelude.is_empty() {
out.push(format!("@layer {{{sep}{inner}{sep}}}"));
} else {
out.push(format!("@layer {prelude} {{{sep}{inner}{sep}}}"));
}
}
i = j;
}
while let Some((_, text)) = items.next_if(|(offset, _)| *offset < end) {
out.push(text);
}
out
}
}
fn find_chunk_index(chunks: &[CssChunk], offset: usize) -> Option<usize> {
chunks
.iter()
.position(|chunk| chunk.range.contains(&offset))
}
#[allow(clippy::arithmetic_side_effects)]
fn compute_rule_chunk_indices(
rules: &[Rule<'_>],
source: &str,
chunks: &[CssChunk],
) -> Vec<Option<usize>> {
let source_start = source.as_ptr() as usize;
let source_end = source_start.saturating_add(source.len());
rules
.iter()
.map(|rule| {
let sel_start = rule.selectors.as_ptr() as usize;
if sel_start >= source_start && sel_start < source_end {
let offset = sel_start.wrapping_sub(source_start);
find_chunk_index(chunks, offset)
} else {
None
}
})
.collect()
}
struct CssBuffer {
raw: String,
chunks: Option<Vec<CssChunk>>,
}
impl CssBuffer {
fn new(track_chunks: bool) -> Self {
CssBuffer {
raw: String::new(),
chunks: track_chunks.then(Vec::new),
}
}
fn push(&mut self, style_node: Option<NodeId>, content: &str, append_newline: bool) {
if content.is_empty() {
return;
}
let start = self.raw.len();
self.raw.push_str(content);
if append_newline {
self.raw.push('\n');
}
if let Some(chunks) = &mut self.chunks {
let end = self.raw.len();
chunks.push(CssChunk {
range: start..end,
style_node,
});
}
}
fn into_parts(self) -> (String, Option<Vec<CssChunk>>) {
(self.raw, self.chunks)
}
}
fn apply_selector_cleanup<'i>(
state: &SelectorCleanupState<'i>,
document: &mut Document,
requested_keep_style_tags: bool,
declarations: &[parser::Declaration<'i>],
kept: &KeptCss<'_, 'i>,
spans: &[Span<'i>],
) -> bool {
let source = kept.source;
let source_start = source.as_ptr() as usize;
let mut chunk_remainders: Vec<Vec<RuleRemainder<'i>>> =
(0..state.chunks.len()).map(|_| Vec::new()).collect();
let mut remainder_lookup: FxHashMap<(usize, usize), usize> = FxHashMap::default();
for usage in &state.usages {
if usage.matched {
continue;
}
let trimmed = usage.selector.trim();
if trimmed.is_empty() {
continue;
}
let key = (usage.chunk_index, usage.rule_id);
let entry_index = remainder_lookup.entry(key).or_insert_with(|| {
let idx = chunk_remainders[usage.chunk_index].len();
chunk_remainders[usage.chunk_index].push(RuleRemainder {
selectors: SelectorList::new(),
declarations: usage.declarations,
offset: (trimmed.as_ptr() as usize).wrapping_sub(source_start),
});
idx
});
chunk_remainders[usage.chunk_index][*entry_index]
.selectors
.push(trimmed);
}
let mut keeps_content = false;
for (idx, chunk) in state.chunks.iter().enumerate() {
let Some(node_id) = chunk.style_node else {
continue;
};
let mut items: Vec<(usize, String)> = Vec::with_capacity(chunk_remainders[idx].len());
for remainder in &chunk_remainders[idx] {
let mut text = String::new();
append_rule(&mut text, remainder, declarations);
items.push((remainder.offset, text));
}
let rules = kept.render(spans, &chunk.range, 0, items, "\n");
if rules.is_empty() {
handle_empty_remainder(document, chunk, requested_keep_style_tags);
continue;
}
keeps_content = true;
overwrite_style_node(document, node_id, &rules);
}
keeps_content
}
fn handle_empty_remainder(
document: &mut Document,
chunk: &CssChunk,
requested_keep_style_tags: bool,
) {
if let Some(node_id) = chunk.style_node {
if requested_keep_style_tags {
overwrite_style_node(document, node_id, &[]);
} else {
document.detach_node(node_id);
}
}
}
fn append_rule<'i>(
buffer: &mut String,
remainder: &RuleRemainder<'i>,
declarations: &[parser::Declaration<'i>],
) {
let (start, end) = remainder.declarations;
if start >= end || end > declarations.len() {
return;
}
let declarations = &declarations[start..end];
let size = remainder
.selectors
.iter()
.map(|selector| selector.len().saturating_add(2))
.chain(
declarations
.iter()
.map(|(name, value)| name.len().saturating_add(value.len()).saturating_add(4)),
)
.fold(2, usize::saturating_add);
buffer.reserve(size);
let mut selectors_iter = remainder.selectors.iter().peekable();
while let Some(selector) = selectors_iter.next() {
buffer.push_str(selector);
if selectors_iter.peek().is_some() {
buffer.push_str(", ");
}
}
buffer.push_str(" {");
for (name, value) in declarations {
buffer.push(' ');
buffer.push_str(name);
buffer.push(':');
buffer.push(' ');
let value_trimmed = value.trim();
buffer.push_str(value_trimmed);
if !value_trimmed.ends_with(';') {
buffer.push(';');
}
}
buffer.push_str(" }");
}
fn overwrite_style_node(document: &mut Document, node_id: NodeId, rules: &[String]) {
if let Some(text_node_id) = document[node_id].first_child {
if let NodeData::Text { text } = &mut document[text_node_id].data {
text.clear();
for (idx, rule) in rules.iter().enumerate() {
debug_assert!(!rule.is_empty() && rule.trim().len() == rule.len());
if idx != 0 {
text.push_char('\n');
}
text.push_slice(rule);
}
}
}
}
impl<'a> InlineOptions<'a> {
#[must_use]
pub fn inline_style_tags(mut self, inline_style_tags: bool) -> Self {
self.inline_style_tags = inline_style_tags;
self
}
#[must_use]
pub fn keep_style_tags(mut self, keep_style_tags: bool) -> Self {
self.keep_style_tags = keep_style_tags;
self
}
#[must_use]
pub fn keep_link_tags(mut self, keep_link_tags: bool) -> Self {
self.keep_link_tags = keep_link_tags;
self
}
#[must_use]
pub fn keep_at_rules(mut self, keep_at_rules: bool) -> Self {
self.keep_at_rules = keep_at_rules;
self
}
#[must_use]
pub fn minify_css(mut self, minify_css: bool) -> Self {
self.minify_css = minify_css;
self
}
#[must_use]
pub fn base_url(mut self, base_url: Option<Url>) -> Self {
self.base_url = base_url;
self
}
#[must_use]
pub fn load_remote_stylesheets(mut self, load_remote_stylesheets: bool) -> Self {
self.load_remote_stylesheets = load_remote_stylesheets;
self
}
#[must_use]
#[cfg(feature = "stylesheet-cache")]
pub fn cache(mut self, cache: impl Into<Option<StylesheetCache>>) -> Self {
if let Some(cache) = cache.into() {
self.cache = Some(std::sync::Mutex::new(cache));
} else {
self.cache = None;
}
self
}
#[must_use]
pub fn extra_css(mut self, extra_css: Option<Cow<'a, str>>) -> Self {
self.extra_css = extra_css;
self
}
#[must_use]
pub fn preallocate_node_capacity(mut self, preallocate_node_capacity: usize) -> Self {
self.preallocate_node_capacity = preallocate_node_capacity;
self
}
#[must_use]
pub fn resolver(mut self, resolver: Arc<dyn StylesheetResolver>) -> Self {
self.resolver = resolver;
self
}
#[must_use]
pub fn remove_inlined_selectors(mut self, enabled: bool) -> Self {
self.remove_inlined_selectors = enabled;
self
}
#[must_use]
pub fn apply_width_attributes(mut self, apply: bool) -> Self {
self.apply_width_attributes = apply;
self
}
#[must_use]
pub fn apply_height_attributes(mut self, apply: bool) -> Self {
self.apply_height_attributes = apply;
self
}
#[must_use]
pub fn resolve_css_variables(mut self, resolve: bool) -> Self {
self.resolve_css_variables = resolve;
self
}
#[must_use]
pub const fn build(self) -> CSSInliner<'a> {
CSSInliner::new(self)
}
}
impl Default for InlineOptions<'_> {
#[inline]
fn default() -> Self {
InlineOptions {
inline_style_tags: true,
keep_style_tags: false,
keep_link_tags: false,
keep_at_rules: false,
minify_css: false,
base_url: None,
load_remote_stylesheets: true,
#[cfg(feature = "stylesheet-cache")]
cache: None,
extra_css: None,
preallocate_node_capacity: 32,
resolver: Arc::new(DefaultStylesheetResolver),
remove_inlined_selectors: false,
apply_width_attributes: false,
apply_height_attributes: false,
resolve_css_variables: false,
}
}
}
pub type Result<T> = std::result::Result<T, InlineError>;
#[derive(Debug)]
pub struct CSSInliner<'a> {
options: InlineOptions<'a>,
}
const GROWTH_COEFFICIENT: f64 = 1.5;
const DECLARATION_SIZE_COEFFICIENT: f64 = 30.0;
fn allocate_output_buffer(html: &str) -> Vec<u8> {
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
Vec::with_capacity(
(html.len() as f64 * GROWTH_COEFFICIENT)
.min(usize::MAX as f64)
.round() as usize,
)
}
fn into_string(out: Vec<u8>) -> String {
String::from_utf8(out)
.unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned())
}
impl<'a> CSSInliner<'a> {
#[must_use]
#[inline]
pub const fn new(options: InlineOptions<'a>) -> Self {
CSSInliner { options }
}
#[must_use]
#[inline]
pub fn options() -> InlineOptions<'a> {
InlineOptions::default()
}
#[inline]
pub fn inline(&self, html: &str) -> Result<String> {
let mut out = allocate_output_buffer(html);
self.inline_to(html, &mut out)?;
Ok(into_string(out))
}
#[inline]
pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
self.options.resolver.retrieve(url)
})
}
pub fn inline_fragment(&self, html: &str, css: &str) -> Result<String> {
let mut out = allocate_output_buffer(html);
self.inline_fragment_to(html, css, &mut out)?;
Ok(into_string(out))
}
pub fn inline_fragment_to<W: Write>(
&self,
html: &str,
css: &str,
target: &mut W,
) -> Result<()> {
self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
self.options.resolver.retrieve(url)
})
}
#[cfg(feature = "async")]
pub async fn inline_async(&self, html: &str) -> Result<String> {
let mut out = allocate_output_buffer(html);
self.inline_to_async(html, &mut out).await?;
Ok(into_string(out))
}
#[cfg(feature = "async")]
pub async fn inline_to_async<W: Write>(&self, html: &str, target: &mut W) -> Result<()> {
let stylesheets = self.fetch_stylesheets(html, InliningMode::Document).await?;
self.inline_to_impl(html, None, target, InliningMode::Document, &|url| {
Ok(stylesheets[url].clone())
})
}
#[cfg(feature = "async")]
pub async fn inline_fragment_async(&self, html: &str, css: &str) -> Result<String> {
let mut out = allocate_output_buffer(html);
self.inline_fragment_to_async(html, css, &mut out).await?;
Ok(into_string(out))
}
#[cfg(feature = "async")]
pub async fn inline_fragment_to_async<W: Write>(
&self,
html: &str,
css: &str,
target: &mut W,
) -> Result<()> {
let stylesheets = self.fetch_stylesheets(html, InliningMode::Fragment).await?;
self.inline_to_impl(html, Some(css), target, InliningMode::Fragment, &|url| {
Ok(stylesheets[url].clone())
})
}
#[cfg(feature = "async")]
async fn fetch_stylesheets(
&self,
html: &str,
mode: InliningMode,
) -> Result<FxHashMap<String, String>> {
let mut stylesheets = FxHashMap::default();
if !self.options.load_remote_stylesheets {
return Ok(stylesheets);
}
let mut missing = Vec::new();
{
let document =
Document::parse_with_options(html, self.options.preallocate_node_capacity, mode);
let mut urls = document
.stylesheets()
.map(|href| self.get_full_url(href).into_owned())
.collect::<Vec<_>>();
urls.sort_unstable();
urls.dedup();
for url in urls {
#[cfg(feature = "stylesheet-cache")]
if let Some(lock) = self.options.cache.as_ref() {
let mut cache = lock.lock().expect("Cache lock is poisoned");
if let Some(css) = cache.get(&url) {
stylesheets.insert(url, css.clone());
continue;
}
}
missing.push(url);
}
}
let fetched = futures_util::future::try_join_all(
missing
.iter()
.map(|url| self.options.resolver.retrieve_async(url)),
)
.await?;
stylesheets.extend(missing.into_iter().zip(fetched));
Ok(stylesheets)
}
#[allow(clippy::too_many_lines)]
fn inline_to_impl<W: Write>(
&self,
html: &str,
css: Option<&str>,
target: &mut W,
mode: InliningMode,
retrieve: &dyn Fn(&str) -> Result<String>,
) -> Result<()> {
let mut document =
Document::parse_with_options(html, self.options.preallocate_node_capacity, mode);
let track_selector_cleanup =
self.options.remove_inlined_selectors && self.options.inline_style_tags;
let mut size_estimate: usize = if self.options.inline_style_tags {
document
.styles()
.map(|(_, s)| {
s.len().saturating_add(1)
})
.sum()
} else {
0
};
if let Some(extra_css) = &self.options.extra_css {
size_estimate = size_estimate.saturating_add(extra_css.len());
}
if let Some(css) = css {
size_estimate = size_estimate.saturating_add(css.len());
}
let mut css_buffer = CssBuffer::new(track_selector_cleanup);
css_buffer.raw.reserve(size_estimate);
if self.options.inline_style_tags || self.options.keep_at_rules {
for (node_id, style) in document.styles() {
let style_node = track_selector_cleanup.then_some(node_id);
css_buffer.push(style_node, style, true);
}
}
let styles_end = css_buffer.raw.len();
if self.options.load_remote_stylesheets {
let mut links = document.stylesheets().collect::<Vec<&str>>();
links.sort_unstable();
links.dedup();
for href in &links {
let url = self.get_full_url(href);
#[cfg(feature = "stylesheet-cache")]
if let Some(lock) = self.options.cache.as_ref() {
let mut cache = lock.lock().expect("Cache lock is poisoned");
if let Some(cached) = cache.get(url.as_ref()) {
css_buffer.push(None, cached, true);
continue;
}
}
let css = retrieve(url.as_ref())?;
css_buffer.push(None, &css, true);
#[cfg(feature = "stylesheet-cache")]
if let Some(lock) = self.options.cache.as_ref() {
let mut cache = lock.lock().expect("Cache lock is poisoned");
cache.put(url.into_owned(), css);
}
}
}
if let Some(extra_css) = &self.options.extra_css {
css_buffer.push(None, extra_css, false);
}
if let Some(css) = css {
css_buffer.push(None, css, false);
}
let (raw_styles, css_chunks) = css_buffer.into_parts();
let mut selector_cleanup_state = if track_selector_cleanup {
Some(SelectorCleanupState::default())
} else {
None
};
if let (Some(state), Some(chunks)) = (&mut selector_cleanup_state, css_chunks) {
state.chunks = chunks;
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
let mut declarations = Vec::with_capacity(
((raw_styles.len() as f64 / DECLARATION_SIZE_COEFFICIENT)
.min(usize::MAX as f64)
.round() as usize)
.max(16),
);
let mut rule_list = Vec::with_capacity(declarations.capacity() / 3);
let mut spans = (self.options.keep_at_rules || track_selector_cleanup).then(Vec::new);
let mut rule_parser = parser::RuleParser::new(
&raw_styles,
&mut declarations,
&mut rule_list,
spans.as_mut(),
);
if !raw_styles.is_empty() {
let mut parser = cssparser::Parser::new(&raw_styles);
let mut items = cssparser::StyleSheetParser::new(&mut parser, &mut rule_parser);
parser::collect_rules(&mut items);
}
let layers = rule_parser.into_layers();
let spans = spans.unwrap_or_default();
if self.options.keep_at_rules && !self.options.inline_style_tags {
rule_list.clear();
}
let ranks = layers.ranks();
let last_rank = u32::try_from(layers.len().saturating_sub(1)).unwrap_or(u32::MAX);
let reversed = |precedence: Precedence| {
Precedence(last_rank.saturating_sub(precedence.0), precedence.1)
};
let rule_chunk_indices = selector_cleanup_state
.as_ref()
.map(|state| compute_rule_chunk_indices(&rule_list, &raw_styles, &state.chunks))
.unwrap_or_default();
let mut styles: Vec<Option<html::ElementStyleMap<'_>>> = vec![None; document.nodes.len()];
let mut caches = SelectorCaches::default();
for (rule_id, rule) in rule_list.iter().enumerate() {
let (start, end) = rule.declarations;
let rank = ranks[rule.layer];
for selector in parser::split_selector_list(rule.selectors) {
let mut matched_any = false;
if !document.may_match(selector) {
if let Some(state) = selector_cleanup_state.as_mut() {
if let Some(chunk_index) =
rule_chunk_indices.get(rule_id).copied().flatten()
{
state.record_usage(SelectorUsage {
selector,
declarations: (start, end),
rule_id,
chunk_index,
matched: false,
});
}
}
continue;
}
if let Ok(matching_elements) = document.select(selector, &mut caches) {
let precedence = Precedence(rank, matching_elements.specificity());
for matching_element in matching_elements {
matched_any = true;
let element_styles = styles[matching_element.node_id.get()]
.get_or_insert_with(SmallVec::new);
for (name, value) in &declarations[start..end] {
let prop_name = name.as_ref();
if let Some(idx) =
element_styles.iter().position(|(n, _, _)| *n == prop_name)
{
let entry = &mut element_styles[idx];
let new_important = parser::strip_important(value).is_some();
let old_important = parser::strip_important(entry.2).is_some();
match (new_important, old_important) {
(false, false) => {
if entry.1 <= precedence {
entry.1 = precedence;
entry.2 = *value;
}
}
(true, true) => {
if reversed(entry.1) <= reversed(precedence) {
entry.1 = precedence;
entry.2 = *value;
}
}
(true, false) => {
entry.1 = precedence;
entry.2 = *value;
}
(false, true) => {}
}
} else {
element_styles.push((prop_name, precedence, *value));
}
}
}
}
if let Some(state) = selector_cleanup_state.as_mut() {
if let Some(chunk_index) = rule_chunk_indices.get(rule_id).copied().flatten() {
state.record_usage(SelectorUsage {
selector,
declarations: (start, end),
rule_id,
chunk_index,
matched: matched_any,
});
}
}
}
}
let mut keep_style_tags = self.options.keep_style_tags;
let mut at_rules = None;
if self.options.keep_at_rules || selector_cleanup_state.is_some() {
let kept = KeptCss::new(
&raw_styles,
&layers,
&spans,
&rule_list,
selector_cleanup_state.as_ref(),
self.options.keep_style_tags && !track_selector_cleanup,
);
if self.options.keep_at_rules {
let at_rules_from = if self.options.keep_style_tags || track_selector_cleanup {
styles_end
} else {
0
};
let has_at_rules = at_rules_from == 0
|| spans.iter().any(|span| {
!matches!(span, Span::LayerBlock { .. }) && span.start() >= at_rules_from
});
if has_at_rules {
let rules = kept.render(
&spans,
&(0..raw_styles.len()),
at_rules_from,
Vec::new(),
" ",
);
at_rules = (!rules.is_empty()).then(|| format!("{} ", rules.join(" ")));
}
}
if let Some(state) = selector_cleanup_state.as_ref() {
keep_style_tags |= apply_selector_cleanup(
state,
&mut document,
self.options.keep_style_tags,
&declarations,
&kept,
&spans,
);
}
}
document.serialize(
target,
styles,
keep_style_tags,
self.options.keep_link_tags,
self.options.minify_css,
at_rules.as_ref(),
mode,
self.options.apply_width_attributes,
self.options.apply_height_attributes,
if !self.options.resolve_css_variables {
CssVariables::Keep
} else if contains_var(raw_styles.as_bytes()) || document.has_var_in_style_attributes()
{
CssVariables::Resolve
} else {
CssVariables::Drop
},
)?;
Ok(())
}
fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
if Url::parse(href).is_ok() {
return Cow::Borrowed(href);
}
if let Some(base_url) = &self.options.base_url {
if href.starts_with("//") {
return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
}
if let Ok(new_url) = base_url.join(href) {
return Cow::Owned(new_url.into());
}
}
Cow::Borrowed(href)
}
}
impl Default for CSSInliner<'_> {
#[inline]
fn default() -> Self {
CSSInliner::new(InlineOptions::default())
}
}
#[inline]
pub fn inline(html: &str) -> Result<String> {
CSSInliner::default().inline(html)
}
#[inline]
pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<()> {
CSSInliner::default().inline_to(html, target)
}
#[inline]
pub fn inline_fragment(html: &str, css: &str) -> Result<String> {
CSSInliner::default().inline_fragment(html, css)
}
#[inline]
pub fn inline_fragment_to<W: Write>(html: &str, css: &str, target: &mut W) -> Result<()> {
CSSInliner::default().inline_fragment_to(html, css, target)
}
#[cfg(feature = "async")]
pub async fn inline_async(html: &str) -> Result<String> {
CSSInliner::default().inline_async(html).await
}
#[cfg(feature = "async")]
pub async fn inline_to_async<W: Write>(html: &str, target: &mut W) -> Result<()> {
CSSInliner::default().inline_to_async(html, target).await
}
#[cfg(feature = "async")]
pub async fn inline_fragment_async(html: &str, css: &str) -> Result<String> {
CSSInliner::default().inline_fragment_async(html, css).await
}
#[cfg(feature = "async")]
pub async fn inline_fragment_to_async<W: Write>(
html: &str,
css: &str,
target: &mut W,
) -> Result<()> {
CSSInliner::default()
.inline_fragment_to_async(html, css, target)
.await
}
#[cfg(test)]
mod tests {
use crate::{CSSInliner, InlineOptions};
#[test]
fn test_inliner_sync_send() {
fn assert_send<T: Send + Sync>() {}
assert_send::<CSSInliner<'_>>();
assert_send::<InlineOptions<'_>>();
}
}