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#[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 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 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 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 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 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 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 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 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}