use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::str::FromStr;
use crate::TomlVersion;
use crate::error::Error;
use crate::parser;
use crate::serializer::format_key;
use crate::span::{CommentIndex, PathSegment, SectionIndex, SpanIndex, TextSpan, parse_value_path};
use crate::value::{Table, Value};
#[derive(Debug, Clone)]
pub struct Document {
source: String,
table: Table,
spans: SpanIndex,
comments: CommentIndex,
sections: SectionIndex,
}
impl Document {
pub fn parse(input: &str) -> Result<Self, Error> {
let (table, spans, comments, sections) =
parser::parse_with_spans(input, TomlVersion::V1_0)?;
Ok(Self {
source: input.to_owned(),
table,
spans,
comments,
sections,
})
}
pub fn as_str(&self) -> &str {
&self.source
}
pub fn as_table(&self) -> &Table {
&self.table
}
pub fn spans(&self) -> &SpanIndex {
&self.spans
}
pub fn comments(&self) -> &CommentIndex {
&self.comments
}
pub fn span(&self, path: &[PathSegment]) -> Option<TextSpan> {
self.spans.get(path)
}
pub fn trailing_comment_span(&self, path: &[PathSegment]) -> Option<TextSpan> {
self.comments.trailing_for(path)
}
pub fn trailing_comment_span_path(&self, path: &str) -> Option<TextSpan> {
let parsed = parse_value_path(path).ok()?;
self.trailing_comment_span(&parsed)
}
pub fn get(&self, path: &[PathSegment]) -> Option<&Value> {
value_at_path(&self.table, path)
}
pub fn get_path(&self, path: &str) -> Option<&Value> {
let parsed = parse_value_path(path).ok()?;
self.get(&parsed)
}
pub fn sections(&self) -> &SectionIndex {
&self.sections
}
pub fn set(&mut self, path: &[PathSegment], new_value: Value) -> Result<(), Error> {
if let Some(span) = self.spans.get(path) {
let replacement = crate::to_inline_string(&new_value)?;
if span.start > span.end || span.end > self.source.len() {
return Err(Error::serialize("invalid value span"));
}
let mut next_source = self.source.clone();
next_source.replace_range(span.start..span.end, &replacement);
self.reparse(next_source)
} else {
self.insert_at_path(path, new_value)
}
}
pub fn set_path(&mut self, path: &str, new_value: Value) -> Result<(), Error> {
let parsed = parse_value_path(path)?;
self.set(&parsed, new_value)
}
fn reparse(&mut self, next_source: String) -> Result<(), Error> {
let (next_table, next_spans, next_comments, next_sections) =
parser::parse_with_spans(&next_source, TomlVersion::V1_0)?;
self.source = next_source;
self.table = next_table;
self.spans = next_spans;
self.comments = next_comments;
self.sections = next_sections;
Ok(())
}
fn insert_at_path(&mut self, path: &[PathSegment], new_value: Value) -> Result<(), Error> {
let last = path.last().ok_or_else(|| Error::serialize("empty path"))?;
let PathSegment::Key(key) = last else {
return Err(Error::serialize("cannot insert an array element via set"));
};
let parent_path = &path[..path.len() - 1];
if !parent_path.is_empty()
&& let Some(parent_span) = self.spans.get(parent_path)
{
let parent_text = &self.source[parent_span.start..parent_span.end];
if parent_text.starts_with('{') {
return self.insert_into_inline_table(path, new_value);
}
}
if !parent_path.is_empty() && value_at_path(&self.table, parent_path).is_none() {
return self.insert_with_new_section(path, new_value);
}
let inline_value = crate::to_inline_string(&new_value)?;
let key_text = format_key(key);
let insert_text = format!("{key_text} = {inline_value}\n");
let insert_pos = self.find_insert_position(parent_path)?;
let needs_newline = insert_pos > 0 && self.source.as_bytes()[insert_pos - 1] != b'\n';
let mut next_source = self.source.clone();
if needs_newline {
next_source.insert_str(insert_pos, &format!("\n{insert_text}"));
} else {
next_source.insert_str(insert_pos, &insert_text);
}
self.reparse(next_source)
}
fn find_insert_position(&self, parent_path: &[PathSegment]) -> Result<usize, Error> {
if parent_path.is_empty() {
let pos = strip_trailing_blank_lines(&self.source, self.sections.root_end);
return Ok(pos);
}
let parent_value = value_at_path(&self.table, parent_path)
.ok_or_else(|| Error::serialize("parent table does not exist"))?;
match parent_value {
Value::Table(_) => {}
_ => {
return Err(Error::serialize("parent path does not point to a table"));
}
}
if let Some(section_span) = self.sections.get(parent_path) {
let pos = strip_trailing_blank_lines(&self.source, section_span.body_end);
return Ok(pos);
}
Err(Error::serialize(
"cannot determine insert position for the parent table",
))
}
fn insert_with_new_section(
&mut self,
path: &[PathSegment],
new_value: Value,
) -> Result<(), Error> {
let last = match path.last() {
Some(PathSegment::Key(key)) => key,
_ => return Err(Error::serialize("leaf must be a key")),
};
let parent_path = &path[..path.len() - 1];
self.validate_intermediate_path(parent_path)?;
let header = format_section_header(parent_path);
let inline_value = crate::to_inline_string(&new_value)?;
let key_text = format_key(last);
let insert_text = format!("{header}\n{key_text} = {inline_value}\n");
let insert_pos = self.find_ancestor_section_end(parent_path)?;
let needs_newline = insert_pos > 0 && self.source.as_bytes()[insert_pos - 1] != b'\n';
let mut next_source = self.source.clone();
if needs_newline {
next_source.insert_str(insert_pos, &format!("\n{insert_text}"));
} else {
next_source.insert_str(insert_pos, &insert_text);
}
self.reparse(next_source)
}
fn validate_intermediate_path(&self, parent_path: &[PathSegment]) -> Result<(), Error> {
let mut found_missing = false;
for i in 0..parent_path.len() {
if found_missing {
if matches!(parent_path[i], PathSegment::Index(_)) {
return Err(Error::serialize("cannot auto-create array elements"));
}
continue;
}
let partial = &parent_path[..=i];
match value_at_path(&self.table, partial) {
Some(v) if v.is_table() => {}
Some(_) => {
return Err(Error::serialize("intermediate path is not a table"));
}
None => {
if matches!(parent_path[i], PathSegment::Index(_)) {
return Err(Error::serialize("cannot auto-create array elements"));
}
found_missing = true;
}
}
}
Ok(())
}
fn find_ancestor_section_end(&self, parent_path: &[PathSegment]) -> Result<usize, Error> {
for depth in (1..=parent_path.len()).rev() {
let candidate = &parent_path[..depth];
if let Some(section_span) = self.sections.get(candidate) {
return Ok(strip_trailing_blank_lines(
&self.source,
section_span.body_end,
));
}
}
Ok(strip_trailing_blank_lines(
&self.source,
self.sections.root_end,
))
}
fn insert_into_inline_table(
&mut self,
path: &[PathSegment],
new_value: Value,
) -> Result<(), Error> {
let last = path.last().ok_or_else(|| Error::serialize("empty path"))?;
let PathSegment::Key(key) = last else {
return Err(Error::serialize("cannot insert an array element via set"));
};
let parent_path = &path[..path.len() - 1];
let parent_span = self
.spans
.get(parent_path)
.ok_or_else(|| Error::serialize("parent span not found"))?;
let parent_value = value_at_path(&self.table, parent_path)
.ok_or_else(|| Error::serialize("parent table does not exist"))?;
let parent_table = parent_value
.as_table()
.ok_or_else(|| Error::serialize("parent is not a table"))?;
let inline_value = crate::to_inline_string(&new_value)?;
let key_text = format_key(key);
let close_brace_pos = self.source[..parent_span.end]
.rfind('}')
.ok_or_else(|| Error::serialize("inline table closing brace not found"))?;
let has_space_before_brace =
close_brace_pos > 0 && self.source.as_bytes()[close_brace_pos - 1] == b' ';
let insert_text = if parent_table.is_empty() {
format!("{key_text} = {inline_value}")
} else {
format!(", {key_text} = {inline_value}")
};
let insert_pos = if has_space_before_brace {
close_brace_pos - 1
} else {
close_brace_pos
};
let mut next_source = self.source.clone();
next_source.insert_str(insert_pos, &insert_text);
self.reparse(next_source)
}
}
impl FromStr for Document {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
fn format_section_header(path: &[PathSegment]) -> String {
let keys: Vec<String> = path
.iter()
.filter_map(|seg| match seg {
PathSegment::Key(k) => Some(format_key(k)),
PathSegment::Index(_) => None,
})
.collect();
format!("[{}]", keys.join("."))
}
fn strip_trailing_blank_lines(source: &str, body_end: usize) -> usize {
let bytes = source.as_bytes();
let mut pos = body_end;
while pos > 0 && bytes[pos - 1] == b'\n' {
let line_end = pos - 1;
let mut line_start = line_end;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
let line_content = &bytes[line_start..line_end];
if line_content.iter().all(|&b| b == b' ' || b == b'\t') {
pos = line_start;
} else {
break;
}
}
pos
}
fn value_at_path<'a>(table: &'a Table, path: &[PathSegment]) -> Option<&'a Value> {
let (first, rest) = path.split_first()?;
let mut current = match first {
PathSegment::Key(key) => table.get(key)?,
PathSegment::Index(_) => return None,
};
for segment in rest {
match segment {
PathSegment::Key(key) => {
current = current.as_table()?.get(key)?;
}
PathSegment::Index(index) => {
current = current.as_array()?.get(*index)?;
}
}
}
Some(current)
}