yamd/parser/
collapsible.rs

1use std::ops::Range;
2
3use crate::{
4    lexer::TokenKind,
5    nodes::{Collapsible, YamdNodes},
6};
7
8use super::{yamd, Parser};
9
10pub(crate) fn collapsible(p: &mut Parser) -> Option<Collapsible> {
11    let start = p.pos();
12    p.next_token();
13    let mut title: Option<Range<usize>> = None;
14    let mut nodes: Option<Vec<YamdNodes>> = None;
15
16    while let Some((t, _)) = p.peek() {
17        match t.kind {
18            TokenKind::Space if title.is_none() => {
19                if let Some((start, end)) = p.advance_until(|t| t.position.column == 0, true) {
20                    title.replace(start + 1..end - 1);
21                } else {
22                    break;
23                }
24            }
25            TokenKind::CollapsibleEnd if nodes.is_some() => {
26                p.next_token();
27                return Some(Collapsible::new(
28                    p.range_to_string(title.expect("title to be initialized")),
29                    nodes.expect("nodes to be initialized"),
30                ));
31            }
32            _ if title.is_some() && nodes.is_none() => {
33                nodes.replace(yamd(p, |t| t.kind == TokenKind::CollapsibleEnd).body);
34            }
35            _ => {
36                break;
37            }
38        }
39    }
40
41    p.backtrack(start);
42    p.flip_to_literal_at(start);
43    None
44}
45
46#[cfg(test)]
47mod tests {
48    use pretty_assertions::assert_eq;
49
50    use crate::{
51        lexer::{Position, Token, TokenKind},
52        nodes::{Collapsible, Heading, Image, Paragraph},
53        parser::{collapsible, Parser},
54    };
55
56    #[test]
57    fn happy_path() {
58        let mut p = Parser::new("{% Title\n# Heading\n\ntext\n\n{% nested\n![a](u)\n%}\n%}");
59        assert_eq!(
60            collapsible(&mut p),
61            Some(Collapsible::new(
62                "Title",
63                vec![
64                    Heading::new(1, vec![String::from("Heading").into()]).into(),
65                    Paragraph::new(vec![String::from("text").into()]).into(),
66                    Collapsible::new("nested", vec![Image::new("a", "u").into()]).into()
67                ]
68            ))
69        );
70    }
71
72    #[test]
73    fn no_title() {
74        let mut p = Parser::new("{%\ntext%}");
75        assert_eq!(collapsible(&mut p), None);
76        assert_eq!(
77            p.peek(),
78            Some((
79                &Token::new(TokenKind::Literal, "{%", Position::default()),
80                0
81            ))
82        );
83    }
84
85    #[test]
86    fn parse_empty() {
87        let mut p = Parser::new("{% Title\n\n%}");
88        assert_eq!(collapsible(&mut p), Some(Collapsible::new("Title", vec![])));
89    }
90
91    #[test]
92    fn no_end_token() {
93        let mut p = Parser::new("{% Title\n# Heading\n\ntext\n\n{% nested\n![a](u)\n%}\n");
94        assert_eq!(collapsible(&mut p), None);
95        assert_eq!(
96            p.peek(),
97            Some((
98                &Token::new(TokenKind::Literal, "{%", Position::default()),
99                0
100            ))
101        );
102    }
103
104    #[test]
105    fn just_heading() {
106        let mut p = Parser::new("{% Title\n# Heading\n%}");
107        assert_eq!(
108            collapsible(&mut p),
109            Some(Collapsible::new(
110                "Title",
111                vec![Heading::new(1, vec![String::from("Heading").into()]).into(),]
112            ))
113        );
114    }
115
116    #[test]
117    fn only_two_tokens() {
118        let mut p = Parser::new("{% ");
119        assert_eq!(collapsible(&mut p), None);
120        assert_eq!(
121            p.peek(),
122            Some((
123                &Token::new(TokenKind::Literal, "{%", Position::default()),
124                0
125            ))
126        );
127    }
128}