]>
Commit | Line | Data |
---|---|---|
8beb2bfa RBR |
1 | #[derive(PartialEq, Eq, Debug)] |
2 | ||
3 | /// Represents one of the six line types in gemtext. | |
4 | /// | |
5 | /// # Examples | |
6 | /// ``` | |
7 | /// use gema_texto::gemini_parser::GeminiLine; | |
8 | /// use gema_texto::gemini_parser::parse; | |
9 | /// | |
10 | /// let gemini_source = "\ | |
11 | /// # Hello | |
12 | /// This is some gemini text! | |
13 | /// => https://test Go to the test page. | |
14 | /// | |
15 | /// This text actually has more linkes | |
16 | /// => https://test2 Go to the second test page. | |
17 | /// => https://test3 Go to the third test page. | |
18 | /// "; | |
19 | /// | |
20 | /// let gemini_lines = parse(&gemini_source); | |
21 | /// // Count the number of links | |
22 | /// let link_count = gemini_lines.iter() | |
23 | /// .filter(|line| matches!(line, GeminiLine::Link(_, _))) | |
24 | /// .count(); | |
25 | /// assert_eq!(link_count, 3); | |
26 | /// ``` | |
27 | pub enum GeminiLine { | |
28 | /// Text line, with a bool that shows whether it's preformatted. | |
29 | Text(String, bool), | |
30 | ||
31 | /// Preformatted toggle line with alt text. | |
32 | /// | |
33 | /// The boolean indicates whether it turned preformatted mode on or off. | |
34 | /// The string is the optional alt-text. | |
35 | PreformattedToggle(bool, String), | |
36 | ||
37 | /// Heading with a number between 1 and 3 inclusive indicating level. | |
38 | Heading(u8, String), | |
39 | ||
40 | /// Link with URL and text. | |
41 | Link(String, String), | |
42 | ||
43 | /// Quote | |
44 | Quote(String), | |
45 | ||
46 | /// List item | |
47 | ListItem(String), | |
48 | } | |
49 | ||
50 | /// Parses gemtext source code into a vector of `GeminiLine` elements. | |
51 | /// | |
52 | /// # Arguments | |
53 | /// * `source` - A string slice that contains the gemtext | |
54 | /// | |
55 | /// # Returns | |
56 | /// A `Vec<GeminiLine>` containing the rendered HTML. | |
57 | /// | |
58 | /// # Examples | |
59 | /// | |
60 | /// ``` | |
61 | /// use std::fs::read_to_string; | |
62 | /// use gema_texto::gemini_parser::parse; | |
63 | /// let gemini_source = "\ | |
64 | /// # Hello | |
65 | /// This is some gemini text! | |
66 | /// => https://test Go to the test page. | |
67 | /// "; | |
68 | /// | |
69 | /// let gemini_lines = parse(&gemini_source); | |
70 | /// | |
71 | /// // Prints the different lines in the array. | |
72 | /// for line in gemini_lines { | |
73 | /// println!("{:#?}", line); | |
74 | /// } | |
75 | /// ``` | |
76 | #[must_use] | |
77 | pub fn parse(source: &str) -> Vec<GeminiLine> { | |
78 | source | |
79 | .lines() | |
80 | .fold((Vec::new(), false), |(mut lines, is_preformatted), line| { | |
81 | let parsed = if is_preformatted { | |
82 | parse_preformatted_line(line) | |
83 | } else { | |
84 | parse_line(line) | |
85 | }; | |
86 | ||
87 | let new_is_preformatted = match parsed { | |
88 | GeminiLine::PreformattedToggle(x, _) => x, | |
89 | _ => is_preformatted, | |
90 | }; | |
91 | ||
92 | lines.push(parsed); | |
93 | (lines, new_is_preformatted) | |
94 | }) | |
95 | .0 | |
96 | } | |
97 | ||
98 | fn parse_preformatted_line(line: &str) -> GeminiLine { | |
99 | match line { | |
100 | s if s.starts_with("```") => GeminiLine::PreformattedToggle(false, String::new()), | |
101 | _ => GeminiLine::Text(line.to_string(), true), | |
102 | } | |
103 | } | |
104 | ||
105 | fn parse_line(line: &str) -> GeminiLine { | |
106 | match line { | |
107 | s if s.starts_with("###") => GeminiLine::Heading(3, s[3..].to_string()), | |
108 | s if s.starts_with("##") => GeminiLine::Heading(2, s[2..].to_string()), | |
109 | s if s.starts_with('#') => GeminiLine::Heading(1, s[1..].to_string()), | |
110 | s if s.starts_with("=>") => { | |
111 | let content = s[2..].trim(); | |
112 | match content.split_once(char::is_whitespace) { | |
113 | Some((url, text)) => { | |
114 | GeminiLine::Link(url.trim().to_string(), text.trim().to_string()) | |
115 | } | |
116 | None => GeminiLine::Link(content.trim().to_string(), String::new()), | |
117 | } | |
118 | } | |
119 | s if s.starts_with("* ") => GeminiLine::ListItem(s[2..].to_string()), | |
120 | s if s.starts_with('>') => GeminiLine::Quote(s[1..].to_string()), | |
121 | s if s.starts_with("```") => GeminiLine::PreformattedToggle(true, s[3..].to_string()), | |
122 | _ => GeminiLine::Text(line.to_string(), false), | |
123 | } | |
124 | } | |
125 | ||
126 | #[cfg(test)] | |
127 | mod tests { | |
128 | use super::*; | |
129 | ||
130 | #[test] | |
131 | fn test_headings() { | |
132 | assert_eq!( | |
133 | parse_line("### Heading"), | |
134 | GeminiLine::Heading(3, " Heading".to_string()) | |
135 | ); | |
136 | assert_eq!( | |
137 | parse_line("## Heading"), | |
138 | GeminiLine::Heading(2, " Heading".to_string()) | |
139 | ); | |
140 | assert_eq!( | |
141 | parse_line("# Heading"), | |
142 | GeminiLine::Heading(1, " Heading".to_string()) | |
143 | ); | |
144 | assert_eq!(parse_line("###"), GeminiLine::Heading(3, String::new())); | |
145 | assert_eq!( | |
146 | parse_line("#####"), | |
147 | GeminiLine::Heading(3, "##".to_string()) | |
148 | ); | |
149 | assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string())); | |
150 | ||
151 | assert_eq!( | |
152 | parse_preformatted_line("### Heading"), | |
153 | GeminiLine::Text("### Heading".to_string(), true) | |
154 | ); | |
155 | assert_eq!( | |
156 | parse_preformatted_line("## Heading"), | |
157 | GeminiLine::Text("## Heading".to_string(), true) | |
158 | ); | |
159 | assert_eq!( | |
160 | parse_preformatted_line("# Heading"), | |
161 | GeminiLine::Text("# Heading".to_string(), true) | |
162 | ); | |
163 | } | |
164 | ||
165 | #[test] | |
166 | fn test_links() { | |
167 | assert_eq!( | |
168 | parse_line("=> https://example.com Link text"), | |
169 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()) | |
170 | ); | |
171 | assert_eq!( | |
172 | parse_line("=> /local/path"), | |
173 | GeminiLine::Link("/local/path".to_string(), String::new()) | |
174 | ); | |
175 | ||
176 | assert_eq!( | |
177 | parse_line("=>"), | |
178 | GeminiLine::Link(String::new(), String::new()) | |
179 | ); | |
180 | assert_eq!( | |
181 | parse_line("=> "), | |
182 | GeminiLine::Link(String::new(), String::new()) | |
183 | ); | |
184 | assert_eq!( | |
185 | parse_line("=> multiple spaces in text"), | |
186 | GeminiLine::Link("multiple".to_string(), "spaces in text".to_string()) | |
187 | ); | |
188 | ||
189 | assert_eq!( | |
190 | parse_preformatted_line("=> https://example.com Link text"), | |
191 | GeminiLine::Text("=> https://example.com Link text".to_string(), true) | |
192 | ); | |
193 | } | |
194 | ||
195 | #[test] | |
196 | fn test_list_items() { | |
197 | assert_eq!( | |
198 | parse_line("* List item"), | |
199 | GeminiLine::ListItem("List item".to_string()) | |
200 | ); | |
201 | ||
202 | assert_eq!(parse_line("* "), GeminiLine::ListItem(String::new())); | |
203 | assert_eq!(parse_line("*"), GeminiLine::Text("*".to_string(), false)); | |
204 | assert_eq!( | |
205 | parse_line("*WithText"), | |
206 | GeminiLine::Text("*WithText".to_string(), false) | |
207 | ); | |
208 | assert_eq!( | |
209 | parse_line("* Multiple spaces"), | |
210 | GeminiLine::ListItem(" Multiple spaces".to_string()) | |
211 | ); | |
212 | } | |
213 | ||
214 | #[test] | |
215 | fn test_quotes() { | |
216 | assert_eq!( | |
217 | parse_line(">Quote text"), | |
218 | GeminiLine::Quote("Quote text".to_string()) | |
219 | ); | |
220 | ||
221 | assert_eq!(parse_line(">"), GeminiLine::Quote(String::new())); | |
222 | assert_eq!(parse_line("> "), GeminiLine::Quote(" ".to_string())); | |
223 | assert_eq!( | |
224 | parse_line(">>Nested"), | |
225 | GeminiLine::Quote(">Nested".to_string()) | |
226 | ); | |
227 | } | |
228 | ||
229 | #[test] | |
230 | fn test_preformatted() { | |
231 | assert_eq!( | |
232 | parse_line("```alt-text"), | |
233 | GeminiLine::PreformattedToggle(true, "alt-text".to_string()) | |
234 | ); | |
235 | ||
236 | assert_eq!( | |
237 | parse_line("```"), | |
238 | GeminiLine::PreformattedToggle(true, String::new()) | |
239 | ); | |
240 | assert_eq!( | |
241 | parse_line("``` "), | |
242 | GeminiLine::PreformattedToggle(true, " ".to_string()) | |
243 | ); | |
244 | assert_eq!( | |
245 | parse_line("````"), | |
246 | GeminiLine::PreformattedToggle(true, "`".to_string()) | |
247 | ); | |
248 | ||
249 | assert_eq!( | |
250 | parse_preformatted_line("```alt-text"), | |
251 | GeminiLine::PreformattedToggle(false, String::new()) | |
252 | ); | |
253 | assert_eq!( | |
254 | parse_preformatted_line("```"), | |
255 | GeminiLine::PreformattedToggle(false, String::new()) | |
256 | ); | |
257 | } | |
258 | ||
259 | #[test] | |
260 | fn test_text() { | |
261 | // Normal case | |
262 | assert_eq!( | |
263 | parse_line("Regular text"), | |
264 | GeminiLine::Text("Regular text".to_string(), false) | |
265 | ); | |
266 | ||
267 | // Edge cases | |
268 | assert_eq!(parse_line(""), GeminiLine::Text(String::new(), false)); | |
269 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
270 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
271 | } | |
272 | ||
273 | #[test] | |
274 | fn test_malformed_input() { | |
275 | assert_eq!( | |
276 | parse_line("= >Not a link"), | |
277 | GeminiLine::Text("= >Not a link".to_string(), false) | |
278 | ); | |
279 | assert_eq!( | |
280 | parse_line("``Not preformatted"), | |
281 | GeminiLine::Text("``Not preformatted".to_string(), false) | |
282 | ); | |
283 | assert_eq!( | |
284 | parse_line("** Not a list"), | |
285 | GeminiLine::Text("** Not a list".to_string(), false) | |
286 | ); | |
287 | } | |
288 | ||
289 | #[test] | |
290 | fn test_full_document() { | |
291 | let input = "\ | |
292 | # Heading 1 | |
293 | ## Heading 2 | |
294 | ### Heading 3 | |
295 | Regular text | |
296 | => https://example.com Link text | |
297 | * List item | |
298 | >Quote | |
299 | ```alt | |
300 | code | |
301 | # Heading 1 | |
302 | ## Heading 2 | |
303 | ### Heading 3 | |
304 | => https://example.com Link text | |
305 | * List item | |
306 | >Quote | |
307 | ```trailing alt"; | |
308 | let result = parse(input); | |
309 | assert_eq!( | |
310 | result, | |
311 | vec![ | |
312 | GeminiLine::Heading(1, " Heading 1".to_string()), | |
313 | GeminiLine::Heading(2, " Heading 2".to_string()), | |
314 | GeminiLine::Heading(3, " Heading 3".to_string()), | |
315 | GeminiLine::Text("Regular text".to_string(), false), | |
316 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()), | |
317 | GeminiLine::ListItem("List item".to_string()), | |
318 | GeminiLine::Quote("Quote".to_string()), | |
319 | GeminiLine::PreformattedToggle(true, "alt".to_string()), | |
320 | GeminiLine::Text("code".to_string(), true), | |
321 | GeminiLine::Text("# Heading 1".to_string(), true), | |
322 | GeminiLine::Text("## Heading 2".to_string(), true), | |
323 | GeminiLine::Text("### Heading 3".to_string(), true), | |
324 | GeminiLine::Text("=> https://example.com Link text".to_string(), true), | |
325 | GeminiLine::Text("* List item".to_string(), true), | |
326 | GeminiLine::Text(">Quote".to_string(), true), | |
327 | GeminiLine::PreformattedToggle(false, String::new()), | |
328 | ] | |
329 | ); | |
330 | } | |
331 | } |