Skip to main content

yaml_edit/
yaml.rs

1//! Lossless YAML parser and editor.
2
3use crate::{lex::SyntaxKind, parse::Parse, PositionedParseError};
4use rowan::ast::AstNode;
5use rowan::GreenNodeBuilder;
6use std::path::Path;
7use std::str::FromStr;
8
9/// The raw result of parsing a YAML file, before it is wrapped in a [`YamlFile`] node.
10///
11/// Contains the green CST root and any errors encountered during parsing.
12/// Most callers should use [`YamlFile::parse`] instead, which returns a [`Parse<YamlFile>`]
13/// wrapper with the same information in a more ergonomic form.
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub(crate) struct ParsedYaml {
16    /// The immutable green-tree root produced by the parser.
17    pub(crate) green_node: rowan::GreenNode,
18    /// Structured parse errors with source positions.
19    pub(crate) positioned_errors: Vec<PositionedParseError>,
20}
21
22// Import Lang, SyntaxNode, and ast_node! macro from nodes module
23use crate::nodes::{ast_node, has_child_token, SyntaxToken};
24pub use crate::nodes::{Lang, SyntaxNode};
25
26// Re-export extracted AST nodes from nodes module
27pub use crate::nodes::{
28    Alias, Comment, Directive, Document, Entry, Mapping, MappingEntry, OccupiedEntry, Scalar,
29    ScalarConversionError, Sequence, TaggedNode, VacantEntry,
30};
31
32ast_node!(
33    YamlFile,
34    ROOT,
35    "A YAML file containing one or more documents"
36);
37
38impl YamlFile {
39    /// Capture an independent snapshot of this YAML file.
40    ///
41    /// The returned value shares the underlying immutable green-node data
42    /// with `self` at the time of the call, but lives in its own mutable
43    /// tree: subsequent mutations to `self` do not propagate to the snapshot.
44    /// Pair with [`Self::tree_eq`] to detect later mutations.
45    pub fn snapshot(&self) -> Self {
46        YamlFile(SyntaxNode::new_root_mut(self.0.green().into_owned()))
47    }
48
49    /// Returns true iff the syntax trees of `self` and `other` are
50    /// value-equal. An O(1) pointer-identity fast path makes this free for
51    /// trees that still share state with a recent `snapshot()`.
52    pub fn tree_eq(&self, other: &Self) -> bool {
53        let a = self.0.green();
54        let b = other.0.green();
55        let a_ref: &rowan::GreenNodeData = &a;
56        let b_ref: &rowan::GreenNodeData = &b;
57        std::ptr::eq(a_ref as *const _, b_ref as *const _) || a_ref == b_ref
58    }
59}
60
61/// Trait for value nodes (Mapping, Sequence, Scalar) with inline detection
62pub trait ValueNode: rowan::ast::AstNode<Language = Lang> {
63    /// Returns whether this value should be rendered inline
64    fn is_inline(&self) -> bool;
65}
66
67impl ValueNode for Mapping {
68    fn is_inline(&self) -> bool {
69        // Flow-style mappings contain brace tokens.
70        has_child_token(&self.0, |k| {
71            matches!(k, SyntaxKind::LEFT_BRACE | SyntaxKind::RIGHT_BRACE)
72        })
73    }
74}
75
76impl ValueNode for Sequence {
77    fn is_inline(&self) -> bool {
78        // Flow-style sequences contain bracket tokens.
79        has_child_token(&self.0, |k| {
80            matches!(k, SyntaxKind::LEFT_BRACKET | SyntaxKind::RIGHT_BRACKET)
81        })
82    }
83}
84
85impl ValueNode for Scalar {
86    fn is_inline(&self) -> bool {
87        // Block scalars (literal `|` and folded `>`) span multiple lines
88        // starting on a NEW line after their indicator, so they don't
89        // render inline. Everything else - plain, quoted, tagged scalars -
90        // sits on the same line as its key.
91        !crate::nodes::is_block_scalar(&self.0)
92    }
93}
94
95impl ValueNode for TaggedNode {
96    fn is_inline(&self) -> bool {
97        // A tagged node is inline iff the value it wraps is inline. Look
98        // for the first NEWLINE inside; if it appears before the wrapped
99        // node, the value sits on the next line (block form).
100        let mut seen_newline = false;
101        for child in self.0.children_with_tokens() {
102            match child {
103                rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::NEWLINE => {
104                    seen_newline = true;
105                }
106                rowan::NodeOrToken::Node(n) => {
107                    if seen_newline {
108                        return false;
109                    }
110                    return match n.kind() {
111                        SyntaxKind::MAPPING => Mapping::cast(n).is_some_and(|m| m.is_inline()),
112                        SyntaxKind::SEQUENCE => Sequence::cast(n).is_some_and(|s| s.is_inline()),
113                        SyntaxKind::SCALAR => Scalar::cast(n).is_some_and(|s| s.is_inline()),
114                        _ => true,
115                    };
116                }
117                _ => {}
118            }
119        }
120        true
121    }
122}
123
124/// Move the comment and blank lines at the start of a block collection out
125/// in front of it, where the parser puts the lines before a collection's
126/// first entry. They are left there when the first entry is removed.
127///
128/// A collection that started on the line of a `- ` or `? ` indicator then
129/// starts on the line after it, so that the lines do not join the
130/// indicator's: `- a: 1\n  # c\n  b: 2\n` with `a` removed gives
131/// `-\n  # c\n  b: 2\n`.
132pub(crate) fn move_leading_lines_out(collection: &SyntaxNode) {
133    let leading: Vec<_> = collection
134        .children_with_tokens()
135        .map_while(|el| el.into_token())
136        .take_while(|t| {
137            matches!(
138                t.kind(),
139                SyntaxKind::COMMENT
140                    | SyntaxKind::INDENT
141                    | SyntaxKind::WHITESPACE
142                    | SyntaxKind::NEWLINE
143            )
144        })
145        .collect();
146    if leading.is_empty() {
147        return;
148    }
149    // A detached collection has nothing to move the lines into.
150    let Some(parent) = collection.parent() else {
151        return;
152    };
153    let starts_with_comment = leading[0].kind() == SyntaxKind::COMMENT;
154    let mut lines: Vec<rowan::NodeOrToken<SyntaxNode, rowan::SyntaxToken<Lang>>> = Vec::new();
155    match leading[0].prev_token() {
156        // The space after a `- ` or `? ` indicator.
157        Some(space) if space.kind() == SyntaxKind::WHITESPACE => {
158            space.detach();
159            lines.push(crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into());
160            // A comment that ended the removed entry's line has no indent
161            // of its own; it takes the entries' column.
162            let column = leading
163                .iter()
164                .rev()
165                .find(|t| t.kind() == SyntaxKind::INDENT)
166                .filter(|_| starts_with_comment);
167            if let Some(column) = column {
168                lines.push(crate::nodes::fresh_token(SyntaxKind::INDENT, column.text()).into());
169            }
170        }
171        // The first line has an indent of its own, or is blank.
172        Some(indent) if indent.kind() == SyntaxKind::INDENT && !starts_with_comment => {
173            indent.detach();
174        }
175        _ => {}
176    }
177    lines.extend(
178        leading
179            .iter()
180            .map(|t| crate::nodes::fresh_token(t.kind(), t.text()).into()),
181    );
182    crate::nodes::replace_children(collection, 0..leading.len(), vec![]);
183    // Only the stream's first document holds the lines before its root;
184    // after a `...` or another document, they are the stream's unless a
185    // `---` comes first.
186    let (host, at) = match parent.parent() {
187        Some(root)
188            if parent.kind() == SyntaxKind::DOCUMENT
189                && parent.prev_sibling_or_token().is_some()
190                && collection.prev_sibling_or_token().is_none() =>
191        {
192            (root, parent.index())
193        }
194        _ => (parent, collection.index()),
195    };
196    crate::nodes::insert_children(&host, at, lines);
197}
198
199/// Whether `indent` starts a comment line.
200pub(crate) fn indents_comment_line(indent: &rowan::SyntaxToken<Lang>) -> bool {
201    indent
202        .next_token()
203        .is_some_and(|t| t.kind() == SyntaxKind::COMMENT)
204}
205
206/// The byte order mark `node` starts with, if any.
207fn leading_bom(node: &SyntaxNode) -> Option<SyntaxToken> {
208    node.first_child_or_token()?
209        .into_token()
210        .filter(|t| t.kind() == SyntaxKind::BOM)
211}
212
213/// A virtual AST node for YAML sets (!!set tagged scalars)
214#[derive(Debug, Clone, PartialEq, Eq, Hash)]
215pub struct Set(SyntaxNode);
216
217impl Set {
218    /// Cast a SyntaxNode to a Set only if it's a TAGGED_NODE with !!set tag
219    /// whose content is a mapping.
220    ///
221    /// A `!!set` on anything else is not a valid set and has no members;
222    /// the validator reports it.
223    pub fn cast(node: SyntaxNode) -> Option<Self> {
224        let tagged_node = TaggedNode::cast(node.clone())?;
225        if tagged_node.normalized_tag().as_deref() == Some(crate::tag::SET)
226            && tagged_node.as_mapping().is_some()
227        {
228            Some(Set(node))
229        } else {
230            None
231        }
232    }
233
234    /// Return the inner `Mapping` of this set (the !!set body).
235    fn inner_mapping(&self) -> Option<Mapping> {
236        self.0.children().find_map(Mapping::cast)
237    }
238
239    /// Iterate over the set members, preserving formatting.
240    ///
241    /// Each member is yielded as a [`YamlNode`](crate::YamlNode) wrapping the
242    /// key content node (the scalar, mapping, or sequence that forms the set
243    /// member). The iteration order follows the document order.
244    ///
245    /// To compare members semantically (ignoring quoting style), use
246    /// [`yaml_eq`](crate::yaml_eq) on the returned nodes.
247    pub fn members(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
248        // The members are the keys of the mapping, each with a null value.
249        self.inner_mapping()
250            .into_iter()
251            .flat_map(|m| m.keys().collect::<Vec<_>>())
252    }
253
254    /// Get the number of members in the set.
255    pub fn len(&self) -> usize {
256        self.members().count()
257    }
258
259    /// Check if the set is empty.
260    pub fn is_empty(&self) -> bool {
261        self.members().next().is_none()
262    }
263
264    /// Check if the set contains a specific value.
265    pub fn contains(&self, value: impl crate::AsYaml) -> bool {
266        self.members()
267            .any(|member| crate::as_yaml::yaml_eq(&member, &value))
268    }
269}
270
271// CST abstraction layer - hides wrapper node complexity
272
273/// Extract the actual content from a VALUE or KEY wrapper node
274fn extract_content_node(wrapper: &SyntaxNode) -> Option<SyntaxNode> {
275    use crate::lex::SyntaxKind;
276    match wrapper.kind() {
277        SyntaxKind::VALUE | SyntaxKind::KEY => wrapper.children().next(),
278        _ => Some(wrapper.clone()),
279    }
280}
281
282/// Cast the content of `node` to `T`, looking through a VALUE or KEY
283/// wrapper.
284pub(crate) fn extract<T: AstNode<Language = Lang>>(node: &SyntaxNode) -> Option<T> {
285    extract_content_node(node).and_then(T::cast)
286}
287
288/// Copy a syntax node and all its children recursively to a builder.
289pub(crate) fn copy_node_to_builder(builder: &mut GreenNodeBuilder, node: &SyntaxNode) {
290    builder.start_node(node.kind().into());
291    crate::as_yaml::copy_node_content(builder, node);
292    builder.finish_node();
293}
294
295// Debug helper to dump CST structure
296pub(crate) fn dump_cst_to_string(node: &SyntaxNode, indent: usize) -> String {
297    let mut result = String::new();
298    let indent_str = "  ".repeat(indent);
299
300    for child in node.children_with_tokens() {
301        match child {
302            rowan::NodeOrToken::Node(n) => {
303                result.push_str(&format!("{}{:?}\n", indent_str, n.kind()));
304                result.push_str(&dump_cst_to_string(&n, indent + 1));
305            }
306            rowan::NodeOrToken::Token(t) => {
307                result.push_str(&format!("{}  {:?} {:?}\n", indent_str, t.kind(), t.text()));
308            }
309        }
310    }
311    result
312}
313
314impl Default for YamlFile {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320impl YamlFile {
321    /// Create a new empty YAML document.
322    pub fn new() -> YamlFile {
323        let mut builder = GreenNodeBuilder::new();
324        builder.start_node(SyntaxKind::ROOT.into());
325        builder.finish_node();
326        YamlFile(SyntaxNode::new_root_mut(builder.finish()))
327    }
328
329    /// Parse YAML text, returning a Parse result
330    pub fn parse(text: &str) -> Parse<YamlFile> {
331        Parse::parse_yaml(text)
332    }
333
334    /// Parse YAML from a file path
335    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<YamlFile, crate::YamlError> {
336        let contents = std::fs::read_to_string(path)?;
337        Self::from_str(&contents)
338    }
339
340    /// Parse YAML text, allowing syntax errors.
341    ///
342    /// Returns the parsed tree even if there are parse errors, along with
343    /// a list of error messages. This allows for error-resilient tooling
344    /// that can work with partial or invalid input.
345    ///
346    /// # Example
347    /// ```
348    /// use yaml_edit::YamlFile;
349    ///
350    /// let (yaml_file, errors) = YamlFile::from_str_relaxed("key: [unclosed");
351    /// // Tree is usable even with errors
352    /// assert!(!errors.is_empty());
353    /// assert!(yaml_file.document().is_some());
354    /// ```
355    pub fn from_str_relaxed(s: &str) -> (YamlFile, Vec<String>) {
356        let parsed = YamlFile::parse(s);
357        let errors = parsed.errors();
358        (parsed.tree(), errors)
359    }
360
361    /// Parse YAML from a file path, allowing syntax errors.
362    ///
363    /// Returns the parsed tree even if there are parse errors, along with
364    /// a list of error messages. I/O errors are still returned as `Err`.
365    pub fn from_path_relaxed<P: AsRef<Path>>(
366        path: P,
367    ) -> Result<(YamlFile, Vec<String>), std::io::Error> {
368        let contents = std::fs::read_to_string(path)?;
369        Ok(Self::from_str_relaxed(&contents))
370    }
371
372    /// Read YAML from a `Read` object, allowing syntax errors.
373    ///
374    /// Returns the parsed tree even if there are parse errors, along with
375    /// a list of error messages. I/O errors are still returned as `Err`.
376    pub fn read_relaxed<R: std::io::Read>(
377        mut r: R,
378    ) -> Result<(YamlFile, Vec<String>), std::io::Error> {
379        let mut buf = String::new();
380        r.read_to_string(&mut buf)?;
381        Ok(Self::from_str_relaxed(&buf))
382    }
383
384    /// Get all documents in this YAML file
385    pub fn documents(&self) -> impl Iterator<Item = Document> {
386        self.0.children().filter_map(Document::cast)
387    }
388
389    /// Iterate over the comments in this file, in source order.
390    ///
391    /// This includes comments inside documents as well as any comments between
392    /// or outside documents.
393    ///
394    /// # Example
395    /// ```
396    /// use yaml_edit::YamlFile;
397    /// use std::str::FromStr;
398    ///
399    /// let file = YamlFile::from_str("# header\nkey: value # trailing\n").unwrap();
400    /// let comments: Vec<_> = file.comments().map(|c| c.content().to_string()).collect();
401    /// assert_eq!(comments, vec!["header", "trailing"]);
402    /// ```
403    pub fn comments(&self) -> impl Iterator<Item = Comment> {
404        crate::nodes::comment::comments(&self.0)
405    }
406
407    /// Get the first document in this YAML file, or `None` if there are none.
408    ///
409    /// Most YAML files have exactly one document. Use [`documents`](Self::documents)
410    /// to iterate over all documents in a multi-document file.
411    pub fn document(&self) -> Option<Document> {
412        self.documents().next()
413    }
414
415    /// Ensure this `YamlFile` contains at least one document, creating an empty mapping document if needed.
416    ///
417    /// Returns the first document.
418    pub fn ensure_document(&self) -> Document {
419        if self.documents().next().is_none() {
420            // No document exists, add an empty one
421            let doc = Document::new_mapping();
422            self.push_document(doc);
423        }
424        self.documents()
425            .next()
426            .expect("Document should exist after ensuring")
427    }
428
429    /// Iterate over all YAML directives (e.g. `%YAML 1.2`) in this file.
430    pub fn directives(&self) -> impl Iterator<Item = Directive> {
431        self.0.children().filter_map(Directive::cast)
432    }
433
434    /// Prepend a YAML directive to this file.
435    ///
436    /// `directive_text` should be the full directive line without a trailing
437    /// newline, e.g. `"%YAML 1.2"` or `"%TAG ! tag:example.com,2000:app/"`.
438    /// The directive is inserted before all existing content, and a `---`
439    /// marker is added to the first document if it does not have one, since
440    /// a document following directives must start explicitly.
441    ///
442    /// Note: the parser does not currently enforce that directives appear
443    /// before any document node; callers are responsible for ordering.
444    pub fn add_directive(&self, directive_text: &str) {
445        if let Some(doc) = self.documents().next() {
446            // A byte order mark must stay first in the stream, so one opening
447            // the first document moves up to the stream ahead of the directive.
448            if let Some(bom) = leading_bom(doc.syntax()) {
449                bom.detach();
450                crate::nodes::insert_children(&self.0, 0, vec![bom.into()]);
451            }
452            if !has_child_token(doc.syntax(), |k| k == SyntaxKind::DOC_START) {
453                crate::nodes::insert_children(
454                    doc.syntax(),
455                    0,
456                    vec![
457                        crate::nodes::fresh_token(SyntaxKind::DOC_START, "---").into(),
458                        crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into(),
459                    ],
460                );
461            }
462        }
463
464        let mut builder = GreenNodeBuilder::new();
465        builder.start_node(SyntaxKind::DIRECTIVE.into());
466        builder.token(SyntaxKind::DIRECTIVE.into(), directive_text);
467        builder.finish_node();
468        let directive_node = SyntaxNode::new_root_mut(builder.finish());
469
470        let index = usize::from(leading_bom(&self.0).is_some());
471        crate::nodes::insert_children(
472            &self.0,
473            index,
474            vec![
475                directive_node.into(),
476                crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into(),
477            ],
478        );
479    }
480
481    /// Add a new document to the end of this YAML file
482    ///
483    /// If the file already has documents or directives, the new document is
484    /// started with a `---` marker (unless it has one) on a line of its own.
485    ///
486    /// A byte order mark is only allowed at the start of the stream or in
487    /// front of a document's `---`, so a byte order mark opening `document`
488    /// is kept in front of its `---`. With no earlier document to follow, it
489    /// moves to the start of the stream, or is dropped if the stream already
490    /// starts with one.
491    pub fn push_document(&self, document: Document) {
492        let bom = leading_bom(document.syntax());
493        let needs_start = self
494            .0
495            .children()
496            .any(|n| matches!(n.kind(), SyntaxKind::DOCUMENT | SyntaxKind::DIRECTIVE));
497        if !self.0.text_range().is_empty() && !crate::nodes::trivia::ends_with_line_break(&self.0) {
498            match self.0.last_child_or_token() {
499                Some(rowan::NodeOrToken::Node(last)) if last.kind() == SyntaxKind::DOCUMENT => {
500                    crate::nodes::trivia::terminate_last_line(&last, "\n")
501                }
502                _ => crate::nodes::append_children(
503                    &self.0,
504                    vec![crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into()],
505                ),
506            }
507        }
508        if needs_start && !has_child_token(document.syntax(), |k| k == SyntaxKind::DOC_START) {
509            let index = usize::from(bom.is_some());
510            crate::nodes::insert_children(
511                document.syntax(),
512                index,
513                vec![
514                    crate::nodes::fresh_token(SyntaxKind::DOC_START, "---").into(),
515                    crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into(),
516                ],
517            );
518        }
519        if let Some(bom) = bom.filter(|_| !self.0.text_range().is_empty()) {
520            bom.detach();
521            self.place_bom(bom);
522        }
523        // Before the first document, the stream's blank lines, and comments
524        // unless it starts with `---`, are that document's.
525        let prefix: Vec<_> = self.0.children_with_tokens().collect();
526        if !needs_start
527            && !(has_child_token(document.syntax(), |k| k == SyntaxKind::DOC_START)
528                && has_child_token(&self.0, |k| k == SyntaxKind::COMMENT))
529        {
530            for c in &prefix {
531                c.detach();
532            }
533            crate::nodes::insert_children(document.syntax(), 0, prefix);
534        }
535        crate::nodes::append_children(&self.0, vec![document.0.into()]);
536    }
537
538    /// Put the byte order mark of a document about to be appended where the
539    /// parser reads it.
540    fn place_bom(&self, bom: SyntaxToken) {
541        match self.0.last_child_or_token() {
542            // A marker in front of `---` ends the document before it, unless
543            // that one was closed with `...`.
544            Some(rowan::NodeOrToken::Node(last))
545                if last.kind() == SyntaxKind::DOCUMENT
546                    && !has_child_token(&last, |k| k == SyntaxKind::DOC_END) =>
547            {
548                crate::nodes::append_children(&last, vec![bom.into()]);
549            }
550            _ if self.documents().next().is_some() => {
551                crate::nodes::append_children(&self.0, vec![bom.into()]);
552            }
553            // Directives or comments only: the marker belongs at the start of
554            // the stream, which needs only one.
555            _ if leading_bom(&self.0).is_some() => {}
556            _ => crate::nodes::insert_children(&self.0, 0, vec![bom.into()]),
557        }
558    }
559
560    /// Set a key-value pair in the first document's mapping.
561    ///
562    /// If the key exists its value is replaced; if not, a new entry is appended.
563    /// If the file has no document yet, one is added (see
564    /// [`ensure_document`](Self::ensure_document)).
565    ///
566    /// Returns `false`, leaving the file unchanged, if the first document's
567    /// root is a sequence or another non-mapping node (see [`Document::set`]).
568    ///
569    /// Mutates in place despite `&self` (see crate docs on interior mutability).
570    pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) -> bool {
571        self.ensure_document().set(key, value)
572    }
573
574    /// Insert a key-value pair immediately after `after_key` in the first document.
575    ///
576    /// Delegates to [`Document::insert_after`], which in turn calls
577    /// [`Mapping::insert_after`]. If `key` already exists it is updated
578    /// in-place rather than moved. Returns `false` if `after_key` is not found
579    /// or the document is not a mapping.
580    ///
581    /// Mutates in place despite `&self` (see crate docs on interior mutability).
582    pub fn insert_after(
583        &self,
584        after_key: impl crate::AsYaml,
585        key: impl crate::AsYaml,
586        value: impl crate::AsYaml,
587    ) -> bool {
588        self.document()
589            .is_some_and(|doc| doc.insert_after(after_key, key, value))
590    }
591
592    /// Insert a key-value pair immediately before `before_key` in the first document.
593    ///
594    /// Delegates to [`Document::insert_before`], which in turn calls
595    /// [`Mapping::insert_before`]. If `key` already exists it is updated
596    /// in-place rather than moved. Returns `false` if `before_key` is not found
597    /// or the document is not a mapping.
598    ///
599    /// Mutates in place despite `&self` (see crate docs on interior mutability).
600    pub fn insert_before(
601        &self,
602        before_key: impl crate::AsYaml,
603        key: impl crate::AsYaml,
604        value: impl crate::AsYaml,
605    ) -> bool {
606        self.document()
607            .is_some_and(|doc| doc.insert_before(before_key, key, value))
608    }
609
610    /// Move a key-value pair to immediately after `after_key` in the first document.
611    ///
612    /// Delegates to [`Document::move_after`]. If `key` already exists it is
613    /// **removed** from its current position and re-inserted after `after_key`.
614    /// Returns `false` if `after_key` is not found or the document is not a mapping.
615    ///
616    /// Use [`insert_after`](Self::insert_after) if you want an existing entry to be
617    /// updated in-place rather than moved.
618    ///
619    /// Mutates in place despite `&self` (see crate docs on interior mutability).
620    pub fn move_after(
621        &self,
622        after_key: impl crate::AsYaml,
623        key: impl crate::AsYaml,
624        value: impl crate::AsYaml,
625    ) -> bool {
626        self.document()
627            .is_some_and(|doc| doc.move_after(after_key, key, value))
628    }
629
630    /// Move a key-value pair to immediately before `before_key` in the first document.
631    ///
632    /// Delegates to [`Document::move_before`]. If `key` already exists it is
633    /// **removed** from its current position and re-inserted before `before_key`.
634    /// Returns `false` if `before_key` is not found or the document is not a mapping.
635    ///
636    /// Use [`insert_before`](Self::insert_before) if you want an existing entry to be
637    /// updated in-place rather than moved.
638    ///
639    /// Mutates in place despite `&self` (see crate docs on interior mutability).
640    pub fn move_before(
641        &self,
642        before_key: impl crate::AsYaml,
643        key: impl crate::AsYaml,
644        value: impl crate::AsYaml,
645    ) -> bool {
646        self.document()
647            .is_some_and(|doc| doc.move_before(before_key, key, value))
648    }
649
650    /// Insert a key-value pair at a specific index (0-based) in the first document.
651    ///
652    /// Delegates to [`Document::insert_at_index`]. If `key` already exists it
653    /// is updated in-place rather than moved. If `index` is out of bounds the
654    /// entry is appended at the end. If the first document has no root node,
655    /// a mapping is created, as is a document if the file has none (see
656    /// [`ensure_document`](Self::ensure_document)).
657    ///
658    /// Returns `false`, leaving the file unchanged, if the first document's
659    /// root is a sequence or another non-mapping node.
660    ///
661    /// Mutates in place despite `&self` (see crate docs on interior mutability).
662    pub fn insert_at_index(
663        &self,
664        index: usize,
665        key: impl crate::AsYaml,
666        value: impl crate::AsYaml,
667    ) -> bool {
668        self.ensure_document().insert_at_index(index, key, value)
669    }
670}
671
672impl FromStr for YamlFile {
673    type Err = crate::YamlError;
674
675    fn from_str(s: &str) -> Result<Self, Self::Err> {
676        let parsed = YamlFile::parse(s);
677        if !parsed.positioned_errors().is_empty() {
678            let first = &parsed.positioned_errors()[0];
679            let lc = crate::byte_offset_to_line_column(s, first.range.start as usize);
680            return Err(crate::YamlError::Parse {
681                message: first.message.clone(),
682                line: Some(lc.line),
683                column: Some(lc.column),
684            });
685        }
686        Ok(parsed.tree())
687    }
688}
689
690pub(crate) use crate::parser::parse;
691
692// Editing methods for Sequence
693
694#[cfg(test)]
695mod tests {
696    // Tests for next_flow_element_is_implicit_mapping lookahead function
697    mod lookahead_tests {
698        use crate::lex::lex;
699        use crate::parser::implicit_mapping_starts;
700
701        /// Helper to test lookahead without creating full parser
702        fn check_implicit_mapping(yaml: &str) -> bool {
703            let flow = format!("[{yaml}]");
704            let tokens = lex(&flow);
705            let kinds: Vec<_> = tokens[1..tokens.len() - 1]
706                .iter()
707                .map(|(kind, _)| *kind)
708                .collect();
709            implicit_mapping_starts(&kinds).first() == Some(&true)
710        }
711
712        #[test]
713        fn test_simple_implicit_mapping() {
714            // Looking at: 'key' : value (inside [ ... ])
715            // Should detect colon at depth 0
716            assert!(check_implicit_mapping("'key' : value"));
717        }
718
719        #[test]
720        fn test_simple_value_no_mapping() {
721            // Looking at: value, ... (stops at comma, no colon)
722            assert!(!check_implicit_mapping("value ,"));
723        }
724
725        #[test]
726        fn test_value_with_comma() {
727            // Looking at: value, more (comma at depth 0, not a mapping)
728            assert!(!check_implicit_mapping("value , more"));
729        }
730
731        #[test]
732        fn test_nested_sequence_as_key() {
733            // Looking at: [a, b]: value (colon after nested sequence completes)
734            assert!(check_implicit_mapping("[a, b]: value"));
735        }
736
737        #[test]
738        fn test_nested_mapping_not_implicit() {
739            // Looking at: {a: 1}, b (colon is inside braces at depth 1, not depth 0)
740            assert!(!check_implicit_mapping("{a: 1}, b"));
741        }
742
743        #[test]
744        fn test_deeply_nested_key() {
745            // Looking at: [[a]]: value (multiple levels of nesting)
746            assert!(check_implicit_mapping("[[a]]: value"));
747        }
748
749        #[test]
750        fn test_with_whitespace() {
751            // Looking at: 'key'  :  value (whitespace shouldn't affect detection)
752            assert!(check_implicit_mapping("'key'  :  value"));
753        }
754
755        #[test]
756        fn test_mapping_value_in_sequence() {
757            // Looking at: a, {key: value} (second element has colon but inside braces)
758            // First element is just "a" before comma
759            assert!(!check_implicit_mapping("a, {key: value}"));
760        }
761
762        #[test]
763        fn test_multiple_colons() {
764            // Looking at: key1: value1, key2: value2 (first element is mapping)
765            assert!(check_implicit_mapping("key1: value1, key2: value2"));
766        }
767
768        #[test]
769        fn test_url_not_mapping() {
770            // Looking at: http://example.com (colon in URL, but no space after)
771            // Lexer should tokenize this as single string token
772            assert!(!check_implicit_mapping("http://example.com"));
773        }
774    }
775}