Skip to main content

ferrum_email_core/
node.rs

1//! The Node tree — an intermediate representation for email content.
2//!
3//! Components produce `Node` trees. The renderer walks the tree and emits email-safe HTML.
4
5use crate::style::Style;
6
7/// An HTML tag used in email rendering.
8#[derive(Debug, Clone, PartialEq)]
9pub enum Tag {
10    Html,
11    Head,
12    Meta,
13    Title,
14    Body,
15    Div,
16    Span,
17    Table,
18    Tbody,
19    Tr,
20    Td,
21    Th,
22    P,
23    H1,
24    H2,
25    H3,
26    H4,
27    H5,
28    H6,
29    A,
30    Img,
31    Hr,
32    Br,
33    Pre,
34    Code,
35    Strong,
36    Em,
37    /// A custom/raw tag name.
38    Custom(String),
39}
40
41impl Tag {
42    /// Returns the HTML tag name string.
43    pub fn as_str(&self) -> &str {
44        match self {
45            Tag::Html => "html",
46            Tag::Head => "head",
47            Tag::Meta => "meta",
48            Tag::Title => "title",
49            Tag::Body => "body",
50            Tag::Div => "div",
51            Tag::Span => "span",
52            Tag::Table => "table",
53            Tag::Tbody => "tbody",
54            Tag::Tr => "tr",
55            Tag::Td => "td",
56            Tag::Th => "th",
57            Tag::P => "p",
58            Tag::H1 => "h1",
59            Tag::H2 => "h2",
60            Tag::H3 => "h3",
61            Tag::H4 => "h4",
62            Tag::H5 => "h5",
63            Tag::H6 => "h6",
64            Tag::A => "a",
65            Tag::Img => "img",
66            Tag::Hr => "hr",
67            Tag::Br => "br",
68            Tag::Pre => "pre",
69            Tag::Code => "code",
70            Tag::Strong => "strong",
71            Tag::Em => "em",
72            Tag::Custom(name) => name.as_str(),
73        }
74    }
75
76    /// Returns true if this is a void/self-closing element.
77    pub fn is_void(&self) -> bool {
78        matches!(self, Tag::Meta | Tag::Img | Tag::Hr | Tag::Br)
79    }
80}
81
82/// An attribute on an HTML element.
83#[derive(Debug, Clone, PartialEq)]
84pub struct Attr {
85    pub name: String,
86    pub value: String,
87}
88
89impl Attr {
90    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
91        Attr {
92            name: name.into(),
93            value: value.into(),
94        }
95    }
96}
97
98/// An HTML element in the node tree.
99#[derive(Debug, Clone, PartialEq)]
100pub struct Element {
101    pub tag: Tag,
102    pub attrs: Vec<Attr>,
103    pub style: Style,
104    pub children: Vec<Node>,
105}
106
107impl Element {
108    /// Create a new element with the given tag.
109    pub fn new(tag: Tag) -> Self {
110        Element {
111            tag,
112            attrs: Vec::new(),
113            style: Style::default(),
114            children: Vec::new(),
115        }
116    }
117
118    /// Add an attribute.
119    pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
120        self.attrs.push(Attr::new(name, value));
121        self
122    }
123
124    /// Set the inline style.
125    pub fn style(mut self, style: Style) -> Self {
126        self.style = style;
127        self
128    }
129
130    /// Add a child node.
131    pub fn child(mut self, child: Node) -> Self {
132        self.children.push(child);
133        self
134    }
135
136    /// Add multiple children.
137    pub fn children(mut self, children: impl IntoIterator<Item = Node>) -> Self {
138        self.children.extend(children);
139        self
140    }
141}
142
143/// A node in the email component tree.
144#[derive(Debug, Clone, PartialEq)]
145#[allow(clippy::large_enum_variant)]
146pub enum Node {
147    /// An HTML element with tag, attributes, style, and children.
148    Element(Element),
149    /// A text node.
150    Text(String),
151    /// A fragment containing multiple nodes (no wrapper element).
152    Fragment(Vec<Node>),
153    /// An empty node (renders nothing).
154    None,
155}
156
157impl Node {
158    /// Create a text node.
159    pub fn text(content: impl Into<String>) -> Self {
160        Node::Text(content.into())
161    }
162
163    /// Create an element node.
164    pub fn element(tag: Tag) -> Element {
165        Element::new(tag)
166    }
167
168    /// Create a fragment from multiple nodes.
169    pub fn fragment(nodes: Vec<Node>) -> Self {
170        Node::Fragment(nodes)
171    }
172}