diff --git a/saphyr/src/char_traits.rs b/saphyr/src/char_traits.rs index 4a08da1..5965cc4 100644 --- a/saphyr/src/char_traits.rs +++ b/saphyr/src/char_traits.rs @@ -109,3 +109,16 @@ pub(crate) fn is_uri_char(c: char) -> bool { pub(crate) fn is_tag_char(c: char) -> bool { is_uri_char(c) && !is_flow(c) && c != '!' } + +/// Check if the string can be expressed a valid literal block scalar. +/// The YAML spec supports all of the following in block literals except #xFEFF: +/// ```ignore +/// #x9 | #xA | [#x20-#x7E] /* 8 bit */ +/// | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] /* 16 bit */ +/// | [#x10000-#x10FFFF] /* 32 bit */ +/// ``` +#[inline] +pub(crate) fn is_valid_literal_block_scalar(string: &str) -> bool { + string.chars().all(|character: char| + matches!(character, '\t' | '\n' | '\x20'..='\x7e' | '\u{0085}' | '\u{00a0}'..='\u{d7fff}')) +} diff --git a/saphyr/src/emitter.rs b/saphyr/src/emitter.rs index 081654a..213da01 100644 --- a/saphyr/src/emitter.rs +++ b/saphyr/src/emitter.rs @@ -1,3 +1,4 @@ +use crate::char_traits; use crate::yaml::{Hash, Yaml}; use std::convert::From; use std::error::Error; @@ -35,8 +36,8 @@ pub struct YamlEmitter<'a> { writer: &'a mut dyn fmt::Write, best_indent: usize, compact: bool, - level: isize, + multiline_strings: bool, } pub type EmitResult = Result<(), EmitError>; @@ -111,6 +112,7 @@ impl<'a> YamlEmitter<'a> { best_indent: 2, compact: true, level: -1, + multiline_strings: false, } } @@ -132,6 +134,40 @@ impl<'a> YamlEmitter<'a> { self.compact } + /// Render strings containing multiple lines in [literal style]. + /// + /// # Examples + /// + /// ```rust + /// use yaml_rust2::{Yaml, YamlEmitter, YamlLoader}; + /// + /// let input = r#"{foo: "bar!\nbar!", baz: 42}"#; + /// let parsed = YamlLoader::load_from_str(input).unwrap(); + /// eprintln!("{:?}", parsed); + /// + /// let mut output = String::new(); + /// let mut emitter = YamlEmitter::new(&mut output); + /// emitter.multiline_strings(true); + /// emitter.dump(&parsed[0]).unwrap(); + /// assert_eq!(output.as_str(), "\ + /// --- + /// foo: | + /// bar! + /// bar! + /// baz: 42"); + /// ``` + /// + /// [literal style]: https://yaml.org/spec/1.2/spec.html#id2795688 + pub fn multiline_strings(&mut self, multiline_strings: bool) { + self.multiline_strings = multiline_strings; + } + + /// Determine if this emitter will emit multiline strings when appropriate. + #[must_use] + pub fn is_multiline_strings(&self) -> bool { + self.multiline_strings + } + pub fn dump(&mut self, doc: &Yaml) -> EmitResult { // write DocumentStart writeln!(self.writer, "---")?; @@ -156,7 +192,20 @@ impl<'a> YamlEmitter<'a> { Yaml::Array(ref v) => self.emit_array(v), Yaml::Hash(ref h) => self.emit_hash(h), Yaml::String(ref v) => { - if need_quotes(v) { + if self.multiline_strings + && v.contains('\n') + && char_traits::is_valid_literal_block_scalar(v) + { + write!(self.writer, "|")?; + self.level += 1; + for line in v.lines() { + writeln!(self.writer)?; + self.write_indent()?; + // It's literal text, so don't escape special chars. + write!(self.writer, "{line}")?; + } + self.level -= 1; + } else if need_quotes(v) { escape_str(self.writer, v)?; } else { write!(self.writer, "{v}")?; @@ -334,3 +383,19 @@ fn need_quotes(string: &str) -> bool { || string.parse::().is_ok() || string.parse::().is_ok() } + +#[cfg(test)] +mod test { + use super::YamlEmitter; + use crate::YamlLoader; + + #[test] + fn test_multiline_string() { + let input = r#"{foo: "bar!\nbar!", baz: 42}"#; + let parsed = YamlLoader::load_from_str(input).unwrap(); + let mut output = String::new(); + let mut emitter = YamlEmitter::new(&mut output); + emitter.multiline_strings(true); + emitter.dump(&parsed[0]).unwrap(); + } +} diff --git a/saphyr/tests/test_round_trip.rs b/saphyr/tests/test_round_trip.rs index d051281..5f0a7a1 100644 --- a/saphyr/tests/test_round_trip.rs +++ b/saphyr/tests/test_round_trip.rs @@ -68,3 +68,15 @@ fn test_issue133() { let doc2 = YamlLoader::load_from_str(&out_str).unwrap().pop().unwrap(); assert_eq!(doc, doc2); // This failed because the type has changed to a number now } + +#[test] +fn test_newline() { + let y = Yaml::Array(vec![Yaml::String("\n".to_owned())]); + roundtrip(&y); +} + +#[test] +fn test_crlf() { + let y = Yaml::Array(vec![Yaml::String("\r\n".to_owned())]); + roundtrip(&y); +}