tetratto_shared/
markdown.rs

1use ammonia::Builder;
2use pulldown_cmark::{Parser, Options, html::push_html};
3use std::collections::HashSet;
4
5/// Render markdown input into HTML
6pub fn render_markdown(input: &str, proxy_images: bool) -> String {
7    let input = &parse_alignment(input);
8
9    let mut options = Options::empty();
10    options.insert(Options::ENABLE_STRIKETHROUGH);
11    options.insert(Options::ENABLE_GFM);
12    options.insert(Options::ENABLE_FOOTNOTES);
13    options.insert(Options::ENABLE_TABLES);
14    options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
15    options.insert(Options::ENABLE_SUBSCRIPT);
16    options.insert(Options::ENABLE_SUPERSCRIPT);
17
18    let parser = Parser::new_ext(input, options);
19
20    let mut html = String::new();
21    push_html(&mut html, parser);
22
23    let mut allowed_attributes = HashSet::new();
24    allowed_attributes.insert("id");
25    allowed_attributes.insert("class");
26    allowed_attributes.insert("ref");
27    allowed_attributes.insert("aria-label");
28    allowed_attributes.insert("lang");
29    allowed_attributes.insert("title");
30    allowed_attributes.insert("align");
31    allowed_attributes.insert("src");
32
33    let output = Builder::default()
34        .generic_attributes(allowed_attributes)
35        .add_tags(&[
36            "video", "source", "img", "b", "span", "p", "i", "strong", "em", "a", "align",
37        ])
38        .rm_tags(&["script", "style", "link", "canvas"])
39        .add_tag_attributes("a", &["href", "target"])
40        .add_url_schemes(&["atto"])
41        .clean(&html)
42        .to_string()
43        .replace("<video loading=", "<video controls loading=");
44
45    if proxy_images {
46        output.replace(
47            "src=\"http",
48            "loading=\"lazy\" src=\"/api/v1/util/proxy?url=http",
49        )
50    } else {
51        output
52    }
53}
54
55fn parse_alignment_line(line: &str, output: &mut String, buffer: &mut String, is_in_pre: bool) {
56    if is_in_pre {
57        output.push_str(&format!("{line}\n"));
58        return;
59    }
60
61    let mut is_alignment_waiting: bool = false;
62    let mut alignment_center: bool = false;
63    let mut has_dash: bool = false;
64    let mut escape: bool = false;
65
66    for char in line.chars() {
67        if alignment_center && char != '-' {
68            // last char was <, but we didn't receive a hyphen directly after
69            alignment_center = false;
70            buffer.push('<');
71        }
72
73        if has_dash && char != '>' {
74            // the last char was -, meaning we need to flip has_dash and push the char since we haven't used it
75            has_dash = false;
76            buffer.push('-');
77        }
78
79        match char {
80            '\\' => {
81                escape = true;
82                continue;
83            }
84            '-' => {
85                if escape {
86                    buffer.push(char);
87                    escape = false;
88                    continue;
89                }
90
91                if alignment_center && is_alignment_waiting {
92                    // this means the previous element was <, so we're wrapping up alignment now
93                    alignment_center = false;
94                    is_alignment_waiting = false;
95                    output.push_str(&format!("<align class=\"center\">{buffer}</align>"));
96                    buffer.clear();
97                    continue;
98                }
99
100                has_dash = true;
101
102                if !is_alignment_waiting {
103                    // we need to go ahead and push/clear the buffer so we don't capture the stuff that came before this
104                    // this only needs to be done on the first of these for a single alignment block
105                    output.push_str(&format!("{buffer}\n"));
106                    buffer.clear();
107                }
108            }
109            '<' => {
110                if escape {
111                    buffer.push(char);
112                    escape = false;
113                    continue;
114                }
115
116                alignment_center = true;
117                continue;
118            }
119            '>' => {
120                if escape {
121                    buffer.push(char);
122                    escape = false;
123                    continue;
124                }
125
126                if has_dash {
127                    has_dash = false;
128
129                    // if we're already waiting for aligmment, this means this is the SECOND aligner arrow
130                    if is_alignment_waiting {
131                        is_alignment_waiting = false;
132                        output.push_str(&format!("<align class=\"right\">{buffer}</align>"));
133                        buffer.clear();
134                        continue;
135                    }
136
137                    // we're now waiting for the next aligner
138                    is_alignment_waiting = true;
139                    continue;
140                } else {
141                    buffer.push('>');
142                }
143            }
144            _ => buffer.push(char),
145        }
146
147        escape = false;
148    }
149
150    output.push_str(&format!("{buffer}\n"));
151    buffer.clear();
152}
153
154pub fn parse_alignment(input: &str) -> String {
155    let lines = input.split("\n");
156
157    let mut is_in_pre: bool = false;
158    let mut output = String::new();
159    let mut buffer = String::new();
160
161    for line in lines {
162        match line {
163            "```" => {
164                is_in_pre = !is_in_pre;
165                output.push_str(&format!("{line}\n"));
166            }
167            _ => parse_alignment_line(line, &mut output, &mut buffer, is_in_pre),
168        }
169    }
170
171    output.push_str(&buffer);
172    output
173}