Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/strategies/docx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.",
Expand Down
13 changes: 8 additions & 5 deletions src/strategies/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions src/strategies/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
175 changes: 175 additions & 0 deletions src/strategies/pandoc_args.rs
Original file line number Diff line number Diff line change
@@ -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<String>` 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<String>,
pdf_engine: Option<String>,
reference_doc: Option<String>,
}

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 <path>` argument (used by HTML and PDF strategies).
pub fn with_template(mut self, path: impl Into<String>) -> Self {
self.template = Some(path.into());
self
}

/// Add a `--pdf-engine=<engine>` argument (used by the PDF strategy).
pub fn with_pdf_engine(mut self, engine: impl Into<String>) -> Self {
self.pdf_engine = Some(engine.into());
self
}

/// Add a `--reference-doc <path>` argument (used by the DOCX strategy).
pub fn with_reference_doc(mut self, path: impl Into<String>) -> Self {
self.reference_doc = Some(path.into());
self
}

/// Consume the builder and return the assembled argument list.
///
/// The returned `Vec<String>` 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<String> {
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");
}
}
21 changes: 9 additions & 12 deletions src/strategies/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`) \
Expand Down