1use toml_edit::{Decor, DocumentMut, RawString};
18
19#[must_use]
22pub fn comments_before(decor: &Decor) -> Vec<String> {
23 lines_of(decor.prefix())
24}
25
26#[must_use]
28pub fn comment_beside(decor: &Decor) -> Option<String> {
29 lines_of(decor.suffix()).into_iter().next()
30}
31
32#[must_use]
34pub fn trailing_comments(doc: &DocumentMut) -> Vec<String> {
35 lines_of(Some(doc.trailing()))
36}
37
38pub fn set_comments_before(decor: &mut Decor, lines: &[String]) {
42 let raw = text_of(decor.prefix());
43 decor.set_prefix(rewritten(raw, lines));
44}
45
46pub fn set_comment_beside(decor: &mut Decor, comment: Option<&str>) {
49 let raw = text_of(decor.suffix());
50 let suffix = match comment {
51 None => String::new(),
52 Some(text) => {
53 let before = raw.split('#').next().unwrap_or("");
54 let before = if before.trim().is_empty() && !before.is_empty() {
57 before
58 } else {
59 " "
60 };
61 format!("{before}{}", hashed(text))
62 }
63 };
64 decor.set_suffix(suffix);
65}
66
67pub fn set_trailing_comments(doc: &mut DocumentMut, lines: &[String]) {
70 let raw = text_of(Some(doc.trailing()));
71 doc.set_trailing(rewritten(raw, lines));
72}
73
74fn text_of(raw: Option<&RawString>) -> &str {
75 raw.and_then(RawString::as_str).unwrap_or("")
76}
77
78fn lines_of(raw: Option<&RawString>) -> Vec<String> {
79 text_of(raw)
80 .lines()
81 .filter_map(|l| l.trim().strip_prefix('#').map(|c| c.trim().to_owned()))
82 .collect()
83}
84
85fn hashed(text: &str) -> String {
88 if text.is_empty() {
89 "#".to_owned()
90 } else {
91 format!("# {text}")
92 }
93}
94
95fn rewritten(raw: &str, lines: &[String]) -> String {
105 let (body, indent) = match raw.rfind('\n') {
106 Some(i) => (&raw[..=i], &raw[i + 1..]),
107 None => ("", raw),
108 };
109 let body: Vec<&str> = body.split_inclusive('\n').collect();
110 let first_comment = body.iter().position(|l| !l.trim().is_empty());
111 let last_comment = body.iter().rposition(|l| !l.trim().is_empty());
112 let (before, after) = match (first_comment, last_comment) {
113 (Some(first), Some(last)) => (&body[..first], &body[last + 1..]),
114 _ => (&body[..], &body[..0]),
115 };
116 let mut out: String = before.concat();
117 for line in lines {
118 out.push_str(indent);
119 out.push_str(&hashed(line));
120 out.push('\n');
121 }
122 out.push_str(&after.concat());
123 out.push_str(indent);
124 out
125}
126
127#[cfg(test)]
128mod tests {
129 use super::{
130 comment_beside, comments_before, set_comment_beside, set_comments_before,
131 set_trailing_comments, trailing_comments,
132 };
133 use toml_edit::DocumentMut;
134
135 const DOC: &str = "\
136# above first
137
138first = 1 # beside first
139
140 # above second, indented
141 second = 2
142
143
144[table]
145# in the table
146third = 3
147
148# at the end
149";
150
151 fn doc() -> DocumentMut {
152 DOC.parse().expect("valid TOML")
153 }
154
155 #[test]
158 fn every_slot_reads_its_comment() {
159 let d = doc();
160 let t = d.as_table();
161 assert_eq!(
162 comments_before(t.key("first").unwrap().leaf_decor()),
163 ["above first"]
164 );
165 assert_eq!(
166 comment_beside(t["first"].as_value().unwrap().decor()),
167 Some("beside first".to_owned())
168 );
169 assert_eq!(
170 comments_before(t.key("second").unwrap().leaf_decor()),
171 ["above second, indented"]
172 );
173 assert_eq!(
174 comment_beside(t["second"].as_value().unwrap().decor()),
175 None
176 );
177 assert_eq!(trailing_comments(&d), ["at the end"]);
178 }
179
180 #[test]
185 fn a_comment_above_is_replaced_and_the_whitespace_stays() {
186 let mut d = doc();
187 let t = d.as_table_mut();
188 set_comments_before(
189 t.key_mut("first").unwrap().leaf_decor_mut(),
190 &["changed".to_owned(), "and a second line".to_owned()],
191 );
192 set_comments_before(t.key_mut("second").unwrap().leaf_decor_mut(), &[]);
193 set_comments_before(
194 t["table"]
195 .as_table_mut()
196 .unwrap()
197 .key_mut("third")
198 .unwrap()
199 .leaf_decor_mut(),
200 &[String::new()],
201 );
202 assert_eq!(
203 d.to_string(),
204 "\
205# changed
206# and a second line
207
208first = 1 # beside first
209
210 second = 2
211
212
213[table]
214#
215third = 3
216
217# at the end
218"
219 );
220 assert_eq!(
221 comments_before(d.as_table().key("first").unwrap().leaf_decor()),
222 ["changed", "and a second line"]
223 );
224 }
225
226 #[test]
230 fn a_comment_added_above_takes_the_key_s_indentation() {
231 let mut d: DocumentMut = "a = 1\n\n b = 2\n".parse().unwrap();
232 set_comments_before(
233 d.as_table_mut().key_mut("b").unwrap().leaf_decor_mut(),
234 &["new".to_owned()],
235 );
236 assert_eq!(d.to_string(), "a = 1\n\n # new\n b = 2\n");
237 }
238
239 #[test]
242 fn a_comment_beside_is_replaced_added_and_removed() {
243 let mut d = doc();
244 let t = d.as_table_mut();
245 set_comment_beside(
246 t["first"].as_value_mut().unwrap().decor_mut(),
247 Some("changed"),
248 );
249 set_comment_beside(t["second"].as_value_mut().unwrap().decor_mut(), Some("new"));
250 let written = d.to_string();
251 assert!(written.contains("first = 1 # changed\n"), "{written}");
252 assert!(written.contains("second = 2 # new\n"), "{written}");
253
254 set_comment_beside(
255 d.as_table_mut()["first"]
256 .as_value_mut()
257 .unwrap()
258 .decor_mut(),
259 None,
260 );
261 assert!(d.to_string().contains("first = 1\n"), "{d}");
262 }
263
264 #[test]
267 fn the_trailing_comment_is_replaced_and_removed() {
268 let mut d = doc();
269 set_trailing_comments(&mut d, &["the end, changed".to_owned()]);
270 assert!(
271 d.to_string().ends_with("third = 3\n\n# the end, changed\n"),
272 "{d}"
273 );
274 set_trailing_comments(&mut d, &[]);
275 assert!(d.to_string().ends_with("third = 3\n\n"), "{d}");
276 }
277
278 #[test]
281 fn a_comment_above_a_header_is_the_table_s() {
282 let mut d: DocumentMut = "a = 1\n\n# about t\n[t]\nb = 2\n".parse().unwrap();
283 let t = d.as_table_mut()["t"].as_table_mut().unwrap();
284 assert_eq!(comments_before(t.decor()), ["about t"]);
285 set_comments_before(t.decor_mut(), &["about t, changed".to_owned()]);
286 assert_eq!(d.to_string(), "a = 1\n\n# about t, changed\n[t]\nb = 2\n");
287 }
288}