02.02.2022 20:29

Jinja in LaTeX

When updating your CV, you usually want to get your information straight across several systems. For example, I want to convey the same information on

However, when updating this info in a single one of these systems, I also need to update all the other systems manually. But I am not a computer guy because I want to do things manually. I have a LaTeX CV (as should everyone) and I know a bit about Python. So one obvious option for me was to implement the macro bits in LaTeX and then fill them out using Python. The necessary information that I need can then be gathered from any kind of data source that Python has access to (read: database, API, text files, you name it).

So, let's start with some juicy bits.

I recently wrote my own CV document class so that my main document basically looks like this (Note to myself: I should publish that somewhere):

\documentclass[a4paper,10pt]{cv-superior}
\usepackage[utf8]{inputenc}

\begin{document}
\defcustomlength{\headerheight}{3.6cm}
\defcustomlength{\contentheight}{\dimexpr\paperheight - \headerheight\relax}
\defcustomlength{\leftblockwidth}{.3\paperwidth}
\defcustomlength{\rightblockwidth}{\dimexpr\paperwidth - \leftblockwidth\relax}
\defcustomlength{\entrysep}{.5cm}

\begin{tikzpicture}[remember picture,overlay]

\header{THOMAS}{NIEBLER}{Software Architect}
\section{PROFILE}
\begin{entrylist}
    \plaintextentry{...}
\end{entrylist}

\section{EXPERIENCE}
\begin{entrylist}
    \listentryemployer{Bosch Rexroth AG}{Apr' 19 -- now}

    \listentry{Software Architect}
    {Bosch Rexroth AG}
    {Lohr am Main, Germany}
    {Feb '21 -- now}
    {
        \begin{itemize}
            \item ...
        \end{itemize}
    }

    % some more list entries, eventually with different employers
\end{entrylist}

\section{COMPETENCIES}
\begin{entrylist}
    \simplelistentry{Technical}{
        \begin{itemize}
            \item
        \end{itemize}
    }

    % some more entries like this
\end{entrylist}

% Everything that is a smallsection needs a smallentrylist and appears on the left hand side.
\smallsection{EDUCATION}
\begin{smallentrylist}
    \smalllistentry{PhD, Computer Science}
    {University of W├╝rzburg}
    {Mar '19}

    % ...
\end{smallentrylist}

\smallsection{TECHNOLOGIES}
\begin{smallentrylist}
    \skills{
    LaTeX tinkering/over 9000,
    other stuff/5
    }
\end{smallentrylist}
\end{tikzpicture}
\end{document}

This is for now pure LaTeX. However, with those macros, the whole document generation can easily be done through Jinja templating.

First off, what is Jinja templating?

Using Jinja templating, we can insert some kind of placeholders ("templates") into a document. This is e.g. done in web documents (Jinja is for example used within the Flask framework), but according to the Jinja webpage, it can be used with any kind of text file (yes, it also says LaTeX).

Rendering Jinja templates is a breeze using the Jinja API when using simple HTML pages (example shamelessly copied from the API page):

from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
    loader=PackageLoader("yourapp")
)
my_variable_dict = {
    "my": "variables",
    "are": "defined",
    "right": "here",
    "this_should": "be filled",
    "with some": "python magic"
}

template = env.get_template("mytemplate.html")

output = template.render(**my_variable_dict)

An exemplary yourapp/templates/mytemplate.html could look like this:

<html>
<body>
My {{my}} are {{are}} {% if right is "here" %}
right here
{% else %}
somewhere else
{% endif %} ...
</body>
</html>

And the final result residing in output would look like this:

<html>
<body>
My variables are defined right here ...
</body>
</html>

Now, rendering a Jinja template in LaTeX is a little bit more tricky, as we cannot easily use curled braces and percentage signs for our template indicators, as those signs are also part of LaTeX's syntax. Just imagine a block opening with a comment directly following it, e.g.:

\textbf{%
some bold text
}

This looks like a block opening to Jinja, causing quite some confusion (and obviously: no rendered documents).

Luckily, we just have to adjust the Environment instantiation a bit, for example:

env = Environment(
    loader=PackageLoader("yourapp"),
    block_start_string='<BLOCK>',
    block_end_string='</BLOCK>',
    variable_start_string='<VAR>',
    variable_end_string='</VAR>',
)


With this, we are now ready to render the following LaTeX template:

\documentclass{a4paper, 10pt}

\begin{document}

<BLOCK>for x in range(5)</BLOCK>
  \textbf{<VAR>x</VAR>}\\
<BLOCK>endfor</BLOCK>

\end{document}

resulting in the syntactically perfectly valid LaTeX document:

\documentclass{a4paper, 10pt}

\begin{document}
  \textbf{0}\\
  \textbf{1}\\
  \textbf{2}\\
  \textbf{3}\\
  \textbf{4}\\

\end{document}

The used example code can be found in this GitHub repository.

Going back to my CV, my final document would now look like this:

\documentclass[a4paper,10pt]{cv-superior}
\usepackage[utf8]{inputenc}

\begin{document}
\defcustomlength{\headerheight}{3.6cm}
\defcustomlength{\contentheight}{\dimexpr\paperheight - \headerheight\relax}
\defcustomlength{\leftblockwidth}{.3\paperwidth}
\defcustomlength{\rightblockwidth}{\dimexpr\paperwidth - \leftblockwidth\relax}
\defcustomlength{\entrysep}{.5cm}

\begin{tikzpicture}[remember picture,overlay]

\header{THOMAS}{NIEBLER}{Software Architect}
\section{PROFILE}
\begin{entrylist}
    \plaintextentry{<VAR>profiletext</VAR>}
\end{entrylist}

\section{EXPERIENCE}
\begin{entrylist}
<BLOCK>for entry in experience_list</BLOCK>
    <BLOCK>if entry.employer_with_several_roles_first_role</BLOCK>
    \listentryemployer{<VAR>entry.employer</VAR>}{<VAR>entry.employer_start</VAR> -- <VAR>entry.employer_end</VAR>}
    <BLOCK>endif</BLOCK>
    \listentry{<VAR>entry.role</VAR>}
    {<VAR>entry.employer</VAR>}
    {<VAR>entry.location</VAR>}
    {<VAR>entry.role_start</VAR> -- <VAR>entry.end</VAR>}
    {
        <VAR>entry.content</VAR>
    }
<BLOCK>endfor</BLOCK>
\end{entrylist}

% and so on...

\end{tikzpicture}
\end{document}

So I'd simply loop over all my entries that I had received from some kind of data origin and that's it. To be honest, the final solution is not as practical as I thought it to be as I had other problems afterwards, especially with compiling the code automatically. In the end, Overleaf was a way more convenient alternative.