use core::fmt::Write as _;
use crate::cst::builder::{
SubtreeContext, document_boundaries, parse_full, parse_subtree, rebuild_with_splice,
};
use crate::cst::emit::{Emit, EmitCtx, emit_key};
use crate::cst::green::{GreenChild, GreenNode};
use crate::cst::syntax::SyntaxKind;
use crate::error::{Error, Result};
use crate::path::{QuerySegment, parse_query_path};
use crate::prelude::*;
use crate::span_context::SpanTree;
use crate::value::{Mapping, Number, Value};
#[derive(Debug)]
pub struct Document {
source: Arc<str>,
green: GreenNode,
cache: core::cell::RefCell<Option<(Value, SpanTree)>>,
last_repair_scope: core::cell::Cell<Option<RepairScope>>,
}
impl Clone for Document {
fn clone(&self) -> Self {
Self {
source: Arc::clone(&self.source),
green: self.green.clone(),
cache: core::cell::RefCell::new(self.cache.borrow().clone()),
last_repair_scope: core::cell::Cell::new(self.last_repair_scope.get()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepairScope {
Scalar,
Entry,
Collection,
Document,
}
impl Document {
#[must_use]
pub fn syntax(&self) -> &GreenNode {
&self.green
}
#[must_use]
pub fn as_value(&self) -> core::cell::Ref<'_, Value> {
self.ensure_cache();
core::cell::Ref::map(self.cache.borrow(), |opt| {
&opt.as_ref().expect("ensure_cache populated").0
})
}
#[must_use]
pub fn source(&self) -> &str {
&self.source
}
#[must_use]
pub fn span_at(&self, path: &str) -> Option<(usize, usize)> {
let segments = parse_query_path(path);
if let Some((s, e)) = resolve_path_in_green(&self.green, &segments, &self.source) {
return Some(trim_value_span(&self.source, s, e));
}
self.ensure_cache();
let cache = self.cache.borrow();
let (value, span_tree) = cache.as_ref().expect("ensure_cache populated");
let ((s, e), _through_alias) = resolve_span(value, span_tree, &segments)?;
Some(trim_value_span(&self.source, s, e))
}
#[must_use]
pub fn key_span(&self, path: &str) -> Option<(usize, usize)> {
const KEY_SPAN_SENTINEL: &str = "\0\0noyalib::key_span sentinel\0\0";
let segments = parse_query_path(path);
if segments.is_empty() {
return None;
}
self.ensure_cache();
let cache = self.cache.borrow();
let (value, span_tree) = cache.as_ref().expect("ensure_cache populated");
match entry_key_site(value, span_tree, &segments, KEY_SPAN_SENTINEL) {
Ok((s, e)) if s != e => Some((s, e)),
_ => None,
}
}
fn ensure_cache(&self) {
if self.cache.borrow().is_some() {
return;
}
let cfg = crate::parser::ParseConfig::default();
let parsed = crate::parser::parse_one(&self.source, &cfg)
.expect("Document source must always parse — local repair invariant violated");
*self.cache.borrow_mut() = Some(parsed);
}
pub fn validate(&self) -> Result<()> {
if self.cache.borrow().is_some() {
return Ok(());
}
let cfg = crate::parser::ParseConfig::default();
let parsed = crate::parser::parse_one(&self.source, &cfg)?;
*self.cache.borrow_mut() = Some(parsed);
Ok(())
}
#[must_use]
pub fn get(&self, path: &str) -> Option<&str> {
let (s, e) = self.span_at(path)?;
Some(&self.source[s..e])
}
pub fn replace_span(&mut self, start: usize, end: usize, replacement: &str) -> Result<()> {
if start > end || end > self.source.len() {
return Err(Error::Parse(format!(
"replace_span range {start}..{end} out of bounds (source length {})",
self.source.len()
)));
}
if !self.source.is_char_boundary(start) || !self.source.is_char_boundary(end) {
return Err(Error::Parse(format!(
"replace_span range {start}..{end} is not a character boundary"
)));
}
let mut new_source =
String::with_capacity(self.source.len() - (end - start) + replacement.len());
new_source.push_str(&self.source[..start]);
new_source.push_str(replacement);
new_source.push_str(&self.source[end..]);
let new_arc: Arc<str> = Arc::from(new_source.as_str());
if let Some((new_green, scope)) =
self.try_local_repair_green(start, end, replacement, &new_source)
{
self.last_repair_scope.set(Some(scope));
self.source = new_arc;
self.green = new_green;
let _ = self.cache.replace(None);
return Ok(());
}
let parsed = parse_full(&new_source)?;
self.last_repair_scope.set(Some(RepairScope::Document));
self.source = parsed.source;
self.green = parsed.green;
let _ = self.cache.replace(Some((parsed.value, parsed.span_tree)));
Ok(())
}
fn try_local_repair_green(
&self,
start: usize,
end: usize,
replacement: &str,
new_source: &str,
) -> Option<(GreenNode, RepairScope)> {
if region_has_anchor_alias_or_tag(&self.green, start, end)
|| replacement_introduces_anchor_alias_or_tag(replacement)
{
return None;
}
let delta = replacement.len() as isize - (end as isize - start as isize);
let candidates = ancestor_candidates(&self.green, start, end);
for cand in &candidates {
if !is_phase_a_repairable(cand.kind) {
continue;
}
let n_old_start = cand.start;
let n_old_end = cand.end;
let n_new_start = n_old_start; let n_new_end_signed = n_old_end as isize + delta;
if n_new_end_signed < n_new_start as isize {
continue;
}
let n_new_end = n_new_end_signed as usize;
if n_new_end > new_source.len() {
continue;
}
let fragment = &new_source[n_new_start..n_new_end];
let indent = entry_indent_column(&self.source, n_old_start);
let ctx = SubtreeContext::block_at(indent);
match parse_subtree(fragment, ctx, cand.kind) {
Ok(new_sub)
if new_sub.kind() == cand.kind && new_sub.text_len() == fragment.len() =>
{
let new_root =
rebuild_with_splice(&self.green, n_old_start, n_old_end, new_sub);
return Some((new_root, scope_for_kind(cand.kind)));
}
Ok(_) | Err(_) => {
continue;
}
}
}
None
}
#[must_use]
pub fn last_repair_scope(&self) -> Option<RepairScope> {
self.last_repair_scope.get()
}
pub fn set(&mut self, path: &str, fragment: &str) -> Result<()> {
let (s, e) = self.write_span(path)?;
self.replace_span(s, e, fragment)
}
fn write_span(&self, path: &str) -> Result<(usize, usize)> {
let segments = parse_query_path(path);
if let Some((s, e)) = resolve_path_in_green(&self.green, &segments, &self.source) {
return Ok(trim_value_span(&self.source, s, e));
}
self.ensure_cache();
let cache = self.cache.borrow();
let (value, span_tree) = cache.as_ref().expect("ensure_cache populated");
let ((s, e), through_alias) = resolve_span(value, span_tree, &segments)
.ok_or_else(|| Error::Parse(format!("path not found: {path}")))?;
if through_alias {
return Err(Error::Parse(format!(
"cannot set `{path}`: its value is (or resolves through) an alias \
reference; edit the anchor definition or replace the alias explicitly"
)));
}
Ok(trim_value_span(&self.source, s, e))
}
pub fn set_value(&mut self, path: &str, value: &Value) -> Result<()> {
let (s, e) = self.write_span(path)?;
let kind = leaf_kind_at(&self.green, s).ok_or_else(|| {
Error::Parse("could not locate green-tree leaf at target span".into())
})?;
let neighbour = sibling_dominant_scalar_kind(&self.green, s)
.filter(|_| kind == SyntaxKind::PlainScalar);
let entry_col = entry_indent_column(&self.source, s);
let ctx = SiteContext {
kind,
neighbour,
entry_col,
};
let fragment = format_value_for_site(value, &ctx)?;
self.replace_span(s, e, &fragment)
}
pub fn remove(&mut self, path: &str) -> Result<()> {
self.ensure_cache();
let segments = parse_query_path(path);
let (line_start, line_end, multiline) = {
let cache = self.cache.borrow();
let (value, span_tree) = cache.as_ref().expect("ensure_cache populated");
entry_line_span(value, span_tree, &self.source, &segments)?
};
if !multiline {
return self.replace_span(line_start, line_end, "");
}
let expected = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
expected_after_remove(value, &segments)?
};
let snapshot = self.clone();
if let Err(e) = self.replace_span(line_start, line_end, "") {
*self = snapshot;
return Err(Error::Parse(format!(
"remove: removing `{path}` could not be spliced ({e}); \
the document was left unchanged"
)));
}
if let Err(e) = self.validate() {
*self = snapshot;
return Err(Error::Parse(format!(
"remove: removing `{path}` left the document unable to re-parse ({e}); \
the document was left unchanged"
)));
}
if *self.as_value() != expected {
*self = snapshot;
return Err(Error::Parse(format!(
"remove: removing `{path}` failed the integrity check — the edit would \
change data beyond the removed entry; the document was left unchanged"
)));
}
Ok(())
}
pub fn rename_key(&mut self, path: &str, new_key: &str) -> Result<()> {
self.validate().map_err(|e| {
Error::Parse(format!(
"rename_key: the document does not parse, so `{path}` cannot be resolved \
({e}); the document was left unchanged"
))
})?;
let segments = parse_rename_path(path)?;
if new_key == MERGE_KEY_SPELLING {
return Err(Error::Parse(format!(
"rename_key: `{MERGE_KEY_SPELLING}` cannot be used as a key name — the loader \
treats any `{MERGE_KEY_SPELLING}` key as a merge directive whatever its quote \
style, so the renamed entry would not round-trip as a key"
)));
}
if let Some(bad) = first_non_printable(new_key) {
return Err(Error::Parse(format!(
"rename_key: the new key contains the non-printable character U+{:04X}, which \
is outside YAML's printable character set — mapping keys may not carry control \
characters (tab excepted)",
bad as u32
)));
}
let (key_start, key_end) = {
let cache = self.cache.borrow();
let (value, span_tree) = cache.as_ref().expect("validate populated the cache");
entry_key_site(value, span_tree, &segments, new_key)?
};
if key_start == key_end {
return Err(Error::Parse(format!(
"rename_key: the key at `{path}` is not a simple scalar token \
(alias keys cannot be renamed)"
)));
}
let (token_kind, (tok_start, tok_end), parent_kind) =
token_at_with_parent(&self.green, key_start, 0).ok_or_else(|| {
Error::Parse(format!(
"rename_key: could not locate the key token for `{path}`"
))
})?;
let (tok_start, tok_end) = trim_trailing_blank(&self.source, tok_start, tok_end);
if let Some(current) = decode_key_token(&self.source[tok_start..tok_end], token_kind) {
if current == new_key {
return Ok(());
}
}
if parent_kind == SyntaxKind::FlowMapping {
return Err(Error::Parse(format!(
"rename_key: `{path}` addresses a flow-mapping entry — only block \
mappings are supported (flow-mapping renames are a follow-up)"
)));
}
if parent_kind != SyntaxKind::MappingEntry {
return Err(Error::Parse(format!(
"rename_key: `{path}` does not address a block-mapping entry key"
)));
}
if !matches!(
token_kind,
SyntaxKind::PlainScalar
| SyntaxKind::SingleQuotedScalar
| SyntaxKind::DoubleQuotedScalar
) {
return Err(Error::Parse(format!(
"rename_key: the key at `{path}` is not a simple scalar token \
(alias keys cannot be renamed)"
)));
}
if let Some((anchor, alias_count)) = self.aliased_anchor_covering(tok_start) {
return Err(Error::Parse(format!(
"rename_key: `{path}` is inside the value anchored by `&{anchor}`, which has \
{alias_count} alias reference(s) — renaming the key here would rename it at \
every `*{anchor}` site too; call `materialise_aliases_of(\"{anchor}\")` first \
to give each site its own copy, then rename"
)));
}
let replacement = format_key_for_site(new_key, token_kind);
if replacement == self.source[tok_start..tok_end] {
return Ok(());
}
let snapshot = self.clone();
let expected = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("validate populated the cache");
expected_after_rename(value, &segments, new_key)?
};
if let Err(e) = self.replace_span(tok_start, tok_end, &replacement) {
*self = snapshot;
return Err(Error::Parse(format!(
"rename_key: renaming `{path}` to `{new_key}` could not be spliced ({e}); \
the document was left unchanged"
)));
}
if let Err(e) = self.validate() {
*self = snapshot;
return Err(Error::Parse(format!(
"rename_key: renaming `{path}` to `{new_key}` left the document unable to \
re-parse ({e}); the document was left unchanged"
)));
}
let matches_expected = *self.as_value() == expected;
if !matches_expected {
*self = snapshot;
return Err(Error::Parse(format!(
"rename_key: renaming `{path}` to `{new_key}` failed the integrity \
check — the edit would change data beyond the single renamed key \
(e.g. a duplicate of the old key elsewhere in the mapping); \
the document was left unchanged"
)));
}
Ok(())
}
pub fn swap_items(&mut self, path: &str, i: usize, j: usize) -> Result<()> {
let segments = parse_query_path(path);
self.ensure_cache();
let len = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
sequence_len_at(value, &segments, path)?
};
if i >= len || j >= len {
return Err(Error::Parse(format!(
"swap_items: index out of bounds for the sequence at `{path}` \
(length {len}): requested {i} and {j}"
)));
}
if i == j {
return Ok(());
}
let (pi, pj) = (item_child_path(path, i), item_child_path(path, j));
let span_i = self.span_at(&pi).ok_or_else(|| {
Error::Parse(format!("swap_items: could not locate item {i} of `{path}`"))
})?;
let span_j = self.span_at(&pj).ok_or_else(|| {
Error::Parse(format!("swap_items: could not locate item {j} of `{path}`"))
})?;
let text_i = self.source()[span_i.0..span_i.1].to_string();
let text_j = self.source()[span_j.0..span_j.1].to_string();
let expected = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
expected_after_swap(value, &segments, i, j, path)?
};
let snapshot = self.clone();
let (lo, hi, lo_text, hi_text) = if span_i.0 < span_j.0 {
(span_i, span_j, &text_j, &text_i)
} else {
(span_j, span_i, &text_i, &text_j)
};
for (span, text) in [(hi, hi_text), (lo, lo_text)] {
if let Err(e) = self.replace_span(span.0, span.1, text) {
*self = snapshot;
return Err(Error::Parse(format!(
"swap_items: swapping items {i} and {j} of `{path}` could not be \
spliced ({e}); the document was left unchanged"
)));
}
}
if let Err(e) = self.validate() {
*self = snapshot;
return Err(Error::Parse(format!(
"swap_items: swapping items {i} and {j} of `{path}` left the document \
unable to re-parse ({e}); the document was left unchanged"
)));
}
if *self.as_value() != expected {
*self = snapshot;
return Err(Error::Parse(format!(
"swap_items: swapping items {i} and {j} of `{path}` failed the integrity \
check — the byte swap could not preserve the items (e.g. multi-line or \
differently-indented values); the document was left unchanged"
)));
}
Ok(())
}
pub fn move_item(&mut self, path: &str, from: usize, to: usize) -> Result<()> {
let segments = parse_query_path(path);
self.ensure_cache();
let len = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
sequence_len_at(value, &segments, path)?
};
if from >= len || to >= len {
return Err(Error::Parse(format!(
"move_item: index out of bounds for the sequence at `{path}` \
(length {len}): from {from}, to {to}"
)));
}
if from == to {
return Ok(());
}
let snapshot = self.clone();
let mut failure = None;
if from < to {
for k in from..to {
if let Err(e) = self.swap_items(path, k, k + 1) {
failure = Some(e);
break;
}
}
} else {
let mut k = from;
while k > to {
if let Err(e) = self.swap_items(path, k, k - 1) {
failure = Some(e);
break;
}
k -= 1;
}
}
if let Some(e) = failure {
*self = snapshot;
return Err(Error::Parse(format!(
"move_item: moving item {from} to {to} in `{path}` failed ({e}); \
the document was left unchanged"
)));
}
Ok(())
}
fn aliased_anchor_covering(&self, pos: usize) -> Option<(String, usize)> {
for anchor in self.anchors() {
let Some((start, end)) = anchored_content_span(&self.green, 0, anchor.mark_span.0)
else {
continue;
};
if pos < start || pos >= end {
continue;
}
let count = self.aliases_of(&anchor.name).len();
if count > 0 {
return Some((anchor.name, count));
}
}
None
}
pub fn push_back(&mut self, path: &str, fragment: &str) -> Result<()> {
self.ensure_cache();
let seq_len = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
let target = path_value(value, path)
.ok_or_else(|| Error::Parse(format!("path not found: {path}")))?;
match target {
Value::Sequence(s) => s.len(),
_ => {
return Err(Error::Parse(
"push_back: target path is not a sequence".into(),
));
}
}
};
if seq_len == 0 {
return Err(Error::Parse(
"push_back: empty sequence has no anchor for indentation — use `set` with a fragment instead"
.into(),
));
}
let item_path = format!("{path}[{}]", seq_len - 1);
let (last_start, last_end) = self
.span_at(&item_path)
.ok_or_else(|| Error::Parse("push_back: could not resolve last item span".into()))?;
let dash_col = column_of_preceding_dash(&self.source, last_start).ok_or_else(|| {
Error::Parse(
"push_back: only block sequences are supported (no `-` anchor before last item)"
.into(),
)
})?;
let line_end = end_of_line(&self.source, last_end);
let indent: String = " ".repeat(dash_col);
let lead = leading_break_for_splice(&self.source, line_end);
let new_line = format!("{lead}{indent}- {fragment}\n");
self.replace_span(line_end, line_end, &new_line)
}
#[must_use]
pub fn indent_unit(&self) -> usize {
detect_indent_unit(&self.source)
}
#[must_use]
pub fn dominant_quote_style(&self) -> crate::ScalarStyle {
detect_dominant_quote_style(&self.green)
}
#[must_use]
pub fn dominant_flow_style(&self) -> crate::FlowStyle {
detect_dominant_flow_style(&self.green)
}
pub fn insert_entry(&mut self, mapping_path: &str, key: &str, fragment: &str) -> Result<()> {
let child_path = if mapping_path.is_empty() {
key.to_owned()
} else {
format!("{mapping_path}.{key}")
};
if self.span_at(&child_path).is_some() {
return self.set(&child_path, fragment);
}
self.ensure_cache();
let last_key: String = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("ensure_cache populated");
let target = if mapping_path.is_empty() {
value
} else {
path_value(value, mapping_path)
.ok_or_else(|| Error::Parse(format!("path not found: {mapping_path}")))?
};
let mapping = match target {
Value::Mapping(m) => m,
_ => {
return Err(Error::Parse(
"insert_entry: target path is not a mapping".into(),
));
}
};
if mapping.is_empty() {
return Err(Error::Parse(
"insert_entry: empty mapping has no anchor for indentation — \
use `set` with a fragment instead"
.into(),
));
}
mapping
.iter()
.last()
.map(|(k, _)| k.clone())
.expect("non-empty mapping has a last entry")
};
let last_path = if mapping_path.is_empty() {
last_key
} else {
format!("{mapping_path}.{last_key}")
};
let (last_value_start, last_value_end) = self.span_at(&last_path).ok_or_else(|| {
Error::Parse("insert_entry: could not resolve last entry span".into())
})?;
let key_col = column_of_key_at(&self.source, last_value_start).ok_or_else(|| {
Error::Parse("insert_entry: could not locate last key's column for indentation".into())
})?;
let line_end = end_of_line(&self.source, last_value_end);
let indent: String = " ".repeat(key_col);
let new_line = if fragment.contains('\n') {
let unit = detect_indent_unit(&self.source);
let inner_indent: String = " ".repeat(key_col + unit);
let body = fragment.trim_start_matches('\n');
let mut buf = format!("{indent}{key}:\n");
for line in body.split('\n') {
if line.is_empty() {
buf.push('\n');
} else {
buf.push_str(&inner_indent);
buf.push_str(line);
buf.push('\n');
}
}
buf
} else {
format!("{indent}{key}: {fragment}\n")
};
let lead = leading_break_for_splice(&self.source, line_end);
self.replace_span(line_end, line_end, &format!("{lead}{new_line}"))
}
pub fn insert_after(&mut self, item_path: &str, fragment: &str) -> Result<()> {
let segments = parse_query_path(item_path);
if !matches!(segments.last(), Some(QuerySegment::Index(_))) {
return Err(Error::Parse(
"insert_after: path must end with a sequence index, e.g. `items[2]`".into(),
));
}
let (item_start, item_end) = self
.span_at(item_path)
.ok_or_else(|| Error::Parse(format!("path not found: {item_path}")))?;
let dash_col = column_of_preceding_dash(&self.source, item_start).ok_or_else(|| {
Error::Parse(
"insert_after: only block sequences are supported (no `-` anchor before item)"
.into(),
)
})?;
let line_end = end_of_line(&self.source, item_end);
let indent: String = " ".repeat(dash_col);
let lead = leading_break_for_splice(&self.source, line_end);
let new_line = format!("{lead}{indent}- {fragment}\n");
self.replace_span(line_end, line_end, &new_line)
}
fn emit_ctx(&self, column: usize) -> EmitCtx {
EmitCtx::new(
self.dominant_quote_style(),
self.dominant_flow_style(),
self.indent_unit(),
column,
)
}
pub fn insert_entry_value<E: Emit + ?Sized>(
&mut self,
mapping_path: &str,
key: &str,
value: &E,
) -> Result<()> {
if let Err(e) = self.validate() {
return Err(Error::Parse(format!(
"insert_entry_value: the document does not parse, so `{mapping_path}` cannot \
be resolved ({e}); the document was left unchanged"
)));
}
if key == MERGE_KEY_SPELLING {
return Err(Error::Parse(format!(
"insert_entry_value: `{MERGE_KEY_SPELLING}` cannot be used as a key name — the \
loader treats any `{MERGE_KEY_SPELLING}` key as a merge directive whatever its \
quote style, so the entry would not round-trip as a key"
)));
}
if let Some(bad) = first_non_printable(key) {
return Err(Error::Parse(format!(
"insert_entry_value: the key contains the non-printable character U+{:04X}, \
which is outside YAML's printable character set — mapping keys may not carry \
control characters (tab excepted)",
bad as u32
)));
}
let expected_child = value.expected_value()?;
let expected = {
let cache = self.cache.borrow();
let (doc_value, _) = cache.as_ref().expect("validate populated the cache");
expected_after_insert_entry(doc_value, mapping_path, key, &expected_child)?
};
let addressable = !key.contains('.') && !key.contains('[');
let in_mapping = {
let cache = self.cache.borrow();
let (doc_value, _) = cache.as_ref().expect("validate populated the cache");
let target = if mapping_path.is_empty() {
Some(doc_value)
} else {
path_value(doc_value, mapping_path)
};
matches!(target, Some(Value::Mapping(m)) if m.get(key).is_some())
};
if in_mapping && !addressable {
return Err(Error::Parse(format!(
"insert_entry_value: `{mapping_path}` already has a key `{key}`, and a key \
containing `.` or `[` cannot be addressed by the path syntax to replace its \
value — `remove` the entry and insert it afresh, or splice it with `set`"
)));
}
let child_path = if mapping_path.is_empty() {
key.to_owned()
} else {
format!("{mapping_path}.{key}")
};
let existing = if in_mapping && addressable {
self.span_at(&child_path)
} else {
None
};
let is_collection = matches!(expected_child, Value::Sequence(_) | Value::Mapping(_));
if existing.is_some() && is_collection {
return Err(Error::Parse(format!(
"insert_entry_value: `{key}` already exists in `{mapping_path}` and its value \
is being replaced with a collection — growing a scalar entry into a nested \
block is not an in-place edit; `remove` the entry first, or splice the \
layout you want with `set`"
)));
}
let (column, anchor_pos, probe) = match existing {
Some((start, _)) => (
column_of_key_at(&self.source, start).ok_or_else(|| {
Error::Parse(format!(
"insert_entry_value: could not locate the column of the existing key \
`{key}` in `{mapping_path}`"
))
})?,
start,
start,
),
None => self.mapping_insert_anchor(mapping_path)?,
};
self.refuse_inside_aliased_anchor("insert_entry_value", mapping_path, probe)?;
let ctx = self.emit_ctx(column);
let fragment = value.emit(&ctx)?;
let key_spelling = emit_key(key, &ctx);
let indent = " ".repeat(column);
let snapshot = self.clone();
let spliced = if existing.is_some() {
let inline = indent_continuation_lines(&fragment, column);
self.set(&child_path, &inline)
} else if is_collection {
let inner = " ".repeat(column + self.indent_unit());
let lead = leading_break_for_splice(&self.source, anchor_pos);
let mut line = format!("{lead}{indent}{key_spelling}:\n");
for body_line in fragment.split('\n') {
if body_line.is_empty() {
line.push('\n');
} else {
line.push_str(&inner);
line.push_str(body_line);
line.push('\n');
}
}
self.replace_span(anchor_pos, anchor_pos, &line)
} else {
let inline = indent_continuation_lines(&fragment, column);
let lead = leading_break_for_splice(&self.source, anchor_pos);
let line = format!("{lead}{indent}{key_spelling}: {inline}\n");
self.replace_span(anchor_pos, anchor_pos, &line)
};
if let Err(e) = spliced {
*self = snapshot;
return Err(Error::Parse(format!(
"insert_entry_value: inserting `{key}` into `{mapping_path}` could not be \
spliced ({e}); the document was left unchanged"
)));
}
if let Err(e) = self.validate() {
*self = snapshot;
return Err(Error::Parse(format!(
"insert_entry_value: inserting `{key}` into `{mapping_path}` left the document \
unable to re-parse ({e}); the document was left unchanged"
)));
}
if *self.as_value() != expected {
*self = snapshot;
return Err(Error::Parse(format!(
"insert_entry_value: inserting `{key}` into `{mapping_path}` failed the \
integrity check — the spliced entry did not load back as the value given \
(e.g. a key the mapping already inherits through a `{MERGE_KEY_SPELLING}` \
merge, or a layout the emitter could not reproduce at this indent); the \
document was left unchanged"
)));
}
Ok(())
}
pub fn push_back_value<E: Emit + ?Sized>(&mut self, path: &str, value: &E) -> Result<()> {
if let Err(e) = self.validate() {
return Err(Error::Parse(format!(
"push_back_value: the document does not parse, so `{path}` cannot be resolved \
({e}); the document was left unchanged"
)));
}
let expected_item = value.expected_value()?;
let (expected, len) = {
let cache = self.cache.borrow();
let (doc_value, _) = cache.as_ref().expect("validate populated the cache");
let len = sequence_len_at(doc_value, &parse_query_path(path), path)?;
(
expected_after_insert_item(doc_value, path, len, &expected_item)?,
len,
)
};
if len == 0 {
return Err(Error::Parse(format!(
"push_back_value: the sequence at `{path}` is empty, so it has no item to \
anchor indentation on — use `set` with a fragment instead"
)));
}
let (column, anchor_pos) = self.sequence_item_anchor(path, len - 1)?;
self.refuse_inside_aliased_anchor("push_back_value", path, anchor_pos)?;
let fragment = self.emit_sequence_item(value, column)?;
let snapshot = self.clone();
self.guarded_item_splice(
|doc| doc.push_back(path, &fragment),
&expected,
&snapshot,
&format!("push_back_value: appending to `{path}`"),
)
}
pub fn insert_after_value<E: Emit + ?Sized>(
&mut self,
item_path: &str,
value: &E,
) -> Result<()> {
if let Err(e) = self.validate() {
return Err(Error::Parse(format!(
"insert_after_value: the document does not parse, so `{item_path}` cannot be \
resolved ({e}); the document was left unchanged"
)));
}
let segments = parse_query_path(item_path);
let Some(&QuerySegment::Index(index)) = segments.last() else {
return Err(Error::Parse(
"insert_after_value: path must end with a sequence index, e.g. `items[2]`".into(),
));
};
let seq_path = sequence_parent_path(item_path);
let expected_item = value.expected_value()?;
let expected = {
let cache = self.cache.borrow();
let (doc_value, _) = cache.as_ref().expect("validate populated the cache");
expected_after_insert_item(doc_value, &seq_path, index + 1, &expected_item)?
};
let (column, anchor_pos) = self.sequence_item_anchor(&seq_path, index)?;
self.refuse_inside_aliased_anchor("insert_after_value", item_path, anchor_pos)?;
let fragment = self.emit_sequence_item(value, column)?;
let snapshot = self.clone();
self.guarded_item_splice(
|doc| doc.insert_after(item_path, &fragment),
&expected,
&snapshot,
&format!("insert_after_value: inserting after `{item_path}`"),
)
}
fn refuse_inside_aliased_anchor(&self, what: &str, path: &str, pos: usize) -> Result<()> {
if let Some((anchor, alias_count)) = self.aliased_anchor_covering(pos) {
return Err(Error::Parse(format!(
"{what}: `{path}` is inside the value anchored by `&{anchor}`, which has \
{alias_count} alias reference(s) — inserting here would insert at every \
`*{anchor}` site too; call `materialise_aliases_of(\"{anchor}\")` first to \
give each site its own copy, then insert"
)));
}
Ok(())
}
fn guarded_item_splice<F>(
&mut self,
splice: F,
expected: &Value,
snapshot: &Self,
what: &str,
) -> Result<()>
where
F: FnOnce(&mut Self) -> Result<()>,
{
if let Err(e) = splice(self) {
*self = snapshot.clone();
return Err(Error::Parse(format!(
"{what} could not be spliced ({e}); the document was left unchanged"
)));
}
if let Err(e) = self.validate() {
*self = snapshot.clone();
return Err(Error::Parse(format!(
"{what} left the document unable to re-parse ({e}); the document was left \
unchanged"
)));
}
if *self.as_value() != *expected {
*self = snapshot.clone();
return Err(Error::Parse(format!(
"{what} failed the integrity check — the spliced item did not load back as the \
value given (e.g. a layout the emitter could not reproduce at this indent); \
the document was left unchanged"
)));
}
Ok(())
}
fn emit_sequence_item<E: Emit + ?Sized>(&self, value: &E, column: usize) -> Result<String> {
let ctx = self.emit_ctx(column);
let fragment = value.emit(&ctx)?;
Ok(indent_continuation_lines(&fragment, column + 2))
}
fn sequence_item_anchor(&self, path: &str, index: usize) -> Result<(usize, usize)> {
let item_path = item_child_path(path, index);
let (start, _) = self.span_at(&item_path).ok_or_else(|| {
Error::Parse(format!(
"could not locate item {index} of `{path}` to anchor the new item's indentation"
))
})?;
let column = column_of_preceding_dash(&self.source, start).ok_or_else(|| {
Error::Parse(format!(
"only block sequences are supported (no `-` anchor before item {index} of \
`{path}`)"
))
})?;
Ok((column, start))
}
fn mapping_insert_anchor(&self, path: &str) -> Result<(usize, usize, usize)> {
let keys: Vec<String> = {
let cache = self.cache.borrow();
let (value, _) = cache.as_ref().expect("caller validated the document");
let target = if path.is_empty() {
value
} else {
path_value(value, path)
.ok_or_else(|| Error::Parse(format!("path not found: {path}")))?
};
let Value::Mapping(m) = target else {
return Err(Error::Parse(format!("`{path}` does not address a mapping")));
};
m.iter().map(|(k, _)| k.clone()).collect()
};
if keys.is_empty() {
return Err(Error::Parse(format!(
"the mapping at `{path}` is empty, so it has no entry to anchor indentation \
on — use `set` with a fragment instead"
)));
}
let anchor = keys.iter().rev().find_map(|key| {
let child = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
self.span_at(&child)
});
let (start, end) = anchor.ok_or_else(|| {
Error::Parse(format!(
"no entry of the mapping at `{path}` has source bytes of its own to anchor \
indentation on (every key is inherited through a `{MERGE_KEY_SPELLING}` \
merge) — use `set` with a fragment instead"
))
})?;
let column = column_of_key_at(&self.source, start).ok_or_else(|| {
Error::Parse(format!(
"could not locate the last key's column in `{path}` for indentation"
))
})?;
Ok((column, end_of_line(&self.source, end), start))
}
}
impl fmt::Display for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.green.text(&self.source))
}
}
pub fn parse_document(input: &str) -> Result<Document> {
let parsed = parse_full(input)?;
Ok(Document {
source: parsed.source,
green: parsed.green,
cache: core::cell::RefCell::new(Some((parsed.value, parsed.span_tree))),
last_repair_scope: core::cell::Cell::new(None),
})
}
pub fn parse_stream(input: &str) -> Result<Vec<Document>> {
let bounds = document_boundaries(input)?;
if bounds.len() <= 1 {
return Ok(vec![parse_document(input)?]);
}
let mut out = Vec::with_capacity(bounds.len());
for (s, e) in bounds {
if s == e {
continue;
}
out.push(parse_document(&input[s..e])?);
}
Ok(out)
}
fn scope_for_kind(kind: SyntaxKind) -> RepairScope {
match kind {
SyntaxKind::MappingEntry | SyntaxKind::SequenceItem => RepairScope::Entry,
SyntaxKind::BlockMapping
| SyntaxKind::BlockSequence
| SyntaxKind::FlowMapping
| SyntaxKind::FlowSequence => RepairScope::Collection,
_ => RepairScope::Document,
}
}
fn is_phase_a_repairable(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::BlockMapping
| SyntaxKind::BlockSequence
| SyntaxKind::MappingEntry
| SyntaxKind::SequenceItem
)
}
struct Candidate {
kind: SyntaxKind,
start: usize,
end: usize,
}
fn ancestor_candidates(root: &GreenNode, start: usize, end: usize) -> Vec<Candidate> {
let mut out = Vec::new();
collect_ancestors(root, start, end, 0, &mut out);
out.reverse();
out
}
fn collect_ancestors(
node: &GreenNode,
start: usize,
end: usize,
base: usize,
out: &mut Vec<Candidate>,
) {
let node_end = base + node.text_len();
if start >= base && end <= node_end {
out.push(Candidate {
kind: node.kind(),
start: base,
end: node_end,
});
let mut pos = base;
for child in node.children() {
let len = child.text_len();
let child_end = pos + len;
if start >= pos && end <= child_end {
if let GreenChild::Node(inner) = child {
collect_ancestors(inner, start, end, pos, out);
}
break;
}
pos += len;
}
}
}
fn region_has_anchor_alias_or_tag(root: &GreenNode, start: usize, end: usize) -> bool {
let mut found = false;
walk_tokens(root, 0, &mut |kind, range| {
if range.start >= end || range.end <= start {
return; }
if matches!(
kind,
SyntaxKind::AnchorMark | SyntaxKind::AliasMark | SyntaxKind::TagMark
) {
found = true;
}
});
found
}
fn walk_tokens(
node: &GreenNode,
base: usize,
visit: &mut dyn FnMut(SyntaxKind, core::ops::Range<usize>),
) {
let mut pos = base;
for child in node.children() {
let len = child.text_len();
match child {
GreenChild::Token { kind, .. } => {
visit(*kind, pos..pos + len);
}
GreenChild::Node(inner) => walk_tokens(inner, pos, visit),
}
pos += len;
}
}
fn replacement_introduces_anchor_alias_or_tag(replacement: &str) -> bool {
replacement.bytes().any(|b| matches!(b, b'&' | b'*' | b'!'))
}
fn resolve_path_in_green(
root: &GreenNode,
segments: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (collection, base) = first_collection_child(root, 0)?;
walk_path(collection, segments, base, source)
}
fn first_collection_child(node: &GreenNode, base: usize) -> Option<(&GreenNode, usize)> {
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(inner) = child {
if matches!(
inner.kind(),
SyntaxKind::BlockMapping
| SyntaxKind::BlockSequence
| SyntaxKind::FlowMapping
| SyntaxKind::FlowSequence
) {
return Some((inner, pos));
}
}
pos += len;
}
None
}
fn walk_path(
node: &GreenNode,
segments: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
if segments.is_empty() {
return Some((base, base + node.text_len()));
}
let (head, tail) = segments.split_first()?;
match (head, node.kind()) {
(QuerySegment::Key(k), SyntaxKind::BlockMapping)
| (QuerySegment::Key(k), SyntaxKind::FlowMapping) => {
walk_mapping(node, k, tail, base, source)
}
(QuerySegment::Index(i), SyntaxKind::BlockSequence)
| (QuerySegment::Index(i), SyntaxKind::FlowSequence) => {
walk_sequence(node, *i, tail, base, source)
}
_ => None,
}
}
fn walk_mapping(
node: &GreenNode,
key: &str,
tail: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
let mut found: Option<(&GreenNode, usize)> = None;
let mut undecodable_key = false;
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(entry) = child {
if entry.kind() == SyntaxKind::MappingEntry {
match entry_key_text(entry, source, pos) {
Some(entry_key) => {
if entry_key == key {
found = Some((entry, pos));
}
}
None => undecodable_key = true,
}
}
}
pos += len;
}
if undecodable_key {
return None;
}
let (entry, entry_pos) = found?;
resolve_value_in_entry(entry, entry_pos, tail, source)
}
fn walk_sequence(
node: &GreenNode,
target_index: usize,
tail: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
let mut pos = base;
let mut idx = 0usize;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(item) = child {
if item.kind() == SyntaxKind::SequenceItem {
if idx == target_index {
return resolve_value_in_item(item, pos, tail, source);
}
idx += 1;
}
}
pos += len;
}
None
}
fn entry_key_text<'s>(entry: &GreenNode, source: &'s str, base: usize) -> Option<Cow<'s, str>> {
let mut pos = base;
for child in entry.children() {
let child_len = child.text_len();
match child {
GreenChild::Token { kind, len } => {
let start = pos;
let end = pos + *len as usize;
match kind {
SyntaxKind::QuestionIndicator
| SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::AnchorMark
| SyntaxKind::TagMark => {}
SyntaxKind::PlainScalar => {
return Some(Cow::Borrowed(&source[start..end]));
}
SyntaxKind::SingleQuotedScalar => {
return decode_single_quoted(&source[start..end]);
}
_ => return None,
}
}
GreenChild::Node(_) => {
return None;
}
}
pos += child_len;
}
None
}
fn decode_single_quoted(raw: &str) -> Option<Cow<'_, str>> {
let inner = raw.strip_prefix('\'')?.strip_suffix('\'')?;
if !inner.contains('\'') {
return Some(Cow::Borrowed(inner));
}
Some(Cow::Owned(inner.replace("''", "'")))
}
fn is_block_collection(k: SyntaxKind) -> bool {
matches!(k, SyntaxKind::BlockMapping | SyntaxKind::BlockSequence)
}
fn extend_to_line_start(source: &str, start: usize) -> usize {
let b = source.as_bytes();
let mut i = start;
while i > 0 && matches!(b[i - 1], b' ' | b'\t') {
i -= 1;
}
if i == 0 || matches!(b[i - 1], b'\n' | b'\r') {
i
} else {
start
}
}
fn resolve_value_in_entry(
entry: &GreenNode,
base: usize,
tail: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (value_kind, value_range, value_node) = entry_value(entry, base)?;
if tail.is_empty() {
let start = if is_block_collection(value_kind) {
extend_to_line_start(source, value_range.0)
} else {
value_range.0
};
return Some((start, value_range.1));
}
let node = value_node?;
walk_path(node, tail, value_range.0, source)
}
fn resolve_value_in_item(
item: &GreenNode,
base: usize,
tail: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (value_kind, value_range, value_node) = item_value(item, base)?;
if tail.is_empty() {
let start = if is_block_collection(value_kind) {
extend_to_line_start(source, value_range.0)
} else {
value_range.0
};
return Some((start, value_range.1));
}
let node = value_node?;
walk_path(node, tail, value_range.0, source)
}
fn entry_value(
entry: &GreenNode,
base: usize,
) -> Option<(SyntaxKind, (usize, usize), Option<&GreenNode>)> {
let mut pos = base;
let mut after_colon = false;
let mut prefix_start: Option<usize> = None;
for child in entry.children() {
let len = child.text_len();
let child_start = pos;
let child_end = pos + len;
match child {
GreenChild::Token { kind, .. } => {
if !after_colon {
if *kind == SyntaxKind::ColonIndicator {
after_colon = true;
}
} else if *kind == SyntaxKind::AliasMark {
return None;
} else if is_value_property_kind(*kind) {
let _ = prefix_start.get_or_insert(child_start);
} else if !is_trivia_kind(*kind) {
let start = prefix_start.unwrap_or(child_start);
return Some((*kind, (start, child_end), None));
}
}
GreenChild::Node(inner) => {
if after_colon {
let start = prefix_start.unwrap_or(child_start);
return Some((inner.kind(), (start, child_end), Some(inner)));
}
}
}
pos += len;
}
prefix_start.map(|start| (SyntaxKind::PlainScalar, (start, pos), None))
}
fn item_value(
item: &GreenNode,
base: usize,
) -> Option<(SyntaxKind, (usize, usize), Option<&GreenNode>)> {
let mut pos = base;
let mut after_dash = false;
let mut prefix_start: Option<usize> = None;
for child in item.children() {
let len = child.text_len();
let child_start = pos;
let child_end = pos + len;
match child {
GreenChild::Token { kind, .. } => {
if !after_dash {
if *kind == SyntaxKind::DashIndicator {
after_dash = true;
}
} else if *kind == SyntaxKind::AliasMark {
return None;
} else if is_value_property_kind(*kind) {
let _ = prefix_start.get_or_insert(child_start);
} else if !is_trivia_kind(*kind) {
let start = prefix_start.unwrap_or(child_start);
return Some((*kind, (start, child_end), None));
}
}
GreenChild::Node(inner) => {
if after_dash {
let start = prefix_start.unwrap_or(child_start);
return Some((inner.kind(), (start, child_end), Some(inner)));
}
}
}
pos += len;
}
prefix_start.map(|start| (SyntaxKind::PlainScalar, (start, pos), None))
}
fn is_trivia_kind(k: SyntaxKind) -> bool {
matches!(
k,
SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::Bom
| SyntaxKind::Directive
)
}
fn is_value_property_kind(k: SyntaxKind) -> bool {
matches!(k, SyntaxKind::AnchorMark | SyntaxKind::TagMark)
}
fn trim_trailing_blank(source: &str, start: usize, mut end: usize) -> (usize, usize) {
let bytes = source.as_bytes();
while end > start {
match bytes[end - 1] {
b' ' | b'\t' | b'\n' | b'\r' => end -= 1,
_ => break,
}
}
(start, end)
}
fn trim_value_span(source: &str, start: usize, end: usize) -> (usize, usize) {
if is_keep_chomped_block_scalar(source, start, end) {
(start, end)
} else {
trim_trailing_blank(source, start, end)
}
}
fn is_keep_chomped_block_scalar(source: &str, start: usize, end: usize) -> bool {
let bytes = source.as_bytes();
let start = skip_value_property_prefix(bytes, start, end);
if start >= end || (bytes[start] != b'|' && bytes[start] != b'>') {
return false;
}
for &b in &bytes[start + 1..end] {
match b {
b'\n' | b'\r' => return false,
b'+' => return true,
_ => {}
}
}
false
}
fn skip_value_property_prefix(bytes: &[u8], mut start: usize, end: usize) -> usize {
loop {
while start < end && matches!(bytes[start], b' ' | b'\t') {
start += 1;
}
if start < end && matches!(bytes[start], b'&' | b'!') {
start += 1;
while start < end && !matches!(bytes[start], b' ' | b'\t' | b'\n' | b'\r') {
start += 1;
}
} else {
return start;
}
}
}
fn span_tree_end(t: &SpanTree) -> usize {
match t {
SpanTree::Leaf(_, e) => *e,
SpanTree::Sequence { end, .. } | SpanTree::Mapping { end, .. } => *end,
SpanTree::Alias(inner) => span_tree_end(inner),
}
}
fn span_tree_bounds(t: &SpanTree) -> (usize, usize) {
match t {
SpanTree::Leaf(s, e) => (*s, *e),
SpanTree::Sequence { start, end, .. } | SpanTree::Mapping { start, end, .. } => {
(*start, *end)
}
SpanTree::Alias(inner) => span_tree_bounds(inner),
}
}
fn resolve_span(
value: &Value,
span_tree: &SpanTree,
segments: &[QuerySegment],
) -> Option<((usize, usize), bool)> {
if let SpanTree::Alias(inner) = span_tree {
return resolve_span(value, inner, segments).map(|(span, _)| (span, true));
}
if segments.is_empty() {
return match span_tree {
SpanTree::Leaf(s, e) if s == e => None,
SpanTree::Leaf(s, e) => Some(((*s, *e), false)),
SpanTree::Sequence { start, end, .. } | SpanTree::Mapping { start, end, .. } => {
Some(((*start, *end), false))
}
SpanTree::Alias(_) => None, };
}
let (head, tail) = segments.split_first()?;
match (head, value, span_tree) {
(QuerySegment::Key(k), Value::Mapping(m), SpanTree::Mapping { entries, .. }) => {
for ((mk, mv), (_, child_tree)) in m.iter().zip(entries.iter()) {
if mk == k {
return resolve_span(mv, child_tree, tail);
}
}
None
}
(QuerySegment::Index(i), Value::Sequence(seq), SpanTree::Sequence { items, .. }) => {
let v = seq.get(*i)?;
let t = items.get(*i)?;
resolve_span(v, t, tail)
}
_ => None,
}
}
fn entry_line_span(
value: &Value,
span_tree: &SpanTree,
source: &str,
segments: &[QuerySegment],
) -> Result<(usize, usize, bool)> {
if segments.is_empty() {
return Err(Error::Parse(
"remove requires a non-empty path (cannot remove the document root)".into(),
));
}
let (head, tail) = segments
.split_first()
.ok_or_else(|| Error::Parse("path not found".into()))?;
if !tail.is_empty() {
let (child_value, child_tree) = match (head, value, span_tree) {
(QuerySegment::Key(k), Value::Mapping(m), SpanTree::Mapping { entries, .. }) => {
let pos = m
.iter()
.position(|(mk, _)| mk == k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?;
(
m.iter().nth(pos).map(|(_, v)| v).expect("pos in range"),
&entries[pos].1,
)
}
(QuerySegment::Index(i), Value::Sequence(seq), SpanTree::Sequence { items, .. }) => (
seq.get(*i).ok_or_else(|| {
Error::Parse(format!("path not found: index {i} out of bounds"))
})?,
items.get(*i).ok_or_else(|| {
Error::Parse(format!("path not found: index {i} out of bounds"))
})?,
),
_ => return Err(Error::Parse("path not found".into())),
};
return entry_line_span(child_value, child_tree, source, tail);
}
match (head, value, span_tree) {
(QuerySegment::Key(k), Value::Mapping(m), SpanTree::Mapping { entries, .. }) => {
if m.len() <= 1 {
return Err(Error::Parse(
"remove cannot delete the only entry of a mapping".into(),
));
}
let pos = m
.iter()
.position(|(mk, _)| mk == k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?;
let ((key_start, _key_end), child_tree) = &entries[pos];
let raw_value_end = span_tree_end(child_tree);
let (_, value_end) = trim_trailing_blank(source, *key_start, raw_value_end);
let (ls, le) = line_extent(source, *key_start, value_end);
let multiline = source[*key_start..value_end].contains('\n');
Ok((ls, le, multiline))
}
(QuerySegment::Index(i), Value::Sequence(seq), SpanTree::Sequence { items, .. }) => {
if seq.len() <= 1 {
return Err(Error::Parse(
"remove cannot delete the only entry of a sequence".into(),
));
}
let item_tree = items
.get(*i)
.ok_or_else(|| Error::Parse(format!("path not found: index {i} out of bounds")))?;
let (value_start, raw_value_end) = span_tree_bounds(item_tree);
let (_, value_end) = trim_trailing_blank(source, value_start, raw_value_end);
let dash_pos = locate_preceding_dash(source, value_start).ok_or_else(|| {
Error::Parse(
"remove: could not locate '-' indicator preceding sequence item".into(),
)
})?;
let (ls, le) = line_extent(source, dash_pos, value_end);
let multiline = source[dash_pos..value_end].contains('\n');
Ok((ls, le, multiline))
}
_ => Err(Error::Parse("path not found".into())),
}
}
const MERGE_KEY_SPELLING: &str = "<<";
fn parse_rename_path(path: &str) -> Result<Vec<QuerySegment>> {
let mut rest = path;
while let Some(open) = rest.find('[') {
let after = &rest[open + 1..];
let close = after.find(']').unwrap_or(after.len());
let content = &after[..close];
if content.parse::<usize>().is_err() {
return Err(Error::Parse(format!(
"rename_key: `{path}` contains the bracket segment `[{content}]`, which is not \
a sequence index — a bracket segment must hold a non-negative integer, and a \
mapping key is addressed with dot notation (`parent.child`)"
)));
}
rest = &after[close..];
}
Ok(parse_query_path(path))
}
fn first_non_printable(key: &str) -> Option<char> {
key.chars().find(|&c| c != '\t' && c.is_control())
}
fn decode_key_token(raw: &str, kind: SyntaxKind) -> Option<String> {
match kind {
SyntaxKind::PlainScalar => Some(raw.to_owned()),
SyntaxKind::SingleQuotedScalar => decode_single_quoted(raw).map(Cow::into_owned),
SyntaxKind::DoubleQuotedScalar => {
let cfg = crate::parser::ParseConfig::default();
match crate::parser::parse_one_value(raw, &cfg).ok()? {
Value::String(s) => Some(s),
_ => None,
}
}
_ => None,
}
}
fn anchored_content_span(
node: &GreenNode,
base: usize,
mark_start: usize,
) -> Option<(usize, usize)> {
let mut pos = base;
let mut seen_mark = false;
for child in node.children() {
let len = child.text_len();
if seen_mark {
let trivia = matches!(
child,
GreenChild::Token { kind, .. }
if matches!(
kind,
SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::TagMark
)
);
if !trivia {
return Some((pos, pos + len));
}
} else if pos == mark_start
&& matches!(child, GreenChild::Token { kind, .. } if *kind == SyntaxKind::AnchorMark)
{
seen_mark = true;
}
pos += len;
}
if seen_mark {
return None;
}
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(inner) = child {
if pos <= mark_start && mark_start < pos + len {
return anchored_content_span(inner, pos, mark_start);
}
}
pos += len;
}
None
}
fn entry_key_site(
value: &Value,
span_tree: &SpanTree,
segments: &[QuerySegment],
new_key: &str,
) -> Result<(usize, usize)> {
if matches!(span_tree, SpanTree::Alias(_)) {
return Err(Error::Parse(
"rename_key: the path addresses alias-expanded content — an `*name` site reflects \
the anchor's entries and owns no key bytes of its own; rename the corresponding \
entry at the anchor's own definition instead"
.into(),
));
}
let (head, tail) = segments.split_first().ok_or_else(|| {
Error::Parse("rename_key requires a non-empty path addressing a mapping entry".into())
})?;
if !tail.is_empty() {
let (child_value, child_tree) = match (head, value, span_tree) {
(QuerySegment::Key(k), Value::Mapping(m), SpanTree::Mapping { entries, .. }) => {
let pos = m
.iter()
.position(|(mk, _)| mk == k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?;
let (_, child_tree) = entries.get(pos).ok_or_else(|| {
Error::Parse(format!(
"rename_key: key {k:?} was produced by a `<<` merge key and has \
no entry of its own to rename in this mapping"
))
})?;
(
m.iter().nth(pos).map(|(_, v)| v).expect("pos in range"),
child_tree,
)
}
(QuerySegment::Index(i), Value::Sequence(seq), SpanTree::Sequence { items, .. }) => (
seq.get(*i).ok_or_else(|| {
Error::Parse(format!("path not found: index {i} out of bounds"))
})?,
items.get(*i).ok_or_else(|| {
Error::Parse(format!("path not found: index {i} out of bounds"))
})?,
),
_ => return Err(Error::Parse("path not found".into())),
};
return entry_key_site(child_value, child_tree, tail, new_key);
}
match (head, value, span_tree) {
(QuerySegment::Key(k), Value::Mapping(m), SpanTree::Mapping { entries, .. }) => {
let pos = m
.iter()
.position(|(mk, _)| mk == k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?;
if k != new_key && m.contains_key(new_key) {
let merge_provided = m
.get_index_of(new_key)
.is_some_and(|idx| idx >= entries.len());
if merge_provided {
return Err(Error::Parse(format!(
"rename_key: {new_key:?} is provided by a `<<` merge key in this \
mapping — renaming {k:?} to it would create an explicit entry that \
overrides the merged value instead of renaming in place"
)));
}
return Err(Error::Parse(format!(
"rename_key: the mapping already has an entry named {new_key:?} — \
renaming {k:?} would create a duplicate key"
)));
}
let (key_span, _) = entries.get(pos).ok_or_else(|| {
Error::Parse(format!(
"rename_key: key {k:?} was produced by a `<<` merge key and has \
no entry of its own to rename in this mapping"
))
})?;
Ok(*key_span)
}
(QuerySegment::Index(_), _, _) => Err(Error::Parse(
"rename_key: path must address a mapping entry, not a sequence item".into(),
)),
_ => Err(Error::Parse("path not found".into())),
}
}
fn token_at_with_parent(
node: &GreenNode,
target: usize,
base: usize,
) -> Option<(SyntaxKind, (usize, usize), SyntaxKind)> {
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if pos <= target && target < pos + len {
return match child {
GreenChild::Token { kind, .. } => Some((*kind, (pos, pos + len), node.kind())),
GreenChild::Node(inner) => token_at_with_parent(inner, target, pos),
};
}
pos += len;
}
None
}
fn format_key_for_site(key: &str, kind: SyntaxKind) -> String {
let single_representable = !key.bytes().any(|b| b < 0x20 || b == 0x7F);
match kind {
SyntaxKind::SingleQuotedScalar if single_representable => format_single_quoted(key),
SyntaxKind::DoubleQuotedScalar => format_double_quoted(key),
_ => {
if is_plain_safe(key) {
key.to_owned()
} else {
format_double_quoted(key)
}
}
}
}
fn expected_after_rename(value: &Value, segments: &[QuerySegment], new_key: &str) -> Result<Value> {
let (last, parents) = segments.split_last().ok_or_else(|| {
Error::Parse("rename_key requires a non-empty path addressing a mapping entry".into())
})?;
let QuerySegment::Key(old_key) = last else {
return Err(Error::Parse(
"rename_key: path must address a mapping entry, not a sequence item".into(),
));
};
let mut expected = value.clone();
let mut cur = &mut expected;
for seg in parents {
cur = match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => m
.get_mut(k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?,
(QuerySegment::Index(i), Value::Sequence(seq)) => seq
.get_mut(*i)
.ok_or_else(|| Error::Parse(format!("path not found: index {i} out of bounds")))?,
_ => return Err(Error::Parse("path not found".into())),
};
}
let Value::Mapping(m) = cur else {
return Err(Error::Parse("path not found".into()));
};
let mut renamed = Mapping::with_capacity(m.len());
for (k, v) in m.iter() {
if k == old_key {
let _ = renamed.insert(new_key, v.clone());
} else {
let _ = renamed.insert(k.clone(), v.clone());
}
}
*m = renamed;
Ok(expected)
}
fn item_child_path(path: &str, i: usize) -> String {
if path.is_empty() {
format!("[{i}]")
} else {
format!("{path}[{i}]")
}
}
fn sequence_len_at(value: &Value, segments: &[QuerySegment], path: &str) -> Result<usize> {
let mut cur = value;
for seg in segments {
cur = match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => m
.get(k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?,
(QuerySegment::Index(i), Value::Sequence(seq)) => seq
.get(*i)
.ok_or_else(|| Error::Parse(format!("path not found: index {i} out of bounds")))?,
_ => {
return Err(Error::Parse(format!(
"swap_items: `{path}` does not resolve to a sequence"
)));
}
};
}
match cur {
Value::Sequence(seq) => Ok(seq.len()),
_ => Err(Error::Parse(format!(
"swap_items: `{path}` does not address a sequence"
))),
}
}
fn expected_after_swap(
value: &Value,
segments: &[QuerySegment],
i: usize,
j: usize,
path: &str,
) -> Result<Value> {
let mut expected = value.clone();
let mut cur = &mut expected;
for seg in segments {
cur = match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => m
.get_mut(k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?,
(QuerySegment::Index(idx), Value::Sequence(seq)) => {
seq.get_mut(*idx).ok_or_else(|| {
Error::Parse(format!("path not found: index {idx} out of bounds"))
})?
}
_ => {
return Err(Error::Parse(format!(
"swap_items: `{path}` does not resolve to a sequence"
)));
}
};
}
let Value::Sequence(seq) = cur else {
return Err(Error::Parse(format!(
"swap_items: `{path}` does not address a sequence"
)));
};
let vi = seq
.get(i)
.cloned()
.ok_or_else(|| Error::Parse(format!("swap_items: index {i} out of bounds")))?;
let vj = seq
.get(j)
.cloned()
.ok_or_else(|| Error::Parse(format!("swap_items: index {j} out of bounds")))?;
*seq.get_mut(i).expect("index i checked above") = vj;
*seq.get_mut(j).expect("index j checked above") = vi;
Ok(expected)
}
fn expected_after_insert_entry(
value: &Value,
mapping_path: &str,
key: &str,
child: &Value,
) -> Result<Value> {
let mut expected = value.clone();
let cur = if mapping_path.is_empty() {
&mut expected
} else {
path_value_mut(&mut expected, &parse_query_path(mapping_path))
.ok_or_else(|| Error::Parse(format!("path not found: {mapping_path}")))?
};
let Value::Mapping(m) = cur else {
return Err(Error::Parse(format!(
"`{mapping_path}` does not address a mapping"
)));
};
let _ = m.insert(key, child.clone());
Ok(expected)
}
fn expected_after_insert_item(
value: &Value,
seq_path: &str,
index: usize,
item: &Value,
) -> Result<Value> {
let mut expected = value.clone();
let cur = if seq_path.is_empty() {
&mut expected
} else {
path_value_mut(&mut expected, &parse_query_path(seq_path))
.ok_or_else(|| Error::Parse(format!("path not found: {seq_path}")))?
};
let Value::Sequence(seq) = cur else {
return Err(Error::Parse(format!(
"`{seq_path}` does not address a sequence"
)));
};
if index > seq.len() {
return Err(Error::Parse(format!(
"index {index} is past the end of the sequence at `{seq_path}` (length {})",
seq.len()
)));
}
seq.insert(index, item.clone());
Ok(expected)
}
fn path_value_mut<'a>(value: &'a mut Value, segments: &[QuerySegment]) -> Option<&'a mut Value> {
let mut cur = value;
for seg in segments {
cur = match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => m.get_mut(k)?,
(QuerySegment::Index(i), Value::Sequence(seq)) => seq.get_mut(*i)?,
_ => return None,
};
}
Some(cur)
}
fn sequence_parent_path(item_path: &str) -> String {
match item_path.rfind('[') {
Some(i) => item_path[..i].to_owned(),
None => item_path.to_owned(),
}
}
fn indent_continuation_lines(fragment: &str, indent: usize) -> String {
if !fragment.contains('\n') {
return fragment.to_owned();
}
let pad = " ".repeat(indent);
let mut out = String::with_capacity(fragment.len() + indent * 4);
for (i, line) in fragment.split('\n').enumerate() {
if i > 0 {
out.push('\n');
if !line.is_empty() {
out.push_str(&pad);
}
}
out.push_str(line);
}
out
}
fn expected_after_remove(value: &Value, segments: &[QuerySegment]) -> Result<Value> {
let (last, parents) = segments
.split_last()
.ok_or_else(|| Error::Parse("remove requires a non-empty path".into()))?;
let mut expected = value.clone();
let mut cur = &mut expected;
for seg in parents {
cur = match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => m
.get_mut(k)
.ok_or_else(|| Error::Parse(format!("path not found: missing key {k:?}")))?,
(QuerySegment::Index(i), Value::Sequence(seq)) => seq
.get_mut(*i)
.ok_or_else(|| Error::Parse(format!("path not found: index {i} out of bounds")))?,
_ => return Err(Error::Parse("path not found".into())),
};
}
match (last, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => {
let mut rebuilt = Mapping::with_capacity(m.len().saturating_sub(1));
for (mk, mv) in m.iter() {
if mk != k {
let _ = rebuilt.insert(mk.clone(), mv.clone());
}
}
*m = rebuilt;
Ok(expected)
}
(QuerySegment::Index(i), Value::Sequence(seq)) => {
if *i >= seq.len() {
return Err(Error::Parse(format!(
"path not found: index {i} out of bounds"
)));
}
let _ = seq.remove(*i);
Ok(expected)
}
_ => Err(Error::Parse("path not found".into())),
}
}
fn path_value<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
let segments = parse_query_path(path);
let mut cur = value;
for seg in &segments {
match (seg, cur) {
(QuerySegment::Key(k), Value::Mapping(m)) => {
let (_k, v) = m.iter().find(|(mk, _)| *mk == k)?;
cur = v;
}
(QuerySegment::Index(i), Value::Sequence(seq)) => {
cur = seq.get(*i)?;
}
_ => return None,
}
}
Some(cur)
}
fn column_of_preceding_dash(source: &str, value_start: usize) -> Option<usize> {
let dash_pos = locate_preceding_dash(source, value_start)?;
let bytes = source.as_bytes();
let mut line_start = dash_pos;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
Some(dash_pos - line_start)
}
fn detect_indent_unit(source: &str) -> usize {
let mut prev_indent: Option<usize> = None;
let mut min_step: Option<usize> = None;
for line in source.lines() {
let mut spaces = 0;
let bytes = line.as_bytes();
let mut tab_seen = false;
for &b in bytes {
if b == b' ' {
spaces += 1;
} else if b == b'\t' {
tab_seen = true;
break;
} else {
break;
}
}
if tab_seen {
continue;
}
let trimmed = &line[spaces..];
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(prev) = prev_indent {
if spaces > prev {
let step = spaces - prev;
min_step = Some(min_step.map_or(step, |m| m.min(step)));
}
}
prev_indent = Some(spaces);
}
min_step.unwrap_or(2)
}
fn column_of_key_at(source: &str, value_start: usize) -> Option<usize> {
let bytes = source.as_bytes();
if value_start > bytes.len() {
return None;
}
let line_start = |pos: usize| -> usize {
let mut s = pos;
while s > 0 && bytes[s - 1] != b'\n' {
s -= 1;
}
s
};
let leading_spaces = |start: usize| -> usize {
let mut c = 0;
while start + c < bytes.len() && bytes[start + c] == b' ' {
c += 1;
}
c
};
let value_line_start = line_start(value_start);
let value_col = leading_spaces(value_line_start);
let mut probe = value_line_start + value_col;
let mut inline_content = false;
while probe < value_start {
let b = bytes[probe];
if b != b' ' && b != b'\t' {
inline_content = true;
break;
}
probe += 1;
}
if inline_content {
return Some(value_col);
}
if value_line_start == 0 {
return Some(value_col);
}
let mut cursor = value_line_start - 1; loop {
let mut prev_start = cursor;
while prev_start > 0 && bytes[prev_start - 1] != b'\n' {
prev_start -= 1;
}
let prev_col = leading_spaces(prev_start);
let first_content = prev_start + prev_col;
let after_content = cursor; let is_blank = first_content >= after_content;
let is_comment = !is_blank && bytes[first_content] == b'#';
if !is_blank && !is_comment && prev_col < value_col {
return Some(prev_col);
}
if prev_start == 0 {
return Some(value_col);
}
cursor = prev_start - 1;
}
}
fn detect_dominant_quote_style(root: &GreenNode) -> crate::ScalarStyle {
let mut single = 0_usize;
let mut double = 0_usize;
walk_tokens(root, 0, &mut |kind, _| match kind {
SyntaxKind::SingleQuotedScalar => single += 1,
SyntaxKind::DoubleQuotedScalar => double += 1,
_ => {}
});
if single == 0 && double == 0 {
return crate::ScalarStyle::Plain;
}
if single >= double {
crate::ScalarStyle::SingleQuoted
} else {
crate::ScalarStyle::DoubleQuoted
}
}
fn detect_dominant_flow_style(root: &GreenNode) -> crate::FlowStyle {
let mut block = 0_usize;
let mut flow = 0_usize;
walk_collections(root, &mut |kind| match kind {
SyntaxKind::BlockMapping | SyntaxKind::BlockSequence => block += 1,
SyntaxKind::FlowMapping | SyntaxKind::FlowSequence => flow += 1,
_ => {}
});
if flow > block {
crate::FlowStyle::Auto
} else {
crate::FlowStyle::Block
}
}
fn walk_collections(node: &GreenNode, visit: &mut dyn FnMut(SyntaxKind)) {
visit(node.kind());
for child in node.children() {
if let GreenChild::Node(inner) = child {
walk_collections(inner, visit);
}
}
}
fn leading_break_for_splice(source: &str, pos: usize) -> &'static str {
if pos == 0 || source.as_bytes()[pos - 1] == b'\n' {
""
} else {
"\n"
}
}
fn end_of_line(source: &str, pos: usize) -> usize {
let bytes = source.as_bytes();
let mut i = pos;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
if i < bytes.len() { i + 1 } else { i }
}
fn locate_preceding_dash(source: &str, value_start: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut i = value_start;
while i > 0 {
i -= 1;
match bytes[i] {
b' ' | b'\t' => {}
b'-' => return Some(i),
b'\n' | b'\r' => return None,
_ => return None,
}
}
None
}
fn line_extent(source: &str, start: usize, end: usize) -> (usize, usize) {
let bytes = source.as_bytes();
let mut s = start;
while s > 0 && bytes[s - 1] != b'\n' {
s -= 1;
}
let mut e = end;
while e < bytes.len() && bytes[e] != b'\n' {
e += 1;
}
if e < bytes.len() {
e += 1;
}
(s, e)
}
fn leaf_kind_at(node: &GreenNode, target: usize) -> Option<SyntaxKind> {
let mut pos = 0;
for child in node.children() {
let len = child.text_len();
match child {
GreenChild::Token { kind, .. } => {
if pos <= target && target < pos + len {
return Some(*kind);
}
}
GreenChild::Node(inner) => {
if pos <= target && target < pos + len {
return leaf_kind_at(inner, target - pos);
}
}
}
pos += len;
}
None
}
fn sibling_dominant_scalar_kind(node: &GreenNode, target: usize) -> Option<SyntaxKind> {
let (mapping, entry) = enclosing_mapping_and_entry(node, target, 0)?;
dominant_sibling_value_kind(mapping, entry)
}
fn enclosing_mapping_and_entry(
node: &GreenNode,
target: usize,
base: usize,
) -> Option<(&GreenNode, &GreenNode)> {
fn walk<'a>(
node: &'a GreenNode,
target: usize,
base: usize,
cur_mapping: Option<&'a GreenNode>,
cur_entry: Option<&'a GreenNode>,
) -> Option<(&'a GreenNode, &'a GreenNode)> {
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if pos <= target && target < pos + len {
match child {
GreenChild::Token { .. } => {
if let (Some(m), Some(e)) = (cur_mapping, cur_entry) {
return Some((m, e));
}
return None;
}
GreenChild::Node(inner) => {
let new_mapping = if inner.kind() == SyntaxKind::BlockMapping {
Some(inner)
} else {
cur_mapping
};
let new_entry = if inner.kind() == SyntaxKind::MappingEntry {
Some(inner)
} else {
cur_entry
};
if let Some(found) = walk(inner, target, pos, new_mapping, new_entry) {
return Some(found);
}
}
}
}
pos += len;
}
None
}
walk(node, target, base, None, None)
}
fn dominant_sibling_value_kind(mapping: &GreenNode, exclude: &GreenNode) -> Option<SyntaxKind> {
let exclude_ptr: *const GreenNode = exclude;
let mut plain = 0usize;
let mut single = 0usize;
let mut double = 0usize;
for child in mapping.children() {
if let GreenChild::Node(entry) = child {
if entry.kind() != SyntaxKind::MappingEntry {
continue;
}
let entry_ptr: *const GreenNode = entry;
if core::ptr::eq(entry_ptr, exclude_ptr) {
continue;
}
match entry_value_scalar_kind(entry) {
Some(SyntaxKind::PlainScalar) => plain += 1,
Some(SyntaxKind::SingleQuotedScalar) => single += 1,
Some(SyntaxKind::DoubleQuotedScalar) => double += 1,
_ => {}
}
}
}
if single >= 2 && single > double && single > plain {
return Some(SyntaxKind::SingleQuotedScalar);
}
if double >= 2 && double > single && double > plain {
return Some(SyntaxKind::DoubleQuotedScalar);
}
None
}
fn entry_value_scalar_kind(entry: &GreenNode) -> Option<SyntaxKind> {
let mut after_colon = false;
for child in entry.children() {
match child {
GreenChild::Token { kind, .. } => {
if *kind == SyntaxKind::ColonIndicator {
after_colon = true;
continue;
}
if after_colon
&& matches!(
kind,
SyntaxKind::PlainScalar
| SyntaxKind::SingleQuotedScalar
| SyntaxKind::DoubleQuotedScalar
| SyntaxKind::LiteralScalar
| SyntaxKind::FoldedScalar
)
{
return Some(*kind);
}
}
GreenChild::Node(_) => {
if after_colon {
return None;
}
}
}
}
None
}
struct SiteContext {
kind: SyntaxKind,
neighbour: Option<SyntaxKind>,
entry_col: usize,
}
fn format_value_for_site(value: &Value, ctx: &SiteContext) -> Result<String> {
match value {
Value::Null => Ok("null".to_string()),
Value::Bool(true) => Ok("true".to_string()),
Value::Bool(false) => Ok("false".to_string()),
Value::Number(n) => Ok(format_number(n)),
Value::String(s) => format_string_for_site(s, ctx),
Value::Sequence(_) | Value::Mapping(_) => Err(Error::Parse(
"set_value cannot replace a scalar with a collection (use `set` with a fragment)"
.into(),
)),
Value::Tagged(t) => format_value_for_site(t.value(), ctx),
}
}
pub(super) fn format_number(n: &Number) -> String {
n.to_string()
}
fn format_string_for_site(s: &str, ctx: &SiteContext) -> Result<String> {
if s.contains('\n') && can_use_block_literal(s) && is_block_site(ctx.kind) {
return Ok(format_block_literal(s, ctx.entry_col));
}
match ctx.kind {
SyntaxKind::PlainScalar => {
match ctx.neighbour {
Some(SyntaxKind::SingleQuotedScalar) if !s.contains('\n') => {
Ok(format_single_quoted(s))
}
Some(SyntaxKind::DoubleQuotedScalar) => Ok(format_double_quoted(s)),
_ => {
if is_plain_safe(s) {
Ok(s.to_string())
} else {
Ok(format_double_quoted(s))
}
}
}
}
SyntaxKind::SingleQuotedScalar => Ok(format_single_quoted(s)),
SyntaxKind::DoubleQuotedScalar => Ok(format_double_quoted(s)),
SyntaxKind::LiteralScalar | SyntaxKind::FoldedScalar => {
if !s.contains('\n') {
if is_plain_safe(s) {
Ok(s.to_string())
} else {
Ok(format_double_quoted(s))
}
} else if can_use_block_literal(s) {
Ok(format_block_literal(s, ctx.entry_col))
} else {
Err(Error::Parse(
"set_value: existing block scalar can only be replaced with a string \
whose content lines do not begin with whitespace or control characters yet"
.into(),
))
}
}
_ => Err(Error::Parse(
"set_value: target site is not a scalar leaf".into(),
)),
}
}
fn is_block_site(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::PlainScalar
| SyntaxKind::SingleQuotedScalar
| SyntaxKind::DoubleQuotedScalar
| SyntaxKind::LiteralScalar
| SyntaxKind::FoldedScalar
)
}
pub(super) fn can_use_block_literal(s: &str) -> bool {
if s.is_empty() {
return false;
}
for &b in s.as_bytes() {
if (b < 0x20 && b != b'\n' && b != b'\t') || b == 0x7F {
return false;
}
}
let trimmed = s.strip_suffix('\n').unwrap_or(s);
if trimmed.ends_with('\n') {
return false;
}
for line in trimmed.split('\n') {
if line.starts_with(' ') || line.starts_with('\t') {
return false;
}
}
true
}
pub(super) fn format_block_literal(s: &str, entry_col: usize) -> String {
let trailing_nl = s.ends_with('\n');
let body = if trailing_nl { &s[..s.len() - 1] } else { s };
let indent_str = " ".repeat(entry_col + 2);
let mut out =
String::with_capacity(s.len() + 8 + indent_str.len() * (body.matches('\n').count() + 1));
out.push('|');
if !trailing_nl {
out.push('-');
}
out.push('\n');
let mut first = true;
for line in body.split('\n') {
if !first {
out.push('\n');
}
first = false;
out.push_str(&indent_str);
out.push_str(line);
}
out
}
fn entry_indent_column(source: &str, pos: usize) -> usize {
let bytes = source.as_bytes();
let mut line_start = pos.min(bytes.len());
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut col = line_start;
while col < bytes.len() && (bytes[col] == b' ' || bytes[col] == b'\t') {
col += 1;
}
col - line_start
}
pub(super) fn is_plain_safe(s: &str) -> bool {
if s.is_empty() {
return false;
}
if matches!(
s,
"null"
| "Null"
| "NULL"
| "~"
| "true"
| "True"
| "TRUE"
| "false"
| "False"
| "FALSE"
| "yes"
| "Yes"
| "YES"
| "no"
| "No"
| "NO"
| "on"
| "On"
| "ON"
| "off"
| "Off"
| "OFF"
) {
return false;
}
if looks_like_number(s) {
return false;
}
let bytes = s.as_bytes();
let first = bytes[0];
if matches!(
first,
b'-' | b'?'
| b':'
| b','
| b'['
| b']'
| b'{'
| b'}'
| b'#'
| b'&'
| b'*'
| b'!'
| b'|'
| b'>'
| b'\''
| b'"'
| b'%'
| b'@'
| b'`'
| b' '
| b'\t'
) {
return false;
}
if matches!(*bytes.last().unwrap(), b' ' | b'\t') {
return false;
}
let mut prev: u8 = 0;
for &b in bytes {
if b < 0x20 || b == 0x7F {
return false;
}
if b == b' ' && prev == b':' {
return false;
}
if b == b'#' && prev == b' ' {
return false;
}
prev = b;
}
true
}
fn looks_like_number(s: &str) -> bool {
let mut chars = s.chars();
let first = match chars.next() {
Some(c) => c,
None => return false,
};
let candidate = matches!(first, '-' | '+' | '.') || first.is_ascii_digit();
if !candidate {
return false;
}
let scalar = crate::streaming::resolve_plain_ext(s, false, false, false, false, false, false);
match scalar {
crate::streaming::Scalar::Int(_) | crate::streaming::Scalar::Float(_) => true,
#[cfg(feature = "lossless-u64")]
crate::streaming::Scalar::Uint(_) => true,
_ => false,
}
}
pub(super) fn format_single_quoted(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for ch in s.chars() {
if ch == '\'' {
out.push_str("''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
pub(super) fn format_double_quoted(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
let _ = write!(&mut out, "\\u{:04X}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}