Skip to main content

ephemeris_documents/
yaml.rs

1//! YAML support: canonical rendering of a whole tree, the source tree that
2//! edits existing YAML, and the backend.
3
4mod backend;
5mod literal;
6mod node;
7mod scalar_text;
8mod source;
9mod string;
10
11use std::fmt::{self, Write as _};
12
13pub(crate) use self::backend::Yaml;
14pub(crate) use self::literal::LiteralBlock;
15use self::scalar_text::ScalarText;
16use self::string::YamlString;
17use crate::annotation::{Annotated, AnnotatedValue};
18use crate::entries::Entries;
19use crate::scalar::Scalar;
20use crate::text::CommentSyntax;
21
22/// Columns per nesting level in canonical output, and the step assumed for
23/// source with nothing to measure.
24const CANONICAL_STEP: usize = 2;
25
26/// A validated tree the canonical renderer writes.
27pub(crate) struct YamlDocument<'a>(&'a Annotated);
28
29impl<'a> YamlDocument<'a> {
30    pub(crate) const fn new(root: &'a Annotated) -> Self {
31        Self(root)
32    }
33}
34
35impl fmt::Display for YamlDocument<'_> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        let mut writer = Writer { out: f, indent: 0 };
38        if self.0.comment.is_some() {
39            writer.comment(self.0)?;
40            writer.line("")?;
41        }
42        match &self.0.value {
43            AnnotatedValue::Scalar(scalar) => writer.scalar(scalar),
44            AnnotatedValue::Array(items) if items.is_empty() => writer.line("[]"),
45            AnnotatedValue::Object(entries) if entries.is_empty() => writer.line("{}"),
46            AnnotatedValue::Array(items) => writer.sequence(items, Lead::Pending),
47            AnnotatedValue::Object(entries) => writer.mapping(entries, Lead::Pending),
48        }
49    }
50}
51
52/// Whether the indent of the first entry or item is already written.
53#[derive(Clone, Copy)]
54enum Lead {
55    Written,
56    Pending,
57}
58
59impl Lead {
60    /// Whether the entry at `index` needs its own indent.
61    const fn pads(self, index: usize) -> bool {
62        matches!(self, Self::Pending) || index > 0
63    }
64}
65
66/// The marker a child follows.
67#[derive(Clone, Copy)]
68enum Marker {
69    Key,
70    Dash,
71}
72
73struct Writer<'a, 'f> {
74    out: &'a mut fmt::Formatter<'f>,
75    indent: usize,
76}
77
78impl Writer<'_, '_> {
79    fn line(&mut self, text: &str) -> fmt::Result {
80        writeln!(self.out, "{text}")
81    }
82
83    fn pad(&mut self) -> fmt::Result {
84        write!(self.out, "{:1$}", "", self.indent)
85    }
86
87    fn nested(&mut self, write: impl FnOnce(&mut Self) -> fmt::Result) -> fmt::Result {
88        let outer = self.indent;
89        self.indent = outer.saturating_add(CANONICAL_STEP);
90        let result = write(self);
91        self.indent = outer;
92        result
93    }
94
95    fn comment(&mut self, node: &Annotated) -> fmt::Result {
96        let Some(comment) = &node.comment else {
97            return Ok(());
98        };
99        let pad = format_args!("{:1$}", "", self.indent);
100        CommentSyntax::Hash.write_lines(self.out, comment, pad)
101    }
102
103    /// Write the entries at the current indent. The `lead` tells whether the
104    /// caller has already written the indent of the first entry.
105    fn mapping(&mut self, entries: &Entries, lead: Lead) -> fmt::Result {
106        for (index, (key, value)) in entries.into_iter().enumerate() {
107            if lead.pads(index) {
108                self.comment(value)?;
109                self.pad()?;
110            }
111            write!(self.out, "{}:", ScalarText::key(key))?;
112            self.after_marker(value, Marker::Key)?;
113        }
114        Ok(())
115    }
116
117    /// Write the items at the current indent. The `lead` tells whether the
118    /// caller has already written the indent of the first item.
119    fn sequence(&mut self, items: &[Annotated], lead: Lead) -> fmt::Result {
120        for (index, item) in items.iter().enumerate() {
121            if lead.pads(index) {
122                self.comment(item)?;
123                self.pad()?;
124            }
125            self.out.write_str("-")?;
126            self.after_marker(item, Marker::Dash)?;
127        }
128        Ok(())
129    }
130
131    /// Write what follows a `key:` or `-` marker and end the line.
132    ///
133    /// A container under a dash starts on the dash line unless its first
134    /// child has a comment. Under a key it starts on the next line.
135    fn after_marker(&mut self, node: &Annotated, marker: Marker) -> fmt::Result {
136        match &node.value {
137            AnnotatedValue::Scalar(scalar) => {
138                self.out.write_str(" ")?;
139                self.scalar(scalar)
140            }
141            AnnotatedValue::Array(items) if items.is_empty() => self.line(" []"),
142            AnnotatedValue::Object(entries) if entries.is_empty() => self.line(" {}"),
143            AnnotatedValue::Array(items) => {
144                let lead = Self::lead(marker, items.first());
145                self.block(lead, |writer, lead| writer.sequence(items, lead))
146            }
147            AnnotatedValue::Object(entries) => {
148                let first = entries.into_iter().next().map(|(_, value)| value);
149                let lead = Self::lead(marker, first);
150                self.block(lead, |writer, lead| writer.mapping(entries, lead))
151            }
152        }
153    }
154
155    /// Whether a container starts on the line of its marker.
156    fn lead(marker: Marker, first: Option<&Annotated>) -> Lead {
157        match marker {
158            Marker::Dash if first.is_some_and(|node| node.comment.is_none()) => Lead::Written,
159            Marker::Dash | Marker::Key => Lead::Pending,
160        }
161    }
162
163    fn block(
164        &mut self,
165        lead: Lead,
166        write: impl FnOnce(&mut Self, Lead) -> fmt::Result,
167    ) -> fmt::Result {
168        self.nested(|writer| {
169            writer.out.write_str(match lead {
170                Lead::Written => " ",
171                Lead::Pending => "\n",
172            })?;
173            write(writer, lead)
174        })
175    }
176
177    /// Write a scalar and end the line.
178    fn scalar(&mut self, scalar: &Scalar) -> fmt::Result {
179        match scalar {
180            Scalar::Null => self.line("null"),
181            Scalar::Bool(value) => writeln!(self.out, "{value}"),
182            Scalar::Integer(value) => writeln!(self.out, "{value}"),
183            Scalar::Unsigned(value) => writeln!(self.out, "{value}"),
184            Scalar::Float(value) => writeln!(self.out, "{value}"),
185            Scalar::String { text, style } => match YamlString::select(text, *style) {
186                YamlString::Block(block) => self.literal(&block),
187                YamlString::Text(plain) => writeln!(self.out, "{plain}"),
188            },
189        }
190    }
191
192    fn literal(&mut self, block: &LiteralBlock<'_>) -> fmt::Result {
193        self.line(&block.header())?;
194        self.nested(|writer| {
195            for line in block.lines() {
196                if !line.is_empty() {
197                    writer.pad()?;
198                    writer.out.write_str(line)?;
199                }
200                writer.out.write_char('\n')?;
201            }
202            Ok(())
203        })
204    }
205}
206
207#[cfg(test)]
208mod tests;