use crate::{FlatDoc, Span, SpanKind};
#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Debug, Clone)]
pub(crate) struct Entry {
pub key_start: usize,
pub value_idx: usize,
}
#[inline]
pub(crate) fn clean_key<'s>(source: &'s str, span: &Span) -> &'s str {
let bytes = source.as_bytes();
let mut s = span.start as usize;
let mut e = span.end as usize;
while s < e && matches!(bytes[s], b' ' | b'\t') {
s += 1;
}
while e > s && matches!(bytes[e - 1], b' ' | b'\t') {
e -= 1;
}
if e > s + 1 && matches!(bytes[s], b'"' | b'\'') && bytes[s] == bytes[e - 1] {
s += 1;
e -= 1;
}
&source[s..e]
}
#[inline]
pub(crate) fn clean_key_span<'s>(source: &'s str, span: &Span) -> &'s str {
clean_key(source, span)
}
pub(crate) fn build_index(doc: &FlatDoc) -> Vec<(Vec<String>, Entry)> {
let mut index = Vec::with_capacity(doc.spans.len() / 8);
let mut current_table: Vec<&str> = Vec::new();
let mut i = 0;
while i < doc.spans.len() {
let span = doc.spans[i];
match span.kind {
SpanKind::ArrayOpen | SpanKind::ArrayTableOpen => {
let is_aot = span.kind == SpanKind::ArrayTableOpen;
let mut path: Vec<&str> = Vec::with_capacity(4);
i += 1;
while i < doc.spans.len() {
match doc.spans[i].kind {
SpanKind::BareKey | SpanKind::BasicString | SpanKind::LiteralString => {
path.push(clean_key(&doc.source, &doc.spans[i]));
i += 1;
}
SpanKind::Dot => {
i += 1;
}
SpanKind::ArrayClose => {
if !is_aot {
current_table = path;
}
i += 1;
break;
}
SpanKind::ArrayTableClose => {
i += 1;
break;
}
_ => {
i += 1;
break;
}
}
}
continue;
}
SpanKind::BareKey | SpanKind::BasicString | SpanKind::LiteralString => {
let mut key_parts: Vec<&str> = Vec::with_capacity(4);
key_parts.push(clean_key(&doc.source, &span));
let key_start = i;
let mut j = i + 1;
loop {
if j >= doc.spans.len() {
break;
}
match doc.spans[j].kind {
SpanKind::Whitespace | SpanKind::Newline | SpanKind::Comment => {
j += 1;
}
SpanKind::Dot => {
j += 1;
}
SpanKind::BareKey | SpanKind::BasicString | SpanKind::LiteralString => {
key_parts.push(clean_key(&doc.source, &doc.spans[j]));
j += 1;
}
SpanKind::Equals => {
j += 1;
let mut k = j;
while k < doc.spans.len() {
if is_value_kind(doc.spans[k].kind) {
let path: Vec<String> = current_table
.iter()
.chain(&key_parts)
.map(|s| s.to_string())
.collect();
index.push((
path,
Entry {
key_start,
value_idx: k,
},
));
i = k;
break;
}
match doc.spans[k].kind {
SpanKind::Whitespace
| SpanKind::Newline
| SpanKind::Comment => {
k += 1;
}
_ => {
break;
}
}
}
break;
}
_ => {
break;
}
}
}
i += 1;
}
_ => {
i += 1;
}
}
}
index
}
pub(crate) fn adjust_spans(spans: &mut [Span], pos: u32, delta: i32) {
if delta == 0 {
return;
}
let first = match spans.binary_search_by_key(&pos, |s| s.start) {
Ok(idx) => idx,
Err(idx) => idx,
};
for span in &mut spans[first..] {
span.start = (span.start as i32 + delta) as u32;
span.end = (span.end as i32 + delta) as u32;
}
}
fn is_value_kind(k: SpanKind) -> bool {
matches!(
k,
SpanKind::Integer
| SpanKind::Float
| SpanKind::Boolean
| SpanKind::Datetime
| SpanKind::BasicString
| SpanKind::LiteralString
| SpanKind::MlBasicString
| SpanKind::MlLiteralString
| SpanKind::InlineTableOpen
| SpanKind::ArrayOpen
)
}
#[derive(Debug)]
pub enum EditError {
NotFound,
InvalidPath,
SectionExists,
TableMismatch,
}
#[cfg(feature = "alloc")]
impl core::fmt::Display for EditError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
EditError::NotFound => write!(f, "key not found"),
EditError::InvalidPath => write!(f, "invalid path or out of bounds"),
EditError::SectionExists => write!(f, "target section already exists"),
EditError::TableMismatch => write!(f, "source and destination tables must match"),
}
}
}
impl FlatDoc {
pub fn span_text(&self, span: &Span) -> &str {
&self.source[span.start as usize..span.end as usize]
}
pub fn set(&mut self, path: &[&str], new_value: &str) -> Result<(), EditError> {
let index = build_index(self);
let entry = index
.iter()
.find(|(p, _)| p.iter().map(|s| s.as_str()).collect::<Vec<_>>() == path)
.map(|(_, e)| e)
.ok_or(EditError::NotFound)?;
let value_span = self.spans[entry.value_idx];
let old_len = (value_span.end - value_span.start) as usize;
let new_bytes = new_value.as_bytes();
let delta = new_bytes.len() as i32 - old_len as i32;
let start = value_span.start as usize;
let end = value_span.end as usize;
self.source.replace_range(start..end, new_value);
adjust_spans(&mut self.spans, value_span.end, delta);
Ok(())
}
pub fn insert(&mut self, table_path: &[&str], key: &str, value: &str) -> Result<(), EditError> {
let index = build_index(self);
let entries_in_table: Vec<_> = index
.iter()
.filter(|(p, _)| {
p.len() == table_path.len() + 1
&& p[..table_path.len()]
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
== table_path
})
.collect();
let (insertion_point, indentation) = if let Some((_, last)) = entries_in_table.last() {
let last_span = self.spans[last.key_start];
let mut line_start = last_span.start as usize;
while line_start > 0 && self.source.as_bytes()[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut indent_end = line_start;
while indent_end < last_span.start as usize
&& matches!(self.source.as_bytes()[indent_end], b' ' | b'\t')
{
indent_end += 1;
}
let indent = &self.source[line_start..indent_end];
let mut end = last_span.end as usize;
while end < self.source.len() && self.source.as_bytes()[end] != b'\n' {
end += 1;
}
if end < self.source.len() {
end += 1;
}
(end as u32, format!("{indent}{key} = {value}\n"))
} else {
let header_start = if table_path.is_empty() {
0 } else {
let mut found = None;
let mut i = 0;
while i < self.spans.len() {
if self.spans[i].kind == SpanKind::ArrayOpen {
let mut j = i + 1;
let mut hdr = Vec::new();
while j < self.spans.len() {
match self.spans[j].kind {
SpanKind::BareKey
| SpanKind::BasicString
| SpanKind::LiteralString => {
hdr.push(
clean_key_span(&self.source, &self.spans[j]).to_string(),
);
j += 1;
}
SpanKind::Dot => {
j += 1;
}
SpanKind::ArrayClose => {
if hdr == table_path {
found = Some(self.spans[j].end);
}
break;
}
_ => {
break;
}
}
}
if found.is_some() {
break;
}
}
i += 1;
}
found.unwrap_or(0)
};
let mut pos = header_start as usize;
while pos < self.source.len() && self.source.as_bytes()[pos] != b'\n' {
pos += 1;
}
if pos < self.source.len() {
pos += 1;
}
(pos as u32, format!("{key} = {value}\n"))
};
let delta = indentation.len() as i32;
self.source
.insert_str(insertion_point as usize, &indentation);
adjust_spans(&mut self.spans, insertion_point, delta);
Ok(())
}
pub fn remove(&mut self, path: &[&str]) -> Result<(), EditError> {
let index = build_index(self);
let entry = index
.iter()
.find(|(p, _)| p.iter().map(|s| s.as_str()).collect::<Vec<_>>() == path)
.map(|(_, e)| e)
.ok_or(EditError::NotFound)?;
let key_span = self.spans[entry.key_start];
let value_span = self.spans[entry.value_idx];
let mut remove_start = key_span.start as usize;
while remove_start > 0 && self.source.as_bytes()[remove_start - 1] != b'\n' {
remove_start -= 1;
}
let mut remove_end = value_span.end as usize;
while remove_end < self.source.len() && self.source.as_bytes()[remove_end] != b'\n' {
remove_end += 1;
}
if remove_end < self.source.len() {
remove_end += 1;
}
let old_len = (remove_end - remove_start) as i32;
self.source.replace_range(remove_start..remove_end, "");
adjust_spans(&mut self.spans, remove_start as u32, -old_len);
Ok(())
}
}