#[must_use]
pub fn is_text_literal(raw: &str) -> bool {
raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"')
}
#[must_use]
pub fn unquote_text(raw: &str) -> String {
if !is_text_literal(raw) {
return raw.to_string();
}
decode_text_body(&raw[1..raw.len() - 1])
}
#[must_use]
pub fn decode_text_body(inner: &str) -> String {
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some(esc) => match decode_escape(esc) {
Some(decoded) => out.push(decoded),
None => {
out.push('\\');
out.push(esc);
}
},
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
#[must_use]
pub fn decode_escape(esc: char) -> Option<char> {
match esc {
'n' => Some('\n'),
't' => Some('\t'),
'r' => Some('\r'),
'"' => Some('"'),
'\\' => Some('\\'),
'0' => Some('\0'),
'{' => Some('{'),
'}' => Some('}'),
_ => None,
}
}
#[must_use]
pub fn quote_text(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\0' => out.push_str("\\0"),
_ => out.push(c),
}
}
out.push('"');
out
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum CharLitError {
Unterminated,
Empty,
TooLong,
}
#[must_use]
pub fn is_char_literal(raw: &str) -> bool {
raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'')
}
pub fn decode_char_literal(raw: &str) -> Result<char, CharLitError> {
if !is_char_literal(raw) {
return Err(CharLitError::Unterminated);
}
let inner = &raw[1..raw.len() - 1];
let mut chars = inner.chars();
let decoded = match chars.next() {
None => return Err(CharLitError::Empty),
Some('\\') => match chars.next() {
None => return Err(CharLitError::Unterminated),
Some(esc) => decode_escape(esc).unwrap_or(esc),
},
Some(c) => c,
};
if chars.next().is_some() {
return Err(CharLitError::TooLong);
}
Ok(decoded)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoting_round_trips_through_the_decoder() {
for s in [
"", "asdf", "\"", "\\", "a\"b\\c", "\n\t\r\0", "{x}", "a\\nb", "héllo",
] {
let quoted = quote_text(s);
assert!(is_text_literal("ed), "quoted is a literal: {quoted:?}");
assert_eq!(unquote_text("ed), s, "round trip of {s:?}");
}
}
#[test]
fn an_empty_text_still_renders_as_something() {
assert_eq!(quote_text(""), "\"\"");
assert_eq!(quote_text("a\nb"), r#""a\nb""#, "and stays on one line");
assert_eq!(quote_text("{x}"), r#""{x}""#);
}
#[test]
fn one_quote_comes_off_each_end() {
assert_eq!(unquote_text(r#""a""#), "a");
assert_eq!(unquote_text(r#""""#), "");
assert_eq!(unquote_text(r#""\"\"""#), "\"\"");
}
#[test]
fn the_six_escapes_decode() {
assert_eq!(unquote_text(r#""\n\t\r\"\\\0""#), "\n\t\r\"\\\0");
}
#[test]
fn an_unknown_escape_is_preserved_verbatim() {
assert_eq!(unquote_text(r#""\q""#), r"\q");
assert_eq!(unquote_text(r#""a\""#), r#"a\"#);
}
#[test]
fn a_non_literal_is_not_a_literal() {
assert!(!is_text_literal("a"));
assert!(!is_text_literal("\""));
assert!(!is_text_literal("\"a"));
assert!(is_text_literal("\"\""));
}
#[test]
fn one_quote_comes_off_each_end_of_a_char() {
assert_eq!(decode_char_literal("'a'"), Ok('a'));
assert_eq!(decode_char_literal("'#'"), Ok('#'));
assert_eq!(decode_char_literal("'\"'"), Ok('"'));
}
#[test]
fn a_char_takes_texts_escapes_plus_the_quote() {
assert_eq!(decode_char_literal(r"'\n'"), Ok('\n'));
assert_eq!(decode_char_literal(r"'\t'"), Ok('\t'));
assert_eq!(decode_char_literal(r"'\r'"), Ok('\r'));
assert_eq!(decode_char_literal(r"'\0'"), Ok('\0'));
assert_eq!(decode_char_literal(r"'\\'"), Ok('\\'));
assert_eq!(decode_char_literal(r#"'\"'"#), Ok('"'));
assert_eq!(decode_char_literal(r"'\''"), Ok('\''));
}
#[test]
fn a_multibyte_scalar_is_one_character() {
assert_eq!(decode_char_literal("'é'"), Ok('é'));
assert_eq!(decode_char_literal("'😀'"), Ok('😀'));
assert_eq!(decode_char_literal("'字'"), Ok('字'));
}
#[test]
fn a_char_literal_names_exactly_one_character() {
assert_eq!(decode_char_literal("''"), Err(CharLitError::Empty));
assert_eq!(decode_char_literal("'ab'"), Err(CharLitError::TooLong));
assert_eq!(decode_char_literal("'éé'"), Err(CharLitError::TooLong));
assert_eq!(decode_char_literal(r"'\na'"), Err(CharLitError::TooLong));
assert_eq!(decode_char_literal("'a"), Err(CharLitError::Unterminated));
assert_eq!(decode_char_literal("'"), Err(CharLitError::Unterminated));
assert_eq!(decode_char_literal(r"'\'"), Err(CharLitError::Unterminated));
}
#[test]
fn an_unknown_char_escape_is_the_escaped_scalar() {
assert_eq!(decode_char_literal(r"'\q'"), Ok('q'));
assert_eq!(unquote_text(r#""\q""#), r"\q");
}
#[test]
fn a_non_char_literal_is_not_a_char_literal() {
assert!(!is_char_literal("a"));
assert!(!is_char_literal("'"));
assert!(!is_char_literal("'a"));
assert!(is_char_literal("''"));
}
}