]> git.r.bdr.sh - rbdr/page/blob - src/gemini_parser.rs
af1a5e0ec37e672c67b53f4855b3917a3b66b86f
[rbdr/page] / src / gemini_parser.rs
1 pub fn parse(source: &str) -> String {
2
3 let lines = source.split("\n");
4 let mut is_preformatted = false;
5
6 let mut block_label: Option<String> = None;
7 let mut html: String = "".to_owned();
8 let mut current_line_type: Option<LineType> = None;
9
10 let mut heading_stack: Vec<u8> = Vec::new();
11 for line in lines {
12 let mut line_type = LineType::Blank;
13 if line.char_indices().count() > 2 {
14 let mut end = line.len();
15 if line.char_indices().count() > 3 {
16 end = line.char_indices().map(|(i, _)| i).nth(3).unwrap();
17 }
18 line_type = identify_line(&line[..end], is_preformatted);
19 }
20 match line_type {
21 LineType::PreformattedToggle => {
22 is_preformatted = !is_preformatted;
23 if is_preformatted && line.char_indices().count() > 3 {
24 block_label = Some(get_partial_line_content(&line_type, line));
25 } else {
26 block_label = None;
27 }
28 },
29 _ => {
30 // Close previous block if needed
31 if let Some(line) = &current_line_type {
32 if line != &line_type && is_block(line) {
33 html.push_str(get_line_closer(line));
34 }
35 }
36
37 // Blocks
38 if is_block(&line_type) {
39 if let Some(line) = &current_line_type {
40 if line != &line_type {
41 html.push_str(&get_line_opener(&line_type, block_label.as_ref()));
42 }
43 } else {
44 html.push_str(&get_line_opener(&line_type, None));
45 }
46
47 let line_content = get_partial_line_content(&line_type, line);
48 html.push_str(&line_content);
49 } else {
50 html.push_str(&get_heading_wrapper(&mut heading_stack, &line_type));
51 html.push_str(&get_full_line_content(&line_type, line));
52 }
53 current_line_type = Some(line_type);
54 },
55 }
56 }
57 if let Some(line) = &current_line_type {
58 if is_block(line) {
59 html.push_str(get_line_closer(line));
60 }
61 }
62 html.push_str(&close_heading_wrapper(&mut heading_stack));
63 html
64 }
65
66 fn is_block(line_type: &LineType) -> bool {
67 return match line_type {
68 LineType::PreformattedText | LineType::ListItem | LineType::Quote => true,
69 _ => false,
70 }
71 }
72
73 fn get_partial_line_content(line_type: &LineType, line: &str) -> String {
74 let encoded_line = line.replace("<", "&lt;").replace(">", "&gt;");
75 return match line_type {
76 LineType::ListItem => format!("<li>{}</li>", encoded_line[2..].trim()),
77 LineType::Quote => encoded_line[1..].trim().to_string(),
78 LineType::PreformattedText => format!("{}\n", encoded_line),
79 LineType::PreformattedToggle => encoded_line[3..].trim().to_string(),
80 _ => "".to_string(),
81 }
82 }
83
84 fn get_full_line_content(line_type: &LineType, line: &str) -> String {
85 let encoded_line = line.replace("<", "&lt;").replace(">", "&gt;");
86 match line_type {
87 LineType::Text => format!("<p>{}</p>\n", encoded_line.trim()),
88 LineType::Blank => "<br/>\n".to_string(),
89 LineType::Link => {
90 let url = get_link_address(line);
91 if url.starts_with("gemini:") {
92 format!("<div><a href=\"{}\">{}</a></div>\n", url, get_link_content(line))
93 } else {
94 format!("<div><a href=\"{}\">{}</a></div>\n", url.replace(".gmi", ".html"), get_link_content(line))
95 }
96 },
97 LineType::Heading1 => format!("<h1>{}</h1>\n", encoded_line[1..].trim()),
98 LineType::Heading2 => format!("<h2>{}</h2>\n", encoded_line[2..].trim()),
99 LineType::Heading3 => format!("<h3>{}</h3>\n", encoded_line[3..].trim()),
100 _ => "".to_string(),
101 }
102 }
103
104 fn get_heading_wrapper(heading_stack: &mut Vec<u8>, line_type: &LineType) -> String {
105 let mut string = String::new();
106 let current_heading: u8 = match line_type {
107 LineType::Heading1 => 1,
108 LineType::Heading2 => 2,
109 LineType::Heading3 => 3,
110 _ => 255
111 };
112
113 if current_heading < 255 {
114 while let Some(open_heading) = heading_stack.pop() {
115 // You just encountered a more important heading.
116 // Put it back. Desist.
117 if open_heading < current_heading {
118 heading_stack.push(open_heading);
119 break;
120 }
121
122 string.push_str("</div>");
123
124 if open_heading == current_heading {
125 break;
126 }
127 }
128 heading_stack.push(current_heading);
129 string.push_str(&format!("<div class=\"h{}\">", current_heading));
130 }
131
132 return string;
133 }
134
135 fn close_heading_wrapper(heading_stack: &mut Vec<u8>) -> String {
136 let mut string = String::new();
137 while let Some(_open_heading) = heading_stack.pop() {
138 string.push_str("</div>");
139 }
140 return string;
141 }
142
143 fn get_line_opener(line_type: &LineType, block_label: Option<&String>) -> String {
144 match line_type {
145 LineType::ListItem => "<ul>".to_string(),
146 LineType::Quote => "<blockquote>".to_string(),
147 LineType::PreformattedText => {
148 if let Some(label) = &block_label {
149 return format!("<pre role=\"img\" aria-label=\"{}\">", label);
150 } else {
151 return "<pre>".to_string();
152 }
153 },
154 _ => "".to_string(),
155 }
156 }
157
158 fn get_line_closer(line_type: &LineType) -> &'static str {
159 match line_type {
160 LineType::ListItem => "</ul>\n",
161 LineType::Quote => "</blockquote>\n",
162 LineType::PreformattedText => "</pre>\n",
163 _ => "",
164 }
165 }
166
167 fn get_link_content(line: &str) -> &str {
168 let components: Vec<&str> = line[2..].trim().splitn(2, " ").collect();
169 if components.len() > 1 {
170 return components[1].trim()
171 }
172 components[0].trim()
173 }
174
175 fn get_link_address(line: &str) -> &str {
176 let components: Vec<&str> = line[2..].trim().splitn(2, " ").collect();
177 components[0].trim()
178 }
179
180 fn identify_line(line: &str, is_preformatted: bool) -> LineType {
181 if line.starts_with("```") {
182 return LineType::PreformattedToggle;
183 }
184 if is_preformatted {
185 return LineType::PreformattedText;
186 }
187 if line.is_empty() {
188 return LineType::Blank;
189 }
190 if line.starts_with("=>") {
191 return LineType::Link;
192 }
193 if line.starts_with("* ") {
194 return LineType::ListItem;
195 }
196 if line.starts_with(">") {
197 return LineType::Quote;
198 }
199 if line.starts_with("###") {
200 return LineType::Heading3;
201 }
202 if line.starts_with("##") {
203 return LineType::Heading2;
204 }
205 if line.starts_with("#") {
206 return LineType::Heading1;
207 }
208
209 LineType::Text
210 }
211
212 #[derive(PartialEq, Eq)]
213 enum LineType {
214 Text,
215 Blank,
216 Link,
217 PreformattedToggle,
218 PreformattedText,
219 Heading1,
220 Heading2,
221 Heading3,
222 ListItem,
223 Quote
224 }