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_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            // we expected to see another percentage to close the main buffer,
56            // not getting that means this wasn't meant to be a color
57            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                    // ending
72                    if !close_1 {
73                        close_1 = true;
74                        continue;
75                    }
76
77                    // by this point, we have:   !
78                    // %color_buffer%main_buffer%%
79                    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                    // ...
87                    in_main_buffer = false;
88                    close_1 = false;
89                    continue;
90                }
91
92                // start
93                // scan ahead
94                let ahead = slice(line, i..);
95                if !ahead.contains("%%") {
96                    // no closing sequence, we're done
97                    buffer.push(char);
98                    continue;
99                }
100
101                // flush buffer
102                output.push_str(&buffer);
103                buffer.clear();
104
105                // toggle open
106                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                    // flush buffer
143                    output.push_str(&buffer);
144                    buffer.clear();
145
146                    // toggle open
147                    open_1 = true;
148                    is_open = true;
149                } else {
150                    if open_1 {
151                        // this is the second open we've recieved
152                        open_2 = true;
153                        open_1 = false;
154                        continue;
155                    }
156
157                    if close_1 {
158                        // this is the second close we've received
159                        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                    // open close
212                    close_1 = true;
213                }
214            }
215            '!' => {
216                if close_1 {
217                    // close
218                    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                            // color
225                            if x == &"default" {
226                                x = &"currentColor";
227                            }
228
229                            style.push_str(&format!("text-decoration-color: {x};"));
230                        } else if i == 1 {
231                            // style
232                            if x == &"default" {
233                                x = &"solid";
234                            }
235
236                            style.push_str(&format!("text-decoration-style: {x};"));
237                        } else if i == 2 {
238                            // line
239                            if x == &"default" {
240                                x = &"underline";
241                            }
242
243                            style.push_str(&format!("text-decoration-line: {x};"));
244                        } else if i == 3 {
245                            // thickness
246                            if x == &"default" {
247                                x = &"1px";
248                            }
249
250                            style.push_str(&format!("text-decoration-thickness: {x}px;"));
251                        }
252                    }
253
254                    // defaults
255                    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                    // ...
268                    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
281                open_1 = true;
282
283                // flush buffer
284                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                    // end
366                    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                // flush buffer
406                output.push_str(&buffer);
407                buffer.clear();
408
409                // ...
410                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                    // end
482                    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                // flush buffer
508                output.push_str(&buffer);
509                buffer.clear();
510
511                // ...
512                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                // flush buffer
539                output.push_str(&buffer);
540                buffer.clear();
541
542                // scan for closing, otherwise quit
543                let haystack = slice(line, i..);
544                if !haystack.contains("]") {
545                    output.push('[');
546                    continue;
547                }
548
549                // ...
550                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                    // end
576                    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
606/// Helper macro to quickly allow parsers to ignore fenced code blocks.
607macro_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}