1use std::sync::OnceLock;
2
3use comrak::nodes::AstNode;
4use comrak::options::Plugins;
5use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
6use comrak::{format_html_with_plugins, markdown_to_html_with_plugins, Options};
7
8pub const CODE_PRE_CLASS: &str = "docgen-code";
14
15fn syntect_adapter() -> &'static SyntectAdapter {
20 static ADAPTER: OnceLock<SyntectAdapter> = OnceLock::new();
21 ADAPTER.get_or_init(|| SyntectAdapterBuilder::new().css().build())
22}
23
24fn rewrite_code_pre_class(html: String) -> String {
30 html.replace(
31 r#"<pre class="syntax-highlighting">"#,
32 &format!(r#"<pre class="{CODE_PRE_CLASS}">"#),
33 )
34}
35
36pub fn comrak_options() -> Options<'static> {
39 let mut options = Options::default();
40 options.extension.strikethrough = true;
41 options.extension.table = true;
42 options.extension.tasklist = true;
43 options.extension.autolink = true;
44 options.extension.footnotes = true;
45 options.extension.math_dollars = true;
48 options.extension.math_code = true;
49 options.render.r#unsafe = true;
52 options
53}
54
55pub fn render_markdown(body: &str) -> String {
58 let options = comrak_options();
59 let mut plugins = Plugins::default();
60 plugins.render.codefence_syntax_highlighter = Some(syntect_adapter());
61 rewrite_code_pre_class(markdown_to_html_with_plugins(body, &options, &plugins))
62}
63
64pub fn format_ast<'a>(root: &'a AstNode<'a>, options: &Options) -> String {
66 let mut plugins = Plugins::default();
67 plugins.render.codefence_syntax_highlighter = Some(syntect_adapter());
68 let mut out = String::new();
69 format_html_with_plugins(root, options, &mut out, &plugins).expect("format AST to HTML");
70 rewrite_code_pre_class(out)
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn renders_heading_to_html() {
79 let html = render_markdown("# Title");
80 assert!(html.contains("<h1>"));
81 assert!(html.contains("Title"));
82 }
83
84 #[test]
85 fn renders_gfm_table() {
86 let md = "| a | b |\n| - | - |\n| 1 | 2 |\n";
87 let html = render_markdown(md);
88 assert!(html.contains("<table>"));
89 }
90
91 #[test]
92 fn renders_strikethrough() {
93 let html = render_markdown("~~gone~~");
94 assert!(html.contains("<del>"));
95 }
96
97 #[test]
98 fn renders_task_list() {
99 let html = render_markdown("- [x] done\n- [ ] todo\n");
100 assert!(html.contains("type=\"checkbox\""));
101 assert!(html.contains("checked"));
102 }
103
104 #[test]
105 fn renders_autolink() {
106 let html = render_markdown("see https://example.com here\n");
107 assert!(html.contains(r#"href="https://example.com""#));
108 }
109
110 #[test]
111 fn renders_footnote() {
112 let html = render_markdown("text[^1]\n\n[^1]: a note\n");
113 assert!(html.contains("<sup"));
114 assert!(html.contains("footnote"));
115 }
116
117 #[test]
118 fn highlights_fenced_rust_code() {
119 let md = "```rust\nfn main() { let x = 1; }\n```\n";
120 let html = render_markdown(md);
121 assert!(html.contains(r#"<pre class="docgen-code">"#));
124 assert!(!html.contains("style=\"color:"));
125 assert!(html.contains(r#"<span class="keyword"#));
127 }
128
129 #[test]
130 fn unknown_language_does_not_crash_and_still_wraps_pre() {
131 let md = "```not-a-real-lang\nplain text\n```\n";
132 let html = render_markdown(md);
133 assert!(html.contains("<pre"));
134 assert!(html.contains("plain text"));
135 }
136
137 #[test]
138 fn math_extension_is_enabled_in_shared_options() {
139 let opts = comrak_options();
140 assert!(opts.extension.math_dollars);
141 assert!(opts.extension.math_code);
142 }
143
144 #[test]
145 fn comrak_options_is_shared_source_of_truth() {
146 let html = render_markdown("~~gone~~\n");
148 assert!(html.contains("<del>"));
149 }
150}