layoutcss_parser/
parser.rs

1use std::collections::HashSet;
2use State::*;
3
4use crate::{
5    builder::{generate, LayoutElement},
6    media_query::{extract_breakpoint, MediaQuery},
7};
8
9#[derive(Debug, PartialEq)]
10pub enum State {
11    Resting,
12    InsideTag,
13    ReadingTagName,
14    AfterTagName,
15    ReadingAttributeName,
16    WaitingAttributeValue,
17    ReadingAttributeValue,
18}
19
20pub struct Parser<'a> {
21    pub state: State,
22    pub text: &'a str,
23    pub tag_name_start: Option<usize>,
24    pub tag_name_end: Option<usize>,
25    pub attribute_name_start: Option<usize>,
26    pub attribute_name_end: Option<usize>,
27    pub layout_attribute_value_start: Option<usize>,
28    pub layout_attribute_value_end: Option<usize>,
29    pub layout_breakpoint_attribute_value_start: Option<usize>,
30    pub layout_breakpoint_attribute_value_end: Option<usize>,
31    pub biggest_breakpoint: Option<usize>,
32    pub biggest_breakpoint_value: Option<&'a str>,
33}
34
35impl<'a> Parser<'a> {
36    pub fn new(text: &'a str) -> Self {
37        Parser {
38            state: Resting,
39            text,
40            tag_name_start: None,
41            tag_name_end: None,
42            attribute_name_start: None,
43            attribute_name_end: None,
44            layout_attribute_value_start: None,
45            layout_attribute_value_end: None,
46            layout_breakpoint_attribute_value_start: None,
47            layout_breakpoint_attribute_value_end: None,
48            biggest_breakpoint: None,
49            biggest_breakpoint_value: None,
50        }
51    }
52
53    pub fn reset_indexes(&mut self) {
54        self.tag_name_start = None;
55        self.tag_name_end = None;
56        self.attribute_name_start = None;
57        self.attribute_name_end = None;
58        self.layout_attribute_value_start = None;
59        self.layout_attribute_value_end = None;
60        self.layout_breakpoint_attribute_value_start = None;
61        self.layout_breakpoint_attribute_value_end = None;
62        self.biggest_breakpoint = None;
63        self.biggest_breakpoint_value = None;
64    }
65
66    pub fn tag_name(&self) -> Option<&'a str> {
67        match (self.tag_name_start, self.tag_name_end) {
68            (Some(start), Some(end)) => Some(&self.text[start..=end]),
69            _ => None,
70        }
71    }
72
73    pub fn tag_name_new(&self) -> &'a str {
74        &self.text[self.tag_name_start.unwrap()..=self.tag_name_end.unwrap()]
75    }
76
77    pub fn attribute_name(&self) -> Option<&'a str> {
78        match (self.attribute_name_start, self.attribute_name_end) {
79            (Some(start), Some(end)) => Some(&self.text[start..=end]),
80            _ => None,
81        }
82    }
83
84    pub fn layout_attribute_value(&self) -> Option<&'a str> {
85        match (
86            self.layout_attribute_value_start,
87            self.layout_attribute_value_end,
88        ) {
89            // we have to check if end > start in the case of an empty attribute value like this `class=""`
90            // because in this case end < start and so it will cause an error
91            (Some(start), Some(end)) if end > start => Some(&self.text[start..=end]),
92            _ => None,
93        }
94    }
95
96    pub fn layout_breakpoint_attribute_value(&self) -> Option<&'a str> {
97        match (
98            self.layout_breakpoint_attribute_value_start,
99            self.layout_breakpoint_attribute_value_end,
100        ) {
101
102            // TODO add a test to check if an empty layout breakpoint reset the component correctly
103            // with a center with and-text in the normal layout for example
104            (Some(start), Some(end)) if end > start => Some(&self.text[start..=end]),
105            // this line allows to correctly reset the component when the breakpoint is empty 
106            (Some(start), Some(end)) if end <= start => Some(""),
107            _ => None,
108        }
109    }
110
111    /// update the biggest_breakpoint of the parser only if the new breakpoint is superior
112    /// to the one of the parser or if the parser has None as biggest_breakpoint
113    pub fn update_biggest_breakpoint(&mut self, breakpoint: usize) -> bool {
114        if let Some(parser_biggest_breakpoint) = self.biggest_breakpoint {
115            if breakpoint > parser_biggest_breakpoint {
116                self.biggest_breakpoint = Some(breakpoint);
117                return true;
118            }
119        } else {
120            self.biggest_breakpoint = Some(breakpoint);
121            return true;
122        }
123        false
124    }
125
126    /// return the new state of the parser (without changing the parser's state) based on the current state and input character,
127    /// if None is returned the state hasn't changed
128    pub fn transition(&self, c: char) -> Option<State> {
129        match (&self.state, c) {
130            (Resting, '<') => Some(InsideTag),
131            (InsideTag, c) if c.is_alphabetic() => Some(ReadingTagName),
132            (ReadingTagName, c) if c.is_whitespace() => Some(AfterTagName),
133            (AfterTagName, c) if c.is_alphabetic() => Some(ReadingAttributeName),
134            (ReadingAttributeName, c) if c.is_whitespace() => Some(AfterTagName),
135            (ReadingAttributeName, '=') => Some(WaitingAttributeValue),
136            //TODO curly braces counter
137            (WaitingAttributeValue, '"') => Some(ReadingAttributeValue),
138            (WaitingAttributeValue, c) if c !='"' => Some(AfterTagName),
139            (ReadingAttributeValue, '"') => Some(AfterTagName),
140            (AfterTagName | ReadingTagName | ReadingAttributeName, '>') => Some(Resting),
141            _ => None,
142        }
143    }
144
145    /// parse the given text to generate layout elements and
146    /// add it to the set passed in parameter.
147    pub fn parse(&mut self, elements: &mut HashSet<LayoutElement<'a>>) {
148        for (i, c) in self.text.char_indices() {
149            let new_state = self.transition(c);
150            // if we enter here, the state has changed
151            if let Some(state) = new_state {
152                match (&self.state, &state) {
153                    (_, ReadingTagName) => self.tag_name_start = Some(i),
154                    (ReadingTagName, AfterTagName) => self.tag_name_end = Some(i - 1),
155                    (_, ReadingAttributeName) => self.attribute_name_start = Some(i),
156                    (ReadingAttributeName, AfterTagName | WaitingAttributeValue) => {
157                        self.attribute_name_end = Some(i - 1)
158                    }
159                    (_, ReadingAttributeValue) => {
160                        if let Some(attribute_name) = self.attribute_name() {
161                            if attribute_name == "layout" {
162                                self.layout_attribute_value_start = Some(i + 1);
163                            } else if attribute_name.starts_with("layout") {
164                                self.layout_breakpoint_attribute_value_start = Some(i + 1);
165                            }
166                        }
167                    }
168                    (ReadingAttributeValue, AfterTagName) => {
169                        if let Some(attribute_name) = self.attribute_name() {
170                            if attribute_name == "layout" {
171                                self.layout_attribute_value_end = Some(i - 1);
172                                // when we are processing a media query layout attribute
173                                // we should call generate too but with a MediaQuery
174                                // as parameter
175                            } else if attribute_name.starts_with("layout") {
176                                self.layout_breakpoint_attribute_value_end = Some(i - 1);
177                                if let (Some(breakpoint), Some(attribute_value)) = (
178                                    extract_breakpoint(attribute_name),
179                                    self.layout_breakpoint_attribute_value(),
180                                ) {
181                                    let new_biggest_breakpoint_found =
182                                        self.update_biggest_breakpoint(breakpoint);
183                                    if new_biggest_breakpoint_found {
184                                        self.biggest_breakpoint_value =
185                                            self.layout_breakpoint_attribute_value();
186                                    }
187                                    // because it's a media-query layout attribute we know it will be InferioOrEqualTo
188                                    let mq_new = MediaQuery::InferiorOrEqualTo(breakpoint);
189                                    generate(
190                                        self.tag_name_new(),
191                                        Some(attribute_value),
192                                        Some(mq_new),
193                                        elements,
194                                    );
195                                }
196                            }
197                        }
198                    }
199
200                    // when we are leaving a tag, we generate layout elements
201                    // and we reset the indexes of the parser
202                    (current_state, Resting) => {
203                        // if we were reading tag name at previous state that means
204                        // that the tag contains only its name
205                        // so we need to set tag_name_end
206                        if current_state == &ReadingTagName {
207                            self.tag_name_end = Some(i - 1);
208                        }
209                        if let (Some(tag_name), layout_value) =
210                            (self.tag_name(), self.layout_attribute_value())
211                        {
212                            let mq = if let (Some(biggest_breakpoint), Some(breakpoint_value)) = (
213                                self.biggest_breakpoint,
214                                self.biggest_breakpoint_value,
215                            ) {
216                                Some(MediaQuery::SuperiorTo(biggest_breakpoint, breakpoint_value.to_string()))
217                            } else {
218                                None
219                            };
220                            generate(tag_name, layout_value, mq, elements);
221                        }
222                        self.reset_indexes();
223                    }
224                    _ => {}
225                };
226                // as the state has changed, whe have to update the state of the parser
227                self.state = state;
228            }
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use crate::builder::LayoutElement;
236
237    use super::*;
238
239    // curly braces tests
240    #[test]
241    fn curly_braces_as_attribute_delimiters_working() {
242        let mut set: HashSet<LayoutElement> = HashSet::new();
243        let mut parser =
244            Parser::new("<div class={hello} layout=\"p:3\"");
245        parser.parse(&mut set);
246        assert_eq!(parser.layout_attribute_value_start, Some(27));
247    }
248
249    // media query test
250    #[test]
251    fn media_query_update_biggest_breakpoint_value_of_parser_when_many_breakpoints() {
252        let mut set: HashSet<LayoutElement> = HashSet::new();
253        let mut parser =
254            Parser::new("<div  layout600px=\"p:3\"  layout900px=\"p:7\"  layout700px=\"p:1\"");
255        parser.parse(&mut set);
256        assert_eq!(parser.biggest_breakpoint_value, Some("p:7"));
257    }
258    #[test]
259    fn media_query_update_biggest_breakpoint_value_of_parser() {
260        let mut set: HashSet<LayoutElement> = HashSet::new();
261        let mut parser = Parser::new("<div  layout600px=\"p:3\"");
262        parser.parse(&mut set);
263        assert_eq!(parser.biggest_breakpoint_value, Some("p:3"));
264    }
265
266    #[test]
267    fn media_query_update_biggest_breakpoint_of_parser() {
268        let mut set: HashSet<LayoutElement> = HashSet::new();
269        let mut parser = Parser::new("<div  layout600px=\"p:3\"");
270        parser.parse(&mut set);
271        println!("{:?}", set);
272        assert_eq!(parser.biggest_breakpoint, Some(600));
273    }
274    #[test]
275    fn media_query_only_become_layout_element() {
276        let mut set: HashSet<LayoutElement> = HashSet::new();
277        let mut parser = Parser::new("<div  layout600px=\"p:3\"");
278        parser.parse(&mut set);
279        println!("{:?}", set);
280    }
281    #[test]
282    fn layout_bp_attribute_value_start_and_end_are_correct() {
283        let mut set: HashSet<LayoutElement> = HashSet::new();
284        let mut parser = Parser::new("<row-l layout=\"gap:1\" layout600px=\"gap:2 p:3\"");
285        parser.parse(&mut set);
286        assert_eq!(parser.attribute_name(), Some("layout600px"));
287        assert_eq!(parser.layout_breakpoint_attribute_value_start, Some(35));
288        assert_eq!(parser.layout_breakpoint_attribute_value_end, Some(43));
289    }
290
291    #[test]
292    fn component_without_layout_attribute_generate_css() {
293        let mut set: HashSet<LayoutElement> = HashSet::new();
294        let mut parser = Parser::new("<row-l>");
295        parser.parse(&mut set);
296        assert!(set.len() == 1);
297    }
298
299    #[test]
300    fn test_extract_breakpoint_attribute_is_added_to_breakpoints() {
301        let mut set: HashSet<LayoutElement> = HashSet::new();
302        let mut parser = Parser::new("<row-l layout600px=\"p:2\"");
303        parser.parse(&mut set);
304        assert_eq!(parser.layout_breakpoint_attribute_value_start, Some(20));
305    }
306
307    #[test]
308    fn test_extract_breakpoint_with_nothing_after_at() {
309        let mq_attribute_value = "layout";
310        let result = extract_breakpoint(mq_attribute_value);
311
312        assert_eq!(result, None);
313    }
314
315   
316
317    #[test]
318    fn test_extract_breakpoint_with_correct_formating() {
319        let mq_attribute_value = "layout600px";
320        let result = extract_breakpoint(mq_attribute_value);
321        assert_eq!(result, Some(600));
322    }
323    //parse tests
324
325    #[test]
326    fn when_state_change_for_resting_the_parser_indexes_have_to_be_reset() {
327        let mut set: HashSet<LayoutElement> = HashSet::new();
328        let mut parser = Parser::new("<div layout=\"gap:2\" >");
329        parser.parse(&mut set);
330        assert_eq!(parser.tag_name_start, None);
331        assert_eq!(parser.tag_name_end, None);
332        assert_eq!(parser.attribute_name_start, None);
333        assert_eq!(parser.attribute_name_end, None);
334        assert_eq!(parser.layout_attribute_value_start, None);
335        assert_eq!(parser.layout_attribute_value_end, None);
336    }
337
338    #[test]
339    fn other_attribute_name_than_layout_doesnt_set_attribute_value_start_and_attribute_value_end() {
340        let mut set: HashSet<LayoutElement> = HashSet::new();
341        let mut parser = Parser::new("<div class=\"bonsoir\"");
342        parser.parse(&mut set);
343        assert_eq!(parser.layout_attribute_value_start, None);
344        assert_eq!(parser.layout_attribute_value_end, None);
345    }
346
347    #[test]
348    fn state_changing_from_reading_attribute_value_to_after_tag_name_set_attribute_value_end() {
349        let mut set: HashSet<LayoutElement> = HashSet::new();
350        let mut parser = Parser::new("<div layout=\"bonsoir\"");
351        parser.parse(&mut set);
352        assert_eq!(parser.layout_attribute_value_end, Some(19));
353    }
354
355    #[test]
356    fn check_attribute_value_start_and_end_when_empty_attribute_value() {
357        let mut set: HashSet<LayoutElement> = HashSet::new();
358        let mut parser = Parser::new("<div layout=\"\" ");
359        parser.parse(&mut set);
360        assert_eq!(parser.layout_attribute_value(), None);
361    }
362
363    #[test]
364    fn state_changing_to_reading_attribute_value_set_attribute_value_start() {
365        let mut set: HashSet<LayoutElement> = HashSet::new();
366        let mut parser = Parser::new("<div layout=\" ");
367        parser.parse(&mut set);
368        assert_eq!(parser.layout_attribute_value_start, Some(13));
369    }
370
371    #[test]
372    fn state_changing_from_reading_attribute_name_to_after_tag_name_or_waiting_attribute_value_set_attribute_name_end(
373    ) {
374        let mut set: HashSet<LayoutElement> = HashSet::new();
375        let mut parser = Parser::new("<div class ");
376        parser.parse(&mut set);
377        assert_eq!(parser.attribute_name_end, Some(9));
378    }
379
380    #[test]
381    fn state_to_reading_attribute_name_set_attribute_name_start() {
382        let mut set: HashSet<LayoutElement> = HashSet::new();
383        let mut parser = Parser::new("<div c");
384        parser.parse(&mut set);
385        assert_eq!(parser.attribute_name_start, Some(5));
386    }
387
388    #[test]
389    fn state_changing_from_reading_tag_name_to_after_tag_name_set_tag_name_end() {
390        let mut set: HashSet<LayoutElement> = HashSet::new();
391        let mut parser = Parser::new("<div ");
392        parser.parse(&mut set);
393        assert_eq!(parser.tag_name_end, Some(3));
394    }
395
396    #[test]
397    fn state_changing_to_reading_tag_name_set_tag_name_start() {
398        let mut set: HashSet<LayoutElement> = HashSet::new();
399        let mut parser = Parser::new("<d");
400        parser.parse(&mut set);
401        assert_eq!(parser.tag_name_start, Some(1));
402    }
403    // transition tests
404
405    #[test]
406    fn reading_attribute_value_and_anything_except_double_quote_return_same_state() {
407        let mut parser = Parser::new("");
408        parser.state = ReadingAttributeValue;
409        assert_eq!(parser.transition('i'), None);
410    }
411
412    #[test]
413    fn reading_tag_name_or_attribute_name_and_alphabetic_return_same_state() {
414        let mut parser = Parser::new("");
415
416        parser.state = ReadingTagName;
417        assert_eq!(parser.transition('i'), None);
418
419        parser.state = ReadingAttributeName;
420        assert_eq!(parser.transition('i'), None);
421    }
422
423    #[test]
424    fn reading_attribute_value_and_double_quote_return_after_tag_name() {
425        let mut parser = Parser::new("");
426        parser.state = ReadingAttributeValue;
427        assert_eq!(parser.transition('"'), Some(AfterTagName));
428    }
429
430    #[test]
431    fn waiting_attribute_value_and_double_quote_return_reading_attribute_value() {
432        let mut parser = Parser::new("");
433        parser.state = WaitingAttributeValue;
434        assert_eq!(parser.transition('"'), Some(ReadingAttributeValue));
435    }
436
437    #[test]
438    fn reading_attribute_name_and_equal_return_waiting_attribute_value() {
439        let mut parser = Parser::new("");
440        parser.state = ReadingAttributeName;
441        assert_eq!(parser.transition('='), Some(WaitingAttributeValue));
442    }
443
444    #[test]
445    fn reading_attribute_name_and_right_chevron_return_resting() {
446        let mut parser = Parser::new("");
447        parser.state = ReadingAttributeName;
448        assert_eq!(parser.transition('>'), Some(Resting));
449    }
450
451    #[test]
452    fn reading_tag_name_and_right_chevron_return_resting() {
453        let mut parser = Parser::new("");
454        parser.state = ReadingTagName;
455        assert_eq!(parser.transition('>'), Some(Resting));
456    }
457
458    #[test]
459    fn resting_and_left_chevron_return_inside_tag() {
460        let parser = Parser::new("");
461        assert_eq!(parser.transition('<'), Some(InsideTag));
462    }
463
464    #[test]
465    fn resting_and_a_return_resting() {
466        let parser = Parser::new("");
467        assert_eq!(parser.transition('a'), None);
468    }
469
470    #[test]
471    fn inside_tag_and_alpha_return_reading_tag_name() {
472        let mut parser = Parser::new("");
473        parser.state = InsideTag;
474        assert_eq!(parser.transition('d'), Some(ReadingTagName));
475    }
476
477    #[test]
478    fn reading_tag_name_and_whitespace_return_after_tag_name() {
479        let mut parser = Parser::new("");
480        parser.state = ReadingTagName;
481        assert_eq!(parser.transition(' '), Some(AfterTagName));
482
483        let mut parser = Parser::new("");
484        parser.state = ReadingTagName;
485        assert_eq!(parser.transition('\n'), Some(AfterTagName));
486
487        let mut parser = Parser::new("");
488        parser.state = ReadingTagName;
489        assert_eq!(parser.transition('\t'), Some(AfterTagName));
490    }
491
492    #[test]
493    fn after_tag_name_and_alphabetic_return_reading_attribute_name() {
494        let mut parser = Parser::new("");
495        parser.state = AfterTagName;
496        assert_eq!(parser.transition('c'), Some(ReadingAttributeName));
497    }
498
499    #[test]
500    fn reading_attribute_name_and_whitespace_return_after_tag_name() {
501        let mut parser = Parser::new("");
502        parser.state = ReadingAttributeName;
503        assert_eq!(parser.transition(' '), Some(AfterTagName));
504    }
505
506    #[test]
507    fn after_tag_name_and_right_chevron_return_resting() {
508        let mut parser = Parser::new("");
509        parser.state = AfterTagName;
510        assert_eq!(parser.transition('>'), Some(Resting));
511    }
512}