Skip to main content

facet_xml/
serializer.rs

1extern crate alloc;
2
3use alloc::{borrow::Cow, format, string::String, vec::Vec};
4use std::collections::HashMap;
5use std::io::Write;
6
7use facet_core::{Def, Facet, ScalarType};
8use facet_dom::{DomSerializeError, DomSerializer};
9use facet_reflect::Peek;
10
11use crate::escaping::EscapingWriter;
12
13pub use facet_dom::FloatFormatter;
14
15/// Write a scalar value directly to a writer.
16/// Returns `Ok(true)` if the value was a scalar and was written,
17/// `Ok(false)` if not a scalar, `Err` if write failed.
18fn write_scalar_value(
19    out: &mut dyn Write,
20    value: Peek<'_, '_>,
21    float_formatter: Option<FloatFormatter>,
22) -> std::io::Result<bool> {
23    // Unwrap transparent wrappers (e.g., PointsProxy -> String)
24    let value = value.innermost_peek();
25
26    // Handle Option<T> by unwrapping if Some
27    if let Def::Option(_) = &value.shape().def
28        && let Ok(opt) = value.into_option()
29    {
30        return match opt.value() {
31            Some(inner) => write_scalar_value(out, inner, float_formatter),
32            None => Ok(false),
33        };
34    }
35
36    let Some(scalar_type) = value.scalar_type() else {
37        // Try Display for Def::Scalar types (SmolStr, etc.)
38        if matches!(value.shape().def, Def::Scalar) && value.shape().vtable.has_display() {
39            write!(out, "{}", value)?;
40            return Ok(true);
41        }
42
43        // Handle enums - unit variants serialize to their variant name
44        if let Ok(enum_) = value.into_enum()
45            && let Ok(variant) = enum_.active_variant()
46            && variant.data.kind == facet_core::StructKind::Unit
47        {
48            // Use effective_name() if there's a rename, otherwise convert to lowerCamelCase
49            let variant_name = if variant.rename.is_some() {
50                Cow::Borrowed(variant.effective_name())
51            } else {
52                facet_dom::naming::to_element_name(variant.name)
53            };
54            out.write_all(variant_name.as_bytes())?;
55            return Ok(true);
56        }
57
58        return Ok(false);
59    };
60
61    match scalar_type {
62        ScalarType::Unit => {
63            out.write_all(b"null")?;
64        }
65        ScalarType::Bool => {
66            let b = value.get::<bool>().unwrap();
67            out.write_all(if *b { b"true" } else { b"false" })?;
68        }
69        ScalarType::Char => {
70            let c = value.get::<char>().unwrap();
71            let mut buf = [0u8; 4];
72            let s = c.encode_utf8(&mut buf);
73            out.write_all(s.as_bytes())?;
74        }
75        ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
76            let s = value.as_str().unwrap();
77            out.write_all(s.as_bytes())?;
78        }
79        ScalarType::F32 => {
80            let v = value.get::<f32>().unwrap();
81            if let Some(fmt) = float_formatter {
82                fmt(*v as f64, out)?;
83            } else {
84                write!(out, "{}", v)?;
85            }
86        }
87        ScalarType::F64 => {
88            let v = value.get::<f64>().unwrap();
89            if let Some(fmt) = float_formatter {
90                fmt(*v, out)?;
91            } else {
92                write!(out, "{}", v)?;
93            }
94        }
95        ScalarType::U8 => write!(out, "{}", value.get::<u8>().unwrap())?,
96        ScalarType::U16 => write!(out, "{}", value.get::<u16>().unwrap())?,
97        ScalarType::U32 => write!(out, "{}", value.get::<u32>().unwrap())?,
98        ScalarType::U64 => write!(out, "{}", value.get::<u64>().unwrap())?,
99        ScalarType::U128 => write!(out, "{}", value.get::<u128>().unwrap())?,
100        ScalarType::USize => write!(out, "{}", value.get::<usize>().unwrap())?,
101        ScalarType::I8 => write!(out, "{}", value.get::<i8>().unwrap())?,
102        ScalarType::I16 => write!(out, "{}", value.get::<i16>().unwrap())?,
103        ScalarType::I32 => write!(out, "{}", value.get::<i32>().unwrap())?,
104        ScalarType::I64 => write!(out, "{}", value.get::<i64>().unwrap())?,
105        ScalarType::I128 => write!(out, "{}", value.get::<i128>().unwrap())?,
106        ScalarType::ISize => write!(out, "{}", value.get::<isize>().unwrap())?,
107        #[cfg(feature = "net")]
108        ScalarType::IpAddr => write!(out, "{}", value.get::<core::net::IpAddr>().unwrap())?,
109        #[cfg(feature = "net")]
110        ScalarType::Ipv4Addr => write!(out, "{}", value.get::<core::net::Ipv4Addr>().unwrap())?,
111        #[cfg(feature = "net")]
112        ScalarType::Ipv6Addr => write!(out, "{}", value.get::<core::net::Ipv6Addr>().unwrap())?,
113        #[cfg(feature = "net")]
114        ScalarType::SocketAddr => write!(out, "{}", value.get::<core::net::SocketAddr>().unwrap())?,
115        _ => return Ok(false),
116    }
117    Ok(true)
118}
119
120/// Options for XML serialization.
121#[derive(Clone)]
122pub struct SerializeOptions {
123    /// Whether to pretty-print with indentation (default: false)
124    pub pretty: bool,
125    /// Indentation string for pretty-printing (default: "  ")
126    pub indent: Cow<'static, str>,
127    /// Custom formatter for floating-point numbers (f32 and f64).
128    /// If `None`, uses the default `Display` implementation.
129    pub float_formatter: Option<FloatFormatter>,
130    /// Whether to preserve entity references (like `&sup1;`, `&#92;`, `&#x5C;`) in string values.
131    ///
132    /// When `true`, entity references in strings are not escaped - the `&` in entity references
133    /// is left as-is instead of being escaped to `&amp;`. This is useful when serializing
134    /// content that already contains entity references (like HTML entities in SVG).
135    ///
136    /// Default: `false` (all `&` characters are escaped to `&amp;`).
137    pub preserve_entities: bool,
138}
139
140impl Default for SerializeOptions {
141    fn default() -> Self {
142        Self {
143            pretty: false,
144            indent: Cow::Borrowed("  "),
145            float_formatter: None,
146            preserve_entities: false,
147        }
148    }
149}
150
151impl core::fmt::Debug for SerializeOptions {
152    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        f.debug_struct("SerializeOptions")
154            .field("pretty", &self.pretty)
155            .field("indent", &self.indent)
156            .field("float_formatter", &self.float_formatter.map(|_| "..."))
157            .field("preserve_entities", &self.preserve_entities)
158            .finish()
159    }
160}
161
162impl SerializeOptions {
163    /// Create new default options (compact output).
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Enable pretty-printing with default indentation.
169    pub const fn pretty(mut self) -> Self {
170        self.pretty = true;
171        self
172    }
173
174    /// Set a custom indentation string (implies pretty-printing).
175    pub fn indent(mut self, indent: impl Into<Cow<'static, str>>) -> Self {
176        self.indent = indent.into();
177        self.pretty = true;
178        self
179    }
180
181    /// Set a custom formatter for floating-point numbers (f32 and f64).
182    ///
183    /// The formatter function receives the value as `f64` (f32 values are upcast)
184    /// and writes the formatted output to the provided writer.
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// # use facet::Facet;
190    /// # use facet_xml as xml;
191    /// # use facet_xml::{to_string_with_options, SerializeOptions};
192    /// # use std::io::Write;
193    /// fn fmt_g(value: f64, w: &mut dyn Write) -> std::io::Result<()> {
194    ///     // Format like C's %g: 6 significant digits, trim trailing zeros
195    ///     let s = format!("{:.6}", value);
196    ///     let s = s.trim_end_matches('0').trim_end_matches('.');
197    ///     write!(w, "{}", s)
198    /// }
199    ///
200    /// #[derive(Facet)]
201    /// struct Point {
202    ///     #[facet(xml::attribute)]
203    ///     x: f64,
204    ///     #[facet(xml::attribute)]
205    ///     y: f64,
206    /// }
207    ///
208    /// let point = Point { x: 1.5, y: 2.0 };
209    /// let options = SerializeOptions::new().float_formatter(fmt_g);
210    /// let xml = to_string_with_options(&point, &options).unwrap();
211    /// // "Point" becomes <point> (lowerCamelCase convention)
212    /// assert_eq!(xml, r#"<point x="1.5" y="2"></point>"#);
213    /// ```
214    pub fn float_formatter(mut self, formatter: FloatFormatter) -> Self {
215        self.float_formatter = Some(formatter);
216        self
217    }
218
219    /// Enable preservation of entity references in string values.
220    ///
221    /// When enabled, entity references like `&sup1;`, `&#92;`, `&#x5C;` are not escaped.
222    /// The `&` in recognized entity patterns is left as-is instead of being escaped to `&amp;`.
223    ///
224    /// This is useful when serializing content that already contains entity references,
225    /// such as HTML entities in SVG content.
226    pub const fn preserve_entities(mut self, preserve: bool) -> Self {
227        self.preserve_entities = preserve;
228        self
229    }
230}
231
232/// Well-known XML namespace URIs and their conventional prefixes.
233#[allow(dead_code)] // Used in namespace serialization
234const WELL_KNOWN_NAMESPACES: &[(&str, &str)] = &[
235    ("http://www.w3.org/2001/XMLSchema-instance", "xsi"),
236    ("http://www.w3.org/2001/XMLSchema", "xs"),
237    ("http://www.w3.org/XML/1998/namespace", "xml"),
238    ("http://www.w3.org/1999/xlink", "xlink"),
239    ("http://www.w3.org/2000/svg", "svg"),
240    ("http://www.w3.org/1999/xhtml", "xhtml"),
241    ("http://schemas.xmlsoap.org/soap/envelope/", "soap"),
242    ("http://www.w3.org/2003/05/soap-envelope", "soap12"),
243    ("http://schemas.android.com/apk/res/android", "android"),
244];
245
246#[derive(Debug)]
247pub struct XmlSerializeError {
248    msg: Cow<'static, str>,
249}
250
251impl core::fmt::Display for XmlSerializeError {
252    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253        f.write_str(&self.msg)
254    }
255}
256
257impl std::error::Error for XmlSerializeError {}
258
259/// XML serializer with configurable output options.
260///
261/// The output is designed to round-trip through `facet-xml`'s parser:
262/// - structs are elements whose children are field elements
263/// - sequences are elements whose children are repeated `<item>` elements
264/// - element names are treated as map keys; the root element name is ignored
265pub struct XmlSerializer {
266    out: Vec<u8>,
267    /// Stack of element names for closing tags
268    element_stack: Vec<String>,
269    /// Namespace URI -> prefix mapping for already-declared namespaces.
270    declared_namespaces: HashMap<String, String>,
271    /// Counter for auto-generating namespace prefixes (ns0, ns1, ...).
272    next_ns_index: usize,
273    /// The currently active default namespace (from xmlns="..." on an ancestor).
274    /// When set, elements in this namespace use unprefixed names.
275    current_default_ns: Option<String>,
276    /// Container-level default namespace (from xml::ns_all) for current struct
277    current_ns_all: Option<String>,
278    /// True if the current field is an attribute (vs element)
279    pending_is_attribute: bool,
280    /// True if the current field is text content (xml::text)
281    pending_is_text: bool,
282    /// True if the current field is an xml::elements list (no wrapper element)
283    pending_is_elements: bool,
284    /// True if the current field is a doctype field (xml::doctype)
285    pending_is_doctype: bool,
286    /// True if the current field is a tag field (xml::tag)
287    pending_is_tag: bool,
288    /// Pending namespace for the next field
289    pending_namespace: Option<String>,
290    /// Serialization options (pretty-printing, float formatting, etc.)
291    options: SerializeOptions,
292    /// Current indentation depth for pretty-printing
293    depth: usize,
294    /// True if we're collecting attributes (between element_start and children_start)
295    collecting_attributes: bool,
296    /// True if the next element should establish a default namespace (from ns_all)
297    pending_establish_default_ns: bool,
298}
299
300impl XmlSerializer {
301    /// Create a new XML serializer with default options.
302    pub fn new() -> Self {
303        Self::with_options(SerializeOptions::default())
304    }
305
306    /// Create a new XML serializer with the given options.
307    pub fn with_options(options: SerializeOptions) -> Self {
308        Self {
309            out: Vec::new(),
310            element_stack: Vec::new(),
311            declared_namespaces: HashMap::new(),
312            next_ns_index: 0,
313            current_default_ns: None,
314            current_ns_all: None,
315            pending_is_attribute: false,
316            pending_is_text: false,
317            pending_is_elements: false,
318            pending_is_doctype: false,
319            pending_is_tag: false,
320            pending_namespace: None,
321            options,
322            depth: 0,
323            collecting_attributes: false,
324            pending_establish_default_ns: false,
325        }
326    }
327
328    pub fn finish(self) -> Vec<u8> {
329        self.out
330    }
331
332    /// Write the opening part of an element tag: `<tag` (without the closing `>`)
333    /// This allows attributes to be written directly afterwards.
334    fn write_element_tag_start(&mut self, name: &str, namespace: Option<&str>) {
335        self.write_indent();
336        self.out.push(b'<');
337
338        // Track the close tag (may include prefix)
339        let close_tag: String;
340
341        // Handle namespace for element
342        if let Some(ns_uri) = namespace {
343            if self.current_default_ns.as_deref() == Some(ns_uri) {
344                // Element is in the current default namespace - use unprefixed form
345                self.out.extend_from_slice(name.as_bytes());
346                close_tag = name.to_string();
347            } else if self.pending_establish_default_ns {
348                // This is a struct root with ns_all - establish as default namespace
349                self.out.extend_from_slice(name.as_bytes());
350                self.out.extend_from_slice(b" xmlns=\"");
351                self.out.extend_from_slice(ns_uri.as_bytes());
352                self.out.push(b'"');
353                self.current_default_ns = Some(ns_uri.to_string());
354                self.pending_establish_default_ns = false;
355                close_tag = name.to_string();
356            } else {
357                // Field-level namespace - use prefix
358                let prefix = self.get_or_create_prefix(ns_uri);
359                self.out.extend_from_slice(prefix.as_bytes());
360                self.out.push(b':');
361                self.out.extend_from_slice(name.as_bytes());
362                // Write xmlns declaration for this prefix
363                self.out.extend_from_slice(b" xmlns:");
364                self.out.extend_from_slice(prefix.as_bytes());
365                self.out.extend_from_slice(b"=\"");
366                self.out.extend_from_slice(ns_uri.as_bytes());
367                self.out.push(b'"');
368                close_tag = format!("{}:{}", prefix, name);
369            }
370        } else {
371            self.out.extend_from_slice(name.as_bytes());
372            close_tag = name.to_string();
373        }
374
375        // Push the close tag for element_end
376        self.element_stack.push(close_tag);
377    }
378
379    /// Write an attribute directly to the output: ` name="escaped_value"`
380    /// Returns Ok(true) if written, Ok(false) if value wasn't a scalar (attribute skipped).
381    fn write_attribute(
382        &mut self,
383        name: &str,
384        value: Peek<'_, '_>,
385        namespace: Option<&str>,
386    ) -> std::io::Result<bool> {
387        // First, write the value to a temporary buffer to check if it's a scalar
388        let mut value_buf = Vec::new();
389        let written = write_scalar_value(
390            &mut EscapingWriter::attribute(&mut value_buf),
391            value,
392            self.options.float_formatter,
393        )?;
394
395        if !written {
396            // Not a scalar (e.g., None) - skip the attribute entirely
397            return Ok(false);
398        }
399
400        // Now write the attribute
401        self.out.push(b' ');
402        if let Some(ns_uri) = namespace {
403            let prefix = self.get_or_create_prefix(ns_uri);
404            // Write xmlns declaration
405            self.out.extend_from_slice(b"xmlns:");
406            self.out.extend_from_slice(prefix.as_bytes());
407            self.out.extend_from_slice(b"=\"");
408            self.out.extend_from_slice(ns_uri.as_bytes());
409            self.out.extend_from_slice(b"\" ");
410            // Write prefixed attribute
411            self.out.extend_from_slice(prefix.as_bytes());
412            self.out.push(b':');
413        }
414        self.out.extend_from_slice(name.as_bytes());
415        self.out.extend_from_slice(b"=\"");
416        self.out.extend_from_slice(&value_buf);
417        self.out.push(b'"');
418        Ok(true)
419    }
420
421    /// Finish the element opening tag by writing `>` and incrementing depth.
422    fn write_element_tag_end(&mut self) {
423        self.out.push(b'>');
424        self.write_newline();
425        self.depth += 1;
426    }
427
428    fn write_close_tag(&mut self, name: &str) {
429        self.depth = self.depth.saturating_sub(1);
430        self.write_indent();
431        self.out.extend_from_slice(b"</");
432        self.out.extend_from_slice(name.as_bytes());
433        self.out.push(b'>');
434        self.write_newline();
435    }
436
437    fn write_text_escaped(&mut self, text: &str) {
438        use std::io::Write;
439        if self.options.preserve_entities {
440            let escaped = escape_preserving_entities(text, false);
441            self.out.extend_from_slice(escaped.as_bytes());
442        } else {
443            // Use EscapingWriter for consistency with attribute escaping
444            let _ = EscapingWriter::text(&mut self.out).write_all(text.as_bytes());
445        }
446    }
447
448    /// Write indentation for the current depth (if pretty-printing is enabled).
449    fn write_indent(&mut self) {
450        if self.options.pretty {
451            for _ in 0..self.depth {
452                self.out.extend_from_slice(self.options.indent.as_bytes());
453            }
454        }
455    }
456
457    /// Write a newline (if pretty-printing is enabled).
458    fn write_newline(&mut self) {
459        if self.options.pretty {
460            self.out.push(b'\n');
461        }
462    }
463
464    /// Get or create a prefix for the given namespace URI.
465    fn get_or_create_prefix(&mut self, namespace_uri: &str) -> String {
466        // Check if we've already assigned a prefix to this URI
467        if let Some(prefix) = self.declared_namespaces.get(namespace_uri) {
468            return prefix.clone();
469        }
470
471        // Try well-known namespaces
472        let prefix = WELL_KNOWN_NAMESPACES
473            .iter()
474            .find(|(uri, _)| *uri == namespace_uri)
475            .map(|(_, prefix)| (*prefix).to_string())
476            .unwrap_or_else(|| {
477                // Auto-generate a prefix
478                let prefix = format!("ns{}", self.next_ns_index);
479                self.next_ns_index += 1;
480                prefix
481            });
482
483        // Ensure the prefix isn't already in use for a different namespace
484        let final_prefix = if self.declared_namespaces.values().any(|p| p == &prefix) {
485            let prefix = format!("ns{}", self.next_ns_index);
486            self.next_ns_index += 1;
487            prefix
488        } else {
489            prefix
490        };
491
492        self.declared_namespaces
493            .insert(namespace_uri.to_string(), final_prefix.clone());
494        final_prefix
495    }
496
497    fn clear_field_state_impl(&mut self) {
498        self.pending_is_attribute = false;
499        self.pending_is_text = false;
500        self.pending_is_elements = false;
501        self.pending_is_doctype = false;
502        self.pending_is_tag = false;
503        self.pending_namespace = None;
504    }
505}
506
507impl Default for XmlSerializer {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513impl DomSerializer for XmlSerializer {
514    type Error = XmlSerializeError;
515
516    fn element_start(&mut self, tag: &str, namespace: Option<&str>) -> Result<(), Self::Error> {
517        // Priority: explicit namespace > pending_namespace > current_ns_all (for struct roots)
518        let ns = namespace
519            .map(|s| s.to_string())
520            .or_else(|| self.pending_namespace.take())
521            .or_else(|| self.current_ns_all.clone());
522
523        // Write the opening tag immediately: `<tag` (attributes will follow)
524        self.write_element_tag_start(tag, ns.as_deref());
525        self.collecting_attributes = true;
526
527        Ok(())
528    }
529
530    fn attribute(
531        &mut self,
532        name: &str,
533        value: Peek<'_, '_>,
534        namespace: Option<&str>,
535    ) -> Result<(), Self::Error> {
536        // Attributes must come before children_start
537        if !self.collecting_attributes {
538            return Err(XmlSerializeError {
539                msg: Cow::Borrowed("attribute() called after children_start()"),
540            });
541        }
542
543        // Use the pending namespace from field_metadata if no explicit namespace given
544        let ns: Option<String> = match namespace {
545            Some(ns) => Some(ns.to_string()),
546            None => self.pending_namespace.clone(),
547        };
548
549        // Write directly to output
550        self.write_attribute(name, value, ns.as_deref())
551            .map_err(|e| XmlSerializeError {
552                msg: Cow::Owned(format!("write error: {}", e)),
553            })?;
554        Ok(())
555    }
556
557    fn children_start(&mut self) -> Result<(), Self::Error> {
558        // Close the element opening tag
559        self.write_element_tag_end();
560        self.collecting_attributes = false;
561        Ok(())
562    }
563
564    fn children_end(&mut self) -> Result<(), Self::Error> {
565        Ok(())
566    }
567
568    fn element_end(&mut self, _tag: &str) -> Result<(), Self::Error> {
569        if let Some(close_tag) = self.element_stack.pop() {
570            self.write_close_tag(&close_tag);
571        }
572        Ok(())
573    }
574
575    fn text(&mut self, content: &str) -> Result<(), Self::Error> {
576        self.write_text_escaped(content);
577        Ok(())
578    }
579
580    fn struct_metadata(&mut self, shape: &facet_core::Shape) -> Result<(), Self::Error> {
581        // Extract xml::ns_all attribute from the struct
582        self.current_ns_all = shape
583            .attributes
584            .iter()
585            .find(|attr| attr.ns == Some("xml") && attr.key == "ns_all")
586            .and_then(|attr| attr.get_as::<&str>().copied())
587            .map(String::from);
588
589        // If ns_all is set, the next element_start should establish it as default namespace
590        self.pending_establish_default_ns = self.current_ns_all.is_some();
591
592        Ok(())
593    }
594
595    fn field_metadata(&mut self, field: &facet_reflect::FieldItem) -> Result<(), Self::Error> {
596        let Some(field_def) = field.field else {
597            // For flattened map entries, treat them as attributes
598            self.pending_is_attribute = true;
599            self.pending_is_text = false;
600            self.pending_is_elements = false;
601            self.pending_is_doctype = false;
602            self.pending_is_tag = false;
603            return Ok(());
604        };
605
606        // Check if this field is an attribute
607        self.pending_is_attribute = field_def.get_attr(Some("xml"), "attribute").is_some();
608        // Check if this field is text content
609        self.pending_is_text = field_def.get_attr(Some("xml"), "text").is_some();
610        // Check if this field is an xml::elements list
611        self.pending_is_elements = field_def.get_attr(Some("xml"), "elements").is_some();
612        // Check if this field is a doctype field
613        self.pending_is_doctype = field_def.get_attr(Some("xml"), "doctype").is_some();
614        // Check if this field is a tag field
615        self.pending_is_tag = field_def.get_attr(Some("xml"), "tag").is_some();
616
617        // Extract xml::ns attribute from the field
618        if let Some(ns_attr) = field_def.get_attr(Some("xml"), "ns")
619            && let Some(ns_uri) = ns_attr.get_as::<&str>().copied()
620        {
621            self.pending_namespace = Some(ns_uri.to_string());
622        } else if !self.pending_is_attribute && !self.pending_is_text {
623            // Apply ns_all to elements only (or None if no ns_all)
624            self.pending_namespace = self.current_ns_all.clone();
625        } else {
626            // Attributes and text don't get namespace from ns_all
627            self.pending_namespace = None;
628        }
629
630        Ok(())
631    }
632
633    fn variant_metadata(
634        &mut self,
635        _variant: &'static facet_core::Variant,
636    ) -> Result<(), Self::Error> {
637        Ok(())
638    }
639
640    fn is_attribute_field(&self) -> bool {
641        self.pending_is_attribute
642    }
643
644    fn is_text_field(&self) -> bool {
645        self.pending_is_text
646    }
647
648    fn is_elements_field(&self) -> bool {
649        self.pending_is_elements
650    }
651
652    fn is_doctype_field(&self) -> bool {
653        self.pending_is_doctype
654    }
655
656    fn is_tag_field(&self) -> bool {
657        self.pending_is_tag
658    }
659
660    fn doctype(&mut self, content: &str) -> Result<(), Self::Error> {
661        // Emit DOCTYPE declaration
662        self.out.write_all(b"<!DOCTYPE ").unwrap();
663        self.out.write_all(content.as_bytes()).unwrap();
664        self.out.write_all(b">").unwrap();
665        if self.options.pretty {
666            self.out.write_all(b"\n").unwrap();
667        }
668        Ok(())
669    }
670
671    fn clear_field_state(&mut self) {
672        self.clear_field_state_impl();
673    }
674
675    fn format_float(&self, value: f64) -> String {
676        if let Some(formatter) = self.options.float_formatter {
677            let mut buf = Vec::new();
678            // If the formatter fails, fall back to default Display
679            if formatter(value, &mut buf).is_ok()
680                && let Ok(s) = String::from_utf8(buf)
681            {
682                return s;
683            }
684        }
685        value.to_string()
686    }
687
688    fn serialize_none(&mut self) -> Result<(), Self::Error> {
689        // For XML, None values should not emit any content
690        Ok(())
691    }
692
693    fn format_namespace(&self) -> Option<&'static str> {
694        Some("xml")
695    }
696}
697
698/// Serialize a value to XML bytes with default options.
699pub fn to_vec<'facet, T>(value: &'_ T) -> Result<Vec<u8>, DomSerializeError<XmlSerializeError>>
700where
701    T: Facet<'facet> + ?Sized,
702{
703    to_vec_with_options(value, &SerializeOptions::default())
704}
705
706/// Serialize a value to XML bytes with custom options.
707pub fn to_vec_with_options<'facet, T>(
708    value: &'_ T,
709    options: &SerializeOptions,
710) -> Result<Vec<u8>, DomSerializeError<XmlSerializeError>>
711where
712    T: Facet<'facet> + ?Sized,
713{
714    let mut serializer = XmlSerializer::with_options(options.clone());
715    facet_dom::serialize(&mut serializer, Peek::new(value))?;
716    Ok(serializer.finish())
717}
718
719/// Serialize a value to an XML string with default options.
720pub fn to_string<'facet, T>(value: &'_ T) -> Result<String, DomSerializeError<XmlSerializeError>>
721where
722    T: Facet<'facet> + ?Sized,
723{
724    let bytes = to_vec(value)?;
725    // SAFETY: XmlSerializer produces valid UTF-8
726    Ok(String::from_utf8(bytes).expect("XmlSerializer produces valid UTF-8"))
727}
728
729/// Serialize a value to a pretty-printed XML string with default indentation.
730pub fn to_string_pretty<'facet, T>(
731    value: &'_ T,
732) -> Result<String, DomSerializeError<XmlSerializeError>>
733where
734    T: Facet<'facet> + ?Sized,
735{
736    to_string_with_options(value, &SerializeOptions::default().pretty())
737}
738
739/// Serialize a value to an XML string with custom options.
740pub fn to_string_with_options<'facet, T>(
741    value: &'_ T,
742    options: &SerializeOptions,
743) -> Result<String, DomSerializeError<XmlSerializeError>>
744where
745    T: Facet<'facet> + ?Sized,
746{
747    let bytes = to_vec_with_options(value, options)?;
748    // SAFETY: XmlSerializer produces valid UTF-8
749    Ok(String::from_utf8(bytes).expect("XmlSerializer produces valid UTF-8"))
750}
751
752/// Escape special characters while preserving entity references.
753///
754/// Recognizes entity reference patterns:
755/// - Named entities: `&name;` (alphanumeric name)
756/// - Decimal numeric entities: `&#digits;`
757/// - Hexadecimal numeric entities: `&#xhex;` or `&#Xhex;`
758fn escape_preserving_entities(s: &str, is_attribute: bool) -> String {
759    let mut result = String::with_capacity(s.len());
760    let chars: Vec<char> = s.chars().collect();
761    let mut i = 0;
762
763    while i < chars.len() {
764        let c = chars[i];
765        match c {
766            '<' => result.push_str("&lt;"),
767            '>' => result.push_str("&gt;"),
768            '"' if is_attribute => result.push_str("&quot;"),
769            '&' => {
770                // Check if this is the start of an entity reference
771                if let Some(entity_len) = try_parse_entity_reference(&chars[i..]) {
772                    // It's a valid entity reference - copy it as-is
773                    for j in 0..entity_len {
774                        result.push(chars[i + j]);
775                    }
776                    i += entity_len;
777                    continue;
778                } else {
779                    // Not a valid entity reference - escape the ampersand
780                    result.push_str("&amp;");
781                }
782            }
783            _ => result.push(c),
784        }
785        i += 1;
786    }
787
788    result
789}
790
791/// Try to parse an entity reference starting at the given position.
792/// Returns the length of the entity reference if valid, or None if not.
793///
794/// Valid patterns:
795/// - `&name;` where name is one or more alphanumeric characters
796/// - `&#digits;` where digits are decimal digits
797/// - `&#xhex;` or `&#Xhex;` where hex is hexadecimal digits
798fn try_parse_entity_reference(chars: &[char]) -> Option<usize> {
799    if chars.is_empty() || chars[0] != '&' {
800        return None;
801    }
802
803    // Need at least `&x;` (3 chars minimum)
804    if chars.len() < 3 {
805        return None;
806    }
807
808    let mut len = 1; // Start after '&'
809
810    if chars[1] == '#' {
811        // Numeric entity reference
812        len = 2;
813
814        if len < chars.len() && (chars[len] == 'x' || chars[len] == 'X') {
815            // Hexadecimal: &#xHEX;
816            len += 1;
817            let start = len;
818            while len < chars.len() && chars[len].is_ascii_hexdigit() {
819                len += 1;
820            }
821            // Need at least one hex digit
822            if len == start {
823                return None;
824            }
825        } else {
826            // Decimal: &#DIGITS;
827            let start = len;
828            while len < chars.len() && chars[len].is_ascii_digit() {
829                len += 1;
830            }
831            // Need at least one digit
832            if len == start {
833                return None;
834            }
835        }
836    } else {
837        // Named entity reference: &NAME;
838        if !chars[len].is_ascii_alphabetic() && chars[len] != '_' {
839            return None;
840        }
841        len += 1;
842        while len < chars.len()
843            && (chars[len].is_ascii_alphanumeric()
844                || chars[len] == '_'
845                || chars[len] == '-'
846                || chars[len] == '.')
847        {
848            len += 1;
849        }
850    }
851
852    // Must end with ';'
853    if len >= chars.len() || chars[len] != ';' {
854        return None;
855    }
856
857    Some(len + 1) // Include the semicolon
858}