1extern crate alloc;
4
5use alloc::{borrow::Cow, string::String, vec::Vec};
6use std::io::Write;
7
8use facet_core::Facet;
9use facet_format::{FieldOrdering, FormatSerializer, ScalarValue, SerializeError, serialize_root};
10use facet_reflect::Peek;
11
12pub type FloatFormatter = fn(f64, &mut dyn Write) -> std::io::Result<()>;
14
15const VOID_ELEMENTS: &[&str] = &[
17 "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
18 "track", "wbr",
19];
20
21const BOOLEAN_ATTRIBUTES: &[&str] = &[
23 "allowfullscreen",
24 "async",
25 "autofocus",
26 "autoplay",
27 "checked",
28 "controls",
29 "default",
30 "defer",
31 "disabled",
32 "formnovalidate",
33 "hidden",
34 "inert",
35 "ismap",
36 "itemscope",
37 "loop",
38 "multiple",
39 "muted",
40 "nomodule",
41 "novalidate",
42 "open",
43 "playsinline",
44 "readonly",
45 "required",
46 "reversed",
47 "selected",
48 "shadowrootclonable",
49 "shadowrootdelegatesfocus",
50 "shadowrootserializable",
51];
52
53#[derive(Clone)]
55pub struct SerializeOptions {
56 pub pretty: bool,
58 pub indent: Cow<'static, str>,
60 pub float_formatter: Option<FloatFormatter>,
62 pub self_closing_void: bool,
65}
66
67impl Default for SerializeOptions {
68 fn default() -> Self {
69 Self {
70 pretty: false,
71 indent: Cow::Borrowed(" "),
72 float_formatter: None,
73 self_closing_void: false,
74 }
75 }
76}
77
78impl core::fmt::Debug for SerializeOptions {
79 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80 f.debug_struct("SerializeOptions")
81 .field("pretty", &self.pretty)
82 .field("indent", &self.indent)
83 .field("float_formatter", &self.float_formatter.map(|_| "..."))
84 .field("self_closing_void", &self.self_closing_void)
85 .finish()
86 }
87}
88
89impl SerializeOptions {
90 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub fn pretty(mut self) -> Self {
97 self.pretty = true;
98 self
99 }
100
101 pub fn indent(mut self, indent: impl Into<Cow<'static, str>>) -> Self {
103 self.indent = indent.into();
104 self.pretty = true;
105 self
106 }
107
108 pub fn float_formatter(mut self, formatter: FloatFormatter) -> Self {
110 self.float_formatter = Some(formatter);
111 self
112 }
113
114 pub fn self_closing_void(mut self, value: bool) -> Self {
116 self.self_closing_void = value;
117 self
118 }
119}
120
121#[derive(Debug)]
123pub struct HtmlSerializeError {
124 msg: &'static str,
125}
126
127impl core::fmt::Display for HtmlSerializeError {
128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129 f.write_str(self.msg)
130 }
131}
132
133impl std::error::Error for HtmlSerializeError {}
134
135#[derive(Debug)]
136enum Ctx {
137 Root,
138 Struct {
139 close: Option<String>,
140 has_content: bool,
142 has_block_content: bool,
144 },
145 Seq {
146 close: Option<String>,
147 },
148}
149
150pub struct HtmlSerializer {
152 out: Vec<u8>,
153 stack: Vec<Ctx>,
154 pending_field: Option<String>,
155 pending_is_attribute: bool,
157 pending_is_text: bool,
159 pending_is_elements: bool,
161 pending_attributes: Vec<(String, String)>,
163 root_tag_written: bool,
165 root_element_name: Option<String>,
167 deferred_open_tag: Option<(String, String)>,
169 elements_stack: Vec<bool>,
171 skip_enum_wrapper: Option<String>,
176 options: SerializeOptions,
178 depth: usize,
180}
181
182impl HtmlSerializer {
183 pub fn new() -> Self {
185 Self::with_options(SerializeOptions::default())
186 }
187
188 pub fn with_options(options: SerializeOptions) -> Self {
190 Self {
191 out: Vec::new(),
192 stack: vec![Ctx::Root],
193 pending_field: None,
194 pending_is_attribute: false,
195 pending_is_text: false,
196 pending_is_elements: false,
197 pending_attributes: Vec::new(),
198 root_tag_written: false,
199 root_element_name: None,
200 deferred_open_tag: None,
201 elements_stack: Vec::new(),
202 skip_enum_wrapper: None,
203 options,
204 depth: 0,
205 }
206 }
207
208 pub fn finish(mut self) -> Vec<u8> {
210 self.flush_deferred_open_tag();
212
213 while let Some(ctx) = self.stack.pop() {
215 match ctx {
216 Ctx::Root => break,
217 Ctx::Struct {
218 close,
219 has_block_content,
220 ..
221 } => {
222 if let Some(name) = close
223 && !is_void_element(&name)
224 {
225 self.write_close_tag(&name, has_block_content);
226 }
227 }
228 Ctx::Seq { close } => {
229 if let Some(name) = close
230 && !is_void_element(&name)
231 {
232 self.write_close_tag(&name, true);
233 }
234 }
235 }
236 }
237
238 self.out
239 }
240
241 fn flush_deferred_open_tag_with_mode(&mut self, inline: bool) {
247 if let Some((element_name, _close_name)) = self.deferred_open_tag.take() {
248 self.write_indent();
249 self.out.push(b'<');
250 self.out.extend_from_slice(element_name.as_bytes());
251
252 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
254 for (attr_name, attr_value) in attrs {
255 if is_boolean_attribute(&attr_name) {
257 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
258 self.out.push(b' ');
259 self.out.extend_from_slice(attr_name.as_bytes());
260 }
261 continue;
263 }
264
265 self.out.push(b' ');
266 self.out.extend_from_slice(attr_name.as_bytes());
267 self.out.extend_from_slice(b"=\"");
268 self.write_attr_escaped(&attr_value);
269 self.out.push(b'"');
270 }
271
272 if is_void_element(&element_name) {
273 if self.options.self_closing_void {
274 self.out.extend_from_slice(b" />");
275 } else {
276 self.out.push(b'>');
277 }
278 } else {
279 self.out.push(b'>');
280 }
281
282 if !inline {
284 self.write_newline();
285 self.depth += 1;
286 }
287
288 if self.root_element_name.as_deref() == Some(&element_name) {
290 self.root_tag_written = true;
291 }
292 }
293 }
294
295 fn flush_deferred_open_tag(&mut self) {
296 self.flush_deferred_open_tag_with_mode(false)
297 }
298
299 fn write_open_tag(&mut self, name: &str) {
300 self.write_indent();
301 self.out.push(b'<');
302 self.out.extend_from_slice(name.as_bytes());
303
304 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
306 for (attr_name, attr_value) in attrs {
307 if is_boolean_attribute(&attr_name) {
309 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
310 self.out.push(b' ');
311 self.out.extend_from_slice(attr_name.as_bytes());
312 }
313 continue;
315 }
316
317 self.out.push(b' ');
318 self.out.extend_from_slice(attr_name.as_bytes());
319 self.out.extend_from_slice(b"=\"");
320 self.write_attr_escaped(&attr_value);
321 self.out.push(b'"');
322 }
323
324 if is_void_element(name) {
325 if self.options.self_closing_void {
326 self.out.extend_from_slice(b" />");
327 } else {
328 self.out.push(b'>');
329 }
330 } else {
331 self.out.push(b'>');
332 }
333 }
334
335 fn write_close_tag_ex(&mut self, name: &str, indent_before: bool, newline_after: bool) {
340 if is_void_element(name) {
341 return; }
343 if indent_before {
344 self.depth = self.depth.saturating_sub(1);
345 self.write_indent();
346 }
347 self.out.extend_from_slice(b"</");
348 self.out.extend_from_slice(name.as_bytes());
349 self.out.push(b'>');
350 if newline_after {
351 self.write_newline();
352 }
353 }
354
355 fn write_close_tag(&mut self, name: &str, block: bool) {
356 self.write_close_tag_ex(name, block, block)
357 }
358
359 fn write_text_escaped(&mut self, text: &str) {
360 for b in text.as_bytes() {
361 match *b {
362 b'&' => self.out.extend_from_slice(b"&"),
363 b'<' => self.out.extend_from_slice(b"<"),
364 b'>' => self.out.extend_from_slice(b">"),
365 _ => self.out.push(*b),
366 }
367 }
368 }
369
370 fn write_attr_escaped(&mut self, text: &str) {
371 for b in text.as_bytes() {
372 match *b {
373 b'&' => self.out.extend_from_slice(b"&"),
374 b'<' => self.out.extend_from_slice(b"<"),
375 b'>' => self.out.extend_from_slice(b">"),
376 b'"' => self.out.extend_from_slice(b"""),
377 _ => self.out.push(*b),
378 }
379 }
380 }
381
382 fn format_float(&self, v: f64) -> String {
383 if let Some(fmt) = self.options.float_formatter {
384 let mut buf = Vec::new();
385 if fmt(v, &mut buf).is_ok()
386 && let Ok(s) = String::from_utf8(buf)
387 {
388 return s;
389 }
390 }
391 #[cfg(feature = "fast")]
392 return zmij::Buffer::new().format(v).to_string();
393 #[cfg(not(feature = "fast"))]
394 v.to_string()
395 }
396
397 fn write_indent(&mut self) {
398 if self.options.pretty {
399 for _ in 0..self.depth {
400 self.out.extend_from_slice(self.options.indent.as_bytes());
401 }
402 }
403 }
404
405 fn write_newline(&mut self) {
406 if self.options.pretty {
407 self.out.push(b'\n');
408 }
409 }
410
411 fn ensure_root_tag_written(&mut self) {
412 if !self.root_tag_written {
413 let root_name = self
414 .root_element_name
415 .as_deref()
416 .unwrap_or("div")
417 .to_string();
418 self.out.push(b'<');
419 self.out.extend_from_slice(root_name.as_bytes());
420
421 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
423 for (attr_name, attr_value) in attrs {
424 if is_boolean_attribute(&attr_name) {
425 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
426 self.out.push(b' ');
427 self.out.extend_from_slice(attr_name.as_bytes());
428 }
429 continue;
430 }
431
432 self.out.push(b' ');
433 self.out.extend_from_slice(attr_name.as_bytes());
434 self.out.extend_from_slice(b"=\"");
435 self.write_attr_escaped(&attr_value);
436 self.out.push(b'"');
437 }
438
439 if is_void_element(&root_name) {
440 if self.options.self_closing_void {
441 self.out.extend_from_slice(b" />");
442 } else {
443 self.out.push(b'>');
444 }
445 } else {
446 self.out.push(b'>');
447 self.write_newline();
448 self.depth += 1;
449 }
450 self.root_tag_written = true;
451 }
452 }
453
454 fn open_value_element_if_needed(&mut self) -> Result<Option<String>, HtmlSerializeError> {
455 self.flush_deferred_open_tag();
456 self.ensure_root_tag_written();
457
458 if let Some(field_name) = self.pending_field.take() {
459 if self.elements_stack.last().copied().unwrap_or(false) {
461 self.write_open_tag(&field_name);
463 return Ok(Some(field_name));
464 }
465
466 if self.pending_is_text {
468 self.pending_is_text = false;
469 return Ok(None); }
471
472 if self.pending_is_attribute {
474 self.pending_is_attribute = false;
475 return Ok(None);
476 }
477
478 self.write_open_tag(&field_name);
480 return Ok(Some(field_name));
481 }
482 Ok(None)
483 }
484
485 fn write_scalar_string(&mut self, value: &str) -> Result<(), HtmlSerializeError> {
486 if self.pending_is_attribute
489 && let Some(attr_name) = self.pending_field.take()
490 {
491 self.pending_is_attribute = false;
492 self.pending_attributes.push((attr_name, value.to_string()));
493 return Ok(());
494 }
495
496 if self.pending_is_text {
498 self.flush_deferred_open_tag_with_mode(true);
500 self.pending_is_text = false;
501 self.pending_field.take();
502 self.write_text_escaped(value);
503
504 if let Some(Ctx::Struct { has_content, .. }) = self.stack.last_mut() {
506 *has_content = true;
507 }
508 return Ok(());
509 }
510
511 self.flush_deferred_open_tag();
513 self.ensure_root_tag_written();
514 let close = self.open_value_element_if_needed()?;
515 self.write_text_escaped(value);
516 if let Some(name) = close {
517 self.write_close_tag(&name, false);
518 }
519 self.write_newline();
520 Ok(())
521 }
522}
523
524impl Default for HtmlSerializer {
525 fn default() -> Self {
526 Self::new()
527 }
528}
529
530impl FormatSerializer for HtmlSerializer {
531 type Error = HtmlSerializeError;
532
533 fn struct_metadata(&mut self, shape: &facet_core::Shape) -> Result<(), Self::Error> {
534 let element_name = shape
536 .get_builtin_attr_value::<&str>("rename")
537 .unwrap_or(shape.type_identifier);
538
539 if matches!(self.stack.last(), Some(Ctx::Root)) {
541 self.root_element_name = Some(element_name.to_string());
542 }
543
544 if self.elements_stack.last() == Some(&true)
548 && self.pending_field.is_none()
549 && self.skip_enum_wrapper.is_none()
550 {
551 self.pending_field = Some(element_name.to_string());
552 }
553
554 Ok(())
555 }
556
557 fn field_metadata(&mut self, field_item: &facet_reflect::FieldItem) -> Result<(), Self::Error> {
558 if let Some(field) = field_item.field {
560 self.pending_is_attribute = field.is_attribute();
561 self.pending_is_text = field.is_text();
562 self.pending_is_elements = field.is_elements();
563 } else {
564 self.pending_is_attribute = true;
566 self.pending_is_text = false;
567 self.pending_is_elements = false;
568 }
569 Ok(())
570 }
571
572 fn variant_metadata(
573 &mut self,
574 variant: &'static facet_core::Variant,
575 ) -> Result<(), Self::Error> {
576 if self.elements_stack.last() == Some(&true) {
589 let element_name = variant
591 .get_builtin_attr("rename")
592 .and_then(|attr| attr.get_as::<&str>().copied())
593 .unwrap_or(variant.name);
594 self.pending_field = Some(element_name.to_string());
595 self.skip_enum_wrapper = Some(variant.name.to_string());
597 }
598 Ok(())
599 }
600
601 fn preferred_field_order(&self) -> FieldOrdering {
602 FieldOrdering::AttributesFirst
603 }
604
605 fn begin_struct(&mut self) -> Result<(), Self::Error> {
606 self.flush_deferred_open_tag();
608
609 for ctx in self.stack.iter_mut().rev() {
612 if let Ctx::Struct {
613 has_content,
614 has_block_content,
615 ..
616 } = ctx
617 {
618 *has_content = true;
619 *has_block_content = true;
620 break;
621 }
622 }
623
624 if self.skip_enum_wrapper.is_some() {
628 let in_elements = self.elements_stack.last().copied().unwrap_or(false);
630 self.elements_stack.push(in_elements);
631 self.stack.push(Ctx::Struct {
632 close: None,
633 has_content: false,
634 has_block_content: false,
635 });
636 return Ok(());
637 }
638
639 if self.pending_is_elements {
641 self.pending_is_elements = false;
642 self.elements_stack.push(true);
643 } else {
644 self.elements_stack.push(false);
645 }
646
647 match self.stack.last() {
648 Some(Ctx::Root) => {
649 if let Some(name) = self.root_element_name.clone() {
652 self.deferred_open_tag = Some((name.clone(), name));
653 }
654 self.stack.push(Ctx::Struct {
655 close: self.root_element_name.clone(),
656 has_content: false,
657 has_block_content: false,
658 });
659 Ok(())
660 }
661 Some(Ctx::Struct { .. }) | Some(Ctx::Seq { .. }) => {
662 let close = if let Some(field_name) = self.pending_field.take() {
664 self.deferred_open_tag = Some((field_name.clone(), field_name.clone()));
665 Some(field_name)
666 } else {
667 None
668 };
669 self.stack.push(Ctx::Struct {
670 close,
671 has_content: false,
672 has_block_content: false,
673 });
674 Ok(())
675 }
676 None => Err(HtmlSerializeError {
677 msg: "serializer state missing context",
678 }),
679 }
680 }
681
682 fn end_struct(&mut self) -> Result<(), Self::Error> {
683 self.elements_stack.pop();
684
685 if let Some(Ctx::Struct {
686 close,
687 has_content,
688 has_block_content,
689 }) = self.stack.pop()
690 {
691 self.flush_deferred_open_tag_with_mode(!has_content && !has_block_content);
694
695 if let Some(name) = close
696 && !is_void_element(&name)
697 {
698 let parent_is_block = matches!(
700 self.stack.last(),
701 Some(Ctx::Seq { .. })
702 | Some(Ctx::Struct {
703 has_block_content: true,
704 ..
705 })
706 );
707
708 self.write_close_tag_ex(
711 &name,
712 has_block_content,
713 has_block_content || parent_is_block,
714 );
715 }
716 }
717 Ok(())
718 }
719
720 fn begin_seq(&mut self) -> Result<(), Self::Error> {
721 if self.pending_is_elements {
724 self.pending_is_elements = false;
725 self.elements_stack.push(true);
726 self.pending_field.take(); self.stack.push(Ctx::Seq { close: None });
728 return Ok(());
729 }
730
731 self.flush_deferred_open_tag();
733 self.ensure_root_tag_written();
734
735 if let Some(Ctx::Struct {
737 has_content,
738 has_block_content,
739 ..
740 }) = self.stack.last_mut()
741 {
742 *has_content = true;
743 *has_block_content = true;
744 }
745
746 let close = if let Some(field_name) = self.pending_field.take() {
747 self.write_open_tag(&field_name);
748 self.write_newline();
749 self.depth += 1;
750 Some(field_name)
751 } else {
752 None
753 };
754 self.elements_stack.push(false);
755 self.stack.push(Ctx::Seq { close });
756 Ok(())
757 }
758
759 fn end_seq(&mut self) -> Result<(), Self::Error> {
760 self.elements_stack.pop();
761 if let Some(Ctx::Seq { close }) = self.stack.pop()
762 && let Some(name) = close
763 {
764 self.write_close_tag(&name, true);
765 }
766 Ok(())
767 }
768
769 fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
770 if let Some(ref variant_name) = self.skip_enum_wrapper
773 && key == variant_name
774 {
775 self.skip_enum_wrapper = None;
778 return Ok(());
779 }
780 self.pending_field = Some(key.to_string());
781 Ok(())
782 }
783
784 fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
785 match scalar {
786 ScalarValue::Null => {
787 self.pending_field.take();
789 self.pending_is_attribute = false;
790 self.pending_is_text = false;
791 Ok(())
792 }
793 ScalarValue::Bool(v) => {
794 if self.pending_is_attribute
796 && let Some(attr_name) = self.pending_field.take()
797 {
798 self.pending_is_attribute = false;
799 if v {
800 self.pending_attributes.push((attr_name.clone(), attr_name));
802 }
803 return Ok(());
805 }
806
807 self.write_scalar_string(if v { "true" } else { "false" })
808 }
809 ScalarValue::I64(v) => self.write_scalar_string(&v.to_string()),
810 ScalarValue::U64(v) => self.write_scalar_string(&v.to_string()),
811 ScalarValue::F64(v) => {
812 let s = self.format_float(v);
813 self.write_scalar_string(&s)
814 }
815 ScalarValue::Str(s) => self.write_scalar_string(&s),
816 ScalarValue::I128(v) => self.write_scalar_string(&v.to_string()),
817 ScalarValue::U128(v) => self.write_scalar_string(&v.to_string()),
818 ScalarValue::Bytes(_) => Err(HtmlSerializeError {
819 msg: "binary data cannot be serialized to HTML",
820 }),
821 }
822 }
823}
824
825fn is_void_element(name: &str) -> bool {
827 VOID_ELEMENTS.iter().any(|&v| v.eq_ignore_ascii_case(name))
828}
829
830fn is_boolean_attribute(name: &str) -> bool {
832 BOOLEAN_ATTRIBUTES
833 .iter()
834 .any(|&v| v.eq_ignore_ascii_case(name))
835}
836
837pub fn to_string<T: Facet<'static>>(
843 value: &T,
844) -> Result<String, SerializeError<HtmlSerializeError>> {
845 to_string_with_options(value, &SerializeOptions::default())
846}
847
848pub fn to_string_pretty<T: Facet<'static>>(
850 value: &T,
851) -> Result<String, SerializeError<HtmlSerializeError>> {
852 to_string_with_options(value, &SerializeOptions::default().pretty())
853}
854
855pub fn to_string_with_options<T: Facet<'static>>(
857 value: &T,
858 options: &SerializeOptions,
859) -> Result<String, SerializeError<HtmlSerializeError>> {
860 let bytes = to_vec_with_options(value, options)?;
861 String::from_utf8(bytes).map_err(|_| {
862 SerializeError::Reflect(facet_reflect::ReflectError::InvalidOperation {
863 operation: "to_string",
864 reason: "invalid UTF-8 in serialized output",
865 })
866 })
867}
868
869pub fn to_vec<T: Facet<'static>>(value: &T) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
871 to_vec_with_options(value, &SerializeOptions::default())
872}
873
874pub fn to_vec_with_options<T: Facet<'static>>(
876 value: &T,
877 options: &SerializeOptions,
878) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
879 let mut serializer = HtmlSerializer::with_options(options.clone());
880 let peek = Peek::new(value);
881 serialize_root(&mut serializer, peek)?;
882 Ok(serializer.finish())
883}
884
885#[cfg(test)]
886mod tests {
887 use super::*;
888 use facet::Facet;
889 use facet_xml as xml;
890
891 #[derive(Debug, Facet)]
892 #[facet(rename = "div")]
893 struct SimpleDiv {
894 #[facet(xml::attribute, default)]
895 class: Option<String>,
896 #[facet(xml::attribute, default)]
897 id: Option<String>,
898 #[facet(xml::text, default)]
899 text: String,
900 }
901
902 #[test]
903 fn test_simple_element() {
904 let div = SimpleDiv {
905 class: Some("container".into()),
906 id: Some("main".into()),
907 text: "Hello, World!".into(),
908 };
909
910 let html = to_string(&div).unwrap();
911 assert!(html.contains("<div"), "Expected <div, got: {}", html);
912 assert!(
913 html.contains("class=\"container\""),
914 "Expected class attr, got: {}",
915 html
916 );
917 assert!(
918 html.contains("id=\"main\""),
919 "Expected id attr, got: {}",
920 html
921 );
922 assert!(
923 html.contains("Hello, World!"),
924 "Expected text content, got: {}",
925 html
926 );
927 assert!(html.contains("</div>"), "Expected </div>, got: {}", html);
928 }
929
930 #[test]
931 fn test_pretty_print() {
932 let div = SimpleDiv {
934 class: Some("test".into()),
935 id: None,
936 text: "Content".into(),
937 };
938
939 let html = to_string_pretty(&div).unwrap();
940 assert_eq!(
941 html, "<div class=\"test\">Content</div>",
942 "Text-only elements should be inline"
943 );
944 }
945
946 #[test]
947 fn test_pretty_print_nested() {
948 let container = Container {
950 class: Some("outer".into()),
951 children: vec![
952 Child::P(Paragraph {
953 text: "First".into(),
954 }),
955 Child::P(Paragraph {
956 text: "Second".into(),
957 }),
958 ],
959 };
960
961 let html = to_string_pretty(&container).unwrap();
962 assert!(
963 html.contains('\n'),
964 "Expected newlines in pretty output: {}",
965 html
966 );
967 assert!(
968 html.contains(" <p>"),
969 "Expected indented child elements: {}",
970 html
971 );
972 }
973
974 #[derive(Debug, Facet)]
975 #[facet(rename = "img")]
976 struct Image {
977 #[facet(xml::attribute)]
978 src: String,
979 #[facet(xml::attribute, default)]
980 alt: Option<String>,
981 }
982
983 #[test]
984 fn test_void_element() {
985 let img = Image {
986 src: "photo.jpg".into(),
987 alt: Some("A photo".into()),
988 };
989
990 let html = to_string(&img).unwrap();
991 assert!(html.contains("<img"), "Expected <img, got: {}", html);
992 assert!(
993 html.contains("src=\"photo.jpg\""),
994 "Expected src attr, got: {}",
995 html
996 );
997 assert!(
998 html.contains("alt=\"A photo\""),
999 "Expected alt attr, got: {}",
1000 html
1001 );
1002 assert!(
1004 !html.contains("</img>"),
1005 "Should not have </img>, got: {}",
1006 html
1007 );
1008 }
1009
1010 #[test]
1011 fn test_void_element_self_closing() {
1012 let img = Image {
1013 src: "photo.jpg".into(),
1014 alt: None,
1015 };
1016
1017 let options = SerializeOptions::new().self_closing_void(true);
1018 let html = to_string_with_options(&img, &options).unwrap();
1019 assert!(html.contains("/>"), "Expected self-closing, got: {}", html);
1020 }
1021
1022 #[derive(Debug, Facet)]
1023 #[facet(rename = "input")]
1024 struct Input {
1025 #[facet(xml::attribute, rename = "type")]
1026 input_type: String,
1027 #[facet(xml::attribute, default)]
1028 disabled: Option<bool>,
1029 #[facet(xml::attribute, default)]
1030 checked: Option<bool>,
1031 }
1032
1033 #[test]
1034 fn test_boolean_attributes() {
1035 let input = Input {
1036 input_type: "checkbox".into(),
1037 disabled: Some(true),
1038 checked: Some(false),
1039 };
1040
1041 let html = to_string(&input).unwrap();
1042 assert!(
1043 html.contains("type=\"checkbox\""),
1044 "Expected type attr, got: {}",
1045 html
1046 );
1047 assert!(
1048 html.contains("disabled"),
1049 "Expected disabled attr, got: {}",
1050 html
1051 );
1052 assert!(
1054 !html.contains("checked"),
1055 "Should not have checked, got: {}",
1056 html
1057 );
1058 }
1059
1060 #[test]
1061 fn test_escape_special_chars() {
1062 let div = SimpleDiv {
1063 class: None,
1064 id: None,
1065 text: "<script>alert('xss')</script>".into(),
1066 };
1067
1068 let html = to_string(&div).unwrap();
1069 assert!(
1070 html.contains("<script>"),
1071 "Expected escaped script tag, got: {}",
1072 html
1073 );
1074 assert!(
1075 !html.contains("<script>"),
1076 "Should not have raw script tag, got: {}",
1077 html
1078 );
1079 }
1080
1081 #[derive(Debug, Facet)]
1083 #[facet(rename = "div")]
1084 struct Container {
1085 #[facet(xml::attribute, default)]
1086 class: Option<String>,
1087 #[facet(xml::elements, default)]
1088 children: Vec<Child>,
1089 }
1090
1091 #[derive(Debug, Facet)]
1092 #[repr(u8)]
1093 enum Child {
1094 #[facet(rename = "p")]
1095 P(#[expect(dead_code)] Paragraph),
1096 #[facet(rename = "span")]
1097 Span(#[expect(dead_code)] Span),
1098 }
1099
1100 #[derive(Debug, Facet)]
1101 struct Paragraph {
1102 #[facet(xml::text, default)]
1103 text: String,
1104 }
1105
1106 #[derive(Debug, Facet)]
1107 struct Span {
1108 #[facet(xml::attribute, default)]
1109 class: Option<String>,
1110 #[facet(xml::text, default)]
1111 text: String,
1112 }
1113
1114 #[test]
1115 fn test_nested_elements_with_enums() {
1116 let container = Container {
1117 class: Some("wrapper".into()),
1118 children: vec![
1119 Child::P(Paragraph {
1120 text: "Hello".into(),
1121 }),
1122 Child::Span(Span {
1123 class: Some("highlight".into()),
1124 text: "World".into(),
1125 }),
1126 ],
1127 };
1128
1129 let html = to_string(&container).unwrap();
1130 let expected =
1131 r#"<div class="wrapper"><p>Hello</p><span class="highlight">World</span></div>"#;
1132 assert_eq!(html, expected);
1133 }
1134
1135 #[test]
1136 fn test_nested_elements_pretty_print() {
1137 let container = Container {
1138 class: Some("wrapper".into()),
1139 children: vec![
1140 Child::P(Paragraph {
1141 text: "Hello".into(),
1142 }),
1143 Child::Span(Span {
1144 class: Some("highlight".into()),
1145 text: "World".into(),
1146 }),
1147 ],
1148 };
1149
1150 let html = to_string_pretty(&container).unwrap();
1151 let expected = "<div class=\"wrapper\">\n <p>Hello</p>\n <span class=\"highlight\">World</span>\n</div>\n";
1153 assert_eq!(html, expected);
1154 }
1155
1156 #[test]
1157 fn test_empty_container() {
1158 let container = Container {
1159 class: Some("empty".into()),
1160 children: vec![],
1161 };
1162
1163 let html = to_string(&container).unwrap();
1164 assert_eq!(html, r#"<div class="empty"></div>"#);
1165
1166 let html_pretty = to_string_pretty(&container).unwrap();
1167 assert_eq!(html_pretty, r#"<div class="empty"></div>"#);
1169 }
1170
1171 #[test]
1172 fn test_deeply_nested() {
1173 #[derive(Debug, Facet)]
1175 #[facet(rename = "article")]
1176 struct Article {
1177 #[facet(xml::elements, default)]
1178 sections: Vec<Section>,
1179 }
1180
1181 #[derive(Debug, Facet)]
1182 #[facet(rename = "section")]
1183 struct Section {
1184 #[facet(xml::attribute, default)]
1185 id: Option<String>,
1186 #[facet(xml::elements, default)]
1187 paragraphs: Vec<Para>,
1188 }
1189
1190 #[derive(Debug, Facet)]
1191 #[facet(rename = "p")]
1192 struct Para {
1193 #[facet(xml::text, default)]
1194 text: String,
1195 }
1196
1197 let article = Article {
1198 sections: vec![Section {
1199 id: Some("intro".into()),
1200 paragraphs: vec![
1201 Para {
1202 text: "First para".into(),
1203 },
1204 Para {
1205 text: "Second para".into(),
1206 },
1207 ],
1208 }],
1209 };
1210
1211 let html = to_string(&article).unwrap();
1212 assert_eq!(
1213 html,
1214 r#"<article><section id="intro"><p>First para</p><p>Second para</p></section></article>"#
1215 );
1216
1217 let html_pretty = to_string_pretty(&article).unwrap();
1218 assert_eq!(
1219 html_pretty,
1220 "<article>\n <section id=\"intro\">\n <p>First para</p>\n <p>Second para</p>\n </section>\n</article>\n"
1221 );
1222 }
1223
1224 #[test]
1225 fn test_event_handlers() {
1226 use crate::elements::{Button, GlobalAttrs};
1227
1228 let button = Button {
1229 attrs: GlobalAttrs {
1230 onclick: Some("handleClick()".into()),
1231 onmouseover: Some("highlight(this)".into()),
1232 ..Default::default()
1233 },
1234 type_: Some("button".into()),
1235 children: vec![crate::elements::PhrasingContent::Text("Click me".into())],
1236 ..Default::default()
1237 };
1238
1239 let html = to_string(&button).unwrap();
1240 assert!(
1241 html.contains(r#"onclick="handleClick()""#),
1242 "Expected onclick handler, got: {}",
1243 html
1244 );
1245 assert!(
1246 html.contains(r#"onmouseover="highlight(this)""#),
1247 "Expected onmouseover handler, got: {}",
1248 html
1249 );
1250 assert!(
1251 html.contains("Click me"),
1252 "Expected button text, got: {}",
1253 html
1254 );
1255 }
1256
1257 #[test]
1258 fn test_event_handlers_with_escaping() {
1259 use crate::elements::{Div, FlowContent, GlobalAttrs};
1260
1261 let div = Div {
1262 attrs: GlobalAttrs {
1263 onclick: Some(r#"alert("Hello \"World\"")"#.into()),
1264 ..Default::default()
1265 },
1266 children: vec![FlowContent::Text("Test".into())],
1267 };
1268
1269 let html = to_string(&div).unwrap();
1270 assert!(
1272 html.contains("onclick="),
1273 "Expected onclick attr, got: {}",
1274 html
1275 );
1276 assert!(
1277 html.contains("""),
1278 "Expected escaped quotes in onclick, got: {}",
1279 html
1280 );
1281 }
1282}