hyperchad_renderer_html/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3#![allow(clippy::multiple_crate_versions)]
4
5use std::{collections::HashMap, io::Write};
6
7use async_trait::async_trait;
8use flume::Receiver;
9use html::{
10    element_classes_to_html, element_style_to_html, number_to_html_string, write_css_attr_important,
11};
12use hyperchad_renderer::{
13    Color, HtmlTagRenderer, PartialView, RenderRunner, Renderer, ToRenderRunner, View,
14    canvas::CanvasUpdate,
15};
16use hyperchad_router::Container;
17use hyperchad_transformer::{
18    OverrideCondition, OverrideItem, ResponsiveTrigger,
19    models::{AlignItems, LayoutDirection, TextAlign, Visibility},
20};
21use maud::{DOCTYPE, PreEscaped, html};
22use tokio::runtime::Handle;
23
24#[cfg(feature = "actix")]
25pub use actix::router_to_actix;
26
27#[cfg(feature = "lambda")]
28pub use lambda::router_to_lambda;
29
30pub mod html;
31pub mod stub;
32
33#[cfg(feature = "actix")]
34pub mod actix;
35
36#[cfg(feature = "lambda")]
37pub mod lambda;
38
39#[cfg(feature = "extend")]
40pub mod extend;
41
42#[derive(Default, Clone)]
43pub struct DefaultHtmlTagRenderer {
44    pub responsive_triggers: HashMap<String, ResponsiveTrigger>,
45}
46
47impl DefaultHtmlTagRenderer {
48    #[must_use]
49    pub fn with_responsive_trigger(
50        mut self,
51        name: impl Into<String>,
52        trigger: ResponsiveTrigger,
53    ) -> Self {
54        self.add_responsive_trigger(name.into(), trigger);
55        self
56    }
57}
58
59impl HtmlTagRenderer for DefaultHtmlTagRenderer {
60    fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger) {
61        self.responsive_triggers.insert(name, trigger);
62    }
63
64    /// # Errors
65    ///
66    /// * If the `HtmlTagRenderer` fails to write the element attributes
67    fn element_attrs_to_html(
68        &self,
69        f: &mut dyn Write,
70        container: &Container,
71        is_flex_child: bool,
72    ) -> Result<(), std::io::Error> {
73        if let Some(id) = &container.str_id {
74            f.write_all(b" id=\"")?;
75            f.write_all(id.as_bytes())?;
76            f.write_all(b"\"")?;
77        }
78
79        element_style_to_html(f, container, is_flex_child)?;
80        element_classes_to_html(f, container)?;
81
82        for (key, value) in &container.data {
83            f.write_all(b" data-")?;
84            f.write_all(key.as_bytes())?;
85            f.write_all(b"=\"")?;
86            f.write_all(html_escape::encode_quoted_attribute(value).as_bytes())?;
87            f.write_all(b"\"")?;
88        }
89
90        Ok(())
91    }
92
93    /// # Errors
94    ///
95    /// * If the `HtmlTagRenderer` fails to write the css media-queries
96    #[allow(clippy::too_many_lines)]
97    fn reactive_conditions_to_css(
98        &self,
99        f: &mut dyn Write,
100        container: &Container,
101    ) -> Result<(), std::io::Error> {
102        f.write_all(b"<style>")?;
103
104        for (container, config) in container.iter_overrides(true) {
105            let Some(id) = &container.str_id else {
106                continue;
107            };
108
109            let Some(trigger) = (match &config.condition {
110                OverrideCondition::ResponsiveTarget { name } => self.responsive_triggers.get(name),
111            }) else {
112                continue;
113            };
114
115            f.write_all(b"@media(")?;
116
117            match trigger {
118                ResponsiveTrigger::MaxWidth(number) => {
119                    f.write_all(b"max-width:")?;
120                    f.write_all(number_to_html_string(number, true).as_bytes())?;
121                }
122                ResponsiveTrigger::MaxHeight(number) => {
123                    f.write_all(b"max-height:")?;
124                    f.write_all(number_to_html_string(number, true).as_bytes())?;
125                }
126            }
127
128            f.write_all(b"){")?;
129
130            f.write_all(b"#")?;
131            f.write_all(id.as_bytes())?;
132            f.write_all(b"{")?;
133
134            for o in &config.overrides {
135                match o {
136                    OverrideItem::Direction(x) => {
137                        write_css_attr_important(
138                            f,
139                            override_item_to_css_name(o),
140                            match x {
141                                LayoutDirection::Row => b"row",
142                                LayoutDirection::Column => b"column",
143                            },
144                        )?;
145                    }
146                    OverrideItem::Visibility(x) => {
147                        write_css_attr_important(
148                            f,
149                            override_item_to_css_name(o),
150                            match x {
151                                Visibility::Visible => b"visible",
152                                Visibility::Hidden => b"hidden",
153                            },
154                        )?;
155                    }
156                    OverrideItem::Hidden(x) => {
157                        write_css_attr_important(
158                            f,
159                            override_item_to_css_name(o),
160                            if *x { b"none" } else { b"initial" },
161                        )?;
162                    }
163                    OverrideItem::AlignItems(x) => {
164                        write_css_attr_important(
165                            f,
166                            override_item_to_css_name(o),
167                            match x {
168                                AlignItems::Start => b"start",
169                                AlignItems::Center => b"center",
170                                AlignItems::End => b"end",
171                            },
172                        )?;
173                    }
174                    OverrideItem::TextAlign(x) => {
175                        write_css_attr_important(
176                            f,
177                            override_item_to_css_name(o),
178                            match x {
179                                TextAlign::Start => b"start",
180                                TextAlign::Center => b"center",
181                                TextAlign::End => b"end",
182                                TextAlign::Justify => b"justify",
183                            },
184                        )?;
185                    }
186                    OverrideItem::MarginLeft(x)
187                    | OverrideItem::MarginRight(x)
188                    | OverrideItem::MarginTop(x)
189                    | OverrideItem::MarginBottom(x)
190                    | OverrideItem::Width(x)
191                    | OverrideItem::MinWidth(x)
192                    | OverrideItem::MaxWidth(x)
193                    | OverrideItem::Height(x)
194                    | OverrideItem::MinHeight(x)
195                    | OverrideItem::MaxHeight(x)
196                    | OverrideItem::Left(x)
197                    | OverrideItem::Right(x)
198                    | OverrideItem::Top(x)
199                    | OverrideItem::Bottom(x)
200                    | OverrideItem::ColumnGap(x)
201                    | OverrideItem::RowGap(x)
202                    | OverrideItem::BorderTopLeftRadius(x)
203                    | OverrideItem::BorderTopRightRadius(x)
204                    | OverrideItem::BorderBottomLeftRadius(x)
205                    | OverrideItem::BorderBottomRightRadius(x)
206                    | OverrideItem::PaddingLeft(x)
207                    | OverrideItem::PaddingRight(x)
208                    | OverrideItem::PaddingTop(x)
209                    | OverrideItem::PaddingBottom(x)
210                    | OverrideItem::Opacity(x)
211                    | OverrideItem::TranslateX(x)
212                    | OverrideItem::TranslateY(x)
213                    | OverrideItem::FontSize(x)
214                    | OverrideItem::GridCellSize(x) => {
215                        write_css_attr_important(
216                            f,
217                            override_item_to_css_name(o),
218                            number_to_html_string(x, true).as_bytes(),
219                        )?;
220                    }
221                    OverrideItem::StrId(..)
222                    | OverrideItem::Classes(..)
223                    | OverrideItem::OverflowX(..)
224                    | OverrideItem::OverflowY(..)
225                    | OverrideItem::JustifyContent(..)
226                    | OverrideItem::TextDecoration(..)
227                    | OverrideItem::FontFamily(..)
228                    | OverrideItem::Flex(..)
229                    | OverrideItem::Cursor(..)
230                    | OverrideItem::Position(..)
231                    | OverrideItem::Background(..)
232                    | OverrideItem::BorderTop(..)
233                    | OverrideItem::BorderRight(..)
234                    | OverrideItem::BorderBottom(..)
235                    | OverrideItem::BorderLeft(..)
236                    | OverrideItem::Color(..) => {}
237                }
238            }
239
240            f.write_all(b"}")?; // container id
241            f.write_all(b"}")?; // media query
242        }
243
244        f.write_all(b"</style>")?;
245
246        Ok(())
247    }
248
249    fn partial_html(
250        &self,
251        _headers: &HashMap<String, String>,
252        _container: &Container,
253        content: String,
254        _viewport: Option<&str>,
255        _background: Option<Color>,
256    ) -> String {
257        content
258    }
259
260    fn root_html(
261        &self,
262        _headers: &HashMap<String, String>,
263        container: &Container,
264        content: String,
265        viewport: Option<&str>,
266        background: Option<Color>,
267        title: Option<&str>,
268        description: Option<&str>,
269    ) -> String {
270        let background = background.map(|x| format!("background:rgb({},{},{})", x.r, x.g, x.b));
271        let background = background.as_deref().unwrap_or("");
272
273        let mut responsive_css = vec![];
274        self.reactive_conditions_to_css(&mut responsive_css, container)
275            .unwrap();
276        let responsive_css = std::str::from_utf8(&responsive_css).unwrap();
277
278        html! {
279            (DOCTYPE)
280            html style="height:100%" lang="en" {
281                head {
282                    @if let Some(title) = title {
283                        title { (title) }
284                    }
285                    @if let Some(description) = description {
286                        meta name="description" content=(description);
287                    }
288                    style {(format!(r"
289                        body {{
290                            margin: 0;{background};
291                            overflow: hidden;
292                        }}
293                        .remove-button-styles {{
294                            background: none;
295                            color: inherit;
296                            border: none;
297                            padding: 0;
298                            font: inherit;
299                            cursor: pointer;
300                            outline: inherit;
301                        }}
302                    "))}
303                    (PreEscaped(responsive_css))
304                    @if let Some(content) = viewport {
305                        meta name="viewport" content=(content);
306                    }
307                }
308                body style="height:100%" {
309                    (PreEscaped(content))
310                }
311            }
312        }
313        .into_string()
314    }
315}
316
317const fn override_item_to_css_name(item: &OverrideItem) -> &'static [u8] {
318    match item {
319        OverrideItem::StrId(..) => b"id",
320        OverrideItem::Classes(..) => b"classes",
321        OverrideItem::Direction(..) => b"flex-direction",
322        OverrideItem::OverflowX(..) => b"overflow-x",
323        OverrideItem::OverflowY(..) => b"overflow-y",
324        OverrideItem::GridCellSize(..) => b"grid-template-columns",
325        OverrideItem::JustifyContent(..) => b"justify-content",
326        OverrideItem::AlignItems(..) => b"align-items",
327        OverrideItem::TextAlign(..) => b"text-align",
328        OverrideItem::TextDecoration(..) => b"text-decoration",
329        OverrideItem::FontFamily(..) => b"font-family",
330        OverrideItem::Width(..) => b"width",
331        OverrideItem::MinWidth(..) => b"min-width",
332        OverrideItem::MaxWidth(..) => b"max-width",
333        OverrideItem::Height(..) => b"height",
334        OverrideItem::MinHeight(..) => b"min-height",
335        OverrideItem::MaxHeight(..) => b"max-height",
336        OverrideItem::Flex(..) => b"flex",
337        OverrideItem::ColumnGap(..) => b"column-gap",
338        OverrideItem::RowGap(..) => b"row-gap",
339        OverrideItem::Opacity(..) => b"opacity",
340        OverrideItem::Left(..) => b"left",
341        OverrideItem::Right(..) => b"right",
342        OverrideItem::Top(..) => b"top",
343        OverrideItem::Bottom(..) => b"bottom",
344        OverrideItem::TranslateX(..) | OverrideItem::TranslateY(..) => b"transform",
345        OverrideItem::Cursor(..) => b"cursor",
346        OverrideItem::Position(..) => b"position",
347        OverrideItem::Background(..) => b"background",
348        OverrideItem::BorderTop(..) => b"border-top",
349        OverrideItem::BorderRight(..) => b"border-right",
350        OverrideItem::BorderBottom(..) => b"border-bottom",
351        OverrideItem::BorderLeft(..) => b"border-left",
352        OverrideItem::BorderTopLeftRadius(..) => b"border-top-left-radius",
353        OverrideItem::BorderTopRightRadius(..) => b"border-top-right-radius",
354        OverrideItem::BorderBottomLeftRadius(..) => b"border-bottom-left-radius",
355        OverrideItem::BorderBottomRightRadius(..) => b"border-bottom-right-radius",
356        OverrideItem::MarginLeft(..) => b"margin-left",
357        OverrideItem::MarginRight(..) => b"margin-right",
358        OverrideItem::MarginTop(..) => b"margin-top",
359        OverrideItem::MarginBottom(..) => b"margin-bottom",
360        OverrideItem::PaddingLeft(..) => b"padding-left",
361        OverrideItem::PaddingRight(..) => b"padding-right",
362        OverrideItem::PaddingTop(..) => b"padding-top",
363        OverrideItem::PaddingBottom(..) => b"padding-bottom",
364        OverrideItem::FontSize(..) => b"font-size",
365        OverrideItem::Color(..) => b"color",
366        OverrideItem::Hidden(..) => b"display",
367        OverrideItem::Visibility(..) => b"visibility",
368    }
369}
370
371pub trait HtmlApp {
372    #[must_use]
373    fn with_responsive_trigger(self, _name: String, _trigger: ResponsiveTrigger) -> Self;
374    fn add_responsive_trigger(&mut self, _name: String, _trigger: ResponsiveTrigger);
375
376    #[cfg(feature = "assets")]
377    #[must_use]
378    fn with_static_asset_routes(
379        self,
380        paths: impl Into<Vec<hyperchad_renderer::assets::StaticAssetRoute>>,
381    ) -> Self;
382
383    #[must_use]
384    fn with_viewport(self, viewport: Option<String>) -> Self;
385    fn set_viewport(&mut self, viewport: Option<String>);
386
387    #[must_use]
388    fn with_title(self, title: Option<String>) -> Self;
389    fn set_title(&mut self, title: Option<String>);
390
391    #[must_use]
392    fn with_description(self, description: Option<String>) -> Self;
393    fn set_description(&mut self, description: Option<String>);
394
395    #[must_use]
396    fn with_background(self, background: Option<Color>) -> Self;
397    fn set_background(&mut self, background: Option<Color>);
398
399    #[cfg(feature = "extend")]
400    #[must_use]
401    fn with_html_renderer_event_rx(self, rx: Receiver<hyperchad_renderer::RendererEvent>) -> Self;
402    #[cfg(feature = "extend")]
403    fn set_html_renderer_event_rx(&mut self, rx: Receiver<hyperchad_renderer::RendererEvent>);
404}
405
406#[derive(Clone)]
407pub struct HtmlRenderer<T: HtmlApp + ToRenderRunner + Send + Sync> {
408    width: Option<f32>,
409    height: Option<f32>,
410    x: Option<i32>,
411    y: Option<i32>,
412    pub app: T,
413    receiver: Receiver<String>,
414    #[cfg(feature = "extend")]
415    extend: Option<std::sync::Arc<Box<dyn extend::ExtendHtmlRenderer + Send + Sync>>>,
416    #[cfg(feature = "extend")]
417    publisher: Option<extend::HtmlRendererEventPub>,
418}
419
420impl<T: HtmlApp + ToRenderRunner + Send + Sync> HtmlRenderer<T> {
421    #[must_use]
422    pub fn new(app: T) -> Self {
423        let (_tx, rx) = flume::unbounded();
424
425        Self {
426            width: None,
427            height: None,
428            x: None,
429            y: None,
430            app,
431            receiver: rx,
432            #[cfg(feature = "extend")]
433            extend: None,
434            #[cfg(feature = "extend")]
435            publisher: None,
436        }
437    }
438
439    #[must_use]
440    pub fn with_background(mut self, background: Option<Color>) -> Self {
441        self.app = self.app.with_background(background);
442        self
443    }
444
445    #[must_use]
446    pub fn with_title(mut self, title: Option<String>) -> Self {
447        self.app = self.app.with_title(title);
448        self
449    }
450
451    #[must_use]
452    pub fn with_description(mut self, description: Option<String>) -> Self {
453        self.app = self.app.with_description(description);
454        self
455    }
456
457    #[must_use]
458    pub async fn wait_for_navigation(&self) -> Option<String> {
459        self.receiver.recv_async().await.ok()
460    }
461
462    #[cfg(feature = "assets")]
463    #[must_use]
464    pub fn with_static_asset_routes(
465        mut self,
466        paths: impl Into<Vec<hyperchad_renderer::assets::StaticAssetRoute>>,
467    ) -> Self {
468        self.app = self.app.with_static_asset_routes(paths);
469        self
470    }
471
472    #[cfg(feature = "extend")]
473    #[must_use]
474    pub fn with_extend_html_renderer(
475        mut self,
476        renderer: impl extend::ExtendHtmlRenderer + Send + Sync + 'static,
477    ) -> Self {
478        self.extend = Some(std::sync::Arc::new(Box::new(renderer)));
479        self
480    }
481
482    #[cfg(feature = "extend")]
483    #[must_use]
484    pub fn with_html_renderer_event_pub(mut self, publisher: extend::HtmlRendererEventPub) -> Self {
485        self.publisher = Some(publisher);
486        self
487    }
488}
489
490impl<T: HtmlApp + ToRenderRunner + Send + Sync> ToRenderRunner for HtmlRenderer<T> {
491    /// # Errors
492    ///
493    /// Will error if html fails to run the event loop.
494    fn to_runner(
495        self,
496        handle: Handle,
497    ) -> Result<Box<dyn RenderRunner>, Box<dyn std::error::Error + Send>> {
498        self.app.to_runner(handle)
499    }
500}
501
502#[async_trait]
503impl<T: HtmlApp + ToRenderRunner + Send + Sync> Renderer for HtmlRenderer<T> {
504    fn add_responsive_trigger(&mut self, name: String, trigger: ResponsiveTrigger) {
505        self.app.add_responsive_trigger(name, trigger);
506    }
507
508    /// # Errors
509    ///
510    /// Will error if html app fails to start
511    async fn init(
512        &mut self,
513        width: f32,
514        height: f32,
515        x: Option<i32>,
516        y: Option<i32>,
517        background: Option<Color>,
518        title: Option<&str>,
519        description: Option<&str>,
520        viewport: Option<&str>,
521    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
522        self.width = Some(width);
523        self.height = Some(height);
524        self.x = x;
525        self.y = y;
526        self.app.set_background(background);
527        self.app.set_title(title.map(ToString::to_string));
528        self.app
529            .set_description(description.map(ToString::to_string));
530        self.app.set_viewport(viewport.map(ToString::to_string));
531
532        Ok(())
533    }
534
535    /// # Errors
536    ///
537    /// Will error if html app fails to emit the event.
538    async fn emit_event(
539        &self,
540        event_name: String,
541        event_value: Option<String>,
542    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
543        log::trace!("emit_event: event_name={event_name} event_value={event_value:?}");
544
545        #[cfg(feature = "extend")]
546        if let (Some(extend), Some(publisher)) = (self.extend.as_ref(), self.publisher.as_ref()) {
547            extend
548                .emit_event(publisher.clone(), event_name, event_value)
549                .await?;
550        }
551
552        Ok(())
553    }
554
555    /// # Errors
556    ///
557    /// Will error if html fails to render the elements.
558    async fn render(
559        &self,
560        elements: View,
561    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
562        moosicbox_logging::debug_or_trace!(
563            ("render: start"),
564            ("render: start {:?}", elements.immediate)
565        );
566
567        #[cfg(feature = "extend")]
568        if let (Some(extend), Some(publisher)) = (self.extend.as_ref(), self.publisher.as_ref()) {
569            extend.render(publisher.clone(), elements).await?;
570        }
571
572        log::debug!("render: finished");
573
574        Ok(())
575    }
576
577    /// # Errors
578    ///
579    /// Will error if html fails to render the partial view.
580    ///
581    /// # Panics
582    ///
583    /// Will panic if elements `Mutex` is poisoned.
584    async fn render_partial(
585        &self,
586        view: PartialView,
587    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
588        moosicbox_logging::debug_or_trace!(
589            ("render_partial: start"),
590            ("render_partial: start {:?}", view)
591        );
592
593        #[cfg(feature = "extend")]
594        if let (Some(extend), Some(publisher)) = (self.extend.as_ref(), self.publisher.as_ref()) {
595            extend.render_partial(publisher.clone(), view).await?;
596        }
597
598        log::debug!("render_partial: finished");
599
600        Ok(())
601    }
602
603    /// # Errors
604    ///
605    /// Will error if html fails to render the canvas update.
606    ///
607    /// # Panics
608    ///
609    /// Will panic if elements `Mutex` is poisoned.
610    #[allow(unused_variables)]
611    async fn render_canvas(
612        &self,
613        update: CanvasUpdate,
614    ) -> Result<(), Box<dyn std::error::Error + Send + 'static>> {
615        log::trace!("render_canvas");
616
617        #[cfg(feature = "extend")]
618        if let (Some(extend), Some(publisher)) = (self.extend.as_ref(), self.publisher.as_ref()) {
619            extend.render_canvas(publisher.clone(), update).await?;
620        }
621
622        log::debug!("render_canvas: finished");
623
624        Ok(())
625    }
626}