Skip to main content

serde_saphyr/ser/
serializer.rs

1mod compound;
2mod helpers;
3
4#[doc(hidden)]
5pub use self::compound::{MapSer, SeqSer, StructVariantSer, TupleSer};
6
7use self::helpers::{StrCapture, scalar_key_to_string};
8use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
9use nohash_hasher::BuildNoHashHasher;
10use serde_core::ser::Error as _;
11use serde_core::ser::{Serialize, Serializer};
12use std::collections::HashMap;
13use std::fmt::Write;
14
15use crate::long_strings::{NAME_FOLD_STR, NAME_LIT_STR};
16use crate::tag::{YAML_TAG_NAMESPACE, simple_enum_variant_name, yaml_core_type_tag_name};
17
18#[cfg(feature = "comments")]
19use super::NAME_TUPLE_COMMENTED;
20#[cfg(feature = "comments")]
21use super::options::CommentPosition;
22use super::options::SerializerOptions;
23use super::quoting::{
24    escape_double_quoted, is_auto_block_scalar_readable, is_block_scalar_content_safe,
25    is_controll_which_needs_escaping, is_plain_value_safe,
26};
27use super::{
28    Error, NAME_DOUBLE_QUOTED, NAME_FLOW_MAP, NAME_FLOW_SEQ, NAME_NULLABLE_TILDE,
29    NAME_SINGLE_QUOTED, NAME_SPACE_AFTER, NAME_TUPLE_ANCHOR, NAME_TUPLE_TAGGED, NAME_TUPLE_WEAK,
30    Result, checked_depth_add, checked_indentation, wrapping, zmij_format,
31};
32
33// ------------------------------------------------------------
34// Core serializer
35// ------------------------------------------------------------
36
37#[derive(Clone, Copy, PartialEq, Eq)]
38enum PendingFlow {
39    AnySeq,
40    AnyMap,
41}
42#[derive(Clone, Copy, PartialEq, Eq)]
43enum StrStyle {
44    Literal, // |
45    Folded,  // >
46}
47
48/// Block-scalar style request waiting to be consumed by the next string.
49///
50/// Wrapper types create `Explicit` requests, while the serializer's readability
51/// heuristics create `Automatic` requests. Keeping the origin together with the
52/// style prevents a stale "automatic" flag from existing when no style is pending.
53#[derive(Clone, Copy, PartialEq, Eq)]
54enum PendingStrStyle {
55    Explicit(StrStyle),
56    Automatic(StrStyle),
57}
58
59impl PendingStrStyle {
60    /// Return the requested style and whether it came from automatic selection.
61    fn into_parts(self) -> (StrStyle, bool) {
62        match self {
63            Self::Explicit(style) => (style, false),
64            Self::Automatic(style) => (style, true),
65        }
66    }
67}
68
69// Numeric anchor id used internally.
70type AnchorId = u32;
71
72const MAX_ANCHOR_NAME_BYTES: usize = 256;
73
74/// A tag staged by a wrapper or by an internal serializer feature.
75///
76/// Equality and anchor compatibility use the resolved identity. `emitted` is
77/// only a presentation choice, retained so existing `!!binary` and tagged-enum
78/// output stays compact when those features stage the tag first.
79#[derive(Clone)]
80struct PendingTag {
81    resolved: String,
82    emitted: String,
83    /// Whether this tag has already replaced one external enum wrapper.
84    variant_selector_claimed: bool,
85}
86
87#[inline]
88const fn is_global_tag_uri_byte(byte: u8) -> bool {
89    byte.is_ascii_alphanumeric()
90        || matches!(
91            byte,
92            b'-' | b'#'
93                | b';'
94                | b'/'
95                | b'?'
96                | b':'
97                | b'@'
98                | b'&'
99                | b'='
100                | b'+'
101                | b'$'
102                | b','
103                | b'_'
104                | b'.'
105                | b'!'
106                | b'~'
107                | b'*'
108                | b'\''
109                | b'('
110                | b')'
111        )
112}
113
114#[inline]
115const fn is_local_tag_suffix_byte(byte: u8) -> bool {
116    is_global_tag_uri_byte(byte) && !matches!(byte, b'!' | b',')
117}
118
119fn push_percent_encoded_byte(out: &mut String, byte: u8, safe: bool) {
120    const HEX: &[u8; 16] = b"0123456789ABCDEF";
121    // A literal '%' must always be escaped: the YAML parser interprets it as
122    // the start of an encoded UTF-8 scalar and decodes it again.
123    if byte != b'%' && safe {
124        out.push(char::from(byte));
125    } else {
126        out.push('%');
127        out.push(char::from(HEX[usize::from(byte >> 4)]));
128        out.push(char::from(HEX[usize::from(byte & 0x0f)]));
129    }
130}
131
132fn push_percent_encoded(out: &mut String, value: &str, safe: fn(u8) -> bool) {
133    for byte in value.bytes() {
134        push_percent_encoded_byte(out, byte, safe(byte));
135    }
136}
137
138/// Whether `host_and_port` is an IP-literal with an optional numeric port.
139fn uri_ip_literal_host_is_valid(host_and_port: &str) -> bool {
140    let Some(literal_and_port) = host_and_port.strip_prefix('[') else {
141        return false;
142    };
143    let Some(close) = literal_and_port.find(']') else {
144        return false;
145    };
146    if !uri_ip_literal_is_valid(&literal_and_port[..close]) {
147        return false;
148    }
149    let port = &literal_and_port[close + 1..];
150    port.is_empty()
151        || port
152            .strip_prefix(':')
153            .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
154}
155
156fn push_uri_authority_percent_encoded(out: &mut String, authority: &str) {
157    let host_and_port = if let Some((userinfo, host_and_port)) = authority.rsplit_once('@') {
158        push_percent_encoded(out, userinfo, is_uri_userinfo_char);
159        out.push('@');
160        host_and_port
161    } else {
162        authority
163    };
164
165    if uri_ip_literal_host_is_valid(host_and_port) {
166        out.push_str(host_and_port);
167        return;
168    }
169
170    let (host, port) = match host_and_port.rsplit_once(':') {
171        Some((host, port)) if port.bytes().all(|byte| byte.is_ascii_digit()) => (host, Some(port)),
172        _ => (host_and_port, None),
173    };
174    push_percent_encoded(out, host, is_uri_reg_name_char);
175    if let Some(port) = port {
176        out.push(':');
177        out.push_str(port);
178    }
179}
180
181fn push_uri_hier_part_percent_encoded(out: &mut String, hier_part: &str) {
182    if let Some(authority_and_path) = hier_part.strip_prefix("//") {
183        out.push_str("//");
184        let path_start = authority_and_path.find('/');
185        let (authority, path) = match path_start {
186            Some(idx) => authority_and_path.split_at(idx),
187            None => (authority_and_path, ""),
188        };
189        push_uri_authority_percent_encoded(out, authority);
190        push_percent_encoded(out, path, is_uri_path_char);
191    } else {
192        push_percent_encoded(out, hier_part, is_uri_path_char);
193    }
194}
195
196fn push_global_tag_uri_percent_encoded(out: &mut String, uri: &str) {
197    let Some((scheme, remainder)) = uri.split_once(':') else {
198        push_percent_encoded(out, uri, is_global_tag_uri_byte);
199        return;
200    };
201    push_percent_encoded(out, scheme, is_uri_scheme_char);
202    out.push(':');
203
204    let (without_fragment, fragment) = match remainder.split_once('#') {
205        Some((head, tail)) => (head, Some(tail)),
206        None => (remainder, None),
207    };
208    let (hier_part, query) = match without_fragment.split_once('?') {
209        Some((head, tail)) => (head, Some(tail)),
210        None => (without_fragment, None),
211    };
212
213    push_uri_hier_part_percent_encoded(out, hier_part);
214    if let Some(query) = query {
215        out.push('?');
216        push_percent_encoded(out, query, is_uri_query_or_fragment_char);
217    }
218    if let Some(fragment) = fragment {
219        out.push('#');
220        push_percent_encoded(out, fragment, is_uri_query_or_fragment_char);
221    }
222}
223
224#[inline]
225const fn is_uri_scheme_char(byte: u8) -> bool {
226    byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.')
227}
228
229/// Convert a resolved tag identity into one canonical, injection-safe YAML token.
230fn resolved_tag_token(resolved: &str) -> String {
231    if let Some(suffix) = resolved.strip_prefix('!') {
232        let mut token = String::with_capacity(resolved.len());
233        token.push('!');
234        push_percent_encoded(&mut token, suffix, is_local_tag_suffix_byte);
235        token
236    } else {
237        let mut token = String::with_capacity(resolved.len() + 3);
238        token.push_str("!<");
239        push_global_tag_uri_percent_encoded(&mut token, resolved);
240        token.push('>');
241        token
242    }
243}
244
245fn core_tag_token(suffix: &str) -> String {
246    let mut token = String::with_capacity(suffix.len() + 2);
247    token.push_str("!!");
248    push_percent_encoded(&mut token, suffix, is_local_tag_suffix_byte);
249    token
250}
251
252#[inline]
253const fn is_uri_unreserved(byte: u8) -> bool {
254    byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
255}
256
257#[inline]
258const fn is_uri_sub_delim(byte: u8) -> bool {
259    matches!(
260        byte,
261        b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
262    )
263}
264
265#[inline]
266const fn is_uri_pchar(byte: u8) -> bool {
267    is_uri_unreserved(byte) || is_uri_sub_delim(byte) || matches!(byte, b':' | b'@')
268}
269
270fn uri_component_is_valid(value: &str, allowed: fn(u8) -> bool) -> bool {
271    let bytes = value.as_bytes();
272    let mut idx = 0;
273    while idx < bytes.len() {
274        if bytes[idx] == b'%' {
275            if idx + 2 >= bytes.len()
276                || !bytes[idx + 1].is_ascii_hexdigit()
277                || !bytes[idx + 2].is_ascii_hexdigit()
278            {
279                return false;
280            }
281            idx += 3;
282        } else if allowed(bytes[idx]) {
283            idx += 1;
284        } else {
285            return false;
286        }
287    }
288    true
289}
290
291#[inline]
292const fn is_uri_userinfo_char(byte: u8) -> bool {
293    is_uri_unreserved(byte) || is_uri_sub_delim(byte) || byte == b':'
294}
295
296#[inline]
297const fn is_uri_reg_name_char(byte: u8) -> bool {
298    is_uri_unreserved(byte) || is_uri_sub_delim(byte)
299}
300
301#[inline]
302const fn is_uri_path_char(byte: u8) -> bool {
303    is_uri_pchar(byte) || byte == b'/'
304}
305
306#[inline]
307const fn is_uri_query_or_fragment_char(byte: u8) -> bool {
308    is_uri_pchar(byte) || matches!(byte, b'/' | b'?')
309}
310
311#[inline]
312const fn is_ipv_future_char(byte: u8) -> bool {
313    is_uri_unreserved(byte) || is_uri_sub_delim(byte) || byte == b':'
314}
315
316fn uri_ip_literal_is_valid(value: &str) -> bool {
317    if value.parse::<std::net::Ipv6Addr>().is_ok() {
318        return true;
319    }
320
321    let Some(version_and_address) = value.strip_prefix('v').or_else(|| value.strip_prefix('V'))
322    else {
323        return false;
324    };
325    let Some((version, address)) = version_and_address.split_once('.') else {
326        return false;
327    };
328    !version.is_empty()
329        && version.bytes().all(|byte| byte.is_ascii_hexdigit())
330        && !address.is_empty()
331        && address.bytes().all(is_ipv_future_char)
332}
333
334fn uri_authority_is_valid(authority: &str) -> bool {
335    let mut userinfo_and_host = authority.split('@');
336    let first = userinfo_and_host.next().unwrap_or_default();
337    let (userinfo, host_and_port) = match userinfo_and_host.next() {
338        Some(host) if userinfo_and_host.next().is_none() => (Some(first), host),
339        Some(_) => return false,
340        None => (None, first),
341    };
342    if userinfo.is_some_and(|value| !uri_component_is_valid(value, is_uri_userinfo_char)) {
343        return false;
344    }
345
346    if let Some(literal_and_port) = host_and_port.strip_prefix('[') {
347        let Some((literal, port)) = literal_and_port.split_once(']') else {
348            return false;
349        };
350        if !uri_ip_literal_is_valid(literal) {
351            return false;
352        }
353        return port.is_empty()
354            || port
355                .strip_prefix(':')
356                .is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()));
357    }
358
359    let (host, port) = match host_and_port.rsplit_once(':') {
360        Some((host, port)) if !host.contains(':') => (host, Some(port)),
361        Some(_) => return false,
362        None => (host_and_port, None),
363    };
364    uri_component_is_valid(host, is_uri_reg_name_char)
365        && port.is_none_or(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
366}
367
368/// Validate the RFC 3986 `URI` production with an explicit scheme.
369///
370/// Unlike the RFC's narrower `absolute-URI` production, YAML global tags may
371/// include a fragment, so this accepts the optional `#fragment` from `URI`.
372fn absolute_uri_is_valid(uri: &str) -> bool {
373    let Some((scheme, remainder)) = uri.split_once(':') else {
374        return false;
375    };
376    if !scheme
377        .as_bytes()
378        .first()
379        .is_some_and(u8::is_ascii_alphabetic)
380        || !scheme
381            .bytes()
382            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
383    {
384        return false;
385    }
386
387    let (without_fragment, fragment) = match remainder.split_once('#') {
388        Some((head, tail)) => (head, Some(tail)),
389        None => (remainder, None),
390    };
391    if fragment.is_some_and(|value| !uri_component_is_valid(value, is_uri_query_or_fragment_char)) {
392        return false;
393    }
394
395    let (hier_part, query) = match without_fragment.split_once('?') {
396        Some((head, tail)) => (head, Some(tail)),
397        None => (without_fragment, None),
398    };
399    if query.is_some_and(|value| !uri_component_is_valid(value, is_uri_query_or_fragment_char)) {
400        return false;
401    }
402
403    if let Some(authority_and_path) = hier_part.strip_prefix("//") {
404        let path_start = authority_and_path.find('/');
405        let (authority, path) = match path_start {
406            Some(idx) => authority_and_path.split_at(idx),
407            None => (authority_and_path, ""),
408        };
409        uri_authority_is_valid(authority) && uri_component_is_valid(path, is_uri_path_char)
410    } else {
411        uri_component_is_valid(hier_part, is_uri_path_char)
412    }
413}
414
415fn is_supported_anchor_name(name: &str) -> bool {
416    !name.is_empty()
417        && name.len() <= MAX_ANCHOR_NAME_BYTES
418        && name.chars().all(|ch| {
419            !ch.is_whitespace() && !ch.is_control() && !matches!(ch, '[' | ']' | '{' | '}' | ',')
420        })
421}
422
423/// Immutable formatting policy copied from the public [`SerializerOptions`].
424///
425/// A serializer captures these values once during construction and never mutates
426/// them. Keeping policy separate from [`SerializerState`] makes it clear that
427/// recursive serialization may change traversal state but not caller-selected
428/// behavior. The boolean fields are independent user-facing switches rather than
429/// coupled states in a state machine.
430#[allow(clippy::struct_excessive_bools)]
431struct SerializerSettings {
432    /// Spaces per indentation level for block-style collections.
433    indent_step: usize,
434    /// Threshold for downgrading block-string wrappers to plain scalars.
435    min_fold_chars: usize,
436    /// Wrap width for folded block scalars (`>`).
437    folded_wrap_col: usize,
438    /// Placement mode for [`crate::Commented`] wrappers in block style.
439    #[cfg(feature = "comments")]
440    comment_position: CommentPosition,
441    /// Emit YAML tags for simple enums that serialize to a single scalar.
442    tagged_enums: bool,
443    /// Emit empty maps as `{}` and empty lists as `[]`.
444    empty_as_braces: bool,
445    /// Emit list items with compact indentation under mapping keys.
446    compact_list_indent: bool,
447    /// Automatically prefer block scalars for eligible strings.
448    prefer_block_scalars: bool,
449    /// Quote all string scalars.
450    quote_all: bool,
451    /// Use YAML 1.2-friendly heuristics and emit a directive unless suppressed.
452    yaml_12: bool,
453    /// Suppress the language directive and its document start marker.
454    no_lang_directive: bool,
455}
456
457impl From<&SerializerOptions> for SerializerSettings {
458    #[allow(deprecated)] // Honor empty_as_braces for existing callers during deprecation.
459    fn from(options: &SerializerOptions) -> Self {
460        Self {
461            indent_step: options.indent_step,
462            min_fold_chars: options.min_fold_chars,
463            folded_wrap_col: options.folded_wrap_chars,
464            #[cfg(feature = "comments")]
465            comment_position: options.comment_position,
466            tagged_enums: options.tagged_enums,
467            empty_as_braces: options.empty_as_braces,
468            compact_list_indent: options.compact_list_indent,
469            prefer_block_scalars: options.prefer_block_scalars,
470            quote_all: options.quote_all,
471            yaml_12: options.yaml_12,
472            no_lang_directive: options.no_lang_directive,
473        }
474    }
475}
476
477/// One-shot layout signals passed between a parent serializer and the nested
478/// collection serializer to which it delegates.
479///
480/// These flags describe pending output work, not durable formatting policy. A
481/// consumer must clear a signal after handling it, and code that temporarily
482/// overrides a signal while serializing a child must restore the parent value.
483#[derive(Default)]
484struct PendingLayout {
485    /// Emit the first key of the next mapping inline.
486    pending_inline_map: bool,
487    /// Defer the space following a mapping colon until the value shape is known.
488    pending_space_after_colon: bool,
489    /// Indent a sequence following an inline-after-dash mapping by one level.
490    inline_map_after_dash: bool,
491}
492
493/// Mutable traversal and output state for one serializer instance.
494///
495/// Serde recursively hands the same serializer to child values, so indentation,
496/// cursor position, flow depth, and pending node decorations must survive those
497/// calls. A fresh state is created for every `YamlSerializer`; none of these
498/// values are copied back into the public [`SerializerOptions`].
499struct SerializerState {
500    /// Current nesting depth used for indentation.
501    depth: usize,
502    /// Whether the output cursor is at the start of a line.
503    at_line_start: bool,
504    /// Pending flow-style hint captured from wrapper types.
505    pending_flow: Option<PendingFlow>,
506    /// Number of nested flow containers currently being emitted.
507    in_flow: usize,
508    /// Pending explicit or automatically selected block-string style.
509    pending_str_style: Option<PendingStrStyle>,
510    /// Tag staged for the next node by a wrapper or a serializer feature.
511    pending_tag: Option<PendingTag>,
512    /// Inline comment waiting for the next scalar.
513    #[cfg(feature = "comments")]
514    pending_inline_comment: Option<String>,
515    /// Short-lived layout signals shared by nested collection serializers.
516    pending_layout: PendingLayout,
517    /// Whether the last serialized value was a block collection.
518    last_value_was_block: bool,
519    /// Depth captured for the node immediately following a sequence dash.
520    after_dash_depth: Option<usize>,
521    /// Current block-map depth for aligning sequences below mapping keys.
522    current_map_depth: Option<usize>,
523    /// Column of the current block map's keys, when it differs from
524    /// `current_map_depth * indent_step` (a map opened after `- ` aligns its keys two columns
525    /// past the dash). Block scalar indentation indicators are relative to this column.
526    current_map_key_column: Option<usize>,
527    /// Whether emission of the current document has begun.
528    doc_started: bool,
529}
530
531impl Default for SerializerState {
532    fn default() -> Self {
533        Self {
534            depth: 0,
535            at_line_start: true,
536            pending_flow: None,
537            in_flow: 0,
538            pending_str_style: None,
539            pending_tag: None,
540            #[cfg(feature = "comments")]
541            pending_inline_comment: None,
542            pending_layout: PendingLayout::default(),
543            last_value_was_block: false,
544            after_dash_depth: None,
545            current_map_depth: None,
546            current_map_key_column: None,
547            doc_started: false,
548        }
549    }
550}
551
552/// Per-serializer registry for YAML anchors and aliases.
553///
554/// Pointer identities receive stable, monotonically increasing ids. When a new
555/// identity is discovered, `pending_id` defers writing its `&anchor` prefix until
556/// the value's serializer reveals whether the next node is scalar or compound.
557/// Custom names are cached by id so later aliases reuse exactly the same name.
558struct AnchorState {
559    /// Map from pointer identity to anchor id.
560    by_ptr: HashMap<usize, AnchorId, BuildNoHashHasher<usize>>,
561    /// Next numeric id to use when generating anchor names.
562    next_id: AnchorId,
563    /// Anchor to prefix onto the next scalar or complex node.
564    pending_id: Option<AnchorId>,
565    /// Optional custom anchor-name generator supplied by the caller.
566    generator: Option<fn(usize) -> String>,
567    /// Cached custom names, indexed by `id - 1`.
568    custom_names: Option<Vec<String>>,
569    /// Resolved tag attached to each anchor definition, indexed by `id - 1`.
570    resolved_tags: Vec<Option<String>>,
571}
572
573impl AnchorState {
574    fn new(generator: Option<fn(usize) -> String>) -> Self {
575        Self {
576            by_ptr: HashMap::with_hasher(BuildNoHashHasher::default()),
577            next_id: 1,
578            pending_id: None,
579            generator,
580            custom_names: None,
581            resolved_tags: Vec::new(),
582        }
583    }
584
585    fn record_resolved_tag(&mut self, id: AnchorId, tag: Option<&str>) {
586        let idx = id as usize - 1;
587        if self.resolved_tags.len() <= idx {
588            self.resolved_tags.resize_with(idx + 1, || None);
589        }
590        self.resolved_tags[idx] = tag.map(ToOwned::to_owned);
591    }
592
593    fn resolved_tag(&self, id: AnchorId) -> Option<&str> {
594        self.resolved_tags
595            .get(id as usize - 1)
596            .and_then(Option::as_deref)
597    }
598}
599
600/// Core YAML serializer used by `to_string`, `to_fmt_writer`, and `to_io_writer` (and their `_with_options` variants).
601///
602/// This type implements `serde::Serializer` and writes YAML to a `fmt::Write`.
603/// It manages indentation, flow/block styles, and YAML anchors/aliases.
604///
605/// This type is also re-exported from the crate root as [`serde_saphyr::Serializer`](crate::Serializer).
606///
607/// ## Example
608///
609/// ```rust
610/// use serde::Serialize;
611///
612/// #[derive(Serialize)]
613/// struct Foo {
614///     a: i32,
615///     b: bool,
616/// }
617///
618/// let mut out = String::new();
619/// let mut ser = serde_saphyr::Serializer::new(&mut out);
620/// Foo { a: 1, b: true }.serialize(&mut ser)?;
621///
622/// assert!(out.contains("a: 1"));
623/// # Ok::<(), serde_saphyr::ser::Error>(())
624/// ```
625pub struct YamlSerializer<'a, W: Write> {
626    /// Destination writer where YAML text is emitted.
627    out: &'a mut W,
628    /// Immutable caller-selected formatting behavior.
629    settings: SerializerSettings,
630    /// Mutable layout and traversal state shared with nested serializers.
631    state: SerializerState,
632    /// Anchor identities, names, and deferred anchor emission.
633    anchors: AnchorState,
634}
635
636impl<'a, W: Write> YamlSerializer<'a, W> {
637    /// Construct a [`Serializer`](crate::Serializer) that writes to `out`.
638    /// Uses the same defaults as `SerializerOptions::default()`.
639    #[must_use]
640    pub fn new(out: &'a mut W) -> Self {
641        Self::from_options_unchecked(out, &SerializerOptions::default())
642    }
643
644    fn from_options_unchecked(out: &'a mut W, options: &SerializerOptions) -> Self {
645        Self {
646            out,
647            settings: SerializerSettings::from(options),
648            state: SerializerState::default(),
649            anchors: AnchorState::new(options.anchor_generator),
650        }
651    }
652    /// Construct a [`Serializer`](crate::Serializer) with a specific indentation step.
653    /// All other settings use `SerializerOptions::default()`. Returns an error if
654    /// `indent_step` is outside `1..=64`.
655    pub fn with_indent(out: &'a mut W, indent_step: usize) -> Result<Self> {
656        let options = SerializerOptions {
657            indent_step,
658            ..SerializerOptions::default()
659        };
660        Self::with_options(out, options)
661    }
662    /// Construct a [`Serializer`](crate::Serializer) from user-supplied [`SerializerOptions`].
663    /// Used by `to_fmt_writer_with_options` and `to_io_writer_with_options`.
664    pub fn with_options(out: &'a mut W, options: SerializerOptions) -> Result<Self> {
665        options.consistent()?;
666        Ok(Self::from_options_unchecked(out, &options))
667    }
668
669    // -------- helpers --------
670
671    /// Determines if a string requires double quotes when `quote_all` is enabled.
672    /// Returns true if the string contains single quotes, backslashes, or control characters
673    /// that need escape processing.
674    #[inline]
675    fn needs_double_quotes(s: &str) -> bool {
676        s.chars().any(|c| {
677            c == '\''       // single quote present - cannot use single-quoted style
678                || c == '\\' // backslash - needs escape processing
679                || is_controll_which_needs_escaping(c) // control chars (includes \n, \t, \r, etc.) need escaping
680        })
681    }
682
683    /// Write a single-quoted string. Single quotes inside the string are escaped by doubling them.
684    fn write_single_quoted(&mut self, s: &str) -> Result<()> {
685        self.out.write_char('\'')?;
686        let mut start = 0;
687        for (index, _) in s.match_indices('\'') {
688            if start < index {
689                self.out.write_str(&s[start..index])?;
690            }
691            self.out.write_str("''")?;
692            start = index + 1;
693        }
694        if start < s.len() {
695            self.out.write_str(&s[start..])?;
696        }
697        self.out.write_char('\'')?;
698        Ok(())
699    }
700
701    /// Append a pending inline comment, if any.
702    ///
703    /// Used both by normal scalar emission (`value # comment\n`) and by
704    /// block-scalar headers (`| # comment\n`). Comments are suppressed in flow
705    /// style, matching the existing serializer policy.
706    #[cfg(feature = "comments")]
707    #[inline]
708    fn write_pending_inline_comment(&mut self) -> Result<()> {
709        if self.state.in_flow == 0
710            && let Some(c) = self.state.pending_inline_comment.take()
711        {
712            self.out.write_str(" # ")?;
713            self.out.write_str(&c)?;
714        }
715        Ok(())
716    }
717
718    /// Called at the end of emitting a scalar in block style: appends a pending inline
719    /// comment (if any) and then emits a newline. In flow style, comments are suppressed.
720    #[inline]
721    fn write_end_of_scalar(&mut self) -> Result<()> {
722        if self.state.in_flow == 0 {
723            #[cfg(feature = "comments")]
724            self.write_pending_inline_comment()?;
725            self.newline()?;
726        }
727        Ok(())
728    }
729
730    /// Make `comment` safe to write after `#`. Comments cannot be escaped, so every character a
731    /// YAML comment may not contain (`nb-char`, YAML 1.2.2 [27]: printable, no line break, no
732    /// BOM) is replaced by a space. Tab stays (it is printable white space).
733    #[cfg(feature = "comments")]
734    #[inline]
735    fn sanitize_comment_text(comment: &str) -> String {
736        comment
737            .chars()
738            .map(|ch| match ch {
739                '\t' => ch,
740                '\n' | '\r' | '\u{85}' | '\u{2028}' | '\u{2029}' => ' ',
741                c if is_controll_which_needs_escaping(c) => ' ',
742                // Surrogates cannot occur in a Rust `char`; other non-printable code points
743                // (C0/C1 controls, U+FFFE/U+FFFF, BOM) are covered above.
744                _ => ch,
745            })
746            .collect()
747    }
748
749    #[cfg(feature = "comments")]
750    fn write_above_comment(&mut self, comment: &str) -> Result<Option<usize>> {
751        if self.state.in_flow > 0 || comment.is_empty() {
752            return Ok(None);
753        }
754
755        let target_depth = if self.state.pending_layout.pending_space_after_colon {
756            let base = self.state.current_map_depth.unwrap_or(self.state.depth);
757            self.state.pending_layout.pending_space_after_colon = false;
758            if !self.state.at_line_start {
759                self.newline()?;
760            }
761            checked_depth_add(base, 1)?
762        } else if !self.state.at_line_start {
763            let base = self.state.after_dash_depth.unwrap_or(self.state.depth);
764            self.state.pending_layout.pending_inline_map = false;
765            self.newline()?;
766            checked_depth_add(base, 1)?
767        } else {
768            self.state.depth
769        };
770
771        self.write_indent(target_depth)?;
772        self.out.write_str("# ")?;
773        self.out.write_str(comment)?;
774        self.newline()?;
775        Ok(Some(target_depth))
776    }
777
778    /// Allocate (or get existing) anchor id for a pointer identity.
779    /// Returns `(id, is_new)`, or an error when the custom generator returns an unsupported name.
780    #[inline]
781    fn alloc_anchor_for(&mut self, ptr: usize) -> Result<(AnchorId, bool)> {
782        match self.anchors.by_ptr.entry(ptr) {
783            std::collections::hash_map::Entry::Occupied(e) => Ok((*e.get(), false)),
784            std::collections::hash_map::Entry::Vacant(v) => {
785                let id = self.anchors.next_id;
786                let custom_name = if let Some(generator) = self.anchors.generator {
787                    let name = generator(id as usize);
788                    if !is_supported_anchor_name(&name) {
789                        return Err(Error::InvalidOptions(format!(
790                            "custom anchor generator returned an invalid name for anchor id {id}; names must be 1..={MAX_ANCHOR_NAME_BYTES} bytes and contain no whitespace, control characters, or '[', ']', '{{', '}}', ','"
791                        )));
792                    }
793                    Some(name)
794                } else {
795                    None
796                };
797
798                self.anchors.next_id = self.anchors.next_id.saturating_add(1);
799                if let Some(name) = custom_name {
800                    self.anchors
801                        .custom_names
802                        .get_or_insert_with(Vec::new)
803                        .push(name);
804                }
805                v.insert(id);
806                Ok((id, true))
807            }
808        }
809    }
810
811    /// Resolve an anchor name for `id` and write it.
812    #[inline]
813    fn write_anchor_name(&mut self, id: AnchorId) -> Result<()> {
814        if let Some(names) = &self.anchors.custom_names {
815            // ids are 1-based; vec is 0-based
816            let idx = id as usize - 1;
817            if let Some(name) = names.get(idx) {
818                self.out.write_str(name)?;
819            } else {
820                // Fallback if generator vector is out of sync
821                write!(self.out, "a{id}")?;
822            }
823        } else {
824            write!(self.out, "a{id}")?;
825        }
826        Ok(())
827    }
828
829    /// If a mapping key has just been written (':' emitted) and we determined the value is a scalar,
830    /// insert a single space before the scalar and clear the pending flag.
831    #[inline]
832    fn write_space_if_pending(&mut self) -> Result<()> {
833        if self.state.pending_layout.pending_space_after_colon {
834            self.out.write_char(' ')?;
835            self.state.pending_layout.pending_space_after_colon = false;
836        }
837        // When a scalar value is serialized, it should reset the block-sibling flag.
838        // Most scalar emitters call this method.
839        self.state.last_value_was_block = false;
840        Ok(())
841    }
842
843    /// Ensure indentation is written if we are at the start of a line.
844    /// Internal: called by most emitters before writing tokens.
845    #[inline]
846    fn write_indent(&mut self, depth: usize) -> Result<()> {
847        if self.state.at_line_start {
848            if !self.state.doc_started {
849                self.state.doc_started = true;
850                if self.settings.yaml_12 && !self.settings.no_lang_directive {
851                    self.out.write_str("%YAML 1.2\n---\n")?;
852                    // Still at start of a line after the directive and document start marker.
853                    self.state.at_line_start = true;
854                }
855            }
856            self.write_indent_spaces(depth)?;
857            self.state.at_line_start = false;
858        }
859        Ok(())
860    }
861
862    #[inline]
863    fn write_indent_spaces(&mut self, depth: usize) -> Result<()> {
864        let spaces = checked_indentation(self.settings.indent_step, depth)?;
865        for _ in 0..spaces {
866            self.out.write_char(' ')?;
867        }
868        Ok(())
869    }
870
871    /// Depth used as the base for a variant's payload after a sequence dash.
872    fn variant_depth_after_dash(
873        &self,
874        dash_depth: usize,
875        anchor_broke_line: bool,
876    ) -> Result<usize> {
877        // An inline variant follows the two-column "- " prefix, even when one
878        // indentation level is only one space. Node properties instead move
879        // the variant onto its own line, one level below the dash.
880        let levels = if anchor_broke_line {
881            1
882        } else {
883            2usize.div_ceil(self.settings.indent_step)
884        };
885        checked_depth_add(dash_depth, levels)
886    }
887
888    /// Emit a newline and mark the next write position as line start.
889    /// Internal utility used after finishing a top-level token.
890    #[inline]
891    fn newline(&mut self) -> Result<()> {
892        self.out.write_char('\n')?;
893        self.state.at_line_start = true;
894        Ok(())
895    }
896
897    /// Write a folded block string body indented to `column` spaces, wrapping to
898    /// `folded_wrap_col` characters. Delegates to the standalone function in `wrapping` module.
899    fn write_folded_block(&mut self, s: &str, column: usize) -> Result<()> {
900        self::wrapping::write_folded_block(self.out, s, column, 1, self.settings.folded_wrap_col)?;
901        self.state.at_line_start = true;
902        Ok(())
903    }
904
905    /// Write a scalar in mapping-key position.
906    ///
907    /// Externally tagged enum variants are emitted as YAML mapping keys
908    /// (`Variant: ...`), so they need the same ambiguity checks as regular
909    /// map and struct keys.
910    fn write_key_scalar(&mut self, s: &str) -> Result<()> {
911        let text = scalar_key_to_string(&s, self.settings.yaml_12)?;
912        self.out.write_str(&text)?;
913        Ok(())
914    }
915
916    /// Write a double-quoted string with necessary escapes.
917    fn write_quoted(&mut self, s: &str) -> Result<()> {
918        self.out.write_char('"')?;
919        escape_double_quoted(s, self.out)?;
920        self.out.write_char('"')?;
921        Ok(())
922    }
923
924    /// Like `write_plain_or_quoted`, but intended for VALUE position where ':' is allowed.
925    #[inline]
926    fn write_plain_or_quoted_value(&mut self, s: &str) -> Result<()> {
927        if self.settings.quote_all {
928            // In quote_all mode: prefer single quotes, use double quotes when needed
929            if Self::needs_double_quotes(s) {
930                self.write_quoted(s)
931            } else {
932                self.write_single_quoted(s)
933            }
934        } else if is_plain_value_safe(s, self.settings.yaml_12, self.state.in_flow > 0) {
935            self.out.write_str(s)?;
936            Ok(())
937        } else {
938            // Force quoted style for problematic value tokens (commas/brackets, bool/num-like, etc.).
939            self.write_quoted(s)
940        }
941    }
942
943    /// Stage a resolved tag supplied by [`crate::Tagged`].
944    fn stage_resolved_tag(&mut self, resolved: &str) -> Result<()> {
945        if resolved.is_empty() {
946            return Err(Error::EmptyResolvedTag);
947        }
948        let emitted = resolved_tag_token(resolved);
949        if !resolved.starts_with('!') && !absolute_uri_is_valid(&emitted[2..emitted.len() - 1]) {
950            return Err(Error::InvalidGlobalTagUri {
951                tag: resolved.to_owned(),
952            });
953        }
954        self.stage_tag_with_token(resolved, emitted)
955    }
956
957    /// Stage `resolved` for `value`, restoring the caller's pending tag if the
958    /// wrapped serialization fails before consuming it.
959    fn serialize_tagged_value<T: ?Sized + Serialize>(
960        &mut self,
961        resolved: &str,
962        value: &T,
963    ) -> Result<()> {
964        let previous = self.state.pending_tag.clone();
965        let result = self
966            .stage_resolved_tag(resolved)
967            .and_then(|()| value.serialize(&mut *self));
968        if result.is_err() {
969            self.state.pending_tag = previous;
970        }
971        result
972    }
973
974    /// Stage a tag with a preferred spelling used by built-in serializer
975    /// features. A YAML node carries at most one tag, so tags that resolve to
976    /// the same identity coalesce and are emitted once; only two different
977    /// identities competing for one node are a clash.
978    fn stage_tag_with_token(&mut self, resolved: &str, emitted: String) -> Result<()> {
979        match self.state.pending_tag.as_ref() {
980            Some(pending) if pending.resolved == resolved => Ok(()),
981            Some(pending) => Err(Error::custom(format_args!(
982                "cannot serialize one YAML node with both tag {:?} and tag {:?}",
983                pending.resolved, resolved
984            ))),
985            None => {
986                self.state.pending_tag = Some(PendingTag {
987                    resolved: resolved.to_owned(),
988                    emitted,
989                    variant_selector_claimed: false,
990                });
991                Ok(())
992            }
993        }
994    }
995
996    fn stage_core_tag(&mut self, suffix: &str) -> Result<()> {
997        let mut resolved = String::with_capacity(YAML_TAG_NAMESPACE.len() + suffix.len());
998        resolved.push_str(YAML_TAG_NAMESPACE);
999        resolved.push_str(suffix);
1000        self.stage_tag_with_token(&resolved, core_tag_token(suffix))
1001    }
1002
1003    /// Whether the tag staged for the next node is a tag that Serde's enum
1004    /// deserializer interprets as `variant`.
1005    ///
1006    /// A YAML core type tag whose normalized name equals the variant is
1007    /// ambiguous so wrong.
1008    #[inline]
1009    fn pending_tag_selects_variant(&mut self, variant: &str) -> Result<bool> {
1010        let Some(tag) = self.state.pending_tag.as_mut() else {
1011            return Ok(false);
1012        };
1013        let resolved = tag.resolved.as_str();
1014
1015        if yaml_core_type_tag_name(resolved) == Some(variant) {
1016            return Err(Error::CoreTypeTagAsEnumVariant {
1017                tag: resolved.to_owned(),
1018                variant: variant.to_owned(),
1019            });
1020        }
1021
1022        if simple_enum_variant_name(resolved) != Some(variant) || tag.variant_selector_claimed {
1023            return Ok(false);
1024        }
1025
1026        tag.variant_selector_claimed = true;
1027        Ok(true)
1028    }
1029
1030    /// Serialize a tagged scalar of the form `!!Type value` using plain or quoted style for
1031    /// the value depending on its content.
1032    fn serialize_tagged_scalar(&mut self, enum_name: &str, variant: &str) -> Result<()> {
1033        self.stage_core_tag(enum_name)?;
1034        self.write_scalar_prefix_if_anchor()?;
1035        if self.state.at_line_start {
1036            self.write_indent(self.state.depth)?;
1037        }
1038        self.write_plain_or_quoted_value(variant)?;
1039        self.write_end_of_scalar()
1040    }
1041
1042    fn serialize_double_quoted_scalar(&mut self, value: &str) -> Result<()> {
1043        self.write_space_if_pending()?;
1044        self.write_scalar_prefix_if_anchor()?;
1045        if self.state.at_line_start {
1046            self.write_indent(self.state.depth)?;
1047        }
1048        self.write_quoted(value)?;
1049        self.write_end_of_scalar()
1050    }
1051
1052    fn serialize_single_quoted_scalar(&mut self, value: &str) -> Result<()> {
1053        if let Some(ch) = value
1054            .chars()
1055            .find(|ch| is_controll_which_needs_escaping(*ch))
1056        {
1057            return Err(Error::SingleQuotedRequiresEscaping { ch });
1058        }
1059        self.write_space_if_pending()?;
1060        self.write_scalar_prefix_if_anchor()?;
1061        if self.state.at_line_start {
1062            self.write_indent(self.state.depth)?;
1063        }
1064        self.write_single_quoted(value)?;
1065        self.write_end_of_scalar()
1066    }
1067
1068    fn serialize_tilde_null(&mut self) -> Result<()> {
1069        self.state.pending_flow = None;
1070        self.write_space_if_pending()?;
1071        self.write_scalar_prefix_if_anchor()?;
1072        if self.state.at_line_start {
1073            self.write_indent(self.state.depth)?;
1074        }
1075        self.out.write_char('~')?;
1076        self.write_end_of_scalar()
1077    }
1078
1079    /// Emit pending YAML node properties before a scalar.
1080    #[inline]
1081    fn write_scalar_prefix_if_anchor(&mut self) -> Result<()> {
1082        let tag = self.state.pending_tag.take();
1083        let anchor_id = self.anchors.pending_id.take();
1084        if tag.is_some() || anchor_id.is_some() {
1085            if self.state.at_line_start {
1086                self.write_indent(self.state.depth)?;
1087            }
1088            if let Some(tag) = tag.as_ref() {
1089                self.out.write_str(&tag.emitted)?;
1090                self.out.write_char(' ')?;
1091            }
1092            if let Some(id) = anchor_id {
1093                self.anchors
1094                    .record_resolved_tag(id, tag.as_ref().map(|tag| tag.resolved.as_str()));
1095                self.out.write_char('&')?;
1096                self.write_anchor_name(id)?;
1097                self.out.write_char(' ')?;
1098            }
1099        }
1100        Ok(())
1101    }
1102
1103    /// Emit pending YAML node properties on their own line before a block node.
1104    #[inline]
1105    fn write_anchor_for_complex_node(&mut self) -> Result<()> {
1106        let tag = self.state.pending_tag.take();
1107        let anchor_id = self.anchors.pending_id.take();
1108        if tag.is_some() || anchor_id.is_some() {
1109            self.write_space_if_pending()?;
1110            if self.state.at_line_start {
1111                self.write_indent(self.state.depth)?;
1112            }
1113            if let Some(tag) = tag.as_ref() {
1114                self.out.write_str(&tag.emitted)?;
1115            }
1116            if let Some(id) = anchor_id {
1117                self.anchors
1118                    .record_resolved_tag(id, tag.as_ref().map(|tag| tag.resolved.as_str()));
1119                if tag.is_some() {
1120                    self.out.write_char(' ')?;
1121                }
1122                self.out.write_char('&')?;
1123                self.write_anchor_name(id)?;
1124            }
1125            self.newline()?;
1126        }
1127        Ok(())
1128    }
1129
1130    /// Emit an alias `*name`. Adds a newline in block style.
1131    /// Used when a previously defined anchor is referenced again.
1132    #[inline]
1133    fn write_alias_id(&mut self, id: AnchorId) -> Result<()> {
1134        let anchor_tag = self.anchors.resolved_tag(id);
1135        if let Some(requested) = self.state.pending_tag.take()
1136            && anchor_tag != Some(requested.resolved.as_str())
1137        {
1138            return Err(Error::custom(format_args!(
1139                "cannot apply tag {:?} to alias whose anchor was defined with tag {:?}",
1140                requested.resolved,
1141                anchor_tag.unwrap_or("")
1142            )));
1143        }
1144        if self.state.at_line_start {
1145            self.write_indent(self.state.depth)?;
1146        }
1147        self.write_space_if_pending()?;
1148        self.out.write_char('*')?;
1149        self.write_anchor_name(id)?;
1150        // Use the shared end-of-scalar path so pending inline comments are appended in block style
1151        self.write_end_of_scalar()?;
1152        Ok(())
1153    }
1154
1155    #[inline]
1156    fn has_pending_node_properties(&self) -> bool {
1157        self.state.pending_tag.is_some() || self.anchors.pending_id.is_some()
1158    }
1159
1160    /// Determine whether the next sequence should be emitted in flow style.
1161    /// Consumes any pending flow hint.
1162    #[inline]
1163    fn take_flow_for_seq(&mut self) -> bool {
1164        if self.state.in_flow > 0 {
1165            true
1166        } else {
1167            matches!(self.state.pending_flow.take(), Some(PendingFlow::AnySeq))
1168        }
1169    }
1170    /// Determine whether the next mapping should be emitted in flow style.
1171    /// Consumes any pending flow hint.
1172    #[inline]
1173    fn take_flow_for_map(&mut self) -> bool {
1174        if self.state.in_flow > 0 {
1175            true
1176        } else {
1177            matches!(self.state.pending_flow.take(), Some(PendingFlow::AnyMap))
1178        }
1179    }
1180
1181    /// Temporarily mark that we are inside a flow container while running `f`.
1182    /// Ensures proper comma insertion and line handling for nested flow nodes.
1183    #[inline]
1184    fn with_in_flow<T>(&mut self, f: impl FnOnce(&mut Self) -> Result<T>) -> Result<T> {
1185        self.state.in_flow += 1;
1186        let r = f(self);
1187        self.state.in_flow -= 1;
1188        r
1189    }
1190}
1191
1192// ------------------------------------------------------------
1193// Impl Serializer for YamlSerializer
1194// ------------------------------------------------------------
1195
1196impl<'a, 'b, W: Write> Serializer for &'a mut YamlSerializer<'b, W> {
1197    type Ok = ();
1198    type Error = Error;
1199
1200    type SerializeSeq = SeqSer<'a, 'b, W>;
1201    type SerializeTuple = SeqSer<'a, 'b, W>;
1202    type SerializeTupleStruct = TupleSer<'a, 'b, W>;
1203    type SerializeTupleVariant = SeqSer<'a, 'b, W>;
1204    type SerializeMap = MapSer<'a, 'b, W>;
1205    type SerializeStruct = MapSer<'a, 'b, W>;
1206    type SerializeStructVariant = StructVariantSer<'a, 'b, W>;
1207
1208    // -------- Scalars --------
1209
1210    fn serialize_bool(self, v: bool) -> Result<()> {
1211        self.write_space_if_pending()?;
1212        self.write_scalar_prefix_if_anchor()?;
1213        if self.state.at_line_start {
1214            self.write_indent(self.state.depth)?;
1215        }
1216        self.out.write_str(if v { "true" } else { "false" })?;
1217        self.write_end_of_scalar()?;
1218        Ok(())
1219    }
1220
1221    fn serialize_i8(self, v: i8) -> Result<()> {
1222        self.serialize_i64(i64::from(v))
1223    }
1224    fn serialize_i16(self, v: i16) -> Result<()> {
1225        self.serialize_i64(i64::from(v))
1226    }
1227    fn serialize_i32(self, v: i32) -> Result<()> {
1228        self.serialize_i64(i64::from(v))
1229    }
1230    fn serialize_i64(self, v: i64) -> Result<()> {
1231        self.write_space_if_pending()?;
1232        self.write_scalar_prefix_if_anchor()?;
1233        if self.state.at_line_start {
1234            self.write_indent(self.state.depth)?;
1235        }
1236        write!(self.out, "{v}")?;
1237        self.write_end_of_scalar()?;
1238        Ok(())
1239    }
1240
1241    fn serialize_i128(self, v: i128) -> Result<()> {
1242        self.write_space_if_pending()?;
1243        self.write_scalar_prefix_if_anchor()?;
1244        if self.state.at_line_start {
1245            self.write_indent(self.state.depth)?;
1246        }
1247        write!(self.out, "{v}")?;
1248        self.write_end_of_scalar()?;
1249        Ok(())
1250    }
1251
1252    fn serialize_u8(self, v: u8) -> Result<()> {
1253        self.serialize_u64(u64::from(v))
1254    }
1255    fn serialize_u16(self, v: u16) -> Result<()> {
1256        self.serialize_u64(u64::from(v))
1257    }
1258    fn serialize_u32(self, v: u32) -> Result<()> {
1259        self.serialize_u64(u64::from(v))
1260    }
1261    fn serialize_u64(self, v: u64) -> Result<()> {
1262        self.write_space_if_pending()?;
1263        self.write_scalar_prefix_if_anchor()?;
1264        if self.state.at_line_start {
1265            self.write_indent(self.state.depth)?;
1266        }
1267        write!(self.out, "{v}")?;
1268        self.write_end_of_scalar()?;
1269        Ok(())
1270    }
1271
1272    fn serialize_u128(self, v: u128) -> Result<()> {
1273        self.write_space_if_pending()?;
1274        self.write_scalar_prefix_if_anchor()?;
1275        if self.state.at_line_start {
1276            self.write_indent(self.state.depth)?;
1277        }
1278        write!(self.out, "{v}")?;
1279        self.write_end_of_scalar()?;
1280        Ok(())
1281    }
1282
1283    fn serialize_f32(self, v: f32) -> Result<()> {
1284        self.write_space_if_pending()?;
1285        self.write_scalar_prefix_if_anchor()?;
1286        if self.state.at_line_start {
1287            self.write_indent(self.state.depth)?;
1288        }
1289        zmij_format::write_float_string(self.out, v)?;
1290        self.write_end_of_scalar()
1291    }
1292
1293    fn serialize_f64(self, v: f64) -> Result<()> {
1294        self.write_space_if_pending()?;
1295        self.write_scalar_prefix_if_anchor()?;
1296        if self.state.at_line_start {
1297            self.write_indent(self.state.depth)?;
1298        }
1299        zmij_format::write_float_string(self.out, v)?;
1300        self.write_end_of_scalar()
1301    }
1302
1303    fn serialize_char(self, v: char) -> Result<()> {
1304        self.write_space_if_pending()?;
1305        let mut buf = [0u8; 4];
1306        self.serialize_str(v.encode_utf8(&mut buf))
1307    }
1308
1309    fn serialize_str(self, v: &str) -> Result<()> {
1310        #[inline]
1311        fn block_indent_indicator_digit(indent_n: usize) -> Result<char> {
1312            // YAML 1.2 8.1.1.1: the block-scalar indentation indicator is `[1-9]`.
1313            // `char::from_digit` would accept 0, so we gate on the range explicitly.
1314            match indent_n {
1315                1..=9 => {
1316                    let digit = u32::try_from(indent_n).expect("checked 1..=9");
1317                    Ok(char::from_digit(digit, 10).expect("checked 1..=9"))
1318                }
1319                _ => Err(Error::custom(
1320                    "indentation indicator must be a single digit (1..=9)",
1321                )),
1322            }
1323        }
1324
1325        // If no explicit style pending, auto-select block style.
1326        //
1327        // Controlled by `prefer_block_scalars`:
1328        //  - multiline → literal (|) whenever the content is representable in a block scalar
1329        //    and is readable enough to auto-select (see `is_auto_block_scalar_readable`).
1330        //    Block scalars happily contain ':', '#', YAML-like text, etc. — those are unsafe
1331        //    in plain style but fine as block content.
1332        //  - single-line + long (by folded_wrap_col) → folded (>)
1333        //
1334        // Also skip block scalars when quote_all is enabled - use quoted strings instead.
1335        if self.state.pending_str_style.is_none()
1336            && self.state.in_flow == 0
1337            && !self.settings.quote_all
1338        {
1339            if v.contains('\n') {
1340                if self.settings.prefer_block_scalars
1341                    && is_block_scalar_content_safe(v)
1342                    && is_auto_block_scalar_readable(v)
1343                {
1344                    self.state.pending_str_style =
1345                        Some(PendingStrStyle::Automatic(StrStyle::Literal));
1346                }
1347            } else if self.settings.prefer_block_scalars
1348                // UTF-8 byte length bounds the character count from above.
1349                && v.len() > self.settings.folded_wrap_col
1350            {
1351                // Single-line string. If it needs quoting as a value, don't auto-fold.
1352                // Folded block scalars can preserve trailing ASCII spaces, unlike plain
1353                // scalars, so ignore only those spaces for the eligibility probe.
1354                let auto_fold_probe = v.trim_end_matches(' ');
1355                let can_auto_fold = !auto_fold_probe.is_empty()
1356                    && is_plain_value_safe(auto_fold_probe, self.settings.yaml_12, false);
1357                if can_auto_fold {
1358                    // Measure in characters, not bytes.
1359                    if v.chars().count() > self.settings.folded_wrap_col {
1360                        self.state.pending_str_style =
1361                            Some(PendingStrStyle::Automatic(StrStyle::Folded));
1362                    }
1363                }
1364            }
1365        }
1366        if let Some(pending_style) = self.state.pending_str_style.take() {
1367            let (style, from_auto) = pending_style.into_parts();
1368            if !is_block_scalar_content_safe(v) {
1369                self.write_space_if_pending()?;
1370                self.write_scalar_prefix_if_anchor()?;
1371                if self.state.at_line_start {
1372                    self.write_indent(self.state.depth)?;
1373                }
1374                self.write_quoted(v)?;
1375                self.write_end_of_scalar()?;
1376                return Ok(());
1377            }
1378
1379            // Emit block string. If we are a mapping value, YAML requires a space after ':'.
1380            // Insert it now if pending.
1381            //
1382            // IMPORTANT: capture whether we were in a map-value position *before* clearing
1383            // `pending_space_after_colon`, as that context influences indentation.
1384            let was_map_value = self.state.pending_layout.pending_space_after_colon;
1385            self.write_space_if_pending()?;
1386            // Determine base indentation for the block scalar header/body.
1387            //
1388            // Important: `after_dash_depth` is only meaningful for the immediate node that
1389            // follows a sequence dash ("- "). It must NOT affect nested scalars inside a
1390            // mapping that happens to be a sequence element, otherwise block scalar bodies
1391            // become under-indented (invalid YAML).
1392            //
1393            // For map values (we are mid-line after `key:`), prefer the mapping depth.
1394            // Otherwise, if we are starting a new node right after a dash, use that depth.
1395            let base = if was_map_value {
1396                self.state.current_map_depth.unwrap_or(self.state.depth)
1397            } else {
1398                // Use after_dash_depth when available (we're after a sequence dash),
1399                // regardless of at_line_start (which is false after writing "- ").
1400                self.state.after_dash_depth.unwrap_or(self.state.depth)
1401            };
1402            if self.state.at_line_start {
1403                self.write_indent(base)?;
1404            }
1405
1406            // Anchors/tags are node properties and must be emitted before scalar content.
1407            // For a block scalar this produces e.g. `text: &a1 |` — without this, a pending
1408            // anchor would leak onto the next scalar.
1409            self.write_scalar_prefix_if_anchor()?;
1410
1411            // Compute the indentation indicator N for block scalars.
1412            //
1413            // Per YAML 1.2 8.1.1.1, the indicator is the *additional* indentation steps
1414            // beyond the parent node's indentation level — NOT the absolute column count.
1415            // Since our body is exactly one serializer depth (i.e. `indent_step` spaces)
1416            // deeper than its parent, the indicator is simply `indent_step`.
1417            //
1418            // We only emit it when the first non-empty content line has leading whitespace,
1419            // which would otherwise prevent automatic indentation detection by the parser.
1420            // The body is one `indent_step` deeper than the parent node, and the indicator is
1421            // relative to the parent's indentation. For a value in a map opened after `- `,
1422            // the keys sit two columns past the dash, not at `base * indent_step`: measure
1423            // from (and indent past) that column.
1424            let parent_column = match (was_map_value, self.state.current_map_key_column) {
1425                (true, Some(col)) => col,
1426                _ => checked_indentation(self.settings.indent_step, base)?,
1427            };
1428            let body_column = parent_column
1429                .checked_add(self.settings.indent_step)
1430                .ok_or_else(|| Error::custom("serializer indentation exceeds usize"))?;
1431            let indent_n = self.settings.indent_step;
1432
1433            // Check if we need an explicit indentation indicator.
1434            // Required when the first non-empty line has leading whitespace.
1435            let content_trimmed = v.trim_end_matches('\n');
1436            let first_line_spaces = self::wrapping::first_line_leading_spaces(content_trimmed);
1437            let needs_indicator = first_line_spaces > 0;
1438
1439            // Resolve the indicator digit up front. If the helper rejects `indent_n`
1440            // (i.e. outside the YAML 1.2 `[1-9]` grammar), fall back to quoting.
1441            // Anchor prefix is already written above, so don't call it again here.
1442            let indicator_digit = if needs_indicator {
1443                let Ok(digit) = block_indent_indicator_digit(indent_n) else {
1444                    self.write_plain_or_quoted_value(v)?;
1445                    self.write_end_of_scalar()?;
1446                    return Ok(());
1447                };
1448                Some(digit)
1449            } else {
1450                None
1451            };
1452
1453            match style {
1454                StrStyle::Literal => {
1455                    // Determine trailing newline count to select chomp indicator:
1456                    //  - 0 → "|-" (strip)
1457                    //  - 1 with non-empty content → "|" (clip)
1458                    //  - newline-only content → "|+" (keep)
1459                    //  - >=2 → "|+" (keep)
1460                    let content = v.trim_end_matches('\n');
1461                    let trailing_nl = v.len() - content.len();
1462
1463                    // Write block scalar header: | or |N with optional chomp indicator
1464                    self.out.write_char('|')?;
1465                    if let Some(digit) = indicator_digit {
1466                        self.out.write_char(digit)?;
1467                    }
1468                    match trailing_nl {
1469                        0 => self.out.write_char('-')?,
1470                        // Clip chomping discards every newline in an empty block.
1471                        1 if !content.is_empty() => {}
1472                        _ => self.out.write_char('+')?,
1473                    }
1474                    #[cfg(feature = "comments")]
1475                    self.write_pending_inline_comment()?;
1476                    self.newline()?;
1477
1478                    // Emit body lines. For non-empty content, write each line exactly once.
1479                    // For keep chomping (>=2), append (trailing_nl - 1) visual empty lines.
1480                    // Newline-only content needs one empty line per original newline.
1481                    // Precompute body indent string once for the entire block
1482                    let mut indent_buf: String = String::new();
1483                    let spaces = body_column;
1484                    if spaces > 0 {
1485                        indent_buf.reserve(spaces);
1486                        for _ in 0..spaces {
1487                            indent_buf.push(' ');
1488                        }
1489                    }
1490                    let indent_str = indent_buf.as_str();
1491
1492                    if content.is_empty() {
1493                        for _ in 0..trailing_nl {
1494                            self.newline()?;
1495                        }
1496                    } else {
1497                        for line in content.split('\n') {
1498                            // Empty lines need no indentation; whitespace in non-empty
1499                            // content lines must be preserved exactly.
1500                            if !line.is_empty() {
1501                                self.out.write_str(indent_str)?;
1502                                self.state.at_line_start = false;
1503                                self.out.write_str(line)?;
1504                            }
1505                            self.newline()?;
1506                        }
1507                        if trailing_nl >= 2 {
1508                            for _ in 0..(trailing_nl - 1) {
1509                                self.newline()?;
1510                            }
1511                        }
1512                    }
1513                }
1514                StrStyle::Folded => {
1515                    // Write block scalar header: > or >N with optional chomp indicator
1516                    self.out.write_char('>')?;
1517                    if let Some(digit) = indicator_digit {
1518                        self.out.write_char(digit)?;
1519                    }
1520                    if from_auto {
1521                        // Auto-selected folded style: choose chomping based on trailing newlines
1522                        // to preserve exact content on round-trip.
1523                        let content = v.trim_end_matches('\n');
1524                        let trailing_nl = v.len() - content.len();
1525                        match trailing_nl {
1526                            0 => self.out.write_char('-')?,
1527                            1 => {} // clip is the default, no indicator needed
1528                            _ => self.out.write_char('+')?,
1529                        }
1530                    }
1531                    // Note: Explicit FoldStr/FoldString wrappers historically used plain '>'
1532                    // regardless of trailing newline; keep that behavior for compatibility.
1533                    #[cfg(feature = "comments")]
1534                    self.write_pending_inline_comment()?;
1535                    self.newline()?;
1536                    self.write_folded_block(v, body_column)?;
1537                }
1538            }
1539            return Ok(());
1540        }
1541        self.write_space_if_pending()?;
1542        self.write_scalar_prefix_if_anchor()?;
1543        if self.state.at_line_start {
1544            self.write_indent(self.state.depth)?;
1545        }
1546        // Special-case: prefer single-quoted style for select 1-char punctuation to
1547        // match expected YAML output in tests ('.', '#', '-').
1548        if v.len() == 1
1549            && let Some(ch) = v.chars().next()
1550            && (ch == '.' || ch == '#' || ch == '-')
1551        {
1552            self.out.write_char('\'')?;
1553            self.out.write_char(ch)?;
1554            self.out.write_char('\'')?;
1555            self.write_end_of_scalar()?;
1556            return Ok(());
1557        }
1558        self.write_plain_or_quoted_value(v)?;
1559        self.write_end_of_scalar()?;
1560        Ok(())
1561    }
1562
1563    fn serialize_bytes(self, v: &[u8]) -> Result<()> {
1564        // Three behaviors are required by tests:
1565        // - Top-level &[u8] should serialize as a block sequence of integers.
1566        // - A captured root !!binary tag must keep the bytes on a base64 scalar;
1567        //   attaching that tag to the usual root sequence changes the YAML node kind.
1568        // - Fields using #[serde(with = "serde_bytes")] should serialize as a tagged !!binary
1569        //   base64 scalar inline after "key: ". The latter ends up calling serialize_bytes in
1570        //   value position (mid-line), whereas plain Vec<u8> without serde_bytes goes through
1571        //   serialize_seq instead.
1572        //
1573        // The untagged sequence is reachable only while no tag is staged at all. Any staged
1574        // tag makes this a tagged scalar node, so the base64 branch below both keeps a
1575        // requested !!binary on a scalar and lets any other identity clash with the
1576        // !!binary that branch stages, exactly as it does in value position.
1577        if self.state.at_line_start && self.state.pending_tag.is_none() {
1578            // Top-level or start-of-line: emit as sequence of numbers
1579            let mut seq = self.serialize_seq(Some(v.len()))?;
1580            for b in v {
1581                serde_core::ser::SerializeSeq::serialize_element(&mut seq, b)?;
1582            }
1583            return serde_core::ser::SerializeSeq::end(seq);
1584        }
1585
1586        // Inline value position, or a root value whose captured tag is binary:
1587        // emit !!binary with base64.
1588        self.write_space_if_pending()?;
1589        self.stage_core_tag("binary")?;
1590        self.write_scalar_prefix_if_anchor()?;
1591        // The prefix writer handles root indentation and inline placement.
1592        let mut s = String::new();
1593        B64.encode_string(v, &mut s);
1594        self.out.write_str(&s)?;
1595        self.write_end_of_scalar()?;
1596        Ok(())
1597    }
1598
1599    fn serialize_none(self) -> Result<()> {
1600        self.write_space_if_pending()?;
1601        self.write_scalar_prefix_if_anchor()?;
1602        self.state.last_value_was_block = false;
1603        if self.state.at_line_start {
1604            self.write_indent(self.state.depth)?;
1605        }
1606        self.out.write_str("null")?;
1607        self.write_end_of_scalar()?;
1608        Ok(())
1609    }
1610
1611    fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<()> {
1612        value.serialize(self)
1613    }
1614
1615    fn serialize_unit(self) -> Result<()> {
1616        self.write_space_if_pending()?;
1617        self.write_scalar_prefix_if_anchor()?;
1618        self.state.last_value_was_block = false;
1619        if self.state.at_line_start {
1620            self.write_indent(self.state.depth)?;
1621        }
1622        self.out.write_str("null")?;
1623        self.write_end_of_scalar()?;
1624        Ok(())
1625    }
1626
1627    fn serialize_unit_struct(self, _name: &'static str) -> Result<()> {
1628        self.serialize_unit()
1629    }
1630
1631    fn serialize_unit_variant(
1632        self,
1633        name: &'static str,
1634        _variant_index: u32,
1635        variant: &'static str,
1636    ) -> Result<()> {
1637        // If we are in a mapping value position, insert the deferred space after ':'
1638        self.write_space_if_pending()?;
1639        if self.settings.tagged_enums {
1640            self.serialize_tagged_scalar(name, variant)
1641        } else {
1642            self.serialize_str(variant)
1643        }
1644    }
1645
1646    fn serialize_newtype_struct<T: ?Sized + Serialize>(
1647        self,
1648        name: &'static str,
1649        value: &T,
1650    ) -> Result<()> {
1651        // Flow hints & block-string hints:
1652        match name {
1653            NAME_FLOW_SEQ => {
1654                self.state.pending_flow = Some(PendingFlow::AnySeq);
1655                return value.serialize(self);
1656            }
1657            NAME_FLOW_MAP => {
1658                self.state.pending_flow = Some(PendingFlow::AnyMap);
1659                return value.serialize(self);
1660            }
1661            NAME_LIT_STR => {
1662                // Always use literal block style for LitStr/LitString wrappers.
1663                // Choose chomping based on trailing newlines during actual emission.
1664                // Capture the inner string first.
1665                let mut cap = StrCapture::default();
1666                value.serialize(&mut cap)?;
1667                let s = cap.finish()?;
1668                self.state.pending_str_style = Some(PendingStrStyle::Explicit(StrStyle::Literal));
1669                return self.serialize_str(&s);
1670            }
1671            NAME_FOLD_STR => {
1672                let mut cap = StrCapture::default();
1673                value.serialize(&mut cap)?;
1674                let s = cap.finish()?;
1675                let is_multiline = s.contains('\n');
1676                if !is_multiline && s.len() < self.settings.min_fold_chars {
1677                    return self.serialize_str(&s);
1678                }
1679                self.state.pending_str_style = Some(PendingStrStyle::Explicit(StrStyle::Folded));
1680                return self.serialize_str(&s);
1681            }
1682            NAME_SPACE_AFTER => {
1683                // Serialize the value, then emit an empty line after (only in block style).
1684                let result = value.serialize(&mut *self);
1685                if self.state.in_flow == 0 {
1686                    // Emit an extra blank line after the value
1687                    self.newline()?;
1688                }
1689                return result;
1690            }
1691            NAME_DOUBLE_QUOTED => {
1692                let mut cap = StrCapture::default();
1693                value.serialize(&mut cap)?;
1694                let text = cap.finish()?;
1695                return self.serialize_double_quoted_scalar(&text);
1696            }
1697            NAME_SINGLE_QUOTED => {
1698                let mut cap = StrCapture::default();
1699                value.serialize(&mut cap)?;
1700                let text = cap.finish()?;
1701                return self.serialize_single_quoted_scalar(&text);
1702            }
1703            NAME_NULLABLE_TILDE => {
1704                return self.serialize_tilde_null();
1705            }
1706            _ => {}
1707        }
1708        // default: ignore the name, serialize the inner as-is
1709        value.serialize(self)
1710    }
1711
1712    fn serialize_newtype_variant<T: ?Sized + Serialize>(
1713        self,
1714        _name: &'static str,
1715        _variant_index: u32,
1716        variant: &'static str,
1717        value: &T,
1718    ) -> Result<()> {
1719        // `!Variant payload` already carries the same variant selection as
1720        // Serde's usual `{Variant: payload}` representation. Keep the tag
1721        // staged for the payload and omit the redundant external wrapper.
1722        if self.pending_tag_selects_variant(variant)? {
1723            return value.serialize(self);
1724        }
1725
1726        let was_inline_value = self.state.pending_layout.pending_space_after_colon;
1727        let anchor_broke_line = self.has_pending_node_properties();
1728        let after_dash_depth = self.state.after_dash_depth;
1729        self.write_anchor_for_complex_node()?;
1730
1731        // If we are the value of a mapping key, YAML forbids "key: Variant: value" inline.
1732        // Emit the variant mapping on the next line indented one level. Also, do not insert
1733        // a space after the colon when the value may itself be a mapping; instead, defer
1734        // space insertion to the value serializer via pending_space_after_colon.
1735        if was_inline_value {
1736            // consume the pending space request and start a new line
1737            self.state.pending_layout.pending_space_after_colon = false;
1738            if !self.state.at_line_start {
1739                self.newline()?;
1740            }
1741            // When used as a mapping value, indent relative to the parent mapping's base,
1742            // not the serializer's current depth (which may still be the outer level).
1743            let base = self.state.current_map_depth.unwrap_or(self.state.depth);
1744            let variant_depth = checked_depth_add(base, 1)?;
1745            self.write_indent(variant_depth)?;
1746            self.write_key_scalar(variant)?;
1747            // Write ':' without trailing space, then mark that a space may be needed
1748            // if the following value is a scalar.
1749            self.out.write_str(":")?;
1750            self.state.pending_layout.pending_space_after_colon = true;
1751            self.state.at_line_start = false;
1752            // Do not let any inline-after-dash hint leak into the variant's inner value.
1753            // After `Variant:`, the next node is in value position and must choose its own layout.
1754            self.state.pending_layout.pending_inline_map = false;
1755            // Ensure that if the value is another variant or a mapping/sequence,
1756            // it indents under this variant label rather than the parent map key.
1757            let prev_map_depth = self.state.current_map_depth.replace(variant_depth);
1758            // The label is on its own line at `variant_depth`: an enclosing aligned key column
1759            // no longer applies.
1760            let prev_key_column = self.state.current_map_key_column.take();
1761            let res = value.serialize(&mut *self);
1762            self.state.current_map_depth = prev_map_depth;
1763            self.state.current_map_key_column = prev_key_column;
1764            return res;
1765        }
1766        // Otherwise (top-level or sequence context).
1767        if self.state.at_line_start {
1768            let depth = if anchor_broke_line {
1769                match after_dash_depth {
1770                    Some(depth) => checked_depth_add(depth, 1)?,
1771                    None => self.state.depth,
1772                }
1773            } else {
1774                self.state.depth
1775            };
1776            self.write_indent(depth)?;
1777        }
1778        self.write_key_scalar(variant)?;
1779        // Write ':' without a space and defer spacing/newline to the value serializer.
1780        self.out.write_str(":")?;
1781        self.state.pending_layout.pending_space_after_colon = true;
1782        self.state.at_line_start = false;
1783        // Do not let SeqSer's "inline first key after dash" hint leak into the variant's inner value.
1784        // Without this, a struct/map value can start as `Variant: a: 1`.
1785        self.state.pending_layout.pending_inline_map = false;
1786        // If this variant is inside a block sequence element (`- Variant:`), ensure the nested
1787        // value indents under the variant label rather than aligning with the list indentation.
1788        // SeqSer stores the dash's indentation depth in `after_dash_depth`.
1789        if let Some(d) = self.state.after_dash_depth.take() {
1790            let nested_depth = self.variant_depth_after_dash(d, anchor_broke_line)?;
1791            let prev_map_depth = self.state.current_map_depth.replace(nested_depth);
1792            // An inline `- Variant:` label sits two columns past the dash; block scalar
1793            // values measure their indentation from that column.
1794            let key_column = if anchor_broke_line {
1795                None
1796            } else {
1797                Some(checked_indentation(self.settings.indent_step, d)? + 2)
1798            };
1799            let prev_key_column =
1800                std::mem::replace(&mut self.state.current_map_key_column, key_column);
1801            let res = value.serialize(&mut *self);
1802            self.state.current_map_depth = prev_map_depth;
1803            self.state.current_map_key_column = prev_key_column;
1804            res
1805        } else {
1806            value.serialize(&mut *self)
1807        }
1808    }
1809
1810    // -------- Collections --------
1811
1812    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
1813        let flow = self.take_flow_for_seq();
1814        if flow {
1815            // Ensure a space after a preceding colon when this sequence is a mapping value.
1816            self.write_space_if_pending()?;
1817            self.write_scalar_prefix_if_anchor()?;
1818            if self.state.at_line_start {
1819                self.write_indent(self.state.depth)?;
1820            }
1821            self.out.write_str("[")?;
1822            self.state.at_line_start = false;
1823            let depth_next = self.state.depth; // inline
1824            Ok(SeqSer {
1825                ser: self,
1826                depth: depth_next,
1827                flow: true,
1828                first: true,
1829                force_empty_marker: false,
1830            })
1831        } else {
1832            // Block sequence. Decide indentation based on whether this is after a map key or after a list dash.
1833            let was_inline_value = !self.state.at_line_start;
1834
1835            // If we are a value following a block sibling, force a newline now.
1836            // However, if a complex-node anchor is pending, we must keep `key: &aN` inline;
1837            // `write_anchor_for_complex_node` will handle emitting the anchor and newline.
1838            if self.state.pending_layout.pending_space_after_colon
1839                && self.state.last_value_was_block
1840                && !self.has_pending_node_properties()
1841            {
1842                self.state.pending_layout.pending_space_after_colon = false;
1843                if !self.state.at_line_start {
1844                    self.newline()?;
1845                }
1846                // Consume the sibling-block marker; it should not affect nested nodes.
1847                self.state.last_value_was_block = false;
1848            }
1849
1850            // For block sequences nested under another dash, keep the first inner dash inline.
1851            // Style expectations in tests prefer the compact form:
1852            // - - 1
1853            // instead of:
1854            // -
1855            //   - 1
1856            // The inline form puts the inner items two columns (the width of "- ") past the
1857            // outer dash, so it only lines up with the following items when one indentation
1858            // level is two columns. With any other `indent_step`, start the inner sequence on
1859            // its own line, one level deeper.
1860            let after_dash = (!self.state.at_line_start)
1861                && self.state.after_dash_depth.is_some()
1862                && !self.state.pending_layout.pending_space_after_colon;
1863            let inline_first = after_dash && self.settings.indent_step == 2;
1864            // `inline_first` assumes we stay mid-line, but a pending anchor writes `&aN\n` first.
1865            let anchor_broke_line = self.has_pending_node_properties();
1866            // Pending node properties (`- &a1`) end the line themselves below.
1867            if after_dash && !inline_first {
1868                self.state.pending_layout.pending_inline_map = false;
1869                if !anchor_broke_line {
1870                    self.newline()?;
1871                }
1872            }
1873            self.write_anchor_for_complex_node()?;
1874            if inline_first {
1875                if anchor_broke_line {
1876                    // Inlining now would drop the nested dashes to column 0, past the anchor.
1877                    self.state.pending_layout.pending_inline_map = false;
1878                } else {
1879                    // Collapsing onto the parent dash yields the preferred `- - 1` shape.
1880                    self.state.at_line_start = false;
1881                }
1882            } else if was_inline_value {
1883                // Mid-line start. If we are here due to a map value (after ':'), defer the newline
1884                // decision until the first element is emitted so that empty sequences can stay inline
1885                // as `key: []`. If we are here due to a list dash, keep inline.
1886                // Intentionally do not clear `pending_space_after_colon` and do not newline here.
1887            }
1888            // Indentation policy mirrors serialize_map:
1889            // - After a list dash inline_first: base is dash depth; indent one level deeper.
1890            // - As a value after a map key: base is current_map_depth (if set), indent one level deeper.
1891            // - Otherwise (top-level or already at line start): base is current depth.
1892            let base = if after_dash {
1893                self.state.after_dash_depth.unwrap_or(self.state.depth)
1894            } else if was_inline_value && self.state.current_map_depth.is_some() {
1895                self.state.current_map_depth.unwrap_or(self.state.depth)
1896            } else {
1897                self.state.depth
1898            };
1899            // For sequences used as a mapping value, indent them one level deeper so the dash is
1900            // nested under the parent key (consistent with serde_yaml's formatting). Keep block
1901            // sequences inline only when they immediately follow another dash.
1902            let depth_next = if after_dash {
1903                checked_depth_add(base, 1)?
1904            } else if was_inline_value {
1905                // Compact: the dashes go at the key's own column. That column is
1906                // `base * indent_step`, except for a map opened after `- `, whose keys are
1907                // aligned two columns past the dash; with `indent_step` 1 that is deeper than
1908                // `base`, so the compact form would put the dashes left of the key.
1909                if self.settings.compact_list_indent
1910                    && self.state.current_map_depth.is_some()
1911                    && !anchor_broke_line
1912                    && self.settings.indent_step >= 2
1913                {
1914                    base
1915                } else {
1916                    checked_depth_add(base, 1)?
1917                }
1918            } else {
1919                base
1920            };
1921            // Starting a complex (block) sequence: drop any staged inline comment.
1922            #[cfg(feature = "comments")]
1923            {
1924                self.state.pending_inline_comment = None;
1925            }
1926            Ok(SeqSer {
1927                ser: self,
1928                depth: depth_next,
1929                flow: false,
1930                first: true,
1931                force_empty_marker: anchor_broke_line,
1932            })
1933        }
1934    }
1935
1936    fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple> {
1937        self.serialize_seq(Some(len))
1938    }
1939
1940    fn serialize_tuple_struct(
1941        self,
1942        name: &'static str,
1943        len: usize,
1944    ) -> Result<Self::SerializeTupleStruct> {
1945        match name {
1946            NAME_TUPLE_ANCHOR => Ok(TupleSer::anchor_strong(self)),
1947            NAME_TUPLE_WEAK => Ok(TupleSer::anchor_weak(self)),
1948            #[cfg(feature = "comments")]
1949            NAME_TUPLE_COMMENTED => Ok(TupleSer::commented(self)),
1950            NAME_TUPLE_TAGGED => Ok(TupleSer::tagged(self)),
1951            // Normal tuple-struct: emit as a block sequence.
1952            _ => Ok(TupleSer::sequence(self.serialize_seq(Some(len))?)),
1953        }
1954    }
1955
1956    fn serialize_tuple_variant(
1957        self,
1958        _name: &'static str,
1959        _variant_index: u32,
1960        variant: &'static str,
1961        len: usize,
1962    ) -> Result<Self::SerializeTupleVariant> {
1963        if self.pending_tag_selects_variant(variant)? {
1964            return self.serialize_seq(Some(len));
1965        }
1966
1967        let was_inline_value = self.state.pending_layout.pending_space_after_colon;
1968        let anchor_broke_line = self.has_pending_node_properties();
1969        let after_dash_depth = self.state.after_dash_depth;
1970        self.write_anchor_for_complex_node()?;
1971
1972        // If we are the value of a mapping key, YAML forbids keeping a nested mapping
1973        // on the same line (e.g., "key: Variant:"). Move the variant mapping to the next line
1974        // indented under the parent mapping's base depth.
1975        if was_inline_value {
1976            self.state.pending_layout.pending_space_after_colon = false;
1977            if !self.state.at_line_start {
1978                self.newline()?;
1979            }
1980            let base =
1981                checked_depth_add(self.state.current_map_depth.unwrap_or(self.state.depth), 1)?;
1982            self.write_indent(base)?;
1983            self.write_key_scalar(variant)?;
1984            self.out.write_str(":\n")?;
1985            self.state.at_line_start = true;
1986            self.state.pending_layout.pending_inline_map = false;
1987            let depth_next = checked_depth_add(base, 1)?;
1988            return Ok(SeqSer {
1989                ser: self,
1990                depth: depth_next,
1991                flow: false,
1992                first: true,
1993                force_empty_marker: false,
1994            });
1995        }
1996        // Otherwise (top-level or sequence context).
1997        if self.state.at_line_start {
1998            let depth = if anchor_broke_line {
1999                match after_dash_depth {
2000                    Some(depth) => checked_depth_add(depth, 1)?,
2001                    None => self.state.depth,
2002                }
2003            } else {
2004                self.state.depth
2005            };
2006            self.write_indent(depth)?;
2007        }
2008        self.write_key_scalar(variant)?;
2009        self.out.write_str(":\n")?;
2010        self.state.at_line_start = true;
2011        let mut depth_next = checked_depth_add(self.state.depth, 1)?;
2012        if let Some(d) = self.state.after_dash_depth.take() {
2013            depth_next =
2014                checked_depth_add(self.variant_depth_after_dash(d, anchor_broke_line)?, 1)?;
2015            self.state.pending_layout.pending_inline_map = false;
2016        }
2017        Ok(SeqSer {
2018            ser: self,
2019            depth: depth_next,
2020            flow: false,
2021            first: true,
2022            force_empty_marker: false,
2023        })
2024    }
2025
2026    fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap> {
2027        let flow = self.take_flow_for_map();
2028        if flow {
2029            // Ensure a space after a preceding colon when this mapping is a value.
2030            self.write_space_if_pending()?;
2031            self.write_scalar_prefix_if_anchor()?;
2032            if self.state.at_line_start {
2033                self.write_indent(self.state.depth)?;
2034            }
2035            self.out.write_str("{")?;
2036            self.state.at_line_start = false;
2037            let depth_next = self.state.depth;
2038            Ok(MapSer::flow(self, depth_next))
2039        } else {
2040            let node_properties_broke_line = self.has_pending_node_properties();
2041            // A map opened right after `- ` has its keys two columns past the dash, but its
2042            // nested values are indented by levels from the dash: with a one-column
2043            // `indent_step` a nested value would land left of its key. Start such a map on its
2044            // own line instead.
2045            if self.state.pending_layout.pending_inline_map
2046                && self.settings.indent_step < 2
2047                && !self.state.at_line_start
2048                && self.state.after_dash_depth.is_some()
2049                && !self.state.pending_layout.pending_space_after_colon
2050            {
2051                self.state.pending_layout.pending_inline_map = false;
2052                self.newline()?;
2053                if let Some(d) = self.state.after_dash_depth {
2054                    self.state.depth = checked_depth_add(d, 1)?;
2055                }
2056            }
2057            let inline_first = self.state.pending_layout.pending_inline_map;
2058            // Starting a complex (block) map: drop any staged inline comment.
2059            #[cfg(feature = "comments")]
2060            {
2061                self.state.pending_inline_comment = None;
2062            }
2063            // We only consider "value position" when immediately after a mapping colon.
2064            let was_inline_value = self.state.pending_layout.pending_space_after_colon;
2065            let mut forced_newline = false;
2066
2067            // If we are a value following a block sibling, force a newline now.
2068            // However, if a complex-node anchor is pending, we must keep `key: &aN` inline;
2069            // `write_anchor_for_complex_node` will handle emitting the anchor and newline.
2070            if was_inline_value
2071                && self.state.last_value_was_block
2072                && !self.has_pending_node_properties()
2073            {
2074                self.state.pending_layout.pending_space_after_colon = false;
2075                if !self.state.at_line_start {
2076                    self.newline()?;
2077                }
2078                forced_newline = true;
2079                // Consume the sibling-block marker; it should not affect nested nodes.
2080                self.state.last_value_was_block = false;
2081            }
2082
2083            self.write_anchor_for_complex_node()?;
2084            if inline_first {
2085                // Suppress newline after a list dash for inline map first key.
2086                self.state.pending_layout.pending_inline_map = false;
2087                // Mark that this sequence element is a mapping printed inline after a dash.
2088                self.state.pending_layout.inline_map_after_dash = true;
2089            } else if was_inline_value {
2090                // Map used as a value after "key: ". If emitting braces for empty maps,
2091                // keep this mapping on the same line so that an empty map renders as "{}".
2092                //
2093                // IMPORTANT: if the map is known to be non-empty (len > 0), we must NOT keep it
2094                // inline (otherwise we can end up emitting the first entry as `key: a: 1`).
2095                // When len is unknown, we keep the legacy behavior and let MapSer decide once the
2096                // first key arrives.
2097                let known_empty = matches!(len, Some(0));
2098                let known_non_empty = matches!(len, Some(n) if n > 0);
2099
2100                if !self.settings.empty_as_braces || known_non_empty {
2101                    // Move the mapping body to the next line.
2102                    // If an anchor was emitted, we are already at the start of a new line.
2103                    self.state.pending_layout.pending_space_after_colon = false;
2104                    if !self.state.at_line_start {
2105                        self.newline()?;
2106                    }
2107                } else if !known_empty {
2108                    // len is unknown: keep it inline for now (so empty maps can still render as
2109                    // `key: {}`), and let MapSer break the line when the first key arrives.
2110                }
2111            }
2112            // Indentation rules:
2113            // - Top-level (at line start, not after dash): use current depth.
2114            // - After dash inline first key or as a value: indent one level deeper for subsequent lines.
2115            // Use the current mapping's depth as base only when we are in a VALUE position.
2116            // For complex KEYS (non-scalar), keep using the current serializer depth so that
2117            // subsequent key lines indent relative to the "? " line, not the parent map's base.
2118            let base = if inline_first {
2119                self.state.after_dash_depth.unwrap_or(self.state.depth)
2120            } else if was_inline_value && self.state.current_map_depth.is_some() {
2121                self.state.current_map_depth.unwrap_or(self.state.depth)
2122            } else {
2123                self.state.depth
2124            };
2125            let depth_next = if inline_first || was_inline_value {
2126                checked_depth_add(base, 1)?
2127            } else {
2128                base
2129            };
2130            let inline_value_start_flag = was_inline_value
2131                && self.settings.empty_as_braces
2132                && len.is_none()
2133                && !inline_first
2134                && !forced_newline;
2135            Ok(MapSer::block(
2136                self,
2137                depth_next,
2138                inline_first,
2139                inline_value_start_flag,
2140                node_properties_broke_line,
2141            ))
2142        }
2143    }
2144
2145    fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
2146        self.serialize_map(Some(len))
2147    }
2148
2149    fn serialize_struct_variant(
2150        self,
2151        _name: &'static str,
2152        _variant_index: u32,
2153        variant: &'static str,
2154        len: usize,
2155    ) -> Result<Self::SerializeStructVariant> {
2156        if self.pending_tag_selects_variant(variant)? {
2157            return Ok(StructVariantSer::payload(self.serialize_map(Some(len))?));
2158        }
2159
2160        let was_inline_value = self.state.pending_layout.pending_space_after_colon;
2161        let anchor_broke_line = self.has_pending_node_properties();
2162        let after_dash_depth = self.state.after_dash_depth;
2163        self.write_anchor_for_complex_node()?;
2164
2165        // If we are the value of a mapping key, YAML forbids keeping a nested mapping
2166        // on the same line (e.g., "key: Variant:"). Move the variant mapping to the next line
2167        // indented under the parent mapping's base depth.
2168        if was_inline_value {
2169            // Value position after a map key: start the variant mapping on the next line.
2170            self.state.pending_layout.pending_space_after_colon = false;
2171            if !self.state.at_line_start {
2172                self.newline()?;
2173            }
2174            // Indent the variant name one level under the parent mapping.
2175            let base =
2176                checked_depth_add(self.state.current_map_depth.unwrap_or(self.state.depth), 1)?;
2177            self.write_indent(base)?;
2178            self.write_key_scalar(variant)?;
2179            self.out.write_str(":\n")?;
2180            self.state.at_line_start = true;
2181            // A complex key stages an inline hint for its value; clear it before the fields.
2182            self.state.pending_layout.pending_inline_map = false;
2183            // Fields indent one more level under the variant label.
2184            let depth_next = checked_depth_add(base, 1)?;
2185            return Ok(StructVariantSer::external(self, depth_next));
2186        }
2187        // Otherwise (top-level or sequence context), emit the variant name at current depth.
2188        if self.state.at_line_start {
2189            let depth = if anchor_broke_line {
2190                match after_dash_depth {
2191                    Some(depth) => checked_depth_add(depth, 1)?,
2192                    None => self.state.depth,
2193                }
2194            } else {
2195                self.state.depth
2196            };
2197            self.write_indent(depth)?;
2198        }
2199        self.write_key_scalar(variant)?;
2200        self.out.write_str(":\n")?;
2201        self.state.at_line_start = true;
2202        // Default indentation for fields under a plain variant line.
2203        let mut depth_next = checked_depth_add(self.state.depth, 1)?;
2204        // Fields must be deeper than the variant label, including the "- " prefix.
2205        if let Some(d) = self.state.after_dash_depth.take() {
2206            depth_next =
2207                checked_depth_add(self.variant_depth_after_dash(d, anchor_broke_line)?, 1)?;
2208            self.state.pending_layout.pending_inline_map = false;
2209        }
2210        Ok(StructVariantSer::external(self, depth_next))
2211    }
2212}
2213
2214#[cfg(test)]
2215mod tests {
2216    use super::{YamlSerializer, absolute_uri_is_valid};
2217
2218    #[test]
2219    fn absolute_uri_validation_covers_global_tag_syntax() {
2220        for valid in [
2221            "tag:yaml.org,2002:str",
2222            "urn:isbn:9780141036144",
2223            "https://example.com/path?query=yes#section",
2224            "https://example.com",
2225            "git+ssh://user@example.com:22/repository",
2226            "custom:",
2227            "http://[2001:db8::1]:8080/path",
2228            "http://[v1.alpha:beta]/path",
2229            "tag:escaped%20text",
2230        ] {
2231            assert!(absolute_uri_is_valid(valid), "expected valid URI: {valid}");
2232        }
2233
2234        for invalid in [
2235            "",
2236            "$:?",
2237            "relative/path",
2238            "1scheme:value",
2239            "bad scheme:value",
2240            "http://[not-ip]/",
2241            "http://[v1]/",
2242            "http://[2001:db8::1/",
2243            "http://example.com:port/",
2244            "http://host:name:80/",
2245            "http://bad[info@example.com/",
2246            "http://first@second@third/",
2247            "http://example.com/path[0]",
2248            "tag:value?bad[query]",
2249            "tag:value#bad[fragment]",
2250            "tag:bad%escape",
2251        ] {
2252            assert!(
2253                !absolute_uri_is_valid(invalid),
2254                "expected invalid URI: {invalid}"
2255            );
2256        }
2257    }
2258
2259    #[test]
2260    fn write_indent_rejects_indentation_overflow() {
2261        let mut out = String::new();
2262        let mut serializer = YamlSerializer::new(&mut out);
2263        let err = serializer.write_indent(usize::MAX).unwrap_err();
2264        assert_eq!(err.to_string(), "serializer indentation exceeds usize");
2265        assert!(out.is_empty());
2266    }
2267}