yamd/parser/
heading.rs

1use crate::{
2    lexer::{Token, TokenKind},
3    nodes::Heading,
4};
5
6use super::{anchor, Parser};
7
8pub(crate) fn heading<Callback>(p: &mut Parser<'_>, new_line_check: Callback) -> Option<Heading>
9where
10    Callback: Fn(&Token) -> bool,
11{
12    let start = p.pos();
13    let mut text_start: Option<usize> = None;
14    let mut end_modifier = 0;
15
16    let mut heading = Heading::new(
17        p.next_token()
18            .expect("to have token")
19            .slice
20            .len()
21            .try_into()
22            .expect("to be < 7"),
23        vec![],
24    );
25
26    if let Some(t) = p.next_token() {
27        if t.kind != TokenKind::Space {
28            p.backtrack(start);
29            p.flip_to_literal_at(start);
30            return None;
31        }
32    }
33
34    while let Some((t, pos)) = p.peek() {
35        match t.kind {
36            TokenKind::Terminator => break,
37            TokenKind::LeftSquareBracket => {
38                if let Some(a) = anchor(p) {
39                    if let Some(start) = text_start.take() {
40                        heading.body.push(p.range_to_string(start..pos).into());
41                    }
42                    heading.body.push(a.into());
43                } else {
44                    text_start.get_or_insert(pos);
45                    p.next_token();
46                }
47            }
48            _ if t.position.column == 0 && new_line_check(t) => {
49                end_modifier = 1;
50                text_start.take_if(|start| pos - *start < 2);
51                break;
52            }
53            _ => {
54                text_start.get_or_insert(pos);
55                p.next_token();
56            }
57        }
58    }
59
60    if let Some(start) = text_start.take() {
61        heading
62            .body
63            .push(p.range_to_string(start..p.pos() - end_modifier).into());
64    }
65
66    if !heading.body.is_empty() {
67        return Some(heading);
68    }
69
70    p.backtrack(start);
71    p.flip_to_literal_at(start);
72
73    None
74}
75
76#[cfg(test)]
77mod tests {
78    use pretty_assertions::assert_eq;
79
80    use crate::{
81        lexer::{Position, Token, TokenKind},
82        nodes::{Anchor, Heading},
83        parser::{heading, Parser},
84    };
85
86    #[test]
87    fn happy_path() {
88        let mut p = Parser::new("## heading [a](u) text");
89        assert_eq!(
90            heading(&mut p, |_| false),
91            Some(Heading::new(
92                2,
93                vec![
94                    String::from("heading ").into(),
95                    Anchor::new("a", "u").into(),
96                    String::from(" text").into()
97                ]
98            ))
99        );
100    }
101
102    #[test]
103    fn start_with_anchor() {
104        let mut p = Parser::new("## [a](u) heading");
105        assert_eq!(
106            heading(&mut p, |_| false),
107            Some(Heading::new(
108                2,
109                vec![
110                    Anchor::new("a", "u").into(),
111                    String::from(" heading").into(),
112                ]
113            ))
114        );
115    }
116
117    #[test]
118    fn broken_anchor() {
119        let mut p = Parser::new("## heading [a](u text");
120        assert_eq!(
121            heading(&mut p, |_| false),
122            Some(Heading::new(
123                2,
124                vec![String::from("heading [a](u text").into(),]
125            ))
126        );
127    }
128
129    #[test]
130    fn with_terminator() {
131        let mut p = Parser::new("## heading\n\ntext");
132        assert_eq!(
133            heading(&mut p, |_| false),
134            Some(Heading::new(2, vec![String::from("heading").into()]))
135        );
136    }
137
138    #[test]
139    fn have_no_space_before_text() {
140        let mut p = Parser::new("##heading\n\ntext");
141        assert_eq!(heading(&mut p, |_| false), None);
142        assert_eq!(
143            p.peek(),
144            Some((
145                &Token::new(TokenKind::Literal, "##", Position::default()),
146                0
147            ))
148        );
149    }
150
151    #[test]
152    fn new_line_check() {
153        let mut p = Parser::new("## heading [a](u) text\n ");
154        assert_eq!(
155            heading(&mut p, |t| t.kind == TokenKind::Space),
156            Some(Heading::new(
157                2,
158                vec![
159                    String::from("heading ").into(),
160                    Anchor::new("a", "u").into(),
161                    String::from(" text").into()
162                ]
163            ))
164        );
165    }
166
167    #[test]
168    fn only_one_token() {
169        let mut p = Parser::new("##");
170        assert_eq!(heading(&mut p, |_| false), None);
171        assert_eq!(
172            p.peek(),
173            Some((
174                &Token::new(TokenKind::Literal, "##", Position::default()),
175                0
176            ))
177        );
178    }
179}