1use crate::char_traits;
4use crate::yaml::{Hash, Yaml};
5use std::convert::From;
6use std::error::Error;
7use std::fmt::{self, Display};
8
9#[derive(Copy, Clone, Debug)]
11pub enum EmitError {
12 FmtError(fmt::Error),
14}
15
16impl Error for EmitError {
17 fn cause(&self) -> Option<&dyn Error> {
18 None
19 }
20}
21
22impl Display for EmitError {
23 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
24 match *self {
25 EmitError::FmtError(ref err) => Display::fmt(err, formatter),
26 }
27 }
28}
29
30impl From<fmt::Error> for EmitError {
31 fn from(f: fmt::Error) -> Self {
32 EmitError::FmtError(f)
33 }
34}
35
36#[allow(clippy::module_name_repetitions)]
51pub struct YamlEmitter<'a> {
52 writer: &'a mut dyn fmt::Write,
53 best_indent: usize,
54 compact: bool,
55 level: isize,
56 multiline_strings: bool,
57}
58
59pub type EmitResult = Result<(), EmitError>;
61
62fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
64 wr.write_str("\"")?;
65
66 let mut start = 0;
67
68 for (i, byte) in v.bytes().enumerate() {
69 let escaped = match byte {
70 b'"' => "\\\"",
71 b'\\' => "\\\\",
72 b'\x00' => "\\u0000",
73 b'\x01' => "\\u0001",
74 b'\x02' => "\\u0002",
75 b'\x03' => "\\u0003",
76 b'\x04' => "\\u0004",
77 b'\x05' => "\\u0005",
78 b'\x06' => "\\u0006",
79 b'\x07' => "\\u0007",
80 b'\x08' => "\\b",
81 b'\t' => "\\t",
82 b'\n' => "\\n",
83 b'\x0b' => "\\u000b",
84 b'\x0c' => "\\f",
85 b'\r' => "\\r",
86 b'\x0e' => "\\u000e",
87 b'\x0f' => "\\u000f",
88 b'\x10' => "\\u0010",
89 b'\x11' => "\\u0011",
90 b'\x12' => "\\u0012",
91 b'\x13' => "\\u0013",
92 b'\x14' => "\\u0014",
93 b'\x15' => "\\u0015",
94 b'\x16' => "\\u0016",
95 b'\x17' => "\\u0017",
96 b'\x18' => "\\u0018",
97 b'\x19' => "\\u0019",
98 b'\x1a' => "\\u001a",
99 b'\x1b' => "\\u001b",
100 b'\x1c' => "\\u001c",
101 b'\x1d' => "\\u001d",
102 b'\x1e' => "\\u001e",
103 b'\x1f' => "\\u001f",
104 b'\x7f' => "\\u007f",
105 _ => continue,
106 };
107
108 if start < i {
109 wr.write_str(&v[start..i])?;
110 }
111
112 wr.write_str(escaped)?;
113
114 start = i + 1;
115 }
116
117 if start != v.len() {
118 wr.write_str(&v[start..])?;
119 }
120
121 wr.write_str("\"")?;
122 Ok(())
123}
124
125impl<'a> YamlEmitter<'a> {
126 pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter<'a> {
128 YamlEmitter {
129 writer,
130 best_indent: 2,
131 compact: true,
132 level: -1,
133 multiline_strings: false,
134 }
135 }
136
137 pub fn compact(&mut self, compact: bool) {
146 self.compact = compact;
147 }
148
149 #[must_use]
151 pub fn is_compact(&self) -> bool {
152 self.compact
153 }
154
155 pub fn multiline_strings(&mut self, multiline_strings: bool) {
180 self.multiline_strings = multiline_strings;
181 }
182
183 #[must_use]
185 pub fn is_multiline_strings(&self) -> bool {
186 self.multiline_strings
187 }
188
189 pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
193 writeln!(self.writer, "---")?;
195 self.level = -1;
196 self.emit_node(doc)
197 }
198
199 fn write_indent(&mut self) -> EmitResult {
200 if self.level <= 0 {
201 return Ok(());
202 }
203 for _ in 0..self.level {
204 for _ in 0..self.best_indent {
205 write!(self.writer, " ")?;
206 }
207 }
208 Ok(())
209 }
210
211 fn emit_node(&mut self, node: &Yaml) -> EmitResult {
212 match *node {
213 Yaml::Array(ref v) => self.emit_array(v),
214 Yaml::Hash(ref h) => self.emit_hash(h),
215 Yaml::String(ref v) => {
216 if self.multiline_strings
217 && v.contains('\n')
218 && char_traits::is_valid_literal_block_scalar(v)
219 {
220 self.emit_literal_block(v)?;
221 } else if need_quotes(v) {
222 escape_str(self.writer, v)?;
223 } else {
224 write!(self.writer, "{v}")?;
225 }
226 Ok(())
227 }
228 Yaml::Boolean(v) => {
229 if v {
230 self.writer.write_str("true")?;
231 } else {
232 self.writer.write_str("false")?;
233 }
234 Ok(())
235 }
236 Yaml::Integer(v) => {
237 write!(self.writer, "{v}")?;
238 Ok(())
239 }
240 Yaml::Real(ref v) => {
241 write!(self.writer, "{v}")?;
242 Ok(())
243 }
244 Yaml::Null | Yaml::BadValue => {
245 write!(self.writer, "~")?;
246 Ok(())
247 }
248 Yaml::Alias(_) => Ok(()),
250 }
251 }
252
253 fn emit_literal_block(&mut self, v: &str) -> EmitResult {
254 let ends_with_newline = v.ends_with('\n');
255 if ends_with_newline {
256 self.writer.write_str("|")?;
257 } else {
258 self.writer.write_str("|-")?;
259 }
260
261 self.level += 1;
262 for line in v.lines() {
264 writeln!(self.writer)?;
265 self.write_indent()?;
266 self.writer.write_str(line)?;
268 }
269 self.level -= 1;
270 Ok(())
271 }
272
273 fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
274 if v.is_empty() {
275 write!(self.writer, "[]")?;
276 } else {
277 self.level += 1;
278 for (cnt, x) in v.iter().enumerate() {
279 if cnt > 0 {
280 writeln!(self.writer)?;
281 self.write_indent()?;
282 }
283 write!(self.writer, "-")?;
284 self.emit_val(true, x)?;
285 }
286 self.level -= 1;
287 }
288 Ok(())
289 }
290
291 fn emit_hash(&mut self, h: &Hash) -> EmitResult {
292 if h.is_empty() {
293 self.writer.write_str("{}")?;
294 } else {
295 self.level += 1;
296 for (cnt, (k, v)) in h.iter().enumerate() {
297 let complex_key = matches!(*k, Yaml::Hash(_) | Yaml::Array(_));
298 if cnt > 0 {
299 writeln!(self.writer)?;
300 self.write_indent()?;
301 }
302 if complex_key {
303 write!(self.writer, "?")?;
304 self.emit_val(true, k)?;
305 writeln!(self.writer)?;
306 self.write_indent()?;
307 write!(self.writer, ":")?;
308 self.emit_val(true, v)?;
309 } else {
310 self.emit_node(k)?;
311 write!(self.writer, ":")?;
312 self.emit_val(false, v)?;
313 }
314 }
315 self.level -= 1;
316 }
317 Ok(())
318 }
319
320 fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
325 match *val {
326 Yaml::Array(ref v) => {
327 if (inline && self.compact) || v.is_empty() {
328 write!(self.writer, " ")?;
329 } else {
330 writeln!(self.writer)?;
331 self.level += 1;
332 self.write_indent()?;
333 self.level -= 1;
334 }
335 self.emit_array(v)
336 }
337 Yaml::Hash(ref h) => {
338 if (inline && self.compact) || h.is_empty() {
339 write!(self.writer, " ")?;
340 } else {
341 writeln!(self.writer)?;
342 self.level += 1;
343 self.write_indent()?;
344 self.level -= 1;
345 }
346 self.emit_hash(h)
347 }
348 _ => {
349 write!(self.writer, " ")?;
350 self.emit_node(val)
351 }
352 }
353 }
354}
355
356#[allow(clippy::doc_markdown)]
371fn need_quotes(string: &str) -> bool {
372 fn need_quotes_spaces(string: &str) -> bool {
373 string.starts_with(' ') || string.ends_with(' ')
374 }
375
376 string.is_empty()
377 || need_quotes_spaces(string)
378 || string.starts_with(|character: char| {
379 matches!(
380 character,
381 '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'
382 )
383 })
384 || string.contains(|character: char| {
385 matches!(character, ':'
386 | '{'
387 | '}'
388 | '['
389 | ']'
390 | ','
391 | '#'
392 | '`'
393 | '\"'
394 | '\''
395 | '\\'
396 | '\0'..='\x06'
397 | '\t'
398 | '\n'
399 | '\r'
400 | '\x0e'..='\x1a'
401 | '\x1c'..='\x1f')
402 })
403 || [
404 "true", "false", "True", "False", "TRUE", "FALSE",
406 "null", "Null", "NULL", "~",
408 "y", "Y", "n", "N", "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "False",
411 "FALSE", "on", "On", "ON", "off", "Off", "OFF",
412 ]
413 .contains(&string)
414 || string.starts_with('.')
415 || string.starts_with("0x")
416 || string.parse::<i64>().is_ok()
417 || string.parse::<f64>().is_ok()
418}
419
420#[cfg(test)]
421mod test {
422 use super::YamlEmitter;
423 use crate::YamlLoader;
424
425 #[test]
426 fn test_multiline_string() {
427 let input = r#"{foo: "bar!\nbar!", baz: 42}"#;
428 let parsed = YamlLoader::load_from_str(input).unwrap();
429 let mut output = String::new();
430 let mut emitter = YamlEmitter::new(&mut output);
431 emitter.multiline_strings(true);
432 emitter.dump(&parsed[0]).unwrap();
433 }
434}