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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ outputs:
| `input_format` | ❌ No | auto-detect | Override the input format; auto-detected from file extension when omitted |
| `output_dir` | ❌ No | `dist` | Directory where output files are written |
| `outputs` | ✅ Yes | — | List of one or more output targets (must contain at least one entry) |
| `outputs[].type` | ✅ Yes | — | Output format: `html`, `pdf`, `docx`, or any supported audio/image format |
| `outputs[].type` | ✅ Yes | — | Output format: `html`, `pdf`, `docx`, `epub`, `kepub`, or any supported audio/image format |
| `outputs[].template` | ❌ No | — | Name of a Tera template in the `templates/` directory to use for this output |
| `variables` | ❌ No | `{}` | Map of string key/value pairs injected into the document via `{{key}}` placeholders |

Expand Down Expand Up @@ -398,6 +398,8 @@ When `input_format` is omitted, Renderflow auto-detects the format from the file
| `html` | Renders to HTML | Pandoc |
| `pdf` | Renders to PDF via LaTeX | Pandoc + Tectonic |
| `docx` | Renders to Word document | Pandoc |
| `epub` | Renders a reflowable EPUB 3 | Pandoc |
| `kepub` | Renders EPUB 3, then Kobo enhancements | Pandoc + Kepubify |

Not every input → output combination is supported. For example, `epub` and `latex` inputs cannot currently be converted to `docx`. Renderflow reports a clear error when an unsupported combination is specified.

Expand Down
53 changes: 51 additions & 2 deletions crates/renderflow-core/data/adapter-packs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,46 @@ providers:
upstream: https://github.com/tesseract-ocr/tesseract
rationale: Experimental local OCR provider pending language-pack discovery and confidence-aware extraction evidence.

- id: adapter.ebooks.kepubify
runtime_tool: tool.kepubify
name: Kepubify Kobo EPUB adapter
families: [ebooks]
maturity: integrated
selection_priority: 10
capabilities: [ebook.convert.kepub]
input_media_types: [application/epub+zip]
output_media_types: [application/epub+zip]
determinism: configuration_dependent
locality: local
fidelity: partial_loss
execution: *bounded_local
configuration_schema:
output: declared_artifact_path
validation: [validator.core.epub, validator.epubcheck]
provenance: [provider_id, provider_version, argv_digest, input_digest, output_digest]
upstream: https://github.com/pgaskin/kepubify
rationale: Adopted as a fast, standalone, MIT-licensed EPUB-to-KEPUB provider behind an explicit graph edge.

- id: adapter.ebooks.epubcheck
runtime_tool: tool.epubcheck
name: EPUBCheck conformance adapter
families: [ebooks]
maturity: integrated
selection_priority: 10
capabilities: [ebook.validate.epub]
input_media_types: [application/epub+zip]
output_media_types: [application/json]
determinism: deterministic
locality: local
fidelity: lossless
execution: *bounded_local
configuration_schema:
report_format: json
validation: [validator.epubcheck]
provenance: [provider_id, provider_version, argv_digest, input_digest, report_digest]
upstream: https://github.com/w3c/epubcheck
rationale: Adopted as the official EPUB conformance checker; absence is reported rather than silently treated as a pass.

evaluations:
- candidate: Pandoc
families: [documents, ebooks, office]
Expand Down Expand Up @@ -280,8 +320,17 @@ evaluations:
families: [ebooks]
decision: defer
upstream: https://calibre-ebook.com/
rationale: "Coordinate with EPUB and KEPUB issue #344 so ebook semantics are not duplicated in this foundation."
follow_up: "#344"
rationale: Broader conversion fallback remains deferred until its larger runtime and output-variation surface has dedicated conformance evidence.
- candidate: Kepubify
families: [ebooks]
decision: adopt
upstream: https://github.com/pgaskin/kepubify
rationale: Focused, portable, MIT-licensed KEPUB conversion with a small local-only execution surface.
- candidate: EPUBCheck
families: [ebooks]
decision: adopt
upstream: https://github.com/w3c/epubcheck
rationale: Official EPUB 3.3 conformance evidence complements the native bounded structural inspector.
- candidate: 7-Zip and libarchive
families: [archives]
decision: adapt
Expand Down
40 changes: 40 additions & 0 deletions crates/renderflow-core/data/tool-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,46 @@ tools:
license_notes: "See the Tesseract upstream Apache-2.0 license and language-data licenses."
distribution_notes: "Experimental OCR provider; language-pack identity and confidence evidence are required before graph integration."

- id: tool.kepubify
name: Kepubify
discovery:
kind: executable
candidates: [kepubify]
version_args: [--version]
version:
min_inclusive: "4.0.0"
operating_systems: [linux, macos, windows]
capabilities:
- ebook.convert.kepub
input_media_types: [application/epub+zip]
output_media_types: [application/epub+zip]
determinism: configuration_dependent
locality: local
fidelity: partial_loss
support_tier: optional
license_notes: "Kepubify is MIT-licensed; preserve the upstream license when redistributing it."
distribution_notes: "Optional EPUB-to-KEPUB provider; Renderflow invokes it with direct argv and explicit output paths."

- id: tool.epubcheck
name: EPUBCheck
discovery:
kind: executable
candidates: [epubcheck]
version_args: [--version]
version:
min_inclusive: "5.0.0"
operating_systems: [linux, macos, windows]
capabilities:
- ebook.validate.epub
input_media_types: [application/epub+zip]
output_media_types: [application/json]
determinism: deterministic
locality: local
fidelity: lossless
support_tier: optional
license_notes: "EPUBCheck is available under the BSD 3-Clause license."
distribution_notes: "Optional authoritative EPUB conformance provider; native structural inspection remains available without Java."


- id: tool.upscayl-ncnn
name: Upscayl NCNN
Expand Down
19 changes: 17 additions & 2 deletions crates/renderflow-core/src/adapters/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ impl StrategyArtifactTransform {
})?;
Ok(path)
}

fn requires_text_preparation(&self) -> bool {
matches!(
self.from,
Format::Markdown | Format::Html | Format::Rst | Format::Latex
)
}
}

impl ArtifactTransform for StrategyArtifactTransform {
Expand Down Expand Up @@ -120,12 +127,17 @@ impl ArtifactTransform for StrategyArtifactTransform {
let work_dir = tempfile::tempdir_in(store.temporary_directory())
.context("failed to create strategy adapter work directory")?;
let document_input = document_input_format(self.from);
let input_path = if document_input.is_some() {
let input_path = if self.requires_text_preparation() {
self.prepare_document_input(input, store, work_dir.path())?
} else {
store.payload_path(input)?
};
let output_path = work_dir.path().join(format!("output.{}", self.to));
let output_name = if self.to == Format::Kepub {
"output.kepub.epub".to_string()
} else {
format!("output.{}", self.to)
};
let output_path = work_dir.path().join(output_name);
let strategy = select_strategy(
&self.output_type,
self.template.as_deref(),
Expand Down Expand Up @@ -176,6 +188,7 @@ pub fn document_input_format(format: Format) -> Option<InputFormat> {
Format::Docx => Some(InputFormat::Docx),
Format::Html => Some(InputFormat::Html),
Format::Epub => Some(InputFormat::Epub),
Format::Kepub => Some(InputFormat::Epub),
Format::Rst => Some(InputFormat::Rst),
Format::Latex => Some(InputFormat::Latex),
_ => None,
Expand All @@ -187,6 +200,8 @@ pub fn output_type_for_format(format: Format) -> Option<OutputType> {
Format::Html => Some(OutputType::Html),
Format::Pdf => Some(OutputType::Pdf),
Format::Docx => Some(OutputType::Docx),
Format::Epub => Some(OutputType::Epub),
Format::Kepub => Some(OutputType::Kepub),
_ => {
let value = format.to_string();
if let Ok(audio) = value.parse::<crate::audio::AudioFormat>() {
Expand Down
11 changes: 10 additions & 1 deletion crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use clap::Parser;
use tracing::info;

use crate::cli::{
AiCommands, Cli, Commands, GraphCommands, PluginCommands, SpecCommands, ToolCommands,
AiCommands, Cli, Commands, EbookCommands, GraphCommands, PluginCommands, SpecCommands,
ToolCommands,
};
use crate::{commands, transforms};

Expand Down Expand Up @@ -190,6 +191,14 @@ pub fn run_cli(cli: Cli) -> Result<()> {
format,
} => commands::tools::run_variants(&id, models_dir.as_deref(), &format)?,
},
Some(Commands::Ebook { subcommand }) => match subcommand {
EbookCommands::Inspect {
input,
format,
epubcheck,
} => commands::ebook::run_inspect(&input, &format, epubcheck)?,
EbookCommands::Capabilities { format } => commands::ebook::run_capabilities(&format)?,
},
Some(Commands::Capabilities {
format,
transforms,
Expand Down
1 change: 1 addition & 0 deletions crates/renderflow-core/src/artifact/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ impl MediaType {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
Format::Epub => "application/epub+zip",
Format::Kepub => "application/epub+zip",
Format::Rst => "text/x-rst",
Format::Latex => "application/x-latex",
Format::Fountain => "text/plain",
Expand Down
27 changes: 27 additions & 0 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,13 @@ pub enum Commands {
subcommand: ToolCommands,
},

/// Inspect and validate EPUB/KEPUB publication derivatives.
#[command(subcommand_required = true, arg_required_else_help = true)]
Ebook {
#[command(subcommand)]
subcommand: EbookCommands,
},

/// List stable provider capability IDs and their implementations.
Capabilities {
/// Output format: text (default), json, or yaml.
Expand Down Expand Up @@ -298,6 +305,26 @@ pub enum Commands {
},
}

/// Subcommands for provider-neutral e-book derivatives.
#[derive(Subcommand)]
pub enum EbookCommands {
/// Inspect EPUB 3 structure, metadata, navigation, layout, and accessibility evidence.
Inspect {
#[arg(long, value_name = "FILE")]
input: String,
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
/// Run the optional local EPUBCheck provider and include its report.
#[arg(long)]
epubcheck: bool,
},
/// Print the honest built-in EPUB/KEPUB capability contract.
Capabilities {
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
}

/// Subcommands for `renderflow spec`.
#[derive(Subcommand)]
pub enum SpecCommands {
Expand Down
35 changes: 35 additions & 0 deletions crates/renderflow-core/src/commands/ebook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use std::path::Path;

use anyhow::Result;

use crate::ebook::{inspect_ebook, EbookCapabilityContract};

pub fn run_inspect(input: &str, format: &str, epubcheck: bool) -> Result<()> {
let inspection = inspect_ebook(Path::new(input), epubcheck)?;
emit(&inspection, format)?;
if !inspection.valid
|| inspection.epubcheck.as_ref().and_then(|item| item.passed) == Some(false)
{
anyhow::bail!("e-book validation failed; see emitted evidence");
}
Ok(())
}

pub fn run_capabilities(format: &str) -> Result<()> {
emit(&EbookCapabilityContract::builtin(), format)
}

fn emit<T: serde::Serialize>(value: &T, format: &str) -> Result<()> {
match format.to_ascii_lowercase().as_str() {
"json" => println!("{}", serde_json::to_string_pretty(value)?),
"yaml" | "yml" => print!("{}", serde_yaml_ng::to_string(value)?),
"text" => {
let value = serde_yaml_ng::to_string(value)?;
print!("{value}");
}
other => {
anyhow::bail!("unknown e-book output format '{other}'; supported: text, json, yaml")
}
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/renderflow-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod ai;
pub mod audit;
pub mod build;
pub mod ebook;
pub mod graph;
pub mod inspect;
pub mod plugin;
Expand Down
46 changes: 33 additions & 13 deletions crates/renderflow-core/src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use crate::input_format::InputFormat;
/// tested and validated.
///
/// ```text
/// Markdown → html, pdf, docx
/// Rst → html, pdf, docx
/// Html → html, pdf, docx
/// Markdown → html, pdf, docx, epub, kepub
/// Rst → html, pdf, docx, epub, kepub
/// Html → html, pdf, docx, epub, kepub
/// Docx → html, pdf
/// Epub → html, pdf (epub → docx is not yet supported)
/// Latex → html, pdf (latex → docx is not yet supported)
Expand All @@ -20,26 +20,40 @@ pub fn is_supported(input: &InputFormat, output: &OutputType) -> bool {
// Markdown
(InputFormat::Markdown, OutputType::Html)
| (InputFormat::Markdown, OutputType::Pdf)
| (InputFormat::Markdown, OutputType::Docx) => true,
| (InputFormat::Markdown, OutputType::Docx)
| (InputFormat::Markdown, OutputType::Epub)
| (InputFormat::Markdown, OutputType::Kepub) => true,

// reStructuredText
(InputFormat::Rst, OutputType::Html)
| (InputFormat::Rst, OutputType::Pdf)
| (InputFormat::Rst, OutputType::Docx) => true,
| (InputFormat::Rst, OutputType::Docx)
| (InputFormat::Rst, OutputType::Epub)
| (InputFormat::Rst, OutputType::Kepub) => true,

// HTML (pass-through or convert)
(InputFormat::Html, OutputType::Html)
| (InputFormat::Html, OutputType::Pdf)
| (InputFormat::Html, OutputType::Docx) => true,
| (InputFormat::Html, OutputType::Docx)
| (InputFormat::Html, OutputType::Epub)
| (InputFormat::Html, OutputType::Kepub) => true,

// Docx (reading a Word document)
(InputFormat::Docx, OutputType::Html) | (InputFormat::Docx, OutputType::Pdf) => true,
(InputFormat::Docx, OutputType::Html)
| (InputFormat::Docx, OutputType::Pdf)
| (InputFormat::Docx, OutputType::Epub)
| (InputFormat::Docx, OutputType::Kepub) => true,

// Epub
(InputFormat::Epub, OutputType::Html) | (InputFormat::Epub, OutputType::Pdf) => true,
(InputFormat::Epub, OutputType::Html)
| (InputFormat::Epub, OutputType::Pdf)
| (InputFormat::Epub, OutputType::Kepub) => true,

// LaTeX
(InputFormat::Latex, OutputType::Html) | (InputFormat::Latex, OutputType::Pdf) => true,
(InputFormat::Latex, OutputType::Html)
| (InputFormat::Latex, OutputType::Pdf)
| (InputFormat::Latex, OutputType::Epub)
| (InputFormat::Latex, OutputType::Kepub) => true,

// Everything else — including Unsupported output types — is not supported.
_ => false,
Expand All @@ -48,10 +62,16 @@ pub fn is_supported(input: &InputFormat, output: &OutputType) -> bool {

/// Return the output types that are supported for a given input format.
pub fn supported_outputs_for(input: &InputFormat) -> Vec<OutputType> {
[OutputType::Html, OutputType::Pdf, OutputType::Docx]
.into_iter()
.filter(|o| is_supported(input, o))
.collect()
[
OutputType::Html,
OutputType::Pdf,
OutputType::Docx,
OutputType::Epub,
OutputType::Kepub,
]
.into_iter()
.filter(|o| is_supported(input, o))
.collect()
}

/// Build a user-facing error message for an unsupported input → output pair.
Expand Down
Loading
Loading