1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Html {
10 value: String,
11 template: Option<TemplateSnapshot>,
12}
13
14impl Html {
15 pub fn new(value: impl Into<String>) -> Self {
17 Self {
18 value: value.into(),
19 template: None,
20 }
21 }
22
23 pub fn as_str(&self) -> &str {
25 &self.value
26 }
27
28 pub fn into_string(self) -> String {
30 self.value
31 }
32
33 pub fn template(&self) -> Option<&TemplateSnapshot> {
35 self.template.as_ref()
36 }
37}
38
39impl From<String> for Html {
40 fn from(value: String) -> Self {
41 Self::new(value)
42 }
43}
44
45impl From<&str> for Html {
46 fn from(value: &str) -> Self {
47 Self::new(value)
48 }
49}
50
51impl fmt::Display for Html {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(&self.value)
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct TemplateSnapshot {
60 static_segments: Vec<&'static str>,
61 dynamic_segments: Vec<String>,
62}
63
64impl TemplateSnapshot {
65 pub fn static_segments(&self) -> &[&'static str] {
67 &self.static_segments
68 }
69
70 pub fn dynamic_segments(&self) -> &[String] {
72 &self.dynamic_segments
73 }
74
75 pub(crate) fn compatible_with(&self, other: &Self) -> bool {
76 self.static_segments == other.static_segments
77 && self.dynamic_segments.len() == other.dynamic_segments.len()
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Template {
88 static_segments: Vec<&'static str>,
89 dynamic_segments: Vec<String>,
90}
91
92impl Template {
93 pub fn new(static_segments: Vec<&'static str>, dynamic_segments: Vec<String>) -> Self {
95 Self {
96 static_segments,
97 dynamic_segments,
98 }
99 }
100
101 pub fn static_segments(&self) -> &[&'static str] {
103 &self.static_segments
104 }
105
106 pub fn dynamic_segments(&self) -> &[String] {
108 &self.dynamic_segments
109 }
110
111 pub fn render(&self) -> Html {
113 let mut out = String::new();
114 for (index, segment) in self.static_segments.iter().enumerate() {
115 out.push_str(segment);
116 if let Some(dynamic) = self.dynamic_segments.get(index) {
117 out.push_str(r#"<span data-shelly-slot=""#);
118 out.push_str(&index.to_string());
119 out.push_str(r#"">"#);
120 out.push_str(dynamic);
121 out.push_str("</span>");
122 }
123 }
124
125 Html {
126 value: out,
127 template: Some(TemplateSnapshot {
128 static_segments: self.static_segments.clone(),
129 dynamic_segments: self.dynamic_segments.clone(),
130 }),
131 }
132 }
133
134 pub fn render_string(&self) -> String {
136 self.render().into_string()
137 }
138}
139
140impl From<Template> for Html {
141 fn from(value: Template) -> Self {
142 value.render()
143 }
144}
145
146pub fn escape_html(value: &str) -> String {
151 value
152 .replace('&', "&")
153 .replace('<', "<")
154 .replace('>', ">")
155 .replace('"', """)
156 .replace('\'', "'")
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{escape_html, Html, Template};
162
163 #[test]
164 fn html_wraps_fragment() {
165 let html = Html::new("<p>Hello</p>");
166 assert_eq!(html.as_str(), "<p>Hello</p>");
167 assert_eq!(html.into_string(), "<p>Hello</p>");
168 }
169
170 #[test]
171 fn escape_html_escapes_text_and_attribute_delimiters() {
172 assert_eq!(
173 escape_html(r#"<script title="x">Tom & 'Ada'</script>"#),
174 "<script title="x">Tom & 'Ada'</script>"
175 );
176 }
177
178 #[test]
179 fn template_renders_static_and_dynamic_segments() {
180 let template = Template::new(vec!["<p>Hello ", "</p>"], vec![escape_html("<Ada & Bob>")]);
181
182 assert_eq!(template.static_segments(), &["<p>Hello ", "</p>"]);
183 assert_eq!(template.dynamic_segments(), &["<Ada & Bob>"]);
184 assert_eq!(
185 template.render_string(),
186 r#"<p>Hello <span data-shelly-slot="0"><Ada & Bob></span></p>"#
187 );
188 }
189
190 #[test]
191 fn html_from_and_template_snapshot_helpers_cover_metadata_paths() {
192 let from_string = Html::from(String::from("<p>a</p>"));
193 let from_str = Html::from("<p>b</p>");
194 assert_eq!(from_string.as_str(), "<p>a</p>");
195 assert_eq!(from_str.as_str(), "<p>b</p>");
196
197 let template_a = Template::new(vec!["<p>", "</p>"], vec!["1".to_string()]);
198 let template_b = Template::new(vec!["<p>", "</p>"], vec!["2".to_string()]);
199 let html_a = template_a.render();
200 let html_b = template_b.render();
201 let snap_a = html_a.template().expect("template metadata");
202 let snap_b = html_b.template().expect("template metadata");
203
204 assert_eq!(snap_a.static_segments(), &["<p>", "</p>"]);
205 assert_eq!(snap_a.dynamic_segments(), &["1".to_string()]);
206 assert!(snap_a.compatible_with(snap_b));
207 }
208}