use std::pin::Pin; use std::collections::HashMap; use std::future::Future; use futures::future::join_all; pub trait Component { fn render(self: Box) -> RenderNode; } pub enum DoctypeElem { Word(&'static str), String(&'static str), } pub enum RenderNode { Doctype(Vec), Suspense { fallback: Box, children: Pin>>> }, Component(Box), Element { name: String, attributes: HashMap, children: Vec }, Fragment { children: Vec }, TextNode { content: String, }, Null, } const VOID_TAGS: [&str; 14] = [ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ]; impl RenderNode { pub(crate) fn render_to_string(self) -> Pin>> { match self { RenderNode::Doctype(elements) => { let strings = elements.iter().map(|elem| match elem { DoctypeElem::Word(word) => word.to_string(), DoctypeElem::String(string) => format!("\"{}\"", string), }).collect::>().join(" ").clone(); Box::pin(async move { format!("", strings) }) }, RenderNode::Component(component) => { let result_root = component.render(); Box::pin(async move { result_root.render_to_string().await }) }, RenderNode::Suspense {fallback: _, children} => { Box::pin(async move { join_all(children.await.into_iter() .map(|child| child.render_to_string())).await .join("") }) }, RenderNode::Element { name, attributes, children } => { let text_attributes = attributes .into_iter() .map(|(key, value)| { let corrected_key = key.replace("_", "-"); format!(" {corrected_key}=\"{value}\"") }) .collect::>() .join(""); Box::pin(async move { let rendered_children = join_all(children.into_iter() .map(|child| child.render_to_string()) .collect::>()).await.join(""); let is_void = VOID_TAGS.iter().any(|&s| s == name); let has_children = rendered_children.trim() != ""; if has_children && is_void { eprintln!("WARN: <{name}/> is a void tag, and should not have children"); } if !has_children && is_void { format!("<{name}{text_attributes}/>") } else { format!("<{name}{text_attributes}>{rendered_children}") } }) }, RenderNode::Fragment { children } => { Box::pin(async move { join_all(children.into_iter() .map(|child| child.render_to_string())).await .join("") }) } RenderNode::TextNode { content } => Box::pin(async move { content }), RenderNode::Null => Box::pin(async move { "".to_string() }), } } } pub trait IntoRender { fn into_render(self) -> Vec; } macro_rules! impl_str { ($t:ty) => { impl IntoRender for $t { fn into_render(self) -> Vec { vec![RenderNode::TextNode { content: String::from(self) }] } } }; } impl_str!(String); impl_str!(&str); impl_str!(std::borrow::Cow<'_,str>); impl IntoRender for Vec { fn into_render(self) -> Vec { self } } impl IntoRender for RenderNode { fn into_render(self) -> Vec { vec![self] } }