Skip to main content

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}