yamd/parser/
collapsible.rs1use 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\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\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}