use anyhow::Result; use async_trait::async_trait; use comrak::html::format_document_with_formatter; use comrak::nodes::Ast; use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; use comrak::{Options, Plugins}; use std::cell::RefCell; use std::io::BufWriter; use std::sync::Arc; /// Trait for rendering markdown content to HTML. #[async_trait] pub trait RenderManager: Send + Sync { /// Render markdown content to HTML. async fn render_markdown(&self, markdown: &str) -> Result; } /// A markdown renderer using the Comrak library with syntax highlighting. pub struct ComrakRenderManager<'a> { syntect_adapter: Arc, options: Options<'a>, external_base: String, } impl ComrakRenderManager<'static> { #[cfg(test)] /// Create a new Comrak render manager with the given external base URL. pub fn new(external_base: &str) -> Self { Self::with_theme("base16-ocean.dark", external_base) } /// Create a new Comrak render manager with the given theme and external base URL. pub fn with_theme(theme: &str, external_base: &str) -> Self { let syntect_adapter = Arc::new( SyntectAdapterBuilder::new() .theme(theme) .build(), ); let mut options = Options::default(); options.extension.strikethrough = true; options.extension.table = true; options.extension.autolink = true; options.extension.tasklist = true; options.extension.superscript = true; options.extension.footnotes = true; options.extension.description_lists = true; options.render.unsafe_ = false; Self { syntect_adapter, options, external_base: external_base.to_string(), } } } fn prepend_image_base<'a>( nl: &mut comrak::nodes::NodeLink, context: &mut comrak::html::Context, _node: &'a comrak::arena_tree::Node<'a, RefCell>, entering: bool, ) { if !entering { return; } if !nl.url.starts_with("https://") && !nl.url.starts_with("/") { nl.url = format!("{}/content/{}", &context.user, nl.url) } } fn formatter<'a>( context: &mut comrak::html::Context, node: &'a comrak::nodes::AstNode<'a>, entering: bool, ) -> std::io::Result { let mut borrow = node.data.borrow_mut(); if let comrak::nodes::NodeValue::Image(ref mut nl) = borrow.value { prepend_image_base(nl, context, node, entering); } drop(borrow); comrak::html::format_node_default(context, node, entering) } #[async_trait] impl RenderManager for ComrakRenderManager<'static> { async fn render_markdown(&self, markdown: &str) -> Result { let adapter = self.syntect_adapter.as_ref(); let mut plugins = Plugins::default(); plugins.render.codefence_syntax_highlighter = Some(adapter); let arena = comrak::Arena::new(); let root = comrak::parse_document(&arena, markdown, &self.options); let mut bw = BufWriter::new(Vec::new()); format_document_with_formatter( root, &self.options, &mut bw, &plugins, formatter, self.external_base.clone(), ) .unwrap(); Ok(String::from_utf8(bw.into_inner().unwrap()).unwrap()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_render_markdown() { let renderer = ComrakRenderManager::new("http://localhost:8080"); let markdown = "# Hello World\n\nThis is a **test**.\n\n```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```"; let html = renderer.render_markdown(markdown).await.unwrap(); assert!(html.contains("

")); assert!(html.contains("Hello World")); assert!(html.contains("test")); assert!(html.contains("main")); } #[tokio::test] async fn test_render_with_extensions() { let renderer = ComrakRenderManager::new("http://localhost:8080"); let markdown = "~~strikethrough~~ and https://example.com autolink"; let html = renderer.render_markdown(markdown).await.unwrap(); assert!(html.contains("")); assert!(html.contains("