this repo has no description
at main 166 lines 5.2 kB view raw
1use anyhow::Result; 2use async_trait::async_trait; 3use comrak::html::format_document_with_formatter; 4use comrak::nodes::Ast; 5use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; 6use comrak::{Options, Plugins}; 7use std::cell::RefCell; 8use std::io::BufWriter; 9use std::sync::Arc; 10 11 12/// Trait for rendering markdown content to HTML. 13#[async_trait] 14pub trait RenderManager: Send + Sync { 15 /// Render markdown content to HTML. 16 async fn render_markdown(&self, markdown: &str) -> Result<String>; 17} 18 19/// A markdown renderer using the Comrak library with syntax highlighting. 20pub struct ComrakRenderManager<'a> { 21 syntect_adapter: Arc<SyntectAdapter>, 22 options: Options<'a>, 23 external_base: String, 24} 25 26impl ComrakRenderManager<'static> { 27 28 #[cfg(test)] 29 /// Create a new Comrak render manager with the given external base URL. 30 pub fn new(external_base: &str) -> Self { 31 Self::with_theme("base16-ocean.dark", external_base) 32 } 33 34 /// Create a new Comrak render manager with the given theme and external base URL. 35 pub fn with_theme(theme: &str, external_base: &str) -> Self { 36 let syntect_adapter = Arc::new( 37 SyntectAdapterBuilder::new() 38 .theme(theme) 39 .build(), 40 ); 41 42 let mut options = Options::default(); 43 options.extension.strikethrough = true; 44 options.extension.table = true; 45 options.extension.autolink = true; 46 options.extension.tasklist = true; 47 options.extension.superscript = true; 48 options.extension.footnotes = true; 49 options.extension.description_lists = true; 50 options.render.unsafe_ = false; 51 52 Self { 53 syntect_adapter, 54 options, 55 external_base: external_base.to_string(), 56 } 57 } 58} 59 60fn prepend_image_base<'a>( 61 nl: &mut comrak::nodes::NodeLink, 62 context: &mut comrak::html::Context<String>, 63 _node: &'a comrak::arena_tree::Node<'a, RefCell<Ast>>, 64 entering: bool, 65) { 66 if !entering { 67 return; 68 } 69 70 if !nl.url.starts_with("https://") && !nl.url.starts_with("/") { 71 nl.url = format!("{}/content/{}", &context.user, nl.url) 72 } 73} 74 75fn formatter<'a>( 76 context: &mut comrak::html::Context<String>, 77 node: &'a comrak::nodes::AstNode<'a>, 78 entering: bool, 79) -> std::io::Result<comrak::html::ChildRendering> { 80 let mut borrow = node.data.borrow_mut(); 81 if let comrak::nodes::NodeValue::Image(ref mut nl) = borrow.value { 82 prepend_image_base(nl, context, node, entering); 83 } 84 drop(borrow); 85 comrak::html::format_node_default(context, node, entering) 86} 87 88#[async_trait] 89impl RenderManager for ComrakRenderManager<'static> { 90 async fn render_markdown(&self, markdown: &str) -> Result<String> { 91 let adapter = self.syntect_adapter.as_ref(); 92 let mut plugins = Plugins::default(); 93 plugins.render.codefence_syntax_highlighter = Some(adapter); 94 95 let arena = comrak::Arena::new(); 96 let root = comrak::parse_document(&arena, markdown, &self.options); 97 let mut bw = BufWriter::new(Vec::new()); 98 format_document_with_formatter( 99 root, 100 &self.options, 101 &mut bw, 102 &plugins, 103 formatter, 104 self.external_base.clone(), 105 ) 106 .unwrap(); 107 108 Ok(String::from_utf8(bw.into_inner().unwrap()).unwrap()) 109 } 110} 111 112#[cfg(test)] 113mod tests { 114 use super::*; 115 116 #[tokio::test] 117 async fn test_render_markdown() { 118 let renderer = ComrakRenderManager::new("http://localhost:8080"); 119 120 let markdown = "# Hello World\n\nThis is a **test**.\n\n```rust\nfn main() {\n println!(\"Hello, world!\");\n}\n```"; 121 122 let html = renderer.render_markdown(markdown).await.unwrap(); 123 124 assert!(html.contains("<h1>")); 125 assert!(html.contains("Hello World")); 126 assert!(html.contains("<strong>test</strong>")); 127 assert!(html.contains("main")); 128 } 129 130 #[tokio::test] 131 async fn test_render_with_extensions() { 132 let renderer = ComrakRenderManager::new("http://localhost:8080"); 133 134 let markdown = "~~strikethrough~~ and https://example.com autolink"; 135 136 let html = renderer.render_markdown(markdown).await.unwrap(); 137 138 assert!(html.contains("<del>")); 139 assert!(html.contains("<a href=\"https://example.com\"")); 140 } 141 142 #[tokio::test] 143 async fn test_syntax_highlighting() { 144 let renderer = ComrakRenderManager::new("http://localhost:8080"); 145 146 let markdown = "```rust\nlet x = 42;\n```"; 147 148 let html = renderer.render_markdown(markdown).await.unwrap(); 149 150 assert!(html.contains("<pre")); 151 assert!(html.contains("style=")); 152 } 153 154 #[tokio::test] 155 async fn test_image_url_prefixing() { 156 let renderer = ComrakRenderManager::new("https://example.com"); 157 158 let markdown = "![alt text](image.jpg) ![absolute](https://other.com/image.jpg) ![root](/root.jpg)"; 159 160 let html = renderer.render_markdown(markdown).await.unwrap(); 161 162 assert!(html.contains("https://example.com/image.jpg")); 163 assert!(html.contains("https://other.com/image.jpg")); 164 assert!(html.contains("\"/root.jpg\"")); 165 } 166}