use toml_edit::{Decor, DocumentMut, RawString};
#[must_use]
pub fn comments_before(decor: &Decor) -> Vec<String> {
lines_of(decor.prefix())
}
#[must_use]
pub fn comment_beside(decor: &Decor) -> Option<String> {
lines_of(decor.suffix()).into_iter().next()
}
#[must_use]
pub fn trailing_comments(doc: &DocumentMut) -> Vec<String> {
lines_of(Some(doc.trailing()))
}
pub fn set_comments_before(decor: &mut Decor, lines: &[String]) {
let raw = text_of(decor.prefix());
decor.set_prefix(rewritten(raw, lines));
}
pub fn set_comment_beside(decor: &mut Decor, comment: Option<&str>) {
let raw = text_of(decor.suffix());
let suffix = match comment {
None => String::new(),
Some(text) => {
let before = raw.split('#').next().unwrap_or("");
let before = if before.trim().is_empty() && !before.is_empty() {
before
} else {
" "
};
format!("{before}{}", hashed(text))
}
};
decor.set_suffix(suffix);
}
pub fn set_trailing_comments(doc: &mut DocumentMut, lines: &[String]) {
let raw = text_of(Some(doc.trailing()));
doc.set_trailing(rewritten(raw, lines));
}
fn text_of(raw: Option<&RawString>) -> &str {
raw.and_then(RawString::as_str).unwrap_or("")
}
fn lines_of(raw: Option<&RawString>) -> Vec<String> {
text_of(raw)
.lines()
.filter_map(|l| l.trim().strip_prefix('#').map(|c| c.trim().to_owned()))
.collect()
}
fn hashed(text: &str) -> String {
if text.is_empty() {
"#".to_owned()
} else {
format!("# {text}")
}
}
fn rewritten(raw: &str, lines: &[String]) -> String {
let (body, indent) = match raw.rfind('\n') {
Some(i) => (&raw[..=i], &raw[i + 1..]),
None => ("", raw),
};
let body: Vec<&str> = body.split_inclusive('\n').collect();
let first_comment = body.iter().position(|l| !l.trim().is_empty());
let last_comment = body.iter().rposition(|l| !l.trim().is_empty());
let (before, after) = match (first_comment, last_comment) {
(Some(first), Some(last)) => (&body[..first], &body[last + 1..]),
_ => (&body[..], &body[..0]),
};
let mut out: String = before.concat();
for line in lines {
out.push_str(indent);
out.push_str(&hashed(line));
out.push('\n');
}
out.push_str(&after.concat());
out.push_str(indent);
out
}
#[cfg(test)]
mod tests {
use super::{
comment_beside, comments_before, set_comment_beside, set_comments_before,
set_trailing_comments, trailing_comments,
};
use toml_edit::DocumentMut;
const DOC: &str = "\
# above first
first = 1 # beside first
# above second, indented
second = 2
[table]
# in the table
third = 3
# at the end
";
fn doc() -> DocumentMut {
DOC.parse().expect("valid TOML")
}
#[test]
fn every_slot_reads_its_comment() {
let d = doc();
let t = d.as_table();
assert_eq!(
comments_before(t.key("first").unwrap().leaf_decor()),
["above first"]
);
assert_eq!(
comment_beside(t["first"].as_value().unwrap().decor()),
Some("beside first".to_owned())
);
assert_eq!(
comments_before(t.key("second").unwrap().leaf_decor()),
["above second, indented"]
);
assert_eq!(
comment_beside(t["second"].as_value().unwrap().decor()),
None
);
assert_eq!(trailing_comments(&d), ["at the end"]);
}
#[test]
fn a_comment_above_is_replaced_and_the_whitespace_stays() {
let mut d = doc();
let t = d.as_table_mut();
set_comments_before(
t.key_mut("first").unwrap().leaf_decor_mut(),
&["changed".to_owned(), "and a second line".to_owned()],
);
set_comments_before(t.key_mut("second").unwrap().leaf_decor_mut(), &[]);
set_comments_before(
t["table"]
.as_table_mut()
.unwrap()
.key_mut("third")
.unwrap()
.leaf_decor_mut(),
&[String::new()],
);
assert_eq!(
d.to_string(),
"\
# changed
# and a second line
first = 1 # beside first
second = 2
[table]
#
third = 3
# at the end
"
);
assert_eq!(
comments_before(d.as_table().key("first").unwrap().leaf_decor()),
["changed", "and a second line"]
);
}
#[test]
fn a_comment_added_above_takes_the_key_s_indentation() {
let mut d: DocumentMut = "a = 1\n\n b = 2\n".parse().unwrap();
set_comments_before(
d.as_table_mut().key_mut("b").unwrap().leaf_decor_mut(),
&["new".to_owned()],
);
assert_eq!(d.to_string(), "a = 1\n\n # new\n b = 2\n");
}
#[test]
fn a_comment_beside_is_replaced_added_and_removed() {
let mut d = doc();
let t = d.as_table_mut();
set_comment_beside(
t["first"].as_value_mut().unwrap().decor_mut(),
Some("changed"),
);
set_comment_beside(t["second"].as_value_mut().unwrap().decor_mut(), Some("new"));
let written = d.to_string();
assert!(written.contains("first = 1 # changed\n"), "{written}");
assert!(written.contains("second = 2 # new\n"), "{written}");
set_comment_beside(
d.as_table_mut()["first"]
.as_value_mut()
.unwrap()
.decor_mut(),
None,
);
assert!(d.to_string().contains("first = 1\n"), "{d}");
}
#[test]
fn the_trailing_comment_is_replaced_and_removed() {
let mut d = doc();
set_trailing_comments(&mut d, &["the end, changed".to_owned()]);
assert!(
d.to_string().ends_with("third = 3\n\n# the end, changed\n"),
"{d}"
);
set_trailing_comments(&mut d, &[]);
assert!(d.to_string().ends_with("third = 3\n\n"), "{d}");
}
#[test]
fn a_comment_above_a_header_is_the_table_s() {
let mut d: DocumentMut = "a = 1\n\n# about t\n[t]\nb = 2\n".parse().unwrap();
let t = d.as_table_mut()["t"].as_table_mut().unwrap();
assert_eq!(comments_before(t.decor()), ["about t"]);
set_comments_before(t.decor_mut(), &["about t, changed".to_owned()]);
assert_eq!(d.to_string(), "a = 1\n\n# about t, changed\n[t]\nb = 2\n");
}
}