1use std::collections::HashSet;
2
3pub fn render_markdown(input: &str) -> String {
4 let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
5 &parse_highlight(&parse_link(&parse_image(&parse_image_size(
6 &parse_underline(&parse_comment(&input.replace("[/]", "<br />"))),
7 )))),
8 ))
9 .replace("$per", "%");
10
11 let mut allowed_attributes = HashSet::new();
12 allowed_attributes.insert("id");
13 allowed_attributes.insert("class");
14 allowed_attributes.insert("ref");
15 allowed_attributes.insert("aria-label");
16 allowed_attributes.insert("lang");
17 allowed_attributes.insert("title");
18 allowed_attributes.insert("align");
19 allowed_attributes.insert("src");
20 allowed_attributes.insert("style");
21
22 tetratto_shared::markdown::clean_html(
23 html.replace("<style>", "<span>:temp_style")
24 .replace("</style>", "</span>:temp_style"),
25 allowed_attributes,
26 )
27 .replace("<span>:temp_style", "<style>")
28 .replace("</span>:temp_style", "</style>")
29}
30
31pub(crate) fn is_numeric(value: &str) -> bool {
32 let mut is_numeric = false;
33
34 for char in value.chars() {
35 is_numeric = char.is_numeric();
36 }
37
38 is_numeric
39}
40
41pub(crate) fn slice(x: &str, range: core::ops::RangeFrom<usize>) -> String {
42 (&x.chars().collect::<Vec<char>>()[range])
43 .iter()
44 .collect::<String>()
45}
46
47fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
48 let mut in_color_buffer = false;
49 let mut in_main_buffer = false;
50 let mut color_buffer = String::new();
51 let mut close_1 = false;
52
53 for (i, char) in line.chars().enumerate() {
54 if close_1 && char != '%' {
55 buffer.push('%');
58 in_main_buffer = false;
59 close_1 = false;
60 }
61
62 match char {
63 '%' => {
64 if in_color_buffer {
65 in_color_buffer = false;
66 in_main_buffer = true;
67 continue;
68 }
69
70 if in_main_buffer {
71 if !close_1 {
73 close_1 = true;
74 continue;
75 }
76
77 output.push_str(&format!(
80 "<span style=\"color: {color_buffer}\" class=\"color_block\">{buffer}</span>"
81 ));
82
83 color_buffer.clear();
84 buffer.clear();
85
86 in_main_buffer = false;
88 close_1 = false;
89 continue;
90 }
91
92 let ahead = slice(line, i..);
95 if !ahead.contains("%%") {
96 buffer.push(char);
98 continue;
99 }
100
101 output.push_str(&buffer);
103 buffer.clear();
104
105 in_color_buffer = true;
107 }
108 ' ' => {
109 if in_color_buffer == true {
110 buffer.push_str(&color_buffer);
111 color_buffer.clear();
112 }
113
114 buffer.push(char);
115 }
116 _ => {
117 if in_color_buffer {
118 color_buffer.push(char)
119 } else {
120 buffer.push(char)
121 }
122 }
123 }
124 }
125}
126
127fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
128 let mut open_1 = false;
129 let mut open_2 = false;
130 let mut close_1 = false;
131 let mut is_open = false;
132
133 for char in line.chars() {
134 if close_1 && char != '=' {
135 buffer.push('=');
136 close_1 = false;
137 }
138
139 match char {
140 '=' => {
141 if !is_open {
142 output.push_str(&buffer);
144 buffer.clear();
145
146 open_1 = true;
148 is_open = true;
149 } else {
150 if open_1 {
151 open_2 = true;
153 open_1 = false;
154 continue;
155 }
156
157 if close_1 {
158 output.push_str(&format!("<mark>{buffer}</mark>\n"));
160 buffer.clear();
161 open_1 = false;
162 open_2 = false;
163 close_1 = false;
164 is_open = false;
165 continue;
166 }
167
168 close_1 = true;
169 }
170 }
171 _ => {
172 if open_1 {
173 open_1 = false;
174 buffer.push('=');
175 }
176
177 if open_2 && is_open {
178 open_2 = false;
179 }
180
181 buffer.push(char);
182 }
183 }
184 }
185}
186
187fn parse_underline_line(output: &mut String, buffer: &mut String, line: &str) {
188 let mut open_1 = false;
189 let mut is_open = false;
190 let mut close_1 = false;
191
192 for char in line.chars() {
193 if open_1 && char != '~' {
194 is_open = false;
195 open_1 = false;
196 buffer.push('!');
197 }
198
199 if close_1 && char != '!' {
200 is_open = false;
201 close_1 = false;
202 buffer.push('~');
203 }
204
205 match char {
206 '~' => {
207 if open_1 {
208 open_1 = false;
209 is_open = true;
210 } else if is_open {
211 close_1 = true;
213 }
214 }
215 '!' => {
216 if close_1 {
217 let mut s: Vec<&str> = buffer.split(";").collect();
219 let text = s.pop().unwrap_or(&"").trim();
220 let mut style = String::new();
221
222 for (i, mut x) in s.iter().enumerate() {
223 if i == 0 {
224 if x == &"default" {
226 x = &"currentColor";
227 }
228
229 style.push_str(&format!("text-decoration-color: {x};"));
230 } else if i == 1 {
231 if x == &"default" {
233 x = &"solid";
234 }
235
236 style.push_str(&format!("text-decoration-style: {x};"));
237 } else if i == 2 {
238 if x == &"default" {
240 x = &"underline";
241 }
242
243 style.push_str(&format!("text-decoration-line: {x};"));
244 } else if i == 3 {
245 if x == &"default" {
247 x = &"1px";
248 }
249
250 style.push_str(&format!("text-decoration-thickness: {x}px;"));
251 }
252 }
253
254 if s.get(1).is_none() {
256 style.push_str(&format!("text-decoration-style: solid;"));
257 }
258
259 if s.get(2).is_none() {
260 style.push_str(&format!("text-decoration-line: underline;"));
261 }
262
263 if s.get(3).is_none() {
264 style.push_str(&format!("text-decoration-thickness: 1px;"));
265 }
266
267 output.push_str(&format!("<span style=\"{style}\">{text}</span>"));
269 buffer.clear();
270
271 open_1 = false;
272 is_open = false;
273 close_1 = false;
274 continue;
275 } else if is_open {
276 buffer.push(char);
277 continue;
278 }
279
280 open_1 = true;
282
283 output.push_str(&buffer);
285 buffer.clear();
286 }
287 _ => buffer.push(char),
288 }
289 }
290}
291
292fn parse_comment_line(output: &mut String, _: &mut String, line: &str) {
293 if line.contains("]:") && line.starts_with("[") {
294 return;
295 }
296
297 if line == "[..]" {
298 output.push_str(" ");
299 return;
300 }
301
302 output.push_str(line);
303}
304
305fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) {
306 let mut image_possible = false;
307 let mut in_image = false;
308 let mut in_size = false;
309 let mut in_size_rhs = false;
310
311 let mut size_lhs = String::new();
312 let mut size_rhs = String::new();
313
314 if !line.contains("{") {
315 output.push_str(line);
316 return;
317 }
318
319 for char in line.chars() {
320 if image_possible && char != '[' {
321 image_possible = false;
322 output.push('!');
323 }
324
325 match char {
326 '[' => {
327 if image_possible {
328 in_image = true;
329 image_possible = false;
330 continue;
331 }
332
333 if in_image {
334 buffer.push(char);
335 } else {
336 output.push(char);
337 }
338 }
339 '{' => {
340 if in_image {
341 in_size = true;
342 continue;
343 }
344
345 if in_image {
346 buffer.push(char);
347 } else {
348 output.push(char);
349 }
350 }
351 ':' => {
352 if in_size {
353 in_size_rhs = true;
354 continue;
355 }
356
357 if in_image {
358 buffer.push(char);
359 } else {
360 output.push(char);
361 }
362 }
363 '}' => {
364 if in_size && in_size_rhs {
365 output.push_str(&format!(
367 "<span style=\"width: {}; height: {}; float: {}\" class=\"img_sizer\">![{buffer}</span>",
368 if is_numeric(&size_lhs) {
369 format!("{size_lhs}px")
370 } else {
371 size_lhs
372 },
373 if is_numeric(&size_rhs) {
374 format!("{size_rhs}px")
375 } else {
376 size_rhs
377 },
378 if buffer.ends_with("#left)") {
379 "left"
380 } else if buffer.ends_with("#right)") {
381 "right"
382 } else {
383 "unset"
384 }
385 ));
386
387 size_lhs = String::new();
388 size_rhs = String::new();
389 in_image = false;
390 in_size = false;
391 in_size_rhs = false;
392 image_possible = false;
393
394 buffer.clear();
395 continue;
396 }
397
398 if in_image {
399 buffer.push(char);
400 } else {
401 output.push(char);
402 }
403 }
404 '!' => {
405 output.push_str(&buffer);
407 buffer.clear();
408
409 image_possible = true
411 }
412 _ => {
413 if in_image {
414 if in_size {
415 if in_size_rhs {
416 size_rhs.push(char);
417 } else {
418 size_lhs.push(char);
419 }
420 } else {
421 buffer.push(char);
422 }
423 } else {
424 output.push(char)
425 }
426 }
427 }
428 }
429}
430
431fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) {
432 let mut image_possible = false;
433 let mut in_image = false;
434 let mut in_alt = false;
435 let mut in_src = false;
436 let mut alt = String::new();
437
438 for char in line.chars() {
439 if image_possible && char != '[' {
440 image_possible = false;
441 output.push('!');
442 }
443
444 match char {
445 '[' => {
446 if image_possible {
447 in_image = true;
448 image_possible = false;
449 in_alt = true;
450 continue;
451 }
452
453 if in_image {
454 buffer.push(char);
455 } else {
456 output.push(char);
457 }
458 }
459 ']' => {
460 if in_alt {
461 in_alt = false;
462 in_src = true;
463 continue;
464 }
465
466 output.push(char);
467 }
468 '(' => {
469 if in_src {
470 continue;
471 }
472
473 if in_image {
474 buffer.push(char);
475 } else {
476 output.push(char);
477 }
478 }
479 ')' => {
480 if in_image {
481 output.push_str(&format!(
483 "<img loading=\"lazy\" alt=\"{alt}\" src=\"{}\" style=\"float: {}\" />",
484 buffer.replace(" ", "$per20"),
485 if buffer.ends_with("#left") {
486 "left"
487 } else if buffer.ends_with("#right") {
488 "right"
489 } else {
490 "unset"
491 }
492 ));
493
494 alt = String::new();
495 in_alt = false;
496 in_src = false;
497 in_image = false;
498 image_possible = false;
499
500 buffer.clear();
501 continue;
502 }
503
504 output.push(char);
505 }
506 '!' => {
507 output.push_str(&buffer);
509 buffer.clear();
510
511 image_possible = true;
513 }
514 _ => {
515 if in_image {
516 if in_alt {
517 alt.push(char)
518 } else {
519 buffer.push(char);
520 }
521 } else {
522 output.push(char)
523 }
524 }
525 }
526 }
527}
528
529fn parse_link_line(output: &mut String, buffer: &mut String, line: &str) {
530 let mut in_link = false;
531 let mut in_text = false;
532 let mut in_src = false;
533 let mut text = String::new();
534
535 for (i, char) in line.chars().enumerate() {
536 match char {
537 '[' => {
538 output.push_str(&buffer);
540 buffer.clear();
541
542 let haystack = slice(line, i..);
544 if !haystack.contains("]") {
545 output.push('[');
546 continue;
547 }
548
549 in_link = true;
551 in_text = true;
552 }
553 ']' => {
554 if in_text {
555 in_text = false;
556 in_src = true;
557 continue;
558 }
559
560 output.push(char);
561 }
562 '(' => {
563 if in_src {
564 continue;
565 }
566
567 if in_link {
568 buffer.push(char);
569 } else {
570 output.push(char);
571 }
572 }
573 ')' => {
574 if in_link {
575 output.push_str(&format!(
577 "<a href=\"{buffer}\" rel=\"noopener noreferrer\">{text}</a>"
578 ));
579
580 text = String::new();
581 in_text = false;
582 in_src = false;
583 in_link = false;
584
585 buffer.clear();
586 continue;
587 }
588
589 output.push(char);
590 }
591 _ => {
592 if in_link {
593 if in_text {
594 text.push(char)
595 } else {
596 buffer.push(char);
597 }
598 } else {
599 output.push(char)
600 }
601 }
602 }
603 }
604}
605
606macro_rules! parser_ignores_pre {
608 ($body:ident, $input:ident) => {{
609 let mut in_pre_block = false;
610 let mut output = String::new();
611 let mut buffer = String::new();
612
613 for line in $input.split("\n") {
614 if line.starts_with("```") | (line == "<style>") | (line == "</style>") {
615 in_pre_block = !in_pre_block;
616 output.push_str(&format!("{line}\n"));
617 continue;
618 }
619
620 if in_pre_block {
621 output.push_str(&format!("{line}\n"));
622 continue;
623 }
624
625 $body(&mut output, &mut buffer, line);
626 output.push_str(&format!("{buffer}\n"));
627 buffer.clear();
628 }
629
630 output
631 }};
632
633 ($body:ident, $input:ident, $id:literal, ..) => {{
634 let mut in_pre_block = false;
635 let mut output = String::new();
636 let mut buffer = String::new();
637 let mut proc_str = String::new();
638 let mut pre_blocks = Vec::new();
639 let mut pre_idx = 0;
640
641 for line in $input.split("\n") {
642 if line.starts_with("```") {
643 in_pre_block = !in_pre_block;
644
645 pre_idx += 1;
646 pre_blocks.push(String::new());
647 pre_blocks[pre_idx - 1] += &(line.to_string() + "\n");
648
649 proc_str += &format!("$pre:{}.{pre_idx}\n", $id);
650 continue;
651 }
652
653 if in_pre_block {
654 pre_blocks[pre_idx - 1] += &(line.to_string() + "\n");
655 continue;
656 }
657
658 proc_str += &(line.to_string() + "\n");
659 }
660
661 $body(&mut output, &mut buffer, &proc_str);
662 output.push_str(&format!("{buffer}\n"));
663 buffer.clear();
664
665 for (mut i, block) in pre_blocks.iter().enumerate() {
666 i += 1;
667
668 if block == "```\n" {
669 output = output.replacen(&format!("$pre:{}.{i}", $id), "", 1);
670 continue;
671 }
672
673 output = output.replacen(&format!("$pre:{}.{i}", $id), &format!("{block}```\n"), 1);
674 }
675
676 output
677 }};
678}
679
680pub fn parse_text_color(input: &str) -> String {
681 parser_ignores_pre!(parse_text_color_line, input, 0, ..)
682}
683
684pub fn parse_highlight(input: &str) -> String {
685 parser_ignores_pre!(parse_highlight_line, input, 1, ..)
686}
687
688pub fn parse_underline(input: &str) -> String {
689 parser_ignores_pre!(parse_underline_line, input, 2, ..)
690}
691
692pub fn parse_comment(input: &str) -> String {
693 parser_ignores_pre!(parse_comment_line, input)
694}
695
696pub fn parse_image_size(input: &str) -> String {
697 parser_ignores_pre!(parse_image_size_line, input)
698}
699
700pub fn parse_image(input: &str) -> String {
701 parser_ignores_pre!(parse_image_line, input)
702}
703
704pub fn parse_link(input: &str) -> String {
705 parser_ignores_pre!(parse_link_line, input)
706}