yew_ansi/
sequences.rs

1use super::{
2    cursor::CharCursor,
3    graphic_rendition::{self, Sgr},
4};
5
6fn cursor_skip_space(cursor: &mut CharCursor) {
7    // skip: !"#$%&'()*+,-./ (SPACE)
8    cursor.read_while(|c| matches!(c, '\u{0020}'..='\u{002f}'));
9}
10
11/// ANSI Escape Sequence.
12#[derive(Clone, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum Escape {
15    Csi(Csi),
16}
17impl Escape {
18    const ESC: char = '\u{001b}';
19
20    fn parse(cursor: &mut CharCursor) -> Option<Self> {
21        cursor.read_char(Self::ESC)?;
22        cursor_skip_space(cursor);
23        if Csi::peek(cursor) {
24            Csi::parse(cursor).map(Self::Csi)
25        } else {
26            None
27        }
28    }
29}
30
31/// Control sequence.
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[non_exhaustive]
34pub enum Csi {
35    Sgr(Vec<Sgr>),
36}
37impl Csi {
38    const START: char = '[';
39
40    fn peek(cursor: &mut CharCursor) -> bool {
41        cursor.peek_char(Self::START)
42    }
43
44    fn read_params<'a>(cursor: &mut CharCursor<'a>) -> Option<(char, Vec<&'a str>)> {
45        let mut start = cursor.position();
46        let mut end = start;
47        let mut params = Vec::new();
48
49        cursor.read_while(|c| {
50            match c {
51                ';' => {
52                    params.push(start..end);
53                    start = end + c.len_utf8();
54                    end = start;
55                    true
56                }
57                // 0–9:;<=>?
58                '\u{0030}'..='\u{003f}' => {
59                    end += c.len_utf8();
60                    true
61                }
62                _ => false,
63            }
64        });
65
66        if start != end {
67            params.push(start..end);
68        }
69
70        cursor_skip_space(cursor);
71
72        // read method name
73        match cursor.read()? {
74            c @ '\u{0040}'..='\u{007e}' => {
75                let params = params.drain(..).map(|r| cursor.get(r).unwrap()).collect();
76                Some((c, params))
77            }
78            _ => None,
79        }
80    }
81
82    fn parse(cursor: &mut CharCursor) -> Option<Self> {
83        cursor.read_char(Self::START)?;
84        let (method, params) = Self::read_params(cursor)?;
85        match method {
86            'm' => {
87                let params: Vec<usize> = params
88                    .iter()
89                    .map(|p| p.parse())
90                    .collect::<Result<_, _>>()
91                    .ok()?;
92                let sgrs = graphic_rendition::parse_sgrs(params.iter().copied());
93                Some(Self::Sgr(sgrs))
94            }
95            _ => None,
96        }
97    }
98}
99
100/// Read the next sequence in the given slice.
101/// Returns the content before the escape sequence, the escape sequence itself, and everything following it.
102/// The escape sequence can be `None` if it's invalid.
103/// If the slice doesn't contain an escape sequence the entire string slice will be returned as the first item.
104///
105/// ```
106/// # use yew_ansi::*;
107/// let (pre, esc, post) = yew_ansi::read_next_sequence("Hello \u{001b}[32mWorld");
108/// assert_eq!(pre, "Hello ");
109/// assert_eq!(
110///     esc,
111///     Some(Escape::Csi(Csi::Sgr(vec![
112///         Sgr::ColorFgName(ColorName::Green),
113///     ])))
114/// );
115/// assert_eq!(post, "World");
116/// ```
117pub fn read_next_sequence(s: &str) -> (&str, Option<Escape>, &str) {
118    s.find(Escape::ESC).map_or((s, None, ""), |index| {
119        let (pre, post) = s.split_at(index);
120
121        let mut cursor = CharCursor::new(post);
122        let esc = Escape::parse(&mut cursor);
123
124        (pre, esc, cursor.remainder())
125    })
126}
127
128/// Parts of a string containing ANSI escape sequences.
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub enum Marker<'a> {
131    /// Raw text without any escape sequences.
132    Text(&'a str),
133    /// Parsed escape sequence.
134    Sequence(Escape),
135}
136
137/// Iterator yielding markers in a string.
138///
139/// Each item is a [`Marker`].
140///
141/// Returned by [`get_markers`].
142#[must_use = "iterators are lazy and do nothing unless consumed"]
143#[derive(Clone, Debug)]
144pub struct MarkerIter<'a> {
145    remaining: &'a str,
146    buf: Option<Marker<'a>>,
147}
148impl<'a> MarkerIter<'a> {
149    fn new(s: &'a str) -> Self {
150        Self {
151            remaining: s,
152            buf: None,
153        }
154    }
155}
156impl<'a> Iterator for MarkerIter<'a> {
157    type Item = Marker<'a>;
158
159    fn next(&mut self) -> Option<Self::Item> {
160        // handle the marker that might have been buffered by last iteration
161        if let Some(marker) = self.buf.take() {
162            return Some(marker);
163        }
164
165        while !self.remaining.is_empty() {
166            let (pre, esc, post) = read_next_sequence(&self.remaining);
167            self.remaining = post;
168
169            let esc_marker = esc.map(Marker::Sequence);
170
171            if pre.is_empty() {
172                if let Some(marker) = esc_marker {
173                    return Some(marker);
174                }
175
176                // nothing to yield right now, this either means we're at the end or we just skipped over an invalid escape sequence.
177                // explicit "continue" here to make it clear.
178                continue;
179            } else {
180                // store the escape code for the next iteration
181                self.buf = esc_marker;
182                return Some(Marker::Text(pre));
183            }
184        }
185
186        None
187    }
188}
189
190/// Iterate over all [`Marker`]s in given string.
191///
192/// ```
193/// # use yew_ansi::*;
194/// let markers = yew_ansi::get_markers("Hello \u{001b}[32mWorld\u{001b}[39;1m!").collect::<Vec<_>>();
195/// assert_eq!(
196///     markers,
197///     vec![
198///         Marker::Text("Hello "),
199///         Marker::Sequence(Escape::Csi(Csi::Sgr(vec![
200///             Sgr::ColorFgName(ColorName::Green),
201///         ]))),
202///         Marker::Text("World"),
203///         Marker::Sequence(Escape::Csi(Csi::Sgr(vec![
204///             Sgr::ResetColorFg,
205///             Sgr::Bold,
206///         ]))),
207///         Marker::Text("!"),
208///     ]
209/// );
210/// ```
211pub fn get_markers(s: &str) -> MarkerIter {
212    MarkerIter::new(s)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::graphic_rendition::ColorName;
219
220    fn parse(s: &str) -> Option<Escape> {
221        let s = s.replace("CSI ", "\u{001b} [");
222        Escape::parse(&mut CharCursor::new(&s))
223    }
224
225    fn parse_sgr(s: &str) -> Vec<Sgr> {
226        match parse(s) {
227            Some(Escape::Csi(Csi::Sgr(sgr))) => sgr,
228            _ => panic!("expected sgr"),
229        }
230    }
231
232    #[test]
233    fn parsing() {
234        assert_eq!(
235            parse_sgr("CSI 32 m"),
236            vec![Sgr::ColorFgName(ColorName::Green)]
237        );
238        assert_eq!(
239            parse_sgr("CSI 32;1m"),
240            vec![Sgr::ColorFgName(ColorName::Green), Sgr::Bold]
241        );
242    }
243
244    #[test]
245    fn marking() {
246        let markers = get_markers("Hello \u{001b} [33mWorld").collect::<Vec<_>>();
247        assert_eq!(
248            markers,
249            vec![
250                Marker::Text("Hello "),
251                Marker::Sequence(Escape::Csi(Csi::Sgr(vec![Sgr::ColorFgName(
252                    ColorName::Yellow
253                )]))),
254                Marker::Text("World"),
255            ]
256        )
257    }
258}