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