use std::borrow::Cow;
pub fn can_be_bare(s: &str) -> bool {
if s.is_empty() {
return false;
}
if s.starts_with("//") || s.starts_with("r#") || s.starts_with("<<") {
return false;
}
!s.chars()
.any(|c| matches!(c, '{' | '}' | '(' | ')' | ',' | '"' | '=' | '@') || c.is_whitespace())
}
pub fn count_escapes(s: &str) -> usize {
s.chars()
.filter(|c| matches!(c, '"' | '\\' | '\n' | '\r' | '\t'))
.count()
}
pub fn count_newlines(s: &str) -> usize {
s.chars().filter(|&c| c == '\n').count()
}
pub fn escape_quoted(s: &str) -> Cow<'_, str> {
if !s
.chars()
.any(|c| matches!(c, '"' | '\\' | '\n' | '\r' | '\t') || c.is_ascii_control())
{
return Cow::Borrowed(s);
}
let mut result = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_ascii_control() => {
let code = c as u32;
result.push_str(&format!("\\u{{{code:04x}}}"));
}
c => result.push(c),
}
}
Cow::Owned(result)
}
pub fn unescape_quoted(s: &str) -> Cow<'_, str> {
if !s.contains('\\') {
return Cow::Borrowed(s);
}
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some('u') => {
if chars.peek() == Some(&'{') {
chars.next(); let mut hex = String::new();
while let Some(&c) = chars.peek() {
if c == '}' {
chars.next();
break;
}
hex.push(chars.next().unwrap());
}
if let Ok(code) = u32::from_str_radix(&hex, 16)
&& let Some(ch) = char::from_u32(code)
{
result.push(ch);
}
} else {
let mut hex = String::new();
for _ in 0..4 {
if let Some(&c) = chars.peek() {
if c.is_ascii_hexdigit() {
hex.push(chars.next().unwrap());
} else {
break;
}
}
}
if let Ok(code) = u32::from_str_radix(&hex, 16)
&& let Some(ch) = char::from_u32(code)
{
result.push(ch);
}
}
}
Some(c) => {
result.push('\\');
result.push(c);
}
None => {
result.push('\\');
}
}
} else {
result.push(c);
}
}
Cow::Owned(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_can_be_bare() {
assert!(can_be_bare("localhost"));
assert!(can_be_bare("8080"));
assert!(can_be_bare("hello-world"));
assert!(can_be_bare("https://example.com/path"));
assert!(can_be_bare("true"));
assert!(can_be_bare("false"));
assert!(!can_be_bare("")); assert!(!can_be_bare("hello world")); assert!(!can_be_bare("{braces}")); assert!(!can_be_bare("(parens)")); assert!(!can_be_bare("key=value")); assert!(!can_be_bare("@tag")); assert!(!can_be_bare("//comment")); assert!(!can_be_bare("r#raw")); assert!(!can_be_bare("<<HERE")); }
#[test]
fn test_escape_quoted() {
assert_eq!(escape_quoted("hello"), "hello");
assert_eq!(escape_quoted("hello world"), "hello world");
assert_eq!(escape_quoted("say \"hi\""), "say \\\"hi\\\"");
assert_eq!(escape_quoted("line1\nline2"), "line1\\nline2");
assert_eq!(escape_quoted("path\\to\\file"), "path\\\\to\\\\file");
}
#[test]
fn test_unescape_quoted() {
assert_eq!(unescape_quoted("hello"), "hello");
assert_eq!(unescape_quoted("say \\\"hi\\\""), "say \"hi\"");
assert_eq!(unescape_quoted("line1\\nline2"), "line1\nline2");
assert_eq!(unescape_quoted("path\\\\to\\\\file"), "path\\to\\file");
assert_eq!(unescape_quoted("tab\\there"), "tab\there");
}
#[test]
fn test_roundtrip() {
let cases = ["hello", "hello world", "say \"hi\"", "line1\nline2", "a\\b"];
for case in cases {
let escaped = escape_quoted(case);
let unescaped = unescape_quoted(&escaped);
assert_eq!(unescaped, case, "roundtrip failed for: {case:?}");
}
}
}