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
16pub 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
32pub 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
44pub 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
55pub 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#[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#[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#[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
961pub 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#[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}