#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(super) enum Quote {
Single,
Double,
AnsiC,
}
impl Quote {
const fn from_char(c: char) -> Option<Self> {
match c {
'\'' => Some(Self::Single),
'"' => Some(Self::Double),
_ => None,
}
}
pub const fn as_char(self) -> char {
match self {
Self::Single | Self::AnsiC => '\'',
Self::Double => '"',
}
}
pub const fn opening(self) -> &'static str {
match self {
Self::Single => "'",
Self::Double => "\"",
Self::AnsiC => "$'",
}
}
}
pub(super) struct ScannedChar {
pub index: usize,
pub c: char,
pub unquoted: bool,
pub open_quote: Option<(usize, Quote)>,
pub escapes_next: bool,
}
pub(super) fn scan(input: &str) -> impl Iterator<Item = ScannedChar> + '_ {
let mut open_quote: Option<(usize, Quote)> = None;
let mut escaped = false;
let mut dollar = None;
input.char_indices().map(move |(index, c)| {
let mut unquoted = false;
let after_dollar = dollar.take();
if escaped {
escaped = false;
} else if let Some((_, q)) = open_quote {
if c == '\\' && q != Quote::Single {
escaped = true;
} else if c == q.as_char() {
open_quote = None;
}
} else if c == '\\' {
escaped = true;
} else if let Some(q) = Quote::from_char(c) {
open_quote = Some(match (q, after_dollar) {
(Quote::Single, Some(dollar)) => (dollar, Quote::AnsiC),
_ => (index, q),
});
} else {
unquoted = true;
if c == '$' && after_dollar.is_none() {
dollar = Some(index);
}
}
ScannedChar {
index,
c,
unquoted,
open_quote,
escapes_next: escaped,
}
})
}
pub(super) fn close(text: &mut String) {
let Some(last) = scan(text).last() else {
return;
};
if last.escapes_next {
text.pop();
}
if let Some((_, q)) = last.open_quote {
text.push(q.as_char());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn close_ends_open_quoting() {
let closed = |text: &str| {
let mut text = text.to_owned();
close(&mut text);
text
};
assert_eq!(closed(""), "");
assert_eq!(closed("abc"), "abc");
assert_eq!(closed("'a b"), "'a b'");
assert_eq!(closed(r#""a\"b"#), r#""a\"b""#);
assert_eq!(closed(r"a\"), "a");
assert_eq!(closed(r#""a\"#), r#""a""#);
assert_eq!(closed(r"'a\"), r"'a\'");
assert_eq!(closed(r"a\\"), r"a\\");
assert_eq!(closed(r#""a\\"#), r#""a\\""#);
assert_eq!(closed(r"$'a\'b"), r"$'a\'b'");
assert_eq!(closed(r"$'a\"), r"$'a'");
assert_eq!(closed(r"$$'a\'b"), r"$$'a\'b");
assert_eq!(closed(r"$$$'a"), r"$$$'a'");
assert_eq!(closed(r#""$'a"#), r#""$'a""#);
}
#[test]
fn scan_finds_ansi_c_quotes() {
let open_quote = |text: &str| scan(text).last().and_then(|sc| sc.open_quote);
assert_eq!(open_quote("$'a"), Some((0, Quote::AnsiC)));
assert_eq!(open_quote("x$'a"), Some((1, Quote::AnsiC)));
assert_eq!(open_quote(r"\$'a"), Some((2, Quote::Single)));
assert_eq!(open_quote("$$'a"), Some((2, Quote::Single)));
assert_eq!(open_quote("$$$'a"), Some((2, Quote::AnsiC)));
assert_eq!(open_quote("$$$$'a"), Some((4, Quote::Single)));
assert_eq!(open_quote("$\"a"), Some((1, Quote::Double)));
}
}