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