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`, [`YamlValue`](crate::YamlValue), …):
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::tag;
23use crate::yaml::{Alias, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
24use rowan::ast::AstNode;
25use std::borrow::Cow;
26use std::fmt;
27
28/// The kind of YAML value.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum YamlKind {
31    /// A mapping (object/dictionary)
32    Mapping,
33    /// A sequence (array/list)
34    Sequence,
35    /// A scalar value (string, number, boolean, null)
36    Scalar,
37    /// An alias reference (e.g. `*anchor_name`)
38    Alias,
39    /// A document (top-level container)
40    Document,
41    /// A tagged value (e.g. `!!set`, `!!omap`, `!!pairs`).
42    ///
43    /// Known built-in tags use a `'static` string; custom tags carry an owned string.
44    Tagged(Cow<'static, str>),
45}
46
47/// Trait for types that can be represented as YAML content.
48///
49/// Bridges the gap between CST nodes (which preserve formatting) and raw Rust
50/// values (which are convenient for constructing new content).
51///
52/// # Using `AsYaml` as a bound
53///
54/// Mutation methods such as [`Mapping::set`](crate::yaml::Mapping::set) accept
55/// `impl AsYaml`, so you can pass any of:
56///
57/// - A string literal (`"hello"`)
58/// - A number (`42_i64`, `3.14_f64`, …)
59/// - A boolean (`true`)
60/// - An existing CST node ([`Mapping`], [`Sequence`], [`Scalar`], [`YamlNode`], …)
61/// - A [`YamlValue`](crate::YamlValue), `Vec<YamlValue>` or `BTreeMap<String, YamlValue>`
62///
63/// A CST node is copied with the comments inside it, but not those around
64/// it, such as a comment line before it or a comment after a scalar.
65pub trait AsYaml {
66    /// Returns a reference to the underlying `SyntaxNode` if one exists.
67    ///
68    /// CST wrappers (`Mapping`, `Sequence`, `Scalar`, `TaggedNode`, `YamlNode`)
69    /// return `Some`.  Raw Rust types (`i64`, `String`, etc.) return `None`.
70    fn as_node(&self) -> Option<&SyntaxNode>;
71
72    /// Returns the kind of YAML value this represents.
73    fn kind(&self) -> YamlKind;
74
75    /// Serialize this value into a `GreenNodeBuilder`.
76    ///
77    /// CST-backed types copy their node content; raw types synthesize an
78    /// appropriate CST structure.
79    ///
80    /// Returns `true` if the emitted content ends with a `NEWLINE` token
81    /// (used to avoid double newlines when nesting collections).
82    fn build_content(
83        &self,
84        builder: &mut rowan::GreenNodeBuilder,
85        indent: usize,
86        flow_context: bool,
87    ) -> bool;
88
89    /// Returns whether this value should be rendered on the same line as its key.
90    ///
91    /// `true` for scalars and empty collections; `false` for non-empty block
92    /// collections.
93    fn is_inline(&self) -> bool;
94
95    /// Like `build_content` inside a flow collection, quoting strings that
96    /// have no explicit style as `quoting` says.
97    #[doc(hidden)]
98    fn build_flow_content(
99        &self,
100        builder: &mut rowan::GreenNodeBuilder,
101        indent: usize,
102        _quoting: FlowQuoting,
103    ) -> bool {
104        self.build_content(builder, indent, true)
105    }
106
107    /// The node this value is copied from: [`as_node`](Self::as_node), or
108    /// for a document its root.
109    #[doc(hidden)]
110    fn value_node(&self) -> Option<SyntaxNode> {
111        self.as_node().cloned()
112    }
113}
114
115/// How strings without an explicit style are quoted inside a flow
116/// collection.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum FlowQuoting {
119    /// Only where a plain scalar would not read back as the same string.
120    Minimal,
121    /// Always double-quoted, as in JSON.
122    Double,
123}
124
125/// Emit `value` in a flow collection quoting strings as `flow` says, or in
126/// block context if `flow` is `None`.
127pub(crate) fn build_in<T: AsYaml + ?Sized>(
128    value: &T,
129    builder: &mut rowan::GreenNodeBuilder,
130    indent: usize,
131    flow: Option<FlowQuoting>,
132) -> bool {
133    match flow {
134        Some(quoting) => value.build_flow_content(builder, indent, quoting),
135        None => value.build_content(builder, indent, false),
136    }
137}
138
139/// Compare two [`AsYaml`] values for semantic equality.
140///
141/// Two nodes are equal when they have the same resolved tag and the same
142/// canonical value, as in the YAML 1.2 spec. Untagged plain scalars resolve
143/// through the core schema and quoted or block scalars are `!!str`, so
144/// `name`, `"name"`, `'name'` and `!!str name` all compare equal, as do
145/// `1`, `0x1` and `!!int 1`, while `1` and `'1'` do not. Mappings compare
146/// regardless of key order.
147///
148/// Both sides can be any combination of CST nodes and raw Rust values.
149/// Aliases are not resolved and never compare equal.
150pub fn yaml_eq<A, B>(a: &A, b: &B) -> bool
151where
152    A: AsYaml + ?Sized,
153    B: AsYaml + ?Sized,
154{
155    match (to_syntax(a), to_syntax(b)) {
156        (Some(a), Some(b)) => node_eq(&a, &b),
157        _ => false,
158    }
159}
160
161/// Feed a hash of `value` into `state`, consistent with [`yaml_eq`]: values
162/// that compare equal hash equally, so they can be used as keys in hash
163/// maps with an equality defined by [`yaml_eq`].
164///
165/// [`yaml_eq`] is not reflexive for values containing aliases or content
166/// that cannot be resolved, so such values need care as hash map keys.
167pub fn yaml_hash<T, H>(value: &T, state: &mut H)
168where
169    T: AsYaml + ?Sized,
170    H: std::hash::Hasher,
171{
172    use std::hash::Hash;
173    if let Some(Ok(Some(identity))) = to_syntax(value).map(|node| node_identity(&node, None)) {
174        identity.hash(state);
175    }
176}
177
178/// The node backing `v`, or for a raw value, the CST it builds.
179fn to_syntax<T: AsYaml + ?Sized>(v: &T) -> Option<SyntaxNode> {
180    use crate::lex::SyntaxKind;
181    if let Some(node) = v.value_node() {
182        return Some(node);
183    }
184    let mut builder = rowan::GreenNodeBuilder::new();
185    builder.start_node(SyntaxKind::ROOT.into());
186    // Build in flow context: a multi-line string renders there as one
187    // double-quoted scalar rather than a literal block, whose trailing
188    // newline would otherwise make `"a\nb"` compare unequal to itself.
189    v.build_content(&mut builder, 0, true);
190    builder.finish_node();
191    SyntaxNode::new_root(builder.finish()).first_child()
192}
193
194/// The identity of a node under YAML node equality: its resolved tag and
195/// canonical content, with aliases followed. Two nodes are equal, and so
196/// the same mapping key, exactly when their identities are.
197#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub(crate) struct NodeIdentity(Identity<NodeIdentity>);
199
200/// A node's identity, with that of each node in it given as a `T`.
201///
202/// Mapping entries are kept sorted, since mappings are unordered.
203#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
204pub(crate) enum Identity<T> {
205    Scalar {
206        tag: String,
207        value: String,
208    },
209    Sequence {
210        tag: String,
211        items: Vec<T>,
212    },
213    Mapping {
214        tag: String,
215        entries: Vec<(T, T)>,
216    },
217    /// An alias that is not followed, as for [`lookup_identity`].
218    Alias {
219        tag: Option<String>,
220        name: String,
221    },
222}
223
224impl NodeIdentity {
225    /// The string form of a node with this identity as a key of a
226    /// [`YamlValue`](crate::YamlValue) mapping: the canonical value of a
227    /// scalar, without its tag, and a flow rendering of a collection.
228    ///
229    /// Equal nodes have the same string form, however they are written.
230    pub(crate) fn key_string(&self) -> String {
231        match &self.0 {
232            Identity::Scalar { value, .. } => value.clone(),
233            _ => self.flow_text(),
234        }
235    }
236
237    /// Flow YAML text for a node with this identity, which reads back as an
238    /// equal node.
239    fn flow_text(&self) -> String {
240        use crate::scalar::ScalarValue;
241        let tagged = |tag: &str, default: &str, content: String| {
242            if tag == default {
243                content
244            } else {
245                format!("{tag} {content}")
246            }
247        };
248        match &self.0 {
249            Identity::Scalar { tag, value } if tag == tag::STR => {
250                ScalarValue::string(value.as_str()).to_flow_string()
251            }
252            Identity::Scalar { tag, value } => {
253                let plain = resolve_scalar(ScalarValue::parse(value.as_str()));
254                if !value.is_empty() && plain.tag == *tag && plain.value == *value {
255                    value.clone()
256                } else {
257                    format!(
258                        "{tag} {}",
259                        ScalarValue::string(value.as_str()).to_flow_string()
260                    )
261                }
262            }
263            Identity::Sequence { tag, items } => {
264                let items: Vec<_> = items.iter().map(Self::flow_text).collect();
265                tagged(tag, tag::SEQ, format!("[{}]", items.join(", ")))
266            }
267            Identity::Mapping { tag, entries } => {
268                let entries: Vec<_> = entries
269                    .iter()
270                    .map(|(k, v)| format!("{}: {}", k.flow_text(), v.flow_text()))
271                    .collect();
272                tagged(tag, tag::MAP, format!("{{{}}}", entries.join(", ")))
273            }
274            Identity::Alias { tag, name } => match tag {
275                Some(tag) => format!("{tag} *{name}"),
276                None => format!("*{name}"),
277            },
278        }
279    }
280}
281
282/// The resolved tag of a collection written with `tag`, if any.
283fn collection_tag(tag: Option<String>, default: &str) -> String {
284    match tag.as_deref() {
285        // The non-specific tag `!` resolves a collection by its kind.
286        None | Some("!") => default.to_string(),
287        Some(t) => tag::normalize(t).into_owned(),
288    }
289}
290
291/// A scalar's resolved tag and canonical value.
292struct ResolvedScalar {
293    tag: String,
294    value: String,
295}
296
297/// Resolve a scalar to its tag and canonical value.
298fn resolve_scalar(scalar: crate::scalar::ScalarValue) -> ResolvedScalar {
299    use crate::scalar::{ScalarType, ScalarValue};
300    let text = scalar.value();
301    let canonical = |parsed: Option<String>| parsed.unwrap_or_else(|| text.to_string());
302    let (tag, value) = match scalar.scalar_type() {
303        ScalarType::Integer => (
304            tag::INT.to_string(),
305            canonical(ScalarValue::canonical_integer(text)),
306        ),
307        ScalarType::Float => (
308            tag::FLOAT.to_string(),
309            canonical(
310                ScalarValue::parse_float(text).map(|f| ScalarValue::format_float(f.to_string())),
311            ),
312        ),
313        ScalarType::Boolean => (
314            tag::BOOL.to_string(),
315            canonical(ScalarValue::parse_bool(text).map(|b| b.to_string())),
316        ),
317        ScalarType::Null => (tag::NULL.to_string(), "null".to_string()),
318        _ => {
319            // A scalar with the non-specific tag `!` is a string.
320            let tag = match scalar.tag() {
321                Some(t) if t != "!" => tag::normalize(t).into_owned(),
322                _ => tag::STR.to_string(),
323            };
324            (tag, text.to_string())
325        }
326    };
327    ResolvedScalar { tag, value }
328}
329
330/// The identity of `node`, a value node or the KEY or VALUE wrapper holding
331/// one.
332///
333/// Aliases are followed through `registry`; without one an alias is
334/// undefined, as no anchors are known.
335/// Returns `Ok(None)` if `node` is not a value node.
336pub(crate) fn node_identity(
337    node: &SyntaxNode,
338    registry: Option<&crate::anchor_resolution::AnchorRegistry>,
339) -> IdentityResult<NodeIdentity> {
340    let aliases = match registry {
341        Some(registry) => Aliases::Follow(registry),
342        None => Aliases::Undefined,
343    };
344    identity_with_tag(node, None, aliases, &mut Vec::new(), &mut Trees)
345}
346
347/// The identity of `node` for looking it up as a key among a mapping's
348/// own keys: as [`node_identity`], but with each alias standing for itself
349/// rather than for the node it refers to, so that a key holding an alias
350/// matches a key written with the same alias (`? [*a]`).
351///
352/// Returns `None` if `node` is not a value node.
353pub(crate) fn lookup_identity(node: &SyntaxNode) -> Option<NodeIdentity> {
354    identity_with_tag(node, None, Aliases::ByName, &mut Vec::new(), &mut Trees)
355        .expect("aliases are not followed")
356}
357
358/// What [`identity_with_tag`] does with an alias.
359#[derive(Clone, Copy)]
360enum Aliases<'a> {
361    /// Report it as undefined, as no anchors are known.
362    Undefined,
363    /// Follow it to the node it refers to.
364    Follow(&'a crate::anchor_resolution::AnchorRegistry),
365    /// Take it as itself, by its name and tag.
366    ByName,
367}
368
369/// The identity of a node, `None` if it is not a value node.
370pub(crate) type IdentityResult<T> = Result<Option<T>, crate::anchor_resolution::AliasError>;
371
372/// The identity of `key` as a mapping key, following aliases through
373/// `registry`; see [`NodeIdentity`]. Without a registry an alias is
374/// undefined.
375pub(crate) fn key_identity(
376    key: &YamlNode,
377    registry: Option<&crate::anchor_resolution::AnchorRegistry>,
378) -> Result<NodeIdentity, crate::anchor_resolution::AliasError> {
379    Ok(node_identity(key.syntax(), registry)?.expect("a YamlNode is a value node"))
380}
381
382/// Makes the identity of a node out of those of the nodes in it.
383trait IdentityBuilder {
384    type Id: Ord;
385
386    fn build(&mut self, identity: Identity<Self::Id>) -> Self::Id;
387
388    /// The identity worked out before for `node`, written without a tag
389    /// from outside it.
390    fn known(&self, _node: &SyntaxNode) -> Option<IdentityResult<Self::Id>> {
391        None
392    }
393
394    fn remember(&mut self, _node: &SyntaxNode, _identity: &IdentityResult<Self::Id>) {}
395}
396
397/// Builds each identity as a [`NodeIdentity`] of its own.
398struct Trees;
399
400impl IdentityBuilder for Trees {
401    type Id = NodeIdentity;
402
403    fn build(&mut self, identity: Identity<NodeIdentity>) -> NodeIdentity {
404        NodeIdentity(identity)
405    }
406}
407
408/// The identities of the nodes of one tree, as numbers that are equal
409/// exactly when the identities are.
410///
411/// A node's identity covers all the nodes in it. Each of those is worked
412/// out once and then shared, so finding the identities of nested keys
413/// takes time linear in the size of the tree. Aliases are undefined, as
414/// for [`node_identity`] without a registry.
415#[derive(Default)]
416pub(crate) struct SharedIdentities {
417    numbers: std::collections::HashMap<Identity<usize>, usize>,
418    nodes: std::collections::HashMap<SyntaxNode, IdentityResult<usize>>,
419}
420
421impl SharedIdentities {
422    /// The identity of `node`, as [`node_identity`] without a registry
423    /// gives it.
424    pub(crate) fn identity(&mut self, node: &SyntaxNode) -> IdentityResult<usize> {
425        identity_with_tag(node, None, Aliases::Undefined, &mut Vec::new(), self)
426    }
427}
428
429impl IdentityBuilder for SharedIdentities {
430    type Id = usize;
431
432    fn build(&mut self, identity: Identity<usize>) -> usize {
433        let next = self.numbers.len();
434        *self.numbers.entry(identity).or_insert(next)
435    }
436
437    fn known(&self, node: &SyntaxNode) -> Option<IdentityResult<usize>> {
438        self.nodes.get(node).cloned()
439    }
440
441    fn remember(&mut self, node: &SyntaxNode, identity: &IdentityResult<usize>) {
442        self.nodes.insert(node.clone(), identity.clone());
443    }
444}
445
446/// The identity of `node` with `tag`, if set, in place of its own tag.
447///
448/// `expanding` holds the anchored nodes whose aliases are being followed,
449/// so an alias within its own anchored node is reported as recursive.
450fn identity_with_tag<B: IdentityBuilder>(
451    node: &SyntaxNode,
452    tag: Option<String>,
453    aliases: Aliases,
454    expanding: &mut Vec<SyntaxNode>,
455    builder: &mut B,
456) -> IdentityResult<B::Id> {
457    use crate::lex::SyntaxKind;
458
459    // A tag from outside changes the identity, so only one without is
460    // shared, and only that of a node with others in it saves any work.
461    let holds_nodes = matches!(
462        node.kind(),
463        SyntaxKind::MAPPING | SyntaxKind::SEQUENCE | SyntaxKind::TAGGED_NODE
464    );
465    if tag.is_some() || !holds_nodes {
466        return build_identity(node, tag, aliases, expanding, builder);
467    }
468    if let Some(identity) = builder.known(node) {
469        return identity;
470    }
471    let identity = build_identity(node, None, aliases, expanding, builder);
472    builder.remember(node, &identity);
473    identity
474}
475
476/// [`identity_with_tag`], without looking for an identity worked out
477/// before.
478fn build_identity<B: IdentityBuilder>(
479    node: &SyntaxNode,
480    tag: Option<String>,
481    aliases: Aliases,
482    expanding: &mut Vec<SyntaxNode>,
483    builder: &mut B,
484) -> IdentityResult<B::Id> {
485    use crate::anchor_resolution::AliasError;
486    use crate::lex::SyntaxKind;
487    use crate::scalar::ScalarValue;
488
489    let with_tag = |scalar: ScalarValue, tag: Option<String>| match tag {
490        Some(tag) => scalar.with_resolved_tag(tag),
491        None => scalar,
492    };
493    let scalar = |builder: &mut B, scalar: ScalarValue, tag: Option<String>| {
494        let ResolvedScalar { tag, value } = resolve_scalar(with_tag(scalar, tag));
495        builder.build(Identity::Scalar { tag, value })
496    };
497    match node.kind() {
498        SyntaxKind::KEY | SyntaxKind::VALUE => match node.first_child() {
499            Some(content) => identity_with_tag(&content, tag, aliases, expanding, builder),
500            None => Ok(None),
501        },
502        SyntaxKind::TAGGED_NODE => {
503            let Some(tagged) = TaggedNode::cast(node.clone()) else {
504                return Ok(None);
505            };
506            // A tag passed in is that of an alias to this node, which takes
507            // the place of the node's own.
508            let tag = tag.or_else(|| tagged.resolved_tag());
509            match node.first_child() {
510                Some(content) => identity_with_tag(&content, tag, aliases, expanding, builder),
511                None => Ok(ScalarValue::from_tagged(&tagged).map(|s| scalar(builder, s, tag))),
512            }
513        }
514        SyntaxKind::ALIAS => {
515            let Some(alias) = Alias::cast(node.clone()) else {
516                return Ok(None);
517            };
518            let registry = match aliases {
519                Aliases::Follow(registry) => registry,
520                Aliases::ByName => {
521                    return Ok(Some(builder.build(Identity::Alias {
522                        tag,
523                        name: alias.name(),
524                    })))
525                }
526                Aliases::Undefined => {
527                    return Err(AliasError::Undefined {
528                        alias: alias.name(),
529                    })
530                }
531            };
532            let target = registry.resolve_alias_through(&alias)?;
533            if expanding.contains(target) {
534                return Err(AliasError::Recursive {
535                    alias: alias.name(),
536                });
537            }
538            expanding.push(target.clone());
539            let identity = identity_with_tag(target, tag, aliases, expanding, builder);
540            expanding.pop();
541            identity
542        }
543        SyntaxKind::SCALAR => {
544            Ok(Scalar::cast(node.clone())
545                .map(|s| scalar(builder, ScalarValue::from_scalar(&s), tag)))
546        }
547        SyntaxKind::MAPPING => {
548            let Some(mapping) = Mapping::cast(node.clone()) else {
549                return Ok(None);
550            };
551            let mut entries = Vec::new();
552            for (key, value) in mapping.pairs() {
553                let key = identity_with_tag(&key, None, aliases, expanding, builder)?;
554                let value = identity_with_tag(&value, None, aliases, expanding, builder)?;
555                let (Some(key), Some(value)) = (key, value) else {
556                    return Ok(None);
557                };
558                entries.push((key, value));
559            }
560            entries.sort();
561            Ok(Some(builder.build(Identity::Mapping {
562                tag: collection_tag(tag, tag::MAP),
563                entries,
564            })))
565        }
566        SyntaxKind::SEQUENCE => {
567            let Some(sequence) = Sequence::cast(node.clone()) else {
568                return Ok(None);
569            };
570            let mut items = Vec::new();
571            for item in sequence.items() {
572                let Some(item) = identity_with_tag(&item, None, aliases, expanding, builder)?
573                else {
574                    return Ok(None);
575                };
576                items.push(item);
577            }
578            Ok(Some(builder.build(Identity::Sequence {
579                tag: collection_tag(tag, tag::SEQ),
580                items,
581            })))
582        }
583        _ => Ok(None),
584    }
585}
586
587/// The identity of `v`, as [`node_identity`] gives it for the node backing
588/// `v` or, for a raw value, the CST it builds.
589pub(crate) fn identity_of<T: AsYaml + ?Sized>(
590    v: &T,
591    registry: Option<&crate::anchor_resolution::AnchorRegistry>,
592) -> Result<Option<NodeIdentity>, crate::anchor_resolution::AliasError> {
593    match to_syntax(v) {
594        Some(node) => node_identity(&node, registry),
595        None => Ok(None),
596    }
597}
598
599/// The resolved tag and canonical value of a scalar node, tagged or not,
600/// or of the scalar a KEY or VALUE wrapper holds.
601///
602/// Returns `None` for anything that is not a scalar.
603pub(crate) fn scalar_canonical(node: &SyntaxNode) -> Option<(String, String)> {
604    match node_identity(node, None) {
605        Ok(Some(NodeIdentity(Identity::Scalar { tag, value }))) => Some((tag, value)),
606        _ => None,
607    }
608}
609
610/// Spec equality of two CST nodes. An alias equals nothing, as it cannot
611/// be followed here.
612fn node_eq(a: &SyntaxNode, b: &SyntaxNode) -> bool {
613    match (node_identity(a, None), node_identity(b, None)) {
614        (Ok(Some(a)), Ok(Some(b))) => a == b,
615        _ => false,
616    }
617}
618
619/// A type-erased handle to a CST node returned from navigation methods.
620///
621/// You get a `YamlNode` back from methods like
622/// [`Mapping::get`](crate::yaml::Mapping::get),
623/// [`Sequence::get`](crate::nodes::sequence::Sequence::get),
624/// [`Mapping::keys`](crate::yaml::Mapping::keys), etc.
625///
626/// Match on the variants to get the concrete type, or use the helper
627/// methods [`as_scalar`](Self::as_scalar), [`as_mapping`](Self::as_mapping), etc.
628///
629/// ```
630/// use yaml_edit::{Document, YamlNode};
631/// use std::str::FromStr;
632///
633/// let doc = Document::from_str("name: Alice\nage: 30").unwrap();
634///
635/// if let Some(node) = doc.get("name") {
636///     match node {
637///         YamlNode::Scalar(s) => println!("scalar: {}", s.as_string()),
638///         YamlNode::Mapping(m) => println!("mapping with {} keys", m.len()),
639///         YamlNode::Sequence(s) => println!("sequence with {} items", s.len()),
640///         YamlNode::Alias(a) => println!("alias: *{}", a.name()),
641///         YamlNode::TaggedNode(t) => println!("tagged: {:?}", t.tag()),
642///     }
643/// }
644/// ```
645#[derive(Debug, Clone, PartialEq)]
646pub enum YamlNode {
647    /// A scalar value (string, integer, float, boolean, null).
648    Scalar(Scalar),
649    /// A key-value mapping.
650    Mapping(Mapping),
651    /// An ordered sequence.
652    Sequence(Sequence),
653    /// An alias reference (e.g. `*anchor_name`).
654    Alias(Alias),
655    /// A tagged node (e.g. `!!set`, `!!omap`, `!!pairs`, or a custom tag).
656    TaggedNode(TaggedNode),
657}
658
659impl YamlNode {
660    /// Cast a `SyntaxNode` to a `YamlNode`.
661    ///
662    /// Returns `None` if the node's kind is not one of `SCALAR`, `MAPPING`,
663    /// `SEQUENCE`, `ALIAS`, or `TAGGED_NODE`.
664    pub fn from_syntax(node: SyntaxNode) -> Option<Self> {
665        use crate::lex::SyntaxKind;
666        match node.kind() {
667            SyntaxKind::SCALAR => Scalar::cast(node).map(YamlNode::Scalar),
668            SyntaxKind::MAPPING => Mapping::cast(node).map(YamlNode::Mapping),
669            SyntaxKind::SEQUENCE => Sequence::cast(node).map(YamlNode::Sequence),
670            SyntaxKind::ALIAS => Alias::cast(node).map(YamlNode::Alias),
671            SyntaxKind::TAGGED_NODE => TaggedNode::cast(node).map(YamlNode::TaggedNode),
672            _ => None,
673        }
674    }
675
676    /// Cast a `SyntaxNode` to a `YamlNode`, peeling any `KEY` or `VALUE`
677    /// wrapper first.
678    ///
679    /// Used internally where `mapping.pairs()` yields raw KEY/VALUE wrapper
680    /// nodes that need to be unwrapped before semantic comparison.
681    pub(crate) fn from_syntax_peeled(node: SyntaxNode) -> Option<Self> {
682        use crate::lex::SyntaxKind;
683        let inner = if matches!(node.kind(), SyntaxKind::KEY | SyntaxKind::VALUE) {
684            node.children().next()?
685        } else {
686            node
687        };
688        Self::from_syntax(inner)
689    }
690
691    /// Returns the kind of YAML value this node represents.
692    pub fn kind(&self) -> YamlKind {
693        match self {
694            YamlNode::Scalar(_) => YamlKind::Scalar,
695            YamlNode::Mapping(_) => YamlKind::Mapping,
696            YamlNode::Sequence(_) => YamlKind::Sequence,
697            YamlNode::Alias(_) => YamlKind::Alias,
698            YamlNode::TaggedNode(t) => t
699                .tag()
700                .map_or(YamlKind::Scalar, |tag| YamlKind::Tagged(Cow::Owned(tag))),
701        }
702    }
703
704    /// Returns the underlying `SyntaxNode`.
705    pub(crate) fn syntax(&self) -> &SyntaxNode {
706        match self {
707            YamlNode::Scalar(s) => s.syntax(),
708            YamlNode::Mapping(m) => m.syntax(),
709            YamlNode::Sequence(s) => s.syntax(),
710            YamlNode::Alias(a) => a.syntax(),
711            YamlNode::TaggedNode(t) => t.syntax(),
712        }
713    }
714
715    /// Compare semantically with another value (ignores quoting/formatting).
716    pub fn yaml_eq<O: AsYaml>(&self, other: &O) -> bool {
717        yaml_eq(self, other)
718    }
719
720    /// If this node is a scalar, return a reference to it.
721    pub fn as_scalar(&self) -> Option<&Scalar> {
722        if let YamlNode::Scalar(s) = self {
723            Some(s)
724        } else {
725            None
726        }
727    }
728
729    /// If this node is a mapping, return a reference to it.
730    pub fn as_mapping(&self) -> Option<&Mapping> {
731        if let YamlNode::Mapping(m) = self {
732            Some(m)
733        } else {
734            None
735        }
736    }
737
738    /// If this node is a sequence, return a reference to it.
739    pub fn as_sequence(&self) -> Option<&Sequence> {
740        if let YamlNode::Sequence(s) = self {
741            Some(s)
742        } else {
743            None
744        }
745    }
746
747    /// The mapping this node is, looking through a tag on it.
748    pub(crate) fn mapping_content(&self) -> Option<Mapping> {
749        match self {
750            YamlNode::Mapping(m) => Some(m.clone()),
751            YamlNode::TaggedNode(t) => t.as_mapping(),
752            _ => None,
753        }
754    }
755
756    /// The sequence this node is, looking through a tag on it.
757    pub(crate) fn sequence_content(&self) -> Option<Sequence> {
758        match self {
759            YamlNode::Sequence(s) => Some(s.clone()),
760            YamlNode::TaggedNode(t) => t.as_sequence(),
761            _ => None,
762        }
763    }
764
765    /// If this node is a tagged node, return a reference to it.
766    pub fn as_tagged(&self) -> Option<&TaggedNode> {
767        if let YamlNode::TaggedNode(t) = self {
768            Some(t)
769        } else {
770            None
771        }
772    }
773
774    /// If this node is an alias, return a reference to it.
775    pub fn as_alias(&self) -> Option<&Alias> {
776        if let YamlNode::Alias(a) = self {
777            Some(a)
778        } else {
779            None
780        }
781    }
782
783    /// Returns `true` if this node is a scalar.
784    pub fn is_scalar(&self) -> bool {
785        matches!(self, YamlNode::Scalar(_))
786    }
787
788    /// Returns `true` if this node is a mapping.
789    pub fn is_mapping(&self) -> bool {
790        matches!(self, YamlNode::Mapping(_))
791    }
792
793    /// Returns `true` if this node is a sequence.
794    pub fn is_sequence(&self) -> bool {
795        matches!(self, YamlNode::Sequence(_))
796    }
797
798    /// Returns `true` if this node is a tagged node.
799    pub fn is_tagged(&self) -> bool {
800        matches!(self, YamlNode::TaggedNode(_))
801    }
802
803    /// Returns `true` if this node is an alias.
804    pub fn is_alias(&self) -> bool {
805        matches!(self, YamlNode::Alias(_))
806    }
807
808    /// Get the byte offset range of this node in the source text.
809    pub fn byte_range(&self) -> crate::TextPosition {
810        self.syntax().text_range().into()
811    }
812
813    /// If this node is a scalar, try to parse it as an `i64`.
814    ///
815    /// Returns `None` if this is not a scalar or cannot be parsed as an integer.
816    pub fn to_i64(&self) -> Option<i64> {
817        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_i64()
818    }
819
820    /// If this node is a scalar, try to parse it as an `f64`.
821    ///
822    /// Returns `None` if this is not a float scalar, such as for an integer.
823    pub fn to_f64(&self) -> Option<f64> {
824        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_f64()
825    }
826
827    /// If this node is a scalar, try to parse it as a `bool`.
828    ///
829    /// Returns `None` if this is not a scalar or cannot be parsed as a boolean.
830    pub fn to_bool(&self) -> Option<bool> {
831        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_bool()
832    }
833
834    /// Get a value by key from a mapping node.
835    ///
836    /// Returns `None` if this node is not a mapping or the key is not found.
837    pub fn get(&self, key: impl crate::AsYaml) -> Option<YamlNode> {
838        self.as_mapping()?
839            .get_node(key)
840            .and_then(YamlNode::from_syntax)
841    }
842
843    /// Get a value by index from a sequence node.
844    ///
845    /// Returns `None` if this node is not a sequence or the index is out of bounds.
846    pub fn get_item(&self, index: usize) -> Option<YamlNode> {
847        self.as_sequence()?
848            .items()
849            .nth(index)
850            .and_then(YamlNode::from_syntax)
851    }
852}
853
854impl AsYaml for YamlNode {
855    fn as_node(&self) -> Option<&SyntaxNode> {
856        Some(self.syntax())
857    }
858
859    fn kind(&self) -> YamlKind {
860        YamlNode::kind(self)
861    }
862
863    fn build_content(
864        &self,
865        builder: &mut rowan::GreenNodeBuilder,
866        indent: usize,
867        flow_context: bool,
868    ) -> bool {
869        match self {
870            YamlNode::Scalar(s) => s.build_content(builder, indent, flow_context),
871            YamlNode::Mapping(m) => m.build_content(builder, indent, flow_context),
872            YamlNode::Sequence(s) => s.build_content(builder, indent, flow_context),
873            YamlNode::Alias(a) => a.build_content(builder, indent, flow_context),
874            YamlNode::TaggedNode(t) => t.build_content(builder, indent, flow_context),
875        }
876    }
877
878    fn is_inline(&self) -> bool {
879        use crate::yaml::ValueNode;
880        match self {
881            YamlNode::Scalar(s) => ValueNode::is_inline(s),
882            YamlNode::Mapping(m) => ValueNode::is_inline(m),
883            YamlNode::Sequence(s) => ValueNode::is_inline(s),
884            YamlNode::Alias(_) => true,
885            YamlNode::TaggedNode(t) => ValueNode::is_inline(t),
886        }
887    }
888}
889
890/// `yaml_node == "some_string"` - compares the node's semantic value.
891impl PartialEq<str> for YamlNode {
892    fn eq(&self, other: &str) -> bool {
893        yaml_eq(self, &other)
894    }
895}
896
897impl PartialEq<&str> for YamlNode {
898    fn eq(&self, other: &&str) -> bool {
899        yaml_eq(self, other)
900    }
901}
902
903impl PartialEq<String> for YamlNode {
904    fn eq(&self, other: &String) -> bool {
905        yaml_eq(self, &other.as_str())
906    }
907}
908
909impl fmt::Display for YamlNode {
910    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911        write!(f, "{}", self.syntax().text())
912    }
913}
914
915/// Blanket impl: any reference to an `AsYaml` type also implements `AsYaml`.
916///
917/// The `?Sized` bound makes this work for trait objects (`&dyn AsYaml`),
918/// which lets [`MappingView`](crate::MappingView) - whose methods take
919/// `&dyn AsYaml` - forward keys to inherent methods that take
920/// `impl AsYaml`.
921impl<T: AsYaml + ?Sized> AsYaml for &T {
922    fn as_node(&self) -> Option<&SyntaxNode> {
923        (*self).as_node()
924    }
925
926    fn kind(&self) -> YamlKind {
927        (*self).kind()
928    }
929
930    fn build_content(
931        &self,
932        builder: &mut rowan::GreenNodeBuilder,
933        indent: usize,
934        flow_context: bool,
935    ) -> bool {
936        (*self).build_content(builder, indent, flow_context)
937    }
938
939    fn is_inline(&self) -> bool {
940        (*self).is_inline()
941    }
942
943    fn build_flow_content(
944        &self,
945        builder: &mut rowan::GreenNodeBuilder,
946        indent: usize,
947        quoting: FlowQuoting,
948    ) -> bool {
949        (*self).build_flow_content(builder, indent, quoting)
950    }
951
952    fn value_node(&self) -> Option<SyntaxNode> {
953        (*self).value_node()
954    }
955}
956
957/// Recursively copy the children of `node` into `builder`.
958pub(crate) fn copy_node_content(builder: &mut rowan::GreenNodeBuilder, node: &SyntaxNode) {
959    for child in node.children_with_tokens() {
960        match child {
961            rowan::NodeOrToken::Node(n) => {
962                builder.start_node(n.kind().into());
963                copy_node_content(builder, &n);
964                builder.finish_node();
965            }
966            rowan::NodeOrToken::Token(t) => {
967                builder.token(t.kind().into(), t.text());
968            }
969        }
970    }
971}
972
973/// Copy the children of `node` into `builder`, shifting every post-newline
974/// whitespace, and the lines inside a multi-line quoted scalar, by `delta`
975/// columns (may be negative to dedent). Only spaces are added or removed;
976/// tabs after them are kept. Line-leading whitespace is written as INDENT,
977/// as the parser does.
978///
979/// Newline state is tracked across node boundaries: a NEWLINE inside a
980/// child node correctly marks
981/// the next sibling token as being at the start of a line, so
982/// sibling INDENT tokens between entries (which is where the parser
983/// places them for block sequences and mappings) are shifted too.
984/// Returns whether the last emitted token was a NEWLINE.
985pub(crate) fn copy_node_content_reindent(
986    builder: &mut rowan::GreenNodeBuilder,
987    node: &SyntaxNode,
988    delta: isize,
989) -> bool {
990    use crate::lex::SyntaxKind;
991    let mut after_newline = false;
992    let pad = |n: isize| " ".repeat(n.max(0) as usize);
993
994    for child in node.children_with_tokens() {
995        match child {
996            rowan::NodeOrToken::Node(n) => {
997                // Empty nodes (e.g. an implicit `null` value after `? key`
998                // in a set) emit nothing, so they must not affect the
999                // `after_newline` state or trigger a pre-node indent. Their
1000                // zero-width content still has to come along.
1001                if n.text_range().is_empty() {
1002                    builder.start_node(n.kind().into());
1003                    copy_node_content(builder, &n);
1004                    builder.finish_node();
1005                    continue;
1006                }
1007                if after_newline && delta > 0 {
1008                    builder.token(SyntaxKind::INDENT.into(), &pad(delta));
1009                }
1010                builder.start_node(n.kind().into());
1011                after_newline = copy_node_content_reindent(builder, &n, delta);
1012                builder.finish_node();
1013            }
1014            rowan::NodeOrToken::Token(t) => match t.kind() {
1015                SyntaxKind::NEWLINE => {
1016                    builder.token(t.kind().into(), t.text());
1017                    after_newline = true;
1018                }
1019                SyntaxKind::WHITESPACE | SyntaxKind::INDENT => {
1020                    if after_newline {
1021                        let shifted = shift_line_start(t.text(), delta);
1022                        if !shifted.is_empty() {
1023                            builder.token(SyntaxKind::INDENT.into(), &shifted);
1024                        }
1025                    } else {
1026                        builder.token(t.kind().into(), t.text());
1027                    }
1028                    after_newline = false;
1029                }
1030                _ => {
1031                    if after_newline && delta > 0 {
1032                        builder.token(SyntaxKind::INDENT.into(), &pad(delta));
1033                    }
1034                    builder.token(t.kind().into(), &shift_inner_lines(t.text(), delta));
1035                    after_newline = false;
1036                }
1037            },
1038        }
1039    }
1040    after_newline
1041}
1042
1043/// `delta`, raised where needed so that every line of `node` after its
1044/// first ends up past column `parent`. The continuation lines of a flow node
1045/// or multi-line scalar have to clear the block entry it is nested in, but
1046/// at the top of a document they may start at column 0.
1047pub(crate) fn clear_parent_column(node: &SyntaxNode, parent: usize, delta: isize) -> isize {
1048    let shallowest = node
1049        .text()
1050        .to_string()
1051        .split(['\n', '\r'])
1052        .skip(1)
1053        .filter(|line| !line.trim().is_empty())
1054        .map(crate::column::space_indent)
1055        .min();
1056    shallowest.map_or(delta, |column| {
1057        delta.max(parent as isize + 1 - column as isize)
1058    })
1059}
1060
1061/// `text`, a line or the whitespace at its start, with `delta` spaces
1062/// added or up to `-delta` leading spaces removed. Tabs are not
1063/// indentation, so any that follow the spaces are kept.
1064pub(crate) fn shift_line_start(text: &str, delta: isize) -> String {
1065    if delta >= 0 {
1066        return format!("{}{text}", " ".repeat(delta as usize));
1067    }
1068    let spaces = crate::column::space_indent(text);
1069    text[spaces.min(delta.unsigned_abs())..].to_string()
1070}
1071
1072/// `text` with the indentation of each line after the first moved by
1073/// `delta` columns, leaving empty lines empty.
1074pub(crate) fn shift_inner_lines(text: &str, delta: isize) -> Cow<'_, str> {
1075    if delta == 0 || !text.contains(['\n', '\r']) {
1076        return Cow::Borrowed(text);
1077    }
1078    // A `\r\n` splits into a line ending in `\r` and an empty one.
1079    let mut lines = text.split_inclusive(['\n', '\r']);
1080    let mut shifted = lines.next().unwrap_or_default().to_string();
1081    for line in lines {
1082        if line.trim_end_matches(['\n', '\r']).is_empty() {
1083            shifted.push_str(line);
1084        } else {
1085            shifted.push_str(&shift_line_start(line, delta));
1086        }
1087    }
1088    Cow::Owned(shifted)
1089}
1090
1091/// Base column to measure a copied collection's indentation from: for a
1092/// flow collection, whose continuation lines are only bound to the
1093/// indentation of the line it starts on, that line's indentation;
1094/// otherwise the column of its first entry.
1095pub(crate) fn source_reindent_base(node: &SyntaxNode, is_flow: bool) -> usize {
1096    if is_flow {
1097        crate::column::line_indent(node)
1098    } else {
1099        crate::column::node_column(node)
1100    }
1101}
1102
1103/// Implement AsYaml for primitives that build as the scalar `$to_scalar`
1104/// turns them into.
1105macro_rules! impl_as_yaml_scalar {
1106    ($to_scalar:expr; $($ty:ty),+ $(,)?) => {
1107        $(impl AsYaml for $ty {
1108            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1109                None
1110            }
1111
1112            fn kind(&self) -> YamlKind {
1113                YamlKind::Scalar
1114            }
1115
1116            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, indent: usize, _flow_context: bool) -> bool {
1117                let to_scalar: fn(&$ty) -> crate::scalar::ScalarValue = $to_scalar;
1118                to_scalar(self).build(builder, indent, None)
1119            }
1120
1121            fn is_inline(&self) -> bool {
1122                true
1123            }
1124        })+
1125    };
1126}
1127
1128impl_as_yaml_scalar!(
1129    |v| crate::scalar::ScalarValue::plain(v.to_string()).with_type(crate::scalar::ScalarType::Integer);
1130    i64, i32, i16, i8, isize, u64, u32, u16, u8, usize
1131);
1132impl_as_yaml_scalar!(|v| crate::scalar::ScalarValue::from(*v); f64, f32, bool);
1133
1134impl AsYaml for String {
1135    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1136        None
1137    }
1138
1139    fn kind(&self) -> YamlKind {
1140        YamlKind::Scalar
1141    }
1142
1143    fn build_content(
1144        &self,
1145        builder: &mut rowan::GreenNodeBuilder,
1146        indent: usize,
1147        flow_context: bool,
1148    ) -> bool {
1149        self.as_str().build_content(builder, indent, flow_context)
1150    }
1151
1152    fn is_inline(&self) -> bool {
1153        true
1154    }
1155
1156    fn build_flow_content(
1157        &self,
1158        builder: &mut rowan::GreenNodeBuilder,
1159        indent: usize,
1160        quoting: FlowQuoting,
1161    ) -> bool {
1162        self.as_str().build_flow_content(builder, indent, quoting)
1163    }
1164}
1165
1166impl AsYaml for &str {
1167    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1168        None
1169    }
1170
1171    fn kind(&self) -> YamlKind {
1172        YamlKind::Scalar
1173    }
1174
1175    fn build_content(
1176        &self,
1177        builder: &mut rowan::GreenNodeBuilder,
1178        indent: usize,
1179        flow_context: bool,
1180    ) -> bool {
1181        crate::scalar::ScalarValue::string(*self).build_content(builder, indent, flow_context)
1182    }
1183
1184    fn is_inline(&self) -> bool {
1185        true
1186    }
1187
1188    fn build_flow_content(
1189        &self,
1190        builder: &mut rowan::GreenNodeBuilder,
1191        indent: usize,
1192        quoting: FlowQuoting,
1193    ) -> bool {
1194        crate::scalar::ScalarValue::string(*self).build_flow_content(builder, indent, quoting)
1195    }
1196}
1197
1198/// `Some(v)` behaves exactly like `v`; `None` emits a bare `null` scalar.
1199impl<T: AsYaml> AsYaml for Option<T> {
1200    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1201        self.as_ref().and_then(AsYaml::as_node)
1202    }
1203
1204    fn kind(&self) -> YamlKind {
1205        match self {
1206            Some(v) => v.kind(),
1207            None => YamlKind::Scalar,
1208        }
1209    }
1210
1211    fn build_content(
1212        &self,
1213        builder: &mut rowan::GreenNodeBuilder,
1214        indent: usize,
1215        flow_context: bool,
1216    ) -> bool {
1217        match self {
1218            Some(v) => v.build_content(builder, indent, flow_context),
1219            None => crate::scalar::ScalarValue::null().build_content(builder, indent, flow_context),
1220        }
1221    }
1222
1223    fn is_inline(&self) -> bool {
1224        match self {
1225            Some(v) => v.is_inline(),
1226            None => true,
1227        }
1228    }
1229
1230    fn build_flow_content(
1231        &self,
1232        builder: &mut rowan::GreenNodeBuilder,
1233        indent: usize,
1234        quoting: FlowQuoting,
1235    ) -> bool {
1236        match self {
1237            Some(v) => v.build_flow_content(builder, indent, quoting),
1238            None => self.build_content(builder, indent, true),
1239        }
1240    }
1241}
1242
1243/// Emit a TAGGED_NODE with `tag`, followed by the content `content` emits:
1244/// on a line of its own indented past `indent` if `block` is set, after a
1245/// space otherwise. Returns whether the content ends with a newline.
1246pub(crate) fn build_tagged(
1247    builder: &mut rowan::GreenNodeBuilder,
1248    tag: &str,
1249    block: bool,
1250    indent: usize,
1251    content: impl FnOnce(&mut rowan::GreenNodeBuilder) -> bool,
1252) -> bool {
1253    use crate::lex::SyntaxKind;
1254
1255    builder.start_node(SyntaxKind::TAGGED_NODE.into());
1256    builder.token(SyntaxKind::TAG.into(), tag);
1257    if block {
1258        builder.token(SyntaxKind::NEWLINE.into(), "\n");
1259        builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent + 2));
1260    } else {
1261        builder.token(SyntaxKind::WHITESPACE.into(), " ");
1262    }
1263    let ends_with_newline = content(builder);
1264    builder.finish_node(); // TAGGED_NODE
1265    ends_with_newline
1266}
1267
1268/// Emit a mapping of `entries`, in flow style quoting strings as `flow`
1269/// says if it is set or there are no entries, in block style otherwise.
1270/// Returns whether the content ends with a newline.
1271pub(crate) fn build_mapping<K: AsYaml, V: AsYaml>(
1272    builder: &mut rowan::GreenNodeBuilder,
1273    entries: impl ExactSizeIterator<Item = (K, V)>,
1274    indent: usize,
1275    flow: Option<FlowQuoting>,
1276) -> bool {
1277    use crate::lex::SyntaxKind;
1278
1279    let len = entries.len();
1280    let block = flow.is_none() && len > 0;
1281    // Entries of an empty block mapping would be written as `{}`.
1282    let flow = (!block).then_some(flow.unwrap_or(FlowQuoting::Minimal));
1283
1284    builder.start_node(SyntaxKind::MAPPING.into());
1285    if !block {
1286        builder.token(SyntaxKind::LEFT_BRACE.into(), "{");
1287    }
1288    for (i, (key, value)) in entries.enumerate() {
1289        if i > 0 && block && indent > 0 {
1290            // The previous entry's newline ends its line.
1291            builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
1292        }
1293        builder.start_node(SyntaxKind::MAPPING_ENTRY.into());
1294        builder.start_node(SyntaxKind::KEY.into());
1295        crate::nodes::build_key_content(builder, &key, indent, flow);
1296        builder.finish_node(); // KEY
1297        builder.token(SyntaxKind::COLON.into(), ":");
1298        let starts_block = block && !value.is_inline();
1299        let ends_with_newline = if starts_block {
1300            builder.start_node(SyntaxKind::VALUE.into());
1301            builder.token(SyntaxKind::NEWLINE.into(), "\n");
1302            builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent + 2));
1303            build_in(&value, builder, indent + 2, None)
1304        } else {
1305            builder.token(SyntaxKind::WHITESPACE.into(), " ");
1306            builder.start_node(SyntaxKind::VALUE.into());
1307            // A block scalar's body has to clear the key's column.
1308            build_in(&value, builder, indent, flow)
1309        };
1310        builder.finish_node(); // VALUE
1311        if block {
1312            if !ends_with_newline {
1313                builder.token(SyntaxKind::NEWLINE.into(), "\n");
1314            }
1315        } else if i + 1 < len {
1316            builder.token(SyntaxKind::COMMA.into(), ",");
1317            builder.token(SyntaxKind::WHITESPACE.into(), " ");
1318        }
1319        builder.finish_node(); // MAPPING_ENTRY
1320    }
1321    if !block {
1322        builder.token(SyntaxKind::RIGHT_BRACE.into(), "}");
1323    }
1324    builder.finish_node(); // MAPPING
1325    block
1326}
1327
1328/// Emit a sequence of `items`, in flow style quoting strings as `flow`
1329/// says if it is set or there are no items, in block style otherwise.
1330/// Returns whether the content ends with a newline.
1331pub(crate) fn build_sequence<T: AsYaml>(
1332    builder: &mut rowan::GreenNodeBuilder,
1333    items: impl ExactSizeIterator<Item = T>,
1334    indent: usize,
1335    flow: Option<FlowQuoting>,
1336) -> bool {
1337    use crate::lex::SyntaxKind;
1338
1339    let len = items.len();
1340    let block = flow.is_none() && len > 0;
1341    let flow = (!block).then_some(flow.unwrap_or(FlowQuoting::Minimal));
1342
1343    builder.start_node(SyntaxKind::SEQUENCE.into());
1344    if !block {
1345        builder.token(SyntaxKind::LEFT_BRACKET.into(), "[");
1346    }
1347    for (i, item) in items.enumerate() {
1348        if i > 0 && block && indent > 0 {
1349            // The previous entry's newline ends its line.
1350            builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
1351        }
1352        builder.start_node(SyntaxKind::SEQUENCE_ENTRY.into());
1353        if block {
1354            builder.token(SyntaxKind::DASH.into(), "-");
1355            builder.token(SyntaxKind::WHITESPACE.into(), " ");
1356            // An inline item, such as a block scalar, indents its body past
1357            // the dash; a block collection aligns with its first entry.
1358            let item_indent = if item.is_inline() { indent } else { indent + 2 };
1359            if !build_in(&item, builder, item_indent, None) {
1360                builder.token(SyntaxKind::NEWLINE.into(), "\n");
1361            }
1362        } else {
1363            build_in(&item, builder, indent, flow);
1364            if i + 1 < len {
1365                builder.token(SyntaxKind::COMMA.into(), ",");
1366                builder.token(SyntaxKind::WHITESPACE.into(), " ");
1367            }
1368        }
1369        builder.finish_node(); // SEQUENCE_ENTRY
1370    }
1371    if !block {
1372        builder.token(SyntaxKind::RIGHT_BRACKET.into(), "]");
1373    }
1374    builder.finish_node(); // SEQUENCE
1375    block
1376}
1377
1378/// Emit a `!!set`: a tagged mapping with each member as a key and a null
1379/// value. Returns whether the content ends with a newline.
1380pub(crate) fn build_set_content<T: AsYaml>(
1381    builder: &mut rowan::GreenNodeBuilder,
1382    members: impl ExactSizeIterator<Item = T>,
1383    indent: usize,
1384    flow: Option<FlowQuoting>,
1385) -> bool {
1386    let block = flow.is_none() && members.len() > 0;
1387    build_tagged(builder, tag::SET, block, indent, |builder| {
1388        let entries = members.map(|m| (m, crate::scalar::ScalarValue::null()));
1389        build_mapping(builder, entries, indent + 2, flow)
1390    })
1391}
1392
1393/// A `!!set` with the members as its keys, in sorted order.
1394///
1395/// There is no impl for `HashSet`, whose iteration order would make the
1396/// output change from run to run.
1397impl<T: AsYaml> AsYaml for std::collections::BTreeSet<T> {
1398    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1399        None
1400    }
1401
1402    fn kind(&self) -> YamlKind {
1403        YamlKind::Tagged(Cow::Borrowed(tag::SET))
1404    }
1405
1406    fn build_content(
1407        &self,
1408        builder: &mut rowan::GreenNodeBuilder,
1409        indent: usize,
1410        flow_context: bool,
1411    ) -> bool {
1412        build_set_content(
1413            builder,
1414            self.iter(),
1415            indent,
1416            flow_context.then_some(FlowQuoting::Minimal),
1417        )
1418    }
1419
1420    fn is_inline(&self) -> bool {
1421        // The tag goes on the key's line, as in `tags: !!set`.
1422        true
1423    }
1424
1425    fn build_flow_content(
1426        &self,
1427        builder: &mut rowan::GreenNodeBuilder,
1428        indent: usize,
1429        quoting: FlowQuoting,
1430    ) -> bool {
1431        build_set_content(builder, self.iter(), indent, Some(quoting))
1432    }
1433}