1use comrak::{markdown_to_html, Options};
2use syntect::highlighting::ThemeSet;
3use syntect::html::highlighted_html_for_string;
4use syntect::parsing::SyntaxSet;
5
6use crate::error::CoreError;
7
8pub fn render_markdown(input: &str) -> Result<String, CoreError> {
10 let mut options = Options::default();
11 options.extension.strikethrough = true;
12 options.extension.table = true;
13 options.extension.autolink = true;
14 options.extension.tasklist = true;
15 options.extension.tagfilter = true;
16
17 Ok(markdown_to_html(input, &options))
18}
19
20pub fn highlight_code(code: &str, extension: &str) -> Result<String, CoreError> {
25 let ss = SyntaxSet::load_defaults_newlines();
26 let ts = ThemeSet::load_defaults();
27
28 let syntax = ss
29 .find_syntax_by_extension(extension)
30 .unwrap_or_else(|| ss.find_syntax_plain_text());
31
32 let theme = ts
33 .themes
34 .get("InspiredGitHub")
35 .or_else(|| ts.themes.get("base16-ocean.dark"))
36 .ok_or_else(|| CoreError::RenderError("No suitable theme found".to_string()))?;
37
38 let html = highlighted_html_for_string(code, &ss, syntax, theme)
39 .map_err(|e| CoreError::RenderError(e.to_string()))?;
40
41 if html.trim_start().starts_with("<pre") {
42 Ok(html)
43 } else {
44 Ok(format!("<pre><code>{html}</code></pre>"))
45 }
46}
47
48pub fn wrap_plain_text(text: &str) -> String {
52 let escaped = html_escape(text);
53 format!("<pre><code>{escaped}</code></pre>")
54}
55
56fn html_escape(text: &str) -> String {
57 let mut result = String::with_capacity(text.len());
58 for ch in text.chars() {
59 match ch {
60 '&' => result.push_str("&"),
61 '<' => result.push_str("<"),
62 '>' => result.push_str(">"),
63 '"' => result.push_str("""),
64 '\'' => result.push_str("'"),
65 c => result.push(c),
66 }
67 }
68 result
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn test_render_markdown() {
77 let result = render_markdown("# Hello").unwrap();
78 assert!(result.contains("<h1>Hello</h1>"));
79 }
80
81 #[test]
82 fn test_render_markdown_gfm_table() {
83 let input = "| Col1 | Col2 |\n|------|------|\n| A | B |";
84 let result = render_markdown(input).unwrap();
85 assert!(result.contains("<table>"));
86 assert!(result.contains("<th>"));
87 }
88
89 #[test]
90 fn test_highlight_code() {
91 let code = r#"{"key": "value"}"#;
92 let result = highlight_code(code, "json").unwrap();
93 assert!(result.contains("<pre") || result.contains("<span"));
95 }
96
97 #[test]
98 fn test_wrap_plain_text() {
99 let result = wrap_plain_text("Hello <world> & \"things\"");
100 assert!(result.starts_with("<pre><code>"));
101 assert!(result.ends_with("</code></pre>"));
102 assert!(result.contains("<world>"));
103 assert!(result.contains("&"));
104 assert!(result.contains("""));
105 }
106
107 #[test]
108 fn test_html_escape() {
109 let result = html_escape("a & b < c > d \"e\" 'f'");
110 assert!(result.contains("&"));
111 assert!(result.contains("<"));
112 assert!(result.contains(">"));
113 assert!(result.contains("""));
114 assert!(result.contains("'"));
115 assert!(!result.contains('&') || result.contains("&"));
116 }
117}