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}