1use std::fs::File;
8use std::path::Path;
9
10use epub_builder::{EpubBuilder, EpubContent, ZipLibrary};
11
12use crate::error::GenResult;
13
14static DEJAVU_SANS: &[u8] = include_bytes!("../assets/DejaVuSans.ttf");
21
22#[derive(Debug, Default, Clone)]
23pub struct RenderReport {
24 pub epub_written: bool,
25 pub pdf_written: bool,
26 pub warnings: Vec<String>,
27}
28
29pub fn render_handbook_formats(handbook_md: &str, human_dir: &Path) -> GenResult<RenderReport> {
35 let mut report = RenderReport::default();
36
37 match render_pdf(handbook_md, &human_dir.join("handbook.pdf")) {
38 Ok(()) => report.pdf_written = true,
39 Err(e) => report.warnings.push(format!("handbook.pdf: {e}")),
40 }
41
42 match render_epub(handbook_md, &human_dir.join("handbook.epub")) {
43 Ok(()) => report.epub_written = true,
44 Err(e) => report.warnings.push(format!("handbook.epub: {e}")),
45 }
46
47 Ok(report)
48}
49
50fn render_pdf(markdown: &str, out: &Path) -> Result<(), String> {
51 let mut font_config = markdown2pdf::fonts::FontConfig::new();
52 font_config.default_font_source = Some(markdown2pdf::fonts::FontSource::bytes(DEJAVU_SANS));
53
54 markdown2pdf::parse_into_file(
55 markdown.to_string(),
56 out,
57 markdown2pdf::config::ConfigSource::Default,
58 Some(&font_config),
59 )
60 .map_err(|e| e.to_string())
61}
62
63fn render_epub(markdown: &str, out: &Path) -> Result<(), String> {
64 let mut html_body = String::new();
65 pulldown_cmark::html::push_html(&mut html_body, pulldown_cmark::Parser::new(markdown));
66 let xhtml = format!(
67 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
68 <!DOCTYPE html>\n\
69 <html xmlns=\"http://www.w3.org/1999/xhtml\">\n\
70 <head><title>Handbook</title></head>\n\
71 <body>\n{html_body}\n</body>\n</html>"
72 );
73
74 let zip = ZipLibrary::new().map_err(|e| e.to_string())?;
75 let mut builder = EpubBuilder::new(zip).map_err(|e| e.to_string())?;
76 builder
77 .metadata("title", "Handbook")
78 .map_err(|e| e.to_string())?;
79 builder
80 .add_content(EpubContent::new("handbook.xhtml", xhtml.as_bytes()).title("Handbook"))
81 .map_err(|e| e.to_string())?;
82
83 let mut file = File::create(out).map_err(|e| e.to_string())?;
84 builder.generate(&mut file).map_err(|e| e.to_string())?;
85 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use tempfile::TempDir;
92
93 #[test]
94 fn renders_both_formats_for_simple_markdown() {
95 let tmp = TempDir::new().unwrap();
96 let markdown = "# Handbook\n\nSome **bold** text and a list:\n\n- one\n- two\n";
97
98 let report = render_handbook_formats(markdown, tmp.path()).unwrap();
99
100 assert!(
101 report.warnings.is_empty(),
102 "warnings: {:?}",
103 report.warnings
104 );
105 assert!(report.pdf_written);
106 assert!(report.epub_written);
107
108 let pdf = tmp.path().join("handbook.pdf");
109 let epub = tmp.path().join("handbook.epub");
110 assert!(pdf.exists());
111 assert!(epub.exists());
112 assert!(std::fs::metadata(&pdf).unwrap().len() > 0);
113 assert!(std::fs::metadata(&epub).unwrap().len() > 0);
114 }
115
116 #[test]
117 fn renders_empty_markdown_without_error() {
118 let tmp = TempDir::new().unwrap();
119 let report = render_handbook_formats("", tmp.path()).unwrap();
120 assert!(
121 report.warnings.is_empty(),
122 "warnings: {:?}",
123 report.warnings
124 );
125 assert!(report.pdf_written);
126 assert!(report.epub_written);
127 }
128}