11use anyhow:: { Context , Result } ;
2+ use std:: path:: Path ;
23use tracing:: info;
34
45use crate :: adapters:: command:: run_command;
@@ -7,18 +8,41 @@ use crate::strategies::OutputStrategy;
78/// Renders a document to HTML format using pandoc.
89pub struct HtmlStrategy {
910 pub template : Option < String > ,
11+ pub template_dir : String ,
1012}
1113
1214impl HtmlStrategy {
13- pub fn new ( template : Option < String > ) -> Self {
14- Self { template }
15+ pub fn new ( template : Option < String > , template_dir : String ) -> Self {
16+ Self { template, template_dir }
1517 }
1618}
1719
1820impl OutputStrategy for HtmlStrategy {
1921 fn render ( & self , input : & str , output_path : & str ) -> Result < ( ) > {
2022 info ! ( input = %input, output = %output_path, template = ?self . template, "Rendering HTML via pandoc" ) ;
21- run_command ( "pandoc" , & [ "--from" , "markdown" , input, "-o" , output_path] )
23+
24+ // Resolve the optional template to a file path within the template directory.
25+ let template_path = if let Some ( ref name) = self . template {
26+ let path = Path :: new ( & self . template_dir ) . join ( name) ;
27+ if !path. exists ( ) {
28+ anyhow:: bail!(
29+ "Template file not found: '{}'. \
30+ Ensure the template exists in the configured template directory.",
31+ path. display( )
32+ ) ;
33+ }
34+ info ! ( "Using template: {}" , name) ;
35+ Some ( path. to_string_lossy ( ) . into_owned ( ) )
36+ } else {
37+ None
38+ } ;
39+
40+ let mut args = vec ! [ "--from" , "markdown" , input, "-o" , output_path] ;
41+ if let Some ( ref path) = template_path {
42+ args. extend_from_slice ( & [ "--template" , path. as_str ( ) ] ) ;
43+ }
44+
45+ run_command ( "pandoc" , & args)
2246 . with_context ( || format ! (
2347 "Failed to render HTML output '{}'. \
2448 Check that pandoc is installed (`pandoc --version`) and that the input file '{}' is valid Markdown.",
@@ -35,7 +59,7 @@ mod tests {
3559
3660 #[ test]
3761 fn test_html_strategy_errors_on_missing_input ( ) {
38- let strategy = HtmlStrategy :: new ( None ) ;
62+ let strategy = HtmlStrategy :: new ( None , "templates" . to_string ( ) ) ;
3963 let result = strategy. render ( "/nonexistent/input.md" , "/tmp/output.html" ) ;
4064 assert ! ( result. is_err( ) ) ;
4165 let msg = format ! ( "{:#}" , result. unwrap_err( ) ) ;
@@ -48,8 +72,41 @@ mod tests {
4872
4973 #[ test]
5074 fn test_html_strategy_stores_template ( ) {
51- let strategy = HtmlStrategy :: new ( Some ( "default" . to_string ( ) ) ) ;
52- assert_eq ! ( strategy. template, Some ( "default" . to_string( ) ) ) ;
75+ let strategy = HtmlStrategy :: new ( Some ( "default.html" . to_string ( ) ) , "templates" . to_string ( ) ) ;
76+ assert_eq ! ( strategy. template, Some ( "default.html" . to_string( ) ) ) ;
77+ }
78+
79+ #[ test]
80+ fn test_html_strategy_missing_template_returns_clear_error ( ) {
81+ use std:: io:: Write ;
82+ use tempfile:: NamedTempFile ;
83+
84+ let mut input = NamedTempFile :: new ( ) . unwrap ( ) ;
85+ writeln ! ( input, "# Hello\n \n This is a test." ) . unwrap ( ) ;
86+
87+ let strategy = HtmlStrategy :: new (
88+ Some ( "nonexistent.html" . to_string ( ) ) ,
89+ "/nonexistent/template/dir" . to_string ( ) ,
90+ ) ;
91+ let result = strategy. render (
92+ input. path ( ) . to_str ( ) . unwrap ( ) ,
93+ "/tmp/output.html" ,
94+ ) ;
95+ assert ! ( result. is_err( ) ) ;
96+ let msg = result. unwrap_err ( ) . to_string ( ) ;
97+ assert ! (
98+ msg. contains( "Template file not found" ) ,
99+ "error should mention missing template, got: {}" ,
100+ msg
101+ ) ;
102+ }
103+
104+ #[ test]
105+ fn test_html_strategy_no_template_does_not_check_template_dir ( ) {
106+ // When no template is configured the template_dir is never accessed,
107+ // so a non-existent directory must not cause an error at construction time.
108+ let strategy = HtmlStrategy :: new ( None , "/nonexistent/dir" . to_string ( ) ) ;
109+ assert ! ( strategy. template. is_none( ) ) ;
53110 }
54111
55112 #[ test]
@@ -64,12 +121,42 @@ mod tests {
64121 let output = NamedTempFile :: new ( ) . unwrap ( ) ;
65122 let output_path = output. path ( ) . with_extension ( "html" ) ;
66123
67- let strategy = HtmlStrategy :: new ( None ) ;
124+ let strategy = HtmlStrategy :: new ( None , "templates" . to_string ( ) ) ;
68125 let result = strategy. render (
69126 input. path ( ) . to_str ( ) . unwrap ( ) ,
70127 output_path. to_str ( ) . unwrap ( ) ,
71128 ) ;
72129 assert ! ( result. is_ok( ) ) ;
73130 assert ! ( output_path. exists( ) ) ;
74131 }
132+
133+ #[ test]
134+ #[ ignore = "requires pandoc to be installed" ]
135+ fn test_html_strategy_with_template_produces_output ( ) {
136+ use std:: fs;
137+ use std:: io:: Write ;
138+ use tempfile:: { NamedTempFile , TempDir } ;
139+
140+ let template_dir = TempDir :: new ( ) . unwrap ( ) ;
141+ let template_path = template_dir. path ( ) . join ( "custom.html" ) ;
142+ // Use pandoc template syntax ($body$) rather than Tera syntax.
143+ fs:: write ( & template_path, "$body$" ) . unwrap ( ) ;
144+
145+ let mut input = NamedTempFile :: new ( ) . unwrap ( ) ;
146+ writeln ! ( input, "# Hello\n \n This is a test." ) . unwrap ( ) ;
147+
148+ let output = NamedTempFile :: new ( ) . unwrap ( ) ;
149+ let output_path = output. path ( ) . with_extension ( "html" ) ;
150+
151+ let strategy = HtmlStrategy :: new (
152+ Some ( "custom.html" . to_string ( ) ) ,
153+ template_dir. path ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ,
154+ ) ;
155+ let result = strategy. render (
156+ input. path ( ) . to_str ( ) . unwrap ( ) ,
157+ output_path. to_str ( ) . unwrap ( ) ,
158+ ) ;
159+ assert ! ( result. is_ok( ) , "expected render to succeed with a valid template: {:?}" , result) ;
160+ assert ! ( output_path. exists( ) ) ;
161+ }
75162}
0 commit comments