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}