Skip to main content

tui_markup/parser/
mod.rs

1//! Parsing stage of the compilation process.
2
3pub use error::{Error, ErrorKind};
4pub use item::{Item, ItemC, ItemG};
5use winnow::{
6    ModalResult, Parser,
7    ascii::take_escaped,
8    combinator::{alt, cut_err, delimited, eof, repeat, repeat_till, separated, terminated},
9    error::ErrMode,
10    stream::{ContainsToken, LocatingSlice, Stream},
11    token::{one_of, take_till, take_while},
12};
13
14use crate::parser::ErrorKind::UnescapedChar;
15
16mod error;
17mod item;
18
19#[cfg(test)]
20mod test;
21
22/// Span with location info.
23pub type LSpan<'a> = LocatingSlice<&'a str>;
24
25type ParseResult<'a, O = &'a str> = ModalResult<O, Error<'a>>;
26
27fn alpha_numeric_set() -> impl ContainsToken<char> {
28    ('a'..='z', 'A'..='Z', '0'..='9')
29}
30
31fn one_tag<'i>(i: &mut LSpan<'i>) -> ParseResult<'i> {
32    take_while(1.., (alpha_numeric_set(), ':', '+', '-')).parse_next(i)
33}
34
35fn tag_list<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, Vec<&'i str>> {
36    separated(1.., one_tag, ',').parse_next(i)
37}
38
39fn element<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, Item<'i>> {
40    let start = *i;
41
42    let tags = delimited('<', tag_list, ' ').parse_next(i)?;
43    let parts = cut_err(terminated(items, ">".context(ErrorKind::ElementNotClose)))
44        .parse_next(i)
45        .map_err(|e: ErrMode<Error<'_>>| {
46            e.map(|e| {
47                if e.kind() == Some(ErrorKind::ElementNotClose) {
48                    e.with_input(&start)
49                } else {
50                    e
51                }
52            })
53        })?;
54
55    Ok(Item::Element(tags, parts))
56}
57
58fn special_chars() -> impl ContainsToken<char> {
59    ('<', '>', '\\')
60}
61
62fn plain_text_normal<'i>(i: &mut LSpan<'i>) -> ParseResult<'i> {
63    take_till(1.., special_chars()).parse_next(i)
64}
65
66fn escapable_char<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, char> {
67    one_of(special_chars()).parse_next(i)
68}
69
70fn plain_text<'i>(i: &mut LSpan<'i>) -> ParseResult<'i> {
71    let mut start = *i;
72
73    take_escaped(plain_text_normal, '\\', escapable_char.context(ErrorKind::UnescapableChar)
74    )
75        // empty result is an error, for used in repeat
76        .verify(|s: &str| !s.is_empty())
77        .map_err(|e: ErrMode<Error<'_>>| {
78            e.map(|mut e| {
79                // Handle trailing backslash at end of line: `\` has nothing to escape.
80                // This should be reported as UnescapedChar at the position of `\`.
81                if e.kind() == Some(ErrorKind::UnescapableChar) && e.input.is_empty() {
82                    start.next_slice(start.eof_offset() - 1);
83                    e = e.with_input(&start).with_kind(ErrorKind::UnescapedChar);
84                }
85
86                // 1. When `take_escaped` try '\\' but it fails, means it's `<` or `>' 
87                // it stops and return consumed as OK result.
88                // 2. next loop of items will try parse it by `element`, if it fails, it call this again
89                // and `plain_text_normal` reject it again
90                // 3. it reenter step 1, but this time "consumed" is empty, which will be rejected by
91                // `verify`, So we get a error without kind.
92                // 4. So we use UnescapedChar for this case.
93                if e.kind().is_none() {
94                    e = e.with_kind(UnescapedChar)
95                }
96
97                e
98            })
99        })
100        .parse_next(i)
101}
102
103fn item<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, Item<'i>> {
104    alt((element, plain_text.map(Item::PlainText))).parse_next(i)
105}
106
107fn items<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, Vec<Item<'i>>> {
108    repeat(0.., item).parse_next(i)
109}
110
111fn output<'i>(i: &mut LSpan<'i>) -> ParseResult<'i, Vec<Item<'i>>> {
112    let (result, _) = repeat_till(0.., item, eof).parse_next(i)?;
113    Ok(result)
114}
115
116fn parse_line(line: usize, i: &str) -> Result<Vec<Item<'_>>, Error<'_>> {
117    let mut located = LSpan::new(i);
118
119    output
120        .map_err(|e| {
121            e.into_inner()
122                .expect("incomplete should never happens")
123                .with_line(line)
124        })
125        .parse_next(&mut located)
126}
127
128/// Parse tui markup source into ast.
129///
130/// ## Errors
131///
132/// If input source has invalid syntax.
133pub fn parse(s: &str) -> Result<Vec<Vec<Item<'_>>>, Error<'_>> {
134    s.lines()
135        .enumerate()
136        .map(|(i, line)| parse_line(i, line))
137        .collect()
138}
139
140fn hex_digit() -> impl ContainsToken<char> {
141    ('0'..='9', 'A'..='F', 'a'..='f')
142}
143
144fn hex_color_part(i: &mut &str) -> ModalResult<u8> {
145    repeat(2..=2, one_of(hex_digit()))
146        .map(|_: ()| ())
147        .take()
148        .try_map(|x| u8::from_str_radix(x, 16))
149        .parse_next(i)
150}
151
152/// Parse string of 6 hex digit into r, g, b value.
153pub fn hex_rgb(s: &str) -> Option<(u8, u8, u8)> {
154    let (r, g, b) = (hex_color_part, hex_color_part, hex_color_part)
155        .parse(s)
156        .ok()?;
157
158    // After all three hex pairs are parsed, verify nothing remains.
159    // Since hex_color_part takes 2 hex chars, the input advances by 6 chars.
160    // If the input was exactly 6 hex chars, the remaining is empty.
161    Some((r, g, b))
162}