-
-
Notifications
You must be signed in to change notification settings - Fork 38
!important stripped from inlined styles when element has existing style attribute #682
Description
When an element already has an inline style attribute and CSS rules with !important are inlined onto it, the !important flags are dropped from the output. This only happens when minify_css is true (the default). Elements without pre-existing style attributes correctly preserve !important.
This is caused by two related bugs in merge_styles in css-inline/src/html/serializer.rs.
Bug 1: Property lookup uses wrong separator when minify_css is true
The find closure that searches for existing properties in the declarations buffer always compares against STYLE_SEPARATOR (": " with space):
.find(|style| {
style.starts_with(property.as_bytes())
&& style.get(property.len()..=property.len().saturating_add(1))
== Some(STYLE_SEPARATOR) // always ": " (with space)
})But when minify_css is true, buffer entries are written using STYLE_SEPARATOR_MIN (":"). The lookup never matches, so:
- Properties are duplicated instead of merged (CSS values prepended, existing inline values appended)
- The
!importantcascade logic in all four match arms is never reached for the correct case - Existing inline values (without
!important) appear last, winning by CSS last-value-wins
Bug 2: !important stripped from output during merge (regardless of minify_css value)
When a CSS rule's value contains !important, merge_styles strips it via strip_suffix("!important") for cascade resolution but never writes it back. Both the (Some(value), Some(buffer)) and (Some(value), None) arms write the stripped value without restoring !important:
// Case 1: !important rule overrides existing
(Some(value), Some(buffer)) => {
if !buffer.ends_with(b"!important") {
buffer.truncate(property.len().saturating_add(STYLE_SEPARATOR.len()));
write_declaration_value(buffer, value)?; // value has !important stripped
// !important is lost
}
}
// Case 2: !important rule, no existing property
(Some(value), None) => {
push_or_update!(..., value, ...); // value has !important stripped
// !important is lost
}Reproduction
let html = r#"<html>
<head>
<style>
.btn a {
color: #fff;
display: inline-block !important;
font-weight: 700 !important;
text-decoration: none !important;
}
</style>
</head>
<body>
<table><tr><td class="btn">
<a href="#" style="color: #fff; display: inline-block; font-weight: 700; text-decoration: none;">
Click
</a>
</td></tr></table>
</body>
</html>"#;
let inliner = CSSInliner::options().build();
let result = inliner.inline(html).unwrap();
println!("{}", result);Current output (v0.20.0, minify_css: true):
<a href="#" style="color:#fff;display:inline-block;font-weight:700;text-decoration:none">All !important flags are dropped.
Expected output:
<a href="#" style="color:#fff;display:inline-block !important;font-weight:700 !important;text-decoration:none !important">Why this matters
This is critical for HTML email rendering. Email clients like Gmail add their own stylesheets (e.g., blue-link overrides via u + #body a { color: inherit !important }). Without !important on inlined button styles, these client-injected rules override button colors and break layouts.
Suggested fix
In merge_styles:
- Use the minification-aware separator for the property lookup:
let sep = if minify_css { STYLE_SEPARATOR_MIN } else { STYLE_SEPARATOR };
// ... use `sep` in both the find closure and the truncate call- Restore
!importantafter writing the stripped value:
// Case 1: after write_declaration_value
buffer.extend_from_slice(b" !important");
// Case 2: after push_or_update!
if let Some(buf) = declarations_buffer.get_mut(parsed_declarations_count.saturating_sub(1)) {
buf.extend_from_slice(b" !important");
}