#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
#[cfg(feature = "assets")]
pub mod assets;
#[cfg(feature = "canvas")]
pub mod canvas;
#[cfg(feature = "viewport")]
pub mod viewport;
use async_trait::async_trait;
use bytes::Bytes;
pub use hyperchad_color::Color;
use hyperchad_transformer::{Container, ResponsiveTrigger, html::ParseError, models::Selector};
pub use switchy_async::runtime::Handle;
pub use hyperchad_transformer as transformer;
#[derive(Debug)]
pub enum RendererEvent {
View(Box<View>),
#[cfg(feature = "canvas")]
CanvasUpdate(canvas::CanvasUpdate),
Event {
name: String,
value: Option<String>,
},
}
#[derive(Debug, Clone)]
pub enum Content {
View(Box<View>),
#[cfg(feature = "json")]
Json(serde_json::Value),
Raw {
data: Bytes,
content_type: String,
},
}
impl Content {
#[must_use]
pub fn builder() -> ContentBuilder {
ContentBuilder::default()
}
pub fn try_view<T: TryInto<View>>(view: T) -> Result<Self, T::Error> {
Ok(Self::View(Box::new(view.try_into()?)))
}
}
#[derive(Debug, Clone)]
pub struct ReplaceContainer {
pub selector: Selector,
pub container: Container,
}
impl From<Container> for ReplaceContainer {
fn from(container: Container) -> Self {
Self {
selector: container
.str_id
.as_ref()
.map_or(Selector::SelfTarget, |id| Selector::Id(id.clone())),
container,
}
}
}
impl From<Vec<Container>> for ReplaceContainer {
fn from(container: Vec<Container>) -> Self {
Self {
selector: Selector::SelfTarget,
container: container.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct View {
pub primary: Option<Container>,
pub fragments: Vec<ReplaceContainer>,
pub delete_selectors: Vec<Selector>,
}
impl View {
#[must_use]
pub fn builder() -> ViewBuilder {
ViewBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ViewBuilder {
primary: Option<Container>,
fragments: Vec<ReplaceContainer>,
delete_selectors: Vec<Selector>,
}
impl ViewBuilder {
#[must_use]
pub fn with_primary(mut self, view: impl Into<Container>) -> Self {
self.primary = Some(view.into());
self
}
pub fn primary(&mut self, view: impl Into<Container>) -> &mut Self {
self.primary = Some(view.into());
self
}
#[must_use]
pub fn with_fragment(mut self, container: impl Into<ReplaceContainer>) -> Self {
self.fragments.push(container.into());
self
}
pub fn fragment(&mut self, container: impl Into<ReplaceContainer>) -> &mut Self {
self.fragments.push(container.into());
self
}
#[must_use]
pub fn with_fragments(
mut self,
containers: impl IntoIterator<Item = impl Into<ReplaceContainer>>,
) -> Self {
self.fragments
.extend(containers.into_iter().map(Into::into));
self
}
pub fn fragments(
&mut self,
containers: impl IntoIterator<Item = impl Into<ReplaceContainer>>,
) -> &mut Self {
self.fragments
.extend(containers.into_iter().map(Into::into));
self
}
#[must_use]
pub fn with_delete_selector(mut self, selector: impl Into<Selector>) -> Self {
self.delete_selectors.push(selector.into());
self
}
pub fn delete_selector(&mut self, selector: impl Into<Selector>) -> &mut Self {
self.delete_selectors.push(selector.into());
self
}
#[must_use]
pub fn with_delete_selectors(
mut self,
selectors: impl IntoIterator<Item = impl Into<Selector>>,
) -> Self {
self.delete_selectors
.extend(selectors.into_iter().map(Into::into));
self
}
pub fn delete_selectors(
&mut self,
selectors: impl IntoIterator<Item = impl Into<Selector>>,
) -> &mut Self {
self.delete_selectors
.extend(selectors.into_iter().map(Into::into));
self
}
#[must_use]
pub fn build(self) -> View {
View {
primary: self.primary,
fragments: self.fragments,
delete_selectors: self.delete_selectors,
}
}
}
#[derive(Debug, Default)]
pub struct ContentBuilder {
builder: ViewBuilder,
}
impl ContentBuilder {
#[must_use]
pub fn with_primary(mut self, view: impl Into<Container>) -> Self {
self.builder = self.builder.with_primary(view);
self
}
pub fn primary(&mut self, view: impl Into<Container>) -> &mut Self {
self.builder.primary(view);
self
}
#[must_use]
pub fn with_fragment(mut self, container: impl Into<ReplaceContainer>) -> Self {
self.builder = self.builder.with_fragment(container);
self
}
pub fn fragment(&mut self, container: impl Into<ReplaceContainer>) -> &mut Self {
self.builder.fragment(container);
self
}
#[must_use]
pub fn with_fragments(
mut self,
containers: impl IntoIterator<Item = impl Into<ReplaceContainer>>,
) -> Self {
self.builder = self.builder.with_fragments(containers);
self
}
pub fn fragments(
&mut self,
containers: impl IntoIterator<Item = impl Into<ReplaceContainer>>,
) -> &mut Self {
self.builder.fragments(containers);
self
}
#[must_use]
pub fn with_delete_selector(mut self, selector: impl Into<Selector>) -> Self {
self.builder = self.builder.with_delete_selector(selector);
self
}
pub fn delete_selector(&mut self, selector: impl Into<Selector>) -> &mut Self {
self.builder.delete_selector(selector);
self
}
#[must_use]
pub fn with_delete_selectors(
mut self,
selectors: impl IntoIterator<Item = impl Into<Selector>>,
) -> Self {
self.builder = self.builder.with_delete_selectors(selectors);
self
}
pub fn delete_selectors(
&mut self,
selectors: impl IntoIterator<Item = impl Into<Selector>>,
) -> &mut Self {
self.builder.delete_selectors(selectors);
self
}
#[must_use]
pub fn build(self) -> Content {
Content::View(Box::new(self.builder.build()))
}
}
#[cfg(feature = "json")]
impl TryFrom<serde_json::Value> for Content {
type Error = serde_json::Error;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
Ok(Self::Json(value))
}
}
impl<'a> TryFrom<&'a str> for Content {
type Error = ParseError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Ok(Self::Raw {
data: value.as_bytes().to_vec().into(),
content_type: "text/html".to_string(),
})
}
}
impl TryFrom<String> for Content {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
impl From<Container> for Content {
fn from(value: Container) -> Self {
Self::View(Box::new(View {
primary: Some(value),
fragments: vec![],
delete_selectors: vec![],
}))
}
}
impl From<Vec<Container>> for Content {
fn from(value: Vec<Container>) -> Self {
Container {
children: value,
..Default::default()
}
.into()
}
}
impl From<View> for Content {
fn from(value: View) -> Self {
Self::View(Box::new(value))
}
}
impl<'a> TryFrom<&'a str> for View {
type Error = ParseError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Ok(Self {
primary: Some(value.try_into()?),
fragments: vec![],
delete_selectors: vec![],
})
}
}
impl TryFrom<String> for View {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self {
primary: Some(value.try_into()?),
fragments: vec![],
delete_selectors: vec![],
})
}
}
impl From<Container> for View {
fn from(value: Container) -> Self {
Self {
primary: Some(value),
fragments: vec![],
delete_selectors: vec![],
}
}
}
impl From<Vec<Container>> for View {
fn from(value: Vec<Container>) -> Self {
Self {
primary: Some(value.into()),
fragments: vec![],
delete_selectors: vec![],
}
}
}
pub trait RenderRunner: Send + Sync {
fn run(&mut self) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
}
pub trait ToRenderRunner {
fn to_runner(
self,
handle: Handle,
) -> Result<Box<dyn RenderRunner>, Box<dyn std::error::Error + Send>>;
}
#[async_trait]
pub trait Renderer: ToRenderRunner + Send + Sync {
#[allow(clippy::too_many_arguments)]
async fn init(
&mut self,
width: f32,
height: f32,
x: Option<i32>,
y: Option<i32>,
background: Option<Color>,
title: Option<&str>,
description: Option<&str>,
viewport: Option<&str>,
) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger);
async fn emit_event(
&self,
event_name: String,
event_value: Option<String>,
) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
async fn render(&self, view: View) -> Result<(), Box<dyn std::error::Error + Send + 'static>>;
#[cfg(feature = "canvas")]
async fn render_canvas(
&self,
update: canvas::CanvasUpdate,
) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
unimplemented!("Unable to render canvas update={update:?}")
}
}
#[cfg(feature = "html")]
pub trait HtmlTagRenderer {
fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger);
fn element_attrs_to_html(
&self,
f: &mut dyn std::io::Write,
container: &Container,
is_flex_child: bool,
) -> Result<(), std::io::Error>;
fn reactive_conditions_to_css(
&self,
_f: &mut dyn std::io::Write,
_container: &Container,
) -> Result<(), std::io::Error> {
Ok(())
}
fn partial_html(
&self,
headers: &std::collections::BTreeMap<String, String>,
container: &Container,
content: String,
viewport: Option<&str>,
background: Option<Color>,
) -> String;
#[allow(clippy::too_many_arguments)]
fn root_html(
&self,
headers: &std::collections::BTreeMap<String, String>,
container: &Container,
content: String,
viewport: Option<&str>,
background: Option<Color>,
title: Option<&str>,
description: Option<&str>,
css_urls: &[String],
css_paths: &[String],
inline_css: &[String],
) -> String;
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_replace_container_from_container_with_id() {
let container = Container {
str_id: Some("test-id".to_string()),
..Default::default()
};
let replace = ReplaceContainer::from(container.clone());
assert_eq!(replace.selector, Selector::Id("test-id".to_string()));
assert_eq!(replace.container.str_id, container.str_id);
}
#[test_log::test]
fn test_replace_container_from_container_without_id() {
let container = Container {
str_id: None,
..Default::default()
};
let replace = ReplaceContainer::from(container);
assert_eq!(replace.selector, Selector::SelfTarget);
}
#[test_log::test]
fn test_replace_container_from_vec_containers() {
let containers = vec![
Container {
str_id: Some("first".to_string()),
..Default::default()
},
Container {
str_id: Some("second".to_string()),
..Default::default()
},
];
let replace = ReplaceContainer::from(containers);
assert_eq!(replace.selector, Selector::SelfTarget);
assert_eq!(replace.container.children.len(), 2);
}
#[test_log::test]
fn test_view_builder_with_primary() {
let container = Container::default();
let view = View::builder().with_primary(container).build();
assert!(view.primary.is_some());
assert!(view.fragments.is_empty());
assert!(view.delete_selectors.is_empty());
}
#[test_log::test]
fn test_view_builder_with_fragments() {
let fragment1 = Container {
str_id: Some("frag1".to_string()),
..Default::default()
};
let fragment2 = Container {
str_id: Some("frag2".to_string()),
..Default::default()
};
let view = View::builder()
.with_fragment(fragment1)
.with_fragment(fragment2)
.build();
assert!(view.primary.is_none());
assert_eq!(view.fragments.len(), 2);
}
#[test_log::test]
fn test_view_builder_with_delete_selectors() {
let view = View::builder()
.with_delete_selector(Selector::Id("remove-me".to_string()))
.with_delete_selector(Selector::Class("hidden".to_string()))
.build();
assert_eq!(view.delete_selectors.len(), 2);
}
#[test_log::test]
fn test_view_builder_mutable_methods() {
let mut builder = View::builder();
builder
.primary(Container::default())
.fragment(Container {
str_id: Some("test".to_string()),
..Default::default()
})
.delete_selector(Selector::Id("del".to_string()));
let view = builder.build();
assert!(view.primary.is_some());
assert_eq!(view.fragments.len(), 1);
assert_eq!(view.delete_selectors.len(), 1);
}
#[test_log::test]
fn test_content_builder_creates_view_content() {
let container = Container::default();
let content = Content::builder().with_primary(container).build();
match content {
Content::View(view) => {
assert!(view.primary.is_some());
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_from_container() {
let container = Container::default();
let content: Content = container.into();
match content {
Content::View(view) => {
assert!(view.primary.is_some());
assert!(view.fragments.is_empty());
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_from_vec_containers() {
let containers = vec![Container::default(), Container::default()];
let content: Content = containers.into();
match content {
Content::View(view) => {
assert!(view.primary.is_some());
if let Some(primary) = &view.primary {
assert_eq!(primary.children.len(), 2);
}
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_from_view() {
let view = View::builder().with_primary(Container::default()).build();
let content: Content = view.into();
match content {
Content::View(boxed_view) => {
assert!(boxed_view.primary.is_some());
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_try_from_str() {
let html = "<div>test</div>";
let content = Content::try_from(html);
assert!(content.is_ok());
match content.unwrap() {
Content::Raw { data, content_type } => {
assert_eq!(data.as_ref(), html.as_bytes());
assert_eq!(content_type, "text/html");
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected Raw, got Json"),
Content::View(_) => panic!("Expected Raw, got View"),
}
}
#[test_log::test]
fn test_content_try_from_string() {
let html = String::from("<div>test</div>");
let content = Content::try_from(html);
assert!(content.is_ok());
}
#[test_log::test]
fn test_view_from_container() {
let container = Container::default();
let view: View = container.into();
assert!(view.primary.is_some());
assert!(view.fragments.is_empty());
assert!(view.delete_selectors.is_empty());
}
#[test_log::test]
fn test_view_from_vec_containers() {
let containers = vec![Container::default(), Container::default()];
let view: View = containers.into();
assert!(view.primary.is_some());
if let Some(primary) = &view.primary {
assert_eq!(primary.children.len(), 2);
}
}
#[test_log::test]
fn test_view_builder_with_fragments_batch() {
let fragments = vec![
Container {
str_id: Some("frag1".to_string()),
..Default::default()
},
Container {
str_id: Some("frag2".to_string()),
..Default::default()
},
Container {
str_id: Some("frag3".to_string()),
..Default::default()
},
];
let view = View::builder().with_fragments(fragments).build();
assert_eq!(view.fragments.len(), 3);
}
#[test_log::test]
fn test_view_builder_fragments_mutable_batch() {
let fragments = vec![
Container {
str_id: Some("a".to_string()),
..Default::default()
},
Container {
str_id: Some("b".to_string()),
..Default::default()
},
];
let mut builder = View::builder();
builder.fragments(fragments);
let view = builder.build();
assert_eq!(view.fragments.len(), 2);
}
#[test_log::test]
fn test_view_builder_with_delete_selectors_batch() {
let selectors = vec![
Selector::Id("del1".to_string()),
Selector::Class("del2".to_string()),
Selector::SelfTarget,
];
let view = View::builder().with_delete_selectors(selectors).build();
assert_eq!(view.delete_selectors.len(), 3);
}
#[test_log::test]
fn test_view_builder_delete_selectors_mutable_batch() {
let selectors = vec![Selector::Id("x".to_string()), Selector::Id("y".to_string())];
let mut builder = View::builder();
builder.delete_selectors(selectors);
let view = builder.build();
assert_eq!(view.delete_selectors.len(), 2);
}
#[test_log::test]
fn test_content_builder_mutable_fragment() {
let mut builder = Content::builder();
builder.fragment(Container {
str_id: Some("frag".to_string()),
..Default::default()
});
let content = builder.build();
match content {
Content::View(view) => {
assert_eq!(view.fragments.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_builder_mutable_delete_selector() {
let mut builder = Content::builder();
builder.delete_selector(Selector::Id("remove".to_string()));
let content = builder.build();
match content {
Content::View(view) => {
assert_eq!(view.delete_selectors.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_builder_with_fragments_batch() {
let fragments = vec![
Container {
str_id: Some("f1".to_string()),
..Default::default()
},
Container {
str_id: Some("f2".to_string()),
..Default::default()
},
];
let content = Content::builder().with_fragments(fragments).build();
match content {
Content::View(view) => {
assert_eq!(view.fragments.len(), 2);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_builder_fragments_mutable_batch() {
let fragments = vec![Container {
str_id: Some("test".to_string()),
..Default::default()
}];
let mut builder = Content::builder();
builder.fragments(fragments);
let content = builder.build();
match content {
Content::View(view) => {
assert_eq!(view.fragments.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_builder_with_delete_selectors_batch() {
let selectors = vec![
Selector::Id("a".to_string()),
Selector::Class("b".to_string()),
];
let content = Content::builder().with_delete_selectors(selectors).build();
match content {
Content::View(view) => {
assert_eq!(view.delete_selectors.len(), 2);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_builder_delete_selectors_mutable_batch() {
let selectors = vec![Selector::SelfTarget];
let mut builder = Content::builder();
builder.delete_selectors(selectors);
let content = builder.build();
match content {
Content::View(view) => {
assert_eq!(view.delete_selectors.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_try_view_with_container() {
let container = Container::default();
let content = Content::try_view(container);
assert!(content.is_ok());
match content.unwrap() {
Content::View(view) => {
assert!(view.primary.is_some());
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_content_try_view_with_view() {
let view = View::builder()
.with_primary(Container::default())
.with_fragment(Container {
str_id: Some("frag".to_string()),
..Default::default()
})
.build();
let content = Content::try_view(view);
assert!(content.is_ok());
match content.unwrap() {
Content::View(boxed_view) => {
assert!(boxed_view.primary.is_some());
assert_eq!(boxed_view.fragments.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_view_try_from_valid_html_str() {
let html = "<div>Hello</div>";
let view = View::try_from(html);
assert!(view.is_ok());
let view = view.unwrap();
assert!(view.primary.is_some());
assert!(view.fragments.is_empty());
assert!(view.delete_selectors.is_empty());
}
#[test_log::test]
fn test_view_try_from_valid_html_string() {
let html = String::from("<span>World</span>");
let view = View::try_from(html);
assert!(view.is_ok());
let view = view.unwrap();
assert!(view.primary.is_some());
}
#[test_log::test]
fn test_view_builder_combined_operations() {
let view = View::builder()
.with_primary(Container::default())
.with_fragment(Container {
str_id: Some("frag1".to_string()),
..Default::default()
})
.with_fragments(vec![Container {
str_id: Some("frag2".to_string()),
..Default::default()
}])
.with_delete_selector(Selector::Id("del1".to_string()))
.with_delete_selectors(vec![Selector::Class("del2".to_string())])
.build();
assert!(view.primary.is_some());
assert_eq!(view.fragments.len(), 2);
assert_eq!(view.delete_selectors.len(), 2);
}
#[test_log::test]
fn test_content_builder_combined_operations() {
let content = Content::builder()
.with_primary(Container::default())
.with_fragment(Container {
str_id: Some("f1".to_string()),
..Default::default()
})
.with_delete_selector(Selector::Id("d1".to_string()))
.build();
match content {
Content::View(view) => {
assert!(view.primary.is_some());
assert_eq!(view.fragments.len(), 1);
assert_eq!(view.delete_selectors.len(), 1);
}
#[cfg(feature = "json")]
Content::Json(_) => panic!("Expected View, got Json"),
Content::Raw { .. } => panic!("Expected View, got Raw"),
}
}
#[test_log::test]
fn test_replace_container_selector_types() {
let container_with_id = Container {
str_id: Some("my-id".to_string()),
..Default::default()
};
let replace = ReplaceContainer::from(container_with_id);
assert!(matches!(replace.selector, Selector::Id(_)));
let container_no_id = Container {
str_id: None,
..Default::default()
};
let replace = ReplaceContainer::from(container_no_id);
assert!(matches!(replace.selector, Selector::SelfTarget));
}
}