Emit multi-line string values as block scalars

This commit is contained in:
David Aguilar 2024-03-17 00:47:11 -07:00 committed by Ethiraric
parent e4ae1d0546
commit a6c8dfe5b8
3 changed files with 92 additions and 2 deletions

View file

@ -109,3 +109,16 @@ pub(crate) fn is_uri_char(c: char) -> bool {
pub(crate) fn is_tag_char(c: char) -> bool { pub(crate) fn is_tag_char(c: char) -> bool {
is_uri_char(c) && !is_flow(c) && c != '!' 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}'))
}

View file

@ -1,3 +1,4 @@
use crate::char_traits;
use crate::yaml::{Hash, Yaml}; use crate::yaml::{Hash, Yaml};
use std::convert::From; use std::convert::From;
use std::error::Error; use std::error::Error;
@ -35,8 +36,8 @@ pub struct YamlEmitter<'a> {
writer: &'a mut dyn fmt::Write, writer: &'a mut dyn fmt::Write,
best_indent: usize, best_indent: usize,
compact: bool, compact: bool,
level: isize, level: isize,
multiline_strings: bool,
} }
pub type EmitResult = Result<(), EmitError>; pub type EmitResult = Result<(), EmitError>;
@ -111,6 +112,7 @@ impl<'a> YamlEmitter<'a> {
best_indent: 2, best_indent: 2,
compact: true, compact: true,
level: -1, level: -1,
multiline_strings: false,
} }
} }
@ -132,6 +134,40 @@ impl<'a> YamlEmitter<'a> {
self.compact 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 { pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
// write DocumentStart // write DocumentStart
writeln!(self.writer, "---")?; writeln!(self.writer, "---")?;
@ -156,7 +192,20 @@ impl<'a> YamlEmitter<'a> {
Yaml::Array(ref v) => self.emit_array(v), Yaml::Array(ref v) => self.emit_array(v),
Yaml::Hash(ref h) => self.emit_hash(h), Yaml::Hash(ref h) => self.emit_hash(h),
Yaml::String(ref v) => { 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)?; escape_str(self.writer, v)?;
} else { } else {
write!(self.writer, "{v}")?; write!(self.writer, "{v}")?;
@ -334,3 +383,19 @@ fn need_quotes(string: &str) -> bool {
|| string.parse::<i64>().is_ok() || string.parse::<i64>().is_ok()
|| string.parse::<f64>().is_ok() || string.parse::<f64>().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();
}
}

View file

@ -68,3 +68,15 @@ fn test_issue133() {
let doc2 = YamlLoader::load_from_str(&out_str).unwrap().pop().unwrap(); 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 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);
}