1use std::process::Command;
7
8use crate::errors::{CoreError, CoreResult};
9
10use super::ViewerBackend;
11use super::util::command_on_path;
12
13pub struct HtmlViewer;
15
16fn browser_opener() -> &'static str {
21 if cfg!(target_os = "macos") {
22 "open"
23 } else {
24 "xdg-open"
25 }
26}
27
28impl ViewerBackend for HtmlViewer {
29 fn name(&self) -> &str {
30 "html"
31 }
32
33 fn description(&self) -> &str {
34 "Open the proposal as HTML in the system browser (requires pandoc)"
35 }
36
37 fn is_available(&self) -> bool {
38 command_on_path("pandoc") && command_on_path(browser_opener())
39 }
40
41 fn availability_hint(&self) -> Option<String> {
42 if !command_on_path("pandoc") {
43 return Some(
44 "pandoc is required for the HTML viewer. \
45 Install it from https://pandoc.org/installing.html"
46 .to_string(),
47 );
48 }
49 let opener = browser_opener();
50 if !command_on_path(opener) {
51 return Some(format!(
52 "'{opener}' is required to open the browser. \
53 Please install it or open the HTML file manually."
54 ));
55 }
56 None
57 }
58
59 fn open(&self, content: &str) -> CoreResult<()> {
60 if !command_on_path("pandoc") {
61 return Err(CoreError::not_found(
62 "pandoc is required for the HTML viewer. \
63 Install it from https://pandoc.org/installing.html",
64 ));
65 }
66
67 let opener = browser_opener();
68 if !command_on_path(opener) {
69 return Err(CoreError::not_found(format!(
70 "'{opener}' is required to open the browser. \
71 Please install it or open the HTML file manually.",
72 )));
73 }
74
75 let mut md_file = tempfile::Builder::new()
77 .prefix("ito-viewer-")
78 .suffix(".md")
79 .tempfile()
80 .map_err(|e| CoreError::io("creating temporary markdown file", e))?;
81 std::io::Write::write_all(&mut md_file, content.as_bytes())
82 .map_err(|e| CoreError::io("writing temporary markdown file", e))?;
83
84 let html_path = md_file.path().with_extension("html");
89
90 let pandoc_output = Command::new("pandoc")
92 .arg("--standalone")
93 .arg("--from=markdown")
94 .arg("--to=html5")
95 .arg("-o")
96 .arg(&html_path)
97 .arg(md_file.path())
98 .output()
99 .map_err(|e| CoreError::io("spawning pandoc", e))?;
100
101 if !pandoc_output.status.success() {
102 return Err(CoreError::process(format!(
103 "pandoc failed: {}",
104 String::from_utf8_lossy(&pandoc_output.stderr).trim()
105 )));
106 }
107
108 let open_output = Command::new(opener)
110 .arg(&html_path)
111 .output()
112 .map_err(|e| CoreError::io(format!("spawning {opener}"), e))?;
113
114 if !open_output.status.success() {
115 return Err(CoreError::process(format!(
116 "{opener} failed: {}",
117 String::from_utf8_lossy(&open_output.stderr).trim()
118 )));
119 }
120
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126#[path = "html_tests.rs"]
127mod html_tests;