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 elements where whitespace is significant (preformatted content).
22/// These elements should NOT have indentation or newlines added during serialization.
23const WHITESPACE_SENSITIVE_ELEMENTS: &[&str] = &["pre", "code", "textarea", "script", "style"];
24
25/// HTML5 boolean attributes that are written without a value when true.
26const 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/// Options for HTML serialization.
58#[derive(Clone)]
59pub struct SerializeOptions {
60    /// Whether to pretty-print with indentation (default: false for minified output)
61    pub pretty: bool,
62    /// Indentation string for pretty-printing (default: "  ")
63    pub indent: Cow<'static, str>,
64    /// Custom formatter for floating-point numbers (f32 and f64).
65    pub float_formatter: Option<FloatFormatter>,
66    /// Whether to use self-closing syntax for void elements (default: false)
67    /// When false: `<br>`, when true: `<br />`
68    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    /// Create new default options (minified output).
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Enable pretty-printing with default indentation.
100    pub fn pretty(mut self) -> Self {
101        self.pretty = true;
102        self
103    }
104
105    /// Set a custom indentation string (implies pretty-printing).
106    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    /// Set a custom formatter for floating-point numbers.
113    pub fn float_formatter(mut self, formatter: FloatFormatter) -> Self {
114        self.float_formatter = Some(formatter);
115        self
116    }
117
118    /// Use self-closing syntax for void elements (`<br />` instead of `<br>`).
119    pub fn self_closing_void(mut self, value: bool) -> Self {
120        self.self_closing_void = value;
121        self
122    }
123}
124
125/// Error type for HTML serialization.
126#[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        /// True if we've written any content inside this struct (text or child elements)
145        has_content: bool,
146        /// True if we've written block content (child elements) that requires newlines
147        has_block_content: bool,
148        /// True if we're inside a whitespace-sensitive element (pre, code, etc.)
149        in_preformatted: bool,
150    },
151    Seq {
152        close: Option<String>,
153        /// True if we're inside a whitespace-sensitive element (pre, code, etc.)
154        in_preformatted: bool,
155    },
156}
157
158/// HTML serializer with configurable output options.
159pub struct HtmlSerializer {
160    out: Vec<u8>,
161    stack: Vec<Ctx>,
162    pending_field: Option<String>,
163    /// True if the current field is an attribute
164    pending_is_attribute: bool,
165    /// True if the current field is text content
166    pending_is_text: bool,
167    /// True if the current field is an elements list
168    pending_is_elements: bool,
169    /// Buffered attributes for the current element (name, value)
170    pending_attributes: Vec<(String, String)>,
171    /// True if we've written the opening root tag
172    root_tag_written: bool,
173    /// Name to use for the root element
174    root_element_name: Option<String>,
175    /// Deferred element tag - wait to write opening tag until we've collected attributes
176    deferred_open_tag: Option<(String, String)>,
177    /// Stack of elements state (true = in elements list)
178    elements_stack: Vec<bool>,
179    /// When set, we're about to serialize an externally-tagged enum inside xml::elements.
180    /// The next begin_struct() should be skipped (it's the wrapper struct), and the
181    /// following field_key(variant_name) should also be skipped because variant_metadata
182    /// already set up pending_field with the variant name.
183    skip_enum_wrapper: Option<String>,
184    /// When true, the next scalar value is a tag name for a custom element
185    pending_is_tag: bool,
186    /// Serialization options
187    options: SerializeOptions,
188    /// Current indentation depth
189    depth: usize,
190    /// DOCTYPE declaration to emit before the root element (e.g., "html" for `<!DOCTYPE html>`)
191    pending_doctype: Option<String>,
192}
193
194impl HtmlSerializer {
195    /// Create a new HTML serializer with default options (minified).
196    pub fn new() -> Self {
197        Self::with_options(SerializeOptions::default())
198    }
199
200    /// Create a new HTML serializer with the given options.
201    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    /// Finish serialization and return the output bytes.
223    pub fn finish(mut self) -> Vec<u8> {
224        // Flush any pending deferred tag
225        self.flush_deferred_open_tag();
226
227        // Close any remaining non-root elements
228        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    /// Flush the deferred open tag.
256    ///
257    /// If `inline` is true, the content will be inline (text), so we don't add
258    /// a newline after the opening tag. If false, content is block-level (child
259    /// elements) so we add a newline and increase indentation.
260    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            // Emit DOCTYPE declaration before the root element if present
263            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            // Write buffered attributes
275            let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
276            for (attr_name, attr_value) in attrs {
277                // Handle boolean attributes
278                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                    // Skip false/empty boolean attributes
284                    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            // Only add newline and increase depth for block content
305            if !inline {
306                self.write_newline();
307                self.depth += 1;
308            }
309
310            // If this was the root element, mark it as written
311            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        // Write buffered attributes
327        let attrs: Vec<_> = self.pending_attributes.drain(..).collect();
328        for (attr_name, attr_value) in attrs {
329            // Handle boolean attributes
330            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                // Skip false/empty boolean attributes
336                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    /// Write a closing tag.
358    ///
359    /// - `indent_before`: if true, decrement depth and write indent before the tag
360    /// - `newline_after`: if true, write a newline after the tag
361    fn write_close_tag_ex(&mut self, name: &str, indent_before: bool, newline_after: bool) {
362        if is_void_element(name) {
363            return; // Void elements have no closing tag
364        }
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"&amp;"),
385                b'<' => self.out.extend_from_slice(b"&lt;"),
386                b'>' => self.out.extend_from_slice(b"&gt;"),
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"&amp;"),
396                b'<' => self.out.extend_from_slice(b"&lt;"),
397                b'>' => self.out.extend_from_slice(b"&gt;"),
398                b'"' => self.out.extend_from_slice(b"&quot;"),
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    /// Check if we're currently inside a whitespace-sensitive element.
420    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            // Write buffered attributes
462            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            // Check if we're in elements mode - if so, don't wrap
500            if self.elements_stack.last().copied().unwrap_or(false) {
501                // In elements mode - the field name is the element tag
502                self.write_open_tag(&field_name);
503                return Ok(Some(field_name));
504            }
505
506            // Handle text content
507            if self.pending_is_text {
508                self.pending_is_text = false;
509                return Ok(None); // Text content - no element wrapper
510            }
511
512            // Handle attributes - shouldn't get here for attributes
513            if self.pending_is_attribute {
514                self.pending_is_attribute = false;
515                return Ok(None);
516            }
517
518            // Regular child element
519            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        // Handle tag field for custom elements BEFORE flushing deferred tag
527        // The tag value sets the element name for the current deferred element
528        if self.pending_is_tag {
529            self.pending_is_tag = false;
530            self.pending_field.take();
531            // Update the deferred tag with the custom element's tag name
532            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                // If there's no deferred tag yet, set up pending_field for when begin_struct is called
537                self.pending_field = Some(value.to_string());
538            }
539            // Also update the close tag in the current struct context
540            if let Some(Ctx::Struct { close, .. }) = self.stack.last_mut() {
541                *close = Some(value.to_string());
542            }
543            return Ok(());
544        }
545
546        // Handle attribute values BEFORE flushing deferred tag
547        // Attributes need to be buffered, not written as content
548        if self.pending_is_attribute
549            && let Some(attr_name) = self.pending_field.take()
550        {
551            self.pending_is_attribute = false;
552            // Special handling for "doctype" pseudo-attribute on the root element
553            // This is emitted as <!DOCTYPE {value}> before the opening tag, not as an attribute
554            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        // Handle text content - flush deferred tag first (inline mode), then write text
563        if self.pending_is_text {
564            // Use inline mode so we don't add newline after opening tag
565            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            // Mark parent struct as having content (but NOT block content)
571            if let Some(Ctx::Struct { has_content, .. }) = self.stack.last_mut() {
572                *has_content = true;
573            }
574            return Ok(());
575        }
576
577        // Regular element content
578        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        // Get the element name from the shape (respecting rename attribute)
601        let element_name = shape
602            .get_builtin_attr_value::<&str>("rename")
603            .unwrap_or(shape.type_identifier);
604
605        // If this is the root element (stack only has Root context), save the name
606        if matches!(self.stack.last(), Some(Ctx::Root)) {
607            self.root_element_name = Some(element_name.to_string());
608        }
609
610        // If we're inside an xml::elements list and no pending field is set,
611        // use the shape's element name. However, if variant_metadata already
612        // set a pending_field (for enums), don't override it.
613        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        // For flattened map entries (field is None), treat as attributes
625        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            // Flattened map entries are attributes
632            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 we're inside an xml::elements list, set the pending field to the variant name
645        // and mark that we should skip the externally-tagged wrapper struct.
646        //
647        // For externally-tagged enums, the serialization flow is:
648        //   1. variant_metadata(variant) - we're here
649        //   2. begin_struct() - creates wrapper struct (we want to SKIP this)
650        //   3. field_key(variant.name) - sets field name (we want to SKIP this)
651        //   4. shared_serialize(inner) - serializes the actual content
652        //
653        // We set pending_field to the variant name, and skip_enum_wrapper to tell
654        // begin_struct() to not create an element, and field_key() to not override
655        // the pending_field we just set.
656        if self.elements_stack.last() == Some(&true) {
657            // Check if this variant is marked as text content (e.g., #[facet(html::text)])
658            // Text variants should be serialized as text content, not as elements.
659            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                // Custom element variant - DON'T set pending_field yet.
664                // The tag name will come from the html::tag field in the struct.
665                // Set skip flag so we skip the wrapper struct machinery.
666                self.skip_enum_wrapper = Some(variant.name.to_string());
667            } else {
668                // Get the element name from the variant (respecting rename attribute)
669                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                // Set the skip flag with the variant name so field_key knows what to skip
675                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        // Flush any deferred tag from parent before starting a new struct
687        self.flush_deferred_open_tag();
688
689        // Mark nearest ancestor struct as having block content (child elements)
690        // We need to find the Struct even if there's a Seq in between (for elements lists)
691        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 we're skipping the enum wrapper struct (for xml::elements enum serialization),
705        // just push a struct context without creating any element.
706        // Keep the elements_stack state - we're still inside the elements list.
707        if self.skip_enum_wrapper.is_some() {
708            // Propagate the current elements state to maintain the "in elements" context
709            let in_elements = self.elements_stack.last().copied().unwrap_or(false);
710            // Propagate preformatted context from parent
711            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 we're starting a new struct that's an elements list item
723        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                // Root struct - defer the opening tag until we've collected attributes
733                // The element name was set in struct_metadata
734                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                // Nested struct - defer the opening tag
752                // Check if parent is preformatted, or if this element is preformatted
753                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            // Flush any remaining deferred tag (in case struct had only attributes or empty content)
790            // Use inline mode if we never had any content
791            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                // Check if parent is a block context (Seq or Struct with block content)
797                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                // If we had block content, indent before closing tag
807                // If parent is block context, add newline after (so next sibling is on new line)
808                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 this is an elements list, DON'T flush the deferred tag yet.
820        // Wait until we have actual items to determine if we have block content.
821        if self.pending_is_elements {
822            self.pending_is_elements = false;
823            self.elements_stack.push(true);
824            self.pending_field.take(); // Consume the field name
825            // Propagate preformatted context from parent
826            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        // For non-elements sequences, flush normally
835        self.flush_deferred_open_tag();
836        self.ensure_root_tag_written();
837
838        // Mark parent struct as having block content (sequences are block content)
839        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        // Propagate preformatted context from parent
850        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 we're skipping the enum wrapper, check if this is the variant name field_key
884        // that we should skip (variant_metadata already set up pending_field)
885        if let Some(ref variant_name) = self.skip_enum_wrapper
886            && key == variant_name
887        {
888            // Clear the skip flag - the wrapper struct's field_key is now consumed
889            // The next begin_struct will be the actual content struct
890            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                // Skip null values in HTML
901                self.pending_field.take();
902                self.pending_is_attribute = false;
903                self.pending_is_text = false;
904                Ok(())
905            }
906            ScalarValue::Bool(v) => {
907                // Handle boolean attribute values BEFORE flushing deferred tag
908                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                        // For boolean attributes, just add the name
914                        self.pending_attributes.push((attr_name.clone(), attr_name));
915                    }
916                    // false boolean attributes are omitted
917                    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
942/// Check if an element is a void element (no closing tag).
943fn is_void_element(name: &str) -> bool {
944    VOID_ELEMENTS.iter().any(|&v| v.eq_ignore_ascii_case(name))
945}
946
947/// Check if an attribute is a boolean attribute.
948fn is_boolean_attribute(name: &str) -> bool {
949    BOOLEAN_ATTRIBUTES
950        .iter()
951        .any(|&v| v.eq_ignore_ascii_case(name))
952}
953
954/// Check if an element is whitespace-sensitive (preformatted content).
955fn is_whitespace_sensitive(name: &str) -> bool {
956    WHITESPACE_SENSITIVE_ELEMENTS
957        .iter()
958        .any(|&v| v.eq_ignore_ascii_case(name))
959}
960
961// =============================================================================
962// Public API
963// =============================================================================
964
965/// Serialize a value to an HTML string with default options (minified).
966pub fn to_string<T: Facet<'static>>(
967    value: &T,
968) -> Result<String, SerializeError<HtmlSerializeError>> {
969    to_string_with_options(value, &SerializeOptions::default())
970}
971
972/// Serialize a value to a pretty-printed HTML string.
973pub 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
979/// Serialize a value to an HTML string with custom options.
980pub 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
993/// Serialize a value to HTML bytes with default options.
994pub fn to_vec<T: Facet<'static>>(value: &T) -> Result<Vec<u8>, SerializeError<HtmlSerializeError>> {
995    to_vec_with_options(value, &SerializeOptions::default())
996}
997
998/// Serialize a value to HTML bytes with custom options.
999pub 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        // Text-only elements should be inline (no newlines)
1057        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        // Nested elements should have newlines and indentation
1073        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        // Void elements should not have a closing tag
1127        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        // false boolean attributes should be omitted
1177        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("&lt;script&gt;"),
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    /// Test nested elements using xml::elements with enum variants
1206    #[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        // Note: trailing newline is expected for pretty output
1276        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        // Empty container should still be inline since no block content
1292        assert_eq!(html_pretty, r#"<div class="empty"></div>"#);
1293    }
1294
1295    #[test]
1296    fn test_deeply_nested() {
1297        // Container with a span that has its own nested content
1298        #[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        // The quotes inside the onclick value should be escaped
1395        assert!(
1396            html.contains("onclick="),
1397            "Expected onclick attr, got: {}",
1398            html
1399        );
1400        assert!(
1401            html.contains("&quot;"),
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        // Parse HTML with DOCTYPE
1414        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        // Verify DOCTYPE was captured
1425        assert_eq!(
1426            parsed.doctype,
1427            Some("html".to_string()),
1428            "DOCTYPE should be captured during parsing"
1429        );
1430
1431        // Serialize back to HTML
1432        let output = to_string(&parsed).unwrap();
1433
1434        // Verify DOCTYPE is present in output
1435        assert!(
1436            output.starts_with("<!DOCTYPE html>"),
1437            "Output should start with DOCTYPE declaration, got: {}",
1438            output
1439        );
1440
1441        // Parse the output again to verify roundtrip
1442        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}