Skip to main content

ephemeris_documents/toml/
render.rs

1//! Canonical TOML rendering of an annotated tree.
2
3use std::fmt::{self, Write as _};
4
5use toml_edit::{ArrayOfTables, Item, Table, Value as TomlValue};
6use toml_writer::{ToTomlValue as _, TomlStringBuilder};
7
8use super::Toml;
9use super::decor::prefix_str;
10use super::tree::TomlTree;
11use crate::annotation::{Annotated, AnnotatedValue, StringStyle};
12use crate::entries::Entries;
13use crate::error::{DocumentError, Feature, InternalError, TomlError};
14use crate::path::Segment;
15use crate::scalar::Scalar;
16use crate::text::{Comment, CommentSyntax};
17
18/// Counts the characters a [`fmt::Display`] would produce, without
19/// allocating them.
20struct Width(usize);
21
22impl fmt::Write for Width {
23    fn write_str(&mut self, s: &str) -> fmt::Result {
24        self.0 = self.0.saturating_add(s.chars().count());
25        Ok(())
26    }
27}
28
29impl Toml {
30    pub(super) fn build_document(self, annotated: &Annotated) -> Result<TomlTree, DocumentError> {
31        let AnnotatedValue::Object(entries) = &annotated.value else {
32            return Err(TomlError::RootNotTable.into());
33        };
34        let mut tree = TomlTree::new();
35        self.populate_table(tree.table_mut(), entries, true)?;
36        if let Some(comment) = &annotated.comment {
37            let mut header = String::new();
38            Self::write_comment(&mut header, comment, "");
39            header.push('\n');
40            tree.set_header(header);
41        }
42        Ok(tree)
43    }
44
45    fn populate_table(
46        self,
47        table: &mut Table,
48        entries: &Entries,
49        is_root: bool,
50    ) -> Result<(), DocumentError> {
51        for (key, annotated) in entries {
52            let item = self
53                .item(key, annotated, is_root)
54                .map_err(|error| error.under(Segment::Key(key.clone())))?;
55            table.insert(key, item);
56            if let Some(comment) = &annotated.comment {
57                Self::attach_comment(table, key, comment);
58            }
59        }
60        let only_subtables = table.iter().all(|(_, item)| Self::has_header(item));
61        table.set_implicit(only_subtables && !table.is_empty());
62        Ok(())
63    }
64
65    /// Whether `item` is printed under a header of its own.
66    fn has_header(item: &Item) -> bool {
67        match item {
68            Item::Table(table) => !table.is_dotted(),
69            Item::ArrayOfTables(_) => true,
70            Item::Value(_) | Item::None => false,
71        }
72    }
73
74    fn item(self, key: &str, annotated: &Annotated, is_root: bool) -> Result<Item, DocumentError> {
75        match &annotated.value {
76            AnnotatedValue::Array(items) => self.array_item(items),
77            AnnotatedValue::Object(entries) => {
78                if !is_root && !entries.is_dotted() && !entries.carries_comment() {
79                    let inline = self.inline_table(entries)?;
80                    if self.fits_inline(key, &inline) {
81                        return Ok(Item::Value(TomlValue::InlineTable(inline)));
82                    }
83                }
84                let mut table = Table::new();
85                self.populate_table(&mut table, entries, false)?;
86                table.set_dotted(entries.is_dotted());
87                Ok(Item::Table(table))
88            }
89            AnnotatedValue::Scalar(value) => Ok(Item::Value(self.scalar(value)?)),
90        }
91    }
92
93    /// Whether `key = inline` fits within the configured columns.
94    fn fits_inline(self, key: &str, inline: &toml_edit::InlineTable) -> bool {
95        let mut width = Width(0);
96        // Width::write_str never fails.
97        let _infallible = write!(width, "{} = {inline}", toml_edit::Key::new(key));
98        width.0 <= self.inline_width.get()
99    }
100
101    fn array_item(self, items: &[Annotated]) -> Result<Item, DocumentError> {
102        let tables: Option<Vec<&Entries>> = items
103            .iter()
104            .map(|item| match &item.value {
105                AnnotatedValue::Object(entries) => Some(entries),
106                AnnotatedValue::Scalar(_) | AnnotatedValue::Array(_) => None,
107            })
108            .collect();
109        let Some(tables) = tables.filter(|tables| !tables.is_empty()) else {
110            return Ok(Item::Value(TomlValue::Array(self.array(items)?)));
111        };
112        let mut array = ArrayOfTables::new();
113        for (index, (entries, item)) in tables.into_iter().zip(items).enumerate() {
114            let mut table = Table::new();
115            self.populate_table(&mut table, entries, false)
116                .map_err(|error| error.under(Segment::Index(index)))?;
117            if let Some(comment) = &item.comment {
118                let mut prefix = String::new();
119                Self::write_comment(&mut prefix, comment, "");
120                table.decor_mut().set_prefix(prefix);
121            }
122            array.push(table);
123        }
124        Ok(Item::ArrayOfTables(array))
125    }
126
127    fn array(self, items: &[Annotated]) -> Result<toml_edit::Array, DocumentError> {
128        let mut array = toml_edit::Array::new();
129        for (index, item) in items.iter().enumerate() {
130            let value = self
131                .array_value(item)
132                .map_err(|error| error.under(Segment::Index(index)))?;
133            array.push(value);
134        }
135        if items.iter().any(|item| item.comment.is_some()) {
136            Self::spread(&mut array, items);
137        }
138        Ok(array)
139    }
140
141    /// Put every item on its own line, with its comment above it.
142    fn spread(array: &mut toml_edit::Array, items: &[Annotated]) {
143        for (value, item) in array.iter_mut().zip(items) {
144            let mut prefix = String::from("\n");
145            if let Some(comment) = &item.comment {
146                Self::write_comment(&mut prefix, comment, "  ");
147            }
148            prefix.push_str("  ");
149            value.decor_mut().set_prefix(prefix);
150        }
151        array.set_trailing("\n");
152        array.set_trailing_comma(true);
153    }
154
155    /// A value inside an inline table, which cannot hold comments.
156    fn inline_value(self, annotated: &Annotated) -> Result<TomlValue, DocumentError> {
157        if annotated.carries_comment() {
158            return Err(self.unsupported(Feature::InlineComment));
159        }
160        self.array_value(annotated)
161    }
162
163    fn array_value(self, annotated: &Annotated) -> Result<TomlValue, DocumentError> {
164        match &annotated.value {
165            AnnotatedValue::Scalar(value) => self.scalar(value),
166            AnnotatedValue::Array(items) => Ok(TomlValue::Array(self.array(items)?)),
167            AnnotatedValue::Object(entries) => {
168                self.inline_table(entries).map(TomlValue::InlineTable)
169            }
170        }
171    }
172
173    fn scalar(self, scalar: &Scalar) -> Result<TomlValue, DocumentError> {
174        match scalar {
175            Scalar::Null => Err(Self::unvalidated(Feature::Null)),
176            Scalar::Bool(value) => Ok(TomlValue::from(*value)),
177            Scalar::Integer(value) => Ok(TomlValue::from(*value)),
178            Scalar::Unsigned(_) => Err(Self::unvalidated(Feature::LargeInteger)),
179            Scalar::Float(value) => Ok(TomlValue::from(value.get())),
180            Scalar::String { text, style } => self.styled_string(text, *style),
181        }
182    }
183
184    const fn unvalidated(feature: Feature) -> DocumentError {
185        DocumentError::Internal(InternalError::FeatureNotValidated { feature })
186    }
187
188    fn styled_string(self, text: &str, style: StringStyle) -> Result<TomlValue, DocumentError> {
189        match style {
190            StringStyle::Plain => Ok(TomlValue::from(text)),
191            StringStyle::Literal => self.literal_string(text),
192        }
193    }
194
195    fn literal_string(self, text: &str) -> Result<TomlValue, DocumentError> {
196        let builder = TomlStringBuilder::new(text);
197        let Some(rendered) = builder.as_literal().or_else(|| builder.as_ml_literal()) else {
198            return Err(self.unsupported(Feature::LiteralString));
199        };
200        rendered
201            .to_toml_value()
202            .parse::<TomlValue>()
203            .map_err(|source| DocumentError::from(InternalError::toml_rendered(source)))
204    }
205
206    fn inline_table(self, entries: &Entries) -> Result<toml_edit::InlineTable, DocumentError> {
207        let mut table = toml_edit::InlineTable::new();
208        for (key, value) in entries {
209            let value = self
210                .inline_value(value)
211                .map_err(|error| error.under(Segment::Key(key.clone())))?;
212            table.insert(key, value);
213        }
214        Ok(table)
215    }
216
217    fn write_comment(out: &mut String, comment: &Comment, indent: &str) {
218        // Writing to a String cannot fail.
219        let _infallible = CommentSyntax::Hash.write_lines(out, comment, indent);
220    }
221
222    /// Put `prefix` before the first line of a dotted table, which prints the
223    /// decor of its leaf keys.
224    fn comment_first_leaf(table: &mut Table, prefix: String) {
225        let Some((mut key, item)) = table.iter_mut().next() else {
226            return;
227        };
228        match item {
229            Item::Table(child) if child.is_dotted() => Self::comment_first_leaf(child, prefix),
230            Item::Table(_) | Item::ArrayOfTables(_) | Item::Value(_) | Item::None => {
231                key.leaf_decor_mut().set_prefix(prefix);
232            }
233        }
234    }
235
236    fn attach_comment(table: &mut Table, key: &str, comment: &Comment) {
237        let mut prefix = String::new();
238        Self::write_comment(&mut prefix, comment, "");
239        match table.get_mut(key) {
240            Some(Item::Value(_)) => {
241                if let Some(mut key_mut) = table.key_mut(key) {
242                    key_mut.leaf_decor_mut().set_prefix(prefix);
243                }
244            }
245            Some(Item::Table(child)) if child.is_dotted() => {
246                Self::comment_first_leaf(child, prefix);
247            }
248            Some(Item::Table(child)) => {
249                child.set_implicit(false);
250                child.decor_mut().set_prefix(prefix);
251            }
252            Some(Item::ArrayOfTables(children)) => {
253                if let Some(first) = children.iter_mut().next() {
254                    let element = prefix_str(first.decor()).unwrap_or_default();
255                    let prefix = [prefix.as_str(), element].concat();
256                    first.decor_mut().set_prefix(prefix);
257                }
258            }
259            Some(Item::None) | None => {}
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests;