use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Html {
value: String,
template: Option<TemplateSnapshot>,
}
impl Html {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
template: None,
}
}
pub fn as_str(&self) -> &str {
&self.value
}
pub fn into_string(self) -> String {
self.value
}
pub fn template(&self) -> Option<&TemplateSnapshot> {
self.template.as_ref()
}
}
impl From<String> for Html {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for Html {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl fmt::Display for Html {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TemplateSnapshot {
static_segments: Vec<&'static str>,
dynamic_segments: Vec<String>,
}
impl TemplateSnapshot {
pub fn static_segments(&self) -> &[&'static str] {
&self.static_segments
}
pub fn dynamic_segments(&self) -> &[String] {
&self.dynamic_segments
}
pub(crate) fn compatible_with(&self, other: &Self) -> bool {
self.static_segments == other.static_segments
&& self.dynamic_segments.len() == other.dynamic_segments.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Template {
static_segments: Vec<&'static str>,
dynamic_segments: Vec<String>,
}
impl Template {
pub fn new(static_segments: Vec<&'static str>, dynamic_segments: Vec<String>) -> Self {
Self {
static_segments,
dynamic_segments,
}
}
pub fn static_segments(&self) -> &[&'static str] {
&self.static_segments
}
pub fn dynamic_segments(&self) -> &[String] {
&self.dynamic_segments
}
pub fn render(&self) -> Html {
let mut out = String::new();
for (index, segment) in self.static_segments.iter().enumerate() {
out.push_str(segment);
if let Some(dynamic) = self.dynamic_segments.get(index) {
out.push_str(r#"<span data-shelly-slot=""#);
out.push_str(&index.to_string());
out.push_str(r#"">"#);
out.push_str(dynamic);
out.push_str("</span>");
}
}
Html {
value: out,
template: Some(TemplateSnapshot {
static_segments: self.static_segments.clone(),
dynamic_segments: self.dynamic_segments.clone(),
}),
}
}
pub fn render_string(&self) -> String {
self.render().into_string()
}
}
impl From<Template> for Html {
fn from(value: Template) -> Self {
value.render()
}
}
pub fn escape_html(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::{escape_html, Html, Template};
#[test]
fn html_wraps_fragment() {
let html = Html::new("<p>Hello</p>");
assert_eq!(html.as_str(), "<p>Hello</p>");
assert_eq!(html.into_string(), "<p>Hello</p>");
}
#[test]
fn escape_html_escapes_text_and_attribute_delimiters() {
assert_eq!(
escape_html(r#"<script title="x">Tom & 'Ada'</script>"#),
"<script title="x">Tom & 'Ada'</script>"
);
}
#[test]
fn template_renders_static_and_dynamic_segments() {
let template = Template::new(vec!["<p>Hello ", "</p>"], vec![escape_html("<Ada & Bob>")]);
assert_eq!(template.static_segments(), &["<p>Hello ", "</p>"]);
assert_eq!(template.dynamic_segments(), &["<Ada & Bob>"]);
assert_eq!(
template.render_string(),
r#"<p>Hello <span data-shelly-slot="0"><Ada & Bob></span></p>"#
);
}
}