Skip to main content

toon/shared/
string_utils.rs

1use crate::shared::constants::{BACKSLASH, CARRIAGE_RETURN, DOUBLE_QUOTE, NEWLINE, TAB};
2
3#[must_use]
4pub fn escape_string(value: &str) -> String {
5    let mut out = String::with_capacity(value.len());
6    for ch in value.chars() {
7        match ch {
8            '\\' => {
9                out.push(BACKSLASH);
10                out.push(BACKSLASH);
11            }
12            '"' => {
13                out.push(BACKSLASH);
14                out.push(DOUBLE_QUOTE);
15            }
16            '\n' => {
17                out.push(BACKSLASH);
18                out.push('n');
19            }
20            '\r' => {
21                out.push(BACKSLASH);
22                out.push('r');
23            }
24            '\t' => {
25                out.push(BACKSLASH);
26                out.push('t');
27            }
28            _ => out.push(ch),
29        }
30    }
31    out
32}
33
34/// Unescape a string literal body.
35///
36/// # Errors
37///
38/// Returns an error when the input contains invalid escape sequences or ends
39/// with a trailing backslash.
40pub fn unescape_string(value: &str) -> Result<String, String> {
41    let mut out = String::with_capacity(value.len());
42    let mut chars = value.chars();
43
44    while let Some(ch) = chars.next() {
45        if ch == BACKSLASH {
46            let next = chars
47                .next()
48                .ok_or_else(|| "Invalid escape sequence: backslash at end of string".to_string())?;
49            match next {
50                'n' => out.push(NEWLINE),
51                't' => out.push(TAB),
52                'r' => out.push(CARRIAGE_RETURN),
53                '\\' => out.push(BACKSLASH),
54                '"' => out.push(DOUBLE_QUOTE),
55                other => {
56                    return Err(format!("Invalid escape sequence: \\{other}"));
57                }
58            }
59        } else {
60            out.push(ch);
61        }
62    }
63
64    Ok(out)
65}
66
67#[must_use]
68pub fn find_closing_quote(content: &str, start: usize) -> Option<usize> {
69    let bytes = content.as_bytes();
70    let mut i = start + 1;
71    while i < bytes.len() {
72        if bytes[i] == BACKSLASH as u8 && i + 1 < bytes.len() {
73            i += 2;
74            continue;
75        }
76        if bytes[i] == DOUBLE_QUOTE as u8 {
77            return Some(i);
78        }
79        i += 1;
80    }
81    None
82}
83
84#[must_use]
85pub fn find_unquoted_char(content: &str, target: char, start: usize) -> Option<usize> {
86    let bytes = content.as_bytes();
87    let mut i = start;
88    let mut in_quotes = false;
89    while i < bytes.len() {
90        let ch = bytes[i] as char;
91        if in_quotes && ch == BACKSLASH && i + 1 < bytes.len() {
92            i += 2;
93            continue;
94        }
95        if ch == DOUBLE_QUOTE {
96            in_quotes = !in_quotes;
97            i += 1;
98            continue;
99        }
100        if ch == target && !in_quotes {
101            return Some(i);
102        }
103        i += 1;
104    }
105    None
106}