Skip to content

Commit ed3d324

Browse files
authored
Add support for parsing Rust-style string continuation escapes (#617)
1 parent 31529b8 commit ed3d324

4 files changed

Lines changed: 121 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
### Format Changes
10+
11+
- Add support for parsing Rust-style string continuation escapes ([#585](https://github.com/ron-rs/ron/issues/585))
12+
913
## [0.12.2] - 2026-06-22
1014

1115
### Format Changes

docs/grammar.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,24 @@ float_suffix = "f", ("32", "64");
8181
```ebnf
8282
string = string_std | string_raw;
8383
string_std = "\"", { no_double_quotation_marks | string_escape }, "\"";
84-
string_escape = "\\", (escape_ascii | escape_byte | escape_unicode);
84+
string_escape = "\\", (escape_ascii | escape_byte | escape_unicode | escape_line);
8585
string_raw = "r", string_raw_content;
8686
string_raw_content = ("#", string_raw_content, "#") | "\"", { unicode_non_greedy }, "\"";
8787
8888
escape_ascii = "'" | "\"" | "\\" | "n" | "r" | "t" | "0";
8989
escape_byte = "x", digit_hexadecimal, digit_hexadecimal;
9090
escape_unicode = "u", digit_hexadecimal, [digit_hexadecimal, [digit_hexadecimal, [digit_hexadecimal, [digit_hexadecimal, [digit_hexadecimal]]]]];
91+
escape_line = ("\n" | "\r\n"), { "\t" | "\n" | "\r" | " " };
9192
```
9293

94+
> Note: RON supports [Rust-style string continuation escapes]. A backslash
95+
immediately before LF removes that LF and all subsequent spaces, tabs,
96+
carriage returns, and line feeds. RON also accepts CRLF at the continuation
97+
site, treating it as LF. String continuation escapes are supported in both
98+
standard strings and standard byte strings. Raw strings and raw byte strings
99+
keep string continuation escapes as-is. Rust warns when a continuation skips
100+
additional lines; RON accepts them without a warning.
101+
93102
> Note: Raw strings start with an `r`, followed by n `#`s and a quotation mark
94103
`"`. They may contain any characters or escapes (except the end sequence).
95104
A raw string ends with a quotation mark (`"`), followed by n `#`s. n may be
@@ -103,6 +112,7 @@ Raw strings cannot be written in EBNF, as they are context-sensitive.
103112
Also see [the Rust document] about context-sensitivity of raw strings.
104113

105114
[the Rust document]: https://github.com/rust-lang/rust/blob/d046ffddc4bd50e04ffc3ff9f766e2ac71f74d50/src/grammar/raw-string-literal-ambiguity.md
115+
[Rust-style string continuation escapes]: https://doc.rust-lang.org/reference/expressions/literal-expr.html#string-continuation-escapes
106116

107117
## Byte String
108118

@@ -208,4 +218,4 @@ ident_raw = "r", "#", ident_raw_rest, { ident_raw_rest };
208218
ident_raw_rest = ident_std_rest | "." | "+" | "-";
209219
```
210220

211-
> Note: [XID_Start](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Start%3A%5D&abb=on&g=&i=) and [XID_Continue](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Continue%3A%5D&abb=on&g=&i=) refer to Unicode character sets.
221+
> Note: [XID_Start](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Start%3A%5D&abb=on&g=&i=) and [XID_Continue](http://unicode.org/cldr/utility/list-unicodeset.jsp?a=%5B%3AXID_Continue%3A%5D&abb=on&g=&i=) refer to Unicode character sets.

src/parse.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ pub const fn is_whitespace_char(c: char) -> bool {
5050
)
5151
}
5252

53+
const fn is_string_continuation_whitespace(c: char) -> bool {
54+
matches!(c, ' ' | '\t' | '\n' | '\r')
55+
}
56+
5357
#[cfg(feature = "integer128")]
5458
pub(crate) type LargeUInt = u128;
5559
#[cfg(not(feature = "integer128"))]
@@ -1215,16 +1219,18 @@ impl<'a> Parser<'a> {
12151219
loop {
12161220
self.advance_bytes(i + 1);
12171221

1218-
match self.parse_escape(encoding, false)? {
1219-
EscapeCharacter::Ascii(c) => s.push(c),
1220-
EscapeCharacter::Utf8(c) => match c.len_utf8() {
1221-
1 => s.push(c as u8),
1222-
len => {
1223-
let start = s.len();
1224-
s.extend(core::iter::repeat(0).take(len));
1225-
c.encode_utf8(&mut s[start..]);
1226-
}
1227-
},
1222+
if !self.consume_string_continuation() {
1223+
match self.parse_escape(encoding, false)? {
1224+
EscapeCharacter::Ascii(c) => s.push(c),
1225+
EscapeCharacter::Utf8(c) => match c.len_utf8() {
1226+
1 => s.push(c as u8),
1227+
len => {
1228+
let start = s.len();
1229+
s.extend(core::iter::repeat(0).take(len));
1230+
c.encode_utf8(&mut s[start..]);
1231+
}
1232+
},
1233+
}
12281234
}
12291235

12301236
// Unlike the non-escaped case above, searching for '"' and '\\'
@@ -1255,6 +1261,27 @@ impl<'a> Parser<'a> {
12551261
}
12561262
}
12571263

1264+
/// Consumes a string continuation after its leading `\`.
1265+
///
1266+
/// Rust [normalizes CRLF before lexing](https://doc.rust-lang.org/reference/input-format.html#crlf-normalization),
1267+
/// so RON accepts CRLF directly too.
1268+
fn consume_string_continuation(&mut self) -> bool {
1269+
let line_ending_len = if self.check_str("\r\n") {
1270+
2
1271+
} else if self.check_char('\n') {
1272+
1
1273+
} else {
1274+
return false;
1275+
};
1276+
1277+
self.advance_bytes(line_ending_len);
1278+
1279+
let whitespace = self.next_chars_while_len(is_string_continuation_whitespace);
1280+
self.advance_bytes(whitespace);
1281+
1282+
true
1283+
}
1284+
12581285
fn raw_byte_buf(&mut self) -> Result<(ParsedByteStr<'a>, usize)> {
12591286
let num_hashes = self.next_chars_while_len(|c| c == '#');
12601287
let hashes = &self.src()[..num_hashes];

tests/escape.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,71 @@ fn test_nul_in_string() {
9898
check_same("Hello\x00World!".to_owned());
9999
check_same("Hello\u{0}World!".to_owned());
100100
}
101+
102+
#[test]
103+
fn test_string_continuation_escape() {
104+
let cases = [
105+
(concat!("\"foo\\", "\n bar\""), "foobar"),
106+
(concat!("\"foo\\", "\n\n \\nbar\""), "foo\nbar"),
107+
(concat!("\"foo\\", "\r\n \tbar\""), "foobar"),
108+
(concat!("\"foo\\", "\n\r \tbar\""), "foobar"),
109+
(concat!("\"foo\\", "\n\""), "foo"),
110+
];
111+
112+
for (source, expected) in cases {
113+
assert_eq!(from_str::<String>(source).unwrap(), expected);
114+
}
115+
116+
for source in [
117+
concat!("b\"foo\\", "\n bar\""),
118+
concat!("b\"foo\\", "\r\n bar\""),
119+
] {
120+
let bytes = from_str::<bytes::Bytes>(source).unwrap();
121+
assert_eq!(&*bytes, b"foobar");
122+
}
123+
}
124+
125+
#[test]
126+
fn test_string_continuation_whitespace_boundary() {
127+
let retained = "\u{a0}\u{b}\u{c}\u{85}\u{200e}\u{200f}\u{2028}\u{2029}";
128+
let source = ["\"foo\\\n", retained, "bar\""].concat();
129+
let expected = ["foo", retained, "bar"].concat();
130+
131+
assert_eq!(from_str::<String>(&source).unwrap(), expected);
132+
}
133+
134+
#[test]
135+
fn test_string_continuation_rejected_outside_strings() {
136+
for error in [
137+
from_str::<char>(concat!("'\\", "\n'")).unwrap_err().code,
138+
from_str::<u8>(concat!("b'\\", "\n'")).unwrap_err().code,
139+
] {
140+
assert_eq!(error, ron::Error::InvalidEscape("Unknown escape character"));
141+
}
142+
}
143+
144+
#[test]
145+
fn test_string_continuation_raw_strings_are_unchanged() {
146+
let raw = from_str::<String>(concat!("r\"foo\\", "\n bar\"")).unwrap();
147+
assert_eq!(raw, concat!("foo\\", "\n bar"));
148+
149+
let raw_bytes = from_str::<bytes::Bytes>(concat!("br\"foo\\", "\n bar\"")).unwrap();
150+
assert_eq!(&*raw_bytes, concat!("foo\\", "\n bar").as_bytes());
151+
}
152+
153+
#[test]
154+
fn test_string_continuation_errors() {
155+
assert_eq!(
156+
from_str::<String>(concat!("\"foo\\", "\rbar\""))
157+
.unwrap_err()
158+
.code,
159+
ron::Error::InvalidEscape("Unknown escape character")
160+
);
161+
162+
assert_eq!(
163+
from_str::<String>(concat!("\"foo\\", "\n"))
164+
.unwrap_err()
165+
.code,
166+
ron::Error::ExpectedStringEnd
167+
);
168+
}

0 commit comments

Comments
 (0)