hyperchad_renderer_html/
html.rs

1#![allow(clippy::module_name_repetitions)]
2
3use std::{collections::HashMap, io::Write};
4
5use hyperchad_renderer::{Color, HtmlTagRenderer};
6use hyperchad_router::Container;
7use hyperchad_transformer::{
8    Calculation, Element, HeaderSize, Input, Number,
9    models::{
10        AlignItems, Cursor, ImageFit, ImageLoading, JustifyContent, LayoutDirection,
11        LayoutOverflow, LinkTarget, Position, TextAlign, TextDecorationLine, TextDecorationStyle,
12        Visibility,
13    },
14};
15
16/// # Errors
17///
18/// * If any of the elements fail to be written as HTML
19pub fn elements_to_html(
20    f: &mut dyn Write,
21    containers: &[Container],
22    tag_renderer: &dyn HtmlTagRenderer,
23    is_flex_child: bool,
24) -> Result<(), std::io::Error> {
25    for container in containers {
26        element_to_html(f, container, tag_renderer, is_flex_child)?;
27    }
28
29    Ok(())
30}
31
32/// # Errors
33///
34/// * If there was an IO error writing the attribute
35pub fn write_attr(f: &mut dyn Write, attr: &[u8], value: &[u8]) -> Result<(), std::io::Error> {
36    f.write_all(b" ")?;
37    f.write_all(attr)?;
38    f.write_all(b"=\"")?;
39    f.write_all(value)?;
40    f.write_all(b"\"")?;
41    Ok(())
42}
43
44/// # Errors
45///
46/// * If there was an IO error writing the css attribute
47pub fn write_css_attr(f: &mut dyn Write, attr: &[u8], value: &[u8]) -> Result<(), std::io::Error> {
48    f.write_all(attr)?;
49    f.write_all(b":")?;
50    f.write_all(value)?;
51    f.write_all(b";")?;
52    Ok(())
53}
54
55/// # Errors
56///
57/// * If there was an IO error writing the css attribute
58pub fn write_css_attr_important(
59    f: &mut dyn Write,
60    attr: &[u8],
61    value: &[u8],
62) -> Result<(), std::io::Error> {
63    f.write_all(attr)?;
64    f.write_all(b":")?;
65    f.write_all(value)?;
66    f.write_all(b" !important;")?;
67    Ok(())
68}
69
70#[must_use]
71pub fn number_to_html_string(number: &Number, px: bool) -> String {
72    match number {
73        Number::Real(x) => {
74            if px {
75                format!("{x}px")
76            } else {
77                x.to_string()
78            }
79        }
80        Number::Integer(x) => {
81            if px {
82                format!("{x}px")
83            } else {
84                x.to_string()
85            }
86        }
87        Number::RealPercent(x) => format!("{x}%"),
88        Number::IntegerPercent(x) => format!("{x}%"),
89        Number::RealVw(x) => format!("{x}vw"),
90        Number::IntegerVw(x) => format!("{x}vw"),
91        Number::RealVh(x) => format!("{x}vh"),
92        Number::IntegerVh(x) => format!("{x}vh"),
93        Number::RealDvw(x) => format!("{x}dvw"),
94        Number::IntegerDvw(x) => format!("{x}dvw"),
95        Number::RealDvh(x) => format!("{x}dvh"),
96        Number::IntegerDvh(x) => format!("{x}dvh"),
97        Number::Calc(x) => format!("calc({})", calc_to_css_string(x, px)),
98    }
99}
100
101#[must_use]
102pub fn color_to_css_string(color: Color) -> String {
103    color.a.map_or_else(
104        || format!("rgb({},{},{})", color.r, color.g, color.b),
105        |a| format!("rgba({},{},{},{})", color.r, color.g, color.b, a),
106    )
107}
108
109#[must_use]
110pub fn calc_to_css_string(calc: &Calculation, px: bool) -> String {
111    match calc {
112        Calculation::Number(number) => number_to_html_string(number, px),
113        Calculation::Add(left, right) => format!(
114            "{} + {}",
115            calc_to_css_string(left, px),
116            calc_to_css_string(right, px)
117        ),
118        Calculation::Subtract(left, right) => format!(
119            "{} - {}",
120            calc_to_css_string(left, px),
121            calc_to_css_string(right, px)
122        ),
123        Calculation::Multiply(left, right) => format!(
124            "{} * {}",
125            calc_to_css_string(left, false),
126            calc_to_css_string(right, false)
127        ),
128        Calculation::Divide(left, right) => format!(
129            "{} / {}",
130            calc_to_css_string(left, false),
131            calc_to_css_string(right, false)
132        ),
133        Calculation::Grouping(value) => format!("({})", calc_to_css_string(value, px)),
134        Calculation::Min(left, right) => format!(
135            "min({}, {})",
136            calc_to_css_string(left, px),
137            calc_to_css_string(right, px)
138        ),
139        Calculation::Max(left, right) => format!(
140            "max({}, {})",
141            calc_to_css_string(left, px),
142            calc_to_css_string(right, px)
143        ),
144    }
145}
146
147const fn is_grid_container(container: &Container) -> bool {
148    matches!(container.overflow_x, LayoutOverflow::Wrap { grid: true })
149}
150
151/// # Errors
152///
153/// * If there were any IO errors writing the element style attribute
154#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
155pub fn element_style_to_html(
156    f: &mut dyn Write,
157    container: &Container,
158    _is_flex_child: bool,
159) -> Result<(), std::io::Error> {
160    let mut printed_start = false;
161
162    macro_rules! write_css_attr {
163        ($key:expr, $value:expr $(,)?) => {{
164            if !printed_start {
165                printed_start = true;
166                f.write_all(b" style=\"")?;
167            }
168            write_css_attr(f, $key, $value)?;
169        }};
170    }
171
172    match &container.element {
173        Element::Image { fit, .. } => {
174            if let Some(fit) = fit {
175                write_css_attr!(
176                    b"object-fit",
177                    match fit {
178                        ImageFit::Default => b"unset",
179                        ImageFit::Contain => b"contain",
180                        ImageFit::Cover => b"cover",
181                        ImageFit::Fill => b"fill",
182                        ImageFit::None => b"none",
183                    }
184                );
185            }
186        }
187        Element::Div
188        | Element::Raw { .. }
189        | Element::Aside
190        | Element::Main
191        | Element::Header
192        | Element::Footer
193        | Element::Section
194        | Element::Form
195        | Element::Span
196        | Element::Input { .. }
197        | Element::Button
198        | Element::Anchor { .. }
199        | Element::Heading { .. }
200        | Element::UnorderedList
201        | Element::OrderedList
202        | Element::ListItem
203        | Element::Table
204        | Element::THead
205        | Element::TH
206        | Element::TBody
207        | Element::TR
208        | Element::TD
209        | Element::Canvas => {}
210    }
211
212    let is_grid = is_grid_container(container);
213    let is_flex = !is_grid && container.is_flex_container();
214
215    if is_flex {
216        write_css_attr!(b"display", b"flex");
217
218        if container.direction == LayoutDirection::Column {
219            write_css_attr!(b"flex-direction", b"column");
220        }
221    } else if is_grid {
222        write_css_attr!(b"display", b"grid");
223    }
224
225    match container.overflow_x {
226        LayoutOverflow::Auto => {
227            write_css_attr!(b"overflow-x", b"auto");
228        }
229        LayoutOverflow::Scroll => {
230            write_css_attr!(b"overflow-x", b"scroll");
231        }
232        LayoutOverflow::Wrap { grid } => {
233            if grid {
234                if let Some(size) = &container.grid_cell_size {
235                    write_css_attr!(
236                        b"grid-template-columns",
237                        format!("repeat(auto-fill, {})", number_to_html_string(size, true))
238                            .as_bytes()
239                    );
240                }
241            } else {
242                write_css_attr!(b"flex-wrap", b"wrap");
243            }
244        }
245        LayoutOverflow::Hidden => {
246            write_css_attr!(b"overflow-x", b"hidden");
247        }
248        LayoutOverflow::Expand | LayoutOverflow::Squash => {}
249    }
250    match container.overflow_y {
251        LayoutOverflow::Auto => {
252            write_css_attr!(b"overflow-y", b"auto");
253        }
254        LayoutOverflow::Scroll => {
255            write_css_attr!(b"overflow-y", b"scroll");
256        }
257        LayoutOverflow::Wrap { grid } => {
258            if grid {
259                if let Some(size) = &container.grid_cell_size {
260                    write_css_attr!(
261                        b"grid-template-columns",
262                        format!("repeat(auto-fill, {})", number_to_html_string(size, true))
263                            .as_bytes()
264                    );
265                }
266            } else {
267                write_css_attr!(b"flex-wrap", b"wrap");
268            }
269        }
270        LayoutOverflow::Hidden => {
271            write_css_attr!(b"overflow-y", b"hidden");
272        }
273        LayoutOverflow::Expand | LayoutOverflow::Squash => {}
274    }
275
276    if let Some(position) = container.position {
277        match position {
278            Position::Relative => {
279                write_css_attr!(b"position", b"relative");
280            }
281            Position::Absolute => {
282                write_css_attr!(b"position", b"absolute");
283                if container.top.is_none() && container.bottom.is_none() {
284                    write_css_attr!(b"top", b"0");
285                }
286                if container.left.is_none() && container.right.is_none() {
287                    write_css_attr!(b"left", b"0");
288                }
289            }
290            Position::Fixed => {
291                write_css_attr!(b"position", b"fixed");
292                if container.top.is_none() && container.bottom.is_none() {
293                    write_css_attr!(b"top", b"0");
294                }
295                if container.left.is_none() && container.right.is_none() {
296                    write_css_attr!(b"left", b"0");
297                }
298            }
299            Position::Sticky => {
300                write_css_attr!(b"position", b"sticky");
301            }
302            Position::Static => {
303                write_css_attr!(b"position", b"static");
304            }
305        }
306    }
307
308    if let Some(margin_left) = &container.margin_left {
309        write_css_attr!(
310            b"margin-left",
311            number_to_html_string(margin_left, true).as_bytes(),
312        );
313    }
314    if let Some(margin_right) = &container.margin_right {
315        write_css_attr!(
316            b"margin-right",
317            number_to_html_string(margin_right, true).as_bytes(),
318        );
319    }
320    if let Some(margin_top) = &container.margin_top {
321        write_css_attr!(
322            b"margin-top",
323            number_to_html_string(margin_top, true).as_bytes(),
324        );
325    }
326    if let Some(margin_bottom) = &container.margin_bottom {
327        write_css_attr!(
328            b"margin-bottom",
329            number_to_html_string(margin_bottom, true).as_bytes(),
330        );
331    }
332
333    if let Some(padding_left) = &container.padding_left {
334        write_css_attr!(
335            b"padding-left",
336            number_to_html_string(padding_left, true).as_bytes(),
337        );
338    }
339    if let Some(padding_right) = &container.padding_right {
340        write_css_attr!(
341            b"padding-right",
342            number_to_html_string(padding_right, true).as_bytes(),
343        );
344    }
345    if let Some(padding_top) = &container.padding_top {
346        write_css_attr!(
347            b"padding-top",
348            number_to_html_string(padding_top, true).as_bytes(),
349        );
350    }
351    if let Some(padding_bottom) = &container.padding_bottom {
352        write_css_attr!(
353            b"padding-bottom",
354            number_to_html_string(padding_bottom, true).as_bytes(),
355        );
356    }
357
358    if let Some(left) = &container.left {
359        write_css_attr!(b"left", number_to_html_string(left, true).as_bytes());
360    }
361    if let Some(right) = &container.right {
362        write_css_attr!(b"right", number_to_html_string(right, true).as_bytes());
363    }
364    if let Some(top) = &container.top {
365        write_css_attr!(b"top", number_to_html_string(top, true).as_bytes());
366    }
367    if let Some(bottom) = &container.bottom {
368        write_css_attr!(b"bottom", number_to_html_string(bottom, true).as_bytes());
369    }
370
371    let mut printed_transform_start = false;
372
373    macro_rules! write_transform_attr {
374        ($key:expr, $value:expr $(,)?) => {{
375            if !printed_transform_start {
376                printed_transform_start = true;
377                f.write_all(b"transform:")?;
378            } else {
379                f.write_all(b" ")?;
380            }
381            f.write_all($key)?;
382            f.write_all(b"(")?;
383            f.write_all($value)?;
384            f.write_all(b")")?;
385        }};
386    }
387
388    if let Some(translate) = &container.translate_x {
389        write_transform_attr!(
390            b"translateX",
391            number_to_html_string(translate, true).as_bytes()
392        );
393    }
394    if let Some(translate) = &container.translate_y {
395        write_transform_attr!(
396            b"translateY",
397            number_to_html_string(translate, true).as_bytes()
398        );
399    }
400
401    if printed_transform_start {
402        f.write_all(b";")?;
403    }
404
405    if let Some(visibility) = container.visibility {
406        match visibility {
407            Visibility::Visible => {}
408            Visibility::Hidden => {
409                write_css_attr!(b"visibility", b"hidden");
410            }
411        }
412    }
413
414    if container.hidden == Some(true) {
415        write_css_attr!(b"display", b"none");
416    }
417
418    if let Some(justify_content) = container.justify_content {
419        match justify_content {
420            JustifyContent::Start => {
421                write_css_attr!(b"justify-content", b"start");
422            }
423            JustifyContent::Center => {
424                write_css_attr!(b"justify-content", b"center");
425            }
426            JustifyContent::End => {
427                write_css_attr!(b"justify-content", b"end");
428            }
429            JustifyContent::SpaceBetween => {
430                write_css_attr!(b"justify-content", b"space-between");
431            }
432            JustifyContent::SpaceEvenly => {
433                write_css_attr!(b"justify-content", b"space-evenly");
434            }
435        }
436    }
437
438    if let Some(align_items) = container.align_items {
439        match align_items {
440            AlignItems::Start => {
441                write_css_attr!(b"align-items", b"start");
442            }
443            AlignItems::Center => {
444                write_css_attr!(b"align-items", b"center");
445            }
446            AlignItems::End => {
447                write_css_attr!(b"align-items", b"end");
448            }
449        }
450    }
451
452    if let Some(gap) = &container.column_gap {
453        write_css_attr!(
454            if is_grid {
455                b"grid-column-gap"
456            } else {
457                b"column-gap"
458            },
459            number_to_html_string(gap, true).as_bytes()
460        );
461    }
462    if let Some(gap) = &container.row_gap {
463        write_css_attr!(
464            if is_grid { b"grid-row-gap" } else { b"row-gap" },
465            number_to_html_string(gap, true).as_bytes()
466        );
467    }
468
469    if let Some(width) = &container.width {
470        write_css_attr!(b"width", number_to_html_string(width, true).as_bytes());
471    }
472    if let Some(height) = &container.height {
473        write_css_attr!(b"height", number_to_html_string(height, true).as_bytes());
474    }
475
476    if let Some(width) = &container.min_width {
477        write_css_attr!(b"min-width", number_to_html_string(width, true).as_bytes());
478    }
479    if let Some(width) = &container.max_width {
480        write_css_attr!(b"max-width", number_to_html_string(width, true).as_bytes());
481    }
482    if let Some(height) = &container.min_height {
483        write_css_attr!(
484            b"min-height",
485            number_to_html_string(height, true).as_bytes()
486        );
487    }
488    if let Some(height) = &container.max_height {
489        write_css_attr!(
490            b"max-height",
491            number_to_html_string(height, true).as_bytes()
492        );
493    }
494
495    if let Some(flex) = &container.flex {
496        write_css_attr!(
497            b"flex-grow",
498            number_to_html_string(&flex.grow, false).as_bytes()
499        );
500        write_css_attr!(
501            b"flex-shrink",
502            number_to_html_string(&flex.shrink, false).as_bytes()
503        );
504        write_css_attr!(
505            b"flex-basis",
506            number_to_html_string(&flex.basis, false).as_bytes()
507        );
508    }
509
510    if let Some(background) = container.background {
511        write_css_attr!(b"background", color_to_css_string(background).as_bytes());
512    }
513
514    if let Some((color, size)) = &container.border_top {
515        write_css_attr!(
516            b"border-top",
517            &[
518                number_to_html_string(size, true).as_bytes(),
519                b" solid ",
520                color_to_css_string(*color).as_bytes(),
521            ]
522            .concat(),
523        );
524    }
525
526    if let Some((color, size)) = &container.border_right {
527        write_css_attr!(
528            b"border-right",
529            &[
530                number_to_html_string(size, true).as_bytes(),
531                b" solid ",
532                color_to_css_string(*color).as_bytes(),
533            ]
534            .concat(),
535        );
536    }
537
538    if let Some((color, size)) = &container.border_bottom {
539        write_css_attr!(
540            b"border-bottom",
541            &[
542                number_to_html_string(size, true).as_bytes(),
543                b" solid ",
544                color_to_css_string(*color).as_bytes(),
545            ]
546            .concat(),
547        );
548    }
549
550    if let Some((color, size)) = &container.border_left {
551        write_css_attr!(
552            b"border-left",
553            &[
554                number_to_html_string(size, true).as_bytes(),
555                b" solid ",
556                color_to_css_string(*color).as_bytes(),
557            ]
558            .concat(),
559        );
560    }
561
562    if let Some(radius) = &container.border_top_left_radius {
563        write_css_attr!(
564            b"border-top-left-radius",
565            number_to_html_string(radius, true).as_bytes(),
566        );
567    }
568
569    if let Some(radius) = &container.border_top_right_radius {
570        write_css_attr!(
571            b"border-top-right-radius",
572            number_to_html_string(radius, true).as_bytes(),
573        );
574    }
575
576    if let Some(radius) = &container.border_bottom_left_radius {
577        write_css_attr!(
578            b"border-bottom-left-radius",
579            number_to_html_string(radius, true).as_bytes(),
580        );
581    }
582
583    if let Some(radius) = &container.border_bottom_right_radius {
584        write_css_attr!(
585            b"border-bottom-right-radius",
586            number_to_html_string(radius, true).as_bytes(),
587        );
588    }
589
590    if let Some(font_size) = &container.font_size {
591        write_css_attr!(
592            b"font-size",
593            number_to_html_string(font_size, true).as_bytes(),
594        );
595    }
596
597    if let Some(color) = &container.color {
598        write_css_attr!(b"color", color_to_css_string(*color).as_bytes(),);
599    }
600
601    if let Some(text_align) = &container.text_align {
602        write_css_attr!(
603            b"text-align",
604            match text_align {
605                TextAlign::Start => b"start",
606                TextAlign::Center => b"center",
607                TextAlign::End => b"end",
608                TextAlign::Justify => b"justify",
609            }
610        );
611    }
612
613    if let Some(text_decoration) = &container.text_decoration {
614        if let Some(color) = text_decoration.color {
615            write_css_attr!(
616                b"text-decoration-color",
617                color_to_css_string(color).as_bytes()
618            );
619        }
620        if !text_decoration.line.is_empty() {
621            write_css_attr!(
622                b"text-decoration-line",
623                text_decoration
624                    .line
625                    .iter()
626                    .map(|x| match x {
627                        TextDecorationLine::Inherit => "inherit",
628                        TextDecorationLine::None => "none",
629                        TextDecorationLine::Underline => "underline",
630                        TextDecorationLine::Overline => "overline",
631                        TextDecorationLine::LineThrough => "line-through",
632                    })
633                    .collect::<Vec<_>>()
634                    .join(" ")
635                    .as_bytes()
636            );
637        }
638        if let Some(style) = text_decoration.style {
639            write_css_attr!(
640                b"text-decoration-style",
641                match style {
642                    TextDecorationStyle::Inherit => b"inherit",
643                    TextDecorationStyle::Solid => b"solid",
644                    TextDecorationStyle::Double => b"double",
645                    TextDecorationStyle::Dotted => b"dotted",
646                    TextDecorationStyle::Dashed => b"dashed",
647                    TextDecorationStyle::Wavy => b"wavy",
648                }
649            );
650        }
651
652        if let Some(thickness) = &text_decoration.thickness {
653            write_css_attr!(
654                b"text-decoration-thickness",
655                number_to_html_string(thickness, false).as_bytes()
656            );
657        }
658    }
659
660    if let Some(font_family) = &container.font_family {
661        write_css_attr!(b"font-family", font_family.join(",").as_bytes());
662    }
663
664    if let Some(cursor) = &container.cursor {
665        write_css_attr!(
666            b"cursor",
667            match cursor {
668                Cursor::Auto => b"auto",
669                Cursor::Pointer => b"pointer",
670                Cursor::Text => b"text",
671                Cursor::Crosshair => b"crosshair",
672                Cursor::Move => b"move",
673                Cursor::NotAllowed => b"not-allowed",
674                Cursor::NoDrop => b"no-drop",
675                Cursor::Grab => b"grab",
676                Cursor::Grabbing => b"grabbing",
677                Cursor::AllScroll => b"all-scroll",
678                Cursor::ColResize => b"col-resize",
679                Cursor::RowResize => b"row-resize",
680                Cursor::NResize => b"n-resize",
681                Cursor::EResize => b"e-resize",
682                Cursor::SResize => b"s-resize",
683                Cursor::WResize => b"w-resize",
684                Cursor::NeResize => b"ne-resize",
685                Cursor::NwResize => b"nw-resize",
686                Cursor::SeResize => b"se-resize",
687                Cursor::SwResize => b"sw-resize",
688                Cursor::EwResize => b"ew-resize",
689                Cursor::NsResize => b"ns-resize",
690                Cursor::NeswResize => b"nesw-resize",
691                Cursor::ZoomIn => b"zoom-in",
692                Cursor::ZoomOut => b"zoom-out",
693            }
694        );
695    }
696
697    if printed_start {
698        f.write_all(b"\"")?;
699    }
700
701    Ok(())
702}
703
704/// # Errors
705///
706/// * If there were any IO errors writing the element style attribute
707#[allow(clippy::too_many_lines)]
708#[allow(clippy::cognitive_complexity)]
709pub fn element_classes_to_html(
710    f: &mut dyn Write,
711    container: &Container,
712) -> Result<(), std::io::Error> {
713    let mut printed_start = false;
714
715    if container.element == Element::Button {
716        if !printed_start {
717            printed_start = true;
718            f.write_all(b" class=\"")?;
719        }
720        f.write_all(b"remove-button-styles")?;
721    }
722
723    if !container.classes.is_empty() {
724        if printed_start {
725            f.write_all(b" ")?;
726        } else {
727            printed_start = true;
728            f.write_all(b" class=\"")?;
729        }
730
731        for class in &container.classes {
732            f.write_all(class.as_bytes())?;
733            f.write_all(b" ")?;
734        }
735    }
736
737    if printed_start {
738        f.write_all(b"\"")?;
739    }
740
741    Ok(())
742}
743
744/// # Errors
745///
746/// * If there were any IO errors writing the element as HTML
747#[allow(clippy::too_many_lines)]
748pub fn element_to_html(
749    f: &mut dyn Write,
750    container: &Container,
751    tag_renderer: &dyn HtmlTagRenderer,
752    is_flex_child: bool,
753) -> Result<(), std::io::Error> {
754    if container.debug == Some(true) {
755        log::info!("element_to_html: DEBUG {container}");
756    }
757
758    match &container.element {
759        Element::Raw { value } => {
760            f.write_all(value.as_bytes())?;
761            return Ok(());
762        }
763        Element::Image {
764            source,
765            alt,
766            source_set,
767            sizes,
768            loading,
769            ..
770        } => {
771            const TAG_NAME: &[u8] = b"img";
772            f.write_all(b"<")?;
773            f.write_all(TAG_NAME)?;
774            if let Some(source) = source {
775                f.write_all(b" src=\"")?;
776                f.write_all(source.as_bytes())?;
777                f.write_all(b"\"")?;
778            }
779            if let Some(srcset) = source_set {
780                f.write_all(b" srcset=\"")?;
781                f.write_all(srcset.as_bytes())?;
782                f.write_all(b"\"")?;
783            }
784            if let Some(sizes) = sizes {
785                f.write_all(b" sizes=\"")?;
786                f.write_all(number_to_html_string(sizes, true).as_bytes())?;
787                f.write_all(b"\"")?;
788            }
789            if let Some(alt) = alt {
790                f.write_all(b" alt=\"")?;
791                f.write_all(alt.as_bytes())?;
792                f.write_all(b"\"")?;
793            }
794            if let Some(loading) = loading {
795                f.write_all(b" loading=\"")?;
796                f.write_all(match loading {
797                    ImageLoading::Eager => b"eager",
798                    ImageLoading::Lazy => b"lazy",
799                })?;
800                f.write_all(b"\"")?;
801            }
802            tag_renderer.element_attrs_to_html(f, container, is_flex_child)?;
803            f.write_all(b">")?;
804            elements_to_html(
805                f,
806                &container.children,
807                tag_renderer,
808                container.is_flex_container(),
809            )?;
810            f.write_all(b"</")?;
811            f.write_all(TAG_NAME)?;
812            f.write_all(b">")?;
813            return Ok(());
814        }
815        Element::Anchor { href, target } => {
816            const TAG_NAME: &[u8] = b"a";
817            f.write_all(b"<")?;
818            f.write_all(TAG_NAME)?;
819            if let Some(href) = href {
820                f.write_all(b" href=\"")?;
821                f.write_all(href.as_bytes())?;
822                f.write_all(b"\"")?;
823            }
824            if let Some(target) = target {
825                f.write_all(b" target=\"")?;
826                f.write_all(match target {
827                    LinkTarget::SelfTarget => b"_self",
828                    LinkTarget::Blank => b"_blank",
829                    LinkTarget::Parent => b"_parent",
830                    LinkTarget::Top => b"_top",
831                    LinkTarget::Custom(target) => target.as_bytes(),
832                })?;
833                f.write_all(b"\"")?;
834            }
835            tag_renderer.element_attrs_to_html(f, container, is_flex_child)?;
836            f.write_all(b">")?;
837            elements_to_html(
838                f,
839                &container.children,
840                tag_renderer,
841                container.is_flex_container(),
842            )?;
843            f.write_all(b"</")?;
844            f.write_all(TAG_NAME)?;
845            f.write_all(b">")?;
846            return Ok(());
847        }
848        Element::Heading { size } => {
849            let tag_name = match size {
850                HeaderSize::H1 => b"h1",
851                HeaderSize::H2 => b"h2",
852                HeaderSize::H3 => b"h3",
853                HeaderSize::H4 => b"h4",
854                HeaderSize::H5 => b"h5",
855                HeaderSize::H6 => b"h6",
856            };
857            f.write_all(b"<")?;
858            f.write_all(tag_name)?;
859            tag_renderer.element_attrs_to_html(f, container, is_flex_child)?;
860            f.write_all(b">")?;
861            elements_to_html(
862                f,
863                &container.children,
864                tag_renderer,
865                container.is_flex_container(),
866            )?;
867            f.write_all(b"</")?;
868            f.write_all(tag_name)?;
869            f.write_all(b">")?;
870            return Ok(());
871        }
872        Element::Input { input } => {
873            const TAG_NAME: &[u8] = b"input";
874            f.write_all(b"<")?;
875            f.write_all(TAG_NAME)?;
876            match input {
877                Input::Checkbox { checked } => {
878                    f.write_all(b" type=\"checkbox\"")?;
879                    if *checked == Some(true) {
880                        f.write_all(b" checked=\"checked\"")?;
881                    }
882                }
883                Input::Text { value, placeholder } => {
884                    f.write_all(b" type=\"text\"")?;
885                    if let Some(value) = value {
886                        f.write_all(b" value=\"")?;
887                        f.write_all(value.as_bytes())?;
888                        f.write_all(b"\"")?;
889                    }
890                    if let Some(placeholder) = placeholder {
891                        f.write_all(b" placeholder=\"")?;
892                        f.write_all(placeholder.as_bytes())?;
893                        f.write_all(b"\"")?;
894                    }
895                }
896                Input::Password { value, placeholder } => {
897                    f.write_all(b" type=\"password\"")?;
898                    if let Some(value) = value {
899                        f.write_all(b" value=\"")?;
900                        f.write_all(value.as_bytes())?;
901                        f.write_all(b"\"")?;
902                    }
903                    if let Some(placeholder) = placeholder {
904                        f.write_all(b" placeholder=\"")?;
905                        f.write_all(placeholder.as_bytes())?;
906                        f.write_all(b"\"")?;
907                    }
908                }
909            }
910            tag_renderer.element_attrs_to_html(f, container, is_flex_child)?;
911            f.write_all(b"></")?;
912            f.write_all(TAG_NAME)?;
913            f.write_all(b">")?;
914            return Ok(());
915        }
916        _ => {}
917    }
918
919    let tag_name = match &container.element {
920        Element::Div => Some("div"),
921        Element::Aside => Some("aside"),
922        Element::Main => Some("main"),
923        Element::Header => Some("header"),
924        Element::Footer => Some("footer"),
925        Element::Section => Some("section"),
926        Element::Form => Some("form"),
927        Element::Span => Some("span"),
928        Element::Button => Some("button"),
929        Element::UnorderedList => Some("ul"),
930        Element::OrderedList => Some("ol"),
931        Element::ListItem => Some("li"),
932        Element::Table => Some("table"),
933        Element::THead => Some("thead"),
934        Element::TH => Some("th"),
935        Element::TBody => Some("tbody"),
936        Element::TR => Some("tr"),
937        Element::TD => Some("td"),
938        Element::Canvas => Some("canvas"),
939        _ => None,
940    };
941
942    if let Some(tag_name) = tag_name {
943        f.write_all(b"<")?;
944        f.write_all(tag_name.as_bytes())?;
945        tag_renderer.element_attrs_to_html(f, container, is_flex_child)?;
946        f.write_all(b">")?;
947        elements_to_html(
948            f,
949            &container.children,
950            tag_renderer,
951            container.is_flex_container(),
952        )?;
953        f.write_all(b"</")?;
954        f.write_all(tag_name.as_bytes())?;
955        f.write_all(b">")?;
956    }
957
958    Ok(())
959}
960
961/// # Errors
962///
963/// * If there were any IO errors writing the `Container` as HTML
964pub fn container_element_to_html(
965    container: &Container,
966    tag_renderer: &dyn HtmlTagRenderer,
967) -> Result<String, std::io::Error> {
968    let mut buffer = vec![];
969
970    elements_to_html(
971        &mut buffer,
972        &container.children,
973        tag_renderer,
974        container.is_flex_container(),
975    )?;
976
977    Ok(std::str::from_utf8(&buffer)
978        .map_err(std::io::Error::other)?
979        .to_string())
980}
981
982/// # Errors
983///
984/// * If there were any IO errors writing the `Container` as an HTML response
985#[allow(clippy::similar_names, clippy::implicit_hasher)]
986pub fn container_element_to_html_response(
987    headers: &HashMap<String, String>,
988    container: &Container,
989    viewport: Option<&str>,
990    background: Option<Color>,
991    title: Option<&str>,
992    description: Option<&str>,
993    tag_renderer: &dyn HtmlTagRenderer,
994) -> Result<String, std::io::Error> {
995    Ok(tag_renderer.root_html(
996        headers,
997        container,
998        container_element_to_html(container, tag_renderer)?,
999        viewport,
1000        background,
1001        title,
1002        description,
1003    ))
1004}