ferrum_email_render/css_inliner.rs
1//! CSS inlining for email-safe rendering.
2//!
3//! Since Gmail and many email clients strip `<style>` blocks from `<head>`,
4//! all CSS must be inlined as `style=""` attributes on each element.
5//! The Ferrum Style system already produces inline CSS via `Style::to_css()`,
6//! so this module handles merging and normalization.
7
8use ferrum_email_core::{Element, Node, Style};
9
10/// Walk a Node tree and ensure all styles are properly inlined.
11///
12/// This is largely a pass-through since Ferrum's component system already
13/// produces inline styles via the `Style` struct. This function handles
14/// edge cases like merging inherited styles and normalizing the output.
15pub fn inline_styles(node: &Node) -> Node {
16 inline_node(node, &Style::default())
17}
18
19fn inline_node(node: &Node, _inherited: &Style) -> Node {
20 match node {
21 Node::Element(element) => {
22 let mut new_element = Element::new(element.tag.clone());
23
24 // Check if the element already has a manually-set style attribute.
25 // If so, we don't override it — the manually-set attribute takes precedence.
26 let has_manual_style = element.attrs.iter().any(|a| a.name == "style");
27
28 // Copy all attributes except style (we'll regenerate it)
29 for attr in &element.attrs {
30 if attr.name == "style" && !has_manual_style {
31 continue;
32 }
33 new_element = new_element.attr(&attr.name, &attr.value);
34 }
35
36 // If there's no manual style attribute, generate one from the Style struct
37 if !has_manual_style {
38 new_element.style = element.style.clone();
39
40 // Also set the style as an attribute for the HTML emitter
41 if let Some(css) = element.style.to_css() {
42 new_element = new_element.attr("style", css);
43 }
44 }
45
46 // Recursively process children
47 let children: Vec<Node> = element
48 .children
49 .iter()
50 .map(|child| inline_node(child, &element.style))
51 .collect();
52 new_element = new_element.children(children);
53
54 Node::Element(new_element)
55 }
56 Node::Fragment(nodes) => {
57 let children: Vec<Node> = nodes
58 .iter()
59 .map(|child| inline_node(child, _inherited))
60 .collect();
61 Node::Fragment(children)
62 }
63 other => other.clone(),
64 }
65}