2023-07-29 15:32:05 +02:00
|
|
|
use crate::helpers;
|
|
|
|
use crate::hilighting;
|
|
|
|
use axum::http::Uri;
|
2023-07-29 20:22:13 +02:00
|
|
|
use cached::once_cell::sync::Lazy;
|
2023-06-18 11:34:08 +02:00
|
|
|
use pulldown_cmark::Event;
|
|
|
|
use pulldown_cmark::Tag;
|
|
|
|
use pulldown_cmark::{Options, Parser};
|
2023-07-29 20:22:13 +02:00
|
|
|
use regex::Regex;
|
2023-11-10 23:09:00 +01:00
|
|
|
use tracing::instrument;
|
2023-07-29 20:22:13 +02:00
|
|
|
|
|
|
|
static STARTS_WITH_SCHEMA_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w+:").unwrap());
|
|
|
|
static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^.+?@\w+(\.\w+)*$").unwrap());
|
2023-06-18 11:34:08 +02:00
|
|
|
|
2023-11-10 23:09:00 +01:00
|
|
|
#[instrument(skip(markdown))]
|
2023-07-29 15:32:05 +02:00
|
|
|
pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String {
|
2023-07-29 20:22:13 +02:00
|
|
|
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);
|
|
|
|
|
2023-06-18 11:34:08 +02:00
|
|
|
let mut content_html = String::new();
|
2023-07-29 20:22:13 +02:00
|
|
|
let parser = Parser::new_ext(markdown, opt);
|
2023-06-18 11:34:08 +02:00
|
|
|
|
|
|
|
let mut code_block = false;
|
|
|
|
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_block {
|
|
|
|
code_accumulator.push_str(&text);
|
2023-06-18 11:46:49 +02:00
|
|
|
} else {
|
2023-06-18 11:34:08 +02:00
|
|
|
events.push(Event::Text(text));
|
|
|
|
}
|
|
|
|
}
|
2023-07-29 15:32:05 +02:00
|
|
|
Event::Start(Tag::Link(t, mut link, title)) => {
|
|
|
|
if let Some(uri) = base_uri {
|
2023-07-29 20:22:13 +02:00
|
|
|
if !link.starts_with('#') && !STARTS_WITH_SCHEMA_RE.is_match(&link) && !EMAIL_RE.is_match(&link) {
|
2023-07-29 15:32:05 +02:00
|
|
|
// convert relative URIs to absolute URIs
|
|
|
|
link = helpers::uri_with_path(uri, &link).to_string().into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
events.push(Event::Start(Tag::Link(t, link, title)));
|
|
|
|
}
|
|
|
|
Event::Start(Tag::Image(t, mut link, title)) => {
|
|
|
|
if let Some(uri) = base_uri {
|
|
|
|
if !link.contains("://") && !link.contains('@') {
|
|
|
|
// convert relative URIs to absolute URIs
|
|
|
|
link = helpers::uri_with_path(uri, &link).to_string().into();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
events.push(Event::Start(Tag::Image(t, link, title)));
|
|
|
|
}
|
2023-06-18 11:34:08 +02:00
|
|
|
Event::Start(Tag::CodeBlock(kind)) => {
|
|
|
|
code_block = true;
|
|
|
|
if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind {
|
|
|
|
code_lang = Some(lang);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Event::End(Tag::CodeBlock(_)) => {
|
|
|
|
code_block = false;
|
|
|
|
|
|
|
|
let lang = code_lang.take().unwrap_or("".into());
|
2023-07-29 11:51:04 +02:00
|
|
|
let res = hilighting::hilight(&code_accumulator, &lang, Some("base16-ocean.dark"))
|
|
|
|
.unwrap();
|
2023-06-18 11:34:08 +02:00
|
|
|
|
|
|
|
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());
|
|
|
|
|
2023-07-29 12:04:37 +02:00
|
|
|
content_html
|
2023-06-18 11:34:08 +02:00
|
|
|
}
|