Repository: ron-rs/ron (rust)

Issue:
Pretty Print Multiline String
Is it possible to add a config to `PrettyConfig` to allow `to_string_pretty` to print mulitline string as it is? For example, print
```
"a
b
c"
```
instead of `"a\nb\nc"`?

Pull request description:
Add `escape_strings` option to `PrettyConfig`
This PR adds an `escape_strings` option to `PrettyConfig` that is enabled by default. When enabled, strings are serialised using escapes. When disabled, strings that contain no `"` are serialised literally. If the string does contain a `"`, a raw string literal with the minimum number of required leading `#` is emitted.

This feature was suggested in #425, i.e. this PR fixes #425.

* [ ] I've included my change in `CHANGELOG.md`

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6da90b8..47598fe6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Error instead of panic when deserializing non-identifiers as field names ([#415](https://github.com/ron-rs/ron/pull/415))
 - Breaking: Fix issue [#307](https://github.com/ron-rs/ron/issues/307) stack overflow with explicit recursion limits in serialising and deserialising ([#420](https://github.com/ron-rs/ron/pull/420))
 - Fix issue [#423](https://github.com/ron-rs/ron/issues/423) deserialising an identifier into a borrowed str ([#424](https://github.com/ron-rs/ron/pull/424))
+- Add `escape_strings` option to `PrettyConfig` to allow serialising with or without escaping ([#426](https://github.com/ron-rs/ron/pull/426))
 
 ## [0.8.0] - 2022-08-17
 
diff --git a/src/ser/mod.rs b/src/ser/mod.rs
index d708198f..691cfb06 100644
--- a/src/ser/mod.rs
+++ b/src/ser/mod.rs
@@ -92,6 +92,9 @@ pub struct PrettyConfig {
     pub extensions: Extensions,
     /// Enable compact arrays
     pub compact_arrays: bool,
+    /// Whether to serialize strings as escaped strings,
+    ///  or fall back onto raw strings if necessary.
+    pub escape_strings: bool,
 }
 
 impl PrettyConfig {
@@ -170,15 +173,17 @@ impl PrettyConfig {
         self
     }
 
-    /// Configures whether every array should be a single line (true) or a multi line one (false)
-    /// When false, `["a","b"]` (as well as any array) will serialize to
+    /// Configures whether every array should be a single line (`true`)
+    /// or a multi line one (`false`).
+    ///
+    /// When `false`, `["a","b"]` (as well as any array) will serialize to
     /// `
     /// [
     ///   "a",
     ///   "b",
     /// ]
     /// `
-    /// When true, `["a","b"]` (as well as any array) will serialize to `["a","b"]`
+    /// When `true`, `["a","b"]` (as well as any array) will serialize to `["a","b"]`
     ///
     /// Default: `false`
     pub fn compact_arrays(mut self, compact_arrays: bool) -> Self {
@@ -195,6 +200,26 @@ impl PrettyConfig {
 
         self
     }
+
+    /// Configures whether strings should be serialized using escapes (true)
+    /// or fall back to raw strings if the string contains a `"` (false).
+    ///
+    /// When `true`, "a\nb" will serialize to
+    /// `
+    /// "a\nb"
+    /// `
+    /// When `false`, "a\nb" will instead serialize to
+    /// `
+    /// "a
+    /// b"
+    /// `
+    ///
+    /// Default: `true`
+    pub fn escape_strings(mut self, escape_strings: bool) -> Self {
+        self.escape_strings = escape_strings;
+
+        self
+    }
 }
 
 impl Default for PrettyConfig {
@@ -213,6 +238,7 @@ impl Default for PrettyConfig {
             enumerate_arrays: false,
             extensions: Extensions::empty(),
             compact_arrays: false,
+            escape_strings: true,
         }
     }
 }
@@ -302,6 +328,12 @@ impl<W: io::Write> Serializer<W> {
                 .map_or(Extensions::empty(), |&(ref config, _)| config.extensions)
     }
 
+    fn escape_strings(&self) -> bool {
+        self.pretty
+            .as_ref()
+            .map_or(true, |&(ref config, _)| config.escape_strings)
+    }
+
     fn start_indent(&mut self) -> Result<()> {
         if let Some((ref config, ref mut pretty)) = self.pretty {
             pretty.indent += 1;
@@ -356,6 +388,28 @@ impl<W: io::Write> Serializer<W> {
         Ok(())
     }
 
+    fn serialize_unescaped_or_raw_str(&mut self, value: &str) -> io::Result<()> {
+        if value.contains('"') {
+            let (_, num_consecutive_hashes) =
+                value.chars().fold((0, 0), |(count, max), c| match c {
+                    '#' => (count + 1, max.max(count + 1)),
+                    _ => (0_usize, max),
+                });
+            let hashes = vec![b'#'; num_consecutive_hashes + 1];
+            self.output.write_all(b"r")?;
+            self.output.write_all(&hashes)?;
+            self.output.write_all(b"\"")?;
+            self.output.write_all(value.as_bytes())?;
+            self.output.write_all(b"\"")?;
+            self.output.write_all(&hashes)?;
+        } else {
+            self.output.write_all(b"\"")?;
+            self.output.write_all(value.as_bytes())?;
+            self.output.write_all(b"\"")?;
+        }
+        Ok(())
+    }
+
     fn serialize_sint(&mut self, value: impl Into<LargeSInt>) -> Result<()> {
         // TODO optimize
         write!(self.output, "{}", value.into())?;
@@ -495,7 +549,11 @@ impl<'a, W: io::Write> ser::Serializer for &'a mut Serializer<W> {
     }
 
     fn serialize_str(self, v: &str) -> Result<()> {
-        self.serialize_escaped_str(v)?;
+        if self.escape_strings() {
+            self.serialize_escaped_str(v)?;
+        } else {
+            self.serialize_unescaped_or_raw_str(v)?;
+        }
 
         Ok(())
     }


Test patch:
diff --git a/tests/425_escape_strings.rs b/tests/425_escape_strings.rs
new file mode 100644
index 00000000..93cd5066
--- /dev/null
+++ b/tests/425_escape_strings.rs
@@ -0,0 +1,60 @@
+use ron::{
+    de::from_str,
+    ser::{to_string, to_string_pretty, PrettyConfig},
+};
+
+fn test_string_roundtrip(s: &str, config: Option<PrettyConfig>) -> String {
+    let ser = match config {
+        Some(config) => to_string_pretty(s, config),
+        None => to_string(s),
+    }
+    .unwrap();
+
+    let de: String = from_str(&ser).unwrap();
+
+    assert_eq!(s, de);
+
+    ser
+}
+
+#[test]
+fn test_escaped_string() {
+    let config = Some(PrettyConfig::default());
+
+    assert_eq!(test_string_roundtrip("a\nb", None), r#""a\nb""#);
+    assert_eq!(test_string_roundtrip("a\nb", config.clone()), r#""a\nb""#);
+
+    assert_eq!(test_string_roundtrip("", None), "\"\"");
+    assert_eq!(test_string_roundtrip("", config.clone()), "\"\"");
+
+    assert_eq!(test_string_roundtrip("\"", None), r#""\"""#);
+    assert_eq!(test_string_roundtrip("\"", config.clone()), r#""\"""#);
+
+    assert_eq!(test_string_roundtrip("#", None), "\"#\"");
+    assert_eq!(test_string_roundtrip("#", config.clone()), "\"#\"");
+
+    assert_eq!(test_string_roundtrip("\"#", None), r##""\"#""##);
+    assert_eq!(test_string_roundtrip("\"#", config.clone()), r##""\"#""##);
+
+    assert_eq!(test_string_roundtrip("#\"#", None), r##""#\"#""##);
+    assert_eq!(test_string_roundtrip("#\"#", config.clone()), r##""#\"#""##);
+
+    assert_eq!(test_string_roundtrip("#\"##", None), r###""#\"##""###);
+    assert_eq!(test_string_roundtrip("#\"##", config), r###""#\"##""###);
+}
+
+#[test]
+fn test_unescaped_string() {
+    let config = Some(PrettyConfig::default().escape_strings(false));
+
+    assert_eq!(test_string_roundtrip("a\nb", config.clone()), "\"a\nb\"");
+    assert_eq!(test_string_roundtrip("", config.clone()), "\"\"");
+    assert_eq!(test_string_roundtrip("\"", config.clone()), "r#\"\"\"#");
+    assert_eq!(test_string_roundtrip("#", config.clone()), "\"#\"");
+    assert_eq!(test_string_roundtrip("\"#", config.clone()), "r##\"\"#\"##");
+    assert_eq!(
+        test_string_roundtrip("#\"#", config.clone()),
+        "r##\"#\"#\"##"
+    );
+    assert_eq!(test_string_roundtrip("#\"##", config), "r###\"#\"##\"###");
+}