use std::sync::OnceLock;
use comrak::nodes::AstNode;
use comrak::options::Plugins;
use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder};
use comrak::{format_html_with_plugins, markdown_to_html_with_plugins, Options};
pub const CODE_PRE_CLASS: &str = "docgen-code";
fn syntect_adapter() -> &'static SyntectAdapter {
static ADAPTER: OnceLock<SyntectAdapter> = OnceLock::new();
ADAPTER.get_or_init(|| SyntectAdapterBuilder::new().css().build())
}
fn rewrite_code_pre_class(html: String) -> String {
html.replace(
r#"<pre class="syntax-highlighting">"#,
&format!(r#"<pre class="{CODE_PRE_CLASS}">"#),
)
}
pub fn comrak_options() -> Options<'static> {
let mut options = Options::default();
options.extension.strikethrough = true;
options.extension.table = true;
options.extension.tasklist = true;
options.extension.autolink = true;
options.extension.footnotes = true;
options.extension.math_dollars = true;
options.extension.math_code = true;
options.render.r#unsafe = true;
options
}
pub fn render_markdown(body: &str) -> String {
let options = comrak_options();
let mut plugins = Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(syntect_adapter());
rewrite_code_pre_class(markdown_to_html_with_plugins(body, &options, &plugins))
}
pub fn format_ast<'a>(root: &'a AstNode<'a>, options: &Options) -> String {
let mut plugins = Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(syntect_adapter());
let mut out = String::new();
format_html_with_plugins(root, options, &mut out, &plugins).expect("format AST to HTML");
rewrite_code_pre_class(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_heading_to_html() {
let html = render_markdown("# Title");
assert!(html.contains("<h1>"));
assert!(html.contains("Title"));
}
#[test]
fn renders_gfm_table() {
let md = "| a | b |\n| - | - |\n| 1 | 2 |\n";
let html = render_markdown(md);
assert!(html.contains("<table>"));
}
#[test]
fn renders_strikethrough() {
let html = render_markdown("~~gone~~");
assert!(html.contains("<del>"));
}
#[test]
fn renders_task_list() {
let html = render_markdown("- [x] done\n- [ ] todo\n");
assert!(html.contains("type=\"checkbox\""));
assert!(html.contains("checked"));
}
#[test]
fn renders_autolink() {
let html = render_markdown("see https://example.com here\n");
assert!(html.contains(r#"href="https://example.com""#));
}
#[test]
fn renders_footnote() {
let html = render_markdown("text[^1]\n\n[^1]: a note\n");
assert!(html.contains("<sup"));
assert!(html.contains("footnote"));
}
#[test]
fn highlights_fenced_rust_code() {
let md = "```rust\nfn main() { let x = 1; }\n```\n";
let html = render_markdown(md);
assert!(html.contains(r#"<pre class="docgen-code">"#));
assert!(!html.contains("style=\"color:"));
assert!(html.contains(r#"<span class="keyword"#));
}
#[test]
fn unknown_language_does_not_crash_and_still_wraps_pre() {
let md = "```not-a-real-lang\nplain text\n```\n";
let html = render_markdown(md);
assert!(html.contains("<pre"));
assert!(html.contains("plain text"));
}
#[test]
fn math_extension_is_enabled_in_shared_options() {
let opts = comrak_options();
assert!(opts.extension.math_dollars);
assert!(opts.extension.math_code);
}
#[test]
fn comrak_options_is_shared_source_of_truth() {
let html = render_markdown("~~gone~~\n");
assert!(html.contains("<del>"));
}
}