ephemeris_documents/
yaml.rs1mod 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
22const CANONICAL_STEP: usize = 2;
25
26pub(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#[derive(Clone, Copy)]
54enum Lead {
55 Written,
56 Pending,
57}
58
59impl Lead {
60 const fn pads(self, index: usize) -> bool {
62 matches!(self, Self::Pending) || index > 0
63 }
64}
65
66#[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 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 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 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 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 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;