use nom::{combinator::map, Slice};
use super::{InternalLexError, InternalLexResult, LexErrorKind, LexInput};
pub(crate) fn unescaped_quoted_string(input: LexInput) -> InternalLexResult<String> {
map(surrounded('"', '"', true), |parsed| {
parsed.replace("\\\"", "\"").replace("\\\\", "\\")
})(input)
}
fn surrounded(
first: char,
last: char,
allow_escaping: bool,
) -> impl Fn(LexInput) -> InternalLexResult<LexInput> {
move |input: LexInput| {
let mut iter = input.char_indices();
let first_char = iter.next().ok_or_else(|| {
nom::Err::Error(InternalLexError::from_kind(
input,
LexErrorKind::UnexpectedEOF,
))
})?;
if first_char != (0, first) {
return Err(nom::Err::Error(InternalLexError::from_kind(
input,
LexErrorKind::ExpectedChar(first),
)));
}
let mut is_escaped: bool = false;
for (i, c) in iter {
if c == '\\' {
is_escaped = !is_escaped;
} else if is_escaped && allow_escaping {
is_escaped = false;
} else if c == last {
return Ok((input.slice(i + 1..), input.slice(1..i)));
}
}
Err(nom::Err::Error(InternalLexError::from_kind(
input,
LexErrorKind::UnexpectedEOF,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instruction::QuotedString;
use nom::Finish;
use nom_locate::LocatedSpan;
use rstest::rstest;
#[rstest]
#[case("\"\"", "", "")]
#[case("\"foo\"", "foo", "")]
#[case("\"\\\"foo\\\"\" extra", "\"foo\"", " extra")]
#[case("\"\\\\\"", "\\", "")]
#[case("\"foo bar (baz) 123\" after", "foo bar (baz) 123", " after")]
#[case(r#""{\"name\": \"quoted json\"}""#, r#"{"name": "quoted json"}"#, "")]
#[case(r#""hello"\n"world""#, "hello", "\\n\"world\"")]
#[case(
"\"a \\\"string\\\" \n with newlines\"",
"a \"string\" \n with newlines",
""
)]
fn string_parser(#[case] input: &str, #[case] output: &str, #[case] leftover: &str) {
let span = LocatedSpan::new(input);
let (remaining, parsed) = unescaped_quoted_string(span).finish().unwrap();
assert_eq!(parsed, output);
assert_eq!(remaining.fragment(), &leftover);
let round_tripped = QuotedString(&parsed).to_string();
assert!(
input.starts_with(&round_tripped),
"expected `{input}` to start with `{round_tripped}`"
);
}
}