Repository: ron-rs/ron (rust)

Issue:
Support for multi-line string literals
Will RON ever support multi-line string literals? This is a feature that missing from JSON, but is present in YAML, TOML, and in Rust proper, like so:

```yaml
# YAML multi-line string literals
key: >
  This is a very long sentence
  that spans several lines in the YAML
  but which will be rendered as a string
  without carriage returns.
```

```toml
# TOML multi-line string literals
key = '''
    This is a very long sentence
    that spans several lines in the TOML
    but which will be rendered as a string
    without carriage returns.
'''
```

```rust
// Rust multi-line string literals
let key = r#"
    This is a very long sentence
    that spans several lines in Rust
    but which will be rendered as a string
    without carriage returns.
"#;

let key = r##"
    This is a sentence which happens
    to contain a '#' character inside.
"##;
```

This could be a useful for storing localized strings for dialogue boxes, for example, or other complex strings that are too tedious and error-prone to escape by hand. Here is some pseudo-RON syntax showing how this feature could be used:

```ron
System(
    arch: "x86_64-linux",
    shell: "/usr/bin/bash",
    packages: ["firefox", "git", "rustup"],
    startup: r##"
        #!/usr/bin/bash
        export PS1="..."
        echo "Done booting!"
    "##,
)
```

Given that RON already mimics Rust's notation quite well, adopting its syntax for raw string literals would make this feature feel right at home. See the [official reference page](https://doc.rust-lang.org/reference/tokens.html#raw-string-literals) and [Rust By Example](https://rustbyexample.com/std/str.html#literals-and-escapes) for details.

Pull request:
Add support for parsing raw string literals
### Added

* Add support for parsing Rust-style raw string literals.

### Changed

* Update string unit test to verify string literal deserialization.

Fixes #111.

Patch:
diff --git a/src/parse.rs b/src/parse.rs
index 26c58068..fb4e8dd3 100644
--- a/src/parse.rs
+++ b/src/parse.rs
@@ -342,11 +342,17 @@ impl<'a> Bytes<'a> {
     }
 
     pub fn string(&mut self) -> Result<ParsedStr> {
-        use std::iter::repeat;
-
-        if !self.consume("\"") {
-            return self.err(ParseError::ExpectedString);
+        if self.consume("\"") {
+            self.escaped_string()
+        } else if self.consume("r") {
+            self.raw_string()
+        } else {
+            self.err(ParseError::ExpectedString)
         }
+    }
+
+    fn escaped_string(&mut self) -> Result<ParsedStr> {
+        use std::iter::repeat;
 
         let (i, end_or_escape) = self.bytes
             .iter()
@@ -398,6 +404,30 @@ impl<'a> Bytes<'a> {
         }
     }
 
+    fn raw_string(&mut self) -> Result<ParsedStr> {
+        let num_hashes = self.bytes.iter().take_while(|&&b| b == b'#').count();
+        let hashes = &self.bytes[..num_hashes];
+        let _ = self.advance(num_hashes);
+
+        if !self.consume("\"") {
+            return self.err(ParseError::ExpectedString);
+        }
+
+        let ending = [&[b'"'], hashes].concat();
+        let i = self.bytes
+            .windows(num_hashes + 1)
+            .position(|window| window == ending.as_slice())
+            .ok_or(self.error(ParseError::ExpectedStringEnd))?;
+
+        let s = from_utf8(&self.bytes[..i]).map_err(|e| self.error(e.into()))?;
+
+        // Advance by the number of bytes of the string
+        // + `num_hashes` + 1 for the `"`.
+        let _ = self.advance(i + num_hashes + 1);
+
+        Ok(ParsedStr::Slice(s))
+    }
+
     fn test_for(&self, s: &str) -> bool {
         s.bytes()
             .enumerate()


Test patch:
diff --git a/src/de/tests.rs b/src/de/tests.rs
index c07fe342..3ab1334d 100644
--- a/src/de/tests.rs
+++ b/src/de/tests.rs
@@ -93,8 +93,19 @@ fn test_map() {
 #[test]
 fn test_string() {
     let s: String = from_str("\"String\"").unwrap();
-
     assert_eq!("String", s);
+
+    let raw: String = from_str("r\"String\"").unwrap();
+    assert_eq!("String", raw);
+
+    let raw_hashes: String = from_str("r#\"String\"#").unwrap();
+    assert_eq!("String", raw_hashes);
+
+    let raw_hashes_multiline: String = from_str("r#\"String with\nmultiple\nlines\n\"#").unwrap();
+    assert_eq!("String with\nmultiple\nlines\n", raw_hashes_multiline);
+
+    let raw_hashes_quote: String = from_str("r##\"String with \"#\"##").unwrap();
+    assert_eq!("String with \"#", raw_hashes_quote);
 }
 
 #[test]