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