facet_html/
serializer.rs

1//! HTML serializer implementing `FormatSerializer`.
2
3extern 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
12/// A function that formats a floating-point number to a writer.
13pub type FloatFormatter = fn(f64, &mut dyn Write) -> std::io::Result<()>;
14
15/// HTML5 void elements that don't have closing tags.
16const VOID_ELEMENTS: &[&str] = &[
17    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
18    "track", "wbr",
19];
20
21/// HTML5 boolean attributes that are written without a value when true.
22const 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/// Options for HTML serialization.
54#[derive(Clone)]
55pub struct SerializeOptions {
56    /// Whether to pretty-print with indentation (default: false for minified output)
57    pub pretty: bool,
58    /// Indentation string for pretty-printing (default: "  ")
59    pub indent: Cow<'static, str>,
60    /// Custom formatter for floating-point numbers (f32 and f64).
61    pub float_formatter: Option<FloatFormatter>,
62    /// Whether to use self-closing syntax for void elements (default: false)
63    /// When false: `<br>`, when true: `<br />`
64    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    /// Create new default options (minified output).
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Enable pretty-printing with default indentation.
96    pub fn pretty(mut self) -> Self {
97        self.pretty = true;
98        self
99    }
100
101    /// Set a custom indentation string (implies pretty-printing).
102    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    /// Set a custom formatter for floating-point numbers.
109    pub fn float_formatter(mut self, formatter: FloatFormatter) -> Self {
110        self.float_formatter = Some(formatter);
111        self
112    }
113
114    /// Use self-closing syntax for void elements (`<br />` instead of `<br>`).
115    pub fn self_closing_void(mut self, value: bool) -> Self {
116        self.self_closing_void = value;
117        self
118    }
119}
120
121/// Error type for HTML serialization.
122#[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        /// True if we've written any content inside this struct (text or child elements)
141        has_content: bool,
142        /// True if we've written block content (child elements) that requires newlines
143        has_block_content: bool,
144    },
145    Seq {
146        close: Option<String>,
147    },
148}
149
150/// HTML serializer with configurable output options.
151pub struct HtmlSerializer {
152    out: Vec<u8>,
153    stack: Vec<Ctx>,
154    pending_field: Option<String>,
155    /// True if the current field is an attribute
156    pending_is_attribute: bool,
157    /// True if the current field is text content
158    pending_is_text: bool,
159    /// True if the current field is an elements list
160    pending_is_elements: bool,
161    /// Buffered attributes for the current element (name, value)
162    pending_attributes: Vec<(String, String)>,
163    /// True if we've written the opening root tag
164    root_tag_written: bool,
165    /// Name to use for the root element
166    root_element_name: Option<String>,
167    /// Deferred element tag - wait to write opening tag until we've collected attributes
168    deferred_open_tag: Option<(String, String)>,
169    /// Stack of elements state (true = in elements list)
170    elements_stack: Vec<bool>,
171    /// When set, we're about to serialize an externally-tagged enum inside xml::elements.
172    /// The next begin_struct() should be skipped (it's the wrapper struct), and the
173    /// following field_key(variant_name) should also be skipped because variant_metadata
174    /// already set up pending_field with the variant name.
175    skip_enum_wrapper: Option<String>,
176    /// Serialization options
177    options: SerializeOptions,
178    /// Current indentation depth
179    depth: usize,
180}
181
182impl HtmlSerializer {
183    /// Create a new HTML serializer with default options (minified).
184    pub fn new() -> Self {
185        Self::with_options(SerializeOptions::default())
186    }
187
188    /// Create a new HTML serializer with the given options.
189    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    /// Finish serialization and return the output bytes.
209    pub fn finish(mut self) -> Vec<u8> {
210        // Flush any pending deferred tag
211        self.flush_deferred_open_tag();
212
213        // Close any remaining non-root elements
214        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    /// Flush the deferred open tag.
242    ///
243    /// If `inline` is true, the content will be inline (text), so we don't add
244    /// a newline after the opening tag. If false, content is block-level (child
245    /// elements) so we add a newline and increase indentation.
246    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            // Write buffered attributes
253            let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
254            for (attr_name, attr_value) in attrs {
255                // Handle boolean attributes
256                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                    // Skip false/empty boolean attributes
262                    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            // Only add newline and increase depth for block content
283            if !inline {
284                self.write_newline();
285                self.depth += 1;
286            }
287
288            // If this was the root element, mark it as written
289            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        // Write buffered attributes
305        let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
306        for (attr_name, attr_value) in attrs {
307            // Handle boolean attributes
308            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                // Skip false/empty boolean attributes
314                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    /// Write a closing tag.
336    ///
337    /// - `indent_before`: if true, decrement depth and write indent before the tag
338    /// - `newline_after`: if true, write a newline after the tag
339    fn write_close_tag_ex(&mut self, name: &str, indent_before: bool, newline_after: bool) {
340        if is_void_element(name) {
341            return; // Void elements have no closing tag
342        }
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"&amp;"),
363                b'<' => self.out.extend_from_slice(b"&lt;"),
364                b'>' => self.out.extend_from_slice(b"&gt;"),
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"&amp;"),
374                b'<' => self.out.extend_from_slice(b"&lt;"),
375                b'>' => self.out.extend_from_slice(b"&gt;"),
376                b'"' => self.out.extend_from_slice(b"&quot;"),
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            // Write buffered attributes
422            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            // Check if we're in elements mode - if so, don't wrap
460            if self.elements_stack.last().copied().unwrap_or(false) {
461                // In elements mode - the field name is the element tag
462                self.write_open_tag(&field_name);
463                return Ok(Some(field_name));
464            }
465
466            // Handle text content
467            if self.pending_is_text {
468                self.pending_is_text = false;
469                return Ok(None); // Text content - no element wrapper
470            }
471
472            // Handle attributes - shouldn't get here for attributes
473            if self.pending_is_attribute {
474                self.pending_is_attribute = false;
475                return Ok(None);
476            }
477
478            // Regular child element
479            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        // Handle attribute values BEFORE flushing deferred tag
487        // Attributes need to be buffered, not written as content
488        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        // Handle text content - flush deferred tag first (inline mode), then write text
497        if self.pending_is_text {
498            // Use inline mode so we don't add newline after opening tag
499            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            // Mark parent struct as having content (but NOT block content)
505            if let Some(Ctx::Struct { has_content, .. }) = self.stack.last_mut() {
506                *has_content = true;
507            }
508            return Ok(());
509        }
510
511        // Regular element content
512        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        // Get the element name from the shape (respecting rename attribute)
535        let element_name = shape
536            .get_builtin_attr_value::<&str>("rename")
537            .unwrap_or(shape.type_identifier);
538
539        // If this is the root element (stack only has Root context), save the name
540        if matches!(self.stack.last(), Some(Ctx::Root)) {
541            self.root_element_name = Some(element_name.to_string());
542        }
543
544        // If we're inside an xml::elements list and no pending field is set,
545        // use the shape's element name. However, if variant_metadata already
546        // set a pending_field (for enums), don't override it.
547        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        // For flattened map entries (field is None), treat as attributes
559        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            // Flattened map entries are attributes
565            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 we're inside an xml::elements list, set the pending field to the variant name
577        // and mark that we should skip the externally-tagged wrapper struct.
578        //
579        // For externally-tagged enums, the serialization flow is:
580        //   1. variant_metadata(variant) - we're here
581        //   2. begin_struct() - creates wrapper struct (we want to SKIP this)
582        //   3. field_key(variant.name) - sets field name (we want to SKIP this)
583        //   4. shared_serialize(inner) - serializes the actual content
584        //
585        // We set pending_field to the variant name, and skip_enum_wrapper to tell
586        // begin_struct() to not create an element, and field_key() to not override
587        // the pending_field we just set.
588        if self.elements_stack.last() == Some(&true) {
589            // Get the element name from the variant (respecting rename attribute)
590            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            // Set the skip flag with the variant name so field_key knows what to skip
596            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        // Flush any deferred tag from parent before starting a new struct
607        self.flush_deferred_open_tag();
608
609        // Mark nearest ancestor struct as having block content (child elements)
610        // We need to find the Struct even if there's a Seq in between (for elements lists)
611        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 we're skipping the enum wrapper struct (for xml::elements enum serialization),
625        // just push a struct context without creating any element.
626        // Keep the elements_stack state - we're still inside the elements list.
627        if self.skip_enum_wrapper.is_some() {
628            // Propagate the current elements state to maintain the "in elements" context
629            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 we're starting a new struct that's an elements list item
640        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                // Root struct - defer the opening tag until we've collected attributes
650                // The element name was set in struct_metadata
651                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                // Nested struct - defer the opening tag
663                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            // Flush any remaining deferred tag (in case struct had only attributes or empty content)
692            // Use inline mode if we never had any content
693            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                // Check if parent is a block context (Seq or Struct with block content)
699                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                // If we had block content, indent before closing tag
709                // If parent is block context, add newline after (so next sibling is on new line)
710                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 this is an elements list, DON'T flush the deferred tag yet.
722        // Wait until we have actual items to determine if we have block content.
723        if self.pending_is_elements {
724            self.pending_is_elements = false;
725            self.elements_stack.push(true);
726            self.pending_field.take(); // Consume the field name
727            self.stack.push(Ctx::Seq { close: None });
728            return Ok(());
729        }
730
731        // For non-elements sequences, flush normally
732        self.flush_deferred_open_tag();
733        self.ensure_root_tag_written();
734
735        // Mark parent struct as having block content (sequences are block content)
736        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 we're skipping the enum wrapper, check if this is the variant name field_key
771        // that we should skip (variant_metadata already set up pending_field)
772        if let Some(ref variant_name) = self.skip_enum_wrapper
773            && key == variant_name
774        {
775            // Clear the skip flag - the wrapper struct's field_key is now consumed
776            // The next begin_struct will be the actual content struct
777            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                // Skip null values in HTML
788                self.pending_field.take();
789                self.pending_is_attribute = false;
790                self.pending_is_text = false;
791                Ok(())
792            }
793            ScalarValue::Bool(v) => {
794                // Handle boolean attribute values BEFORE flushing deferred tag
795                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                        // For boolean attributes, just add the name
801                        self.pending_attributes.push((attr_name.clone(), attr_name));
802                    }
803                    // false boolean attributes are omitted
804                    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
825/// Check if an element is a void element (no closing tag).
826fn is_void_element(name: &str) -> bool {
827    VOID_ELEMENTS.iter().any(|&v| v.eq_ignore_ascii_case(name))
828}
829
830/// Check if an attribute is a boolean attribute.
831fn is_boolean_attribute(name: &str) -> bool {
832    BOOLEAN_ATTRIBUTES
833        .iter()
834        .any(|&v| v.eq_ignore_ascii_case(name))
835}
836
837// =============================================================================
838// Public API
839// =============================================================================
840
841/// Serialize a value to an HTML string with default options (minified).
842pub fn to_string<T: Facet<'static>>(
843    value: &T,
844) -> Result<String, SerializeError<HtmlSerializeError>> {
845    to_string_with_options(value, &SerializeOptions::default())
846}
847
848/// Serialize a value to a pretty-printed HTML string.
849pub 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
855/// Serialize a value to an HTML string with custom options.
856pub 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
869/// Serialize a value to HTML bytes with default options.
870pub fn to_vec<T: Facet<'static>>(value: &T) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
871    to_vec_with_options(value, &SerializeOptions::default())
872}
873
874/// Serialize a value to HTML bytes with custom options.
875pub 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        // Text-only elements should be inline (no newlines)
933        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        // Nested elements should have newlines and indentation
949        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        // Void elements should not have a closing tag
1003        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        // false boolean attributes should be omitted
1053        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("&lt;script&gt;"),
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    /// Test nested elements using xml::elements with enum variants
1082    #[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        // Note: trailing newline is expected for pretty output
1152        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        // Empty container should still be inline since no block content
1168        assert_eq!(html_pretty, r#"<div class="empty"></div>"#);
1169    }
1170
1171    #[test]
1172    fn test_deeply_nested() {
1173        // Container with a span that has its own nested content
1174        #[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        // The quotes inside the onclick value should be escaped
1271        assert!(
1272            html.contains("onclick="),
1273            "Expected onclick attr, got: {}",
1274            html
1275        );
1276        assert!(
1277            html.contains("&quot;"),
1278            "Expected escaped quotes in onclick, got: {}",
1279            html
1280        );
1281    }
1282}