1use comrak::{parse_document, format_html_with_plugins, Arena, Options, Plugins};
2use comrak::nodes::AstNode;
3
4use mdpdf_theme::{Layout, Theme};
5
6use crate::directives;
7use crate::frontmatter::parse_frontmatter;
8use crate::highlight::SyntectHighlighter;
9
10pub struct RenderResult {
12 pub html: String,
13 pub layout: Layout,
14}
15
16pub fn render_to_html(src: &str, theme: Theme, dense: bool) -> RenderResult {
25 let (fm, body) = parse_frontmatter(src);
26 let layout = if dense || fm.is_dense() {
27 Layout::Dense
28 } else {
29 Layout::Normal
30 };
31
32 let highlighter = SyntectHighlighter::new(theme.syntect_theme());
33
34 let mut options = Options::default();
35 options.extension.table = true;
36 options.extension.strikethrough = true;
37 options.extension.autolink = true;
38 options.render.unsafe_ = true; let mut plugins = Plugins::default();
41 plugins.render.codefence_syntax_highlighter = Some(&highlighter);
42
43 let arena = Arena::new();
45 let root = parse_document(&arena, body, &options);
46 directives::transform(root);
47 let rendered = render_ast(root, &options, &plugins);
48
49 let css = mdpdf_theme::generate_css(&theme, &layout);
50
51 let h1_font_size = match layout {
53 Layout::Dense => "18pt",
54 Layout::Normal => "22pt",
55 };
56 let header_html = match &fm.title {
57 Some(title) => {
58 let subtitle = fm.subtitle.as_deref().map(|s| {
59 format!("<div class=\"subtitle\">{s}</div>")
60 }).unwrap_or_default();
61 let date = fm.date.as_deref().map(|d| {
62 format!("<div class=\"date\">{d}</div>")
63 }).unwrap_or_default();
64 format!(
65 r#"<div class="doc-header">
66 <h1 style="border:none;margin:0;padding:0;font-size:{h1_font_size};color:var(--header-fg)">{title}</h1>
67 {subtitle}
68 {date}
69 </div>"#,
70 )
71 }
72 None => String::new(),
73 };
74
75 let html = format!(
76 r#"<!DOCTYPE html>
77<html>
78<head>
79 <meta charset="utf-8">
80 <style>{css}</style>
81</head>
82<body>
83 {header_html}
84 {rendered}
85</body>
86</html>"#,
87 );
88
89 RenderResult { html, layout }
90}
91
92fn render_ast<'a>(root: &'a AstNode<'a>, options: &Options, plugins: &Plugins<'_>) -> String {
93 let mut output = Vec::new();
94 format_html_with_plugins(root, options, &mut output, plugins)
95 .expect("HTML rendering failed");
96 String::from_utf8(output).expect("HTML output was not valid UTF-8")
97}