1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum QuoteState {
9 #[default]
11 None,
12 Opening(char),
14 String(char),
16 Escaping(char),
18 Escaped(char),
20 Closing(char),
22}
23
24pub struct QuoteStateCharIndices<'a> {
27 iter: std::str::CharIndices<'a>,
28 state: QuoteState,
29}
30
31impl<'a> QuoteStateCharIndices<'a> {
32 fn new(string: &'a str) -> Self {
33 Self { iter: string.char_indices(), state: QuoteState::None }
34 }
35 pub fn with_state(mut self, state: QuoteState) -> Self {
36 self.state = state;
37 self
38 }
39}
40
41impl Iterator for QuoteStateCharIndices<'_> {
42 type Item = (QuoteState, usize, char);
43 fn next(&mut self) -> Option<Self::Item> {
44 let (idx, ch) = self.iter.next()?;
45 match self.state {
46 QuoteState::None | QuoteState::Closing(_) => {
47 if ch == '\'' || ch == '"' {
48 self.state = QuoteState::Opening(ch);
49 } else {
50 self.state = QuoteState::None
51 }
52 }
53 QuoteState::String(quote) | QuoteState::Opening(quote) | QuoteState::Escaped(quote) => {
54 if ch == quote {
55 self.state = QuoteState::Closing(quote)
56 } else if ch == '\\' {
57 self.state = QuoteState::Escaping(quote)
58 } else {
59 self.state = QuoteState::String(quote)
60 }
61 }
62 QuoteState::Escaping(quote) => self.state = QuoteState::Escaped(quote),
63 }
64 Some((self.state, idx, ch))
65 }
66}
67
68pub struct QuotedRanges<'a>(QuoteStateCharIndices<'a>);
70
71impl QuotedRanges<'_> {
72 pub fn with_state(mut self, state: QuoteState) -> Self {
73 self.0 = self.0.with_state(state);
74 self
75 }
76}
77
78impl Iterator for QuotedRanges<'_> {
79 type Item = (char, usize, usize);
80 fn next(&mut self) -> Option<Self::Item> {
81 let (quote, start) = loop {
82 let (state, idx, _) = self.0.next()?;
83 match state {
84 QuoteState::Opening(quote) |
85 QuoteState::Escaping(quote) |
86 QuoteState::Escaped(quote) |
87 QuoteState::String(quote) => break (quote, idx),
88 QuoteState::Closing(quote) => return Some((quote, idx, idx)),
89 QuoteState::None => {}
90 }
91 };
92 for (state, idx, _) in self.0.by_ref() {
93 if matches!(state, QuoteState::Closing(_)) {
94 return Some((quote, start, idx))
95 }
96 }
97 None
98 }
99}
100
101pub trait QuotedStringExt {
103 fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_>;
105
106 fn quoted_ranges(&self) -> QuotedRanges<'_> {
108 QuotedRanges(self.quote_state_char_indices())
109 }
110
111 fn is_quoted(&self) -> bool {
114 let mut iter = self.quote_state_char_indices();
115 if !matches!(iter.next(), Some((QuoteState::Opening(_), _, _))) {
116 return false
117 }
118 while let Some((state, _, _)) = iter.next() {
119 if matches!(state, QuoteState::Closing(_)) {
120 return iter.next().is_none()
121 }
122 }
123 false
124 }
125}
126
127impl<T> QuotedStringExt for T
128where
129 T: AsRef<str>,
130{
131 fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> {
132 QuoteStateCharIndices::new(self.as_ref())
133 }
134}
135
136impl QuotedStringExt for str {
137 fn quote_state_char_indices(&self) -> QuoteStateCharIndices<'_> {
138 QuoteStateCharIndices::new(self)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use similar_asserts::assert_eq;
146
147 #[test]
148 fn quote_state_char_indices() {
149 assert_eq!(
150 r#"a'a"\'\"\n\\'a"#.quote_state_char_indices().collect::<Vec<_>>(),
151 vec![
152 (QuoteState::None, 0, 'a'),
153 (QuoteState::Opening('\''), 1, '\''),
154 (QuoteState::String('\''), 2, 'a'),
155 (QuoteState::String('\''), 3, '"'),
156 (QuoteState::Escaping('\''), 4, '\\'),
157 (QuoteState::Escaped('\''), 5, '\''),
158 (QuoteState::Escaping('\''), 6, '\\'),
159 (QuoteState::Escaped('\''), 7, '"'),
160 (QuoteState::Escaping('\''), 8, '\\'),
161 (QuoteState::Escaped('\''), 9, 'n'),
162 (QuoteState::Escaping('\''), 10, '\\'),
163 (QuoteState::Escaped('\''), 11, '\\'),
164 (QuoteState::Closing('\''), 12, '\''),
165 (QuoteState::None, 13, 'a'),
166 ]
167 );
168 }
169
170 #[test]
171 fn quoted_ranges() {
172 let string = r#"testing "double quoted" and 'single quoted' strings"#;
173 assert_eq!(
174 string
175 .quoted_ranges()
176 .map(|(quote, start, end)| (quote, &string[start..=end]))
177 .collect::<Vec<_>>(),
178 vec![('"', r#""double quoted""#), ('\'', "'single quoted'")]
179 );
180 }
181}