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