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
25 changes: 24 additions & 1 deletion crates/renderflow-core/src/strategies/pdf.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::Path;
use tracing::info;

Expand Down Expand Up @@ -38,6 +39,27 @@ impl PdfStrategy {
}
Ok(())
}

/// Add paths owned by Renderflow's template bundle without overriding
/// explicit profile variables. This keeps shared LaTeX components and
/// local font assets discoverable when pandoc stages its intermediate
/// document in a temporary directory.
fn template_variables(&self, variables: &HashMap<String, String>) -> HashMap<String, String> {
let mut resolved = variables.clone();
let template_root = Path::new(&self.template_dir);

for (key, directory) in [
("renderflow-style-root", template_root.join("latex")),
("renderflow-font-root", template_root.join("fonts")),
] {
if !resolved.contains_key(key) && directory.is_dir() {
let path = directory.canonicalize().unwrap_or(directory);
resolved.insert(key.to_string(), path.to_string_lossy().into_owned());
}
}

resolved
}
}

impl OutputStrategy for PdfStrategy {
Expand Down Expand Up @@ -70,6 +92,7 @@ impl OutputStrategy for PdfStrategy {
None
};

let variables = self.template_variables(ctx.variables);
let builder = PandocArgs::new(
ctx.input_format.as_pandoc_format(),
ctx.input_path,
Expand All @@ -80,7 +103,7 @@ impl OutputStrategy for PdfStrategy {
Some(ref path) => builder.with_template(path.as_str()),
None => builder,
}
.with_variables(ctx.variables)
.with_variables(&variables)
.build();
let args_refs: Vec<&str> = args.iter().map(String::as_str).collect();

Expand Down
123 changes: 123 additions & 0 deletions docs/user-guide/latex-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# LaTeX component library

Renderflow's PDF path keeps document meaning in publication profiles and keeps
reusable presentation in a shared LaTeX component library. The current flow is:

```text
publication profile -> Pandoc template -> Renderflow styles -> Tectonic
```

The built-in research template at `templates/research/research.tex` is the
reference composition. Selecting it through the normal PDF target automatically
injects the absolute `renderflow-style-root` for Pandoc. There is no separate
LaTeX executor or manual package installation step.

## Component boundaries

The aggregate package is `templates/latex/renderflow-core.sty`. It centralizes
the design tokens and loads these cohesive components:

| Component | Shared responsibility |
|---|---|
| `renderflow-color.sty` | semantic colors, links, print-safe link behavior |
| `renderflow-typography.sty` | font roles, heading rhythm, quotations |
| `renderflow-layout.sty` | geometry, paragraph rhythm, lists, running furniture |
| `renderflow-figures.sty` | image bounds, placement, captions |
| `renderflow-tables.sty` | booktabs/longtable support and table rhythm |
| `renderflow-code.sty` | Pandoc highlighting containers and listings defaults |
| `renderflow-callouts.sty` | callout and aside presentation primitives |
| `renderflow-metadata.sty` | title/author/date roles and citation-list primitives |

Shared packages own visual treatment. A publication profile or its Pandoc
template still owns article order, front matter, title placement, column model,
editorial section meaning, and which components appear. Do not add research,
magazine, or article semantics to a `.sty` file.

## Compose a profile

A target selects the profile template through the existing artifact graph:

```yaml
targets:
exact:
- id: research-pdf
role: publication/research
format: pdf
template: research/research.tex
variables:
renderflow-color-accent: "4058A6"
renderflow-margin-inner: "28mm"
```

The template declares any overrides before loading the aggregate package:

```tex
\def\RenderflowStyleRoot{templates/latex}
\def\RenderflowColorAccent{4058A6}
\input{\RenderflowStyleRoot/renderflow-core.sty}
```

Profiles can override the supported Pandoc variables without forking the style
library:

- `renderflow-color-accent`, `renderflow-color-ink`,
`renderflow-color-muted`, `renderflow-color-rule`, and
`renderflow-color-surface` accept six-digit hexadecimal colors.
- `renderflow-margin-top`, `renderflow-margin-bottom`,
`renderflow-margin-inner`, `renderflow-margin-outer`, and
`renderflow-paragraph-skip` accept LaTeX dimensions.
- `mainfont`, `sansfont`, and `monofont` select semantic font-family roles.
- `renderflow-main-font-file`, `renderflow-sans-font-file`, and
`renderflow-mono-font-file` select local files under `renderflow-font-root`.

Profiles that need deeper changes may redefine a documented `Renderflow...`
token before loading `renderflow-core.sty`. A change that benefits multiple
profiles belongs in the smallest existing component that owns the concern. Add
a new package only when the concern is cohesive and independently reusable,
then load it from `renderflow-core.sty`.

## Fonts and deterministic fallback

The three font roles default to Latin Modern. Tectonic carries the TeX support
bundle needed for that deterministic fallback, so Renderflow does not assume a
developer workstation font. When `templates/fonts/` exists, the PDF strategy
also injects it as `renderflow-font-root`. A user-provided root remains
authoritative.

Local font variables name files rather than host-installed families. If a
declared file cannot be found, the style layer emits a package warning and uses
the corresponding Latin Modern role. Renderflow does not ship third-party font
binaries; publication owners remain responsible for font licenses and for PDF
font-embedding validation required by their publication contract.

## Pandoc and Tectonic compatibility

The library uses ordinary LaTeX2e packages available to the current Tectonic
toolchain. The research template exposes Pandoc's highlighting macros, table and
figure output, header includes, table of contents, metadata, and supported
Natbib/BibLaTeX hooks. `renderflow-style-root` is an implementation variable;
profile authors normally do not set it.

The synthetic fixture exercises title metadata, headings, prose, a quotation,
callout, table, figure/caption, highlighted code, footnote, and link:

```bash
renderflow build \
--config "tests/fixtures/latex-components/renderflow.yaml"
```

When Tectonic is unavailable, the Pandoc composition can still be inspected
without producing a PDF:

```bash
pandoc "tests/fixtures/latex-components/showcase.md" \
--from "markdown" \
--to "latex" \
--template "templates/research/research.tex" \
--variable "renderflow-style-root=$PWD/templates/latex" \
--output "/tmp/renderflow-component-showcase.tex"
```

Keep custom templates close to Pandoc's current template contract. Pandoc may
add generated commands over time, so profile templates should be checked when
the supported Pandoc tool version changes.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ nav:
- Adapter Ecosystem: user-guide/adapter-ecosystem.md
- EPUB and KEPUB Derivatives: user-guide/ebook-derivatives.md
- Magazine Publications: user-guide/magazine-publications.md
- LaTeX Components: user-guide/latex-components.md
- Lulu Publication Pack: user-guide/lulu-publication-pack.md
- Whole-file Video: handbrake-adapter.md
- Super Resolution: user-guide/super-resolution.md
Expand Down
21 changes: 21 additions & 0 deletions templates/latex/renderflow-callouts.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-callouts.sty}[2026/09/11 Renderflow callouts]

\RequirePackage{framed}
\newenvironment{RenderflowCallout}[1][Note]{%
\def\FrameCommand{%
{\color{RenderflowAccent}\vrule width \RenderflowCalloutRuleWeight}%
\hspace{0.8em}%
}%
\MakeFramed{\advance\hsize-\width\FrameRestore}%
\noindent\textbf{\color{RenderflowAccent}#1}\par\smallskip
}{%
\endMakeFramed
}
\newenvironment{RenderflowAside}{%
\begin{RenderflowCallout}[Aside]\small\color{RenderflowMuted}
}{%
\end{RenderflowCallout}
}

\endinput
22 changes: 22 additions & 0 deletions templates/latex/renderflow-code.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-code.sty}[2026/09/11 Renderflow code presentation]

\RequirePackage{fancyvrb}
\RequirePackage{listings}
\RequirePackage{framed}
\definecolor{shadecolor}{HTML}{\RenderflowColorSurface}
\providecommand{\passthrough}[1]{#1}
\newenvironment{RenderflowShaded}{\begin{snugshade}}{\end{snugshade}}
\lstset{
basicstyle=\small\ttfamily,
backgroundcolor=\color{RenderflowSurface},
rulecolor=\color{RenderflowRule},
breaklines=true,
frame=single,
framerule=\RenderflowRuleWeight,
xleftmargin=0.5em,
xrightmargin=0.5em,
showstringspaces=false
}

\endinput
24 changes: 24 additions & 0 deletions templates/latex/renderflow-color.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-color.sty}[2026/09/11 Renderflow color roles]

\RequirePackage{xcolor}
\definecolor{RenderflowInk}{HTML}{\RenderflowColorInk}
\definecolor{RenderflowMuted}{HTML}{\RenderflowColorMuted}
\definecolor{RenderflowAccent}{HTML}{\RenderflowColorAccent}
\definecolor{RenderflowRule}{HTML}{\RenderflowColorRule}
\definecolor{RenderflowSurface}{HTML}{\RenderflowColorSurface}
\color{RenderflowInk}

\PassOptionsToPackage{hyphens}{url}
\RequirePackage{hyperref}
\IfFileExists{xurl.sty}{\RequirePackage{xurl}}{}
\hypersetup{
colorlinks=true,
linkcolor=RenderflowAccent,
citecolor=RenderflowAccent,
urlcolor=RenderflowAccent,
pdfcreator={Renderflow via Pandoc and LaTeX}
}
\urlstyle{same}

\endinput
32 changes: 32 additions & 0 deletions templates/latex/renderflow-core.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
%% Renderflow shared LaTeX presentation layer.
%% Profiles set semantic token commands before loading this file.
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-core.sty}[2026/09/11 v1.0 Renderflow components]

%% Central design tokens. Profiles may define any token before this package is
%% loaded, or use \renewcommand after loading where LaTeX permits it.
\providecommand{\RenderflowColorInk}{1F2937}
\providecommand{\RenderflowColorMuted}{5F6B7A}
\providecommand{\RenderflowColorAccent}{334E9E}
\providecommand{\RenderflowColorRule}{CBD5E1}
\providecommand{\RenderflowColorSurface}{F3F6FA}
\providecommand{\RenderflowMarginTop}{25mm}
\providecommand{\RenderflowMarginBottom}{25mm}
\providecommand{\RenderflowMarginInner}{30mm}
\providecommand{\RenderflowMarginOuter}{30mm}
\providecommand{\RenderflowParagraphSkip}{0.65em}
\providecommand{\RenderflowHeadingBefore}{1.5ex}
\providecommand{\RenderflowHeadingAfter}{0.8ex}
\providecommand{\RenderflowRuleWeight}{0.4pt}
\providecommand{\RenderflowCalloutRuleWeight}{2pt}

\input{\RenderflowStyleRoot/renderflow-color.sty}
\input{\RenderflowStyleRoot/renderflow-typography.sty}
\input{\RenderflowStyleRoot/renderflow-layout.sty}
\input{\RenderflowStyleRoot/renderflow-figures.sty}
\input{\RenderflowStyleRoot/renderflow-tables.sty}
\input{\RenderflowStyleRoot/renderflow-code.sty}
\input{\RenderflowStyleRoot/renderflow-callouts.sty}
\input{\RenderflowStyleRoot/renderflow-metadata.sty}

\endinput
20 changes: 20 additions & 0 deletions templates/latex/renderflow-figures.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-figures.sty}[2026/09/11 Renderflow figures]

\RequirePackage{graphicx}
\RequirePackage{caption}
\makeatletter
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
\def\fps@figure{htbp}
\makeatother
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
\captionsetup{
font=small,
labelfont=bf,
textfont={color=RenderflowMuted},
labelsep=period,
skip=0.5em
}

\endinput
30 changes: 30 additions & 0 deletions templates/latex/renderflow-layout.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-layout.sty}[2026/09/11 Renderflow page layout]

\RequirePackage[
top=\RenderflowMarginTop,
bottom=\RenderflowMarginBottom,
inner=\RenderflowMarginInner,
outer=\RenderflowMarginOuter
]{geometry}
\RequirePackage{parskip}
\RequirePackage{enumitem}
\RequirePackage{fancyhdr}

\setlength{\parskip}{\RenderflowParagraphSkip}
\setlength{\parindent}{0pt}
\setlength{\emergencystretch}{3em}
\setlist{noitemsep,topsep=0.5em}
\setlength{\headheight}{14pt}

\providecommand{\RenderflowRunningTitle}{}
\providecommand{\RenderflowRunningAuthor}{}
\fancyhf{}
\fancyhead[L]{\small\itshape\color{RenderflowMuted}\RenderflowRunningTitle}
\fancyhead[R]{\small\itshape\color{RenderflowMuted}\RenderflowRunningAuthor}
\fancyfoot[C]{\small\color{RenderflowMuted}\thepage}
\renewcommand{\headrulewidth}{\RenderflowRuleWeight}
\renewcommand{\footrulewidth}{0pt}
\pagestyle{fancy}

\endinput
43 changes: 43 additions & 0 deletions templates/latex/renderflow-metadata.sty
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
\NeedsTeXFormat{LaTeX2e}
\ProvidesFile{renderflow-metadata.sty}[2026/09/11 Renderflow metadata primitives]

\providecommand{\RenderflowDocumentTitle}{}
\providecommand{\RenderflowDocumentSubtitle}{}
\providecommand{\RenderflowDocumentAuthors}{}
\providecommand{\RenderflowDocumentDate}{}
\newcommand{\RenderflowMakeTitle}{%
\begin{center}
{\LARGE\bfseries\color{RenderflowInk}\RenderflowDocumentTitle\par}
\ifx\RenderflowDocumentSubtitle\empty\else
\vspace{0.45em}{\large\color{RenderflowMuted}\RenderflowDocumentSubtitle\par}
\fi
\ifx\RenderflowDocumentAuthors\empty\else
\vspace{0.8em}{\large\RenderflowDocumentAuthors\par}
\fi
\ifx\RenderflowDocumentDate\empty\else
\vspace{0.35em}{\normalsize\color{RenderflowMuted}\RenderflowDocumentDate\par}
\fi
\end{center}
\vspace{1em}
}

\providecommand{\tightlist}{%
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}

\newlength{\cslhangindent}
\setlength{\cslhangindent}{1.5em}
\newlength{\csllabelwidth}
\setlength{\csllabelwidth}{3em}
\newlength{\cslentryspacingunit}
\setlength{\cslentryspacingunit}{\parskip}
\newenvironment{CSLReferences}[2]{%
\setlength{\parindent}{0pt}%
\setlength{\parskip}{#2\cslentryspacingunit}%
\ifodd #1\let\oldpar\par\def\par{\hangindent=\cslhangindent\oldpar}\fi
}{}
\newcommand{\CSLBlock}[1]{#1\hfill\break}
\newcommand{\CSLLeftMargin}[1]{\parbox[t]{\csllabelwidth}{#1}}
\newcommand{\CSLRightInline}[1]{\parbox[t]{\linewidth-\csllabelwidth}{#1}\break}
\newcommand{\CSLIndent}[1]{\hspace{\cslhangindent}#1}

\endinput
Loading
Loading