]>
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), | |
5732d284 | 8 | ListItem(String), |
8d4fac52 RBR |
9 | } |
10 | ||
260e8ec6 | 11 | /// Parses gemtext source code into a vector of GeminiLine elements. |
5732d284 | 12 | /// |
260e8ec6 RBR |
13 | /// # Arguments |
14 | /// * `source` - A string slice that contains the gemtext | |
5732d284 | 15 | /// |
260e8ec6 RBR |
16 | /// # Returns |
17 | /// A `Vec<GeminiLine>` containing the rendered HTML. | |
18 | pub fn parse(source: &str) -> Vec<GeminiLine> { | |
5732d284 RBR |
19 | source |
20 | .lines() | |
21 | .fold((Vec::new(), false), |(mut lines, is_preformatted), line| { | |
22 | let parsed = if is_preformatted { | |
23 | parse_preformatted_line(line) | |
24 | } else { | |
25 | parse_line(line) | |
26 | }; | |
27 | ||
28 | let new_is_preformatted = match parsed { | |
29 | GeminiLine::PreformattedToggle(x, _) => x, | |
30 | _ => is_preformatted, | |
31 | }; | |
32 | ||
33 | lines.push(parsed); | |
34 | (lines, new_is_preformatted) | |
35 | }) | |
260e8ec6 | 36 | .0 |
6a8515fe RBR |
37 | } |
38 | ||
260e8ec6 RBR |
39 | fn parse_preformatted_line(line: &str) -> GeminiLine { |
40 | match line { | |
41 | s if s.starts_with("```") => GeminiLine::PreformattedToggle(false, String::new()), | |
42 | _ => GeminiLine::Text(line.to_string(), true), | |
6a8515fe | 43 | } |
6a8515fe RBR |
44 | } |
45 | ||
260e8ec6 RBR |
46 | fn parse_line(line: &str) -> GeminiLine { |
47 | match line { | |
48 | s if s.starts_with("###") => GeminiLine::Heading(3, s[3..].to_string()), | |
49 | s if s.starts_with("##") => GeminiLine::Heading(2, s[2..].to_string()), | |
50 | s if s.starts_with("#") => GeminiLine::Heading(1, s[1..].to_string()), | |
51 | s if s.starts_with("=>") => { | |
52 | let content = s[2..].trim(); | |
53 | match content.split_once(char::is_whitespace) { | |
5732d284 RBR |
54 | Some((url, text)) => { |
55 | GeminiLine::Link(url.trim().to_string(), text.trim().to_string()) | |
56 | } | |
260e8ec6 | 57 | None => GeminiLine::Link(content.trim().to_string(), String::new()), |
f0739225 | 58 | } |
5732d284 | 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() { | |
5732d284 RBR |
73 | assert_eq!( |
74 | parse_line("### Heading"), | |
75 | GeminiLine::Heading(3, " Heading".to_string()) | |
76 | ); | |
77 | assert_eq!( | |
78 | parse_line("## Heading"), | |
79 | GeminiLine::Heading(2, " Heading".to_string()) | |
80 | ); | |
81 | assert_eq!( | |
82 | parse_line("# Heading"), | |
83 | GeminiLine::Heading(1, " Heading".to_string()) | |
84 | ); | |
260e8ec6 | 85 | assert_eq!(parse_line("###"), GeminiLine::Heading(3, "".to_string())); |
5732d284 RBR |
86 | assert_eq!( |
87 | parse_line("#####"), | |
88 | GeminiLine::Heading(3, "##".to_string()) | |
89 | ); | |
260e8ec6 RBR |
90 | assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string())); |
91 | ||
5732d284 RBR |
92 | assert_eq!( |
93 | parse_preformatted_line("### Heading"), | |
94 | GeminiLine::Text("### Heading".to_string(), true) | |
95 | ); | |
96 | assert_eq!( | |
97 | parse_preformatted_line("## Heading"), | |
98 | GeminiLine::Text("## Heading".to_string(), true) | |
99 | ); | |
100 | assert_eq!( | |
101 | parse_preformatted_line("# Heading"), | |
102 | GeminiLine::Text("# Heading".to_string(), true) | |
103 | ); | |
260e8ec6 RBR |
104 | } |
105 | ||
106 | #[test] | |
107 | fn test_links() { | |
108 | assert_eq!( | |
109 | parse_line("=> https://example.com Link text"), | |
110 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()) | |
111 | ); | |
112 | assert_eq!( | |
113 | parse_line("=> /local/path"), | |
114 | GeminiLine::Link("/local/path".to_string(), "".to_string()) | |
115 | ); | |
116 | ||
117 | assert_eq!( | |
118 | parse_line("=>"), | |
119 | GeminiLine::Link("".to_string(), "".to_string()) | |
120 | ); | |
121 | assert_eq!( | |
122 | parse_line("=> "), | |
123 | GeminiLine::Link("".to_string(), "".to_string()) | |
124 | ); | |
125 | assert_eq!( | |
126 | parse_line("=> multiple spaces in text"), | |
127 | GeminiLine::Link("multiple".to_string(), "spaces in text".to_string()) | |
128 | ); | |
129 | ||
130 | assert_eq!( | |
131 | parse_preformatted_line("=> https://example.com Link text"), | |
132 | GeminiLine::Text("=> https://example.com Link text".to_string(), true) | |
133 | ); | |
134 | } | |
135 | ||
136 | #[test] | |
137 | fn test_list_items() { | |
138 | assert_eq!( | |
139 | parse_line("* List item"), | |
140 | GeminiLine::ListItem("List item".to_string()) | |
141 | ); | |
142 | ||
143 | assert_eq!(parse_line("* "), GeminiLine::ListItem("".to_string())); | |
144 | assert_eq!(parse_line("*"), GeminiLine::Text("*".to_string(), false)); | |
5732d284 RBR |
145 | assert_eq!( |
146 | parse_line("*WithText"), | |
147 | GeminiLine::Text("*WithText".to_string(), false) | |
148 | ); | |
149 | assert_eq!( | |
150 | parse_line("* Multiple spaces"), | |
151 | GeminiLine::ListItem(" Multiple spaces".to_string()) | |
152 | ); | |
260e8ec6 RBR |
153 | } |
154 | ||
155 | #[test] | |
156 | fn test_quotes() { | |
157 | assert_eq!( | |
158 | parse_line(">Quote text"), | |
159 | GeminiLine::Quote("Quote text".to_string()) | |
160 | ); | |
161 | ||
162 | assert_eq!(parse_line(">"), GeminiLine::Quote("".to_string())); | |
163 | assert_eq!(parse_line("> "), GeminiLine::Quote(" ".to_string())); | |
5732d284 RBR |
164 | assert_eq!( |
165 | parse_line(">>Nested"), | |
166 | GeminiLine::Quote(">Nested".to_string()) | |
167 | ); | |
260e8ec6 RBR |
168 | } |
169 | ||
170 | #[test] | |
171 | fn test_preformatted() { | |
172 | assert_eq!( | |
173 | parse_line("```alt-text"), | |
174 | GeminiLine::PreformattedToggle(true, "alt-text".to_string()) | |
175 | ); | |
176 | ||
5732d284 RBR |
177 | assert_eq!( |
178 | parse_line("```"), | |
179 | GeminiLine::PreformattedToggle(true, "".to_string()) | |
180 | ); | |
181 | assert_eq!( | |
182 | parse_line("``` "), | |
183 | GeminiLine::PreformattedToggle(true, " ".to_string()) | |
184 | ); | |
185 | assert_eq!( | |
186 | parse_line("````"), | |
187 | GeminiLine::PreformattedToggle(true, "`".to_string()) | |
188 | ); | |
260e8ec6 RBR |
189 | |
190 | assert_eq!( | |
191 | parse_preformatted_line("```alt-text"), | |
192 | GeminiLine::PreformattedToggle(false, "".to_string()) | |
193 | ); | |
194 | assert_eq!( | |
195 | parse_preformatted_line("```"), | |
196 | GeminiLine::PreformattedToggle(false, "".to_string()) | |
197 | ); | |
260e8ec6 RBR |
198 | } |
199 | ||
200 | #[test] | |
201 | fn test_text() { | |
202 | // Normal case | |
203 | assert_eq!( | |
204 | parse_line("Regular text"), | |
205 | GeminiLine::Text("Regular text".to_string(), false) | |
206 | ); | |
207 | ||
208 | // Edge cases | |
209 | assert_eq!(parse_line(""), GeminiLine::Text("".to_string(), false)); | |
210 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
211 | assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); | |
212 | } | |
213 | ||
214 | #[test] | |
215 | fn test_malformed_input() { | |
5732d284 RBR |
216 | assert_eq!( |
217 | parse_line("= >Not a link"), | |
218 | GeminiLine::Text("= >Not a link".to_string(), false) | |
219 | ); | |
220 | assert_eq!( | |
221 | parse_line("``Not preformatted"), | |
222 | GeminiLine::Text("``Not preformatted".to_string(), false) | |
223 | ); | |
224 | assert_eq!( | |
225 | parse_line("** Not a list"), | |
226 | GeminiLine::Text("** Not a list".to_string(), false) | |
227 | ); | |
260e8ec6 RBR |
228 | } |
229 | ||
230 | #[test] | |
231 | fn test_full_document() { | |
232 | let input = "\ | |
233 | # Heading 1 | |
234 | ## Heading 2 | |
235 | ### Heading 3 | |
236 | Regular text | |
237 | => https://example.com Link text | |
238 | * List item | |
239 | >Quote | |
240 | ```alt | |
241 | code | |
242 | # Heading 1 | |
243 | ## Heading 2 | |
244 | ### Heading 3 | |
245 | => https://example.com Link text | |
246 | * List item | |
247 | >Quote | |
248 | ```trailing alt"; | |
249 | let result = parse(input); | |
5732d284 RBR |
250 | assert_eq!( |
251 | result, | |
252 | vec![ | |
253 | GeminiLine::Heading(1, " Heading 1".to_string()), | |
254 | GeminiLine::Heading(2, " Heading 2".to_string()), | |
255 | GeminiLine::Heading(3, " Heading 3".to_string()), | |
256 | GeminiLine::Text("Regular text".to_string(), false), | |
257 | GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()), | |
258 | GeminiLine::ListItem("List item".to_string()), | |
259 | GeminiLine::Quote("Quote".to_string()), | |
260 | GeminiLine::PreformattedToggle(true, "alt".to_string()), | |
261 | GeminiLine::Text("code".to_string(), true), | |
262 | GeminiLine::Text("# Heading 1".to_string(), true), | |
263 | GeminiLine::Text("## Heading 2".to_string(), true), | |
264 | GeminiLine::Text("### Heading 3".to_string(), true), | |
265 | GeminiLine::Text("=> https://example.com Link text".to_string(), true), | |
266 | GeminiLine::Text("* List item".to_string(), true), | |
267 | GeminiLine::Text(">Quote".to_string(), true), | |
268 | GeminiLine::PreformattedToggle(false, "".to_string()), | |
269 | ] | |
270 | ); | |
8d4fac52 RBR |
271 | } |
272 | } |