Skip to main content

yaml_rt_core/
doc.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt;
4
5use crate::syntax::node_link;
6use crate::{
7    Children, CollectionStyle, CollectionTarget, Diagnostic, DiagnosticKind, FromYamlDoc, Node,
8    NodeId, NodeKind, Parser, ScalarStyle, SemanticKind, SemanticStore, Source, Span, ToYamlDoc,
9    ToYamlFragment, Token, YamlError, YamlEvent, decode_scalar_value_with_content_indent,
10    directive_emit_error, double_quoted_scalar_end, edits_conflict, events_to_test_string,
11    format_scalar_value, lex, next_line_content_start, parse_node_properties, plain_scalar_end,
12    resolve_tag, single_quoted_scalar_end, strip_inline_comment, validate_plain_mapping_fragment,
13    validate_tag_directive_parts_for_emit, validate_yaml_chars,
14    validate_yaml_directive_version_for_emit,
15};
16
17/// Pending source edit used by the patch-based emitter.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Edit {
20    /// Span to replace. Empty spans represent insertions.
21    pub span: Span,
22    /// Replacement text.
23    pub replacement: String,
24}
25
26/// On-demand iterator over semantic YAML events.
27pub struct YamlEvents<'doc> {
28    doc: &'doc YamlDoc,
29    tasks: Vec<EventTask>,
30}
31
32impl Iterator for YamlEvents<'_> {
33    type Item = YamlEvent;
34
35    fn next(&mut self) -> Option<Self::Item> {
36        while let Some(task) = self.tasks.pop() {
37            match task {
38                EventTask::StreamStart => {
39                    return Some(YamlEvent {
40                        kind: crate::YamlEventKind::StreamStart,
41                        span: Span::from_usize(0, self.doc.source.len()),
42                        cst: None,
43                        content_indent: None,
44                    });
45                }
46                EventTask::Documents(index) => self.schedule_document(index),
47                EventTask::DocumentStart(document) => {
48                    let Some(metadata) = self.doc.semantics.get(document) else {
49                        continue;
50                    };
51                    return Some(YamlEvent {
52                        kind: crate::YamlEventKind::DocumentStart {
53                            explicit: metadata.explicit_start(),
54                        },
55                        span: self.doc.semantic_span(document, metadata),
56                        cst: Some(document),
57                        content_indent: None,
58                    });
59                }
60                EventTask::DocumentChildren(next) => self.schedule_document_child(next),
61                EventTask::DocumentEnd(document) => {
62                    let Some(metadata) = self.doc.semantics.get(document) else {
63                        continue;
64                    };
65                    return Some(YamlEvent {
66                        kind: crate::YamlEventKind::DocumentEnd {
67                            explicit: metadata.explicit_end(),
68                        },
69                        span: self.doc.semantic_end_span(document, metadata),
70                        cst: None,
71                        content_indent: None,
72                    });
73                }
74                EventTask::Node(node) => {
75                    if let Some(event) = self.schedule_node(node) {
76                        return Some(event);
77                    }
78                }
79                EventTask::MappingEntries(next) => self.schedule_mapping_entry(next),
80                EventTask::SequenceEntries(next) => self.schedule_sequence_entry(next),
81                EventTask::CollectionEnd { node, mapping } => {
82                    let Some(metadata) = self.doc.semantics.get(node) else {
83                        continue;
84                    };
85                    return Some(YamlEvent {
86                        kind: if mapping {
87                            crate::YamlEventKind::MappingEnd
88                        } else {
89                            crate::YamlEventKind::SequenceEnd
90                        },
91                        span: self.doc.semantic_end_span(node, metadata),
92                        cst: None,
93                        content_indent: None,
94                    });
95                }
96                EventTask::StreamEnd => {
97                    return Some(YamlEvent {
98                        kind: crate::YamlEventKind::StreamEnd,
99                        span: Span::empty_from_usize(self.doc.source.len()),
100                        cst: None,
101                        content_indent: None,
102                    });
103                }
104            }
105        }
106        None
107    }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum EventTask {
112    StreamStart,
113    Documents(usize),
114    DocumentStart(NodeId),
115    DocumentChildren(u32),
116    DocumentEnd(NodeId),
117    Node(NodeId),
118    MappingEntries(u32),
119    SequenceEntries(u32),
120    CollectionEnd { node: NodeId, mapping: bool },
121    StreamEnd,
122}
123
124impl YamlEvents<'_> {
125    fn schedule_document(&mut self, index: usize) {
126        let Some(&document) = self.doc.semantics.documents.get(index) else {
127            return;
128        };
129        let Some(node) = self.doc.node(document) else {
130            self.tasks.push(EventTask::Documents(index + 1));
131            return;
132        };
133        self.tasks.push(EventTask::Documents(index + 1));
134        self.tasks.push(EventTask::DocumentEnd(document));
135        self.tasks
136            .push(EventTask::DocumentChildren(node.first_child));
137        self.tasks.push(EventTask::DocumentStart(document));
138    }
139
140    fn schedule_document_child(&mut self, next: u32) {
141        let Some(child) = node_link(next) else {
142            return;
143        };
144        self.tasks.push(EventTask::DocumentChildren(
145            self.doc.nodes[child.as_usize()].next_sibling,
146        ));
147        if self.doc.semantics.get(child).is_some() {
148            self.tasks.push(EventTask::Node(child));
149        }
150    }
151
152    fn schedule_node(&mut self, node: NodeId) -> Option<YamlEvent> {
153        let metadata = self.doc.semantics.get(node)?;
154        let span = self.doc.semantic_span(node, metadata);
155        match metadata.kind {
156            SemanticKind::Document => None,
157            SemanticKind::Mapping { style } => {
158                self.tasks.push(EventTask::CollectionEnd {
159                    node,
160                    mapping: true,
161                });
162                self.tasks.push(EventTask::MappingEntries(
163                    self.doc.nodes[node.as_usize()].first_child,
164                ));
165                Some(YamlEvent {
166                    kind: crate::YamlEventKind::MappingStart {
167                        style,
168                        tag: self
169                            .doc
170                            .resolved_tag(node)
171                            .ok()
172                            .flatten()
173                            .map(Cow::into_owned),
174                        anchor: self.doc.anchor(node).map(str::to_owned),
175                    },
176                    span,
177                    cst: Some(node),
178                    content_indent: None,
179                })
180            }
181            SemanticKind::Sequence { style } => {
182                self.tasks.push(EventTask::CollectionEnd {
183                    node,
184                    mapping: false,
185                });
186                self.tasks.push(EventTask::SequenceEntries(
187                    self.doc.nodes[node.as_usize()].first_child,
188                ));
189                Some(YamlEvent {
190                    kind: crate::YamlEventKind::SequenceStart {
191                        style,
192                        tag: self
193                            .doc
194                            .resolved_tag(node)
195                            .ok()
196                            .flatten()
197                            .map(Cow::into_owned),
198                        anchor: self.doc.anchor(node).map(str::to_owned),
199                    },
200                    span,
201                    cst: Some(node),
202                    content_indent: None,
203                })
204            }
205            SemanticKind::Scalar { style } => Some(YamlEvent {
206                kind: crate::YamlEventKind::Scalar {
207                    style,
208                    value: self
209                        .doc
210                        .scalar_value(node)
211                        .map(Cow::into_owned)
212                        .unwrap_or_default(),
213                    tag: self
214                        .doc
215                        .resolved_tag(node)
216                        .ok()
217                        .flatten()
218                        .map(Cow::into_owned),
219                    anchor: self.doc.anchor(node).map(str::to_owned),
220                },
221                span,
222                cst: Some(node),
223                content_indent: self
224                    .doc
225                    .semantics
226                    .properties(node)
227                    .and_then(|properties| properties.content_indent),
228            }),
229            SemanticKind::Alias => Some(YamlEvent {
230                kind: crate::YamlEventKind::Alias {
231                    name: self.doc.alias_name(node).unwrap_or_default().to_owned(),
232                },
233                span,
234                cst: Some(node),
235                content_indent: None,
236            }),
237        }
238    }
239
240    fn schedule_mapping_entry(&mut self, next: u32) {
241        let Some(entry) = node_link(next) else {
242            return;
243        };
244        self.tasks.push(EventTask::MappingEntries(
245            self.doc.nodes[entry.as_usize()].next_sibling,
246        ));
247        if self.doc.nodes[entry.as_usize()].kind != NodeKind::MappingEntry {
248            return;
249        }
250        let Some(key) = self.first_semantic_child(entry) else {
251            return;
252        };
253        let Some(value) = self.next_semantic_sibling(key) else {
254            return;
255        };
256        self.tasks.push(EventTask::Node(value));
257        self.tasks.push(EventTask::Node(key));
258    }
259
260    fn schedule_sequence_entry(&mut self, next: u32) {
261        let Some(entry) = node_link(next) else {
262            return;
263        };
264        self.tasks.push(EventTask::SequenceEntries(
265            self.doc.nodes[entry.as_usize()].next_sibling,
266        ));
267        if self.doc.nodes[entry.as_usize()].kind != NodeKind::SequenceEntry {
268            return;
269        }
270        if let Some(item) = self.first_semantic_child(entry) {
271            self.tasks.push(EventTask::Node(item));
272        }
273    }
274
275    fn first_semantic_child(&self, parent: NodeId) -> Option<NodeId> {
276        let next = self.doc.nodes[parent.as_usize()].first_child;
277        self.next_semantic(next)
278    }
279
280    fn next_semantic_sibling(&self, node: NodeId) -> Option<NodeId> {
281        let next = self.doc.nodes[node.as_usize()].next_sibling;
282        self.next_semantic(next)
283    }
284
285    fn next_semantic(&self, mut next: u32) -> Option<NodeId> {
286        while let Some(node) = node_link(next) {
287            if self.doc.semantics.get(node).is_some() {
288                return Some(node);
289            }
290            next = self.doc.nodes[node.as_usize()].next_sibling;
291        }
292        None
293    }
294}
295
296/// Parsed `%YAML` directive metadata.
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct YamlDirective {
299    /// Directive version text, such as `1.2`.
300    pub version: String,
301    /// CST directive node.
302    pub node: NodeId,
303}
304
305/// Parsed `%TAG` directive metadata.
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct TagDirective {
308    /// Tag handle, such as `!` or `!e!`.
309    pub handle: String,
310    /// Tag prefix text.
311    pub prefix: String,
312    /// CST directive node.
313    pub node: NodeId,
314}
315
316/// Parsed reserved directive metadata.
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct ReservedDirective {
319    /// Directive name, including the leading `%`.
320    pub name: String,
321    /// Whitespace-separated directive parameters.
322    pub parameters: Vec<String>,
323    /// CST directive node.
324    pub node: NodeId,
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
328enum ParsedDirective {
329    Yaml(YamlDirective),
330    Tag(TagDirective),
331    Reserved(ReservedDirective),
332}
333
334/// Formatting controls for inserting a block mapping entry.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
336pub enum MappingEntryStyle {
337    /// Reuse the target mapping indentation and the document line ending.
338    #[default]
339    Inherit,
340    /// Insert with an explicit indentation width, in spaces.
341    Indent(usize),
342}
343
344/// A source-preserving YAML document.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct YamlDoc {
347    /// Original source buffer.
348    pub(crate) source: Source,
349    /// CST and semantic nodes. The CST remains the source of truth.
350    pub(crate) nodes: Vec<Node>,
351    /// Compact semantic metadata keyed by CST node IDs.
352    pub(crate) semantics: SemanticStore,
353    /// Optional mapping root used by nested typed overlays.
354    pub(crate) root_override: Option<NodeId>,
355    /// Pending patch edits applied from highest offset to lowest offset.
356    pub(crate) edits: Vec<Edit>,
357}
358
359impl YamlDoc {
360    /// Parses a YAML stream into a round-trip document.
361    ///
362    /// This bootstrap parser preserves the input text exactly and records a root
363    /// stream node. Real lexing and parsing will replace this placeholder.
364    ///
365    /// # Errors
366    ///
367    /// Returns an error when source validation, CST parsing, or semantic view
368    /// composition fails.
369    pub fn parse(input: &str) -> Result<Self, YamlError> {
370        Self::parse_owned(input.to_owned())
371    }
372
373    /// Parses an owned YAML stream without copying its source buffer.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error when source validation, CST parsing, or semantic view
378    /// composition fails.
379    pub fn parse_owned(input: String) -> Result<Self, YamlError> {
380        let source = Source::new(input)?;
381        let parsed = Parser::new(&source)
382            .parse()
383            .map_err(|error| error.with_position_from(&source))?;
384        Ok(Self {
385            source,
386            nodes: parsed.nodes,
387            semantics: parsed.semantics,
388            root_override: None,
389            edits: Vec::new(),
390        })
391    }
392
393    /// Applies pending edits, reparses the rendered YAML, and clears the edit queue.
394    ///
395    /// `YamlDoc::to_string` only previews the original source with queued byte
396    /// patches applied. It does not prove that the patched stream is still
397    /// valid YAML, because low-level edit APIs can replace arbitrary node spans
398    /// or insert conservative-but-raw fragments. Reparse on commit is the point
399    /// where the document regains a validated CST and semantic view.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error when the patched YAML cannot be parsed.
404    pub fn commit_edits(&mut self) -> Result<(), YamlError> {
405        if self.edits.is_empty() {
406            return Ok(());
407        }
408
409        let edited = self.to_string();
410        *self = Self::parse(&edited)?;
411        Ok(())
412    }
413
414    /// Returns the original source text.
415    #[must_use]
416    pub fn as_source(&self) -> &str {
417        self.source.as_str()
418    }
419
420    /// Returns the original source buffer and its line index.
421    #[must_use]
422    pub const fn source(&self) -> &Source {
423        &self.source
424    }
425
426    /// Returns a freshly owned copy of the lossless token stream.
427    ///
428    /// The owned return type keeps this API stable when tokenization becomes
429    /// on demand rather than retained by every document.
430    ///
431    /// # Errors
432    ///
433    /// Returns a lexer diagnostic if the source cannot be tokenized.
434    pub fn tokens(&self) -> Result<Vec<Token>, YamlError> {
435        lex(&self.source).map_err(|error| error.with_position_from(&self.source))
436    }
437
438    /// Returns the root node identifier when present.
439    #[must_use]
440    pub fn root(&self) -> Option<NodeId> {
441        (!self.nodes.is_empty()).then_some(NodeId(0))
442    }
443
444    /// Derives the semantic event stream from CST-linked metadata.
445    #[must_use]
446    pub fn events(&self) -> YamlEvents<'_> {
447        let mut tasks = Vec::with_capacity(8);
448        tasks.push(EventTask::StreamEnd);
449        tasks.push(EventTask::Documents(0));
450        tasks.push(EventTask::StreamStart);
451        YamlEvents { doc: self, tasks }
452    }
453
454    /// Renders semantic events in the YAML Test Suite `test.event` format.
455    #[must_use]
456    pub fn events_to_test_string(&self) -> String {
457        events_to_test_string(self.events())
458    }
459
460    /// Returns the number of documents in this YAML stream.
461    #[must_use]
462    pub fn document_count(&self) -> usize {
463        self.root_override
464            .map_or(self.semantics.documents.len(), |_| 1)
465    }
466
467    /// Queues an explicit document append at the end of this YAML stream.
468    ///
469    /// The appended document becomes visible to document-indexed lookup after
470    /// [`YamlDoc::commit_edits`] reparses the stream.
471    ///
472    /// # Errors
473    ///
474    /// Returns an error when `value` cannot be formatted as a YAML fragment or
475    /// the append conflicts with another pending edit at the stream end.
476    pub fn append_document<T>(&mut self, value: &T) -> Result<(), YamlError>
477    where
478        T: ToYamlFragment,
479    {
480        let line_ending = self.preferred_line_ending();
481        let mut replacement = self.document_append_prefix(line_ending);
482        replacement.push_str("---");
483        replacement.push_str(line_ending);
484        let fragment = value.to_yaml_fragment(0, line_ending)?;
485        replacement.push_str(&fragment);
486        replacement.push_str(line_ending);
487        self.queue_edit(Span::empty_from_usize(self.source.len()), replacement)
488    }
489
490    /// Queues an explicit empty mapping document append.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error when the append conflicts with another pending edit at
495    /// the stream end.
496    pub fn append_empty_mapping_document(&mut self) -> Result<(), YamlError> {
497        self.append_document(&std::collections::BTreeMap::<String, String>::new())
498    }
499
500    /// Returns the `%YAML` directive when one is present in the stream prologue.
501    #[must_use]
502    pub fn yaml_directive(&self) -> Option<YamlDirective> {
503        self.directive_nodes()
504            .filter_map(|node| self.parse_directive_node(node).ok())
505            .find_map(|directive| match directive {
506                ParsedDirective::Yaml(directive) => Some(directive),
507                ParsedDirective::Tag(_) | ParsedDirective::Reserved(_) => None,
508            })
509    }
510
511    /// Returns `%TAG` directives from the stream prologue in source order.
512    #[must_use]
513    pub fn tag_directives(&self) -> Vec<TagDirective> {
514        self.directive_nodes()
515            .filter_map(|node| self.parse_directive_node(node).ok())
516            .filter_map(|directive| match directive {
517                ParsedDirective::Tag(directive) => Some(directive),
518                ParsedDirective::Yaml(_) | ParsedDirective::Reserved(_) => None,
519            })
520            .collect()
521    }
522
523    /// Returns reserved directives from the stream prologue in source order.
524    #[must_use]
525    pub fn reserved_directives(&self) -> Vec<ReservedDirective> {
526        self.directive_nodes()
527            .filter_map(|node| self.parse_directive_node(node).ok())
528            .filter_map(|directive| match directive {
529                ParsedDirective::Reserved(directive) => Some(directive),
530                ParsedDirective::Yaml(_) | ParsedDirective::Tag(_) => None,
531            })
532            .collect()
533    }
534
535    /// Queues insertion or update of the stream `%YAML` directive.
536    ///
537    /// # Errors
538    ///
539    /// Returns an error when `version` is not valid YAML directive version
540    /// syntax or when the edit overlaps an existing pending edit.
541    pub fn set_yaml_directive(&mut self, version: &str) -> Result<(), YamlError> {
542        validate_yaml_directive_version_for_emit(version)?;
543        let replacement = format!("%YAML {version}");
544        if let Some(directive) = self.yaml_directive() {
545            let span = self.directive_content_span(directive.node)?;
546            self.queue_edit(span, replacement)
547        } else {
548            self.insert_directive_line(replacement)
549        }
550    }
551
552    /// Queues insertion or update of a stream `%TAG` directive.
553    ///
554    /// # Errors
555    ///
556    /// Returns an error when `handle` or `prefix` is invalid or when the edit
557    /// overlaps an existing pending edit.
558    pub fn set_tag_directive(&mut self, handle: &str, prefix: &str) -> Result<(), YamlError> {
559        validate_tag_directive_parts_for_emit(handle, prefix)?;
560        let replacement = format!("%TAG {handle} {prefix}");
561        if let Some(directive) = self
562            .tag_directives()
563            .into_iter()
564            .find(|directive| directive.handle == handle)
565        {
566            let span = self.directive_content_span(directive.node)?;
567            self.queue_edit(span, replacement)
568        } else {
569            self.insert_directive_line(replacement)
570        }
571    }
572
573    /// Queues removal of the stream `%YAML` directive when present.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error when the removal edit overlaps an existing pending edit.
578    pub fn remove_yaml_directive(&mut self) -> Result<(), YamlError> {
579        if let Some(directive) = self.yaml_directive() {
580            self.remove_directive_node(directive.node)?;
581        }
582        Ok(())
583    }
584
585    /// Queues removal of the stream `%TAG` directive with `handle` when present.
586    ///
587    /// # Errors
588    ///
589    /// Returns an error when the removal edit overlaps an existing pending edit.
590    pub fn remove_tag_directive(&mut self, handle: &str) -> Result<(), YamlError> {
591        if let Some(directive) = self
592            .tag_directives()
593            .into_iter()
594            .find(|directive| directive.handle == handle)
595        {
596            self.remove_directive_node(directive.node)?;
597        }
598        Ok(())
599    }
600
601    /// Returns a node by identifier.
602    #[must_use]
603    pub fn node(&self, node: NodeId) -> Option<&Node> {
604        self.nodes.get(node.0 as usize)
605    }
606
607    /// Iterates over a node's children in source order.
608    #[must_use]
609    pub fn children(&self, node: NodeId) -> Children<'_> {
610        Children::new(&self.nodes, node)
611    }
612
613    fn semantic_children(&self, node: NodeId) -> impl Iterator<Item = NodeId> + '_ {
614        self.children(node)
615            .filter(|child| self.semantics.get(*child).is_some())
616    }
617
618    /// Returns a node's semantic interpretation.
619    #[must_use]
620    pub fn semantic_kind(&self, node: NodeId) -> Option<SemanticKind> {
621        self.semantics.get(node).map(|node| node.kind)
622    }
623
624    /// Returns the explicit tag spelling, including its leading `!`.
625    #[must_use]
626    pub fn raw_tag(&self, node: NodeId) -> Option<&str> {
627        let span = self.semantics.properties(node)?.tag?;
628        Some(self.source.slice(span))
629    }
630
631    /// Resolves an explicit tag through the built-in or document-local handle table.
632    pub fn resolved_tag(&self, node: NodeId) -> Result<Option<Cow<'_, str>>, YamlError> {
633        let Some(raw) = self.raw_tag(node) else {
634            return Ok(None);
635        };
636        let document = self.semantics.property_document(node).unwrap_or(node);
637        let handles = self
638            .semantics
639            .tag_directives(document)
640            .map(|(handle, prefix)| {
641                (
642                    self.source.slice(handle).to_owned(),
643                    self.source.slice(prefix).to_owned(),
644                )
645            })
646            .collect::<BTreeMap<_, _>>();
647        let span = self
648            .semantics
649            .properties(node)
650            .and_then(|properties| properties.tag)
651            .unwrap_or_else(|| self.node(node).map_or(Span::empty(0), Node::span));
652        resolve_tag(raw, &handles, span).map(|tag| Some(Cow::Owned(tag)))
653    }
654
655    /// Returns an anchor name without its leading `&`.
656    #[must_use]
657    pub fn anchor(&self, node: NodeId) -> Option<&str> {
658        let span = self.semantics.properties(node)?.anchor?;
659        Some(self.source.slice(span))
660    }
661
662    /// Returns an alias name without its leading `*`.
663    #[must_use]
664    pub fn alias_name(&self, node: NodeId) -> Option<&str> {
665        let span = self.semantics.properties(node)?.alias?;
666        Some(self.source.slice(span))
667    }
668
669    /// Resolves an alias to the most recent matching anchor in its document.
670    #[must_use]
671    pub fn resolve_alias(&self, node: NodeId) -> Option<NodeId> {
672        let name = self.alias_name(node)?;
673        let document = self.semantics.property_document(node)?;
674        let alias_start = self.node(node)?.span.start;
675        self.semantics
676            .anchors()
677            .rev()
678            .find(|(span, target, anchor_document)| {
679                *anchor_document == document
680                    && self
681                        .node(*target)
682                        .is_some_and(|node| node.span.start <= alias_start)
683                    && self.source.slice(*span) == name
684            })
685            .map(|(_, target, _)| target)
686    }
687
688    fn semantic_span(&self, node: NodeId, metadata: &crate::semantic::SemanticNode) -> Span {
689        Span::new(
690            metadata.span_start,
691            self.node(node)
692                .map_or(metadata.end_offset, |node| node.span.end),
693        )
694    }
695
696    fn semantic_end_span(&self, node: NodeId, metadata: &crate::semantic::SemanticNode) -> Span {
697        if metadata.explicit_end()
698            && let Some(marker) = self.children(node).find_map(|child| {
699                let child = self.node(child)?;
700                (child.kind == NodeKind::DocumentMarker
701                    && self.source.slice(child.span).starts_with("..."))
702                .then_some(child.span)
703            })
704        {
705            return marker;
706        }
707        if matches!(
708            metadata.kind,
709            SemanticKind::Mapping {
710                style: CollectionStyle::Flow
711            } | SemanticKind::Sequence {
712                style: CollectionStyle::Flow
713            }
714        ) {
715            return self.semantic_span(node, metadata);
716        }
717        Span::empty(metadata.end_offset)
718    }
719
720    /// Iterates over semantic document CST nodes in stream order.
721    pub fn documents(&self) -> impl Iterator<Item = NodeId> + '_ {
722        self.semantics.documents.iter().copied()
723    }
724
725    /// Returns the semantic root value of a selected document.
726    ///
727    /// Empty documents have no root value and return `Ok(None)`. Unlike
728    /// [`YamlDoc::document_root_mapping`], this method accepts scalar, sequence,
729    /// mapping, and alias roots.
730    ///
731    /// # Errors
732    ///
733    /// Returns an error when `index` is outside the YAML stream.
734    pub fn document_root(&self, index: usize) -> Result<Option<NodeId>, YamlError> {
735        if let Some(root) = self.root_override {
736            return (index == 0)
737                .then_some(Some(root))
738                .ok_or_else(|| self.document_index_error(index));
739        }
740        let document = self
741            .semantics
742            .documents
743            .get(index)
744            .copied()
745            .ok_or_else(|| self.document_index_error(index))?;
746        let root = self.semantic_children(document).next();
747        Ok(root.filter(|root| {
748            !matches!(self.semantic_kind(*root), Some(SemanticKind::Scalar { .. }))
749                || self.node(*root).is_some_and(|node| !node.span.is_empty())
750        }))
751    }
752
753    /// Iterates over mapping key/value CST node pairs in source order.
754    pub fn mapping_entries(&self, mapping: NodeId) -> impl Iterator<Item = (NodeId, NodeId)> + '_ {
755        let is_mapping = matches!(
756            self.semantic_kind(mapping),
757            Some(SemanticKind::Mapping { .. })
758        );
759        self.children(mapping).filter_map(move |entry| {
760            if !is_mapping || self.node(entry)?.kind != NodeKind::MappingEntry {
761                return None;
762            }
763            let mut children = self.semantic_children(entry);
764            Some((children.next()?, children.next()?))
765        })
766    }
767
768    /// Iterates over sequence item CST nodes in source order.
769    pub fn sequence_items(&self, sequence: NodeId) -> impl Iterator<Item = NodeId> + '_ {
770        let is_sequence = matches!(
771            self.semantic_kind(sequence),
772            Some(SemanticKind::Sequence { .. })
773        );
774        self.children(sequence).filter_map(move |entry| {
775            if !is_sequence || self.node(entry)?.kind != NodeKind::SequenceEntry {
776                return None;
777            }
778            self.semantic_children(entry).next()
779        })
780    }
781
782    /// Returns the first root-level block mapping in the document.
783    ///
784    /// # Errors
785    ///
786    /// Returns an error when no root block mapping exists or when the semantic
787    /// root mapping is not linked back to a CST node.
788    pub fn root_mapping(&self) -> Result<NodeId, YamlError> {
789        self.document_root_mapping(0)
790    }
791
792    /// Returns the root-level block mapping in a selected document.
793    ///
794    /// # Errors
795    ///
796    /// Returns an error when the selected document does not exist, has no block
797    /// mapping root, or the root mapping is not linked back to the CST.
798    pub fn document_root_mapping(&self, index: usize) -> Result<NodeId, YamlError> {
799        if let Some(root) = self.root_override {
800            return (index == 0)
801                .then_some(root)
802                .ok_or_else(|| self.document_index_error(index));
803        }
804        let document = self
805            .semantics
806            .documents
807            .get(index)
808            .copied()
809            .ok_or_else(|| self.document_index_error(index))?;
810        self.semantic_children(document)
811            .find(|child| {
812                self.node(*child)
813                    .is_some_and(|node| node.kind == NodeKind::BlockMapping)
814                    && matches!(
815                        self.semantic_kind(*child),
816                        Some(SemanticKind::Mapping { .. })
817                    )
818            })
819            .ok_or_else(|| {
820                YamlError::new(
821                    Diagnostic::new(
822                        DiagnosticKind::Semantic,
823                        "document does not contain a root mapping",
824                        self.node(document).map_or(Span::empty(0), |node| node.span),
825                    )
826                    .with_expected("a block mapping node"),
827                )
828            })
829    }
830
831    /// Reads a typed overlay from a selected document.
832    ///
833    /// # Errors
834    ///
835    /// Returns an error when the selected document is missing, does not contain
836    /// a root block mapping, or the typed overlay cannot be read.
837    pub fn read_document<T>(&self, index: usize) -> Result<T, YamlError>
838    where
839        T: FromYamlDoc,
840    {
841        let root = self.document_root_mapping(index)?;
842        let nested = self.rerooted_at_mapping(root)?;
843        T::from_yaml_doc(&nested)
844    }
845
846    /// Writes a typed overlay to a selected document.
847    ///
848    /// # Errors
849    ///
850    /// Returns an error when the selected document is missing, does not contain
851    /// a root block mapping, or the typed overlay cannot be written.
852    pub fn write_document<T>(&mut self, index: usize, value: &T) -> Result<(), YamlError>
853    where
854        T: ToYamlDoc,
855    {
856        let root = match self.document_root_mapping(index) {
857            Ok(root) => root,
858            Err(error) => {
859                if let Some(root) = self.empty_flow_mapping_document_root(index)? {
860                    let replacement = value.to_yaml_fragment(0, self.preferred_line_ending())?;
861                    return self.replace_node_text(root, replacement);
862                }
863                return Err(error);
864            }
865        };
866        let mut nested = self.rerooted_at_mapping(root)?;
867        value.apply_to_yaml_doc(&mut nested)?;
868        self.queue_edits_from(&nested)
869    }
870
871    /// Looks up a mapping entry by key inside `mapping`.
872    ///
873    /// # Errors
874    ///
875    /// Returns an error when a scalar key cannot be decoded.
876    pub fn get_mapping_entry(
877        &self,
878        mapping: NodeId,
879        key: &str,
880    ) -> Result<Option<NodeId>, YamlError> {
881        Ok(self
882            .find_mapping_pair(mapping, key)?
883            .and_then(|(key, _)| self.containing_entry(key)))
884    }
885
886    /// Looks up a mapping value by key inside `mapping`.
887    ///
888    /// # Errors
889    ///
890    /// Returns an error when a scalar key cannot be decoded.
891    pub fn get_mapping_value(
892        &self,
893        mapping: NodeId,
894        key: &str,
895    ) -> Result<Option<NodeId>, YamlError> {
896        Ok(self
897            .find_mapping_pair(mapping, key)?
898            .map(|(_, value)| value))
899    }
900
901    /// Looks up a nested path of mapping keys.
902    ///
903    /// # Errors
904    ///
905    /// Returns an error when semantic path lookup cannot decode a mapping key.
906    pub fn get_path(&self, path: &[&str]) -> Result<Option<NodeId>, YamlError> {
907        self.get_path_in_document(0, path)
908    }
909
910    /// Looks up a nested path of mapping keys in a selected document.
911    ///
912    /// # Errors
913    ///
914    /// Returns an error when semantic path lookup fails while resolving the
915    /// graph path.
916    pub fn get_path_in_document(
917        &self,
918        index: usize,
919        path: &[&str],
920    ) -> Result<Option<NodeId>, YamlError> {
921        let Some((first, rest)) = path.split_first() else {
922            return Ok(None);
923        };
924        let Some((_, mut current)) =
925            self.find_mapping_pair(self.document_root_mapping(index)?, first)?
926        else {
927            return Ok(None);
928        };
929        for segment in rest {
930            let Some((_, value)) = self.find_mapping_pair(current, segment)? else {
931                return Ok(None);
932            };
933            current = value;
934        }
935        Ok(Some(current))
936    }
937
938    fn empty_flow_mapping_document_root(&self, index: usize) -> Result<Option<NodeId>, YamlError> {
939        let document = self
940            .semantics
941            .documents
942            .get(index)
943            .copied()
944            .ok_or_else(|| self.document_index_error(index))?;
945        let Some(child) = self.semantic_children(document).next() else {
946            return Ok(None);
947        };
948        let Some(SemanticKind::Mapping { style, .. }) = self.semantic_kind(child) else {
949            return Ok(None);
950        };
951        if style != CollectionStyle::Flow || self.mapping_entries(child).next().is_some() {
952            return Ok(None);
953        }
954        Ok(self
955            .node(child)
956            .is_some_and(|node| node.kind == NodeKind::FlowMapping)
957            .then_some(child))
958    }
959
960    fn document_index_error(&self, index: usize) -> YamlError {
961        YamlError::new(
962            Diagnostic::new(
963                DiagnosticKind::Semantic,
964                format!("document index {index} is out of range"),
965                Span::empty_from_usize(self.source.len()),
966            )
967            .with_expected("an existing document index"),
968        )
969    }
970
971    fn find_mapping_pair(
972        &self,
973        mapping: NodeId,
974        key: &str,
975    ) -> Result<Option<(NodeId, NodeId)>, YamlError> {
976        for (key_node, value_node) in self.mapping_entries(mapping) {
977            if self.scalar_value(key_node)? == key {
978                return Ok(Some((key_node, value_node)));
979            }
980        }
981        Ok(None)
982    }
983
984    pub(crate) fn rerooted_at_mapping(&self, mapping: NodeId) -> Result<Self, YamlError> {
985        self.expect_node_kind(mapping, NodeKind::BlockMapping)?;
986        if !matches!(
987            self.semantic_kind(mapping),
988            Some(SemanticKind::Mapping { .. })
989        ) {
990            return Err(YamlError::new(
991                Diagnostic::new(
992                    DiagnosticKind::Semantic,
993                    "mapping does not have semantic metadata",
994                    self.expect_node(mapping)?.span,
995                )
996                .with_expected("a semantic mapping"),
997            )
998            .with_position_from(&self.source));
999        }
1000        let mut doc = self.clone();
1001        doc.root_override = Some(mapping);
1002        doc.edits.clear();
1003        Ok(doc)
1004    }
1005
1006    pub(crate) fn queue_edits_from(&mut self, other: &YamlDoc) -> Result<(), YamlError> {
1007        for edit in &other.edits {
1008            self.queue_edit(edit.span, edit.replacement.clone())?;
1009        }
1010        Ok(())
1011    }
1012
1013    /// Returns the source text for a scalar node.
1014    ///
1015    /// # Errors
1016    ///
1017    /// Returns an error when `node` is unknown or does not identify a plain CST
1018    /// scalar node.
1019    pub fn scalar_text(&self, node: NodeId) -> Result<&str, YamlError> {
1020        let node = self.expect_node_kind(node, NodeKind::Scalar)?;
1021        Ok(self.source.slice(node.span))
1022    }
1023
1024    /// Returns the decoded value text for a scalar node.
1025    ///
1026    /// Plain scalars have trailing inline comments stripped, single-quoted
1027    /// scalars unescape doubled apostrophes, and double-quoted scalars unescape
1028    /// the common JSON/YAML escapes used by typed overlays.
1029    ///
1030    /// # Errors
1031    ///
1032    /// Returns an error when `node` is unknown, is not a scalar node, has
1033    /// malformed node properties, or contains unsupported scalar escape syntax.
1034    pub fn scalar_value(&self, node: NodeId) -> Result<Cow<'_, str>, YamlError> {
1035        let node_ref = self.expect_node(node)?;
1036        if !matches!(
1037            node_ref.kind,
1038            NodeKind::Scalar | NodeKind::LiteralScalar | NodeKind::FoldedScalar
1039        ) {
1040            return Err(YamlError::new(
1041                Diagnostic::new(
1042                    DiagnosticKind::Semantic,
1043                    format!("expected scalar value, found {:?}", node_ref.kind),
1044                    node_ref.span,
1045                )
1046                .with_expected("Scalar, LiteralScalar, or FoldedScalar"),
1047            )
1048            .with_position_from(&self.source));
1049        }
1050        let text = self.source.slice(node_ref.span);
1051        let properties = parse_node_properties(text, node_ref.span)?;
1052        let value_text = &text[properties.value_start..];
1053        if matches!(
1054            self.semantic_kind(node),
1055            Some(SemanticKind::Scalar {
1056                style: crate::YamlScalarStyle::Plain,
1057                ..
1058            })
1059        ) && !value_text.contains(['\n', '\r'])
1060        {
1061            return Ok(Cow::Borrowed(&value_text[..plain_scalar_end(value_text)]));
1062        }
1063        decode_scalar_value_with_content_indent(
1064            value_text,
1065            self.semantics
1066                .properties(node)
1067                .and_then(|properties| properties.content_indent)
1068                .map(|indent| indent as usize),
1069        )
1070        .map(Cow::Owned)
1071    }
1072
1073    /// Returns the source span of a scalar whose decoded value can be borrowed
1074    /// byte-for-byte from the original input.
1075    ///
1076    /// This currently returns a span only for single-line plain scalars. Quoted,
1077    /// escaped, folded, literal, and multiline scalars require decoding and
1078    /// return `Ok(None)`.
1079    ///
1080    /// # Errors
1081    ///
1082    /// Returns an error when `node` is unknown, is not a scalar, or has malformed
1083    /// node properties.
1084    pub fn borrowable_scalar_span(&self, node: NodeId) -> Result<Option<Span>, YamlError> {
1085        let node_ref = self.expect_node(node)?;
1086        if !matches!(
1087            self.semantic_kind(node),
1088            Some(SemanticKind::Scalar {
1089                style: crate::YamlScalarStyle::Plain,
1090            })
1091        ) {
1092            if matches!(
1093                node_ref.kind,
1094                NodeKind::Scalar | NodeKind::LiteralScalar | NodeKind::FoldedScalar
1095            ) {
1096                return Ok(None);
1097            }
1098            return Err(YamlError::new(
1099                Diagnostic::new(
1100                    DiagnosticKind::Semantic,
1101                    format!("expected scalar value, found {:?}", node_ref.kind),
1102                    node_ref.span,
1103                )
1104                .with_expected("Scalar, LiteralScalar, or FoldedScalar"),
1105            )
1106            .with_position_from(&self.source));
1107        }
1108
1109        let text = self.source.slice(node_ref.span);
1110        if text.contains(['\n', '\r']) {
1111            return Ok(None);
1112        }
1113        let properties = parse_node_properties(text, node_ref.span)?;
1114        let value_text = &text[properties.value_start..];
1115        let value_len = plain_scalar_end(value_text);
1116        let start = Span::offset_from_usize(node_ref.span.start, properties.value_start);
1117        Ok(Some(Span::new(
1118            start,
1119            Span::offset_from_usize(start, value_len),
1120        )))
1121    }
1122
1123    /// Queues a scalar value replacement at `path` while preserving the existing
1124    /// scalar style where the editor can do so safely.
1125    ///
1126    /// Plain scalars remain plain, single-quoted scalars remain single-quoted,
1127    /// and double-quoted scalars remain double-quoted. Inline comments and
1128    /// trailing whitespace outside the scalar spelling are left untouched.
1129    ///
1130    /// # Errors
1131    ///
1132    /// Returns an error when `path` does not resolve to an existing scalar, the
1133    /// current scalar style cannot be rewritten safely, `value` cannot be
1134    /// represented in that style, or the queued edit conflicts with another
1135    /// pending edit.
1136    pub fn set_scalar(&mut self, path: &[&str], value: &str) -> Result<(), YamlError> {
1137        let node = self.get_path(path)?.ok_or_else(|| {
1138            YamlError::new(
1139                Diagnostic::new(
1140                    DiagnosticKind::Semantic,
1141                    format!("path `{}` does not exist", path.join(".")),
1142                    Span::empty(0),
1143                )
1144                .with_expected("an existing scalar node"),
1145            )
1146        })?;
1147
1148        let (span, style) = self.scalar_replacement_target(node)?;
1149        let replacement = format_scalar_value(value, style)?;
1150        self.queue_edit(span, replacement)
1151    }
1152
1153    /// Queues a patch that replaces the exact source span covered by `node`.
1154    ///
1155    /// The CST remains unchanged until the edited text is parsed again; callers
1156    /// can inspect the pending minimal-diff output through `doc.to_string()`.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Returns an error when `node` is unknown, `text` contains invalid YAML
1161    /// characters, or the replacement overlaps an existing pending edit.
1162    pub fn replace_node_text(
1163        &mut self,
1164        node: NodeId,
1165        text: impl Into<String>,
1166    ) -> Result<(), YamlError> {
1167        let span = self.expect_node(node)?.span;
1168        self.queue_edit(span, text.into())
1169    }
1170
1171    /// Queues insertion of a plain `key: value` entry into a block mapping.
1172    ///
1173    /// This low-level writer accepts raw plain scalar text. Use typed values or
1174    /// node fragments when quoting or schema-aware formatting is required.
1175    ///
1176    /// # Errors
1177    ///
1178    /// Returns an error when `mapping` is not a block mapping, `key` or `value`
1179    /// is not valid as a plain mapping fragment, or the insertion conflicts with
1180    /// another pending edit.
1181    pub fn insert_mapping_entry(
1182        &mut self,
1183        mapping: NodeId,
1184        key: &str,
1185        value: &str,
1186        style: MappingEntryStyle,
1187    ) -> Result<(), YamlError> {
1188        self.insert_mapping_entry_with_comment(mapping, key, value, style, None)
1189    }
1190
1191    /// Queues insertion of a plain `key: value` entry with optional preceding
1192    /// comment lines.
1193    ///
1194    /// Comments are emitted only for inserted entries; existing YAML comments are
1195    /// never overwritten by this helper.
1196    ///
1197    /// # Errors
1198    ///
1199    /// Returns an error when `mapping` is not a block mapping, `key`, `value`, or
1200    /// `comment` cannot be emitted as valid YAML text, or the insertion conflicts
1201    /// with another pending edit.
1202    pub fn insert_mapping_entry_with_comment(
1203        &mut self,
1204        mapping: NodeId,
1205        key: &str,
1206        value: &str,
1207        style: MappingEntryStyle,
1208        comment: Option<&str>,
1209    ) -> Result<(), YamlError> {
1210        let mapping_node = self.expect_node_kind(mapping, NodeKind::BlockMapping)?;
1211        let indent = match style {
1212            MappingEntryStyle::Inherit => self.node_indent(mapping_node),
1213            MappingEntryStyle::Indent(indent) => indent,
1214        };
1215        let insertion_offset = self.mapping_insertion_offset(mapping_node);
1216        let needs_leading_break =
1217            insertion_offset == self.source.len() && !self.source_ends_with_line_break();
1218        let preserve_paragraph_break = comment.is_some()
1219            && insertion_offset == self.source.len()
1220            && self.mapping_has_blank_line(mapping_node);
1221        let replacement = self.format_mapping_entry_replacement(
1222            indent,
1223            key,
1224            value,
1225            comment,
1226            needs_leading_break,
1227            preserve_paragraph_break,
1228        )?;
1229
1230        self.queue_edit(Span::empty_from_usize(insertion_offset), replacement)
1231    }
1232
1233    /// Queues insertion of a typed YAML value under `key` in a block mapping.
1234    ///
1235    /// # Errors
1236    ///
1237    /// Returns an error when `mapping` is not a block mapping, the value cannot
1238    /// be formatted as a block YAML fragment, or the insertion conflicts with an
1239    /// existing pending edit.
1240    pub fn insert_mapping_value_with_comment<T>(
1241        &mut self,
1242        mapping: NodeId,
1243        key: &str,
1244        value: &T,
1245        style: MappingEntryStyle,
1246        comment: Option<&str>,
1247    ) -> Result<(), YamlError>
1248    where
1249        T: ToYamlFragment,
1250    {
1251        let mapping_node = self.expect_node_kind(mapping, NodeKind::BlockMapping)?;
1252        let indent = match style {
1253            MappingEntryStyle::Inherit => self.node_indent(mapping_node),
1254            MappingEntryStyle::Indent(indent) => indent,
1255        };
1256        let insertion_offset = self.mapping_insertion_offset(mapping_node);
1257        let needs_leading_break =
1258            insertion_offset == self.source.len() && !self.source_ends_with_line_break();
1259        let preserve_paragraph_break = comment.is_some()
1260            && insertion_offset == self.source.len()
1261            && self.mapping_has_blank_line(mapping_node);
1262        let replacement = self.format_mapping_value_replacement(
1263            indent,
1264            key,
1265            value,
1266            comment,
1267            needs_leading_break,
1268            preserve_paragraph_break,
1269        )?;
1270
1271        self.queue_edit(Span::empty_from_usize(insertion_offset), replacement)
1272    }
1273
1274    /// Queues insertion of a typed YAML value according to declaration order.
1275    ///
1276    /// # Errors
1277    ///
1278    /// Returns an error when mapping lookup fails or the selected insertion
1279    /// cannot be formatted or queued.
1280    pub fn insert_mapping_value_ordered_with_comment<T>(
1281        &mut self,
1282        mapping: NodeId,
1283        key: &str,
1284        value: &T,
1285        style: MappingEntryStyle,
1286        comment: Option<&str>,
1287        ordered_keys: &[&str],
1288    ) -> Result<(), YamlError>
1289    where
1290        T: ToYamlFragment,
1291    {
1292        let mut next_entry = None;
1293        if let Some(position) = ordered_keys.iter().position(|ordered| *ordered == key) {
1294            for later_key in &ordered_keys[position + 1..] {
1295                if let Some(entry) = self.get_mapping_entry(mapping, later_key)? {
1296                    next_entry = Some(entry);
1297                    break;
1298                }
1299            }
1300        }
1301
1302        if let Some(next_entry) = next_entry {
1303            self.insert_mapping_value_before_with_comment(next_entry, key, value, style, comment)
1304        } else {
1305            self.insert_mapping_value_with_comment(mapping, key, value, style, comment)
1306        }
1307    }
1308
1309    /// Queues insertion of a plain `key: value` entry before `before_entry`.
1310    ///
1311    /// # Errors
1312    ///
1313    /// Returns an error when `before_entry` is not a mapping entry, `key`,
1314    /// `value`, or `comment` cannot be emitted as valid YAML text, or the
1315    /// insertion conflicts with another pending edit.
1316    pub fn insert_mapping_entry_before_with_comment(
1317        &mut self,
1318        before_entry: NodeId,
1319        key: &str,
1320        value: &str,
1321        style: MappingEntryStyle,
1322        comment: Option<&str>,
1323    ) -> Result<(), YamlError> {
1324        let before_node = self.expect_node_kind(before_entry, NodeKind::MappingEntry)?;
1325        let indent = match style {
1326            MappingEntryStyle::Inherit => self.node_indent(before_node),
1327            MappingEntryStyle::Indent(indent) => indent,
1328        };
1329        let insertion_offset = self.line_start_for_offset(before_node.span.start as usize);
1330        let replacement =
1331            self.format_mapping_entry_replacement(indent, key, value, comment, false, false)?;
1332
1333        self.queue_edit(Span::empty_from_usize(insertion_offset), replacement)
1334    }
1335
1336    /// Queues insertion of a typed YAML value before an existing mapping entry.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns an error when the insertion target is invalid, the value cannot
1341    /// be formatted, or the insertion conflicts with an existing pending edit.
1342    pub fn insert_mapping_value_before_with_comment<T>(
1343        &mut self,
1344        before_entry: NodeId,
1345        key: &str,
1346        value: &T,
1347        style: MappingEntryStyle,
1348        comment: Option<&str>,
1349    ) -> Result<(), YamlError>
1350    where
1351        T: ToYamlFragment,
1352    {
1353        let before_node = self.expect_node_kind(before_entry, NodeKind::MappingEntry)?;
1354        let indent = match style {
1355            MappingEntryStyle::Inherit => self.node_indent(before_node),
1356            MappingEntryStyle::Indent(indent) => indent,
1357        };
1358        let insertion_offset = self.line_start_for_offset(before_node.span.start as usize);
1359        let replacement =
1360            self.format_mapping_value_replacement(indent, key, value, comment, false, false)?;
1361
1362        self.queue_edit(Span::empty_from_usize(insertion_offset), replacement)
1363    }
1364
1365    /// Queues insertion according to a declaration-order key list.
1366    ///
1367    /// If a later key from `ordered_keys` already exists in `mapping`, the new
1368    /// entry is inserted before that entry. Otherwise this falls back to append
1369    /// insertion. This is the primitive behind `insert_order = "struct"`.
1370    ///
1371    /// # Errors
1372    ///
1373    /// Returns an error when mapping lookup fails, the selected insertion target
1374    /// has the wrong node kind, inserted text is invalid YAML, or the queued edit
1375    /// conflicts with another pending edit.
1376    pub fn insert_mapping_entry_ordered_with_comment(
1377        &mut self,
1378        mapping: NodeId,
1379        key: &str,
1380        value: &str,
1381        style: MappingEntryStyle,
1382        comment: Option<&str>,
1383        ordered_keys: &[&str],
1384    ) -> Result<(), YamlError> {
1385        let mut next_entry = None;
1386        if let Some(position) = ordered_keys.iter().position(|ordered| *ordered == key) {
1387            for later_key in &ordered_keys[position + 1..] {
1388                if let Some(entry) = self.get_mapping_entry(mapping, later_key)? {
1389                    next_entry = Some(entry);
1390                    break;
1391                }
1392            }
1393        }
1394
1395        if let Some(next_entry) = next_entry {
1396            self.insert_mapping_entry_before_with_comment(next_entry, key, value, style, comment)
1397        } else {
1398            self.insert_mapping_entry_with_comment(mapping, key, value, style, comment)
1399        }
1400    }
1401
1402    /// Queues removal of the mapping entry with `key` from `mapping` when it exists.
1403    ///
1404    /// The removal is line-wise, so comments and fields outside the selected entry
1405    /// remain byte-for-byte unchanged. Missing keys are a no-op.
1406    ///
1407    /// # Errors
1408    ///
1409    /// Returns an error when mapping lookup fails, the selected entry cannot be
1410    /// removed, or the removal overlaps an existing pending edit.
1411    pub fn remove_mapping_entry(&mut self, mapping: NodeId, key: &str) -> Result<(), YamlError> {
1412        let Some(entry) = self.get_mapping_entry(mapping, key)? else {
1413            return Ok(());
1414        };
1415        self.remove_node(entry)
1416    }
1417
1418    /// Queues line-wise removal edits for mapping entries whose keys are not allowed.
1419    ///
1420    /// This is the patch-emitter primitive used by typed overlays that choose to
1421    /// prune unknown fields. It preserves the order and bytes of retained entries.
1422    ///
1423    /// # Errors
1424    ///
1425    /// Returns an error when `mapping` is not a block mapping, a retained entry
1426    /// cannot be inspected as a scalar key, or a removal edit conflicts with
1427    /// another pending edit.
1428    pub fn retain_mapping_entries(
1429        &mut self,
1430        mapping: NodeId,
1431        allowed_keys: &[&str],
1432    ) -> Result<(), YamlError> {
1433        self.expect_node_kind(mapping, NodeKind::BlockMapping)?;
1434        let mut removals = Vec::new();
1435
1436        for entry in self.children(mapping) {
1437            let entry_node = self.expect_node(entry)?;
1438            if entry_node.kind != NodeKind::MappingEntry {
1439                continue;
1440            }
1441
1442            let Some(key_node) = self.children(entry).next() else {
1443                continue;
1444            };
1445            let key = self.scalar_text(key_node)?;
1446            if !allowed_keys.contains(&key) {
1447                removals.push(entry);
1448            }
1449        }
1450
1451        for entry in removals {
1452            self.remove_node(entry)?;
1453        }
1454
1455        Ok(())
1456    }
1457
1458    /// Queues removal of `node` from the rendered document.
1459    ///
1460    /// Mapping and sequence entries are removed line-wise, including their line
1461    /// break when one is present. Other nodes use their exact source span.
1462    ///
1463    /// # Errors
1464    ///
1465    /// Returns an error when `node` is unknown or the removal overlaps an
1466    /// existing pending edit.
1467    pub fn remove_node(&mut self, node: NodeId) -> Result<(), YamlError> {
1468        let node = self.expect_node(node)?;
1469        let span = if matches!(node.kind, NodeKind::MappingEntry | NodeKind::SequenceEntry) {
1470            self.line_span_including_break(node.span)
1471        } else {
1472            node.span
1473        };
1474        self.queue_edit(span, String::new())
1475    }
1476
1477    pub(crate) fn scalar_replacement_target(
1478        &self,
1479        node: NodeId,
1480    ) -> Result<(Span, ScalarStyle), YamlError> {
1481        let node = self.expect_node_kind(node, NodeKind::Scalar)?;
1482        let text = self.source.slice(node.span);
1483        let properties = parse_node_properties(text, node.span)?;
1484        let value_text = &text[properties.value_start..];
1485        let value_start = Span::offset_from_usize(node.span.start, properties.value_start);
1486
1487        if value_text.starts_with('"') {
1488            let end = double_quoted_scalar_end(value_text).ok_or_else(|| {
1489                YamlError::new(
1490                    Diagnostic::new(
1491                        DiagnosticKind::Emitter,
1492                        "could not find the end of the double-quoted scalar",
1493                        node.span,
1494                    )
1495                    .with_expected("a closed double-quoted scalar"),
1496                )
1497            })?;
1498            return Ok((
1499                Span::new(value_start, Span::offset_from_usize(value_start, end)),
1500                ScalarStyle::DoubleQuoted,
1501            ));
1502        }
1503
1504        if value_text.starts_with('\'') {
1505            let end = single_quoted_scalar_end(value_text).ok_or_else(|| {
1506                YamlError::new(
1507                    Diagnostic::new(
1508                        DiagnosticKind::Emitter,
1509                        "could not find the end of the single-quoted scalar",
1510                        node.span,
1511                    )
1512                    .with_expected("a closed single-quoted scalar"),
1513                )
1514            })?;
1515            return Ok((
1516                Span::new(value_start, Span::offset_from_usize(value_start, end)),
1517                ScalarStyle::SingleQuoted,
1518            ));
1519        }
1520
1521        let end = plain_scalar_end(value_text);
1522        if end == 0 {
1523            return Err(YamlError::new(
1524                Diagnostic::new(
1525                    DiagnosticKind::Emitter,
1526                    "could not find plain scalar text to replace",
1527                    node.span,
1528                )
1529                .with_expected("plain scalar text"),
1530            ));
1531        }
1532
1533        Ok((
1534            Span::new(value_start, Span::offset_from_usize(value_start, end)),
1535            ScalarStyle::Plain,
1536        ))
1537    }
1538
1539    pub(crate) fn collection_replacement_target(
1540        &self,
1541        node: NodeId,
1542    ) -> Result<CollectionTarget, YamlError> {
1543        let node = self.expect_node(node)?;
1544        let text = self.source.slice(node.span);
1545        let properties = parse_node_properties(text, node.span)?;
1546        let mut body_start = properties.value_start;
1547
1548        if text[body_start..].starts_with(['\r', '\n']) {
1549            body_start = next_line_content_start(text, body_start);
1550        }
1551
1552        let start = Span::offset_from_usize(node.span.start, body_start);
1553        Ok(CollectionTarget {
1554            span: Span::new(start, node.span.end),
1555            indent: self.line_indent_for_offset(start as usize),
1556        })
1557    }
1558
1559    fn directive_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
1560        self.root()
1561            .into_iter()
1562            .flat_map(|root| self.children(root))
1563            .filter(|node| {
1564                self.node(*node)
1565                    .is_some_and(|node| node.kind == NodeKind::Directive)
1566            })
1567    }
1568
1569    fn parse_directive_node(&self, node: NodeId) -> Result<ParsedDirective, YamlError> {
1570        let node_ref = self.expect_node_kind(node, NodeKind::Directive)?;
1571        let body = strip_inline_comment(self.source.slice(node_ref.span)).trim();
1572        let mut parts = body.split_whitespace();
1573        let Some(name) = parts.next() else {
1574            return Err(directive_emit_error(
1575                "directive is missing a name",
1576                node_ref.span,
1577                "%YAML, %TAG, or reserved directive syntax",
1578            )
1579            .with_position_from(&self.source));
1580        };
1581
1582        Ok(match name {
1583            "%YAML" => ParsedDirective::Yaml(YamlDirective {
1584                version: parts.next().unwrap_or_default().to_owned(),
1585                node,
1586            }),
1587            "%TAG" => ParsedDirective::Tag(TagDirective {
1588                handle: parts.next().unwrap_or_default().to_owned(),
1589                prefix: parts.next().unwrap_or_default().to_owned(),
1590                node,
1591            }),
1592            _ => ParsedDirective::Reserved(ReservedDirective {
1593                name: name.to_owned(),
1594                parameters: parts.map(str::to_owned).collect(),
1595                node,
1596            }),
1597        })
1598    }
1599
1600    fn directive_content_span(&self, node: NodeId) -> Result<Span, YamlError> {
1601        let node = self.expect_node_kind(node, NodeKind::Directive)?;
1602        let text = self.source.slice(node.span);
1603        let end = strip_inline_comment(text)
1604            .trim_end_matches([' ', '\t'])
1605            .len();
1606        Ok(Span::new(
1607            node.span.start,
1608            Span::offset_from_usize(node.span.start, end),
1609        ))
1610    }
1611
1612    fn insert_directive_line(&mut self, replacement: String) -> Result<(), YamlError> {
1613        let insertion_offset = self.directive_insertion_offset();
1614        let mut line = replacement;
1615        line.push_str(self.preferred_line_ending());
1616        self.queue_edit(Span::empty_from_usize(insertion_offset), line)
1617    }
1618
1619    fn remove_directive_node(&mut self, node: NodeId) -> Result<(), YamlError> {
1620        let node = self.expect_node_kind(node, NodeKind::Directive)?;
1621        self.queue_edit(self.line_span_including_break(node.span), String::new())
1622    }
1623
1624    fn directive_insertion_offset(&self) -> usize {
1625        if let Some(last_directive) = self
1626            .directive_nodes()
1627            .filter_map(|node| self.node(node))
1628            .max_by_key(|node| node.span.start)
1629        {
1630            return self.line_span_including_break(last_directive.span).end as usize;
1631        }
1632
1633        self.root()
1634            .and_then(|root| self.children(root).next())
1635            .and_then(|node| self.node(node))
1636            .map_or(0, |node| {
1637                self.line_start_for_offset(node.span.start as usize)
1638            })
1639    }
1640
1641    pub(crate) fn expect_node(&self, node: NodeId) -> Result<&Node, YamlError> {
1642        self.node(node).ok_or_else(|| {
1643            YamlError::new(Diagnostic::new(
1644                DiagnosticKind::Semantic,
1645                format!("unknown node id {}", node.0),
1646                Span::empty_from_usize(self.source.len()),
1647            ))
1648        })
1649    }
1650
1651    pub(crate) fn expect_node_kind(
1652        &self,
1653        node: NodeId,
1654        expected: NodeKind,
1655    ) -> Result<&Node, YamlError> {
1656        let actual = self.expect_node(node)?;
1657        if actual.kind == expected {
1658            Ok(actual)
1659        } else {
1660            Err(YamlError::new(
1661                Diagnostic::new(
1662                    DiagnosticKind::Semantic,
1663                    format!("expected {expected:?}, found {:?}", actual.kind),
1664                    actual.span,
1665                )
1666                .with_expected(format!("{expected:?}")),
1667            )
1668            .with_position_from(&self.source))
1669        }
1670    }
1671
1672    pub(crate) fn containing_entry(&self, value: NodeId) -> Option<NodeId> {
1673        self.node(value).and_then(Node::parent).filter(|parent| {
1674            self.node(*parent).is_some_and(|node| {
1675                matches!(node.kind, NodeKind::MappingEntry | NodeKind::SequenceEntry)
1676            })
1677        })
1678    }
1679
1680    fn mapping_has_blank_line(&self, mapping: &Node) -> bool {
1681        let start = self.line_start_for_offset(mapping.span.start as usize);
1682        let end = mapping.span.end as usize;
1683        let text = &self.source.as_str()[start..end];
1684        text.contains("\n\n") || text.contains("\r\n\r\n")
1685    }
1686
1687    fn format_mapping_entry_replacement(
1688        &self,
1689        indent: usize,
1690        key: &str,
1691        value: &str,
1692        comment: Option<&str>,
1693        needs_leading_break: bool,
1694        preserve_paragraph_break: bool,
1695    ) -> Result<String, YamlError> {
1696        validate_plain_mapping_fragment(key, "mapping key")?;
1697        validate_plain_mapping_fragment(value, "mapping value")?;
1698        if let Some(comment) = comment {
1699            validate_yaml_chars(comment)?;
1700        }
1701
1702        let line_ending = self.preferred_line_ending();
1703        let indent_text = " ".repeat(indent);
1704        let mut replacement = String::new();
1705        if needs_leading_break {
1706            replacement.push_str(line_ending);
1707        }
1708        if preserve_paragraph_break {
1709            replacement.push_str(line_ending);
1710        }
1711        if let Some(comment) = comment {
1712            for line in comment.lines() {
1713                replacement.push_str(&indent_text);
1714                replacement.push('#');
1715                if !line.is_empty() {
1716                    replacement.push(' ');
1717                    replacement.push_str(line.trim());
1718                }
1719                replacement.push_str(line_ending);
1720            }
1721        }
1722        replacement.push_str(&indent_text);
1723        replacement.push_str(key);
1724        replacement.push_str(": ");
1725        replacement.push_str(value);
1726        replacement.push_str(line_ending);
1727        Ok(replacement)
1728    }
1729
1730    fn format_mapping_value_replacement<T>(
1731        &self,
1732        indent: usize,
1733        key: &str,
1734        value: &T,
1735        comment: Option<&str>,
1736        needs_leading_break: bool,
1737        preserve_paragraph_break: bool,
1738    ) -> Result<String, YamlError>
1739    where
1740        T: ToYamlFragment,
1741    {
1742        validate_plain_mapping_fragment(key, "mapping key")?;
1743        if let Some(comment) = comment {
1744            validate_yaml_chars(comment)?;
1745        }
1746
1747        let line_ending = self.preferred_line_ending();
1748        let indent_text = " ".repeat(indent);
1749        let child_indent = indent + 2;
1750        let fragment = value.to_yaml_fragment(child_indent, line_ending)?;
1751        let mut replacement = String::new();
1752        if needs_leading_break {
1753            replacement.push_str(line_ending);
1754        }
1755        if preserve_paragraph_break {
1756            replacement.push_str(line_ending);
1757        }
1758        if let Some(comment) = comment {
1759            for line in comment.lines() {
1760                replacement.push_str(&indent_text);
1761                replacement.push('#');
1762                if !line.is_empty() {
1763                    replacement.push(' ');
1764                    replacement.push_str(line.trim());
1765                }
1766                replacement.push_str(line_ending);
1767            }
1768        }
1769        replacement.push_str(&indent_text);
1770        replacement.push_str(key);
1771        if fragment.contains('\n') || fragment.starts_with(' ') {
1772            replacement.push(':');
1773            replacement.push_str(line_ending);
1774            replacement.push_str(&fragment);
1775        } else {
1776            replacement.push_str(": ");
1777            replacement.push_str(&fragment);
1778        }
1779        replacement.push_str(line_ending);
1780        Ok(replacement)
1781    }
1782
1783    pub(crate) fn node_indent(&self, node: &Node) -> usize {
1784        let line_start = self.line_start_for_offset(node.span.start as usize);
1785        self.source.as_str()[line_start..node.span.start as usize]
1786            .bytes()
1787            .filter(|byte| *byte == b' ')
1788            .count()
1789    }
1790
1791    fn line_indent_for_offset(&self, offset: usize) -> usize {
1792        let line_start = self.line_start_for_offset(offset);
1793        self.source.as_str()[line_start..offset]
1794            .bytes()
1795            .filter(|byte| *byte == b' ')
1796            .count()
1797    }
1798
1799    fn line_start_for_offset(&self, offset: usize) -> usize {
1800        let offset = Span::usize_to_u32(offset);
1801        match self.source.line_starts().binary_search(&offset) {
1802            Ok(index) => self.source.line_starts()[index] as usize,
1803            Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
1804        }
1805    }
1806
1807    pub(crate) fn find_nested_collection_after(
1808        &self,
1809        entry: &Node,
1810        parent_indent: usize,
1811    ) -> Option<NodeId> {
1812        self.nodes
1813            .iter()
1814            .enumerate()
1815            .filter(|(_, node)| {
1816                matches!(node.kind, NodeKind::BlockMapping | NodeKind::BlockSequence)
1817                    && node.span.start >= entry.span.end
1818                    && self.node_indent(node) > parent_indent
1819            })
1820            .min_by_key(|(_, node)| node.span.start)
1821            .map(|(index, _)| NodeId::from_usize(index))
1822    }
1823
1824    pub(crate) fn block_scalar_content_indent(&self, scalar: &Node) -> Option<usize> {
1825        let text = self.source.slice(scalar.span);
1826        let header_end = text.find(['\r', '\n'])?;
1827        let mut rest = &text[header_end..];
1828        while let Some(stripped) = rest.strip_prefix('\r').or_else(|| rest.strip_prefix('\n')) {
1829            rest = stripped;
1830        }
1831        for line in rest.lines() {
1832            if line.trim().is_empty() {
1833                continue;
1834            }
1835            return Some(line.bytes().take_while(|byte| *byte == b' ').count());
1836        }
1837        None
1838    }
1839
1840    pub(crate) fn queue_edit(&mut self, span: Span, replacement: String) -> Result<(), YamlError> {
1841        self.source.try_slice(span)?;
1842        validate_yaml_chars(&replacement)?;
1843
1844        if span.is_empty()
1845            && let Some(existing) = self
1846                .edits
1847                .iter_mut()
1848                .find(|edit| edit.span.is_empty() && edit.span.start == span.start)
1849        {
1850            existing.replacement.push_str(&replacement);
1851            return Ok(());
1852        }
1853
1854        if let Some(existing) = self
1855            .edits
1856            .iter()
1857            .find(|edit| edits_conflict(edit.span, span))
1858        {
1859            return Err(YamlError::new(
1860                Diagnostic::new(
1861                    DiagnosticKind::Emitter,
1862                    "edit overlaps an existing pending edit",
1863                    span,
1864                )
1865                .with_note(format!(
1866                    "existing edit covers bytes {}..{}",
1867                    existing.span.start, existing.span.end
1868                )),
1869            )
1870            .with_position_from(&self.source));
1871        }
1872
1873        self.edits.push(Edit { span, replacement });
1874        Ok(())
1875    }
1876
1877    pub(crate) fn mapping_insertion_offset(&self, mapping: &Node) -> usize {
1878        node_link(mapping.last_child)
1879            .and_then(|child| self.node(child))
1880            .map_or(mapping.span.end as usize, |last_child| {
1881                self.line_span_including_break(last_child.span).end as usize
1882            })
1883    }
1884
1885    pub(crate) fn sequence_insertion_offset(&self, sequence: &Node) -> usize {
1886        node_link(sequence.last_child)
1887            .and_then(|child| self.node(child))
1888            .map_or(sequence.span.end as usize, |last_child| {
1889                self.line_span_including_break(last_child.span).end as usize
1890            })
1891    }
1892
1893    fn line_span_including_break(&self, span: Span) -> Span {
1894        let start = self.line_start_for_offset(span.start as usize);
1895        let mut end = span.end as usize;
1896        let bytes = self.source.as_str().as_bytes();
1897
1898        if end < bytes.len() {
1899            if bytes[end] == b'\r' {
1900                end += 1;
1901                if end < bytes.len() && bytes[end] == b'\n' {
1902                    end += 1;
1903                }
1904            } else if bytes[end] == b'\n' {
1905                end += 1;
1906            }
1907        }
1908
1909        Span::from_usize(start, end)
1910    }
1911
1912    pub(crate) fn preferred_line_ending(&self) -> &str {
1913        let bytes = self.source.as_str().as_bytes();
1914        for (index, byte) in bytes.iter().enumerate() {
1915            if *byte == b'\r' {
1916                return if bytes.get(index + 1) == Some(&b'\n') {
1917                    "\r\n"
1918                } else {
1919                    "\r"
1920                };
1921            }
1922            if *byte == b'\n' {
1923                return if index > 0 && bytes[index - 1] == b'\r' {
1924                    "\r\n"
1925                } else {
1926                    "\n"
1927                };
1928            }
1929        }
1930        "\n"
1931    }
1932
1933    fn source_ends_with_line_break(&self) -> bool {
1934        self.source
1935            .as_str()
1936            .as_bytes()
1937            .last()
1938            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
1939    }
1940
1941    fn document_append_prefix(&self, line_ending: &str) -> String {
1942        if self.source.as_str().is_empty() || self.source_ends_with_line_break() {
1943            String::new()
1944        } else {
1945            line_ending.to_owned()
1946        }
1947    }
1948}
1949impl fmt::Display for YamlDoc {
1950    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1951        if self.edits.is_empty() {
1952            return formatter.write_str(self.source.as_str());
1953        }
1954
1955        let mut output = self.source.as_str().to_owned();
1956        let mut edits = self.edits.clone();
1957        edits.sort_by_key(|edit| std::cmp::Reverse(edit.span.start));
1958
1959        for edit in edits {
1960            output.replace_range(
1961                edit.span.start as usize..edit.span.end as usize,
1962                &edit.replacement,
1963            );
1964        }
1965
1966        formatter.write_str(&output)
1967    }
1968}