insta/content/yaml/vendored/
emitter.rs

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