Skip to main content

yaml_edit/nodes/
document.rs

1use super::{Lang, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::error::YamlResult;
4use crate::lex::SyntaxKind;
5use crate::yaml::YamlFile;
6use rowan::ast::AstNode;
7use rowan::GreenNodeBuilder;
8use std::path::Path;
9use std::str::FromStr;
10
11ast_node!(Document, DOCUMENT, "A single YAML document");
12
13impl Document {
14    /// Create a new document
15    pub fn new() -> Document {
16        let mut builder = GreenNodeBuilder::new();
17        builder.start_node(SyntaxKind::DOCUMENT.into());
18        // Add the document start marker "---"
19        builder.token(SyntaxKind::DOC_START.into(), "---");
20        builder.token(SyntaxKind::NEWLINE.into(), "\n");
21        builder.finish_node();
22        Document(SyntaxNode::new_root_mut(builder.finish()))
23    }
24
25    /// Create a new document with an empty mapping
26    pub fn new_mapping() -> Document {
27        let mut builder = GreenNodeBuilder::new();
28        builder.start_node(SyntaxKind::DOCUMENT.into());
29        // Don't add document start marker "---" for programmatically created documents
30        builder.start_node(SyntaxKind::MAPPING.into());
31        builder.finish_node(); // End MAPPING
32        builder.finish_node(); // End DOCUMENT
33        Document(SyntaxNode::new_root_mut(builder.finish()))
34    }
35
36    /// Load a document from a file
37    ///
38    /// Returns an error if the file contains multiple documents.
39    /// For multi-document YAML files, parse manually with `YamlFile::from_str()`.
40    ///
41    /// This follows the standard Rust naming convention of `from_file()`
42    /// to pair with `to_file()`.
43    pub fn from_file<P: AsRef<Path>>(path: P) -> YamlResult<Document> {
44        let content = std::fs::read_to_string(path)?;
45        Self::from_str(&content)
46    }
47
48    /// Parse a YAML string, allowing syntax errors.
49    ///
50    /// Returns the first document even if there are parse errors, along with
51    /// a list of error messages. This allows for error-resilient tooling
52    /// that can work with partial or invalid input.
53    ///
54    /// Unlike [`Document::from_str`], this does not return an error if the
55    /// input contains multiple documents - only the first document is returned.
56    ///
57    /// # Example
58    /// ```
59    /// use yaml_edit::Document;
60    ///
61    /// let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
62    /// // Tree is usable even with errors
63    /// assert!(!errors.is_empty());
64    /// assert!(doc.as_mapping().is_some());
65    /// ```
66    pub fn from_str_relaxed(s: &str) -> (Document, Vec<String>) {
67        let parsed = YamlFile::parse(s);
68        let errors = parsed.errors();
69        let first = parsed.tree().documents().next().unwrap_or_default();
70        (first, errors)
71    }
72
73    /// Parse a document from a YAML string, discarding stream-level content.
74    ///
75    /// Like [`Document::from_str`] but succeeds when the input contains
76    /// stream-level comments or `%YAML`/`%TAG` directives outside the document
77    /// body. Those elements are silently dropped: the returned `Document` will
78    /// not round-trip to the original text. Use this when you only need the
79    /// document's content and are happy to lose stream-scoped presentation.
80    ///
81    /// Multiple documents are still rejected - use [`YamlFile::from_str`] for
82    /// multi-document streams.
83    ///
84    /// # Example
85    /// ```
86    /// use yaml_edit::Document;
87    ///
88    /// let yaml = "# generated by tool\n---\nkey: value\n";
89    /// let doc = Document::from_str_ignoring_comments(yaml).unwrap();
90    /// assert_eq!(doc.to_string(), "---\nkey: value\n");
91    /// ```
92    pub fn from_str_ignoring_comments(s: &str) -> YamlResult<Document> {
93        let parsed = YamlFile::parse(s);
94
95        if !parsed.positioned_errors().is_empty() {
96            let first_error = &parsed.positioned_errors()[0];
97            let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
98            return Err(crate::error::YamlError::Parse {
99                message: first_error.message.clone(),
100                line: Some(lc.line),
101                column: Some(lc.column),
102            });
103        }
104
105        let mut docs = parsed.tree().documents();
106        let first = docs.next().unwrap_or_default();
107
108        if docs.next().is_some() {
109            return Err(crate::error::YamlError::InvalidOperation {
110                operation: "Document::from_str_ignoring_comments".to_string(),
111                reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
112            });
113        }
114
115        Ok(first)
116    }
117
118    /// Load a document from a file, allowing syntax errors.
119    ///
120    /// Returns the first document even if there are parse errors, along with
121    /// a list of error messages. I/O errors are still returned as `Err`.
122    pub fn from_file_relaxed<P: AsRef<Path>>(
123        path: P,
124    ) -> Result<(Document, Vec<String>), std::io::Error> {
125        let content = std::fs::read_to_string(path)?;
126        Ok(Self::from_str_relaxed(&content))
127    }
128
129    /// Write the document to a file, creating directories as needed
130    ///
131    /// This follows the standard Rust naming convention of `to_file()`
132    /// to pair with `from_file()`.
133    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> YamlResult<()> {
134        let path = path.as_ref();
135        if let Some(parent) = path.parent() {
136            std::fs::create_dir_all(parent)?;
137        }
138        let mut content = self.to_string();
139        // Ensure the file ends with a newline
140        if !content.ends_with('\n') {
141            content.push('\n');
142        }
143        std::fs::write(path, content)?;
144        Ok(())
145    }
146
147    /// Get the root node of this document (could be mapping, sequence, scalar, or alias)
148    pub(crate) fn root_node(&self) -> Option<SyntaxNode> {
149        self.0.children().find(|child| {
150            matches!(
151                child.kind(),
152                SyntaxKind::MAPPING
153                    | SyntaxKind::SEQUENCE
154                    | SyntaxKind::SCALAR
155                    | SyntaxKind::ALIAS
156                    | SyntaxKind::TAGGED_NODE
157            )
158        })
159    }
160
161    fn root_yaml_node(&self) -> Option<crate::as_yaml::YamlNode> {
162        self.root_node()
163            .and_then(crate::as_yaml::YamlNode::from_syntax)
164    }
165
166    /// The document's root value, stepping through a tag that annotates it.
167    ///
168    /// A tag says what the node beneath it means, so `!!map\n a: 1\n` is
169    /// still a mapping. Without this the accessors below all answer `None`
170    /// for an annotated root, leaving it unreachable.
171    fn root_value_node(&self) -> Option<SyntaxNode> {
172        let node = self.root_node()?;
173        if node.kind() != SyntaxKind::TAGGED_NODE {
174            return Some(node);
175        }
176        node.children().find(|child| {
177            matches!(
178                child.kind(),
179                SyntaxKind::MAPPING | SyntaxKind::SEQUENCE | SyntaxKind::SCALAR | SyntaxKind::ALIAS
180            )
181        })
182    }
183
184    /// Get this document as a mapping, if it is one.
185    ///
186    /// Note: `Mapping` supports mutation even though this method takes `&self`.
187    /// Mutations are applied directly to the underlying syntax tree via rowan's
188    /// persistent data structures. All references to the tree will see the changes.
189    pub fn as_mapping(&self) -> Option<Mapping> {
190        self.root_value_node().and_then(Mapping::cast)
191    }
192
193    /// Get this document's root value as a sequence, or `None` if it isn't one.
194    pub fn as_sequence(&self) -> Option<Sequence> {
195        self.root_value_node().and_then(Sequence::cast)
196    }
197
198    /// Get this document's root value as a scalar, or `None` if it isn't one.
199    pub fn as_scalar(&self) -> Option<Scalar> {
200        self.root_value_node().and_then(Scalar::cast)
201    }
202
203    /// Get this document's root value as a tagged node, or `None` if it
204    /// carries no tag.
205    pub fn as_tagged(&self) -> Option<crate::yaml::TaggedNode> {
206        self.root_node()
207            .filter(|node| node.kind() == SyntaxKind::TAGGED_NODE)
208            .and_then(crate::yaml::TaggedNode::cast)
209    }
210
211    /// Returns `true` if this document is a mapping that contains the given key.
212    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
213        self.as_mapping().is_some_and(|m| m.contains_key(key))
214    }
215
216    /// Get the value for a key from the document's root mapping.
217    ///
218    /// Returns `None` if the document is not a mapping or the key doesn't exist.
219    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
220        self.as_mapping().and_then(|m| m.get(key))
221    }
222
223    /// Get the raw syntax node for a key in the document's root mapping.
224    ///
225    /// Returns `None` if the document is not a mapping or the key doesn't exist.
226    /// Prefer [`get`](Self::get) for most use cases; this is for advanced CST access.
227    pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
228        self.as_mapping().and_then(|m| m.get_node(key))
229    }
230
231    /// Set a key in the document's root mapping.
232    ///
233    /// If the document has no root node, a mapping is created first.
234    ///
235    /// Returns `true` if the key was set. Returns `false` if the root is a
236    /// sequence or another non-mapping node (the document is left unchanged).
237    pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) -> bool {
238        self.edit_root_mapping(|mapping| mapping.set(key, value))
239    }
240
241    /// Run `edit` on the root mapping, created first if the document has no
242    /// root. Returns `false`, leaving the document unchanged, if the root is
243    /// another node.
244    fn edit_root_mapping(&self, edit: impl FnOnce(&Mapping)) -> bool {
245        self.try_edit_root_mapping(|mapping| {
246            edit(mapping);
247            Ok::<_, std::convert::Infallible>(())
248        })
249        .is_some()
250    }
251
252    /// Like [`edit_root_mapping`](Self::edit_root_mapping), but `edit` can
253    /// fail. A root mapping created for it is only attached if it succeeds.
254    /// Returns `None` if the root is another node.
255    pub(crate) fn try_edit_root_mapping<T, E>(
256        &self,
257        edit: impl FnOnce(&Mapping) -> Result<T, E>,
258    ) -> Option<Result<T, E>> {
259        if let Some(mapping) = self.as_mapping() {
260            return Some(edit(&mapping));
261        }
262        if self.root_node().is_some() {
263            return None;
264        }
265        let mapping = Mapping::new();
266        let result = edit(&mapping);
267        if result.is_ok() {
268            self.attach_root(mapping.0);
269        }
270        Some(result)
271    }
272
273    /// Set a key-value pair with field ordering support.
274    ///
275    /// If the key exists, updates its value. If the key doesn't exist, inserts it
276    /// at the correct position based on the provided field order.
277    /// Fields not in the order list are placed at the end.
278    ///
279    /// Returns `true` if the key was set. Returns `false` if the root is a
280    /// sequence or another non-mapping node (the document is left unchanged).
281    pub fn set_with_field_order<I, K>(
282        &self,
283        key: impl crate::AsYaml,
284        value: impl crate::AsYaml,
285        field_order: I,
286    ) -> bool
287    where
288        I: IntoIterator<Item = K>,
289        K: crate::AsYaml,
290    {
291        self.edit_root_mapping(|mapping| mapping.set_with_field_order(key, value, field_order))
292    }
293
294    /// Remove a key from the document (assumes document is a mapping).
295    ///
296    /// Returns `Some(entry)` if the key existed and was removed, or `None` if
297    /// the key was not found or the document is not a mapping. The returned
298    /// [`MappingEntry`](super::MappingEntry) is detached from the tree; callers can inspect its key
299    /// and value or re-insert it elsewhere.
300    pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
301        self.as_mapping()?.remove(key)
302    }
303
304    /// Get all key nodes from the document (assumes document is a mapping)
305    pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
306        self.as_mapping()
307            .into_iter()
308            .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
309    }
310
311    /// Iterate over all keys in the document as [`YamlNode`](crate::as_yaml::YamlNode)s.
312    ///
313    /// Each key is a [`YamlNode`](crate::as_yaml::YamlNode) wrapping the
314    /// underlying CST node.  Assumes the document is a mapping; yields nothing
315    /// if it is not.
316    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
317        self.key_nodes().filter_map(|k| crate::nodes::held_node(&k))
318    }
319
320    /// Check if the document is empty
321    pub fn is_empty(&self) -> bool {
322        self.as_mapping().map_or(true, |m| m.is_empty())
323    }
324
325    /// Create a document from a mapping
326    pub fn from_mapping(mapping: Mapping) -> Self {
327        // Create a document directly from the mapping syntax node to avoid recursion
328        let mut builder = GreenNodeBuilder::new();
329        builder.start_node(SyntaxKind::DOCUMENT.into());
330        // Add the document start marker "---"
331        builder.token(SyntaxKind::DOC_START.into(), "---");
332        builder.token(SyntaxKind::NEWLINE.into(), "\n");
333
334        crate::yaml::copy_node_to_builder(&mut builder, &mapping.0);
335        builder.finish_node(); // End DOCUMENT
336
337        Document(SyntaxNode::new_root_mut(builder.finish()))
338    }
339
340    /// Insert a key-value pair immediately after `after_key` in this document's mapping.
341    ///
342    /// If `key` already exists, its value is updated in-place and it stays at its
343    /// current position (it is **not** moved). Returns `false` if `after_key` is not
344    /// found or the document is not a mapping.
345    ///
346    /// Use [`move_after`](Self::move_after) if you want an existing entry to be
347    /// removed from its current position and re-inserted after `after_key`.
348    pub fn insert_after(
349        &self,
350        after_key: impl crate::AsYaml,
351        key: impl crate::AsYaml,
352        value: impl crate::AsYaml,
353    ) -> bool {
354        self.as_mapping()
355            .is_some_and(|mapping| mapping.insert_after(after_key, key, value))
356    }
357
358    /// Move a key-value pair to immediately after `after_key` in this document's mapping.
359    ///
360    /// If `key` already exists, it is **removed** from its current position and
361    /// re-inserted after `after_key` with the new value. Returns `false` if
362    /// `after_key` is not found or the document is not a mapping.
363    ///
364    /// Use [`insert_after`](Self::insert_after) if you want an existing entry to be
365    /// updated in-place rather than moved.
366    pub fn move_after(
367        &self,
368        after_key: impl crate::AsYaml,
369        key: impl crate::AsYaml,
370        value: impl crate::AsYaml,
371    ) -> bool {
372        self.as_mapping()
373            .is_some_and(|mapping| mapping.move_after(after_key, key, value))
374    }
375
376    /// Insert a key-value pair immediately before `before_key` in this document's mapping.
377    ///
378    /// If `key` already exists, its value is updated in-place and it stays at its
379    /// current position (it is **not** moved). Returns `false` if `before_key` is not
380    /// found or the document is not a mapping.
381    ///
382    /// Use [`move_before`](Self::move_before) if you want an existing entry to be
383    /// removed from its current position and re-inserted before `before_key`.
384    pub fn insert_before(
385        &self,
386        before_key: impl crate::AsYaml,
387        key: impl crate::AsYaml,
388        value: impl crate::AsYaml,
389    ) -> bool {
390        self.as_mapping()
391            .is_some_and(|mapping| mapping.insert_before(before_key, key, value))
392    }
393
394    /// Move a key-value pair to immediately before `before_key` in this document's mapping.
395    ///
396    /// If `key` already exists, it is **removed** from its current position and
397    /// re-inserted before `before_key` with the new value. Returns `false` if
398    /// `before_key` is not found or the document is not a mapping.
399    ///
400    /// Use [`insert_before`](Self::insert_before) if you want an existing entry to be
401    /// updated in-place rather than moved.
402    pub fn move_before(
403        &self,
404        before_key: impl crate::AsYaml,
405        key: impl crate::AsYaml,
406        value: impl crate::AsYaml,
407    ) -> bool {
408        self.as_mapping()
409            .is_some_and(|mapping| mapping.move_before(before_key, key, value))
410    }
411
412    /// Insert a key-value pair at a specific index.
413    ///
414    /// Delegates to [`Mapping::insert_at_index`]. If the document has no
415    /// root node, a mapping is created first.
416    ///
417    /// Returns `true` if the key was inserted. Returns `false` if the root is
418    /// a sequence or another non-mapping node (the document is left
419    /// unchanged), as [`set`](Self::set) does.
420    pub fn insert_at_index(
421        &self,
422        index: usize,
423        key: impl crate::AsYaml,
424        value: impl crate::AsYaml,
425    ) -> bool {
426        self.edit_root_mapping(|mapping| mapping.insert_at_index(index, key, value))
427    }
428
429    /// Splice `root` into this document, which has none, where the parser
430    /// reads it: on a line of its own after the `---` marker and any comment
431    /// lines, and before a `...` marker.
432    fn attach_root(&self, root: SyntaxNode) {
433        let children: Vec<_> = self.0.children_with_tokens().collect();
434        let index = children
435            .iter()
436            .position(|c| c.kind() == SyntaxKind::DOC_END)
437            .unwrap_or(children.len());
438        let mut new_children = Vec::new();
439        if index > 0
440            && !matches!(
441                children[index - 1].kind(),
442                SyntaxKind::NEWLINE | SyntaxKind::BOM
443            )
444        {
445            new_children.push(crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into());
446        }
447        new_children.push(root.into());
448        super::insert_children(&self.0, index, new_children);
449    }
450
451    /// Get the scalar value for `key` as a decoded `String`.
452    ///
453    /// Returns `None` if the key does not exist or its value is not a scalar
454    /// (i.e. the value is a sequence or mapping). Quotes are stripped and
455    /// escape sequences are processed (e.g. `\"` → `"`, `\n` → newline).
456    /// For tagged scalars (e.g. `!!str foo`) the tag is ignored and the
457    /// value part is returned.
458    pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
459        // get_node() returns the content node (SCALAR/MAPPING/SEQUENCE/TAGGED_NODE),
460        // already unwrapped from the VALUE wrapper.
461        let content = self.get_node(key)?;
462        if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
463            tagged_node.value().map(|s| s.as_string())
464        } else {
465            // Returns None if content is a sequence or mapping (not a scalar).
466            Scalar::cast(content).map(|s| s.as_string())
467        }
468    }
469
470    /// Get the number of top-level key-value pairs in this document.
471    ///
472    /// Returns `0` if the document is not a mapping or is empty.
473    pub fn len(&self) -> usize {
474        self.as_mapping().map_or(0, |m| m.len())
475    }
476
477    /// Get a nested mapping value for a key.
478    ///
479    /// Returns `None` if the key doesn't exist or its value is not a mapping.
480    /// A tag on the value is looked through.
481    pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
482        self.get(key)?.mapping_content()
483    }
484
485    /// Get a nested sequence value for a key.
486    ///
487    /// Returns `None` if the key doesn't exist or its value is not a sequence.
488    /// A tag on the value is looked through.
489    pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
490        self.get(key)?.sequence_content()
491    }
492
493    /// Rename a top-level key while preserving its value and formatting.
494    ///
495    /// The new key is automatically escaped/quoted as needed. Returns `true` if
496    /// the key was found and renamed, `false` if `old_key` does not exist.
497    /// An existing `new_key` entry is replaced; see [`Mapping::rename_key`].
498    pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
499        self.as_mapping()
500            .is_some_and(|m| m.rename_key(old_key, new_key))
501    }
502
503    /// Returns `true` if `key` exists and its value is a sequence.
504    pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
505        self.get(key)
506            .is_some_and(|node| node.as_sequence().is_some())
507    }
508
509    /// Reorder fields in the document's root mapping according to the specified order.
510    ///
511    /// Fields not in the order list will appear after the ordered fields, in their
512    /// original relative order.
513    ///
514    /// Returns `false`, leaving the document unchanged, if it is not a
515    /// mapping.
516    pub fn reorder_fields<I, K>(&self, order: I) -> bool
517    where
518        I: IntoIterator<Item = K>,
519        K: crate::AsYaml,
520    {
521        self.as_mapping()
522            .map(|mapping| mapping.reorder_fields(order))
523            .is_some()
524    }
525
526    /// Validate this document against a YAML schema
527    ///
528    /// # Examples
529    ///
530    /// ```rust
531    /// use yaml_edit::{Document, Schema, SchemaValidator};
532    ///
533    /// let yaml = r#"
534    /// name: "John"
535    /// age: 30
536    /// active: true
537    /// "#;
538    ///
539    /// let parsed = yaml.parse::<yaml_edit::YamlFile>().unwrap();
540    /// let doc = parsed.document().unwrap();
541    ///
542    /// // Validate against JSON schema
543    /// let validator = SchemaValidator::json();
544    /// match doc.validate_schema(&validator) {
545    ///     Ok(_) => println!("Valid JSON schema"),
546    ///     Err(errors) => {
547    ///         for error in errors {
548    ///             println!("Validation error: {}", error);
549    ///         }
550    ///     }
551    /// }
552    /// ```
553    pub fn validate_schema(
554        &self,
555        validator: &crate::schema::SchemaValidator,
556    ) -> crate::schema::ValidationResult<()> {
557        validator.validate(self)
558    }
559
560    /// Get the byte offset range of this document in the source text.
561    ///
562    /// Returns the start and end byte offsets as a `TextPosition`.
563    ///
564    /// # Examples
565    ///
566    /// ```
567    /// use yaml_edit::Document;
568    /// use std::str::FromStr;
569    ///
570    /// let text = "name: Alice\nage: 30";
571    /// let doc = Document::from_str(text).unwrap();
572    /// let range = doc.byte_range();
573    /// assert_eq!(range.start, 0);
574    /// ```
575    pub fn byte_range(&self) -> crate::TextPosition {
576        self.0.text_range().into()
577    }
578
579    /// Get the line and column where this document starts.
580    ///
581    /// Requires the original source text to calculate line/column from byte offsets.
582    /// Line and column numbers are 1-indexed.
583    ///
584    /// # Arguments
585    ///
586    /// * `source_text` - The original YAML source text
587    ///
588    /// # Examples
589    ///
590    /// ```
591    /// use yaml_edit::Document;
592    /// use std::str::FromStr;
593    ///
594    /// let text = "name: Alice";
595    /// let doc = Document::from_str(text).unwrap();
596    /// let pos = doc.start_position(text);
597    /// assert_eq!(pos.line, 1);
598    /// assert_eq!(pos.column, 1);
599    /// ```
600    pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
601        let range = self.byte_range();
602        crate::byte_offset_to_line_column(source_text, range.start as usize)
603    }
604
605    /// Get the line and column where this document ends.
606    ///
607    /// Requires the original source text to calculate line/column from byte offsets.
608    /// Line and column numbers are 1-indexed.
609    ///
610    /// # Arguments
611    ///
612    /// * `source_text` - The original YAML source text
613    pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
614        let range = self.byte_range();
615        crate::byte_offset_to_line_column(source_text, range.end as usize)
616    }
617
618    /// Iterate over the comments in this document, in source order.
619    ///
620    /// # Example
621    /// ```
622    /// use yaml_edit::Document;
623    /// use std::str::FromStr;
624    ///
625    /// let doc = Document::from_str("key: value # trailing\n# standalone\n").unwrap();
626    /// let comments: Vec<_> = doc.comments().map(|c| c.content().to_string()).collect();
627    /// assert_eq!(comments, vec!["trailing", "standalone"]);
628    /// ```
629    pub fn comments(&self) -> impl Iterator<Item = super::Comment> {
630        super::comment::comments(&self.0)
631    }
632}
633
634impl Default for Document {
635    fn default() -> Self {
636        Self::new()
637    }
638}
639
640impl FromStr for Document {
641    type Err = crate::error::YamlError;
642
643    /// Parse a document from a YAML string.
644    ///
645    /// Returns an error if:
646    /// - the string contains multiple documents, or
647    /// - the string contains stream-level content (comments or `%YAML`/`%TAG`
648    ///   directives outside the document body).
649    ///
650    /// Per the YAML 1.2 spec, comments before the leading `---` (or after the
651    /// trailing `...`) and top-level directives belong to the stream, not the
652    /// document, so `Document` cannot round-trip them. Use
653    /// [`YamlFile::from_str`] if you need to preserve them, or
654    /// [`Document::from_str_ignoring_comments`] if you're happy to drop them.
655    ///
656    /// A byte order mark and blank lines before the document are kept in it,
657    /// as are comments before a document without `---`, and input with no
658    /// document at all gives an empty document holding its byte order mark
659    /// and blank lines, so the result always renders back to `s`.
660    ///
661    /// # Example
662    /// ```
663    /// use yaml_edit::Document;
664    /// use std::str::FromStr;
665    ///
666    /// let doc = Document::from_str("key: value").unwrap();
667    /// assert!(doc.as_mapping().is_some());
668    /// ```
669    fn from_str(s: &str) -> Result<Self, Self::Err> {
670        let parsed = YamlFile::parse(s);
671
672        if !parsed.positioned_errors().is_empty() {
673            let first_error = &parsed.positioned_errors()[0];
674            let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
675            return Err(crate::error::YamlError::Parse {
676                message: first_error.message.clone(),
677                line: Some(lc.line),
678                column: Some(lc.column),
679            });
680        }
681
682        let tree = parsed.tree();
683        let mut docs = tree.documents();
684        let first = docs.next();
685
686        if docs.next().is_some() {
687            return Err(crate::error::YamlError::InvalidOperation {
688                operation: "Document::from_str".to_string(),
689                reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
690            });
691        }
692
693        // Per the YAML 1.2 spec, comments and directives outside a document
694        // (before the leading `---` or after the trailing `...`) belong to the
695        // stream, not to any document. `Document` cannot round-trip them, so
696        // reject rather than silently discard. Callers who need to preserve
697        // stream-level content should use `YamlFile::from_str` instead.
698        for child in tree.0.children_with_tokens() {
699            match child {
700                rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::COMMENT => {
701                    return Err(crate::error::YamlError::InvalidOperation {
702                        operation: "Document::from_str".to_string(),
703                        reason: "Input contains stream-level comments outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
704                    });
705                }
706                rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::DIRECTIVE => {
707                    return Err(crate::error::YamlError::InvalidOperation {
708                        operation: "Document::from_str".to_string(),
709                        reason: "Input contains YAML directives outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
710                    });
711                }
712                // Tokens the parser could not attach to the document are
713                // swept into a stream-level ERROR node without a reported
714                // error. Returning `first` would drop them from the output,
715                // so say so instead of losing the text.
716                rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::ERROR => {
717                    let lc = crate::byte_offset_to_line_column(s, n.text_range().start().into());
718                    return Err(crate::error::YamlError::Parse {
719                        message: format!(
720                            "could not be parsed as part of the document: {:?}",
721                            n.text().to_string()
722                        ),
723                        line: Some(lc.line),
724                        column: Some(lc.column),
725                    });
726                }
727                _ => {}
728            }
729        }
730
731        first.map_or_else(|| Self::from_blank_stream(&tree), Ok)
732    }
733}
734
735impl Document {
736    /// An empty document holding the BOM and blank lines of a stream with
737    /// no document in it, so that it renders back to the same text.
738    fn from_blank_stream(tree: &YamlFile) -> YamlResult<Document> {
739        let mut builder = GreenNodeBuilder::new();
740        builder.start_node(SyntaxKind::DOCUMENT.into());
741        for child in tree.0.children_with_tokens() {
742            let token = child.as_token().filter(|t| {
743                matches!(
744                    t.kind(),
745                    SyntaxKind::BOM
746                        | SyntaxKind::WHITESPACE
747                        | SyntaxKind::NEWLINE
748                        | SyntaxKind::INDENT
749                )
750            });
751            let Some(token) = token else {
752                return Err(crate::error::YamlError::InvalidOperation {
753                    operation: "Document::from_str".to_string(),
754                    reason: format!(
755                        "Input contains stream-level content outside a document: {:?}. Use YamlFile::from_str() to preserve it.",
756                        child.to_string()
757                    ),
758                });
759            };
760            builder.token(token.kind().into(), token.text());
761        }
762        builder.finish_node();
763        Ok(Document(SyntaxNode::new_root_mut(builder.finish())))
764    }
765}
766
767/// As a value, a document is its root node, or null if it has none.
768///
769/// Only the root is copied: comments outside it, such as a comment line
770/// before it or after `---`, are not.
771impl AsYaml for Document {
772    fn as_node(&self) -> Option<&SyntaxNode> {
773        Some(&self.0)
774    }
775
776    fn kind(&self) -> YamlKind {
777        self.root_yaml_node()
778            .map_or(YamlKind::Scalar, |root| AsYaml::kind(&root))
779    }
780
781    fn build_content(
782        &self,
783        builder: &mut rowan::GreenNodeBuilder,
784        indent: usize,
785        flow_context: bool,
786    ) -> bool {
787        match self.root_yaml_node() {
788            Some(root) => root.build_content(builder, indent, flow_context),
789            None => crate::ScalarValue::null().build_content(builder, indent, flow_context),
790        }
791    }
792
793    fn is_inline(&self) -> bool {
794        self.root_yaml_node().map_or(true, |root| root.is_inline())
795    }
796
797    fn value_node(&self) -> Option<SyntaxNode> {
798        self.root_node()
799    }
800}