]>
Commit | Line | Data |
---|---|---|
260e8ec6 RBR |
1 | #[derive(PartialEq, Eq, Debug)] |
2 | pub enum GeminiLine { | |
3 | Text(String, bool), | |
4 | PreformattedToggle(bool, String), | |
5 | Heading(u8, String), | |
6 | Link(String, String), | |
7 | Quote(String), | |
8 | ListItem(String) | |
8d4fac52 RBR |
9 | } |
10 | ||
260e8ec6 RBR |
11 | /// Parses gemtext source code into a vector of GeminiLine elements. |
12 | /// | |
13 | /// # Arguments | |
14 | /// * `source` - A string slice that contains the gemtext | |
15 | /// | |
16 | /// # Returns | |
17 | /// A `Vec<GeminiLine>` containing the rendered HTML. | |
18 | pub fn parse(source: &str) -> Vec<GeminiLine> { | |
19 | source.lines() | |
20 | .fold( | |
21 | (Vec::new(), false), | |
22 | |(mut lines, is_preformatted), line| { | |
23 | let parsed = if is_preformatted { | |
24 | parse_preformatted_line(line) | |
25 | } else { | |
26 | parse_line(line) | |
27 | }; | |
6a8515fe | 28 | |
260e8ec6 RBR |
29 | let new_is_preformatted = match parsed { |
30 | GeminiLine::PreformattedToggle(x, _) => x, | |
31 | _ => is_preformatted | |
32 | }; | |
6a8515fe | 33 | |
260e8ec6 RBR |
34 | lines.push(parsed); |
35 | (lines, new_is_preformatted) | |
6a8515fe | 36 | } |
260e8ec6 RBR |
37 | ) |
38 | .0 | |
6a8515fe RBR |
39 | } |
40 | ||
260e8ec6 RBR |
41 | fn parse_preformatted_line(line: &str) -> GeminiLine { |
42 | match line { | |
43 | s if s.starts_with("```") => GeminiLine::PreformattedToggle(false, String::new()), | |
44 | _ => GeminiLine::Text(line.to_string(), true), | |
6a8515fe | 45 | } |
6a8515fe RBR |
46 | } |
47 | ||
260e8ec6 RBR |
48 | fn parse_line(line: &str) -> GeminiLine { |
49 | match line { | |
50 | s if s.starts_with("###") => GeminiLine::Heading(3, s[3..].to_string()), | |
51 | s if s.starts_with("##") => GeminiLine::Heading(2, s[2..].to_string()), | |
52 | s if s.starts_with("#") => GeminiLine::Heading(1, s[1..].to_string()), | |
53 | s if s.starts_with("=>") => { | |
54 | let content = s[2..].trim(); | |
55 | match content.split_once(char::is_whitespace) { | |
56 | Some((url, text)) => GeminiLine::Link(url.trim().to_string(), text.trim().to_string()), | |
57 | None => GeminiLine::Link(content.trim().to_string(), String::new()), | |
f0739225 RBR |
58 | } |
59 | }, | |
260e8ec6 RBR |
60 | s if s.starts_with("* ") => GeminiLine::ListItem(s[2..].to_string()), |
61 | s if s.starts_with(">") => GeminiLine::Quote(s[1..].to_string()), | |
62 | s if s.starts_with("```") => GeminiLine::PreformattedToggle(true, s[3..].to_string()), | |
63 | _ => GeminiLine::Text(line.to_string(), false), | |
8d4fac52 RBR |
64 | } |
65 | } | |
66 | ||
260e8ec6 RBR |
67 | #[cfg(test)] |
68 | mod tests { | |
69 | use super::*; | |
70 | ||
71 | #[test] | |
72 | fn test_headings() { | |
73 | assert_eq!(parse_line("### Heading"), GeminiLine::Heading(3, " Heading".to_string())); | |
74 | assert_eq!(parse_line("## Heading"), GeminiLine::Heading(2, " Heading".to_string())); | |
75 | assert_eq!(parse_line("# Heading"), GeminiLine::Heading(1, " Heading".to_string())); | |
76 | assert_eq!(parse_line("###"), GeminiLine::Heading(3, "".to_string())); | |
77 | assert_eq!(parse_line("#####"), GeminiLine::Heading(3, "##".to_string())); | |
78 | assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string())); | |
79 | ||
80 | assert_eq!(parse_preformatted_line("### Heading"), GeminiLine::Text("### Heading".to_string(), true)); | |
81 | assert_eq!(parse_preformatted_line("## Heading"), GeminiLine::Text("## Heading".to_string(), true)); | |
82 | assert_eq!(parse_preformatted_line("# Heading"), GeminiLine::Text("# Heading".to_string(), true)); | |
83 | } | |
84 | ||
85 | #[test] | |
86 | fn test_links() { | |
87 | assert_eq!( | |
88 | parse_line("=> https://example.com Link text"), | |
89 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()) | |
90 | ); | |
91 | assert_eq!( | |
92 | parse_line("=> /local/path"), | |
93 | GeminiLine::Link("/local/path".to_string(), "".to_string()) | |
94 | ); | |
95 | ||
96 | assert_eq!( | |
97 | parse_line("=>"), | |
98 | GeminiLine::Link("".to_string(), "".to_string()) | |
99 | ); | |
100 | assert_eq!( | |
101 | parse_line("=> "), | |
102 | GeminiLine::Link("".to_string(), "".to_string()) | |
103 | ); | |
104 | assert_eq!( | |
105 | parse_line("=> multiple spaces in text"), | |
106 | GeminiLine::Link("multiple".to_string(), "spaces in text".to_string()) | |
107 | ); | |
108 | ||
109 | assert_eq!( | |
110 | parse_preformatted_line("=> https://example.com Link text"), | |
111 | GeminiLine::Text("=> https://example.com Link text".to_string(), true) | |
112 | ); | |
113 | } | |
114 | ||
115 | #[test] | |
116 | fn test_list_items() { | |
117 | assert_eq!( | |
118 | parse_line("* List item"), | |
119 | GeminiLine::ListItem("List item".to_string()) | |
120 | ); | |
121 | ||
122 | assert_eq!(parse_line("* "), GeminiLine::ListItem("".to_string())); | |
123 | assert_eq!(parse_line("*"), GeminiLine::Text("*".to_string(), false)); | |
124 | assert_eq!(parse_line("*WithText"), GeminiLine::Text("*WithText".to_string(), false)); | |
125 | assert_eq!(parse_line("* Multiple spaces"), GeminiLine::ListItem(" Multiple spaces".to_string())); | |
126 | } | |
127 | ||
128 | #[test] | |
129 | fn test_quotes() { | |
130 | assert_eq!( | |
131 | parse_line(">Quote text"), | |
132 | GeminiLine::Quote("Quote text".to_string()) | |
133 | ); | |
134 | ||
135 | assert_eq!(parse_line(">"), GeminiLine::Quote("".to_string())); | |
136 | assert_eq!(parse_line("> "), GeminiLine::Quote(" ".to_string())); | |
137 | assert_eq!(parse_line(">>Nested"), GeminiLine::Quote(">Nested".to_string())); | |
138 | } | |
139 | ||
140 | #[test] | |
141 | fn test_preformatted() { | |
142 | assert_eq!( | |
143 | parse_line("```alt-text"), | |
144 | GeminiLine::PreformattedToggle(true, "alt-text".to_string()) | |
145 | ); | |
146 | ||
147 | assert_eq!(parse_line("```"), GeminiLine::PreformattedToggle(true, "".to_string())); | |
148 | assert_eq!(parse_line("``` "), GeminiLine::PreformattedToggle(true, " ".to_string())); | |
149 | assert_eq!(parse_line("````"), GeminiLine::PreformattedToggle(true, "`".to_string())); | |
150 | ||
151 | assert_eq!( | |
152 | parse_preformatted_line("```alt-text"), | |
153 | GeminiLine::PreformattedToggle(false, "".to_string()) | |
154 | ); | |
155 | assert_eq!( | |
156 | parse_preformatted_line("```"), | |
157 | GeminiLine::PreformattedToggle(false, "".to_string()) | |
158 | ); | |
159 | ||
160 | } | |
161 | ||
162 | #[test] | |
163 | fn test_text() { | |
164 | // Normal case | |
165 | assert_eq!( | |
166 | parse_line("Regular text"), | |
167 | GeminiLine::Text("Regular text".to_string(), false) | |
168 | ); | |
169 | ||
170 | // Edge cases | |
171 | assert_eq!(parse_line(""), GeminiLine::Text("".to_string(), false)); | |
172 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
173 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
174 | } | |
175 | ||
176 | #[test] | |
177 | fn test_malformed_input() { | |
178 | assert_eq!(parse_line("= >Not a link"), GeminiLine::Text("= >Not a link".to_string(), false)); | |
179 | assert_eq!(parse_line("``Not preformatted"), GeminiLine::Text("``Not preformatted".to_string(), false)); | |
180 | assert_eq!(parse_line("** Not a list"), GeminiLine::Text("** Not a list".to_string(), false)); | |
181 | } | |
182 | ||
183 | #[test] | |
184 | fn test_full_document() { | |
185 | let input = "\ | |
186 | # Heading 1 | |
187 | ## Heading 2 | |
188 | ### Heading 3 | |
189 | Regular text | |
190 | => https://example.com Link text | |
191 | * List item | |
192 | >Quote | |
193 | ```alt | |
194 | code | |
195 | # Heading 1 | |
196 | ## Heading 2 | |
197 | ### Heading 3 | |
198 | => https://example.com Link text | |
199 | * List item | |
200 | >Quote | |
201 | ```trailing alt"; | |
202 | let result = parse(input); | |
203 | assert_eq!(result, vec![ | |
204 | GeminiLine::Heading(1, " Heading 1".to_string()), | |
205 | GeminiLine::Heading(2, " Heading 2".to_string()), | |
206 | GeminiLine::Heading(3, " Heading 3".to_string()), | |
207 | GeminiLine::Text("Regular text".to_string(), false), | |
208 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()), | |
209 | GeminiLine::ListItem("List item".to_string()), | |
210 | GeminiLine::Quote("Quote".to_string()), | |
211 | GeminiLine::PreformattedToggle(true, "alt".to_string()), | |
212 | GeminiLine::Text("code".to_string(), true), | |
213 | GeminiLine::Text("# Heading 1".to_string(), true), | |
214 | GeminiLine::Text("## Heading 2".to_string(), true), | |
215 | GeminiLine::Text("### Heading 3".to_string(), true), | |
216 | GeminiLine::Text("=> https://example.com Link text".to_string(), true), | |
217 | GeminiLine::Text("* List item".to_string(), true), | |
218 | GeminiLine::Text(">Quote".to_string(), true), | |
219 | GeminiLine::PreformattedToggle(false, "".to_string()), | |
220 | ]); | |
8d4fac52 RBR |
221 | } |
222 | } |