Nanoc»Creating PDFs

While Nanoc is typically used to generate websites, it can also be a useful tool when generating other types of content. This guide will give an example of how you could use Nanoc to generate PDFs via LaTeX.

Initial setup

This guide assumes a new site, populated with a handful of Markdown documents in the content/ directory, along with the default stylesheet generated by the create-site command:

% tree content
content
├── stylesheet.css
├── index.md
└── essays
    ├── the-nature-of-infinity.md
    └── self-consistency-in-spacetime.md

The Rules file for such a site will contain some code to handle Markdown files and pass them through kramdown:

compile '/**/*.md' do
  filter :kramdown
  layout '/default.*'
end

route '/**/*.md' do
  if item.identifier =~ '/index.*'
    '/index.html'
  else
    item.identifier.without_ext + '/index.html'
  end
end

compile '/**/*' do
  write item.identifier.to_s
end

layout '/**/*', :erb

Adding PDF support

Generating a PDF from a Markdown document will happen in two steps: first convert Markdown to LaTeX, and then convert LaTeX to PDF. There will be two Nanoc filters that implement these conversions, as well as a compilation rule setup that generates a PDF in addition to the HTML files that are already being generated.

The first step will be a Markdown-to-LaTeX filter. Create lib/filters/kramdown2latex.rb with the following content:

require 'kramdown'

Nanoc::Filter.define(:kramdown2latex) do |content, params|
  ::Kramdown::Document.new(content, params).to_latex
end

In order to generate a proper LaTeX file, however, the output from kramdown needs to be wrapped to make it a proper document. A Nanoc layout fits this use case perfectly. Create layouts/article.tex.erb with the following content:

\documentclass{article}

\usepackage{inputenc}
\usepackage{fontenc}
\usepackage{listings}
\usepackage{hyperref}

\begin{document}

<%= yield %>

\end{document}

The extra packages are needed by kramdown; it generates LaTeX content that makes use of these packages.

Next up is a filter that converts LaTeX to PDF. Since LaTeX is text and PDF is not, the filter will need to convert from :text to :binary. The filter itself will invoke an external executable that generates the PDF (in this case, pdflatex). The filter is somewhat complex, partially due to the complexity of invoking the executable:

Class.new(Nanoc::Filter) do
  identifier :latex2pdf
  type :text => :binary

  TMP_BASENAME = 'nanoc-latex'
  TMP_EXTENSION = '.tex'

  def run(content, params = {})
    Tempfile.open([TMP_BASENAME, TMP_EXTENSION]) do |f|
      f.write(content)
      f.flush

      system(
        'pdflatex',
        '-halt-on-error',
        '-output-directory',
        File.dirname(f.path),
        f.path)

      system('mv', f.path.sub('.tex', '.pdf'), output_filename)
    end
  end
end

With both the Markdown-to-LaTeX and the LaTeX-to-PDF pieces in place, this site is nearly ready to create PDFs. Add the following piece of code to the top of the Rules file:

compile '/**/*.md', rep: :pdf do
  filter :kramdown2latex
  layout '/article.tex.erb'
  filter :latex2pdf
  write item.identifier.without_ext + '.pdf'
end

Note the rep: :pdf on the first line. This means that compilation rule generates a new item representation (named pdf) in addition to the default item representation that already exists (and generates HTML).

Now run nanoc, and you’ll see PDF files being generated into the output/ directory.