Skip to main content

ephemeris_documents/yaml/source/
mapping.rs

1//! Block mappings and their entries.
2
3use std::fmt;
4
5use super::Value;
6use super::text::{Origin, Reindent, Text};
7use crate::yaml::CANONICAL_STEP;
8use crate::yaml::scalar_text::ScalarText;
9
10/// A mapping whose entries can be listed, replaced, appended, and removed.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub(crate) struct Mapping {
13    shape: Shape,
14    entries: Vec<Entry>,
15    indent: usize,
16    step: usize,
17}
18
19/// How a mapping is spelled in the source.
20#[derive(Debug, Clone, PartialEq, Eq)]
21enum Shape {
22    /// Entries on their own lines. The text is what follows the owning key
23    /// on its line, including the line break.
24    Block(Text),
25    /// An empty `{}`, possibly with whitespace between the braces. The
26    /// texts surround the braces, and the last one runs to the end of the
27    /// line.
28    EmptyFlow {
29        before: Text,
30        braces: Text,
31        after: Text,
32    },
33}
34
35impl Mapping {
36    /// A block mapping whose keys sit at `indent`, with nested mappings
37    /// `step` columns deeper.
38    pub(super) const fn block(
39        opening: Text,
40        entries: Vec<Entry>,
41        indent: usize,
42        step: usize,
43    ) -> Self {
44        Self {
45            shape: Shape::Block(opening),
46            entries,
47            indent,
48            step,
49        }
50    }
51
52    /// An empty flow mapping whose block entries will sit at `indent`.
53    pub(super) const fn empty_flow(
54        before: Text,
55        braces: Text,
56        after: Text,
57        indent: usize,
58        step: usize,
59    ) -> Self {
60        Self {
61            shape: Shape::EmptyFlow {
62                before,
63                braces,
64                after,
65            },
66            entries: Vec::new(),
67            indent,
68            step,
69        }
70    }
71
72    /// This mapping opened by `opening`, with its keys moved to `indent` and
73    /// nested mappings `step` columns deeper.
74    pub(super) fn reopened(mut self, opening: Text, indent: usize, step: usize) -> Self {
75        self.reindent_to(indent, step);
76        self.shape = Shape::Block(opening);
77        self
78    }
79
80    /// Split off the leading text of the first entry, except for the comment
81    /// block directly above its key.
82    pub(super) fn take_header(&mut self) -> Text {
83        self.entries
84            .first_mut()
85            .map(|entry| entry.leading.take_before_comment_block())
86            .unwrap_or_default()
87    }
88
89    pub(crate) fn keys(&self) -> impl Iterator<Item = &str> {
90        self.entries.iter().map(Entry::key)
91    }
92
93    pub(crate) fn get(&self, key: &str) -> Option<&Entry> {
94        self.entries.iter().find(|entry| entry.key == key)
95    }
96
97    pub(crate) fn get_mut(&mut self, key: &str) -> Option<&mut Entry> {
98        self.entries.iter_mut().find(|entry| entry.key == key)
99    }
100
101    /// Put `entry` in the mapping, laid out at this mapping's indent and
102    /// step.
103    ///
104    /// An entry with the same key is replaced in place. It keeps the
105    /// existing key text, trailing comment of a plain value, and leading
106    /// text unless the new entry brings a comment. Any other entry is
107    /// appended.
108    pub(crate) fn insert(&mut self, mut entry: Entry) {
109        entry.reindent_to(self.indent, self.step);
110        entry.value.terminate();
111        self.expand();
112        if let Some(existing) = self.get_mut(&entry.key) {
113            existing.replace_keeping_decor(entry);
114            return;
115        }
116        self.terminate();
117        self.entries.push(entry);
118    }
119
120    /// Remove the entry with `key`, together with its leading text.
121    /// A mapping left without entries becomes `{}`.
122    pub(crate) fn remove(&mut self, key: &str) {
123        let Some(index) = self.entries.iter().position(|entry| entry.key == key) else {
124            return;
125        };
126        self.entries.remove(index);
127        if self.entries.is_empty() {
128            self.collapse();
129        }
130    }
131
132    /// The comment on the key line, with the whitespace before it.
133    pub(super) fn comment(&self) -> Option<&str> {
134        Text::comment_in(self.key_line().as_str())
135    }
136
137    /// Put `comment` on the key line unless it has one.
138    pub(super) fn keep_comment(&mut self, comment: &str) {
139        if self.comment().is_some() {
140            return;
141        }
142        let text = self.key_line_mut();
143        if !text.is_empty() {
144            *text = Text::from(format!("{}{comment}\n", text.as_str().trim_end()));
145        }
146    }
147
148    /// What follows the owning key on its line.
149    const fn key_line(&self) -> &Text {
150        let (Shape::Block(text)
151        | Shape::EmptyFlow {
152            before: _,
153            braces: _,
154            after: text,
155        }) = &self.shape;
156        text
157    }
158
159    const fn key_line_mut(&mut self) -> &mut Text {
160        let (Shape::Block(text)
161        | Shape::EmptyFlow {
162            before: _,
163            braces: _,
164            after: text,
165        }) = &mut self.shape;
166        text
167    }
168
169    /// Turn an emptied block mapping into `{}`, as an empty value would read
170    /// back as null. Anchors and a trailing comment stay.
171    pub(super) fn collapse(&mut self) {
172        let opening = match &self.shape {
173            Shape::Block(opening) => opening,
174            Shape::EmptyFlow {
175                before: _,
176                braces: _,
177                after: _,
178            } => return,
179        };
180        if opening.is_empty() {
181            self.shape = Shape::EmptyFlow {
182                before: Text::default(),
183                braces: Text::from("{}"),
184                after: Text::from("\n"),
185            };
186            return;
187        }
188        let line = opening.as_str().trim_end_matches('\n');
189        let comment = Text::comment_start(line).unwrap_or(line.len());
190        let (properties, note) = line.split_at_checked(comment).unwrap_or((line, ""));
191        let note = note.trim_start();
192        let before = Text::from(format!("{} ", properties.trim_end()));
193        let after = Text::from(if note.is_empty() {
194            "\n".to_owned()
195        } else {
196            format!(" {note}\n")
197        });
198        self.shape = Shape::EmptyFlow {
199            before,
200            braces: Text::from("{}"),
201            after,
202        };
203    }
204
205    /// Move the keys to `indent` and nested mappings to `step` columns per
206    /// level, carrying every entry's lines along.
207    fn reindent_to(&mut self, indent: usize, step: usize) {
208        self.indent = indent;
209        self.step = step;
210        for entry in &mut self.entries {
211            entry.reindent_to(indent, step);
212        }
213    }
214
215    /// Make the last line of the mapping end with a line break.
216    pub(super) fn terminate(&mut self) {
217        match (self.entries.last_mut(), &mut self.shape) {
218            (
219                _,
220                Shape::EmptyFlow {
221                    before: _,
222                    braces: _,
223                    after,
224                },
225            ) => after.terminate(),
226            (Some(last), Shape::Block(_)) => last.value.terminate(),
227            (None, Shape::Block(opening)) if !opening.is_empty() => opening.terminate(),
228            (None, Shape::Block(_)) => {}
229        }
230    }
231
232    /// Turn an empty flow mapping into a block mapping, keeping what
233    /// surrounds it.
234    ///
235    /// The line holding the braces stays when something else is on it, such
236    /// as a key or a `---` marker. A bare `{}` line disappears. A comment
237    /// after the closing brace moves up to the key's line.
238    fn expand(&mut self) {
239        let Shape::EmptyFlow {
240            before,
241            braces: _,
242            after,
243        } = &self.shape
244        else {
245            return;
246        };
247        let before = before.as_str();
248        let kept = before.trim_end();
249        let owned = self.indent > 0 || before.starts_with([' ', '\t']) || !kept.is_empty();
250        let mut opening = Text::from(format!("{kept}{after}"));
251        if owned {
252            opening.terminate();
253        } else if after.as_str().trim().is_empty() {
254            opening = Text::default();
255        }
256        self.shape = Shape::Block(opening);
257    }
258}
259
260impl fmt::Display for Mapping {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        match &self.shape {
263            Shape::Block(opening) => write!(f, "{opening}")?,
264            Shape::EmptyFlow {
265                before,
266                braces,
267                after,
268            } => write!(f, "{before}{braces}{after}")?,
269        }
270        self.entries
271            .iter()
272            .try_for_each(|entry| write!(f, "{entry}"))
273    }
274}
275
276/// One `key: value` pair with the text before it.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub(crate) struct Entry {
279    leading: Text,
280    key: String,
281    head: Text,
282    value: Value,
283}
284
285impl Entry {
286    pub(super) const fn new(leading: Text, key: String, head: Text, value: Value) -> Self {
287        Self {
288            leading,
289            key,
290            head,
291            value,
292        }
293    }
294
295    /// The root of a document, which has no key.
296    pub(super) fn root(value: Value) -> Self {
297        Self::new(Text::default(), String::new(), Text::default(), value)
298    }
299
300    /// An empty mapping that prints as `{}` until entries are inserted.
301    pub(crate) fn empty_mapping() -> Self {
302        let mapping = Mapping::empty_flow(
303            Text::from(" "),
304            Text::from("{}"),
305            Text::from("\n"),
306            0,
307            CANONICAL_STEP,
308        );
309        Self::root(Value::Mapping(mapping))
310    }
311
312    /// An empty mapping with this entry's leading text and key.
313    #[must_use]
314    pub(crate) fn empty_with_decor(&self) -> Self {
315        let mut empty = Self::empty_mapping();
316        empty.leading.clone_from(&self.leading);
317        empty.head.clone_from(&self.head);
318        empty.key.clone_from(&self.key);
319        empty
320    }
321
322    /// This entry under another key, spelled as the canonical renderer
323    /// spells keys.
324    #[must_use]
325    pub(crate) fn named(mut self, key: &str) -> Self {
326        let indent = " ".repeat(self.indent());
327        self.head = Text::from(format!("{indent}{}:", ScalarText::key(key)));
328        key.clone_into(&mut self.key);
329        self
330    }
331
332    /// Take the value of `incoming`, and its leading text when that holds
333    /// a comment. The key text and the comment on the key line stay.
334    fn replace_keeping_decor(&mut self, incoming: Self) {
335        if !incoming.leading.as_str().trim().is_empty() {
336            self.leading = incoming.leading;
337        }
338        let comment = self.value.comment().map(str::to_owned);
339        self.value = incoming.value;
340        if let Some(comment) = comment {
341            self.value.keep_comment(&comment);
342        }
343        self.value.terminate();
344    }
345
346    pub(crate) fn key(&self) -> &str {
347        &self.key
348    }
349
350    pub(crate) const fn value(&self) -> &Value {
351        &self.value
352    }
353
354    pub(crate) const fn value_mut(&mut self) -> &mut Value {
355        &mut self.value
356    }
357
358    pub(super) fn into_value(self) -> Value {
359        self.value
360    }
361
362    /// The column of the key.
363    pub(super) fn indent(&self) -> usize {
364        self.head
365            .as_str()
366            .bytes()
367            .take_while(|byte| *byte == b' ')
368            .count()
369    }
370
371    /// Move the key to `indent` and nested mappings `step` columns per
372    /// level. Text lines of the value, such as block scalar content, move
373    /// with the key.
374    fn reindent_to(&mut self, indent: usize, step: usize) {
375        let shift = Reindent::new(self.indent(), indent);
376        self.leading.reindent(shift, Origin::LineStart);
377        self.head.reindent(shift, Origin::LineStart);
378        match &mut self.value {
379            Value::Leaf(leaf) => leaf.reindent(shift),
380            Value::Opaque(opaque) => opaque.reindent(shift),
381            Value::Mapping(mapping) => mapping.reindent_to(indent.saturating_add(step), step),
382        }
383    }
384}
385
386impl fmt::Display for Entry {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(f, "{}{}{}", self.leading, self.head, self.value)
389    }
390}