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 WHITESPACE_SENSITIVE_ELEMENTS: &[&str] = &["pre", "code", "textarea", "script", "style"];
24
25const BOOLEAN_ATTRIBUTES: &[&str] = &[
27 "allowfullscreen",
28 "async",
29 "autofocus",
30 "autoplay",
31 "checked",
32 "controls",
33 "default",
34 "defer",
35 "disabled",
36 "formnovalidate",
37 "hidden",
38 "inert",
39 "ismap",
40 "itemscope",
41 "loop",
42 "multiple",
43 "muted",
44 "nomodule",
45 "novalidate",
46 "open",
47 "playsinline",
48 "readonly",
49 "required",
50 "reversed",
51 "selected",
52 "shadowrootclonable",
53 "shadowrootdelegatesfocus",
54 "shadowrootserializable",
55];
56
57#[derive(Clone)]
59pub struct SerializeOptions {
60 pub pretty: bool,
62 pub indent: Cow<'static, str>,
64 pub float_formatter: Option<FloatFormatter>,
66 pub self_closing_void: bool,
69}
70
71impl Default for SerializeOptions {
72 fn default() -> Self {
73 Self {
74 pretty: false,
75 indent: Cow::Borrowed(" "),
76 float_formatter: None,
77 self_closing_void: false,
78 }
79 }
80}
81
82impl core::fmt::Debug for SerializeOptions {
83 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
84 f.debug_struct("SerializeOptions")
85 .field("pretty", &self.pretty)
86 .field("indent", &self.indent)
87 .field("float_formatter", &self.float_formatter.map(|_| "..."))
88 .field("self_closing_void", &self.self_closing_void)
89 .finish()
90 }
91}
92
93impl SerializeOptions {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn pretty(mut self) -> Self {
101 self.pretty = true;
102 self
103 }
104
105 pub fn indent(mut self, indent: impl Into<Cow<'static, str>>) -> Self {
107 self.indent = indent.into();
108 self.pretty = true;
109 self
110 }
111
112 pub fn float_formatter(mut self, formatter: FloatFormatter) -> Self {
114 self.float_formatter = Some(formatter);
115 self
116 }
117
118 pub fn self_closing_void(mut self, value: bool) -> Self {
120 self.self_closing_void = value;
121 self
122 }
123}
124
125#[derive(Debug)]
127pub struct HtmlSerializeError {
128 msg: &'static str,
129}
130
131impl core::fmt::Display for HtmlSerializeError {
132 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
133 f.write_str(self.msg)
134 }
135}
136
137impl std::error::Error for HtmlSerializeError {}
138
139#[derive(Debug)]
140enum Ctx {
141 Root,
142 Struct {
143 close: Option<String>,
144 has_content: bool,
146 has_block_content: bool,
148 in_preformatted: bool,
150 },
151 Seq {
152 close: Option<String>,
153 in_preformatted: bool,
155 },
156}
157
158pub struct HtmlSerializer {
160 out: Vec<u8>,
161 stack: Vec<Ctx>,
162 pending_field: Option<String>,
163 pending_is_attribute: bool,
165 pending_is_text: bool,
167 pending_is_elements: bool,
169 pending_attributes: Vec<(String, String)>,
171 root_tag_written: bool,
173 root_element_name: Option<String>,
175 deferred_open_tag: Option<(String, String)>,
177 elements_stack: Vec<bool>,
179 skip_enum_wrapper: Option<String>,
184 pending_is_tag: bool,
186 options: SerializeOptions,
188 depth: usize,
190 pending_doctype: Option<String>,
192}
193
194impl HtmlSerializer {
195 pub fn new() -> Self {
197 Self::with_options(SerializeOptions::default())
198 }
199
200 pub fn with_options(options: SerializeOptions) -> Self {
202 Self {
203 out: Vec::new(),
204 stack: vec![Ctx::Root],
205 pending_field: None,
206 pending_is_attribute: false,
207 pending_is_text: false,
208 pending_is_elements: false,
209 pending_attributes: Vec::new(),
210 root_tag_written: false,
211 root_element_name: None,
212 deferred_open_tag: None,
213 elements_stack: Vec::new(),
214 skip_enum_wrapper: None,
215 pending_is_tag: false,
216 options,
217 depth: 0,
218 pending_doctype: None,
219 }
220 }
221
222 pub fn finish(mut self) -> Vec<u8> {
224 self.flush_deferred_open_tag();
226
227 while let Some(ctx) = self.stack.pop() {
229 match ctx {
230 Ctx::Root => break,
231 Ctx::Struct {
232 close,
233 has_block_content,
234 ..
235 } => {
236 if let Some(name) = close
237 && !is_void_element(&name)
238 {
239 self.write_close_tag(&name, has_block_content);
240 }
241 }
242 Ctx::Seq { close, .. } => {
243 if let Some(name) = close
244 && !is_void_element(&name)
245 {
246 self.write_close_tag(&name, true);
247 }
248 }
249 }
250 }
251
252 self.out
253 }
254
255 fn flush_deferred_open_tag_with_mode(&mut self, inline: bool) {
261 if let Some((element_name, _close_name)) = self.deferred_open_tag.take() {
262 if let Some(doctype) = self.pending_doctype.take() {
264 self.out.extend_from_slice(b"<!DOCTYPE ");
265 self.out.extend_from_slice(doctype.as_bytes());
266 self.out.push(b'>');
267 self.write_newline();
268 }
269
270 self.write_indent();
271 self.out.push(b'<');
272 self.out.extend_from_slice(element_name.as_bytes());
273
274 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
276 for (attr_name, attr_value) in attrs {
277 if is_boolean_attribute(&attr_name) {
279 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
280 self.out.push(b' ');
281 self.out.extend_from_slice(attr_name.as_bytes());
282 }
283 continue;
285 }
286
287 self.out.push(b' ');
288 self.out.extend_from_slice(attr_name.as_bytes());
289 self.out.extend_from_slice(b"=\"");
290 self.write_attr_escaped(&attr_value);
291 self.out.push(b'"');
292 }
293
294 if is_void_element(&element_name) {
295 if self.options.self_closing_void {
296 self.out.extend_from_slice(b" />");
297 } else {
298 self.out.push(b'>');
299 }
300 } else {
301 self.out.push(b'>');
302 }
303
304 if !inline {
306 self.write_newline();
307 self.depth += 1;
308 }
309
310 if self.root_element_name.as_deref() == Some(&element_name) {
312 self.root_tag_written = true;
313 }
314 }
315 }
316
317 fn flush_deferred_open_tag(&mut self) {
318 self.flush_deferred_open_tag_with_mode(false)
319 }
320
321 fn write_open_tag(&mut self, name: &str) {
322 self.write_indent();
323 self.out.push(b'<');
324 self.out.extend_from_slice(name.as_bytes());
325
326 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
328 for (attr_name, attr_value) in attrs {
329 if is_boolean_attribute(&attr_name) {
331 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
332 self.out.push(b' ');
333 self.out.extend_from_slice(attr_name.as_bytes());
334 }
335 continue;
337 }
338
339 self.out.push(b' ');
340 self.out.extend_from_slice(attr_name.as_bytes());
341 self.out.extend_from_slice(b"=\"");
342 self.write_attr_escaped(&attr_value);
343 self.out.push(b'"');
344 }
345
346 if is_void_element(name) {
347 if self.options.self_closing_void {
348 self.out.extend_from_slice(b" />");
349 } else {
350 self.out.push(b'>');
351 }
352 } else {
353 self.out.push(b'>');
354 }
355 }
356
357 fn write_close_tag_ex(&mut self, name: &str, indent_before: bool, newline_after: bool) {
362 if is_void_element(name) {
363 return; }
365 if indent_before {
366 self.depth = self.depth.saturating_sub(1);
367 self.write_indent();
368 }
369 self.out.extend_from_slice(b"</");
370 self.out.extend_from_slice(name.as_bytes());
371 self.out.push(b'>');
372 if newline_after {
373 self.write_newline();
374 }
375 }
376
377 fn write_close_tag(&mut self, name: &str, block: bool) {
378 self.write_close_tag_ex(name, block, block)
379 }
380
381 fn write_text_escaped(&mut self, text: &str) {
382 for b in text.as_bytes() {
383 match *b {
384 b'&' => self.out.extend_from_slice(b"&"),
385 b'<' => self.out.extend_from_slice(b"<"),
386 b'>' => self.out.extend_from_slice(b">"),
387 _ => self.out.push(*b),
388 }
389 }
390 }
391
392 fn write_attr_escaped(&mut self, text: &str) {
393 for b in text.as_bytes() {
394 match *b {
395 b'&' => self.out.extend_from_slice(b"&"),
396 b'<' => self.out.extend_from_slice(b"<"),
397 b'>' => self.out.extend_from_slice(b">"),
398 b'"' => self.out.extend_from_slice(b"""),
399 _ => self.out.push(*b),
400 }
401 }
402 }
403
404 fn format_float(&self, v: f64) -> String {
405 if let Some(fmt) = self.options.float_formatter {
406 let mut buf = Vec::new();
407 if fmt(v, &mut buf).is_ok()
408 && let Ok(s) = String::from_utf8(buf)
409 {
410 return s;
411 }
412 }
413 #[cfg(feature = "fast")]
414 return zmij::Buffer::new().format(v).to_string();
415 #[cfg(not(feature = "fast"))]
416 v.to_string()
417 }
418
419 fn in_preformatted(&self) -> bool {
421 for ctx in self.stack.iter().rev() {
422 match ctx {
423 Ctx::Struct {
424 in_preformatted: true,
425 ..
426 }
427 | Ctx::Seq {
428 in_preformatted: true,
429 ..
430 } => return true,
431 _ => {}
432 }
433 }
434 false
435 }
436
437 fn write_indent(&mut self) {
438 if self.options.pretty && !self.in_preformatted() {
439 for _ in 0..self.depth {
440 self.out.extend_from_slice(self.options.indent.as_bytes());
441 }
442 }
443 }
444
445 fn write_newline(&mut self) {
446 if self.options.pretty && !self.in_preformatted() {
447 self.out.push(b'\n');
448 }
449 }
450
451 fn ensure_root_tag_written(&mut self) {
452 if !self.root_tag_written {
453 let root_name = self
454 .root_element_name
455 .as_deref()
456 .unwrap_or("div")
457 .to_string();
458 self.out.push(b'<');
459 self.out.extend_from_slice(root_name.as_bytes());
460
461 let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
463 for (attr_name, attr_value) in attrs {
464 if is_boolean_attribute(&attr_name) {
465 if attr_value == "true" || attr_value == "1" || attr_value == attr_name {
466 self.out.push(b' ');
467 self.out.extend_from_slice(attr_name.as_bytes());
468 }
469 continue;
470 }
471
472 self.out.push(b' ');
473 self.out.extend_from_slice(attr_name.as_bytes());
474 self.out.extend_from_slice(b"=\"");
475 self.write_attr_escaped(&attr_value);
476 self.out.push(b'"');
477 }
478
479 if is_void_element(&root_name) {
480 if self.options.self_closing_void {
481 self.out.extend_from_slice(b" />");
482 } else {
483 self.out.push(b'>');
484 }
485 } else {
486 self.out.push(b'>');
487 self.write_newline();
488 self.depth += 1;
489 }
490 self.root_tag_written = true;
491 }
492 }
493
494 fn open_value_element_if_needed(&mut self) -> Result<Option<String>, HtmlSerializeError> {
495 self.flush_deferred_open_tag();
496 self.ensure_root_tag_written();
497
498 if let Some(field_name) = self.pending_field.take() {
499 if self.elements_stack.last().copied().unwrap_or(false) {
501 self.write_open_tag(&field_name);
503 return Ok(Some(field_name));
504 }
505
506 if self.pending_is_text {
508 self.pending_is_text = false;
509 return Ok(None); }
511
512 if self.pending_is_attribute {
514 self.pending_is_attribute = false;
515 return Ok(None);
516 }
517
518 self.write_open_tag(&field_name);
520 return Ok(Some(field_name));
521 }
522 Ok(None)
523 }
524
525 fn write_scalar_string(&mut self, value: &str) -> Result<(), HtmlSerializeError> {
526 if self.pending_is_tag {
529 self.pending_is_tag = false;
530 self.pending_field.take();
531 if let Some((ref mut element_name, ref mut _close_name)) = self.deferred_open_tag {
533 *element_name = value.to_string();
534 *_close_name = value.to_string();
535 } else {
536 self.pending_field = Some(value.to_string());
538 }
539 if let Some(Ctx::Struct { close, .. }) = self.stack.last_mut() {
541 *close = Some(value.to_string());
542 }
543 return Ok(());
544 }
545
546 if self.pending_is_attribute
549 && let Some(attr_name) = self.pending_field.take()
550 {
551 self.pending_is_attribute = false;
552 if attr_name == "doctype" && matches!(self.stack.last(), Some(Ctx::Struct { .. })) {
555 self.pending_doctype = Some(value.to_string());
556 return Ok(());
557 }
558 self.pending_attributes.push((attr_name, value.to_string()));
559 return Ok(());
560 }
561
562 if self.pending_is_text {
564 self.flush_deferred_open_tag_with_mode(true);
566 self.pending_is_text = false;
567 self.pending_field.take();
568 self.write_text_escaped(value);
569
570 if let Some(Ctx::Struct { has_content, .. }) = self.stack.last_mut() {
572 *has_content = true;
573 }
574 return Ok(());
575 }
576
577 self.flush_deferred_open_tag();
579 self.ensure_root_tag_written();
580 let close = self.open_value_element_if_needed()?;
581 self.write_text_escaped(value);
582 if let Some(name) = close {
583 self.write_close_tag(&name, false);
584 }
585 self.write_newline();
586 Ok(())
587 }
588}
589
590impl Default for HtmlSerializer {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596impl FormatSerializer for HtmlSerializer {
597 type Error = HtmlSerializeError;
598
599 fn struct_metadata(&mut self, shape: &facet_core::Shape) -> Result<(), Self::Error> {
600 let element_name = shape
602 .get_builtin_attr_value::<&str>("rename")
603 .unwrap_or(shape.type_identifier);
604
605 if matches!(self.stack.last(), Some(Ctx::Root)) {
607 self.root_element_name = Some(element_name.to_string());
608 }
609
610 if self.elements_stack.last() == Some(&true)
614 && self.pending_field.is_none()
615 && self.skip_enum_wrapper.is_none()
616 {
617 self.pending_field = Some(element_name.to_string());
618 }
619
620 Ok(())
621 }
622
623 fn field_metadata(&mut self, field_item: &facet_reflect::FieldItem) -> Result<(), Self::Error> {
624 if let Some(field) = field_item.field {
626 self.pending_is_attribute = field.is_attribute();
627 self.pending_is_text = field.is_text();
628 self.pending_is_elements = field.is_elements();
629 self.pending_is_tag = field.is_tag();
630 } else {
631 self.pending_is_attribute = true;
633 self.pending_is_text = false;
634 self.pending_is_elements = false;
635 self.pending_is_tag = false;
636 }
637 Ok(())
638 }
639
640 fn variant_metadata(
641 &mut self,
642 variant: &'static facet_core::Variant,
643 ) -> Result<(), Self::Error> {
644 if self.elements_stack.last() == Some(&true) {
657 if variant.is_text() {
660 self.pending_is_text = true;
661 self.skip_enum_wrapper = Some(variant.name.to_string());
662 } else if variant.is_custom_element() {
663 self.skip_enum_wrapper = Some(variant.name.to_string());
667 } else {
668 let element_name = variant
670 .get_builtin_attr("rename")
671 .and_then(|attr| attr.get_as::<&str>().copied())
672 .unwrap_or(variant.name);
673 self.pending_field = Some(element_name.to_string());
674 self.skip_enum_wrapper = Some(variant.name.to_string());
676 }
677 }
678 Ok(())
679 }
680
681 fn preferred_field_order(&self) -> FieldOrdering {
682 FieldOrdering::AttributesFirst
683 }
684
685 fn begin_struct(&mut self) -> Result<(), Self::Error> {
686 self.flush_deferred_open_tag();
688
689 for ctx in self.stack.iter_mut().rev() {
692 if let Ctx::Struct {
693 has_content,
694 has_block_content,
695 ..
696 } = ctx
697 {
698 *has_content = true;
699 *has_block_content = true;
700 break;
701 }
702 }
703
704 if self.skip_enum_wrapper.is_some() {
708 let in_elements = self.elements_stack.last().copied().unwrap_or(false);
710 let in_preformatted = self.in_preformatted();
712 self.elements_stack.push(in_elements);
713 self.stack.push(Ctx::Struct {
714 close: None,
715 has_content: false,
716 has_block_content: false,
717 in_preformatted,
718 });
719 return Ok(());
720 }
721
722 if self.pending_is_elements {
724 self.pending_is_elements = false;
725 self.elements_stack.push(true);
726 } else {
727 self.elements_stack.push(false);
728 }
729
730 match self.stack.last() {
731 Some(Ctx::Root) => {
732 let element_name = self.root_element_name.clone();
735 let in_preformatted = element_name
736 .as_ref()
737 .map(|n| is_whitespace_sensitive(n))
738 .unwrap_or(false);
739 if let Some(name) = element_name.clone() {
740 self.deferred_open_tag = Some((name.clone(), name));
741 }
742 self.stack.push(Ctx::Struct {
743 close: element_name,
744 has_content: false,
745 has_block_content: false,
746 in_preformatted,
747 });
748 Ok(())
749 }
750 Some(Ctx::Struct { .. }) | Some(Ctx::Seq { .. }) => {
751 let parent_preformatted = self.in_preformatted();
754 let close = if let Some(field_name) = self.pending_field.take() {
755 self.deferred_open_tag = Some((field_name.clone(), field_name.clone()));
756 Some(field_name)
757 } else {
758 None
759 };
760 let in_preformatted = parent_preformatted
761 || close
762 .as_ref()
763 .map(|n| is_whitespace_sensitive(n))
764 .unwrap_or(false);
765 self.stack.push(Ctx::Struct {
766 close,
767 has_content: false,
768 has_block_content: false,
769 in_preformatted,
770 });
771 Ok(())
772 }
773 None => Err(HtmlSerializeError {
774 msg: "serializer state missing context",
775 }),
776 }
777 }
778
779 fn end_struct(&mut self) -> Result<(), Self::Error> {
780 self.elements_stack.pop();
781
782 if let Some(Ctx::Struct {
783 close,
784 has_content,
785 has_block_content,
786 ..
787 }) = self.stack.pop()
788 {
789 self.flush_deferred_open_tag_with_mode(!has_content && !has_block_content);
792
793 if let Some(name) = close
794 && !is_void_element(&name)
795 {
796 let parent_is_block = matches!(
798 self.stack.last(),
799 Some(Ctx::Seq { .. })
800 | Some(Ctx::Struct {
801 has_block_content: true,
802 ..
803 })
804 );
805
806 self.write_close_tag_ex(
809 &name,
810 has_block_content,
811 has_block_content || parent_is_block,
812 );
813 }
814 }
815 Ok(())
816 }
817
818 fn begin_seq(&mut self) -> Result<(), Self::Error> {
819 if self.pending_is_elements {
822 self.pending_is_elements = false;
823 self.elements_stack.push(true);
824 self.pending_field.take(); let in_preformatted = self.in_preformatted();
827 self.stack.push(Ctx::Seq {
828 close: None,
829 in_preformatted,
830 });
831 return Ok(());
832 }
833
834 self.flush_deferred_open_tag();
836 self.ensure_root_tag_written();
837
838 if let Some(Ctx::Struct {
840 has_content,
841 has_block_content,
842 ..
843 }) = self.stack.last_mut()
844 {
845 *has_content = true;
846 *has_block_content = true;
847 }
848
849 let parent_preformatted = self.in_preformatted();
851 let close = if let Some(field_name) = self.pending_field.take() {
852 self.write_open_tag(&field_name);
853 self.write_newline();
854 self.depth += 1;
855 Some(field_name)
856 } else {
857 None
858 };
859 let in_preformatted = parent_preformatted
860 || close
861 .as_ref()
862 .map(|n| is_whitespace_sensitive(n))
863 .unwrap_or(false);
864 self.elements_stack.push(false);
865 self.stack.push(Ctx::Seq {
866 close,
867 in_preformatted,
868 });
869 Ok(())
870 }
871
872 fn end_seq(&mut self) -> Result<(), Self::Error> {
873 self.elements_stack.pop();
874 if let Some(Ctx::Seq { close, .. }) = self.stack.pop()
875 && let Some(name) = close
876 {
877 self.write_close_tag(&name, true);
878 }
879 Ok(())
880 }
881
882 fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
883 if let Some(ref variant_name) = self.skip_enum_wrapper
886 && key == variant_name
887 {
888 self.skip_enum_wrapper = None;
891 return Ok(());
892 }
893 self.pending_field = Some(key.to_string());
894 Ok(())
895 }
896
897 fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
898 match scalar {
899 ScalarValue::Null => {
900 self.pending_field.take();
902 self.pending_is_attribute = false;
903 self.pending_is_text = false;
904 Ok(())
905 }
906 ScalarValue::Bool(v) => {
907 if self.pending_is_attribute
909 && let Some(attr_name) = self.pending_field.take()
910 {
911 self.pending_is_attribute = false;
912 if v {
913 self.pending_attributes.push((attr_name.clone(), attr_name));
915 }
916 return Ok(());
918 }
919
920 self.write_scalar_string(if v { "true" } else { "false" })
921 }
922 ScalarValue::Char(c) => {
923 let mut buf = [0u8; 4];
924 self.write_scalar_string(c.encode_utf8(&mut buf))
925 }
926 ScalarValue::I64(v) => self.write_scalar_string(&v.to_string()),
927 ScalarValue::U64(v) => self.write_scalar_string(&v.to_string()),
928 ScalarValue::F64(v) => {
929 let s = self.format_float(v);
930 self.write_scalar_string(&s)
931 }
932 ScalarValue::Str(s) | ScalarValue::StringlyTyped(s) => self.write_scalar_string(&s),
933 ScalarValue::I128(v) => self.write_scalar_string(&v.to_string()),
934 ScalarValue::U128(v) => self.write_scalar_string(&v.to_string()),
935 ScalarValue::Bytes(_) => Err(HtmlSerializeError {
936 msg: "binary data cannot be serialized to HTML",
937 }),
938 }
939 }
940}
941
942fn is_void_element(name: &str) -> bool {
944 VOID_ELEMENTS.iter().any(|&v| v.eq_ignore_ascii_case(name))
945}
946
947fn is_boolean_attribute(name: &str) -> bool {
949 BOOLEAN_ATTRIBUTES
950 .iter()
951 .any(|&v| v.eq_ignore_ascii_case(name))
952}
953
954fn is_whitespace_sensitive(name: &str) -> bool {
956 WHITESPACE_SENSITIVE_ELEMENTS
957 .iter()
958 .any(|&v| v.eq_ignore_ascii_case(name))
959}
960
961pub fn to_string<T: Facet<'static>>(
967 value: &T,
968) -> Result<String, SerializeError<HtmlSerializeError>> {
969 to_string_with_options(value, &SerializeOptions::default())
970}
971
972pub fn to_string_pretty<T: Facet<'static>>(
974 value: &T,
975) -> Result<String, SerializeError<HtmlSerializeError>> {
976 to_string_with_options(value, &SerializeOptions::default().pretty())
977}
978
979pub fn to_string_with_options<T: Facet<'static>>(
981 value: &T,
982 options: &SerializeOptions,
983) -> Result<String, SerializeError<HtmlSerializeError>> {
984 let bytes = to_vec_with_options(value, options)?;
985 String::from_utf8(bytes).map_err(|_| {
986 SerializeError::Reflect(facet_reflect::ReflectError::InvalidOperation {
987 operation: "to_string",
988 reason: "invalid UTF-8 in serialized output",
989 })
990 })
991}
992
993pub fn to_vec<T: Facet<'static>>(value: &T) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
995 to_vec_with_options(value, &SerializeOptions::default())
996}
997
998pub fn to_vec_with_options<T: Facet<'static>>(
1000 value: &T,
1001 options: &SerializeOptions,
1002) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
1003 let mut serializer = HtmlSerializer::with_options(options.clone());
1004 let peek = Peek::new(value);
1005 serialize_root(&mut serializer, peek)?;
1006 Ok(serializer.finish())
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012 use facet::Facet;
1013 use facet_xml as xml;
1014
1015 #[derive(Debug, Facet)]
1016 #[facet(rename = "div")]
1017 struct SimpleDiv {
1018 #[facet(xml::attribute, default)]
1019 class: Option<String>,
1020 #[facet(xml::attribute, default)]
1021 id: Option<String>,
1022 #[facet(xml::text, default)]
1023 text: String,
1024 }
1025
1026 #[test]
1027 fn test_simple_element() {
1028 let div = SimpleDiv {
1029 class: Some("container".into()),
1030 id: Some("main".into()),
1031 text: "Hello, World!".into(),
1032 };
1033
1034 let html = to_string(&div).unwrap();
1035 assert!(html.contains("<div"), "Expected <div, got: {}", html);
1036 assert!(
1037 html.contains("class=\"container\""),
1038 "Expected class attr, got: {}",
1039 html
1040 );
1041 assert!(
1042 html.contains("id=\"main\""),
1043 "Expected id attr, got: {}",
1044 html
1045 );
1046 assert!(
1047 html.contains("Hello, World!"),
1048 "Expected text content, got: {}",
1049 html
1050 );
1051 assert!(html.contains("</div>"), "Expected </div>, got: {}", html);
1052 }
1053
1054 #[test]
1055 fn test_pretty_print() {
1056 let div = SimpleDiv {
1058 class: Some("test".into()),
1059 id: None,
1060 text: "Content".into(),
1061 };
1062
1063 let html = to_string_pretty(&div).unwrap();
1064 assert_eq!(
1065 html, "<div class=\"test\">Content</div>",
1066 "Text-only elements should be inline"
1067 );
1068 }
1069
1070 #[test]
1071 fn test_pretty_print_nested() {
1072 let container = Container {
1074 class: Some("outer".into()),
1075 children: vec![
1076 Child::P(Paragraph {
1077 text: "First".into(),
1078 }),
1079 Child::P(Paragraph {
1080 text: "Second".into(),
1081 }),
1082 ],
1083 };
1084
1085 let html = to_string_pretty(&container).unwrap();
1086 assert!(
1087 html.contains('\n'),
1088 "Expected newlines in pretty output: {}",
1089 html
1090 );
1091 assert!(
1092 html.contains(" <p>"),
1093 "Expected indented child elements: {}",
1094 html
1095 );
1096 }
1097
1098 #[derive(Debug, Facet)]
1099 #[facet(rename = "img")]
1100 struct Image {
1101 #[facet(xml::attribute)]
1102 src: String,
1103 #[facet(xml::attribute, default)]
1104 alt: Option<String>,
1105 }
1106
1107 #[test]
1108 fn test_void_element() {
1109 let img = Image {
1110 src: "photo.jpg".into(),
1111 alt: Some("A photo".into()),
1112 };
1113
1114 let html = to_string(&img).unwrap();
1115 assert!(html.contains("<img"), "Expected <img, got: {}", html);
1116 assert!(
1117 html.contains("src=\"photo.jpg\""),
1118 "Expected src attr, got: {}",
1119 html
1120 );
1121 assert!(
1122 html.contains("alt=\"A photo\""),
1123 "Expected alt attr, got: {}",
1124 html
1125 );
1126 assert!(
1128 !html.contains("</img>"),
1129 "Should not have </img>, got: {}",
1130 html
1131 );
1132 }
1133
1134 #[test]
1135 fn test_void_element_self_closing() {
1136 let img = Image {
1137 src: "photo.jpg".into(),
1138 alt: None,
1139 };
1140
1141 let options = SerializeOptions::new().self_closing_void(true);
1142 let html = to_string_with_options(&img, &options).unwrap();
1143 assert!(html.contains("/>"), "Expected self-closing, got: {}", html);
1144 }
1145
1146 #[derive(Debug, Facet)]
1147 #[facet(rename = "input")]
1148 struct Input {
1149 #[facet(xml::attribute, rename = "type")]
1150 input_type: String,
1151 #[facet(xml::attribute, default)]
1152 disabled: Option<bool>,
1153 #[facet(xml::attribute, default)]
1154 checked: Option<bool>,
1155 }
1156
1157 #[test]
1158 fn test_boolean_attributes() {
1159 let input = Input {
1160 input_type: "checkbox".into(),
1161 disabled: Some(true),
1162 checked: Some(false),
1163 };
1164
1165 let html = to_string(&input).unwrap();
1166 assert!(
1167 html.contains("type=\"checkbox\""),
1168 "Expected type attr, got: {}",
1169 html
1170 );
1171 assert!(
1172 html.contains("disabled"),
1173 "Expected disabled attr, got: {}",
1174 html
1175 );
1176 assert!(
1178 !html.contains("checked"),
1179 "Should not have checked, got: {}",
1180 html
1181 );
1182 }
1183
1184 #[test]
1185 fn test_escape_special_chars() {
1186 let div = SimpleDiv {
1187 class: None,
1188 id: None,
1189 text: "<script>alert('xss')</script>".into(),
1190 };
1191
1192 let html = to_string(&div).unwrap();
1193 assert!(
1194 html.contains("<script>"),
1195 "Expected escaped script tag, got: {}",
1196 html
1197 );
1198 assert!(
1199 !html.contains("<script>"),
1200 "Should not have raw script tag, got: {}",
1201 html
1202 );
1203 }
1204
1205 #[derive(Debug, Facet)]
1207 #[facet(rename = "div")]
1208 struct Container {
1209 #[facet(xml::attribute, default)]
1210 class: Option<String>,
1211 #[facet(xml::elements, default)]
1212 children: Vec<Child>,
1213 }
1214
1215 #[derive(Debug, Facet)]
1216 #[repr(u8)]
1217 enum Child {
1218 #[facet(rename = "p")]
1219 P(#[expect(dead_code)] Paragraph),
1220 #[facet(rename = "span")]
1221 Span(#[expect(dead_code)] Span),
1222 }
1223
1224 #[derive(Debug, Facet)]
1225 struct Paragraph {
1226 #[facet(xml::text, default)]
1227 text: String,
1228 }
1229
1230 #[derive(Debug, Facet)]
1231 struct Span {
1232 #[facet(xml::attribute, default)]
1233 class: Option<String>,
1234 #[facet(xml::text, default)]
1235 text: String,
1236 }
1237
1238 #[test]
1239 fn test_nested_elements_with_enums() {
1240 let container = Container {
1241 class: Some("wrapper".into()),
1242 children: vec![
1243 Child::P(Paragraph {
1244 text: "Hello".into(),
1245 }),
1246 Child::Span(Span {
1247 class: Some("highlight".into()),
1248 text: "World".into(),
1249 }),
1250 ],
1251 };
1252
1253 let html = to_string(&container).unwrap();
1254 let expected =
1255 r#"<div class="wrapper"><p>Hello</p><span class="highlight">World</span></div>"#;
1256 assert_eq!(html, expected);
1257 }
1258
1259 #[test]
1260 fn test_nested_elements_pretty_print() {
1261 let container = Container {
1262 class: Some("wrapper".into()),
1263 children: vec![
1264 Child::P(Paragraph {
1265 text: "Hello".into(),
1266 }),
1267 Child::Span(Span {
1268 class: Some("highlight".into()),
1269 text: "World".into(),
1270 }),
1271 ],
1272 };
1273
1274 let html = to_string_pretty(&container).unwrap();
1275 let expected = "<div class=\"wrapper\">\n <p>Hello</p>\n <span class=\"highlight\">World</span>\n</div>\n";
1277 assert_eq!(html, expected);
1278 }
1279
1280 #[test]
1281 fn test_empty_container() {
1282 let container = Container {
1283 class: Some("empty".into()),
1284 children: vec![],
1285 };
1286
1287 let html = to_string(&container).unwrap();
1288 assert_eq!(html, r#"<div class="empty"></div>"#);
1289
1290 let html_pretty = to_string_pretty(&container).unwrap();
1291 assert_eq!(html_pretty, r#"<div class="empty"></div>"#);
1293 }
1294
1295 #[test]
1296 fn test_deeply_nested() {
1297 #[derive(Debug, Facet)]
1299 #[facet(rename = "article")]
1300 struct Article {
1301 #[facet(xml::elements, default)]
1302 sections: Vec<Section>,
1303 }
1304
1305 #[derive(Debug, Facet)]
1306 #[facet(rename = "section")]
1307 struct Section {
1308 #[facet(xml::attribute, default)]
1309 id: Option<String>,
1310 #[facet(xml::elements, default)]
1311 paragraphs: Vec<Para>,
1312 }
1313
1314 #[derive(Debug, Facet)]
1315 #[facet(rename = "p")]
1316 struct Para {
1317 #[facet(xml::text, default)]
1318 text: String,
1319 }
1320
1321 let article = Article {
1322 sections: vec![Section {
1323 id: Some("intro".into()),
1324 paragraphs: vec![
1325 Para {
1326 text: "First para".into(),
1327 },
1328 Para {
1329 text: "Second para".into(),
1330 },
1331 ],
1332 }],
1333 };
1334
1335 let html = to_string(&article).unwrap();
1336 assert_eq!(
1337 html,
1338 r#"<article><section id="intro"><p>First para</p><p>Second para</p></section></article>"#
1339 );
1340
1341 let html_pretty = to_string_pretty(&article).unwrap();
1342 assert_eq!(
1343 html_pretty,
1344 "<article>\n <section id=\"intro\">\n <p>First para</p>\n <p>Second para</p>\n </section>\n</article>\n"
1345 );
1346 }
1347
1348 #[test]
1349 fn test_event_handlers() {
1350 use facet_html_dom::{Button, GlobalAttrs};
1351
1352 let button = Button {
1353 attrs: GlobalAttrs {
1354 onclick: Some("handleClick()".into()),
1355 onmouseover: Some("highlight(this)".into()),
1356 ..Default::default()
1357 },
1358 type_: Some("button".into()),
1359 children: vec![facet_html_dom::PhrasingContent::Text("Click me".into())],
1360 ..Default::default()
1361 };
1362
1363 let html = to_string(&button).unwrap();
1364 assert!(
1365 html.contains(r#"onclick="handleClick()""#),
1366 "Expected onclick handler, got: {}",
1367 html
1368 );
1369 assert!(
1370 html.contains(r#"onmouseover="highlight(this)""#),
1371 "Expected onmouseover handler, got: {}",
1372 html
1373 );
1374 assert!(
1375 html.contains("Click me"),
1376 "Expected button text, got: {}",
1377 html
1378 );
1379 }
1380
1381 #[test]
1382 fn test_event_handlers_with_escaping() {
1383 use facet_html_dom::{Div, FlowContent, GlobalAttrs};
1384
1385 let div = Div {
1386 attrs: GlobalAttrs {
1387 onclick: Some(r#"alert("Hello \"World\"")"#.into()),
1388 ..Default::default()
1389 },
1390 children: vec![FlowContent::Text("Test".into())],
1391 };
1392
1393 let html = to_string(&div).unwrap();
1394 assert!(
1396 html.contains("onclick="),
1397 "Expected onclick attr, got: {}",
1398 html
1399 );
1400 assert!(
1401 html.contains("""),
1402 "Expected escaped quotes in onclick, got: {}",
1403 html
1404 );
1405 }
1406
1407 #[test]
1408 fn test_doctype_roundtrip() {
1409 use crate::parser::HtmlParser;
1410 use facet_format::FormatDeserializer;
1411 use facet_html_dom::Html;
1412
1413 let input = br#"<!DOCTYPE html>
1415<html>
1416<head><title>Test</title></head>
1417<body></body>
1418</html>"#;
1419
1420 let parser = HtmlParser::new(input);
1421 let mut deserializer = FormatDeserializer::new(parser);
1422 let parsed: Html = deserializer.deserialize().unwrap();
1423
1424 assert_eq!(
1426 parsed.doctype,
1427 Some("html".to_string()),
1428 "DOCTYPE should be captured during parsing"
1429 );
1430
1431 let output = to_string(&parsed).unwrap();
1433
1434 assert!(
1436 output.starts_with("<!DOCTYPE html>"),
1437 "Output should start with DOCTYPE declaration, got: {}",
1438 output
1439 );
1440
1441 let parser2 = HtmlParser::new(output.as_bytes());
1443 let mut deserializer2 = FormatDeserializer::new(parser2);
1444 let reparsed: Html = deserializer2.deserialize().unwrap();
1445
1446 assert_eq!(
1447 reparsed.doctype,
1448 Some("html".to_string()),
1449 "DOCTYPE should survive roundtrip"
1450 );
1451 }
1452}