From eb79d1cad91ec8cc1ccd11226300f1b15bc5faaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:28:54 +0000 Subject: [PATCH 1/2] Initial plan From bc3b1a21fcc2f956b823446933d7ca1d46290888 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:37:19 +0000 Subject: [PATCH 2/2] Extract PandocArgs builder for consistent pandoc command construction Agent-Logs-Url: https://github.com/egohygiene/renderflow/sessions/2ad1a11f-c40f-44c1-9e3c-45fa29dcfed7 Co-authored-by: szmyty <14865041+szmyty@users.noreply.github.com> --- src/strategies/docx.rs | 13 ++- src/strategies/html.rs | 13 ++- src/strategies/mod.rs | 2 + src/strategies/pandoc_args.rs | 175 ++++++++++++++++++++++++++++++++++ src/strategies/pdf.rs | 21 ++-- 5 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 src/strategies/pandoc_args.rs diff --git a/src/strategies/docx.rs b/src/strategies/docx.rs index 62aba75..8baa1af 100644 --- a/src/strategies/docx.rs +++ b/src/strategies/docx.rs @@ -3,7 +3,7 @@ use std::path::Path; use tracing::info; use crate::adapters::command::run_command; -use crate::strategies::{OutputStrategy, RenderContext}; +use crate::strategies::{OutputStrategy, PandocArgs, RenderContext}; /// Renders a document to DOCX (Microsoft Word) format using pandoc. pub struct DocxStrategy { @@ -39,12 +39,15 @@ impl OutputStrategy for DocxStrategy { None }; - let mut args = vec!["--from", ctx.input_format.as_pandoc_format(), ctx.input_path, "-o", ctx.output_path]; - if let Some(ref path) = reference_doc { - args.extend_from_slice(&["--reference-doc", path.as_str()]); + let builder = PandocArgs::new(ctx.input_format.as_pandoc_format(), ctx.input_path, ctx.output_path); + let args = match reference_doc { + Some(ref path) => builder.with_reference_doc(path.as_str()), + None => builder, } + .build(); + let args_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - run_command("pandoc", &args) + run_command("pandoc", &args_refs) .with_context(|| format!( "Failed to render DOCX output '{}'. \ Check that pandoc is installed (`pandoc --version`) and that the input file '{}' is valid Markdown.", diff --git a/src/strategies/html.rs b/src/strategies/html.rs index 05c9485..94fca98 100644 --- a/src/strategies/html.rs +++ b/src/strategies/html.rs @@ -3,7 +3,7 @@ use std::path::Path; use tracing::info; use crate::adapters::command::run_command; -use crate::strategies::{OutputStrategy, RenderContext}; +use crate::strategies::{OutputStrategy, PandocArgs, RenderContext}; /// Renders a document to HTML format using pandoc. pub struct HtmlStrategy { @@ -37,12 +37,15 @@ impl OutputStrategy for HtmlStrategy { None }; - let mut args = vec!["--from", ctx.input_format.as_pandoc_format(), ctx.input_path, "-o", ctx.output_path]; - if let Some(ref path) = template_path { - args.extend_from_slice(&["--template", path.as_str()]); + let builder = PandocArgs::new(ctx.input_format.as_pandoc_format(), ctx.input_path, ctx.output_path); + let args = match template_path { + Some(ref path) => builder.with_template(path.as_str()), + None => builder, } + .build(); + let args_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - run_command("pandoc", &args) + run_command("pandoc", &args_refs) .with_context(|| format!( "Failed to render HTML output '{}'. \ Check that pandoc is installed (`pandoc --version`) and that the input file '{}' is valid Markdown.", diff --git a/src/strategies/mod.rs b/src/strategies/mod.rs index 571d4ea..814105c 100644 --- a/src/strategies/mod.rs +++ b/src/strategies/mod.rs @@ -1,11 +1,13 @@ pub mod docx; pub mod html; +pub mod pandoc_args; pub mod pdf; pub mod selector; pub mod strategy; pub use docx::DocxStrategy; pub use html::HtmlStrategy; +pub use pandoc_args::PandocArgs; pub use pdf::PdfStrategy; pub use selector::select_strategy; pub use strategy::{OutputStrategy, RenderContext}; diff --git a/src/strategies/pandoc_args.rs b/src/strategies/pandoc_args.rs new file mode 100644 index 0000000..ea09cab --- /dev/null +++ b/src/strategies/pandoc_args.rs @@ -0,0 +1,175 @@ +/// Builder for pandoc command-line arguments. +/// +/// Centralises argument construction so that all strategies produce a +/// consistent invocation of `pandoc`. Call [`PandocArgs::new`] with the +/// mandatory arguments, chain optional modifiers, then call [`PandocArgs::build`] +/// to obtain the final `Vec` ready to pass to a command runner. +/// +/// # Example +/// +/// ```rust +/// use renderflow::strategies::pandoc_args::PandocArgs; +/// +/// let args = PandocArgs::new("markdown", "/tmp/input.md", "/tmp/output.html") +/// .with_template("/tmp/templates/default.html") +/// .build(); +/// ``` +pub struct PandocArgs { + input_format: String, + input_path: String, + output_path: String, + template: Option, + pdf_engine: Option, + reference_doc: Option, +} + +impl PandocArgs { + /// Create a new builder with the three mandatory pandoc arguments. + /// + /// * `input_format` – value passed to `--from` (e.g. `"markdown"`) + /// * `input_path` – path of the source document + /// * `output_path` – destination path for the rendered output + pub fn new(input_format: &str, input_path: &str, output_path: &str) -> Self { + Self { + input_format: input_format.to_owned(), + input_path: input_path.to_owned(), + output_path: output_path.to_owned(), + template: None, + pdf_engine: None, + reference_doc: None, + } + } + + /// Add a `--template ` argument (used by HTML and PDF strategies). + pub fn with_template(mut self, path: impl Into) -> Self { + self.template = Some(path.into()); + self + } + + /// Add a `--pdf-engine=` argument (used by the PDF strategy). + pub fn with_pdf_engine(mut self, engine: impl Into) -> Self { + self.pdf_engine = Some(engine.into()); + self + } + + /// Add a `--reference-doc ` argument (used by the DOCX strategy). + pub fn with_reference_doc(mut self, path: impl Into) -> Self { + self.reference_doc = Some(path.into()); + self + } + + /// Consume the builder and return the assembled argument list. + /// + /// The returned `Vec` can be converted to `Vec<&str>` for use with + /// [`crate::adapters::command::run_command`]: + /// + /// ```rust,ignore + /// let args = builder.build(); + /// let args_refs: Vec<&str> = args.iter().map(String::as_str).collect(); + /// run_command("pandoc", &args_refs)?; + /// ``` + pub fn build(self) -> Vec { + let mut args = vec![ + "--from".to_owned(), + self.input_format, + self.input_path, + "-o".to_owned(), + self.output_path, + ]; + + if let Some(engine) = self.pdf_engine { + args.push(format!("--pdf-engine={engine}")); + } + + if let Some(template) = self.template { + args.push("--template".to_owned()); + args.push(template); + } + + if let Some(reference_doc) = self.reference_doc { + args.push("--reference-doc".to_owned()); + args.push(reference_doc); + } + + args + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_minimal_args() { + let args = PandocArgs::new("markdown", "input.md", "output.html").build(); + assert_eq!(args, vec!["--from", "markdown", "input.md", "-o", "output.html"]); + } + + #[test] + fn test_build_with_template() { + let args = PandocArgs::new("markdown", "input.md", "output.html") + .with_template("/templates/default.html") + .build(); + assert_eq!( + args, + vec!["--from", "markdown", "input.md", "-o", "output.html", "--template", "/templates/default.html"] + ); + } + + #[test] + fn test_build_with_pdf_engine() { + let args = PandocArgs::new("markdown", "input.md", "output.pdf") + .with_pdf_engine("tectonic") + .build(); + assert_eq!( + args, + vec!["--from", "markdown", "input.md", "-o", "output.pdf", "--pdf-engine=tectonic"] + ); + } + + #[test] + fn test_build_with_pdf_engine_and_template() { + let args = PandocArgs::new("markdown", "input.md", "output.pdf") + .with_pdf_engine("tectonic") + .with_template("/templates/default.tex") + .build(); + assert_eq!( + args, + vec![ + "--from", "markdown", "input.md", "-o", "output.pdf", + "--pdf-engine=tectonic", + "--template", "/templates/default.tex", + ] + ); + } + + #[test] + fn test_build_with_reference_doc() { + let args = PandocArgs::new("markdown", "input.md", "output.docx") + .with_reference_doc("/templates/reference.docx") + .build(); + assert_eq!( + args, + vec![ + "--from", "markdown", "input.md", "-o", "output.docx", + "--reference-doc", "/templates/reference.docx", + ] + ); + } + + #[test] + fn test_build_different_input_formats() { + for (format, expected) in [("rst", "rst"), ("html", "html"), ("latex", "latex"), ("docx", "docx")] { + let args = PandocArgs::new(format, "input", "output").build(); + assert_eq!(args[1], expected, "input format should be passed as-is"); + } + } + + #[test] + fn test_no_optional_flags_when_not_set() { + let args = PandocArgs::new("markdown", "input.md", "output.html").build(); + assert!(!args.iter().any(|a| a.contains("--template")), "should not have --template"); + assert!(!args.iter().any(|a| a.contains("--pdf-engine")), "should not have --pdf-engine"); + assert!(!args.iter().any(|a| a.contains("--reference-doc")), "should not have --reference-doc"); + } +} diff --git a/src/strategies/pdf.rs b/src/strategies/pdf.rs index 0f73de7..6cca502 100644 --- a/src/strategies/pdf.rs +++ b/src/strategies/pdf.rs @@ -4,7 +4,7 @@ use std::path::Path; use tracing::info; use crate::adapters::command::run_command; -use crate::strategies::{OutputStrategy, RenderContext}; +use crate::strategies::{OutputStrategy, PandocArgs, RenderContext}; /// Renders a document to PDF format using pandoc with the tectonic PDF engine. pub struct PdfStrategy { @@ -58,19 +58,16 @@ impl OutputStrategy for PdfStrategy { None }; - let mut args = vec![ - "--from", - ctx.input_format.as_pandoc_format(), - ctx.input_path, - "-o", - ctx.output_path, - "--pdf-engine=tectonic", - ]; - if let Some(ref path) = template_path { - args.extend_from_slice(&["--template", path.as_str()]); + let builder = PandocArgs::new(ctx.input_format.as_pandoc_format(), ctx.input_path, ctx.output_path) + .with_pdf_engine("tectonic"); + let args = match template_path { + Some(ref path) => builder.with_template(path.as_str()), + None => builder, } + .build(); + let args_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - run_command("pandoc", &args) + run_command("pandoc", &args_refs) .with_context(|| format!( "Failed to render PDF output '{}'. \ Check that pandoc and tectonic are installed (`pandoc --version`, `tectonic --version`) \