]>
Commit | Line | Data |
---|---|---|
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), | |
9 | } | |
10 | ||
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 | |
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 | }) | |
36 | .0 | |
37 | } | |
38 | ||
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), | |
43 | } | |
44 | } | |
45 | ||
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) { | |
54 | Some((url, text)) => { | |
55 | GeminiLine::Link(url.trim().to_string(), text.trim().to_string()) | |
56 | } | |
57 | None => GeminiLine::Link(content.trim().to_string(), String::new()), | |
58 | } | |
59 | } | |
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), | |
64 | } | |
65 | } | |
66 | ||
67 | #[cfg(test)] | |
68 | mod tests { | |
69 | use super::*; | |
70 | ||
71 | #[test] | |
72 | fn test_headings() { | |
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 | ); | |
85 | assert_eq!(parse_line("###"), GeminiLine::Heading(3, "".to_string())); | |
86 | assert_eq!( | |
87 | parse_line("#####"), | |
88 | GeminiLine::Heading(3, "##".to_string()) | |
89 | ); | |
90 | assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string())); | |
91 | ||
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 | ); | |
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)); | |
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 | ); | |
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())); | |
164 | assert_eq!( | |
165 | parse_line(">>Nested"), | |
166 | GeminiLine::Quote(">Nested".to_string()) | |
167 | ); | |
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 | ||
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 | ); | |
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 | ); | |
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() { | |
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 | ); | |
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); | |
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 | ); | |
271 | } | |
272 | } |