fluffle/
markdown.rs

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            // we expected to see another percentage to close the main buffer,
65            // not getting that means this wasn't meant to be a color
66            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                    // ending
81                    if !close_1 {
82                        close_1 = true;
83                        continue;
84                    }
85
86                    // by this point, we have:   !
87                    // %color_buffer%main_buffer%%
88                    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                    // ...
96                    in_main_buffer = false;
97                    close_1 = false;
98                    continue;
99                }
100
101                // start
102                // scan ahead
103                let ahead = slice(line, i..);
104                if !ahead.contains("%%") {
105                    // no closing sequence, we're done
106                    buffer.push(char);
107                    continue;
108                }
109
110                // flush buffer
111                output.push_str(&buffer);
112                buffer.clear();
113
114                // toggle open
115                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                    // flush buffer
158                    output.push_str(&buffer);
159                    buffer.clear();
160
161                    // toggle open
162                    open_1 = true;
163                    is_open = true;
164                } else {
165                    if open_1 {
166                        // this is the second open we've recieved
167                        open_2 = true;
168                        open_1 = false;
169                        continue;
170                    }
171
172                    if close_1 {
173                        // this is the second close we've received
174                        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                // image
214                buffer.push('!');
215            } else {
216                buffer.push_str("&excl;");
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                    // open close
233                    close_1 = true;
234                }
235            }
236            '!' => {
237                if close_1 {
238                    // close
239                    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                            // color
246                            if x == &"default" {
247                                x = &"currentColor";
248                            }
249
250                            style.push_str(&format!("text-decoration-color: {x};"));
251                        } else if i == 1 {
252                            // style
253                            if x == &"default" {
254                                x = &"solid";
255                            }
256
257                            style.push_str(&format!("text-decoration-style: {x};"));
258                        } else if i == 2 {
259                            // line
260                            if x == &"default" {
261                                x = &"underline";
262                            }
263
264                            style.push_str(&format!("text-decoration-line: {x};"));
265                        } else if i == 3 {
266                            // thickness
267                            if x == &"default" {
268                                x = &"1px";
269                            }
270
271                            style.push_str(&format!("text-decoration-thickness: {x}px;"));
272                        }
273                    }
274
275                    // defaults
276                    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                    // ...
289                    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
302                open_1 = true;
303
304                // flush buffer
305                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                    // end
387                    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                // flush buffer
427                output.push_str(&buffer);
428                buffer.clear();
429
430                // ...
431                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                    // end
503                    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                // flush buffer
529                output.push_str(&buffer);
530                buffer.clear();
531
532                // ...
533                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                // flush buffer
560                output.push_str(&buffer);
561                buffer.clear();
562
563                // scan for closing, otherwise quit
564                let haystack = slice(line, i..);
565                if !haystack.contains("]") {
566                    output.push('[');
567                    continue;
568                }
569
570                // ...
571                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                    // end
597                    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
627/// Helper macro to quickly allow parsers to ignore fenced code blocks.
628macro_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
729/// Match page definitions.
730///
731/// Each page is denoted with two at symbols, followed by the name of the page.
732/// The page can also have an optional second argument (separated by a semicolon)
733/// which accepts the "visible" value; marking the page as visible by default.
734///
735/// To close a page (after you're done with the page's content), just put two
736/// at symbols with nothing else on the line.
737///
738/// You're able to put content AFTER the page closing line. This allows you to have
739/// persistant content which is shared between every page. Only content within pages
740/// is hidden when navigating to another page. This means everything in the entry
741/// that isn't part of a page will remian throughout navigations.
742///
743/// # Example
744/// ```md
745/// @@ home; visible
746/// this is the homepage which is shown by default!
747/// @@
748///
749/// @@ about
750/// this is the about page which is NOT shown by default! a link with an href of "#/about" will open this page
751/// @@
752/// ```
753pub 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        // not in pre
786        if line == "@@" {
787            // ending block
788            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) // recurse to render markdown since the renderer is ignoring the div content :/
793                ));
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        // otherwise
820        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
830/// Parse the markdown syntax for the expandable `<details>` element.
831///
832/// Similar to the [`parse_page`] page definitions, details elements are denoted
833/// with two ampersand symbols. The opening line should look like `&& [summary]`.
834///
835/// The block is closed with a line of exactly two ampersand symbols.
836///
837/// # Example
838/// ```md
839/// && other summary
840/// this element starts closed, but can be expanded
841/// &&
842/// ```
843pub 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        // not in pre
875        if line == "&&" {
876            // ending block
877            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        // otherwise
896        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
914/// Get the list of headers needed for [`parse_toc`].
915pub 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        // not in pre
934        if line.starts_with("#") {
935            // get heading count
936            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                    // offset this count to 1 so the list renders properly
951                    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            // add heading with id
961            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            // add heading to toc
975            toc += &format!("{}- <a href=\"#{id}\">{htext}</a>\n", "  ".repeat(hc));
976
977            // ...
978            continue;
979        }
980
981        // otherwise
982        output.push_str(&format!("{line}\n"));
983    }
984
985    (toc, output)
986}
987
988/// Parse the `[toc]` table-of-contents syntax.
989pub 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        // not in pre
1008        if line.len() == 5 && line.to_lowercase() == "[toc]" {
1009            // add toc
1010            output.push_str(&format!("\n{toc_list}"));
1011            continue;
1012        }
1013
1014        // otherwise
1015        output.push_str(&format!("{line}\n"));
1016    }
1017
1018    output
1019}
1020
1021/// Handle the `<markdown>` HTML element.
1022fn 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}