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        if let Some(mapping) = self.as_mapping() {
246            edit(&mapping);
247            return true;
248        }
249        if self.root_node().is_some() {
250            return false;
251        }
252        let mapping = Mapping::new();
253        edit(&mapping);
254        self.attach_root(mapping.0);
255        true
256    }
257
258    /// Set a key-value pair with field ordering support.
259    ///
260    /// If the key exists, updates its value. If the key doesn't exist, inserts it
261    /// at the correct position based on the provided field order.
262    /// Fields not in the order list are placed at the end.
263    ///
264    /// Returns `true` if the key was set. Returns `false` if the root is a
265    /// sequence or another non-mapping node (the document is left unchanged).
266    pub fn set_with_field_order<I, K>(
267        &self,
268        key: impl crate::AsYaml,
269        value: impl crate::AsYaml,
270        field_order: I,
271    ) -> bool
272    where
273        I: IntoIterator<Item = K>,
274        K: crate::AsYaml,
275    {
276        self.edit_root_mapping(|mapping| mapping.set_with_field_order(key, value, field_order))
277    }
278
279    /// Remove a key from the document (assumes document is a mapping).
280    ///
281    /// Returns `Some(entry)` if the key existed and was removed, or `None` if
282    /// the key was not found or the document is not a mapping. The returned
283    /// [`MappingEntry`](super::MappingEntry) is detached from the tree; callers can inspect its key
284    /// and value or re-insert it elsewhere.
285    pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
286        self.as_mapping()?.remove(key)
287    }
288
289    /// Get all key nodes from the document (assumes document is a mapping)
290    pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
291        self.as_mapping()
292            .into_iter()
293            .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
294    }
295
296    /// Iterate over all keys in the document as [`YamlNode`](crate::as_yaml::YamlNode)s.
297    ///
298    /// Each key is a [`YamlNode`](crate::as_yaml::YamlNode) wrapping the
299    /// underlying CST node.  Assumes the document is a mapping; yields nothing
300    /// if it is not.
301    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
302        self.key_nodes().filter_map(|k| crate::nodes::held_node(&k))
303    }
304
305    /// Check if the document is empty
306    pub fn is_empty(&self) -> bool {
307        self.as_mapping().map_or(true, |m| m.is_empty())
308    }
309
310    /// Create a document from a mapping
311    pub fn from_mapping(mapping: Mapping) -> Self {
312        // Create a document directly from the mapping syntax node to avoid recursion
313        let mut builder = GreenNodeBuilder::new();
314        builder.start_node(SyntaxKind::DOCUMENT.into());
315        // Add the document start marker "---"
316        builder.token(SyntaxKind::DOC_START.into(), "---");
317        builder.token(SyntaxKind::NEWLINE.into(), "\n");
318
319        crate::yaml::copy_node_to_builder(&mut builder, &mapping.0);
320        builder.finish_node(); // End DOCUMENT
321
322        Document(SyntaxNode::new_root_mut(builder.finish()))
323    }
324
325    /// Insert a key-value pair immediately after `after_key` in this document's mapping.
326    ///
327    /// If `key` already exists, its value is updated in-place and it stays at its
328    /// current position (it is **not** moved). Returns `false` if `after_key` is not
329    /// found or the document is not a mapping.
330    ///
331    /// Use [`move_after`](Self::move_after) if you want an existing entry to be
332    /// removed from its current position and re-inserted after `after_key`.
333    pub fn insert_after(
334        &self,
335        after_key: impl crate::AsYaml,
336        key: impl crate::AsYaml,
337        value: impl crate::AsYaml,
338    ) -> bool {
339        self.as_mapping()
340            .is_some_and(|mapping| mapping.insert_after(after_key, key, value))
341    }
342
343    /// Move a key-value pair to immediately after `after_key` in this document's mapping.
344    ///
345    /// If `key` already exists, it is **removed** from its current position and
346    /// re-inserted after `after_key` with the new value. Returns `false` if
347    /// `after_key` is not found or the document is not a mapping.
348    ///
349    /// Use [`insert_after`](Self::insert_after) if you want an existing entry to be
350    /// updated in-place rather than moved.
351    pub fn move_after(
352        &self,
353        after_key: impl crate::AsYaml,
354        key: impl crate::AsYaml,
355        value: impl crate::AsYaml,
356    ) -> bool {
357        self.as_mapping()
358            .is_some_and(|mapping| mapping.move_after(after_key, key, value))
359    }
360
361    /// Insert a key-value pair immediately before `before_key` in this document's mapping.
362    ///
363    /// If `key` already exists, its value is updated in-place and it stays at its
364    /// current position (it is **not** moved). Returns `false` if `before_key` is not
365    /// found or the document is not a mapping.
366    ///
367    /// Use [`move_before`](Self::move_before) if you want an existing entry to be
368    /// removed from its current position and re-inserted before `before_key`.
369    pub fn insert_before(
370        &self,
371        before_key: impl crate::AsYaml,
372        key: impl crate::AsYaml,
373        value: impl crate::AsYaml,
374    ) -> bool {
375        self.as_mapping()
376            .is_some_and(|mapping| mapping.insert_before(before_key, key, value))
377    }
378
379    /// Move a key-value pair to immediately before `before_key` in this document's mapping.
380    ///
381    /// If `key` already exists, it is **removed** from its current position and
382    /// re-inserted before `before_key` with the new value. Returns `false` if
383    /// `before_key` is not found or the document is not a mapping.
384    ///
385    /// Use [`insert_before`](Self::insert_before) if you want an existing entry to be
386    /// updated in-place rather than moved.
387    pub fn move_before(
388        &self,
389        before_key: impl crate::AsYaml,
390        key: impl crate::AsYaml,
391        value: impl crate::AsYaml,
392    ) -> bool {
393        self.as_mapping()
394            .is_some_and(|mapping| mapping.move_before(before_key, key, value))
395    }
396
397    /// Insert a key-value pair at a specific index.
398    ///
399    /// Delegates to [`Mapping::insert_at_index`]. If the document has no
400    /// root node, a mapping is created first.
401    ///
402    /// Returns `true` if the key was inserted. Returns `false` if the root is
403    /// a sequence or another non-mapping node (the document is left
404    /// unchanged), as [`set`](Self::set) does.
405    pub fn insert_at_index(
406        &self,
407        index: usize,
408        key: impl crate::AsYaml,
409        value: impl crate::AsYaml,
410    ) -> bool {
411        self.edit_root_mapping(|mapping| mapping.insert_at_index(index, key, value))
412    }
413
414    /// Splice `root` into this document, which has none, where the parser
415    /// reads it: on a line of its own after the `---` marker and any comment
416    /// lines, and before a `...` marker.
417    fn attach_root(&self, root: SyntaxNode) {
418        let children: Vec<_> = self.0.children_with_tokens().collect();
419        let index = children
420            .iter()
421            .position(|c| c.kind() == SyntaxKind::DOC_END)
422            .unwrap_or(children.len());
423        let mut new_children = Vec::new();
424        if index > 0
425            && !matches!(
426                children[index - 1].kind(),
427                SyntaxKind::NEWLINE | SyntaxKind::BOM
428            )
429        {
430            new_children.push(crate::nodes::fresh_token(SyntaxKind::NEWLINE, "\n").into());
431        }
432        new_children.push(root.into());
433        super::insert_children(&self.0, index, new_children);
434    }
435
436    /// Get the scalar value for `key` as a decoded `String`.
437    ///
438    /// Returns `None` if the key does not exist or its value is not a scalar
439    /// (i.e. the value is a sequence or mapping). Quotes are stripped and
440    /// escape sequences are processed (e.g. `\"` → `"`, `\n` → newline).
441    /// For tagged scalars (e.g. `!!str foo`) the tag is ignored and the
442    /// value part is returned.
443    pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
444        // get_node() returns the content node (SCALAR/MAPPING/SEQUENCE/TAGGED_NODE),
445        // already unwrapped from the VALUE wrapper.
446        let content = self.get_node(key)?;
447        if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
448            tagged_node.value().map(|s| s.as_string())
449        } else {
450            // Returns None if content is a sequence or mapping (not a scalar).
451            Scalar::cast(content).map(|s| s.as_string())
452        }
453    }
454
455    /// Get the number of top-level key-value pairs in this document.
456    ///
457    /// Returns `0` if the document is not a mapping or is empty.
458    pub fn len(&self) -> usize {
459        self.as_mapping().map_or(0, |m| m.len())
460    }
461
462    /// Get a nested mapping value for a key.
463    ///
464    /// Returns `None` if the key doesn't exist or its value is not a mapping.
465    /// A tag on the value is looked through.
466    pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
467        self.get(key)?.mapping_content()
468    }
469
470    /// Get a nested sequence value for a key.
471    ///
472    /// Returns `None` if the key doesn't exist or its value is not a sequence.
473    /// A tag on the value is looked through.
474    pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
475        self.get(key)?.sequence_content()
476    }
477
478    /// Rename a top-level key while preserving its value and formatting.
479    ///
480    /// The new key is automatically escaped/quoted as needed. Returns `true` if
481    /// the key was found and renamed, `false` if `old_key` does not exist.
482    /// An existing `new_key` entry is replaced; see [`Mapping::rename_key`].
483    pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
484        self.as_mapping()
485            .is_some_and(|m| m.rename_key(old_key, new_key))
486    }
487
488    /// Returns `true` if `key` exists and its value is a sequence.
489    pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
490        self.get(key)
491            .is_some_and(|node| node.as_sequence().is_some())
492    }
493
494    /// Reorder fields in the document's root mapping according to the specified order.
495    ///
496    /// Fields not in the order list will appear after the ordered fields, in their
497    /// original relative order.
498    ///
499    /// Returns `false`, leaving the document unchanged, if it is not a
500    /// mapping.
501    pub fn reorder_fields<I, K>(&self, order: I) -> bool
502    where
503        I: IntoIterator<Item = K>,
504        K: crate::AsYaml,
505    {
506        self.as_mapping()
507            .map(|mapping| mapping.reorder_fields(order))
508            .is_some()
509    }
510
511    /// Validate this document against a YAML schema
512    ///
513    /// # Examples
514    ///
515    /// ```rust
516    /// use yaml_edit::{Document, Schema, SchemaValidator};
517    ///
518    /// let yaml = r#"
519    /// name: "John"
520    /// age: 30
521    /// active: true
522    /// "#;
523    ///
524    /// let parsed = yaml.parse::<yaml_edit::YamlFile>().unwrap();
525    /// let doc = parsed.document().unwrap();
526    ///
527    /// // Validate against JSON schema
528    /// let validator = SchemaValidator::json();
529    /// match doc.validate_schema(&validator) {
530    ///     Ok(_) => println!("Valid JSON schema"),
531    ///     Err(errors) => {
532    ///         for error in errors {
533    ///             println!("Validation error: {}", error);
534    ///         }
535    ///     }
536    /// }
537    /// ```
538    pub fn validate_schema(
539        &self,
540        validator: &crate::schema::SchemaValidator,
541    ) -> crate::schema::ValidationResult<()> {
542        validator.validate(self)
543    }
544
545    /// Get the byte offset range of this document in the source text.
546    ///
547    /// Returns the start and end byte offsets as a `TextPosition`.
548    ///
549    /// # Examples
550    ///
551    /// ```
552    /// use yaml_edit::Document;
553    /// use std::str::FromStr;
554    ///
555    /// let text = "name: Alice\nage: 30";
556    /// let doc = Document::from_str(text).unwrap();
557    /// let range = doc.byte_range();
558    /// assert_eq!(range.start, 0);
559    /// ```
560    pub fn byte_range(&self) -> crate::TextPosition {
561        self.0.text_range().into()
562    }
563
564    /// Get the line and column where this document starts.
565    ///
566    /// Requires the original source text to calculate line/column from byte offsets.
567    /// Line and column numbers are 1-indexed.
568    ///
569    /// # Arguments
570    ///
571    /// * `source_text` - The original YAML source text
572    ///
573    /// # Examples
574    ///
575    /// ```
576    /// use yaml_edit::Document;
577    /// use std::str::FromStr;
578    ///
579    /// let text = "name: Alice";
580    /// let doc = Document::from_str(text).unwrap();
581    /// let pos = doc.start_position(text);
582    /// assert_eq!(pos.line, 1);
583    /// assert_eq!(pos.column, 1);
584    /// ```
585    pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
586        let range = self.byte_range();
587        crate::byte_offset_to_line_column(source_text, range.start as usize)
588    }
589
590    /// Get the line and column where this document ends.
591    ///
592    /// Requires the original source text to calculate line/column from byte offsets.
593    /// Line and column numbers are 1-indexed.
594    ///
595    /// # Arguments
596    ///
597    /// * `source_text` - The original YAML source text
598    pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
599        let range = self.byte_range();
600        crate::byte_offset_to_line_column(source_text, range.end as usize)
601    }
602
603    /// Iterate over the comments in this document, in source order.
604    ///
605    /// # Example
606    /// ```
607    /// use yaml_edit::Document;
608    /// use std::str::FromStr;
609    ///
610    /// let doc = Document::from_str("key: value # trailing\n# standalone\n").unwrap();
611    /// let comments: Vec<_> = doc.comments().map(|c| c.content().to_string()).collect();
612    /// assert_eq!(comments, vec!["trailing", "standalone"]);
613    /// ```
614    pub fn comments(&self) -> impl Iterator<Item = super::Comment> {
615        super::comment::comments(&self.0)
616    }
617}
618
619impl Default for Document {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624
625impl FromStr for Document {
626    type Err = crate::error::YamlError;
627
628    /// Parse a document from a YAML string.
629    ///
630    /// Returns an error if:
631    /// - the string contains multiple documents, or
632    /// - the string contains stream-level content (comments or `%YAML`/`%TAG`
633    ///   directives outside the document body).
634    ///
635    /// Per the YAML 1.2 spec, comments before the leading `---` (or after the
636    /// trailing `...`) and top-level directives belong to the stream, not the
637    /// document, so `Document` cannot round-trip them. Use
638    /// [`YamlFile::from_str`] if you need to preserve them, or
639    /// [`Document::from_str_ignoring_comments`] if you're happy to drop them.
640    ///
641    /// A byte order mark and blank lines before the document are kept in it,
642    /// as are comments before a document without `---`, and input with no
643    /// document at all gives an empty document holding its byte order mark
644    /// and blank lines, so the result always renders back to `s`.
645    ///
646    /// # Example
647    /// ```
648    /// use yaml_edit::Document;
649    /// use std::str::FromStr;
650    ///
651    /// let doc = Document::from_str("key: value").unwrap();
652    /// assert!(doc.as_mapping().is_some());
653    /// ```
654    fn from_str(s: &str) -> Result<Self, Self::Err> {
655        let parsed = YamlFile::parse(s);
656
657        if !parsed.positioned_errors().is_empty() {
658            let first_error = &parsed.positioned_errors()[0];
659            let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
660            return Err(crate::error::YamlError::Parse {
661                message: first_error.message.clone(),
662                line: Some(lc.line),
663                column: Some(lc.column),
664            });
665        }
666
667        let tree = parsed.tree();
668        let mut docs = tree.documents();
669        let first = docs.next();
670
671        if docs.next().is_some() {
672            return Err(crate::error::YamlError::InvalidOperation {
673                operation: "Document::from_str".to_string(),
674                reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
675            });
676        }
677
678        // Per the YAML 1.2 spec, comments and directives outside a document
679        // (before the leading `---` or after the trailing `...`) belong to the
680        // stream, not to any document. `Document` cannot round-trip them, so
681        // reject rather than silently discard. Callers who need to preserve
682        // stream-level content should use `YamlFile::from_str` instead.
683        for child in tree.0.children_with_tokens() {
684            match child {
685                rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::COMMENT => {
686                    return Err(crate::error::YamlError::InvalidOperation {
687                        operation: "Document::from_str".to_string(),
688                        reason: "Input contains stream-level comments outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
689                    });
690                }
691                rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::DIRECTIVE => {
692                    return Err(crate::error::YamlError::InvalidOperation {
693                        operation: "Document::from_str".to_string(),
694                        reason: "Input contains YAML directives outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
695                    });
696                }
697                // Tokens the parser could not attach to the document are
698                // swept into a stream-level ERROR node without a reported
699                // error. Returning `first` would drop them from the output,
700                // so say so instead of losing the text.
701                rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::ERROR => {
702                    let lc = crate::byte_offset_to_line_column(s, n.text_range().start().into());
703                    return Err(crate::error::YamlError::Parse {
704                        message: format!(
705                            "could not be parsed as part of the document: {:?}",
706                            n.text().to_string()
707                        ),
708                        line: Some(lc.line),
709                        column: Some(lc.column),
710                    });
711                }
712                _ => {}
713            }
714        }
715
716        first.map_or_else(|| Self::from_blank_stream(&tree), Ok)
717    }
718}
719
720impl Document {
721    /// An empty document holding the BOM and blank lines of a stream with
722    /// no document in it, so that it renders back to the same text.
723    fn from_blank_stream(tree: &YamlFile) -> YamlResult<Document> {
724        let mut builder = GreenNodeBuilder::new();
725        builder.start_node(SyntaxKind::DOCUMENT.into());
726        for child in tree.0.children_with_tokens() {
727            let token = child.as_token().filter(|t| {
728                matches!(
729                    t.kind(),
730                    SyntaxKind::BOM
731                        | SyntaxKind::WHITESPACE
732                        | SyntaxKind::NEWLINE
733                        | SyntaxKind::INDENT
734                )
735            });
736            let Some(token) = token else {
737                return Err(crate::error::YamlError::InvalidOperation {
738                    operation: "Document::from_str".to_string(),
739                    reason: format!(
740                        "Input contains stream-level content outside a document: {:?}. Use YamlFile::from_str() to preserve it.",
741                        child.to_string()
742                    ),
743                });
744            };
745            builder.token(token.kind().into(), token.text());
746        }
747        builder.finish_node();
748        Ok(Document(SyntaxNode::new_root_mut(builder.finish())))
749    }
750}
751
752/// As a value, a document is its root node, or null if it has none.
753///
754/// Only the root is copied: comments outside it, such as a comment line
755/// before it or after `---`, are not.
756impl AsYaml for Document {
757    fn as_node(&self) -> Option<&SyntaxNode> {
758        Some(&self.0)
759    }
760
761    fn kind(&self) -> YamlKind {
762        self.root_yaml_node()
763            .map_or(YamlKind::Scalar, |root| AsYaml::kind(&root))
764    }
765
766    fn build_content(
767        &self,
768        builder: &mut rowan::GreenNodeBuilder,
769        indent: usize,
770        flow_context: bool,
771    ) -> bool {
772        match self.root_yaml_node() {
773            Some(root) => root.build_content(builder, indent, flow_context),
774            None => crate::ScalarValue::null().build_content(builder, indent, flow_context),
775        }
776    }
777
778    fn is_inline(&self) -> bool {
779        self.root_yaml_node().map_or(true, |root| root.is_inline())
780    }
781
782    fn value_node(&self) -> Option<SyntaxNode> {
783        self.root_node()
784    }
785}