Skip to main content

ephemeris_documents/toml/
node.rs

1//! `Item` as a node of the generic merge, and the decor rules for it.
2
3use std::mem;
4
5use toml_edit::{InlineTable, Item, Table, TableLike, Value as TomlValue};
6
7use super::Toml;
8use super::decor::{prefix_str, with_first_decor_mut};
9use super::layout::Siblings;
10use super::order::TableOrder;
11use crate::merge::Node;
12
13impl Toml {
14    /// The comment above the value key `key` of `parent`, if it has one.
15    fn key_comment(parent: &Item, key: &str) -> Option<String> {
16        let (key, _) = parent.as_table_like()?.get_key_value(key)?;
17        prefix_str(key.leaf_decor())
18            .filter(|prefix| !prefix.is_empty())
19            .map(str::to_owned)
20    }
21
22    fn set_key_comment(table: &mut dyn TableLike, key: &str, comment: Option<String>) {
23        if let (Some(comment), Some(mut key)) = (comment, table.key_mut(key)) {
24            key.leaf_decor_mut().set_prefix(comment);
25        }
26    }
27
28    /// Drop the blank lines before the first entry of `table`, which only
29    /// separated it from an entry that is gone.
30    fn trim_leading_blank_lines(table: &mut Table) {
31        with_first_decor_mut(table, |decor| {
32            let Some(prefix) = prefix_str(decor) else {
33                return;
34            };
35            let trimmed = prefix.trim_start_matches(['\n', '\r']);
36            if trimmed.len() != prefix.len() {
37                let trimmed = trimmed.to_owned();
38                decor.set_prefix(trimmed);
39            }
40        });
41    }
42
43    /// `new`, with the decor of the item it replaces so that the user's
44    /// spacing and comments stay. A table keeps the comment above its header
45    /// unless the new table brings one, and then keeps the blank lines above
46    /// the old one.
47    fn replace_keeping_decor(old: &Item, mut new: Item) -> Item {
48        match (old, &mut new) {
49            (Item::Value(old), Item::Value(new)) => *new.decor_mut() = old.decor().clone(),
50            (Item::Table(old), Item::Table(new)) => {
51                let comment = prefix_str(new.decor()).unwrap_or_default().to_owned();
52                if comment.is_empty() {
53                    *new.decor_mut() = old.decor().clone();
54                } else if !comment.starts_with(['\n', '\r']) {
55                    let blank: String = prefix_str(old.decor())
56                        .unwrap_or_default()
57                        .to_owned()
58                        .chars()
59                        .take_while(|c| matches!(c, '\n' | '\r'))
60                        .collect();
61                    new.decor_mut()
62                        .set_prefix([blank.as_str(), comment.as_str()].concat());
63                }
64                new.set_position(old.position());
65            }
66            _ => {}
67        }
68        new
69    }
70}
71
72impl Node for Item {
73    fn is_container(&self) -> bool {
74        self.as_table_like().is_some()
75    }
76
77    fn keys(&self) -> impl Iterator<Item = &str> + '_ {
78        self.as_table_like()
79            .into_iter()
80            .flat_map(|table| table.iter().map(|(key, _)| key))
81    }
82
83    fn child(&self, key: &str) -> Option<&Self> {
84        self.as_table_like()?.get(key)
85    }
86
87    fn child_mut(&mut self, key: &str) -> Option<&mut Self> {
88        self.as_table_like_mut()?.get_mut(key)
89    }
90
91    fn adopt(&mut self, key: &str, source: &Self) {
92        let Some(desired) = source.child(key) else {
93            return;
94        };
95        let comment = Toml::key_comment(source, key);
96        let mut node = desired.clone();
97        TableOrder::forget(&mut node);
98        let mut header_comment = None;
99        if self.child(key).is_none() {
100            Siblings::of(self).append(key, node);
101        } else if let Some(slot) = self
102            .as_table_like_mut()
103            .and_then(|table| table.get_mut(key))
104        {
105            let replaced = mem::take(slot);
106            if let (Self::Table(old), Self::Value(_)) = (&replaced, &node) {
107                header_comment = Some(prefix_str(old.decor()).unwrap_or_default().to_owned());
108            }
109            *slot = Toml::replace_keeping_decor(&replaced, node);
110        }
111        let Some(mut target) = self
112            .as_table_like_mut()
113            .and_then(|table| table.key_mut(key))
114        else {
115            return;
116        };
117        if let Some(text) = header_comment {
118            let decor = target.leaf_decor_mut();
119            decor.set_suffix(" ");
120            if !text.is_empty() {
121                decor.set_prefix(text);
122            }
123        }
124        if let Some(text) = comment {
125            target.leaf_decor_mut().set_prefix(text);
126        }
127    }
128
129    fn adopt_shell(&mut self, key: &str, source: &Self) {
130        let Some(table) = self.as_table_like_mut() else {
131            return;
132        };
133        match source.child(key) {
134            Some(Self::Table(own)) if !own.is_dotted() => {
135                let mut shell = Table::new();
136                *shell.decor_mut() = own.decor().clone();
137                let comment = prefix_str(shell.decor()).unwrap_or_default().to_owned();
138                if !table.is_empty() && !comment.is_empty() && !comment.starts_with(['\n', '\r']) {
139                    shell.decor_mut().set_prefix(format!("\n{comment}"));
140                }
141                table.insert(key, Self::Table(shell));
142            }
143            Some(Self::Table(_)) => {
144                let mut shell = Table::new();
145                shell.set_dotted(true);
146                table.insert(key, Self::Table(shell));
147            }
148            Some(Self::Value(_)) => {
149                let shell = Self::Value(TomlValue::InlineTable(InlineTable::new()));
150                table.insert(key, shell);
151                Toml::set_key_comment(table, key, Toml::key_comment(source, key));
152            }
153            Some(Self::ArrayOfTables(_) | Self::None) | None => {}
154        }
155    }
156
157    fn remove(&mut self, key: &str) {
158        if let Self::Table(table) = self {
159            let was_first = table.iter().next().is_some_and(|(first, _)| first == key);
160            table.remove(key);
161            if was_first {
162                Toml::trim_leading_blank_lines(table);
163            }
164        } else if let Some(table) = self.as_table_like_mut() {
165            table.remove(key);
166        }
167    }
168}