1
0
Fork 0
website/src/markdown.rs
2024-04-17 19:12:59 +02:00

109 lines
3.6 KiB
Rust

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<Regex> = Lazy::new(|| Regex::new(r"^\w+:").unwrap());
static EMAIL_RE: Lazy<Regex> = 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
}