use crate::cli::Theme;
use crate::engine::html_to_pdf::render_html_to_pdf;
use crate::theme::get_theme_css;
use anyhow::{Context, Result};
use pulldown_cmark::{html, CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use std::fs;
use std::path::Path;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
pub fn compile_markdown(input_path: &Path, output_pdf: &Path, theme: Theme) -> Result<()> {
let content = fs::read_to_string(input_path)
.with_context(|| format!("Failed to read markdown file: {}", input_path.display()))?;
let abs_input = fs::canonicalize(input_path)
.with_context(|| format!("Failed to canonicalize markdown path: {}", input_path.display()))?;
let parent_dir = abs_input
.parent()
.ok_or_else(|| anyhow::anyhow!("Input file has no parent directory"))?;
let file_stem = abs_input
.file_stem()
.map(|s| s.to_string_lossy())
.unwrap_or_else(|| "document".into());
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
options.insert(Options::ENABLE_MATH);
let parser = Parser::new_ext(&content, options);
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
let syntect_theme = match theme {
Theme::Dark => &theme_set.themes["base16-eighties.dark"],
_ => &theme_set.themes["InspiredGitHub"],
};
let mut events = Vec::new();
let mut in_code_block = false;
let mut code_lang = String::new();
let mut code_content = String::new();
for event in parser {
match event {
Event::Start(Tag::CodeBlock(ref kind)) => {
in_code_block = true;
code_content.clear();
code_lang = match kind {
CodeBlockKind::Fenced(lang) => lang.to_string(),
CodeBlockKind::Indented => String::new(),
};
}
Event::End(TagEnd::CodeBlock) => {
in_code_block = false;
let syntax = syntax_set
.find_syntax_by_token(&code_lang)
.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
match highlighted_html_for_string(
&code_content,
&syntax_set,
syntax,
syntect_theme,
) {
Ok(highlighted) => {
events.push(Event::Html(highlighted.into()));
}
Err(_) => {
let escaped = html_escape(&code_content);
let html = format!("<pre><code>{}</code></pre>", escaped);
events.push(Event::Html(html.into()));
}
}
}
Event::Text(text) if in_code_block => {
code_content.push_str(&text);
}
other => {
events.push(other);
}
}
}
let mut body_html = String::new();
html::push_html(&mut body_html, events.into_iter());
let css = get_theme_css(theme);
let full_html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
<!-- KaTeX for math rendering -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body, {{
delimiters: [
{{left: '$$', right: '$$', display: true}},
{{left: '$', right: '$', display: false}},
{{left: '\\(', right: '\\)', display: false}},
{{left: '\\[', right: '\\]', display: true}}
],
throwOnError: false
}});"></script>
<style>
{}
</style>
</head>
<body>
<article class="markdown-body">
{}
</article>
</body>
</html>"#,
file_stem, css, body_html
);
let temp_html_path = parent_dir.join(format!(".{}_papr_temp.html", file_stem));
fs::write(&temp_html_path, full_html)?;
let result = render_html_to_pdf(&temp_html_path, output_pdf);
let _ = fs::remove_file(temp_html_path);
result
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}