tetratto_shared/
markdown.rs1use ammonia::Builder;
2use pulldown_cmark::{Parser, Options, html::push_html};
3use std::collections::HashSet;
4
5pub fn render_markdown(input: &str, proxy_images: bool) -> String {
7 let input = &autolinks(&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 alignment_center = false;
70 buffer.push('<');
71 }
72
73 if has_dash && char != '>' {
74 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 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 output.push_str(&buffer);
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 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 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}
174
175pub fn autolinks(input: &str) -> String {
180 if input.len() == 0 {
181 return String::new();
182 }
183
184 let pattern = regex::Regex::new(
185 r"(?ix)\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))(\s|$)",
186 )
187 .unwrap();
188
189 pattern
190 .replace_all(input, "<a href=\"$0\">$0</a> ")
191 .to_string()
192}