yaml_rust_davvid/
emitter.rs

1use crate::yaml::{Hash, Yaml};
2
3use std::convert::From;
4use std::error::Error;
5use std::fmt::{self, Display};
6
7#[derive(Copy, Clone, Debug)]
8pub enum EmitError {
9    FmtError(fmt::Error),
10    BadHashmapKey,
11}
12
13impl Error for EmitError {
14    fn cause(&self) -> Option<&dyn Error> {
15        None
16    }
17}
18
19impl Display for EmitError {
20    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
21        match *self {
22            EmitError::FmtError(ref err) => Display::fmt(err, formatter),
23            EmitError::BadHashmapKey => formatter.write_str("bad hashmap key"),
24        }
25    }
26}
27
28impl From<fmt::Error> for EmitError {
29    fn from(f: fmt::Error) -> Self {
30        EmitError::FmtError(f)
31    }
32}
33
34pub struct YamlEmitter<'a> {
35    writer: &'a mut dyn fmt::Write,
36    best_indent: usize,
37    compact: bool,
38    multiline_strings: bool,
39
40    level: isize,
41}
42
43pub type EmitResult = Result<(), EmitError>;
44
45// from serialize::json
46fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
47    wr.write_str("\"")?;
48
49    let mut start = 0;
50
51    for (i, byte) in v.bytes().enumerate() {
52        let escaped = match byte {
53            b'"' => "\\\"",
54            b'\\' => "\\\\",
55            b'\x00' => "\\u0000",
56            b'\x01' => "\\u0001",
57            b'\x02' => "\\u0002",
58            b'\x03' => "\\u0003",
59            b'\x04' => "\\u0004",
60            b'\x05' => "\\u0005",
61            b'\x06' => "\\u0006",
62            b'\x07' => "\\u0007",
63            b'\x08' => "\\b",
64            b'\t' => "\\t",
65            b'\n' => "\\n",
66            b'\x0b' => "\\u000b",
67            b'\x0c' => "\\f",
68            b'\r' => "\\r",
69            b'\x0e' => "\\u000e",
70            b'\x0f' => "\\u000f",
71            b'\x10' => "\\u0010",
72            b'\x11' => "\\u0011",
73            b'\x12' => "\\u0012",
74            b'\x13' => "\\u0013",
75            b'\x14' => "\\u0014",
76            b'\x15' => "\\u0015",
77            b'\x16' => "\\u0016",
78            b'\x17' => "\\u0017",
79            b'\x18' => "\\u0018",
80            b'\x19' => "\\u0019",
81            b'\x1a' => "\\u001a",
82            b'\x1b' => "\\u001b",
83            b'\x1c' => "\\u001c",
84            b'\x1d' => "\\u001d",
85            b'\x1e' => "\\u001e",
86            b'\x1f' => "\\u001f",
87            b'\x7f' => "\\u007f",
88            _ => continue,
89        };
90
91        if start < i {
92            wr.write_str(&v[start..i])?;
93        }
94
95        wr.write_str(escaped)?;
96
97        start = i + 1;
98    }
99
100    if start != v.len() {
101        wr.write_str(&v[start..])?;
102    }
103
104    wr.write_str("\"")?;
105    Ok(())
106}
107
108impl<'a> YamlEmitter<'a> {
109    pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter {
110        YamlEmitter {
111            writer,
112            best_indent: 2,
113            compact: true,
114            level: -1,
115            multiline_strings: false,
116        }
117    }
118
119    /// Set 'compact inline notation' on or off, as described for block
120    /// [sequences](http://www.yaml.org/spec/1.2/spec.html#id2797382)
121    /// and
122    /// [mappings](http://www.yaml.org/spec/1.2/spec.html#id2798057).
123    ///
124    /// In this form, blocks cannot have any properties (such as anchors
125    /// or tags), which should be OK, because this emitter doesn't
126    /// (currently) emit those anyways.
127    pub fn compact(&mut self, compact: bool) {
128        self.compact = compact;
129    }
130
131    /// Determine if this emitter is using 'compact inline notation'.
132    pub fn is_compact(&self) -> bool {
133        self.compact
134    }
135
136    /// Render strings containing multiple lines in [literal style].
137    ///
138    /// # Examples
139    ///
140    /// ```rust
141    /// use yaml_rust_davvid::{Yaml, YamlEmitter, YamlLoader};
142    ///
143    /// let input = r#"{foo: "bar!\nbar!", baz: 42}"#;
144    /// let parsed = YamlLoader::load_from_str(input).unwrap();
145    /// eprintln!("{:?}", parsed);
146    ///
147    /// let mut output = String::new();
148    /// # {
149    /// let mut emitter = YamlEmitter::new(&mut output);
150    /// emitter.multiline_strings(true);
151    /// emitter.dump(&parsed[0]).unwrap();
152    /// # }
153    ///
154    /// assert_eq!(output.as_str(), "\
155    /// ---
156    /// foo: |
157    ///   bar!
158    ///   bar!
159    /// baz: 42");
160    /// ```
161    ///
162    /// [literal style]: https://yaml.org/spec/1.2/spec.html#id2795688
163    pub fn multiline_strings(&mut self, multiline_strings: bool) {
164        self.multiline_strings = multiline_strings
165    }
166
167    /// Determine if this emitter will emit multiline strings when appropriate.
168    pub fn is_multiline_strings(&self) -> bool {
169        self.multiline_strings
170    }
171
172    pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
173        // write DocumentStart
174        writeln!(self.writer, "---")?;
175        self.level = -1;
176        self.emit_node(doc)
177    }
178
179    fn write_indent(&mut self) -> EmitResult {
180        if self.level <= 0 {
181            return Ok(());
182        }
183        for _ in 0..self.level {
184            for _ in 0..self.best_indent {
185                write!(self.writer, " ")?;
186            }
187        }
188        Ok(())
189    }
190
191    fn emit_node(&mut self, node: &Yaml) -> EmitResult {
192        match *node {
193            Yaml::Array(ref v) => self.emit_array(v),
194            Yaml::Hash(ref h) => self.emit_hash(h),
195            Yaml::String(ref v) => {
196                if self.multiline_strings && v.contains('\n') {
197                    write!(self.writer, "|")?;
198                    self.level += 1;
199                    for line in v.lines() {
200                        writeln!(self.writer)?;
201                        self.write_indent()?;
202                        // It's literal text, so don't escape special chars!
203                        write!(self.writer, "{}", line)?;
204                    }
205                    self.level -= 1;
206                } else if need_quotes(v) {
207                    escape_str(self.writer, v)?;
208                } else {
209                    write!(self.writer, "{}", v)?;
210                }
211
212                Ok(())
213            }
214            Yaml::Boolean(v) => {
215                if v {
216                    self.writer.write_str("true")?;
217                } else {
218                    self.writer.write_str("false")?;
219                }
220                Ok(())
221            }
222            Yaml::Integer(v) => {
223                write!(self.writer, "{}", v)?;
224                Ok(())
225            }
226            Yaml::Real(ref v) => {
227                write!(self.writer, "{}", v)?;
228                Ok(())
229            }
230            Yaml::Null | Yaml::BadValue => {
231                write!(self.writer, "~")?;
232                Ok(())
233            }
234            // XXX(chenyh) Alias
235            _ => Ok(()),
236        }
237    }
238
239    fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
240        if v.is_empty() {
241            write!(self.writer, "[]")?;
242        } else {
243            self.level += 1;
244            for (cnt, x) in v.iter().enumerate() {
245                if cnt > 0 {
246                    writeln!(self.writer)?;
247                    self.write_indent()?;
248                }
249                write!(self.writer, "-")?;
250                self.emit_val(true, x)?;
251            }
252            self.level -= 1;
253        }
254        Ok(())
255    }
256
257    fn emit_hash(&mut self, h: &Hash) -> EmitResult {
258        if h.is_empty() {
259            self.writer.write_str("{}")?;
260        } else {
261            self.level += 1;
262            for (cnt, (k, v)) in h.iter().enumerate() {
263                let complex_key = matches!(*k, Yaml::Hash(_) | Yaml::Array(_));
264                if cnt > 0 {
265                    writeln!(self.writer)?;
266                    self.write_indent()?;
267                }
268                if complex_key {
269                    write!(self.writer, "?")?;
270                    self.emit_val(true, k)?;
271                    writeln!(self.writer)?;
272                    self.write_indent()?;
273                    write!(self.writer, ":")?;
274                    self.emit_val(true, v)?;
275                } else {
276                    self.emit_node(k)?;
277                    write!(self.writer, ":")?;
278                    self.emit_val(false, v)?;
279                }
280            }
281            self.level -= 1;
282        }
283        Ok(())
284    }
285
286    /// Emit a yaml as a hash or array value: i.e., which should appear
287    /// following a ":" or "-", either after a space, or on a new line.
288    /// If `inline` is true, then the preceding characters are distinct
289    /// and short enough to respect the compact flag.
290    fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
291        match *val {
292            Yaml::Array(ref v) => {
293                if (inline && self.compact) || v.is_empty() {
294                    write!(self.writer, " ")?;
295                } else {
296                    writeln!(self.writer)?;
297                    self.level += 1;
298                    self.write_indent()?;
299                    self.level -= 1;
300                }
301                self.emit_array(v)
302            }
303            Yaml::Hash(ref h) => {
304                if (inline && self.compact) || h.is_empty() {
305                    write!(self.writer, " ")?;
306                } else {
307                    writeln!(self.writer)?;
308                    self.level += 1;
309                    self.write_indent()?;
310                    self.level -= 1;
311                }
312                self.emit_hash(h)
313            }
314            _ => {
315                write!(self.writer, " ")?;
316                self.emit_node(val)
317            }
318        }
319    }
320}
321
322/// Check if the string requires quoting.
323/// Strings starting with any of the following characters must be quoted.
324/// :, &, *, ?, |, -, <, >, =, !, %, @
325/// Strings containing any of the following characters must be quoted.
326/// {, }, [, ], ,, #, `
327///
328/// If the string contains any of the following control characters, it must be escaped with double quotes:
329/// \0, \x01, \x02, \x03, \x04, \x05, \x06, \a, \b, \t, \n, \v, \f, \r, \x0e, \x0f, \x10, \x11, \x12, \x13, \x14, \x15, \x16, \x17, \x18, \x19, \x1a, \e, \x1c, \x1d, \x1e, \x1f, \N, \_, \L, \P
330///
331/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
332/// * When the string is true or false (otherwise, it would be treated as a boolean value);
333/// * When the string is null or ~ (otherwise, it would be considered as a null value);
334/// * When the string looks like a number, such as integers (e.g. 2, 14, etc.), floats (e.g. 2.6, 14.9) and exponential numbers (e.g. 12e7, etc.) (otherwise, it would be treated as a numeric value);
335/// * When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).
336fn need_quotes(string: &str) -> bool {
337    fn need_quotes_spaces(string: &str) -> bool {
338        string.starts_with(' ') || string.ends_with(' ')
339    }
340
341    string.is_empty()
342        || need_quotes_spaces(string)
343        || string.starts_with(|character: char| {
344            matches!(
345                character,
346                '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'
347            )
348        })
349        || string.contains(|character: char| {
350            matches!(character, ':'
351                | '{'
352                | '}'
353                | '['
354                | ']'
355                | ','
356                | '#'
357                | '`'
358                | '\"'
359                | '\''
360                | '\\'
361                | '\0'..='\x06'
362                | '\t'
363                | '\n'
364                | '\r'
365                | '\x0e'..='\x1a'
366                | '\x1c'..='\x1f'
367            )
368        })
369        || [
370            // http://yaml.org/type/bool.html
371            // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse
372            // them as string, not booleans, although it is violating the YAML 1.1 specification.
373            // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088.
374            "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE",
375            "false", "on", "On", "ON", "off", "Off", "OFF",
376            // http://yaml.org/type/null.html
377            "null", "Null", "NULL", "~",
378        ]
379        .contains(&string)
380        || string.starts_with('.')
381        || string.starts_with("0x")
382        || string.parse::<i64>().is_ok()
383        || string.parse::<f64>().is_ok()
384}
385
386#[cfg(test)]
387mod test {
388    use super::*;
389    use crate::YamlLoader;
390
391    #[test]
392    fn test_emit_simple() {
393        let s = "
394# comment
395a0 bb: val
396a1:
397    b1: 4
398    b2: d
399a2: 4 # i'm comment
400a3: [1, 2, 3]
401a4:
402    - [a1, a2]
403    - 2
404";
405
406        let docs = YamlLoader::load_from_str(s).unwrap();
407        let doc = &docs[0];
408        let mut writer = String::new();
409        {
410            let mut emitter = YamlEmitter::new(&mut writer);
411            emitter.dump(doc).unwrap();
412        }
413        println!("original:\n{}", s);
414        println!("emitted:\n{}", writer);
415        let docs_new = match YamlLoader::load_from_str(&writer) {
416            Ok(y) => y,
417            Err(e) => panic!("{}", e),
418        };
419        let doc_new = &docs_new[0];
420
421        assert_eq!(doc, doc_new);
422    }
423
424    #[test]
425    fn test_emit_complex() {
426        let s = r#"
427cataloge:
428  product: &coffee   { name: Coffee,    price: 2.5  ,  unit: 1l  }
429  product: &cookies  { name: Cookies!,  price: 3.40 ,  unit: 400g}
430
431products:
432  *coffee:
433    amount: 4
434  *cookies:
435    amount: 4
436  [1,2,3,4]:
437    array key
438  2.4:
439    real key
440  true:
441    bool key
442  {}:
443    empty hash key
444            "#;
445        let docs = YamlLoader::load_from_str(s).unwrap();
446        let doc = &docs[0];
447        let mut writer = String::new();
448        {
449            let mut emitter = YamlEmitter::new(&mut writer);
450            emitter.dump(doc).unwrap();
451        }
452        let docs_new = match YamlLoader::load_from_str(&writer) {
453            Ok(y) => y,
454            Err(e) => panic!("{}", e),
455        };
456        let doc_new = &docs_new[0];
457        assert_eq!(doc, doc_new);
458    }
459
460    #[test]
461    fn test_emit_avoid_quotes() {
462        let s = r#"---
463a7: 你好
464boolean: "true"
465boolean2: "false"
466date: 2014-12-31
467empty_string: ""
468empty_string1: " "
469empty_string2: "    a"
470empty_string3: "    a "
471exp: "12e7"
472field: ":"
473field2: "{"
474field3: "\\"
475field4: "\n"
476field5: "can't avoid quote"
477float: "2.6"
478int: "4"
479nullable: "null"
480nullable2: "~"
481products:
482  "*coffee":
483    amount: 4
484  "*cookies":
485    amount: 4
486  ".milk":
487    amount: 1
488  "2.4": real key
489  "[1,2,3,4]": array key
490  "true": bool key
491  "{}": empty hash key
492x: test
493y: avoid quoting here
494z: string with spaces"#;
495
496        let docs = YamlLoader::load_from_str(s).unwrap();
497        let doc = &docs[0];
498        let mut writer = String::new();
499        {
500            let mut emitter = YamlEmitter::new(&mut writer);
501            emitter.dump(doc).unwrap();
502        }
503
504        assert_eq!(s, writer, "actual:\n\n{}\n", writer);
505    }
506
507    #[test]
508    fn emit_quoted_bools() {
509        let input = r#"---
510string0: yes
511string1: no
512string2: "true"
513string3: "false"
514string4: "~"
515null0: ~
516[true, false]: real_bools
517[True, TRUE, False, FALSE, y,Y,yes,Yes,YES,n,N,no,No,NO,on,On,ON,off,Off,OFF]: false_bools
518bool0: true
519bool1: false"#;
520        let expected = r#"---
521string0: "yes"
522string1: "no"
523string2: "true"
524string3: "false"
525string4: "~"
526null0: ~
527? - true
528  - false
529: real_bools
530? - "True"
531  - "TRUE"
532  - "False"
533  - "FALSE"
534  - y
535  - Y
536  - "yes"
537  - "Yes"
538  - "YES"
539  - n
540  - N
541  - "no"
542  - "No"
543  - "NO"
544  - "on"
545  - "On"
546  - "ON"
547  - "off"
548  - "Off"
549  - "OFF"
550: false_bools
551bool0: true
552bool1: false"#;
553
554        let docs = YamlLoader::load_from_str(input).unwrap();
555        let doc = &docs[0];
556        let mut writer = String::new();
557        {
558            let mut emitter = YamlEmitter::new(&mut writer);
559            emitter.dump(doc).unwrap();
560        }
561
562        assert_eq!(
563            expected, writer,
564            "expected:\n{}\nactual:\n{}\n",
565            expected, writer
566        );
567    }
568
569    #[test]
570    fn test_empty_and_nested() {
571        test_empty_and_nested_flag(false)
572    }
573
574    #[test]
575    fn test_empty_and_nested_compact() {
576        test_empty_and_nested_flag(true)
577    }
578
579    fn test_empty_and_nested_flag(compact: bool) {
580        let s = if compact {
581            r#"---
582a:
583  b:
584    c: hello
585  d: {}
586e:
587  - f
588  - g
589  - h: []"#
590        } else {
591            r#"---
592a:
593  b:
594    c: hello
595  d: {}
596e:
597  - f
598  - g
599  -
600    h: []"#
601        };
602
603        let docs = YamlLoader::load_from_str(s).unwrap();
604        let doc = &docs[0];
605        let mut writer = String::new();
606        {
607            let mut emitter = YamlEmitter::new(&mut writer);
608            emitter.compact(compact);
609            emitter.dump(doc).unwrap();
610        }
611
612        assert_eq!(s, writer);
613    }
614
615    #[test]
616    fn test_nested_arrays() {
617        let s = r#"---
618a:
619  - b
620  - - c
621    - d
622    - - e
623      - f"#;
624
625        let docs = YamlLoader::load_from_str(s).unwrap();
626        let doc = &docs[0];
627        let mut writer = String::new();
628        {
629            let mut emitter = YamlEmitter::new(&mut writer);
630            emitter.dump(doc).unwrap();
631        }
632        println!("original:\n{}", s);
633        println!("emitted:\n{}", writer);
634
635        assert_eq!(s, writer);
636    }
637
638    #[test]
639    fn test_deeply_nested_arrays() {
640        let s = r#"---
641a:
642  - b
643  - - c
644    - d
645    - - e
646      - - f
647      - - e"#;
648
649        let docs = YamlLoader::load_from_str(s).unwrap();
650        let doc = &docs[0];
651        let mut writer = String::new();
652        {
653            let mut emitter = YamlEmitter::new(&mut writer);
654            emitter.dump(doc).unwrap();
655        }
656        println!("original:\n{}", s);
657        println!("emitted:\n{}", writer);
658
659        assert_eq!(s, writer);
660    }
661
662    #[test]
663    fn test_nested_hashes() {
664        let s = r#"---
665a:
666  b:
667    c:
668      d:
669        e: f"#;
670
671        let docs = YamlLoader::load_from_str(s).unwrap();
672        let doc = &docs[0];
673        let mut writer = String::new();
674        {
675            let mut emitter = YamlEmitter::new(&mut writer);
676            emitter.dump(doc).unwrap();
677        }
678        println!("original:\n{}", s);
679        println!("emitted:\n{}", writer);
680
681        assert_eq!(s, writer);
682    }
683}