1#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum ButtonType {
7 Primary,
9 Danger,
11 Warning,
13 Info,
15 Success,
17}
18
19impl ButtonType {
20 pub fn colors<'a>(&self, brand_color: Option<&'a str>) -> (&'a str, &'a str) {
24 match self {
25 Self::Primary => (brand_color.unwrap_or("#2563eb"), "#ffffff"),
26 Self::Danger => ("#dc2626", "#ffffff"),
27 Self::Warning => ("#d97706", "#ffffff"),
28 Self::Info => ("#0891b2", "#ffffff"),
29 Self::Success => ("#16a34a", "#ffffff"),
30 }
31 }
32}
33
34pub fn parse_button(text: &str) -> Option<(ButtonType, &str)> {
37 let rest = text.strip_prefix("button")?;
38
39 if let Some(rest) = rest.strip_prefix('|') {
40 if rest.is_empty() {
42 return None;
43 }
44 return Some((ButtonType::Primary, rest));
45 }
46
47 if let Some(rest) = rest.strip_prefix(':') {
48 let (type_str, label) = rest.split_once('|')?;
50 if label.is_empty() {
51 return None;
52 }
53 let btn_type = match type_str {
54 "primary" => ButtonType::Primary,
55 "danger" => ButtonType::Danger,
56 "warning" => ButtonType::Warning,
57 "info" => ButtonType::Info,
58 "success" => ButtonType::Success,
59 _ => return None,
60 };
61 return Some((btn_type, label));
62 }
63
64 None
65}
66
67pub fn render_button_html(
69 label: &str,
70 url: &str,
71 btn_type: ButtonType,
72 brand_color: Option<&str>,
73) -> String {
74 use crate::email::render;
75 let (bg, fg) = btn_type.colors(brand_color);
76 let label = render::escape_html(label);
77 let url = render::escape_html(url);
78 format!(
79 r#"<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 16px 0;"><tr><td style="background-color: {bg}; border-radius: 6px; padding: 12px 24px;"><a href="{url}" style="color: {fg}; text-decoration: none; font-weight: 600; display: inline-block;">{label}</a></td></tr></table>"#
80 )
81}
82
83pub fn render_button_text(label: &str, url: &str) -> String {
85 format!("{label}: {url}")
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn parse_button_primary_default() {
94 let (btn_type, label) = parse_button("button|Get Started").unwrap();
95 assert_eq!(btn_type, ButtonType::Primary);
96 assert_eq!(label, "Get Started");
97 }
98
99 #[test]
100 fn parse_button_with_type() {
101 let (btn_type, label) = parse_button("button:danger|Delete Account").unwrap();
102 assert_eq!(btn_type, ButtonType::Danger);
103 assert_eq!(label, "Delete Account");
104 }
105
106 #[test]
107 fn parse_button_all_types() {
108 assert_eq!(
109 parse_button("button:primary|X").unwrap().0,
110 ButtonType::Primary
111 );
112 assert_eq!(
113 parse_button("button:danger|X").unwrap().0,
114 ButtonType::Danger
115 );
116 assert_eq!(
117 parse_button("button:warning|X").unwrap().0,
118 ButtonType::Warning
119 );
120 assert_eq!(parse_button("button:info|X").unwrap().0, ButtonType::Info);
121 assert_eq!(
122 parse_button("button:success|X").unwrap().0,
123 ButtonType::Success
124 );
125 }
126
127 #[test]
128 fn parse_button_not_a_button() {
129 assert!(parse_button("Click here").is_none());
130 assert!(parse_button("").is_none());
131 assert!(parse_button("button").is_none());
132 assert!(parse_button("button|").is_none());
133 assert!(parse_button("button:unknown|Label").is_none());
134 assert!(parse_button("button:danger|").is_none());
135 }
136
137 #[test]
138 fn render_html_contains_expected_parts() {
139 let html = render_button_html("Go", "https://x.com", ButtonType::Primary, None);
140 assert!(html.contains("background-color: #2563eb"));
141 assert!(html.contains("href=\"https://x.com\""));
142 assert!(html.contains(">Go</a>"));
143 assert!(html.contains("role=\"presentation\""));
144 }
145
146 #[test]
147 fn render_html_brand_color_overrides_primary() {
148 let html = render_button_html("Go", "https://x.com", ButtonType::Primary, Some("#ff0000"));
149 assert!(html.contains("background-color: #ff0000"));
150 }
151
152 #[test]
153 fn render_html_brand_color_does_not_affect_other_types() {
154 let html = render_button_html("Go", "https://x.com", ButtonType::Danger, Some("#ff0000"));
155 assert!(html.contains("background-color: #dc2626"));
156 }
157
158 #[test]
159 fn render_text_format() {
160 let text = render_button_text("Get Started", "https://example.com");
161 assert_eq!(text, "Get Started: https://example.com");
162 }
163
164 #[test]
165 fn render_html_escapes_special_chars() {
166 let html = render_button_html(
167 "<b>Bold</b>",
168 r#"https://x.com/?a=1&b="2""#,
169 ButtonType::Primary,
170 None,
171 );
172 assert!(html.contains("><b>Bold</b></a>"));
173 assert!(html.contains("href=\"https://x.com/?a=1&b="2"\""));
174 }
175}