1use std::collections::HashSet;
2
3pub fn render_markdown(input: &str) -> String {
4 let html = tetratto_shared::markdown::render_markdown_dirty(&parse_page(&parse_details(
5 &parse_text_color(&parse_highlight(&parse_link(&parse_image(
6 &parse_image_size(&parse_toc(&parse_underline(&parse_markdown_element(
7 &parse_comment(&input.replace("[/]", "<br />")),
8 )))),
9 )))),
10 )))
11 .replace("$per", "%");
12
13 let mut allowed_attributes = HashSet::new();
14 allowed_attributes.insert("id");
15 allowed_attributes.insert("class");
16 allowed_attributes.insert("ref");
17 allowed_attributes.insert("aria-label");
18 allowed_attributes.insert("lang");
19 allowed_attributes.insert("title");
20 allowed_attributes.insert("align");
21 allowed_attributes.insert("src");
22 allowed_attributes.insert("style");
23 allowed_attributes.insert("controls");
24 allowed_attributes.insert("autoplay");
25 allowed_attributes.insert("loop");
26
27 tetratto_shared::markdown::clean_html(
28 html.replace("<style>", "<span>:temp_style")
29 .replace("</style>", "</span>:temp_style")
30 .replace("<audio", ":temp_audio<span")
31 .replace("</audio>", "</span>:temp_audio"),
32 allowed_attributes,
33 )
34 .replace("<span>:temp_style", "<style>")
35 .replace("</span>:temp_style", "</style>")
36 .replace(":temp_audio<span", "<audio")
37 .replace("</span>:temp_audio", "</audio>")
38}
39
40pub(crate) fn is_numeric(value: &str) -> bool {
41 let mut is_numeric = false;
42
43 for char in value.chars() {
44 is_numeric = char.is_numeric();
45 }
46
47 is_numeric
48}
49
50pub(crate) fn slice(x: &str, range: core::ops::RangeFrom<usize>) -> String {
51 (&x.chars().collect::<Vec<char>>()[range])
52 .iter()
53 .collect::<String>()
54}
55
56fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
57 let mut in_color_buffer = false;
58 let mut in_main_buffer = false;
59 let mut color_buffer = String::new();
60 let mut close_1 = false;
61
62 for (i, char) in line.chars().enumerate() {
63 if close_1 && char != '%' {
64 buffer.push('%');
67 in_main_buffer = false;
68 close_1 = false;
69 }
70
71 match char {
72 '%' => {
73 if in_color_buffer {
74 in_color_buffer = false;
75 in_main_buffer = true;
76 continue;
77 }
78
79 if in_main_buffer {
80 if !close_1 {
82 close_1 = true;
83 continue;
84 }
85
86 output.push_str(&format!(
89 "<span style=\"color: {color_buffer}\" class=\"color_block\">{buffer}</span>"
90 ));
91
92 color_buffer.clear();
93 buffer.clear();
94
95 in_main_buffer = false;
97 close_1 = false;
98 continue;
99 }
100
101 let ahead = slice(line, i..);
104 if !ahead.contains("%%") {
105 buffer.push(char);
107 continue;
108 }
109
110 output.push_str(&buffer);
112 buffer.clear();
113
114 in_color_buffer = true;
116 }
117 ' ' => {
118 if in_color_buffer == true {
119 buffer.push_str(&color_buffer);
120 color_buffer.clear();
121 }
122
123 buffer.push(char);
124 }
125 _ => {
126 if in_color_buffer {
127 color_buffer.push(char)
128 } else {
129 buffer.push(char)
130 }
131 }
132 }
133 }
134}
135
136fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
137 let mut open_1 = false;
138 let mut open_2 = false;
139 let mut close_1 = false;
140 let mut is_open = false;
141
142 for char in line.chars() {
143 if close_1 && char != '=' {
144 buffer.push('=');
145 close_1 = false;
146 }
147
148 if open_1 && char != '=' {
149 buffer.push('=');
150 open_1 = false;
151 is_open = false;
152 }
153
154 match char {
155 '=' => {
156 if !is_open {
157 output.push_str(&buffer);
159 buffer.clear();
160
161 open_1 = true;
163 is_open = true;
164 } else {
165 if open_1 {
166 open_2 = true;
168 open_1 = false;
169 continue;
170 }
171
172 if close_1 {
173 output.push_str(&format!("<mark>{buffer}</mark>\n"));
175 buffer.clear();
176 open_1 = false;
177 open_2 = false;
178 close_1 = false;
179 is_open = false;
180 continue;
181 }
182
183 close_1 = true;
184 }
185 }
186 _ => {
187 if open_1 {
188 open_1 = false;
189 buffer.push('=');
190 }
191
192 if open_2 && is_open {
193 open_2 = false;
194 }
195
196 buffer.push(char);
197 }
198 }
199 }
200}
201
202fn parse_underline_line(output: &mut String, buffer: &mut String, line: &str) {
203 let mut open_1 = false;
204 let mut is_open = false;
205 let mut close_1 = false;
206
207 for char in line.chars() {
208 if open_1 && char != '~' {
209 is_open = false;
210 open_1 = false;
211
212 if char == '[' {
213 buffer.push('!');
215 } else {
216 buffer.push_str("!");
217 }
218 }
219
220 if close_1 && char != '!' {
221 is_open = false;
222 close_1 = false;
223 buffer.push('~');
224 }
225
226 match char {
227 '~' => {
228 if open_1 {
229 open_1 = false;
230 is_open = true;
231 } else if is_open {
232 close_1 = true;
234 }
235 }
236 '!' => {
237 if close_1 {
238 let mut s: Vec<&str> = buffer.split(";").collect();
240 let text = s.pop().unwrap_or(&"").trim();
241 let mut style = String::new();
242
243 for (i, mut x) in s.iter().enumerate() {
244 if i == 0 {
245 if x == &"default" {
247 x = &"currentColor";
248 }
249
250 style.push_str(&format!("text-decoration-color: {x};"));
251 } else if i == 1 {
252 if x == &"default" {
254 x = &"solid";
255 }
256
257 style.push_str(&format!("text-decoration-style: {x};"));
258 } else if i == 2 {
259 if x == &"default" {
261 x = &"underline";
262 }
263
264 style.push_str(&format!("text-decoration-line: {x};"));
265 } else if i == 3 {
266 if x == &"default" {
268 x = &"1px";
269 }
270
271 style.push_str(&format!("text-decoration-thickness: {x}px;"));
272 }
273 }
274
275 if s.get(1).is_none() {
277 style.push_str(&format!("text-decoration-style: solid;"));
278 }
279
280 if s.get(2).is_none() {
281 style.push_str(&format!("text-decoration-line: underline;"));
282 }
283
284 if s.get(3).is_none() {
285 style.push_str(&format!("text-decoration-thickness: 1px;"));
286 }
287
288 output.push_str(&format!("<span style=\"{style}\">{text}</span>"));
290 buffer.clear();
291
292 open_1 = false;
293 is_open = false;
294 close_1 = false;
295 continue;
296 } else if is_open {
297 buffer.push(char);
298 continue;
299 }
300
301 open_1 = true;
303
304 output.push_str(&buffer);
306 buffer.clear();
307 }
308 _ => buffer.push(char),
309 }
310 }
311}
312
313fn parse_comment_line(output: &mut String, _: &mut String, line: &str) {
314 if line.contains("]:") && line.starts_with("[") {
315 return;
316 }
317
318 if line == "[..]" {
319 output.push_str(" ");
320 return;
321 }
322
323 output.push_str(line);
324}
325
326fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) {
327 let mut image_possible = false;
328 let mut in_image = false;
329 let mut in_size = false;
330 let mut in_size_rhs = false;
331
332 let mut size_lhs = String::new();
333 let mut size_rhs = String::new();
334
335 if !line.contains("{") {
336 output.push_str(line);
337 return;
338 }
339
340 for char in line.chars() {
341 if image_possible && char != '[' {
342 image_possible = false;
343 output.push('!');
344 }
345
346 match char {
347 '[' => {
348 if image_possible {
349 in_image = true;
350 image_possible = false;
351 continue;
352 }
353
354 if in_image {
355 buffer.push(char);
356 } else {
357 output.push(char);
358 }
359 }
360 '{' => {
361 if in_image {
362 in_size = true;
363 continue;
364 }
365
366 if in_image {
367 buffer.push(char);
368 } else {
369 output.push(char);
370 }
371 }
372 ':' => {
373 if in_size {
374 in_size_rhs = true;
375 continue;
376 }
377
378 if in_image {
379 buffer.push(char);
380 } else {
381 output.push(char);
382 }
383 }
384 '}' => {
385 if in_size && in_size_rhs {
386 output.push_str(&format!(
388 "<span style=\"width: {}; height: {}; float: {}\" class=\"img_sizer\">![{buffer}</span>",
389 if is_numeric(&size_lhs) {
390 format!("{size_lhs}px")
391 } else {
392 size_lhs
393 },
394 if is_numeric(&size_rhs) {
395 format!("{size_rhs}px")
396 } else {
397 size_rhs
398 },
399 if buffer.ends_with("#left)") {
400 "left"
401 } else if buffer.ends_with("#right)") {
402 "right"
403 } else {
404 "unset"
405 }
406 ));
407
408 size_lhs = String::new();
409 size_rhs = String::new();
410 in_image = false;
411 in_size = false;
412 in_size_rhs = false;
413 image_possible = false;
414
415 buffer.clear();
416 continue;
417 }
418
419 if in_image {
420 buffer.push(char);
421 } else {
422 output.push(char);
423 }
424 }
425 '!' => {
426 output.push_str(&buffer);
428 buffer.clear();
429
430 image_possible = true
432 }
433 _ => {
434 if in_image {
435 if in_size {
436 if in_size_rhs {
437 size_rhs.push(char);
438 } else {
439 size_lhs.push(char);
440 }
441 } else {
442 buffer.push(char);
443 }
444 } else {
445 output.push(char)
446 }
447 }
448 }
449 }
450}
451
452fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) {
453 let mut image_possible = false;
454 let mut in_image = false;
455 let mut in_alt = false;
456 let mut in_src = false;
457 let mut alt = String::new();
458
459 for char in line.chars() {
460 if image_possible && char != '[' {
461 image_possible = false;
462 output.push('!');
463 }
464
465 match char {
466 '[' => {
467 if image_possible {
468 in_image = true;
469 image_possible = false;
470 in_alt = true;
471 continue;
472 }
473
474 if in_image {
475 buffer.push(char);
476 } else {
477 output.push(char);
478 }
479 }
480 ']' => {
481 if in_alt {
482 in_alt = false;
483 in_src = true;
484 continue;
485 }
486
487 output.push(char);
488 }
489 '(' => {
490 if in_src {
491 continue;
492 }
493
494 if in_image {
495 buffer.push(char);
496 } else {
497 output.push(char);
498 }
499 }
500 ')' => {
501 if in_image {
502 output.push_str(&format!(
504 "<img loading=\"lazy\" alt=\"{alt}\" src=\"{}\" style=\"float: {}\" />",
505 buffer.replace(" ", "$per20"),
506 if buffer.ends_with("#left") {
507 "left"
508 } else if buffer.ends_with("#right") {
509 "right"
510 } else {
511 "unset"
512 }
513 ));
514
515 alt = String::new();
516 in_alt = false;
517 in_src = false;
518 in_image = false;
519 image_possible = false;
520
521 buffer.clear();
522 continue;
523 }
524
525 output.push(char);
526 }
527 '!' => {
528 output.push_str(&buffer);
530 buffer.clear();
531
532 image_possible = true;
534 }
535 _ => {
536 if in_image {
537 if in_alt {
538 alt.push(char)
539 } else {
540 buffer.push(char);
541 }
542 } else {
543 output.push(char)
544 }
545 }
546 }
547 }
548}
549
550fn parse_link_line(output: &mut String, buffer: &mut String, line: &str) {
551 let mut in_link = false;
552 let mut in_text = false;
553 let mut in_src = false;
554 let mut text = String::new();
555
556 for (i, char) in line.chars().enumerate() {
557 match char {
558 '[' => {
559 output.push_str(&buffer);
561 buffer.clear();
562
563 let haystack = slice(line, i..);
565 if !haystack.contains("]") {
566 output.push('[');
567 continue;
568 }
569
570 in_link = true;
572 in_text = true;
573 }
574 ']' => {
575 if in_text {
576 in_text = false;
577 in_src = true;
578 continue;
579 }
580
581 output.push(char);
582 }
583 '(' => {
584 if in_src {
585 continue;
586 }
587
588 if in_link {
589 buffer.push(char);
590 } else {
591 output.push(char);
592 }
593 }
594 ')' => {
595 if in_link {
596 output.push_str(&format!(
598 "<a href=\"{buffer}\" rel=\"noopener noreferrer\">{text}</a>"
599 ));
600
601 text = String::new();
602 in_text = false;
603 in_src = false;
604 in_link = false;
605
606 buffer.clear();
607 continue;
608 }
609
610 output.push(char);
611 }
612 _ => {
613 if in_link {
614 if in_text {
615 text.push(char)
616 } else {
617 buffer.push(char);
618 }
619 } else {
620 output.push(char)
621 }
622 }
623 }
624 }
625}
626
627macro_rules! parser_ignores_pre {
629 ($body:ident, $input:ident) => {{
630 let mut in_pre_block = false;
631 let mut output = String::new();
632 let mut buffer = String::new();
633
634 for line in $input.split("\n") {
635 if line.starts_with("```") | (line == "<style>") | (line == "</style>") {
636 in_pre_block = !in_pre_block;
637 output.push_str(&format!("{line}\n"));
638 continue;
639 }
640
641 if in_pre_block {
642 output.push_str(&format!("{line}\n"));
643 continue;
644 }
645
646 $body(&mut output, &mut buffer, line);
647 output.push_str(&format!("{buffer}\n"));
648 buffer.clear();
649 }
650
651 output
652 }};
653
654 ($body:ident, $input:ident, $id:literal, ..) => {{
655 let mut in_pre_block = false;
656 let mut output = String::new();
657 let mut buffer = String::new();
658 let mut proc_str = String::new();
659 let mut pre_blocks = Vec::new();
660 let mut pre_idx = 0;
661
662 for line in $input.split("\n") {
663 if line.starts_with("```") {
664 in_pre_block = !in_pre_block;
665
666 pre_idx += 1;
667 pre_blocks.push(String::new());
668 pre_blocks[pre_idx - 1] += &(line.to_string() + "\n");
669
670 proc_str += &format!("$pre:{}.{pre_idx}\n", $id);
671 continue;
672 }
673
674 if in_pre_block {
675 pre_blocks[pre_idx - 1] += &(line.to_string() + "\n");
676 continue;
677 }
678
679 proc_str += &(line.to_string() + "\n");
680 }
681
682 $body(&mut output, &mut buffer, &proc_str);
683 output.push_str(&format!("{buffer}\n"));
684 buffer.clear();
685
686 for (mut i, block) in pre_blocks.iter().enumerate() {
687 i += 1;
688
689 if block == "```\n" {
690 output = output.replacen(&format!("$pre:{}.{i}", $id), "", 1);
691 continue;
692 }
693
694 output = output.replacen(&format!("$pre:{}.{i}", $id), &format!("{block}```\n"), 1);
695 }
696
697 output
698 }};
699}
700
701pub fn parse_text_color(input: &str) -> String {
702 parser_ignores_pre!(parse_text_color_line, input, 0, ..)
703}
704
705pub fn parse_highlight(input: &str) -> String {
706 parser_ignores_pre!(parse_highlight_line, input, 1, ..)
707}
708
709pub fn parse_underline(input: &str) -> String {
710 parser_ignores_pre!(parse_underline_line, input, 2, ..)
711}
712
713pub fn parse_comment(input: &str) -> String {
714 parser_ignores_pre!(parse_comment_line, input)
715}
716
717pub fn parse_image_size(input: &str) -> String {
718 parser_ignores_pre!(parse_image_size_line, input)
719}
720
721pub fn parse_image(input: &str) -> String {
722 parser_ignores_pre!(parse_image_line, input)
723}
724
725pub fn parse_link(input: &str) -> String {
726 parser_ignores_pre!(parse_link_line, input)
727}
728
729pub fn parse_page(input: &str) -> String {
754 let mut output = String::new();
755 let mut buffer = String::new();
756 let mut page_id = String::new();
757
758 let mut start_shown = false;
759 let mut in_page = false;
760 let mut in_pre = false;
761
762 for line in input.split("\n") {
763 if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
764 in_pre = !in_pre;
765
766 if in_page {
767 buffer.push_str(&format!("{line}\n"));
768 } else {
769 output.push_str(&format!("{line}\n"));
770 }
771
772 continue;
773 }
774
775 if in_pre {
776 if in_page {
777 buffer.push_str(&format!("{line}\n"));
778 } else {
779 output.push_str(&format!("{line}\n"));
780 }
781
782 continue;
783 }
784
785 if line == "@@" {
787 if in_page {
789 output.push_str(&format!(
790 "<div id=\"#/{page_id}\" class=\"{}subpage no_p_margin fadein\">\n{}\n</div>",
791 if !start_shown { "hidden " } else { "" },
792 render_markdown(&buffer) ));
794
795 start_shown = false;
796 in_page = false;
797
798 buffer.clear();
799 continue;
800 }
801 } else if line.starts_with("@@") {
802 if !in_page {
803 in_page = true;
804
805 let x = line.replace("@@", "").trim().to_string();
806 let id_parts: Vec<&str> = x.split(";").map(|x| x.trim()).collect();
807 page_id = id_parts[0].to_string();
808
809 if let Some(x) = id_parts.get(1) {
810 if *x == "visible" {
811 start_shown = true;
812 }
813 }
814
815 continue;
816 }
817 }
818
819 if in_page {
821 buffer.push_str(&format!("{line}\n"));
822 } else {
823 output.push_str(&format!("{line}\n"));
824 }
825 }
826
827 output
828}
829
830pub fn parse_details(input: &str) -> String {
844 let mut output = String::new();
845 let mut buffer = String::new();
846 let mut summary = String::new();
847
848 let mut in_details = false;
849 let mut in_pre = false;
850
851 for line in input.split("\n") {
852 if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
853 in_pre = !in_pre;
854
855 if in_details {
856 buffer.push_str(&format!("{line}\n"));
857 } else {
858 output.push_str(&format!("{line}\n"));
859 }
860
861 continue;
862 }
863
864 if in_pre {
865 if in_details {
866 buffer.push_str(&format!("{line}\n"));
867 } else {
868 output.push_str(&format!("{line}\n"));
869 }
870
871 continue;
872 }
873
874 if line == "&&" {
876 if in_details {
878 output.push_str(&format!(
879 "<details><summary>{summary}</summary><div class=\"content\">{}</div></details>",
880 render_markdown(&buffer),
881 ));
882
883 in_details = false;
884 buffer.clear();
885 continue;
886 }
887 } else if line.starts_with("&&") {
888 if !in_details {
889 in_details = true;
890 summary = line.replace("&&", "").trim().to_string();
891 continue;
892 }
893 }
894
895 if in_details {
897 buffer.push_str(&format!("{line}\n"));
898 } else {
899 output.push_str(&format!("{line}\n"));
900 }
901 }
902
903 output
904}
905
906fn underscore_chars(mut x: String, chars: &[&str]) -> String {
907 for y in chars {
908 x = x.replace(y, "_");
909 }
910
911 x
912}
913
914pub fn get_toc_list(input: &str) -> (String, String) {
916 let mut output = String::new();
917 let mut toc = String::new();
918 let mut in_pre = false;
919 let mut hc_offset: Option<usize> = None;
920
921 for line in input.split("\n") {
922 if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
923 in_pre = !in_pre;
924 output.push_str(&format!("{line}\n"));
925 continue;
926 }
927
928 if in_pre {
929 output.push_str(&format!("{line}\n"));
930 continue;
931 }
932
933 if line.starts_with("#") {
935 let mut hc = 0;
937 let real_hc;
938
939 for x in line.chars() {
940 if x != '#' {
941 break;
942 }
943
944 hc += 1;
945 }
946
947 real_hc = hc.clone();
948 if hc_offset.is_none() {
949 if hc > 1 {
950 hc_offset = Some(hc - 1);
952 hc = 1;
953 } else {
954 hc_offset = Some(0);
955 }
956 } else if let Some(offset) = hc_offset {
957 hc -= offset;
958 }
959
960 let x = line.replacen(&"#".repeat(real_hc), "", 1);
962 let htext = x.trim();
963
964 let id = underscore_chars(
965 htext.to_lowercase(),
966 &[" ", "(", ")", "[", "]", "{", "}", ":", "?", "#", "&"],
967 );
968
969 output.push_str(&format!(
970 "<h{real_hc} id=\"{id}\">{}</h{real_hc}>\n\n",
971 render_markdown(&htext)
972 ));
973
974 toc += &format!("{}- <a href=\"#{id}\">{htext}</a>\n", " ".repeat(hc));
976
977 continue;
979 }
980
981 output.push_str(&format!("{line}\n"));
983 }
984
985 (toc, output)
986}
987
988pub fn parse_toc(input: &str) -> String {
990 let (toc_list, new_input) = get_toc_list(input);
991
992 let mut output = String::new();
993 let mut in_pre = false;
994
995 for line in new_input.split("\n") {
996 if line.starts_with("```") || line.starts_with("<style>") || line.starts_with("</style>") {
997 in_pre = !in_pre;
998 output.push_str(&format!("{line}\n"));
999 continue;
1000 }
1001
1002 if in_pre {
1003 output.push_str(&format!("{line}\n"));
1004 continue;
1005 }
1006
1007 if line.len() == 5 && line.to_lowercase() == "[toc]" {
1009 output.push_str(&format!("\n{toc_list}"));
1011 continue;
1012 }
1013
1014 output.push_str(&format!("{line}\n"));
1016 }
1017
1018 output
1019}
1020
1021fn parse_markdown_element_line(output: &mut String, buffer: &mut String, line: &str) {
1023 let mut in_markdown = false;
1024
1025 for char in line.chars() {
1026 if buffer.ends_with("<markdown>") {
1027 in_markdown = true;
1028 output.push_str(&buffer.replace("<markdown>", ""));
1029 buffer.clear();
1030 } else if in_markdown && buffer.ends_with("</markdown>") {
1031 in_markdown = false;
1032 output.push_str(&render_markdown(&buffer.replace("</markdown>", "")));
1033 buffer.clear();
1034 }
1035
1036 buffer.push(char);
1037 }
1038}
1039
1040pub fn parse_markdown_element(input: &str) -> String {
1041 parser_ignores_pre!(parse_markdown_element_line, input)
1042}