Skip to main content

yaml_rt_core/
edit.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt;
3
4use crate::fragment::indent_text;
5use crate::pointer::parse_sequence_index;
6use crate::{
7    CollectionStyle, Diagnostic, DiagnosticKind, FragmentError, JsonPointer, NodeId, PointerError,
8    ResolvedScalar, SemanticKind, SemanticValueError, Span, YamlDoc, YamlError, YamlFragment,
9    YamlScalarStyle, resolve_scalar, semantically_equal,
10};
11
12/// Failure while applying a pointer-addressed YAML edit.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct YamlEditError {
15    message: String,
16}
17
18impl YamlEditError {
19    pub(crate) fn new(message: impl Into<String>) -> Self {
20        Self {
21            message: message.into(),
22        }
23    }
24
25    pub(crate) fn into_yaml_error(self) -> YamlError {
26        YamlError::new(
27            Diagnostic::new(DiagnosticKind::Emitter, self.message, Span::empty(0))
28                .with_expected("a source-preserving YAML edit"),
29        )
30    }
31}
32
33impl fmt::Display for YamlEditError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter.write_str(&self.message)
36    }
37}
38
39impl std::error::Error for YamlEditError {}
40
41impl From<PointerError> for YamlEditError {
42    fn from(error: PointerError) -> Self {
43        Self::new(error.to_string())
44    }
45}
46
47impl From<FragmentError> for YamlEditError {
48    fn from(error: FragmentError) -> Self {
49        Self::new(error.to_string())
50    }
51}
52
53impl From<YamlError> for YamlEditError {
54    fn from(error: YamlError) -> Self {
55        Self::new(error.to_string())
56    }
57}
58
59impl From<SemanticValueError> for YamlEditError {
60    fn from(error: SemanticValueError) -> Self {
61        Self::new(error.to_string())
62    }
63}
64
65enum AddLocation {
66    Root(NodeId),
67    Mapping {
68        mapping: NodeId,
69        existing: Option<NodeId>,
70        key: String,
71    },
72    Sequence {
73        sequence: NodeId,
74        index: usize,
75    },
76}
77
78#[derive(Clone, Copy)]
79struct RenameTarget {
80    mapping: NodeId,
81    key: NodeId,
82}
83
84impl YamlDoc {
85    /// Applies RFC 6902 `add` semantics at a JSON Pointer destination.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error when the document or destination does not exist, the
90    /// destination cannot accept the value, or the edit cannot be emitted.
91    pub fn add_at(
92        &mut self,
93        document: usize,
94        pointer: &JsonPointer,
95        value: &YamlFragment,
96    ) -> Result<(), YamlEditError> {
97        self.transaction(|work| {
98            let location = work.resolve_add_location(document, pointer)?;
99            work.queue_add(location, value)
100        })
101    }
102
103    /// Removes an existing value. Removing a document root is unsupported.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error when the document or target does not exist, the target
108    /// is a document root, or removing it would invalidate the document.
109    pub fn remove_at(
110        &mut self,
111        document: usize,
112        pointer: &JsonPointer,
113    ) -> Result<(), YamlEditError> {
114        if pointer.is_root() {
115            return Err(YamlEditError::new(
116                "removing a YAML document root is not supported",
117            ));
118        }
119        self.transaction(|work| {
120            let target = work.resolve_pointer(document, pointer)?;
121            work.queue_value_removal(target)
122        })
123    }
124
125    /// Replaces an existing value while preserving its surrounding syntax.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error when the document or target does not exist or the
130    /// replacement cannot be emitted at that location.
131    pub fn replace_at(
132        &mut self,
133        document: usize,
134        pointer: &JsonPointer,
135        value: &YamlFragment,
136    ) -> Result<(), YamlEditError> {
137        self.transaction(|work| {
138            let target = work.resolve_pointer(document, pointer)?;
139            work.queue_fragment_replacement(target, value)
140        })
141    }
142
143    /// Moves a value using RFC 6902 remove-then-add semantics.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error when either pointer is invalid for the document, the
148    /// move would be recursive, or anchors, aliases, or syntax prevent it.
149    pub fn move_at(
150        &mut self,
151        document: usize,
152        from: &JsonPointer,
153        path: &JsonPointer,
154    ) -> Result<(), YamlEditError> {
155        if from == path {
156            return Ok(());
157        }
158        if from.is_proper_prefix_of(path) {
159            return Err(YamlEditError::new(
160                "move source must not be a proper prefix of its destination",
161            ));
162        }
163        let mut work = self.clone();
164        let source = work.resolve_pointer(document, from)?;
165        if work.anchor_has_external_alias(source) {
166            return Err(YamlEditError::new(
167                "cannot move anchored subtree because an alias outside it depends on that anchor",
168            ));
169        }
170        let fragment = YamlFragment::from_document_node(&work, source)?;
171        if from.is_root() {
172            return Err(YamlEditError::new(
173                "a document root cannot be moved into another location",
174            ));
175        }
176        work.queue_value_removal(source)?;
177        work.commit_edits()?;
178        let location = work.resolve_add_location(document, path)?;
179        work.queue_add(location, &fragment)?;
180        work.commit_edits()?;
181        *self = work;
182        Ok(())
183    }
184
185    /// Deep-copies a value using RFC 6902 `copy` semantics.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error when either pointer is invalid for the document, the
190    /// source contains an anchor, or the copied value cannot be inserted.
191    pub fn copy_at(
192        &mut self,
193        document: usize,
194        from: &JsonPointer,
195        path: &JsonPointer,
196    ) -> Result<(), YamlEditError> {
197        let source = self.resolve_pointer(document, from)?;
198        let fragment = YamlFragment::from_document_node(self, source)?;
199        if fragment.contains_anchor() {
200            return Err(YamlEditError::new(format!(
201                "cannot copy {:?}: subtree contains an anchor",
202                from.as_str()
203            )));
204        }
205        self.add_at(document, path, &fragment)
206    }
207
208    /// Compares a pointer-selected target with a YAML value.
209    ///
210    /// # Errors
211    ///
212    /// Returns an error when the document or target does not exist or either
213    /// value cannot be compared using RFC 6902 equality.
214    pub fn test_at(
215        &self,
216        document: usize,
217        pointer: &JsonPointer,
218        value: &YamlFragment,
219    ) -> Result<bool, YamlEditError> {
220        let target = self.resolve_pointer(document, pointer)?;
221        semantically_equal(self, target, value.document(), value.root()).map_err(Into::into)
222    }
223
224    /// Renames the mapping key that owns a pointer-selected value.
225    ///
226    /// The operation is transactional and changes only the key scalar spelling.
227    /// The pointer must select a mapping member; document roots and sequence
228    /// elements do not have a key to rename.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error when the pointer does not select a supported string key,
233    /// the destination key would collide with another mapping member, or the
234    /// edited document cannot be emitted.
235    pub fn rename_key_at(
236        &mut self,
237        document: usize,
238        pointer: &JsonPointer,
239        new_key: &str,
240    ) -> Result<(), YamlEditError> {
241        self.rename_keys_at(document, std::slice::from_ref(pointer), new_key)
242    }
243
244    /// Renames the mapping keys that own several pointer-selected values.
245    ///
246    /// Targets are resolved against the original document and duplicate source
247    /// key nodes are edited once. All collision checks and edits are applied as
248    /// one transaction. An empty pointer slice succeeds without changing the
249    /// document.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error when any pointer does not select a supported string key,
254    /// any affected mapping would contain duplicate final keys, or the edited
255    /// document cannot be emitted.
256    pub fn rename_keys_at(
257        &mut self,
258        document: usize,
259        pointers: &[JsonPointer],
260        new_key: &str,
261    ) -> Result<(), YamlEditError> {
262        if pointers.is_empty() {
263            return Ok(());
264        }
265
266        let mut work = self.clone();
267        let mut seen_keys = HashSet::new();
268        let mut targets = Vec::new();
269        for pointer in pointers {
270            let target = work.resolve_rename_target(document, pointer)?;
271            if seen_keys.insert(target.key) {
272                targets.push(target);
273            }
274        }
275
276        work.validate_rename_targets(&targets, new_key)?;
277        for target in targets {
278            work.queue_key_rename(target.key, new_key)?;
279        }
280        work.commit_edits()?;
281        *self = work;
282        Ok(())
283    }
284
285    fn transaction(
286        &mut self,
287        operation: impl FnOnce(&mut YamlDoc) -> Result<(), YamlEditError>,
288    ) -> Result<(), YamlEditError> {
289        let mut work = self.clone();
290        operation(&mut work)?;
291        work.commit_edits()?;
292        *self = work;
293        Ok(())
294    }
295
296    fn resolve_rename_target(
297        &self,
298        document: usize,
299        pointer: &JsonPointer,
300    ) -> Result<RenameTarget, YamlEditError> {
301        let Some((parent_pointer, token)) = pointer.parent() else {
302            return Err(YamlEditError::new(
303                "a YAML document root does not have a mapping key to rename",
304            ));
305        };
306        let token_index = pointer.tokens().len().saturating_sub(1);
307        let mut parent = self.resolve_pointer(document, &parent_pointer)?;
308        parent = self.resolve_aliases_for_pointer(parent, pointer, token_index)?;
309        if !matches!(
310            self.semantic_kind(parent),
311            Some(SemanticKind::Mapping { .. })
312        ) {
313            return Err(YamlEditError::new(format!(
314                "JSON Pointer {:?} does not select a mapping member",
315                pointer.as_str()
316            )));
317        }
318        let matched = self
319            .mapping_match(parent, token, pointer, token_index)?
320            .ok_or_else(|| {
321                YamlEditError::new(format!(
322                    "mapping has no member {:?} to rename",
323                    token.as_str()
324                ))
325            })?;
326        Ok(RenameTarget {
327            mapping: parent,
328            key: matched.key,
329        })
330    }
331
332    fn validate_rename_targets(
333        &self,
334        targets: &[RenameTarget],
335        new_key: &str,
336    ) -> Result<(), YamlEditError> {
337        crate::validate_yaml_chars(new_key)?;
338        let mut targets_by_mapping = HashMap::<NodeId, HashSet<NodeId>>::new();
339        for target in targets {
340            self.validate_rename_key_node(target.key)?;
341            targets_by_mapping
342                .entry(target.mapping)
343                .or_default()
344                .insert(target.key);
345        }
346
347        for (mapping, renamed_keys) in targets_by_mapping {
348            let mut final_keys = HashSet::new();
349            for (key, _) in self.mapping_entries(mapping) {
350                let decoded = if renamed_keys.contains(&key) {
351                    new_key.to_owned()
352                } else {
353                    self.string_mapping_key(key)?
354                };
355                if !final_keys.insert(decoded.clone()) {
356                    return Err(YamlEditError::new(format!(
357                        "renaming a mapping key to {new_key:?} would create duplicate key {decoded:?}"
358                    )));
359                }
360            }
361        }
362        Ok(())
363    }
364
365    fn validate_rename_key_node(&self, key: NodeId) -> Result<(), YamlEditError> {
366        if self
367            .node(key)
368            .is_none_or(|node| node.kind() != crate::NodeKind::Scalar)
369            || !matches!(self.semantic_kind(key), Some(SemanticKind::Scalar { .. }))
370        {
371            return Err(YamlEditError::new(
372                "mapping-key rename supports only plain, single-quoted, and double-quoted string keys",
373            ));
374        }
375        if !scalar_is_string(self, key)? {
376            return Err(YamlEditError::new(
377                "mapping-key rename target is not a string scalar key",
378            ));
379        }
380        Ok(())
381    }
382
383    fn string_mapping_key(&self, key: NodeId) -> Result<String, YamlEditError> {
384        let mut resolved = key;
385        let mut seen = HashSet::new();
386        while matches!(self.semantic_kind(resolved), Some(SemanticKind::Alias)) {
387            if !seen.insert(resolved) {
388                return Err(YamlEditError::new("cyclic YAML alias key"));
389            }
390            resolved = self
391                .resolve_alias(resolved)
392                .ok_or_else(|| YamlEditError::new("unresolved YAML alias key"))?;
393        }
394        if !scalar_is_string(self, resolved)? {
395            return Err(YamlEditError::new(
396                "affected mapping contains a non-string key",
397            ));
398        }
399        self.scalar_value(resolved)
400            .map(std::borrow::Cow::into_owned)
401            .map_err(Into::into)
402    }
403
404    fn queue_key_rename(&mut self, key: NodeId, new_key: &str) -> Result<(), YamlEditError> {
405        let current = self.scalar_value(key)?;
406        if current == new_key {
407            return Ok(());
408        }
409        let (span, style) = self.scalar_replacement_target(key)?;
410        let tag = self.resolved_tag(key)?;
411        let replacement = match style {
412            crate::ScalarStyle::Plain if safe_plain_key_with_tag(new_key, tag.as_deref()) => {
413                new_key.to_owned()
414            }
415            crate::ScalarStyle::Plain => crate::fragment::quote_string(new_key),
416            style => crate::format_scalar_value(new_key, style)
417                .unwrap_or_else(|_| crate::fragment::quote_string(new_key)),
418        };
419        self.queue_edit(span, replacement)?;
420        Ok(())
421    }
422
423    fn resolve_add_location(
424        &self,
425        document: usize,
426        pointer: &JsonPointer,
427    ) -> Result<AddLocation, YamlEditError> {
428        let Some((parent_pointer, token)) = pointer.parent() else {
429            let root = self
430                .document_root(document)?
431                .ok_or_else(|| YamlEditError::new("selected document has no root node"))?;
432            return Ok(AddLocation::Root(root));
433        };
434        let mut parent = self.resolve_pointer(document, &parent_pointer)?;
435        parent = self.resolve_aliases_for_pointer(
436            parent,
437            pointer,
438            pointer.tokens().len().saturating_sub(1),
439        )?;
440        match self.semantic_kind(parent) {
441            Some(SemanticKind::Mapping { .. }) => {
442                let existing = self
443                    .mapping_match(
444                        parent,
445                        token,
446                        pointer,
447                        pointer.tokens().len().saturating_sub(1),
448                    )?
449                    .map(|entry| entry.value);
450                Ok(AddLocation::Mapping {
451                    mapping: parent,
452                    existing,
453                    key: token.as_str().to_owned(),
454                })
455            }
456            Some(SemanticKind::Sequence { .. }) => {
457                let length = self.sequence_items(parent).count();
458                let parsed = parse_sequence_index(
459                    token,
460                    pointer,
461                    pointer.tokens().len().saturating_sub(1),
462                    true,
463                )?;
464                let index = if parsed == usize::MAX { length } else { parsed };
465                if index > length {
466                    return Err(YamlEditError::new(format!(
467                        "sequence index {index} is out of bounds for insertion into length {length}"
468                    )));
469                }
470                Ok(AddLocation::Sequence {
471                    sequence: parent,
472                    index,
473                })
474            }
475            _ => Err(YamlEditError::new(format!(
476                "add parent {:?} is not a mapping or sequence",
477                parent_pointer.as_str()
478            ))),
479        }
480    }
481
482    fn queue_add(
483        &mut self,
484        location: AddLocation,
485        value: &YamlFragment,
486    ) -> Result<(), YamlEditError> {
487        match location {
488            AddLocation::Root(root) => self.queue_fragment_replacement(root, value),
489            AddLocation::Mapping {
490                existing: Some(existing),
491                ..
492            } => self.queue_fragment_replacement(existing, value),
493            AddLocation::Mapping {
494                mapping,
495                existing: None,
496                key,
497            } => self.queue_mapping_insert(mapping, &key, value),
498            AddLocation::Sequence { sequence, index } => {
499                self.queue_sequence_insert(sequence, index, value)
500            }
501        }
502    }
503
504    pub(crate) fn queue_fragment_replacement(
505        &mut self,
506        target: NodeId,
507        value: &YamlFragment,
508    ) -> Result<(), YamlEditError> {
509        if self
510            .node(target)
511            .is_some_and(|node| node.kind() == crate::NodeKind::Scalar)
512            && matches!(
513                value.document().semantic_kind(value.root()),
514                Some(SemanticKind::Scalar { .. })
515            )
516            && value.document().raw_tag(value.root()).is_none()
517            && value.document().anchor(value.root()).is_none()
518        {
519            let mut replacement = value.to_yaml()?;
520            if !replacement.contains(['\n', '\r']) {
521                let (span, target_style) = self.scalar_replacement_target(target)?;
522                if scalar_is_string(self, target)?
523                    && scalar_is_string(value.document(), value.root())?
524                {
525                    let decoded = value.document().scalar_value(value.root())?;
526                    if let Ok(styled) = crate::format_scalar_value(&decoded, target_style) {
527                        replacement = styled;
528                    }
529                }
530                self.queue_edit(span, replacement)?;
531                return Ok(());
532            }
533        }
534        self.queue_fragment_replacement_whole(target, value)
535    }
536
537    pub(crate) fn queue_fragment_replacement_whole(
538        &mut self,
539        target: NodeId,
540        value: &YamlFragment,
541    ) -> Result<(), YamlEditError> {
542        let target_is_flow = matches!(
543            self.semantic_kind(target),
544            Some(
545                SemanticKind::Mapping {
546                    style: CollectionStyle::Flow
547                } | SemanticKind::Sequence {
548                    style: CollectionStyle::Flow
549                }
550            )
551        );
552        let replacement = if target_is_flow || self.is_flow_context(target) {
553            value.render_flow_for_replacement(self, target)?
554        } else {
555            let yaml = value.prepared_for_replacement(self, target)?.to_yaml()?;
556            indent_continuation_lines(&yaml, self.node_indent(self.expect_node(target)?))
557        };
558        self.replace_node_text(target, replacement)?;
559        Ok(())
560    }
561
562    pub(crate) fn queue_value_removal(&mut self, target: NodeId) -> Result<(), YamlEditError> {
563        let Some(entry) = self.containing_entry(target) else {
564            self.remove_node(target)?;
565            return Ok(());
566        };
567        let Some(collection) = self.node(entry).and_then(super::syntax::Node::parent) else {
568            self.remove_node(entry)?;
569            return Ok(());
570        };
571        let flow = matches!(
572            self.semantic_kind(collection),
573            Some(
574                SemanticKind::Mapping {
575                    style: CollectionStyle::Flow
576                } | SemanticKind::Sequence {
577                    style: CollectionStyle::Flow
578                }
579            )
580        );
581        if !flow {
582            self.remove_node(entry)?;
583            return Ok(());
584        }
585
586        let entries = self
587            .children(collection)
588            .filter(|node| self.containing_entry_child(*node))
589            .collect::<Vec<_>>();
590        let index = entries
591            .iter()
592            .position(|candidate| *candidate == entry)
593            .ok_or_else(|| YamlEditError::new("flow collection entry is missing"))?;
594        let entry_span = self.expect_node(entry)?.span;
595        let span = if let Some(next) = entries.get(index + 1).copied() {
596            Span::new(entry_span.start, self.expect_node(next)?.span.start)
597        } else if index > 0 {
598            let previous = self.expect_node(entries[index - 1])?.span;
599            Span::new(previous.end, entry_span.end)
600        } else {
601            entry_span
602        };
603        self.queue_edit(span, String::new())?;
604        Ok(())
605    }
606
607    fn containing_entry_child(&self, node: NodeId) -> bool {
608        self.node(node).is_some_and(|node| {
609            matches!(
610                node.kind(),
611                crate::NodeKind::MappingEntry | crate::NodeKind::SequenceEntry
612            )
613        })
614    }
615
616    pub(crate) fn queue_mapping_insert(
617        &mut self,
618        mapping: NodeId,
619        key: &str,
620        value: &YamlFragment,
621    ) -> Result<(), YamlEditError> {
622        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
623            return Err(YamlEditError::new(
624                "mapping insertion target is not a mapping",
625            ));
626        };
627        let key = emit_string_key(key);
628        match style {
629            CollectionStyle::Flow => {
630                let mapping_node = self.expect_node(mapping)?;
631                let close = closing_delimiter_offset(self, mapping_node.span, '}')?;
632                let has_pending_entry = self.edits.iter().any(|edit| {
633                    edit.span == Span::empty_from_usize(close) && !edit.replacement.is_empty()
634                });
635                let prefix = if self.mapping_entries(mapping).next().is_some() || has_pending_entry
636                {
637                    ", "
638                } else {
639                    ""
640                };
641                let value = value.render_flow(self)?;
642                self.queue_edit(
643                    Span::empty_from_usize(close),
644                    format!("{prefix}{key}: {value}"),
645                )?;
646            }
647            CollectionStyle::Block => {
648                let mapping_node = self.expect_node(mapping)?;
649                let indent = self.node_indent(mapping_node);
650                let offset = self.mapping_insertion_offset(mapping_node);
651                let mut insertion = insertion_prefix(self, offset);
652                let value = value.prepared(self)?.to_yaml()?;
653                insertion.push_str(&format_block_mapping_entry(
654                    &key,
655                    &value,
656                    indent,
657                    self.preferred_line_ending(),
658                ));
659                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
660            }
661        }
662        Ok(())
663    }
664
665    pub(crate) fn queue_mapping_insert_before(
666        &mut self,
667        mapping: NodeId,
668        before_entry: NodeId,
669        key: &str,
670        value: &YamlFragment,
671    ) -> Result<(), YamlEditError> {
672        let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
673            return Err(YamlEditError::new(
674                "mapping insertion target is not a mapping",
675            ));
676        };
677        if style != CollectionStyle::Flow {
678            return Err(YamlEditError::new(
679                "ordered flow insertion requires a flow mapping",
680            ));
681        }
682        let entry = self.expect_node(before_entry)?;
683        let key = emit_string_key(key);
684        let value = value.render_flow(self)?;
685        self.queue_edit(Span::empty(entry.span.start), format!("{key}: {value}, "))?;
686        Ok(())
687    }
688
689    pub(crate) fn queue_sequence_insert(
690        &mut self,
691        sequence: NodeId,
692        index: usize,
693        value: &YamlFragment,
694    ) -> Result<(), YamlEditError> {
695        let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
696            return Err(YamlEditError::new(
697                "sequence insertion target is not a sequence",
698            ));
699        };
700        let items = self.sequence_items(sequence).collect::<Vec<_>>();
701        match style {
702            CollectionStyle::Flow => {
703                let sequence_node = self.expect_node(sequence)?;
704                let offset = if let Some(item) = items.get(index).copied() {
705                    self.expect_node(item)?.span.start as usize
706                } else {
707                    closing_delimiter_offset(self, sequence_node.span, ']')?
708                };
709                let value = value.render_flow(self)?;
710                let insertion = if items.is_empty() {
711                    value
712                } else if index < items.len() {
713                    format!("{value}, ")
714                } else {
715                    format!(", {value}")
716                };
717                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
718            }
719            CollectionStyle::Block => {
720                let sequence_node = self.expect_node(sequence)?;
721                let indent = self.node_indent(sequence_node);
722                let offset = if let Some(item) = items.get(index).copied() {
723                    let entry = self.containing_entry(item).unwrap_or(item);
724                    self.line_start_offset(self.expect_node(entry)?.span.start as usize)
725                } else {
726                    self.sequence_insertion_offset(sequence_node)
727                };
728                let mut insertion = insertion_prefix(self, offset);
729                let value = value.prepared(self)?.to_yaml()?;
730                insertion.push_str(&format_block_sequence_entry(
731                    &value,
732                    indent,
733                    self.preferred_line_ending(),
734                ));
735                self.queue_edit(Span::empty_from_usize(offset), insertion)?;
736            }
737        }
738        Ok(())
739    }
740
741    pub(crate) fn is_flow_context(&self, mut node: NodeId) -> bool {
742        while let Some(parent) = self.node(node).and_then(super::syntax::Node::parent) {
743            if matches!(
744                self.semantic_kind(parent),
745                Some(
746                    SemanticKind::Mapping {
747                        style: CollectionStyle::Flow
748                    } | SemanticKind::Sequence {
749                        style: CollectionStyle::Flow
750                    }
751                )
752            ) {
753                return true;
754            }
755            node = parent;
756        }
757        false
758    }
759
760    fn anchor_has_external_alias(&self, root: NodeId) -> bool {
761        let Some(root_span) = self.node(root).map(super::syntax::Node::span) else {
762            return false;
763        };
764        let anchored = self
765            .nodes
766            .iter()
767            .enumerate()
768            .map(|(index, _)| NodeId::from_usize(index))
769            .filter(|node| {
770                self.anchor(*node).is_some()
771                    && self.node(*node).is_some_and(|node| {
772                        node.span().start >= root_span.start && node.span().end <= root_span.end
773                    })
774            })
775            .collect::<Vec<_>>();
776        self.nodes
777            .iter()
778            .enumerate()
779            .map(|(index, _)| NodeId::from_usize(index))
780            .filter(|node| matches!(self.semantic_kind(*node), Some(SemanticKind::Alias)))
781            .any(|alias| {
782                let outside = self.node(alias).is_some_and(|node| {
783                    node.span().start < root_span.start || node.span().end > root_span.end
784                });
785                outside
786                    && self
787                        .resolve_alias(alias)
788                        .is_some_and(|target| anchored.contains(&target))
789            })
790    }
791
792    pub(crate) fn line_start_offset(&self, offset: usize) -> usize {
793        let offset = u32::try_from(offset).unwrap_or(u32::MAX);
794        match self.source.line_starts().binary_search(&offset) {
795            Ok(index) => self.source.line_starts()[index] as usize,
796            Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
797        }
798    }
799}
800
801pub(crate) fn closing_delimiter_offset(
802    doc: &YamlDoc,
803    span: Span,
804    delimiter: char,
805) -> Result<usize, YamlEditError> {
806    let source = doc.source.slice(span);
807    let relative = source
808        .rfind(delimiter)
809        .ok_or_else(|| YamlEditError::new(format!("missing `{delimiter}` delimiter")))?;
810    Ok(span.start as usize + relative)
811}
812
813fn insertion_prefix(doc: &YamlDoc, offset: usize) -> String {
814    if offset == doc.source.len()
815        && !doc
816            .source
817            .as_str()
818            .as_bytes()
819            .last()
820            .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
821    {
822        doc.preferred_line_ending().to_owned()
823    } else {
824        String::new()
825    }
826}
827
828fn format_block_mapping_entry(key: &str, value: &str, indent: usize, ending: &str) -> String {
829    let prefix = " ".repeat(indent);
830    if !value.contains(['\n', '\r']) {
831        return format!("{prefix}{key}: {value}{ending}");
832    }
833    let value = indent_text(value, indent + 2);
834    let mut output = format!("{prefix}{key}:{ending}{value}");
835    if !output.ends_with(['\n', '\r']) {
836        output.push_str(ending);
837    }
838    output
839}
840
841fn format_block_sequence_entry(value: &str, indent: usize, ending: &str) -> String {
842    let prefix = " ".repeat(indent);
843    if !value.contains(['\n', '\r']) {
844        return format!("{prefix}- {value}{ending}");
845    }
846    let value = indent_text(value, indent + 2);
847    let mut output = format!("{prefix}-{ending}{value}");
848    if !output.ends_with(['\n', '\r']) {
849        output.push_str(ending);
850    }
851    output
852}
853
854fn indent_continuation_lines(value: &str, indent: usize) -> String {
855    if indent == 0 {
856        return value.to_owned();
857    }
858    let prefix = " ".repeat(indent);
859    let mut output = String::with_capacity(value.len());
860    let mut after_break = false;
861    for character in value.chars() {
862        if after_break && !matches!(character, '\r' | '\n') {
863            output.push_str(&prefix);
864            after_break = false;
865        }
866        output.push(character);
867        if character == '\n' {
868            after_break = true;
869        } else if character != '\r' {
870            after_break = false;
871        }
872    }
873    output
874}
875
876pub(crate) fn emit_string_key(value: &str) -> String {
877    if safe_plain_string(value) {
878        value.to_owned()
879    } else {
880        crate::fragment::quote_string(value)
881    }
882}
883
884pub(crate) fn safe_plain_string(value: &str) -> bool {
885    if !safe_plain_string_syntax(value) {
886        return false;
887    }
888    matches!(
889        resolve_scalar(value, YamlScalarStyle::Plain, None),
890        Ok(ResolvedScalar::String)
891    )
892}
893
894fn safe_plain_string_syntax(value: &str) -> bool {
895    if value.is_empty()
896        || value.trim() != value
897        || value.contains(['\n', '\r', '\t', ':', '#', '[', ']', '{', '}', ','])
898        || value.starts_with(['-', '?', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'])
899    {
900        return false;
901    }
902    true
903}
904
905fn safe_plain_key_with_tag(value: &str, tag: Option<&str>) -> bool {
906    const STRING_TAG: &str = "tag:yaml.org,2002:str";
907
908    safe_plain_string_syntax(value)
909        && (tag == Some(STRING_TAG)
910            || matches!(
911                resolve_scalar(value, YamlScalarStyle::Plain, None),
912                Ok(ResolvedScalar::String)
913            ))
914}
915
916fn scalar_is_string(doc: &YamlDoc, node: NodeId) -> Result<bool, YamlEditError> {
917    let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(node) else {
918        return Ok(false);
919    };
920    let value = doc.scalar_value(node)?;
921    let tag = doc.resolved_tag(node)?;
922    Ok(matches!(
923        resolve_scalar(&value, style, tag.as_deref()),
924        Ok(ResolvedScalar::String)
925    ))
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931
932    fn pointer(value: &str) -> JsonPointer {
933        JsonPointer::parse(value).unwrap()
934    }
935
936    fn fragment(value: &str) -> YamlFragment {
937        YamlFragment::parse(value).unwrap()
938    }
939
940    #[test]
941    fn renames_block_flow_and_explicit_mapping_keys_losslessly() {
942        let input = "old: 1 # keep\nflow: {old: 2}\nexplicit:\n  ? 'old'\n  : 3\n";
943        let mut doc = YamlDoc::parse(input).unwrap();
944        doc.rename_keys_at(
945            0,
946            &[
947                pointer("/old"),
948                pointer("/flow/old"),
949                pointer("/explicit/old"),
950            ],
951            "true",
952        )
953        .unwrap();
954
955        assert_eq!(
956            doc.as_source(),
957            "\"true\": 1 # keep\nflow: {\"true\": 2}\nexplicit:\n  ? 'true'\n  : 3\n"
958        );
959    }
960
961    #[test]
962    fn rename_key_quotes_plain_names_that_are_not_safe_strings() {
963        for new_key in ["true", "", "a: b", "line\nbreak"] {
964            let mut doc = YamlDoc::parse("old: value\n").unwrap();
965            doc.rename_key_at(0, &pointer("/old"), new_key).unwrap();
966            assert_eq!(
967                doc.as_source(),
968                format!("{}: value\n", crate::fragment::quote_string(new_key))
969            );
970        }
971
972        let mut flow = YamlDoc::parse("{old: value}\n").unwrap();
973        flow.rename_key_at(0, &pointer("/old"), "a,b").unwrap();
974        assert_eq!(flow.as_source(), "{\"a,b\": value}\n");
975    }
976
977    #[test]
978    fn rename_key_preserves_quoted_styles_properties_comments_and_line_endings() {
979        let mut single = YamlDoc::parse("'old': value\n").unwrap();
980        single.rename_key_at(0, &pointer("/old"), "Bob's").unwrap();
981        assert_eq!(single.as_source(), "'Bob''s': value\n");
982
983        let mut double = YamlDoc::parse("\"old\": value\n").unwrap();
984        double
985            .rename_key_at(0, &pointer("/old"), "new \"key\"")
986            .unwrap();
987        assert_eq!(double.as_source(), "\"new \\\"key\\\"\": value\n");
988
989        let mut tagged = YamlDoc::parse("!!str &key old: value # keep\r\n").unwrap();
990        tagged.rename_key_at(0, &pointer("/old"), "true").unwrap();
991        assert_eq!(tagged.as_source(), "!!str &key true: value # keep\r\n");
992    }
993
994    #[test]
995    fn rename_keys_resolves_all_targets_before_editing_and_deduplicates_alias_routes() {
996        let mut nested = YamlDoc::parse("parent:\n  old: 1\nold: 2\n").unwrap();
997        nested
998            .rename_keys_at(0, &[pointer("/parent/old"), pointer("/parent")], "renamed")
999            .unwrap();
1000        assert_eq!(nested.as_source(), "renamed:\n  renamed: 1\nold: 2\n");
1001
1002        let mut aliases = YamlDoc::parse("base: &base\n  name: Ada\ncopy: *base\n").unwrap();
1003        aliases
1004            .rename_keys_at(
1005                0,
1006                &[pointer("/base/name"), pointer("/copy/name")],
1007                "display-name",
1008            )
1009            .unwrap();
1010        assert_eq!(
1011            aliases.as_source(),
1012            "base: &base\n  display-name: Ada\ncopy: *base\n"
1013        );
1014    }
1015
1016    #[test]
1017    fn rename_keys_rejects_collisions_transactionally() {
1018        let input = "a: 1\nb: 2\n";
1019        let mut existing = YamlDoc::parse(input).unwrap();
1020        let error = existing.rename_key_at(0, &pointer("/a"), "b").unwrap_err();
1021        assert!(error.to_string().contains("duplicate key \"b\""));
1022        assert_eq!(existing.as_source(), input);
1023
1024        let mut selected = YamlDoc::parse(input).unwrap();
1025        let error = selected
1026            .rename_keys_at(0, &[pointer("/a"), pointer("/b")], "x")
1027            .unwrap_err();
1028        assert!(error.to_string().contains("duplicate key \"x\""));
1029        assert_eq!(selected.as_source(), input);
1030    }
1031
1032    #[test]
1033    fn rename_keys_treats_duplicate_and_unchanged_targets_as_no_ops() {
1034        let input = "\"old\": value\n";
1035        let mut doc = YamlDoc::parse(input).unwrap();
1036        doc.rename_keys_at(0, &[pointer("/old"), pointer("/old")], "old")
1037            .unwrap();
1038        doc.rename_keys_at(0, &[], "ignored").unwrap();
1039        assert_eq!(doc.as_source(), input);
1040    }
1041
1042    #[test]
1043    fn rename_key_rejects_non_members_and_unsupported_key_forms() {
1044        let mut root = YamlDoc::parse("key: value\n").unwrap();
1045        assert!(
1046            root.rename_key_at(0, &pointer(""), "new")
1047                .unwrap_err()
1048                .to_string()
1049                .contains("document root")
1050        );
1051        assert_eq!(root.as_source(), "key: value\n");
1052
1053        let mut sequence = YamlDoc::parse("- value\n").unwrap();
1054        assert!(
1055            sequence
1056                .rename_key_at(0, &pointer("/0"), "new")
1057                .unwrap_err()
1058                .to_string()
1059                .contains("does not select a mapping member")
1060        );
1061
1062        let mut block = YamlDoc::parse("? >\n  old\n: value\n").unwrap();
1063        assert!(
1064            block
1065                .rename_key_at(0, &pointer("/old\n"), "new")
1066                .unwrap_err()
1067                .to_string()
1068                .contains("plain, single-quoted, and double-quoted")
1069        );
1070
1071        let mut alias = YamlDoc::parse("name: &key target\n? *key\n: value\n").unwrap();
1072        let error = alias
1073            .rename_key_at(0, &pointer("/target"), "new")
1074            .unwrap_err();
1075        assert!(
1076            error
1077                .to_string()
1078                .contains("plain, single-quoted, and double-quoted"),
1079            "{error}"
1080        );
1081
1082        let mut complex = YamlDoc::parse("? [a, b]\n: value\n").unwrap();
1083        assert!(
1084            complex
1085                .rename_key_at(0, &pointer("/anything"), "new")
1086                .is_err()
1087        );
1088    }
1089
1090    #[test]
1091    fn adds_replaces_and_removes_block_mapping_values() {
1092        let mut doc = YamlDoc::parse("server:\n  host: localhost # keep\n").unwrap();
1093        doc.add_at(0, &pointer("/server/port"), &fragment("8080"))
1094            .unwrap();
1095        assert_eq!(
1096            doc.as_source(),
1097            "server:\n  host: localhost # keep\n  port: 8080\n"
1098        );
1099        doc.replace_at(0, &pointer("/server/host"), &fragment("example.com"))
1100            .unwrap();
1101        assert_eq!(
1102            doc.as_source(),
1103            "server:\n  host: example.com # keep\n  port: 8080\n"
1104        );
1105        doc.remove_at(0, &pointer("/server/port")).unwrap();
1106        assert_eq!(doc.as_source(), "server:\n  host: example.com # keep\n");
1107    }
1108
1109    #[test]
1110    fn adds_string_keys_without_changing_their_schema_type() {
1111        let mut doc = YamlDoc::parse("{}\n").unwrap();
1112        doc.add_at(0, &pointer("/true"), &fragment("value"))
1113            .unwrap();
1114        assert_eq!(doc.as_source(), "{\"true\": value}\n");
1115        assert!(
1116            doc.resolve_pointer(0, &pointer("/true")).is_ok(),
1117            "{}",
1118            doc.as_source()
1119        );
1120    }
1121
1122    #[test]
1123    fn inserts_block_and_flow_sequence_items() {
1124        let mut block = YamlDoc::parse("items:\n  - a\n  - c\n").unwrap();
1125        block
1126            .add_at(0, &pointer("/items/1"), &fragment("b"))
1127            .unwrap();
1128        block
1129            .add_at(0, &pointer("/items/-"), &fragment("d"))
1130            .unwrap();
1131        assert_eq!(block.as_source(), "items:\n  - a\n  - b\n  - c\n  - d\n");
1132
1133        let mut flow = YamlDoc::parse("items: [a, c]\n").unwrap();
1134        flow.add_at(0, &pointer("/items/1"), &fragment("b"))
1135            .unwrap();
1136        assert_eq!(flow.as_source(), "items: [a, b, c]\n");
1137    }
1138
1139    #[test]
1140    fn mutations_are_transactional() {
1141        let input = "items: [a]\n";
1142        let mut doc = YamlDoc::parse(input).unwrap();
1143        assert!(doc.add_at(0, &pointer("/items/4"), &fragment("x")).is_err());
1144        assert_eq!(doc.as_source(), input);
1145    }
1146
1147    #[test]
1148    fn move_uses_remove_then_add_sequence_indices() {
1149        let mut doc = YamlDoc::parse("[a, b, c]\n").unwrap();
1150        doc.move_at(0, &pointer("/0"), &pointer("/2")).unwrap();
1151        assert_eq!(doc.as_source(), "[b, c, a]\n");
1152    }
1153
1154    #[test]
1155    fn copy_rejects_anchors_and_test_is_semantic() {
1156        let mut doc = YamlDoc::parse("one: &one {value: 1}\ntwo: null\n").unwrap();
1157        assert!(
1158            doc.copy_at(0, &pointer("/one"), &pointer("/two"))
1159                .unwrap_err()
1160                .to_string()
1161                .contains("anchor")
1162        );
1163        assert!(
1164            doc.test_at(0, &pointer("/one/value"), &fragment("1.0"))
1165                .unwrap()
1166        );
1167    }
1168}