Skip to main content

yaml_edit/
as_yaml.rs

1//! AsYaml trait and YamlNode for unified access to YAML values.
2//!
3//! ## Type hierarchy
4//!
5//! This library has two tiers:
6//!
7//! - **CST types** ([`Document`](crate::yaml::Document), [`Mapping`](crate::yaml::Mapping),
8//!   [`Sequence`](crate::nodes::sequence::Sequence), [`Scalar`](crate::yaml::Scalar),
9//!   [`TaggedNode`](crate::yaml::TaggedNode)) — format-preserving wrappers around the
10//!   concrete syntax tree.  Parse a file to get these; navigate and mutate them in place.
11//!
12//! - **Input types** (`&str`, `i64`, `bool`, [`MappingBuilder`](crate::builder::MappingBuilder), …)
13//!   — supply these to mutation methods such as
14//!   [`Mapping::set`](crate::yaml::Mapping::set).  They implement [`AsYaml`] but are
15//!   never returned from navigation methods.
16//!
17//! [`YamlNode`] is the type-erased return type for navigation: you get one back from
18//! [`Mapping::get`](crate::yaml::Mapping::get), iterator methods like
19//! [`Mapping::keys`](crate::yaml::Mapping::keys), etc.  It is always backed by a real
20//! CST node; match on it to get the concrete type.
21
22use crate::yaml::{Alias, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
23use rowan::ast::AstNode;
24use std::borrow::Cow;
25use std::fmt;
26
27/// The kind of YAML value.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum YamlKind {
30    /// A mapping (object/dictionary)
31    Mapping,
32    /// A sequence (array/list)
33    Sequence,
34    /// A scalar value (string, number, boolean, null)
35    Scalar,
36    /// An alias reference (e.g. `*anchor_name`)
37    Alias,
38    /// A document (top-level container)
39    Document,
40    /// A tagged value (e.g. `!!set`, `!!omap`, `!!pairs`).
41    ///
42    /// Known built-in tags use a `'static` string; custom tags carry an owned string.
43    Tagged(Cow<'static, str>),
44}
45
46/// Trait for types that can be represented as YAML content.
47///
48/// Bridges the gap between CST nodes (which preserve formatting) and raw Rust
49/// values (which are convenient for constructing new content).
50///
51/// # Using `AsYaml` as a bound
52///
53/// Mutation methods such as [`Mapping::set`](crate::yaml::Mapping::set) accept
54/// `impl AsYaml`, so you can pass any of:
55///
56/// - A string literal (`"hello"`)
57/// - A number (`42_i64`, `3.14_f64`, …)
58/// - A boolean (`true`)
59/// - An existing CST node ([`Mapping`], [`Sequence`], [`Scalar`], [`YamlNode`], …)
60/// - A builder ([`MappingBuilder`](crate::builder::MappingBuilder), [`SequenceBuilder`](crate::builder::SequenceBuilder))
61pub trait AsYaml {
62    /// Returns a reference to the underlying `SyntaxNode` if one exists.
63    ///
64    /// CST wrappers (`Mapping`, `Sequence`, `Scalar`, `TaggedNode`, `YamlNode`)
65    /// return `Some`.  Raw Rust types (`i64`, `String`, etc.) return `None`.
66    fn as_node(&self) -> Option<&SyntaxNode>;
67
68    /// Returns the kind of YAML value this represents.
69    fn kind(&self) -> YamlKind;
70
71    /// Serialize this value into a `GreenNodeBuilder`.
72    ///
73    /// CST-backed types copy their node content; raw types synthesize an
74    /// appropriate CST structure.
75    ///
76    /// Returns `true` if the emitted content ends with a `NEWLINE` token
77    /// (used to avoid double newlines when nesting collections).
78    fn build_content(
79        &self,
80        builder: &mut rowan::GreenNodeBuilder,
81        indent: usize,
82        flow_context: bool,
83    ) -> bool;
84
85    /// Returns whether this value should be rendered on the same line as its key.
86    ///
87    /// `true` for scalars and empty collections; `false` for non-empty block
88    /// collections.
89    fn is_inline(&self) -> bool;
90}
91
92/// Compare two [`AsYaml`] values for semantic equality.
93///
94/// Semantic equality ignores formatting: `name`, `"name"`, and `'name'` all
95/// compare equal.  Both sides can be any combination of CST nodes and raw
96/// Rust values.
97pub fn yaml_eq<A, B>(a: &A, b: &B) -> bool
98where
99    A: AsYaml + ?Sized,
100    B: AsYaml + ?Sized,
101{
102    // If the left side has a backing node, dispatch on its concrete kind.
103    if let Some(node) = a.as_node() {
104        use crate::lex::SyntaxKind;
105        return match node.kind() {
106            SyntaxKind::SCALAR => Scalar::cast(node.clone()).is_some_and(|s| scalar_eq_rhs(&s, b)),
107            SyntaxKind::MAPPING => {
108                Mapping::cast(node.clone()).is_some_and(|m| mapping_eq_rhs(&m, b))
109            }
110            SyntaxKind::SEQUENCE => {
111                Sequence::cast(node.clone()).is_some_and(|s| sequence_eq_rhs(&s, b))
112            }
113            SyntaxKind::TAGGED_NODE => {
114                TaggedNode::cast(node.clone()).is_some_and(|t| tagged_eq_rhs(&t, b))
115            }
116            _ => false,
117        };
118    }
119
120    // Left side is raw — try the right side's node instead (symmetric).
121    if let Some(node) = b.as_node() {
122        use crate::lex::SyntaxKind;
123        return match node.kind() {
124            SyntaxKind::SCALAR => Scalar::cast(node.clone()).is_some_and(|s| scalar_eq_rhs(&s, a)),
125            SyntaxKind::MAPPING => {
126                Mapping::cast(node.clone()).is_some_and(|m| mapping_eq_rhs(&m, a))
127            }
128            SyntaxKind::SEQUENCE => {
129                Sequence::cast(node.clone()).is_some_and(|s| sequence_eq_rhs(&s, a))
130            }
131            SyntaxKind::TAGGED_NODE => {
132                TaggedNode::cast(node.clone()).is_some_and(|t| tagged_eq_rhs(&t, a))
133            }
134            _ => false,
135        };
136    }
137
138    // Both sides are raw — compare by kind, then decoded scalar string.
139    if a.kind() != b.kind() {
140        return false;
141    }
142    match (raw_scalar_str(a), raw_scalar_str(b)) {
143        (Some(sa), Some(sb)) => sa == sb,
144        _ => false,
145    }
146}
147
148/// Extract the decoded scalar string from a raw (no-node) `AsYaml` value.
149fn raw_scalar_str<T: AsYaml + ?Sized>(v: &T) -> Option<String> {
150    if v.as_node().is_some() || v.kind() != YamlKind::Scalar {
151        return None;
152    }
153    let mut builder = rowan::GreenNodeBuilder::new();
154    v.build_content(&mut builder, 0, false);
155    let green = builder.finish();
156    let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
157    Scalar::cast(node.clone())
158        .map(|s| s.as_string())
159        .or_else(|| Some(node.text().to_string()))
160}
161
162/// Get the semantic type and normalized value of a scalar for YAML-level comparison.
163///
164/// Returns (type_kind, normalized_value) where normalized values are comparable:
165/// - Different integer formats (123, 0x7B, 0o173) normalize to same i64
166/// - Different null representations (null, ~, Null) all become "null"
167/// - Different boolean cases (true, True, TRUE) normalize to lowercase
168fn scalar_semantic_value(scalar: &Scalar) -> Option<(crate::lex::SyntaxKind, String)> {
169    use crate::lex::SyntaxKind;
170    use crate::scalar::ScalarValue;
171
172    // Get the first token to determine the lexical type
173    let token = scalar.0.first_token()?;
174    let kind = token.kind();
175    let text = token.text();
176
177    let normalized = match kind {
178        SyntaxKind::INT => {
179            // Normalize all integer representations to their numeric value
180            ScalarValue::parse_integer(text)
181                .map(|v| v.to_string())
182                .unwrap_or_else(|| text.to_string())
183        }
184        SyntaxKind::FLOAT => {
185            // Normalize float representations
186            text.parse::<f64>()
187                .map(|v| v.to_string())
188                .unwrap_or_else(|_| text.to_string())
189        }
190        SyntaxKind::BOOL => {
191            // Normalize booleans to lowercase
192            text.to_lowercase()
193        }
194        SyntaxKind::NULL => {
195            // All null representations become "null"
196            "null".to_string()
197        }
198        SyntaxKind::STRING => {
199            // For strings, use the unescaped/unquoted value
200            scalar.as_string()
201        }
202        _ => {
203            // Block scalars, MERGE_KEY, etc. - use as_string()
204            scalar.as_string()
205        }
206    };
207
208    Some((kind, normalized))
209}
210
211fn scalar_eq_rhs<B: AsYaml + ?Sized>(lhs: &Scalar, rhs: &B) -> bool {
212    // Get or build the RHS scalar node
213    let rhs_scalar = if let Some(node) = rhs.as_node() {
214        let Some(scalar) = Scalar::cast(node.clone()) else {
215            return false;
216        };
217        scalar
218    } else {
219        // RHS is a raw AsYaml value (e.g., &str, &i64) - build it into a scalar
220        if rhs.kind() != YamlKind::Scalar {
221            return false;
222        }
223        let mut builder = rowan::GreenNodeBuilder::new();
224        rhs.build_content(&mut builder, 0, false);
225        let green = builder.finish();
226        let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
227        let Some(scalar) = Scalar::cast(node) else {
228            return false;
229        };
230        scalar
231    };
232
233    // Get semantic values for YAML-level comparison
234    let Some((lhs_kind, lhs_value)) = scalar_semantic_value(lhs) else {
235        return false;
236    };
237    let Some((rhs_kind, rhs_value)) = scalar_semantic_value(&rhs_scalar) else {
238        return false;
239    };
240
241    // For YAML semantic equality:
242    // - Types must match (STRING != INT even if text looks the same)
243    // - Normalized values must match (0x7B == 123, true == True)
244    lhs_kind == rhs_kind && lhs_value == rhs_value
245}
246
247fn mapping_eq_rhs<B: AsYaml + ?Sized>(lhs: &Mapping, rhs: &B) -> bool {
248    let Some(node) = rhs.as_node() else {
249        return false;
250    };
251    let Some(r) = Mapping::cast(node.clone()) else {
252        return false;
253    };
254    let lhs_pairs: Vec<_> = lhs.pairs().collect();
255    let rhs_pairs: Vec<_> = r.pairs().collect();
256    if lhs_pairs.len() != rhs_pairs.len() {
257        return false;
258    }
259    // pairs() yields raw KEY/VALUE wrapper nodes — peel them before comparing.
260    lhs_pairs
261        .iter()
262        .zip(rhs_pairs.iter())
263        .all(|((lk, lv), (rk, rv))| {
264            let Some(lk) = YamlNode::from_syntax_peeled(lk.clone()) else {
265                return false;
266            };
267            let Some(rk) = YamlNode::from_syntax_peeled(rk.clone()) else {
268                return false;
269            };
270            let Some(lv) = YamlNode::from_syntax_peeled(lv.clone()) else {
271                return false;
272            };
273            let Some(rv) = YamlNode::from_syntax_peeled(rv.clone()) else {
274                return false;
275            };
276            yaml_eq(&lk, &rk) && yaml_eq(&lv, &rv)
277        })
278}
279
280fn sequence_eq_rhs<B: AsYaml + ?Sized>(lhs: &Sequence, rhs: &B) -> bool {
281    let Some(node) = rhs.as_node() else {
282        return false;
283    };
284    let Some(r) = Sequence::cast(node.clone()) else {
285        return false;
286    };
287    let lhs_items: Vec<_> = lhs.items().collect();
288    let rhs_items: Vec<_> = r.items().collect();
289    if lhs_items.len() != rhs_items.len() {
290        return false;
291    }
292    // items() already yields peeled content nodes.
293    lhs_items.iter().zip(rhs_items.iter()).all(|(l, r)| {
294        match (
295            YamlNode::from_syntax(l.clone()),
296            YamlNode::from_syntax(r.clone()),
297        ) {
298            (Some(l), Some(r)) => yaml_eq(&l, &r),
299            _ => false,
300        }
301    })
302}
303
304fn tagged_eq_rhs<B: AsYaml + ?Sized>(lhs: &TaggedNode, rhs: &B) -> bool {
305    let Some(node) = rhs.as_node() else {
306        return false;
307    };
308    TaggedNode::cast(node.clone())
309        .is_some_and(|r| lhs.tag() == r.tag() && lhs.as_string() == r.as_string())
310}
311
312/// A type-erased handle to a CST node returned from navigation methods.
313///
314/// You get a `YamlNode` back from methods like
315/// [`Mapping::get`](crate::yaml::Mapping::get),
316/// [`Sequence::get`](crate::nodes::sequence::Sequence::get),
317/// [`Mapping::keys`](crate::yaml::Mapping::keys), etc.
318///
319/// Match on the variants to get the concrete type, or use the helper
320/// methods [`as_scalar`](Self::as_scalar), [`as_mapping`](Self::as_mapping), etc.
321///
322/// ```
323/// use yaml_edit::{Document, YamlNode};
324/// use std::str::FromStr;
325///
326/// let doc = Document::from_str("name: Alice\nage: 30").unwrap();
327///
328/// if let Some(node) = doc.get("name") {
329///     match node {
330///         YamlNode::Scalar(s) => println!("scalar: {}", s.as_string()),
331///         YamlNode::Mapping(m) => println!("mapping with {} keys", m.len()),
332///         YamlNode::Sequence(s) => println!("sequence with {} items", s.len()),
333///         YamlNode::Alias(a) => println!("alias: *{}", a.name()),
334///         YamlNode::TaggedNode(t) => println!("tagged: {:?}", t.tag()),
335///     }
336/// }
337/// ```
338#[derive(Debug, Clone, PartialEq)]
339pub enum YamlNode {
340    /// A scalar value (string, integer, float, boolean, null).
341    Scalar(Scalar),
342    /// A key-value mapping.
343    Mapping(Mapping),
344    /// An ordered sequence.
345    Sequence(Sequence),
346    /// An alias reference (e.g. `*anchor_name`).
347    Alias(Alias),
348    /// A tagged node (e.g. `!!set`, `!!omap`, `!!pairs`, or a custom tag).
349    TaggedNode(TaggedNode),
350}
351
352impl YamlNode {
353    /// Cast a `SyntaxNode` to a `YamlNode`.
354    ///
355    /// Returns `None` if the node's kind is not one of `SCALAR`, `MAPPING`,
356    /// `SEQUENCE`, `ALIAS`, or `TAGGED_NODE`.
357    pub fn from_syntax(node: SyntaxNode) -> Option<Self> {
358        use crate::lex::SyntaxKind;
359        match node.kind() {
360            SyntaxKind::SCALAR => Scalar::cast(node).map(YamlNode::Scalar),
361            SyntaxKind::MAPPING => Mapping::cast(node).map(YamlNode::Mapping),
362            SyntaxKind::SEQUENCE => Sequence::cast(node).map(YamlNode::Sequence),
363            SyntaxKind::ALIAS => Alias::cast(node).map(YamlNode::Alias),
364            SyntaxKind::TAGGED_NODE => TaggedNode::cast(node).map(YamlNode::TaggedNode),
365            _ => None,
366        }
367    }
368
369    /// Cast a `SyntaxNode` to a `YamlNode`, peeling any `KEY` or `VALUE`
370    /// wrapper first.
371    ///
372    /// Used internally where `mapping.pairs()` yields raw KEY/VALUE wrapper
373    /// nodes that need to be unwrapped before semantic comparison.
374    pub(crate) fn from_syntax_peeled(node: SyntaxNode) -> Option<Self> {
375        use crate::lex::SyntaxKind;
376        let inner = if matches!(node.kind(), SyntaxKind::KEY | SyntaxKind::VALUE) {
377            node.children().next()?
378        } else {
379            node
380        };
381        Self::from_syntax(inner)
382    }
383
384    /// Returns the kind of YAML value this node represents.
385    pub fn kind(&self) -> YamlKind {
386        match self {
387            YamlNode::Scalar(_) => YamlKind::Scalar,
388            YamlNode::Mapping(_) => YamlKind::Mapping,
389            YamlNode::Sequence(_) => YamlKind::Sequence,
390            YamlNode::Alias(_) => YamlKind::Alias,
391            YamlNode::TaggedNode(t) => t
392                .tag()
393                .map(|tag| YamlKind::Tagged(Cow::Owned(tag)))
394                .unwrap_or(YamlKind::Scalar),
395        }
396    }
397
398    /// Returns the underlying `SyntaxNode`.
399    pub(crate) fn syntax(&self) -> &SyntaxNode {
400        match self {
401            YamlNode::Scalar(s) => s.syntax(),
402            YamlNode::Mapping(m) => m.syntax(),
403            YamlNode::Sequence(s) => s.syntax(),
404            YamlNode::Alias(a) => a.syntax(),
405            YamlNode::TaggedNode(t) => t.syntax(),
406        }
407    }
408
409    /// Compare semantically with another value (ignores quoting/formatting).
410    pub fn yaml_eq<O: AsYaml>(&self, other: &O) -> bool {
411        yaml_eq(self, other)
412    }
413
414    /// If this node is a scalar, return a reference to it.
415    pub fn as_scalar(&self) -> Option<&Scalar> {
416        if let YamlNode::Scalar(s) = self {
417            Some(s)
418        } else {
419            None
420        }
421    }
422
423    /// If this node is a mapping, return a reference to it.
424    pub fn as_mapping(&self) -> Option<&Mapping> {
425        if let YamlNode::Mapping(m) = self {
426            Some(m)
427        } else {
428            None
429        }
430    }
431
432    /// If this node is a sequence, return a reference to it.
433    pub fn as_sequence(&self) -> Option<&Sequence> {
434        if let YamlNode::Sequence(s) = self {
435            Some(s)
436        } else {
437            None
438        }
439    }
440
441    /// If this node is a tagged node, return a reference to it.
442    pub fn as_tagged(&self) -> Option<&TaggedNode> {
443        if let YamlNode::TaggedNode(t) = self {
444            Some(t)
445        } else {
446            None
447        }
448    }
449
450    /// If this node is an alias, return a reference to it.
451    pub fn as_alias(&self) -> Option<&Alias> {
452        if let YamlNode::Alias(a) = self {
453            Some(a)
454        } else {
455            None
456        }
457    }
458
459    /// Returns `true` if this node is a scalar.
460    pub fn is_scalar(&self) -> bool {
461        matches!(self, YamlNode::Scalar(_))
462    }
463
464    /// Returns `true` if this node is a mapping.
465    pub fn is_mapping(&self) -> bool {
466        matches!(self, YamlNode::Mapping(_))
467    }
468
469    /// Returns `true` if this node is a sequence.
470    pub fn is_sequence(&self) -> bool {
471        matches!(self, YamlNode::Sequence(_))
472    }
473
474    /// Returns `true` if this node is a tagged node.
475    pub fn is_tagged(&self) -> bool {
476        matches!(self, YamlNode::TaggedNode(_))
477    }
478
479    /// Returns `true` if this node is an alias.
480    pub fn is_alias(&self) -> bool {
481        matches!(self, YamlNode::Alias(_))
482    }
483
484    /// If this node is a scalar, try to parse it as an `i64`.
485    ///
486    /// Returns `None` if this is not a scalar or cannot be parsed as an integer.
487    pub fn to_i64(&self) -> Option<i64> {
488        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_i64()
489    }
490
491    /// If this node is a scalar, try to parse it as an `f64`.
492    ///
493    /// Returns `None` if this is not a scalar or cannot be parsed as a float.
494    pub fn to_f64(&self) -> Option<f64> {
495        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_f64()
496    }
497
498    /// If this node is a scalar, try to parse it as a `bool`.
499    ///
500    /// Returns `None` if this is not a scalar or cannot be parsed as a boolean.
501    pub fn to_bool(&self) -> Option<bool> {
502        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_bool()
503    }
504
505    /// Get a value by key from a mapping node.
506    ///
507    /// Returns `None` if this node is not a mapping or the key is not found.
508    pub fn get(&self, key: impl crate::AsYaml) -> Option<YamlNode> {
509        self.as_mapping()?
510            .get_node(key)
511            .and_then(YamlNode::from_syntax)
512    }
513
514    /// Get a value by index from a sequence node.
515    ///
516    /// Returns `None` if this node is not a sequence or the index is out of bounds.
517    pub fn get_item(&self, index: usize) -> Option<YamlNode> {
518        self.as_sequence()?
519            .items()
520            .nth(index)
521            .and_then(YamlNode::from_syntax)
522    }
523}
524
525impl AsYaml for YamlNode {
526    fn as_node(&self) -> Option<&SyntaxNode> {
527        Some(self.syntax())
528    }
529
530    fn kind(&self) -> YamlKind {
531        YamlNode::kind(self)
532    }
533
534    fn build_content(
535        &self,
536        builder: &mut rowan::GreenNodeBuilder,
537        _indent: usize,
538        _flow_context: bool,
539    ) -> bool {
540        let node = self.syntax();
541        copy_node_content(builder, node);
542        node.last_token()
543            .map(|t| t.kind() == crate::lex::SyntaxKind::NEWLINE)
544            .unwrap_or(false)
545    }
546
547    fn is_inline(&self) -> bool {
548        use crate::yaml::ValueNode;
549        match self {
550            YamlNode::Scalar(s) => ValueNode::is_inline(s),
551            YamlNode::Mapping(m) => ValueNode::is_inline(m),
552            YamlNode::Sequence(s) => ValueNode::is_inline(s),
553            YamlNode::Alias(_) => true,
554            YamlNode::TaggedNode(t) => ValueNode::is_inline(t),
555        }
556    }
557}
558
559/// `yaml_node == "some_string"` — compares the node's semantic value.
560impl PartialEq<str> for YamlNode {
561    fn eq(&self, other: &str) -> bool {
562        yaml_eq(self, &other)
563    }
564}
565
566impl PartialEq<&str> for YamlNode {
567    fn eq(&self, other: &&str) -> bool {
568        yaml_eq(self, other)
569    }
570}
571
572impl PartialEq<String> for YamlNode {
573    fn eq(&self, other: &String) -> bool {
574        yaml_eq(self, &other.as_str())
575    }
576}
577
578impl fmt::Display for YamlNode {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        write!(f, "{}", self.syntax().text())
581    }
582}
583
584/// Blanket impl: any reference to an `AsYaml` type also implements `AsYaml`.
585///
586/// The `?Sized` bound makes this work for trait objects (`&dyn AsYaml`),
587/// which lets [`MappingView`](crate::MappingView) — whose methods take
588/// `&dyn AsYaml` — forward keys to inherent methods that take
589/// `impl AsYaml`.
590impl<T: AsYaml + ?Sized> AsYaml for &T {
591    fn as_node(&self) -> Option<&SyntaxNode> {
592        (*self).as_node()
593    }
594
595    fn kind(&self) -> YamlKind {
596        (*self).kind()
597    }
598
599    fn build_content(
600        &self,
601        builder: &mut rowan::GreenNodeBuilder,
602        indent: usize,
603        flow_context: bool,
604    ) -> bool {
605        (*self).build_content(builder, indent, flow_context)
606    }
607
608    fn is_inline(&self) -> bool {
609        (*self).is_inline()
610    }
611}
612
613/// Recursively copy the children of `node` into `builder`.
614pub(crate) fn copy_node_content(builder: &mut rowan::GreenNodeBuilder, node: &SyntaxNode) {
615    for child in node.children_with_tokens() {
616        match child {
617            rowan::NodeOrToken::Node(n) => {
618                builder.start_node(n.kind().into());
619                copy_node_content(builder, &n);
620                builder.finish_node();
621            }
622            rowan::NodeOrToken::Token(t) => {
623                builder.token(t.kind().into(), t.text());
624            }
625        }
626    }
627}
628
629/// Recursively copy the children of `node` into `builder`, adjusting indentation.
630///
631/// When copying WHITESPACE or INDENT tokens that appear after a NEWLINE,
632/// replaces them with the specified `indent` amount. This is useful when
633/// inserting pre-built sequences/mappings into a different nesting context.
634pub(crate) fn copy_node_content_with_indent(
635    builder: &mut rowan::GreenNodeBuilder,
636    node: &SyntaxNode,
637    indent: usize,
638) {
639    use crate::lex::SyntaxKind;
640    let mut after_newline = false;
641
642    for child in node.children_with_tokens() {
643        match child {
644            rowan::NodeOrToken::Node(n) => {
645                // If this node follows a newline, add indent before starting the node
646                if after_newline && indent > 0 {
647                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
648                }
649                builder.start_node(n.kind().into());
650                copy_node_content_with_indent(builder, &n, indent);
651                builder.finish_node();
652                after_newline = false;
653            }
654            rowan::NodeOrToken::Token(t) => {
655                match t.kind() {
656                    SyntaxKind::NEWLINE => {
657                        builder.token(t.kind().into(), t.text());
658                        after_newline = true;
659                    }
660                    SyntaxKind::WHITESPACE | SyntaxKind::INDENT => {
661                        // If this whitespace follows a newline, add our indent to the existing indent
662                        if after_newline && indent > 0 {
663                            let existing_indent = t.text().len();
664                            let total_indent = indent + existing_indent;
665                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(total_indent));
666                        } else if after_newline {
667                            // indent == 0, skip the whitespace entirely
668                        } else {
669                            // Not after newline, keep as-is (e.g., space after dash)
670                            builder.token(t.kind().into(), t.text());
671                        }
672                        after_newline = false;
673                    }
674                    SyntaxKind::DASH => {
675                        // For sequence items: if DASH follows NEWLINE, add indent first
676                        if after_newline && indent > 0 {
677                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
678                        }
679                        builder.token(t.kind().into(), t.text());
680                        after_newline = false;
681                    }
682                    _ => {
683                        // For any other token after a newline, add indent if needed
684                        if after_newline && indent > 0 {
685                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
686                        }
687                        builder.token(t.kind().into(), t.text());
688                        after_newline = false;
689                    }
690                }
691            }
692        }
693    }
694}
695
696/// Copy the children of `node` into `builder`, shifting every post-newline
697/// whitespace by `delta` columns (may be negative to dedent).
698///
699/// Unlike [`copy_node_content_with_indent`], newline state is tracked
700/// across node boundaries: a NEWLINE inside a child node correctly marks
701/// the next sibling token as being at the start of a line, so
702/// sibling INDENT tokens between entries (which is where the parser
703/// places them for block sequences and mappings) are shifted too.
704/// Returns whether the last emitted token was a NEWLINE.
705pub(crate) fn copy_node_content_reindent(
706    builder: &mut rowan::GreenNodeBuilder,
707    node: &SyntaxNode,
708    delta: isize,
709) -> bool {
710    use crate::lex::SyntaxKind;
711    let mut after_newline = false;
712    let pad = |n: isize| " ".repeat(n.max(0) as usize);
713
714    for child in node.children_with_tokens() {
715        match child {
716            rowan::NodeOrToken::Node(n) => {
717                // Empty nodes (e.g. an implicit `null` value after `? key`
718                // in a set) emit nothing, so they must not affect the
719                // `after_newline` state or trigger a pre-node indent.
720                if n.text_range().is_empty() {
721                    builder.start_node(n.kind().into());
722                    builder.finish_node();
723                    continue;
724                }
725                if after_newline && delta > 0 {
726                    builder.token(SyntaxKind::WHITESPACE.into(), &pad(delta));
727                }
728                builder.start_node(n.kind().into());
729                after_newline = copy_node_content_reindent(builder, &n, delta);
730                builder.finish_node();
731            }
732            rowan::NodeOrToken::Token(t) => match t.kind() {
733                SyntaxKind::NEWLINE => {
734                    builder.token(t.kind().into(), t.text());
735                    after_newline = true;
736                }
737                SyntaxKind::WHITESPACE | SyntaxKind::INDENT => {
738                    if after_newline {
739                        let total = t.text().len() as isize + delta;
740                        if total > 0 {
741                            builder.token(SyntaxKind::WHITESPACE.into(), &pad(total));
742                        }
743                    } else {
744                        builder.token(t.kind().into(), t.text());
745                    }
746                    after_newline = false;
747                }
748                _ => {
749                    if after_newline && delta > 0 {
750                        builder.token(SyntaxKind::WHITESPACE.into(), &pad(delta));
751                    }
752                    builder.token(t.kind().into(), t.text());
753                    after_newline = false;
754                }
755            },
756        }
757    }
758    after_newline
759}
760
761/// Column at which `node` starts on its line.
762///
763/// For a MAPPING or SEQUENCE this is the column of its first entry; for
764/// a MAPPING_ENTRY it's the column of the key. Returns 0 for anything at
765/// the start of the tree's text.
766pub(crate) fn source_base_indent(node: &SyntaxNode) -> usize {
767    // Walk backwards through preceding tokens (climbing to parent siblings
768    // when we run out), accumulating characters until we hit a NEWLINE or
769    // the start of the tree. This works regardless of how the parser
770    // distributed INDENT/WHITESPACE tokens (siblings, inside a
771    // `SEQUENCE_ENTRY`'s dash-space, etc.) and only materializes text
772    // subtrees that we've already confirmed contain a newline.
773    let mut column: usize = 0;
774    let mut cursor = node.prev_sibling_or_token();
775    let mut current = node.clone();
776    loop {
777        while let Some(item) = cursor {
778            match item {
779                rowan::NodeOrToken::Token(t) => {
780                    let text = t.text();
781                    if let Some(nl) = text.rfind('\n') {
782                        column += text[nl + 1..].chars().count();
783                        return column;
784                    }
785                    column += text.chars().count();
786                    cursor = t.prev_sibling_or_token();
787                }
788                rowan::NodeOrToken::Node(n) => {
789                    // For nodes containing no newline we still need the
790                    // character count (bytes lie for multi-byte input);
791                    // materialize.
792                    let text = n.text().to_string();
793                    if let Some(nl) = text.rfind('\n') {
794                        column += text[nl + 1..].chars().count();
795                        return column;
796                    }
797                    column += text.chars().count();
798                    cursor = n.prev_sibling_or_token();
799                }
800            }
801        }
802        // Ran out of siblings; climb up and continue with the parent's siblings.
803        match current.parent() {
804            Some(p) => {
805                cursor = p.prev_sibling_or_token();
806                current = p;
807            }
808            None => return column,
809        }
810    }
811}
812
813// AsYaml trait implementations for primitive types
814//
815// Uses macros to reduce boilerplate for the 12 numeric primitive types.
816
817/// Macro to implement AsYaml for integer types
818macro_rules! impl_as_yaml_int {
819    ($($ty:ty),+ $(,)?) => {
820        $(impl AsYaml for $ty {
821            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
822                None
823            }
824
825            fn kind(&self) -> YamlKind {
826                YamlKind::Scalar
827            }
828
829            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
830                use crate::lex::SyntaxKind;
831                builder.start_node(SyntaxKind::SCALAR.into());
832                builder.token(SyntaxKind::INT.into(), &self.to_string());
833                builder.finish_node();
834                false
835            }
836
837            fn is_inline(&self) -> bool {
838                true
839            }
840        })+
841    };
842}
843
844/// Macro to implement AsYaml for floating-point types
845macro_rules! impl_as_yaml_float {
846    ($($ty:ty),+ $(,)?) => {
847        $(impl AsYaml for $ty {
848            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
849                None
850            }
851
852            fn kind(&self) -> YamlKind {
853                YamlKind::Scalar
854            }
855
856            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
857                use crate::lex::SyntaxKind;
858                builder.start_node(SyntaxKind::SCALAR.into());
859                let s = self.to_string();
860                // Ensure floats always have a decimal point for YAML clarity
861                let float_str = if s.contains('.') || s.contains('e') || s.contains('E') {
862                    s
863                } else {
864                    format!("{}.0", s)
865                };
866                builder.token(SyntaxKind::FLOAT.into(), &float_str);
867                builder.finish_node();
868                false
869            }
870
871            fn is_inline(&self) -> bool {
872                true
873            }
874        })+
875    };
876}
877
878// Implement AsYaml for integer types
879impl_as_yaml_int!(i64, i32, i16, i8, isize, u64, u32, u16, u8, usize);
880
881// Implement AsYaml for floating-point types
882impl_as_yaml_float!(f64, f32);
883
884impl AsYaml for bool {
885    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
886        None
887    }
888
889    fn kind(&self) -> YamlKind {
890        YamlKind::Scalar
891    }
892
893    fn build_content(
894        &self,
895        builder: &mut rowan::GreenNodeBuilder,
896        _indent: usize,
897        _flow_context: bool,
898    ) -> bool {
899        use crate::lex::SyntaxKind;
900        builder.start_node(SyntaxKind::SCALAR.into());
901        builder.token(
902            SyntaxKind::BOOL.into(),
903            if *self { "true" } else { "false" },
904        );
905        builder.finish_node();
906        false
907    }
908
909    fn is_inline(&self) -> bool {
910        true
911    }
912}
913
914impl AsYaml for String {
915    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
916        None
917    }
918
919    fn kind(&self) -> YamlKind {
920        YamlKind::Scalar
921    }
922
923    fn build_content(
924        &self,
925        builder: &mut rowan::GreenNodeBuilder,
926        _indent: usize,
927        flow_context: bool,
928    ) -> bool {
929        self.as_str().build_content(builder, _indent, flow_context)
930    }
931
932    fn is_inline(&self) -> bool {
933        true
934    }
935}
936
937impl AsYaml for &str {
938    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
939        None
940    }
941
942    fn kind(&self) -> YamlKind {
943        YamlKind::Scalar
944    }
945
946    fn build_content(
947        &self,
948        builder: &mut rowan::GreenNodeBuilder,
949        _indent: usize,
950        flow_context: bool,
951    ) -> bool {
952        use crate::lex::SyntaxKind;
953        use crate::scalar::ScalarValue;
954
955        // In flow context (JSON), always use double-quoted strings for compatibility
956        // In block context (YAML), use standard quoting rules
957        let scalar = if flow_context {
958            ScalarValue::double_quoted(*self)
959        } else {
960            ScalarValue::string(*self)
961        };
962
963        let yaml_text = scalar.to_yaml_string();
964        // Both quoted and unquoted strings use STRING token kind;
965        // the token text includes any quotes needed for disambiguation.
966        builder.start_node(SyntaxKind::SCALAR.into());
967        builder.token(SyntaxKind::STRING.into(), &yaml_text);
968        builder.finish_node();
969        false
970    }
971
972    fn is_inline(&self) -> bool {
973        true
974    }
975}
976
977/// `Some(v)` behaves exactly like `v`; `None` emits a bare `null` scalar.
978impl<T: AsYaml> AsYaml for Option<T> {
979    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
980        self.as_ref().and_then(AsYaml::as_node)
981    }
982
983    fn kind(&self) -> YamlKind {
984        match self {
985            Some(v) => v.kind(),
986            None => YamlKind::Scalar,
987        }
988    }
989
990    fn build_content(
991        &self,
992        builder: &mut rowan::GreenNodeBuilder,
993        indent: usize,
994        flow_context: bool,
995    ) -> bool {
996        match self {
997            Some(v) => v.build_content(builder, indent, flow_context),
998            None => crate::scalar::ScalarValue::null().build_content(builder, indent, flow_context),
999        }
1000    }
1001
1002    fn is_inline(&self) -> bool {
1003        match self {
1004            Some(v) => v.is_inline(),
1005            None => true,
1006        }
1007    }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::*;
1013    use crate::yaml::{Document, YamlFile};
1014    use std::str::FromStr;
1015
1016    #[test]
1017    fn test_yaml_eq_different_quoting_styles() {
1018        let yaml = r#"
1019plain: value
1020single: 'value'
1021double: "value"
1022"#;
1023
1024        let doc = Document::from_str(yaml).unwrap();
1025        let mapping = doc.as_mapping().unwrap();
1026
1027        let plain = mapping.get("plain").unwrap();
1028        let single = mapping.get("single").unwrap();
1029        let double = mapping.get("double").unwrap();
1030
1031        // All three should be equal (semantic equality ignores quoting)
1032        assert!(yaml_eq(&plain, &single));
1033        assert!(yaml_eq(&single, &double));
1034        assert!(yaml_eq(&plain, &double));
1035
1036        // Compare with raw strings
1037        assert!(yaml_eq(&plain, &"value"));
1038        assert!(yaml_eq(&single, &"value"));
1039        assert!(yaml_eq(&double, &"value"));
1040    }
1041
1042    #[test]
1043    fn test_yaml_eq_escape_sequences() {
1044        let yaml = r#"
1045newline1: "line1\nline2"
1046newline2: "line1
1047line2"
1048tab1: "a\tb"
1049tab2: "a	b"
1050backslash1: "path\\file"
1051backslash2: 'path\file'
1052quote1: "say \"hi\""
1053quote2: 'say "hi"'
1054"#;
1055
1056        let doc = Document::from_str(yaml).unwrap();
1057        let mapping = doc.as_mapping().unwrap();
1058
1059        let newline1 = mapping.get("newline1").unwrap();
1060        let newline2 = mapping.get("newline2").unwrap();
1061        let tab1 = mapping.get("tab1").unwrap();
1062        let tab2 = mapping.get("tab2").unwrap();
1063        let backslash1 = mapping.get("backslash1").unwrap();
1064        let backslash2 = mapping.get("backslash2").unwrap();
1065        let quote1 = mapping.get("quote1").unwrap();
1066        let quote2 = mapping.get("quote2").unwrap();
1067
1068        // Escaped newlines should equal actual newlines
1069        assert!(yaml_eq(&newline1, &newline2));
1070
1071        // Escaped tabs should equal actual tabs
1072        assert!(yaml_eq(&tab1, &tab2));
1073
1074        // Backslash handling: single-quoted strings don't interpret backslashes as escapes
1075        // So 'path\file' is literally "path\file" (one backslash)
1076        // And "path\\file" escapes to "path\file" (one backslash)
1077        // Therefore they ARE equal!
1078        assert!(yaml_eq(&backslash1, &backslash2));
1079        assert!(yaml_eq(&backslash1, &"path\\file"));
1080        assert!(yaml_eq(&backslash2, &"path\\file"));
1081
1082        // Quote handling
1083        assert!(yaml_eq(&quote1, &quote2));
1084        assert!(yaml_eq(&quote1, &r#"say "hi""#));
1085    }
1086
1087    #[test]
1088    fn test_yaml_eq_single_quote_escaping() {
1089        let yaml = r#"
1090single1: 'can''t'
1091single2: "can't"
1092"#;
1093
1094        let doc = Document::from_str(yaml).unwrap();
1095        let mapping = doc.as_mapping().unwrap();
1096
1097        let single1 = mapping.get("single1").unwrap();
1098        let single2 = mapping.get("single2").unwrap();
1099
1100        // Single-quoted '' should equal double-quoted '
1101        assert!(yaml_eq(&single1, &single2));
1102        assert!(yaml_eq(&single1, &"can't"));
1103    }
1104
1105    #[test]
1106    fn test_yaml_eq_unicode_escapes() {
1107        let yaml = r#"
1108unicode1: "hello\x20world"
1109unicode2: "hello world"
1110unicode3: "smiley\u0020face"
1111unicode4: "smiley face"
1112"#;
1113
1114        let doc = Document::from_str(yaml).unwrap();
1115        let mapping = doc.as_mapping().unwrap();
1116
1117        let unicode1 = mapping.get("unicode1").unwrap();
1118        let unicode2 = mapping.get("unicode2").unwrap();
1119        let unicode3 = mapping.get("unicode3").unwrap();
1120        let unicode4 = mapping.get("unicode4").unwrap();
1121
1122        // \x20 is space
1123        assert!(yaml_eq(&unicode1, &unicode2));
1124        assert!(yaml_eq(&unicode1, &"hello world"));
1125
1126        // \u0020 is also space
1127        assert!(yaml_eq(&unicode3, &unicode4));
1128        assert!(yaml_eq(&unicode3, &"smiley face"));
1129    }
1130
1131    #[test]
1132    fn test_yaml_eq_with_comments() {
1133        let yaml1 = r#"
1134# This is a comment
1135key: value # inline comment
1136"#;
1137
1138        let yaml2 = r#"
1139key: value
1140"#;
1141
1142        // yaml1 has a stream-level leading comment, so parse via YamlFile
1143        // rather than Document::from_str (which rejects stream-scoped content).
1144        let doc1 = YamlFile::from_str(yaml1).unwrap().document().unwrap();
1145        let doc2 = Document::from_str(yaml2).unwrap();
1146
1147        let mapping1 = doc1.as_mapping().unwrap();
1148        let mapping2 = doc2.as_mapping().unwrap();
1149
1150        // Mappings with and without comments should be equal (semantic equality)
1151        assert!(yaml_eq(&mapping1, &mapping2));
1152
1153        let value1 = mapping1.get("key").unwrap();
1154        let value2 = mapping2.get("key").unwrap();
1155
1156        assert!(yaml_eq(&value1, &value2));
1157        assert!(yaml_eq(&value1, &"value"));
1158    }
1159
1160    #[test]
1161    fn test_yaml_eq_mappings() {
1162        let yaml1 = r#"
1163a: 1
1164b: 2
1165c: 3
1166"#;
1167
1168        let yaml2 = r#"
1169a: 1
1170b: 2
1171c: 3
1172"#;
1173
1174        let yaml3 = r#"
1175a: 1
1176c: 3
1177b: 2
1178"#;
1179
1180        let doc1 = Document::from_str(yaml1).unwrap();
1181        let doc2 = Document::from_str(yaml2).unwrap();
1182        let doc3 = Document::from_str(yaml3).unwrap();
1183
1184        let mapping1 = doc1.as_mapping().unwrap();
1185        let mapping2 = doc2.as_mapping().unwrap();
1186        let mapping3 = doc3.as_mapping().unwrap();
1187
1188        // Same mappings should be equal
1189        assert!(yaml_eq(&mapping1, &mapping2));
1190
1191        // Different order should NOT be equal (order matters for yaml_eq)
1192        assert!(!yaml_eq(&mapping1, &mapping3));
1193    }
1194
1195    #[test]
1196    fn test_yaml_eq_sequences() {
1197        let yaml1 = r#"
1198- one
1199- two
1200- three
1201"#;
1202
1203        let yaml2 = r#"
1204- one
1205- two
1206- three
1207"#;
1208
1209        let yaml3 = r#"
1210- one
1211- three
1212- two
1213"#;
1214
1215        let doc1 = Document::from_str(yaml1).unwrap();
1216        let doc2 = Document::from_str(yaml2).unwrap();
1217        let doc3 = Document::from_str(yaml3).unwrap();
1218
1219        let seq1 = doc1.as_sequence().unwrap();
1220        let seq2 = doc2.as_sequence().unwrap();
1221        let seq3 = doc3.as_sequence().unwrap();
1222
1223        // Same sequences should be equal
1224        assert!(yaml_eq(&seq1, &seq2));
1225
1226        // Different order should NOT be equal
1227        assert!(!yaml_eq(&seq1, &seq3));
1228    }
1229
1230    #[test]
1231    fn test_yaml_eq_nested_structures() {
1232        let yaml1 = r#"
1233outer:
1234  inner:
1235    key: "value"
1236"#;
1237
1238        let yaml2 = r#"
1239outer:
1240  inner:
1241    key: 'value'
1242"#;
1243
1244        let doc1 = Document::from_str(yaml1).unwrap();
1245        let doc2 = Document::from_str(yaml2).unwrap();
1246
1247        let mapping1 = doc1.as_mapping().unwrap();
1248        let mapping2 = doc2.as_mapping().unwrap();
1249
1250        // Nested structures with different quoting should be equal
1251        assert!(yaml_eq(&mapping1, &mapping2));
1252    }
1253
1254    #[test]
1255    fn test_yaml_eq_special_characters() {
1256        let yaml = r#"
1257bell: "\a"
1258escape: "\e"
1259"null": "\0"
1260backspace: "\b"
1261formfeed: "\f"
1262carriagereturn: "\r"
1263verticaltab: "\v"
1264"#;
1265
1266        let doc = Document::from_str(yaml).unwrap();
1267        let mapping = doc.as_mapping().unwrap();
1268
1269        // Note: These all compare STRING values with escaped chars against raw Rust strings
1270        // They should be equal because both sides are STRING type
1271        assert!(yaml_eq(&mapping.get("bell").unwrap(), &"\x07"));
1272        assert!(yaml_eq(&mapping.get("escape").unwrap(), &"\x1B"));
1273        assert!(yaml_eq(&mapping.get("null").unwrap(), &"\0"));
1274        assert!(yaml_eq(&mapping.get("backspace").unwrap(), &"\x08"));
1275        assert!(yaml_eq(&mapping.get("formfeed").unwrap(), &"\x0C"));
1276        assert!(yaml_eq(&mapping.get("carriagereturn").unwrap(), &"\r"));
1277        assert!(yaml_eq(&mapping.get("verticaltab").unwrap(), &"\x0B"));
1278    }
1279
1280    #[test]
1281    fn test_yaml_eq_empty_strings() {
1282        let yaml = r#"
1283empty1: ""
1284empty2: ''
1285"#;
1286
1287        let doc = Document::from_str(yaml).unwrap();
1288        let mapping = doc.as_mapping().unwrap();
1289
1290        let empty1 = mapping.get("empty1").unwrap();
1291        let empty2 = mapping.get("empty2").unwrap();
1292
1293        assert!(yaml_eq(&empty1, &empty2));
1294        assert!(yaml_eq(&empty1, &""));
1295    }
1296
1297    #[test]
1298    fn test_yaml_eq_whitespace_handling() {
1299        let yaml = r#"
1300spaces1: "  leading"
1301spaces2: "trailing  "
1302spaces3: "  both  "
1303plain: value
1304"#;
1305
1306        let doc = Document::from_str(yaml).unwrap();
1307        let mapping = doc.as_mapping().unwrap();
1308
1309        let spaces1 = mapping.get("spaces1").unwrap();
1310        let spaces2 = mapping.get("spaces2").unwrap();
1311        let spaces3 = mapping.get("spaces3").unwrap();
1312        let plain = mapping.get("plain").unwrap();
1313
1314        // Whitespace should be preserved exactly
1315        assert!(yaml_eq(&spaces1, &"  leading"));
1316        assert!(yaml_eq(&spaces2, &"trailing  "));
1317        assert!(yaml_eq(&spaces3, &"  both  "));
1318        assert!(yaml_eq(&plain, &"value"));
1319
1320        // These should NOT be equal
1321        assert!(!yaml_eq(&spaces1, &"leading"));
1322        assert!(!yaml_eq(&spaces2, &"trailing"));
1323    }
1324
1325    #[test]
1326    fn test_yaml_eq_different_types() {
1327        let yaml = r#"
1328string: "123"
1329number: 123
1330sequence:
1331  - item
1332mapping:
1333  key: value
1334"#;
1335
1336        let doc = Document::from_str(yaml).unwrap();
1337        let mapping = doc.as_mapping().unwrap();
1338
1339        let string_val = mapping.get("string").unwrap();
1340        let number_val = mapping.get("number").unwrap();
1341        let sequence_val = mapping.get("sequence").unwrap();
1342        let mapping_val = mapping.get("mapping").unwrap();
1343
1344        // yaml_eq does YAML-level semantic comparison:
1345        // "123" (string) and 123 (integer) are DIFFERENT types
1346        assert!(!yaml_eq(&string_val, &number_val));
1347        assert!(!yaml_eq(&string_val, &sequence_val));
1348        assert!(!yaml_eq(&string_val, &mapping_val));
1349        assert!(!yaml_eq(&number_val, &sequence_val));
1350        assert!(!yaml_eq(&sequence_val, &mapping_val));
1351    }
1352
1353    #[test]
1354    fn test_yaml_eq_line_folding_escapes() {
1355        let yaml = r#"
1356folded1: "line1\
1357 line2"
1358folded2: "line1 line2"
1359"#;
1360
1361        let doc = Document::from_str(yaml).unwrap();
1362        let mapping = doc.as_mapping().unwrap();
1363
1364        let folded1 = mapping.get("folded1").unwrap();
1365        let folded2 = mapping.get("folded2").unwrap();
1366
1367        // Per YAML spec, backslash at end of line folds to a space
1368        assert!(yaml_eq(&folded1, &folded2));
1369        assert!(yaml_eq(&folded1, &"line1 line2"));
1370    }
1371
1372    #[test]
1373    fn test_yaml_eq_complex_unicode() {
1374        let yaml = r#"
1375emoji1: "😀"
1376emoji2: "\U0001F600"
1377"#;
1378
1379        let doc = Document::from_str(yaml).unwrap();
1380        let mapping = doc.as_mapping().unwrap();
1381
1382        let emoji1 = mapping.get("emoji1").unwrap();
1383        let emoji2 = mapping.get("emoji2").unwrap();
1384
1385        // Unicode escape for emoji should equal literal emoji
1386        assert!(yaml_eq(&emoji1, &emoji2));
1387        assert!(yaml_eq(&emoji1, &"😀"));
1388    }
1389
1390    #[test]
1391    fn test_yaml_eq_block_scalars() {
1392        let yaml1 = "literal1: |\n  line1\n  line2\n";
1393        let yaml2 = "literal2: |\n  line1\n  line2\n";
1394
1395        let doc1 = Document::from_str(yaml1).unwrap();
1396        let doc2 = Document::from_str(yaml2).unwrap();
1397
1398        let mapping1 = doc1.as_mapping().unwrap();
1399        let mapping2 = doc2.as_mapping().unwrap();
1400
1401        let literal1 = mapping1.get("literal1").unwrap();
1402        let literal2 = mapping2.get("literal2").unwrap();
1403
1404        // Same literal block scalars should be equal
1405        assert!(yaml_eq(&literal1, &literal2));
1406    }
1407
1408    #[test]
1409    fn test_yaml_eq_null_and_special_values() {
1410        let yaml = r#"
1411null1: null
1412null2: null
1413null3:
1414empty: ""
1415"#;
1416
1417        let doc = Document::from_str(yaml).unwrap();
1418        let mapping = doc.as_mapping().unwrap();
1419
1420        let null1 = mapping.get("null1").unwrap();
1421        let null2 = mapping.get("null2").unwrap();
1422        let null3 = mapping.get("null3").unwrap();
1423        let empty = mapping.get("empty").unwrap();
1424
1425        // Same null representations should be equal (both are NULL type)
1426        assert!(yaml_eq(&null1, &null2));
1427
1428        // null3 (implicit null via empty value) should also be equal to explicit null
1429        // Per YAML spec, "key:" is semantically identical to "key: null"
1430        assert!(yaml_eq(&null3, &null1));
1431        assert!(yaml_eq(&null3, &null2));
1432
1433        // NULL type != STRING type (even though empty might look like null)
1434        assert!(!yaml_eq(&null1, &empty));
1435
1436        // NULL scalar != STRING "null" (different types)
1437        assert!(!yaml_eq(&null1, &"null"));
1438    }
1439
1440    #[test]
1441    fn test_yaml_eq_boolean_representations() {
1442        let yaml = r#"
1443true1: true
1444true2: True
1445true3: TRUE
1446false1: false
1447false2: False
1448"#;
1449
1450        let doc = Document::from_str(yaml).unwrap();
1451        let mapping = doc.as_mapping().unwrap();
1452
1453        let true1 = mapping.get("true1").unwrap();
1454        let true2 = mapping.get("true2").unwrap();
1455        let true3 = mapping.get("true3").unwrap();
1456        let false1 = mapping.get("false1").unwrap();
1457        let false2 = mapping.get("false2").unwrap();
1458
1459        // Different case booleans ARE semantically equal (normalized to lowercase)
1460        assert!(yaml_eq(&true1, &true2));
1461        assert!(yaml_eq(&true1, &true3));
1462        assert!(yaml_eq(&false1, &false2));
1463
1464        // BOOL scalars do NOT equal STRING scalars
1465        assert!(!yaml_eq(&true1, &"true"));
1466        assert!(!yaml_eq(&false1, &"false"));
1467    }
1468
1469    #[test]
1470    fn test_yaml_eq_numeric_formats() {
1471        let yaml = r#"
1472decimal: 123
1473octal: 0o173
1474hex: 0x7B
1475"#;
1476
1477        let doc = Document::from_str(yaml).unwrap();
1478        let mapping = doc.as_mapping().unwrap();
1479
1480        let decimal = mapping.get("decimal").unwrap();
1481        let octal = mapping.get("octal").unwrap();
1482        let hex = mapping.get("hex").unwrap();
1483
1484        // Different numeric formats ARE semantically equal (all equal 123)
1485        assert!(yaml_eq(&decimal, &octal));
1486        assert!(yaml_eq(&decimal, &hex));
1487        assert!(yaml_eq(&octal, &hex));
1488
1489        // INT scalars do NOT equal STRING scalars (even if text is same)
1490        assert!(!yaml_eq(&decimal, &"123"));
1491
1492        // But they DO equal raw integer values
1493        assert!(yaml_eq(&decimal, &123));
1494        assert!(yaml_eq(&octal, &123));
1495        assert!(yaml_eq(&hex, &123));
1496    }
1497
1498    #[test]
1499    fn test_yaml_eq_with_anchors() {
1500        let yaml = r#"
1501original: &anchor value
1502duplicate: value
1503"#;
1504
1505        let doc = Document::from_str(yaml).unwrap();
1506        let mapping = doc.as_mapping().unwrap();
1507
1508        let original = mapping.get("original").unwrap();
1509        let duplicate = mapping.get("duplicate").unwrap();
1510
1511        // Values with anchors should equal plain values (anchor syntax is ignored)
1512        assert!(yaml_eq(&original, &duplicate));
1513        assert!(yaml_eq(&original, &"value"));
1514    }
1515
1516    #[test]
1517    fn test_yaml_eq_flow_vs_block_collections() {
1518        let yaml = r#"
1519flow_seq: [1, 2, 3]
1520block_seq:
1521  - 1
1522  - 2
1523  - 3
1524flow_map: {a: 1, b: 2}
1525block_map:
1526  a: 1
1527  b: 2
1528"#;
1529
1530        let doc = Document::from_str(yaml).unwrap();
1531        let mapping = doc.as_mapping().unwrap();
1532
1533        let flow_seq = mapping.get("flow_seq").unwrap();
1534        let block_seq = mapping.get("block_seq").unwrap();
1535        let flow_map = mapping.get("flow_map").unwrap();
1536        let block_map = mapping.get("block_map").unwrap();
1537
1538        // Flow and block styles should be semantically equal
1539        assert!(yaml_eq(&flow_seq, &block_seq));
1540        assert!(yaml_eq(&flow_map, &block_map));
1541    }
1542
1543    #[test]
1544    fn test_option_none_emits_null() {
1545        let doc = Document::from_str("key: value\n").unwrap();
1546        doc.as_mapping().unwrap().set("key", None::<&str>);
1547        assert_eq!(doc.to_string(), "key: null\n");
1548    }
1549
1550    #[test]
1551    fn test_option_some_delegates() {
1552        let doc = Document::from_str("key: value\n").unwrap();
1553        doc.as_mapping().unwrap().set("key", Some("hello"));
1554        assert_eq!(doc.to_string(), "key: hello\n");
1555
1556        let doc = Document::from_str("key: value\n").unwrap();
1557        doc.as_mapping().unwrap().set("key", Some(42i64));
1558        assert_eq!(doc.to_string(), "key: 42\n");
1559    }
1560}