use crate::helpers; use crate::hilighting; use axum::http::Uri; use cached::once_cell::sync::Lazy; use pulldown_cmark::CodeBlockKind; use pulldown_cmark::Event; use pulldown_cmark::Tag; use pulldown_cmark::TagEnd; use pulldown_cmark::{Options, Parser}; use regex::Regex; use tracing::instrument; static STARTS_WITH_SCHEMA_RE: Lazy = Lazy::new(|| Regex::new(r"^\w+:").unwrap()); static EMAIL_RE: Lazy = Lazy::new(|| Regex::new(r"^.+?@\w+(\.\w+)*$").unwrap()); #[instrument(skip(markdown))] pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String { let mut opt = Options::empty(); opt.insert(Options::ENABLE_FOOTNOTES); opt.insert(Options::ENABLE_HEADING_ATTRIBUTES); opt.insert(Options::ENABLE_STRIKETHROUGH); opt.insert(Options::ENABLE_TABLES); opt.insert(Options::ENABLE_TASKLISTS); opt.insert(Options::ENABLE_SMART_PUNCTUATION); let mut content_html = String::new(); let parser = Parser::new_ext(markdown, opt); let mut code_lang = None; let mut code_accumulator = String::new(); let mut events = Vec::new(); for event in parser { match event { Event::Text(text) => { if code_lang.is_some() { code_accumulator.push_str(&text); } else { events.push(Event::Text(text)); } } Event::Start(Tag::Link { mut dest_url, link_type, title, id, }) => { if let Some(uri) = base_uri { if !dest_url.starts_with('#') && !STARTS_WITH_SCHEMA_RE.is_match(&dest_url) && !EMAIL_RE.is_match(&dest_url) { // convert relative URIs to absolute URIs dest_url = helpers::uri_with_path(uri, &dest_url).to_string().into(); } } events.push(Event::Start(Tag::Link { dest_url, link_type, title, id, })); } Event::Start(Tag::Image { link_type, mut dest_url, title, id, }) => { if let Some(uri) = base_uri { if !dest_url.contains("://") && !dest_url.contains('@') { // convert relative URIs to absolute URIs dest_url = helpers::uri_with_path(uri, &dest_url).to_string().into(); } } events.push(Event::Start(Tag::Image { link_type, dest_url, title, id, })); } Event::Start(Tag::CodeBlock(kind)) => { if let CodeBlockKind::Fenced(lang) = kind { code_lang = Some(lang); } } Event::End(TagEnd::CodeBlock) => { let lang = code_lang.take().unwrap_or("".into()); let res = hilighting::hilight(&code_accumulator, &lang, Some("base16-ocean.dark")) .unwrap(); events.push(Event::Html(res.into())); code_accumulator.clear(); } _ => events.push(event), } } events.retain(|e| match e { Event::Text(t) | Event::Html(t) => !t.is_empty(), _ => true, }); pulldown_cmark::html::push_html(&mut content_html, events.into_iter()); content_html }