use std::sync::Arc;
use comrak::nodes::NodeValue;
use comrak::plugins::syntect::SyntectAdapter;
use comrak::{Arena, Options, format_html_with_plugins, options, parse_document};
use crate::highlight;
use crate::theme::SyntaxTheme;
pub fn render(markdown: &str, syntax_theme: Option<&SyntaxTheme>) -> String {
let options = options();
let adapter = match syntax_theme {
Some(st) => highlight::adapter_with_theme(st),
None => highlight::adapter(),
};
let plugins = plugins(&adapter);
let arena = Arena::new();
let root = parse_document(&arena, markdown, &options);
for node in root.descendants() {
let mut data = node.data.borrow_mut();
match &mut data.value {
NodeValue::CodeBlock(code_block) if code_block.info == "mermaid" => {
let html = format!(
"<pre class=\"mermaid\">{}</pre>\n",
html_escape(&code_block.literal)
);
data.value = NodeValue::HtmlBlock(comrak::nodes::NodeHtmlBlock {
block_type: 6,
literal: html,
});
}
NodeValue::HtmlBlock(block) => {
block.literal = rewrite_html_img_srcs(&block.literal);
}
NodeValue::HtmlInline(raw) => {
*raw = rewrite_html_img_srcs(raw);
}
_ => {}
}
}
let mut html = String::new();
format_html_with_plugins(root, &options, &mut html, &plugins).unwrap();
html
}
fn plugins(adapter: &SyntectAdapter) -> options::Plugins<'_> {
let mut plugins = options::Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(adapter);
plugins
}
fn options() -> Options<'static> {
let mut options = Options::default();
options.extension.table = true;
options.extension.strikethrough = true;
options.extension.autolink = true;
options.extension.tasklist = true;
options.extension.footnotes = true;
options.extension.description_lists = true;
options.extension.shortcodes = true;
options.extension.header_ids = Some(String::new());
options.extension.alerts = true;
options.extension.math_dollars = true;
options.extension.math_code = true;
options.extension.image_url_rewriter = Some(Arc::new(|url: &str| {
if should_rewrite(url) {
let clean = url.strip_prefix("./").unwrap_or(url);
format!("/local/{clean}")
} else {
url.to_string()
}
}));
options.parse.smart = true;
options.render.github_pre_lang = true;
options.render.r#unsafe = true;
options.render.sourcepos = true;
options
}
fn should_rewrite(src: &str) -> bool {
!src.is_empty()
&& !src.starts_with("http://")
&& !src.starts_with("https://")
&& !src.starts_with('/')
&& !src.starts_with("data:")
&& !src.starts_with('#')
}
fn rewrite_html_img_srcs(html: &str) -> String {
let mut result = String::with_capacity(html.len());
let mut rest = html;
while let Some(pos) = rest.find("src=\"") {
result.push_str(&rest[..pos]);
let after_src = &rest[pos + 5..]; if let Some(end) = after_src.find('"') {
let url = &after_src[..end];
if should_rewrite(url) {
let clean = url.strip_prefix("./").unwrap_or(url);
result.push_str(&format!("src=\"/local/{clean}\""));
} else {
result.push_str(&format!("src=\"{url}\""));
}
rest = &after_src[end + 1..];
} else {
result.push_str(&rest[pos..]);
rest = "";
break;
}
}
result.push_str(rest);
result
}
fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(c),
}
}
out
}