]> git.r.bdr.sh - rbdr/page/blame - src/gemini_parser.rs
Address pedantic issues
[rbdr/page] / src / gemini_parser.rs
CommitLineData
260e8ec6
RBR
1#[derive(PartialEq, Eq, Debug)]
2pub 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
8766e441 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.
18pub 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
39fn 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
46fn 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()),
8766e441 50 s if s.starts_with('#') => GeminiLine::Heading(1, s[1..].to_string()),
260e8ec6
RBR
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 60 s if s.starts_with("* ") => GeminiLine::ListItem(s[2..].to_string()),
8766e441 61 s if s.starts_with('>') => GeminiLine::Quote(s[1..].to_string()),
260e8ec6
RBR
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)]
68mod 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
236Regular text
237=> https://example.com Link text
238* List item
239>Quote
240```alt
241code
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}