Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

LaTeX & Typst (papers, theses, resumes and slides)  Module 12 – Automation, collaboration, conversion and publishing

Choosing LaTeX or Typst, and migrating

Choose LaTeX or Typst from the requirements that decide alone, then move a report to Typst with pandoc and prove it, with an audit, fixes, counts and timings.

  • Intermediate
  • 20 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7, pandoc 3.12 and poppler 26.09.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Choose LaTeX or Typst for a project from the requirements that decide by themselves, then from measured differences
  • Estimate the work of a migration by auditing what pandoc cannot carry over, before anything is converted
  • Migrate a LaTeX report to Typst and prove it with counts of sections, equations, tables and figures
  • Check markup and references an assistant suggests by compiling them and looking up every DOI
  • Organise a project so that its data, references and figures sit in files that both systems read

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose LaTeX or Typst for a project from the requirements that decide by themselves, then from measured differences
  • Estimate the work of a migration by auditing what pandoc cannot carry over, before anything is converted
  • Migrate a LaTeX report to Typst and prove it with counts of sections, equations, tables and figures
  • Check markup and references an assistant suggests by compiling them and looking up every DOI
  • Organise a project so that its data, references and figures sit in files that both systems read

Before you start

On this page

A four-page LaTeX report took 7.80 seconds to build from a clean folder in this lesson’s session, and its Typst version 0.16 seconds. That difference counts for nothing if the journal builds only LaTeX source, and most projects are settled by one or two requirements of that kind before speed, error messages or habit come into it.

This lesson turns those requirements into a decision you can defend, then moves the report to Typst: an audit counts 23 places to fix or check before anything converts, a script makes the fixes, and counts prove the result. It ends with the checks for markup and references an assistant suggests, and with the layout that keeps a project portable. The sessions ran on TeX Live 2026 (TinyTeX 2026.10), Typst 0.15.1 and pandoc 3.12.

A few requirements decide the system by themselves

Start with what the document must satisfy that nobody in the group can change. Four requirements settle the choice on their own, and the first one that applies wins:

Requirement Points to Because
The venue builds LaTeX source or gives its own LaTeX class LaTeX A journal that compiles its class cannot use a Typst file
The PDF must meet PDF/UA: ISO 14289, the standard for accessible PDF files: tagged content, a title and a structure that assistive software can follow. PDF/UA-1 builds on PDF 1.7 and PDF/UA-2 on PDF 2.0. More in “A one-page resume in LaTeX that parses” LaTeX, with LuaLaTeX Typst’s accessibility guide lists PDF/UA-1 as supported and PDF/UA-2 as planned
Something the document needs exists only as a LaTeX package LaTeX A department’s thesis class or a drawing built on PGF/TikZ has no Typst copy
Every writer already works in one system That system Their edits come back in that system’s files

arXiv is the exception that people get wrong. It compiles TeX source itself and says a PDF created from TeX “will typically be rejected”, but a paper written in Typst is not TeX: it goes to arXiv as one PDF, with every font embedded and no JavaScript, as Submitting to arXiv and journals shows. For tagged PDF, the LaTeX tagging instructions call LuaLaTeX the preferred engine for new documents, and only with LuaLaTeX can the MathML of every equation be made automatically.

Typst’s own guide for LaTeX users names two limits that can become such a requirement. Its plotting ecosystem does not yet offer the breadth of PGF/TikZ, though “the ecosystem around the cetz package is catching up quickly”, and in Typst a change of the page margins forces a page break, where LaTeX can change them anywhere. A document that needs either belongs in LaTeX. Answer the questions for your own document; each answer rules out what cannot meet it, or adds the caveat that comes with it:

Interactive · answer and compare Which system fits your document? Answer four questions about your document. Each answer rules out a system that cannot meet it, or adds the caveat that comes with it.
Does the venue build LaTeX source, or give its own LaTeX class?
Must the PDF meet PDF/UA-2?
Does the document need many plots or drawings made inside the document itself?
Will you rebuild the whole document many times an hour while you write?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

pdfLaTeX, LuaLaTeX and Typst against the requirements that decide alone and the differences measured in this lesson
System Venue wants LaTeX source PDF/UA-2 today Plots and drawings Full build of the report A misspelt option
LaTeX with pdfLaTeX Built with: latexmk -pdf Good LaTeX source, and pdflatex is on arXiv's list of processors Mixed tags too, but the tagging instructions prefer LuaLaTeX for new documents Good PGF/TikZ and pgfplots, the breadth Typst's guide says it lacks Mixed 3 pdflatex and 2 BibTeX runs: 7.80 s from a clean folder Poor reported at line 1017 of geometry.sty, not at line 3 of the file
LaTeX with LuaLaTeX Built with: latexmk -lualatex Mixed LaTeX source, but arXiv's processors do not include LuaLaTeX Good the preferred engine for tagging, and the one that adds MathML by itself Good the same PGF/TikZ and pgfplots Mixed 3 lualatex and 2 BibTeX runs: 8.40 s from a clean folder Poor the same keyval error at line 1017 of geometry.sty
Typst Built with: typst compile Poor no LaTeX source: a journal that compiles its class cannot use it Poor PDF/UA-1 is supported; PDF/UA-2 is planned, its accessibility guide says Mixed CeTZ is catching up quickly, its guide says, without PGF/TikZ's breadth yet Good one compile: 0.16 s for the same report Good reported at margins.typ line 2, column 23, under the argument
How the answers decide
  • “Does the venue build LaTeX source, or give its own LaTeX class?” Yes, LaTeX source or its class: rules out Typst. A journal that compiles its own LaTeX class needs .tex files; a Typst paper would have to be converted and checked first.
  • “Does the venue build LaTeX source, or give its own LaTeX class?” Yes, LaTeX source or its class: adds a caveat to LaTeX with LuaLaTeX. arXiv lists tex, pdftex, latex, pdflatex and xelatex as its processors; check that the venue runs LuaLaTeX before relying on it.
  • “Must the PDF meet PDF/UA-2?” Yes, PDF/UA-2: rules out Typst. Typst's accessibility guide lists PDF/UA-1 as supported and PDF/UA-2 as planned, not yet available.
  • “Must the PDF meet PDF/UA-2?” Yes, PDF/UA-2: adds a caveat to LaTeX with pdfLaTeX. The LaTeX tagging instructions prefer LuaLaTeX for new documents, and only LuaLaTeX makes the MathML of equations automatically.
  • “Does the document need many plots or drawings made inside the document itself?” Yes, many: adds a caveat to Typst. Typst's guide for LaTeX users says its plotting ecosystem does not yet offer the breadth of PGF/TikZ.
  • “Will you rebuild the whole document many times an hour while you write?” Yes: adds a caveat to LaTeX with pdfLaTeX and LaTeX with LuaLaTeX. A full build of the report took 7.80 s with pdfLaTeX and 8.40 s with LuaLaTeX, against 0.16 s with Typst; time your own document.

Only the last requirement can point to Typst. When none applies, the measured differences of the next section decide.

Choosing by habit

Wrong: starting a paper in the system you used last, and reading the venue’s author instructions at the deadline. Right: read them first, write the requirements into the project’s README, and choose from that list.

Checkpoint

Check: Requirements that decide alone

  1. Question 1 of 2 A group that writes everything in Typst must send a paper to a journal that compiles its own LaTeX class. Which system should the paper be written in?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: LaTeX, in the journal's class, because the venue's requirement is checked first

    The requirements are checked in order and the first that applies decides. A venue that builds LaTeX source comes first, because nothing else matters if the venue cannot take the file.

  2. Question 2 of 2 Which statement matches the documentation this lesson cites about PDF/UA?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Typst supports PDF/UA-1 and plans PDF/UA-2; LaTeX's tagging instructions prefer LuaLaTeX for new documents

    A PDF that must meet PDF/UA-2 today points to LaTeX built with LuaLaTeX, which also makes the MathML of equations by itself.

Measured on one report: Typst builds faster and points at the faulty line

When no requirement decides, two differences can be measured on your own document: how long a full build takes, and how precisely an error is reported. The report that the next sections migrate was built from a clean copy of its sources three times by each build, and timings.py printed the median:

The same report built by pdfLaTeX, LuaLaTeX and Typst Python · timing_session.py
from session import files, needs, run

# The report built from a clean copy of its sources, three times by each build, timed by timings.py, which prints the
# median. Typst builds main.typ with the body that this lesson's migration converts and fixes.
needs("pdflatex", "lualatex", "latexmk", "pandoc", "typst", "pdftotext", "python3")
files("report", "figures.py", "fixes.py", "timings.py")
run("""
python3 figures.py report
cd report && pandoc report.tex -t typst -o body.typ && python3 ../fixes.py body.typ > /dev/null
python3 timings.py report
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ python3 figures.py report
$ cd report && pandoc report.tex -t typst -o body.typ && python3 ../fixes.py body.typ > /dev/null
$ python3 timings.py report
build               runs                 pages  seconds
latexmk, pdfLaTeX   3 pdflatex, 2 BibTeX     4     7.80
latexmk, LuaLaTeX   3 lualatex, 2 BibTeX     4     8.40
typst compile       1 compile                3     0.16

This output changes from run to run: the seconds depend on the computer and its load, so they change from run to run; the runs and the pages do not

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timing_session.py

latexmk ran pdfLaTeX three times and BibTeX twice, as the references and citations needed, and took 7.80 seconds; with LuaLaTeX, the preferred engine for tagged PDF, it took 8.40 seconds. Typst compiled once in 0.16 seconds. Time your own document before you count on a number: the runs and pages stay the same on every computer, the seconds do not.

A misspelt option shows the other difference. Both files below ask for margins, where geometry’s option and Typst’s page parameter are both called margin:

One misspelt option name in each system

typo/margins.tex

% margins.tex: a page set-up with one misspelt option name, margins for margin.
\documentclass{article}
\usepackage[margins=2cm]{geometry}
\begin{document}
Two gauges logged the rain.
\end{document}

typo/margins.typ

// margins.typ: the same page set-up in Typst, with the same misspelt argument name.
#set page(paper: "a4", margins: 2cm)
Two gauges logged the rain.
Where pdfLaTeX, LuaLaTeX and Typst report the misspelt option Python · errors_session.py
from session import files, needs, run

# One misspelt option name, margins for margin, in each system: where pdfLaTeX, LuaLaTeX and Typst report it, then
# the builds with the name that geometry's documentation and Typst's page reference give.
needs("pdflatex", "lualatex", "typst")
files("typo/margins.tex", "typo/margins.typ")
run(r"""
!pdflatex -interaction=nonstopmode -halt-on-error -file-line-error margins.tex > /dev/null
grep -E ':[0-9]+: |^l\.[0-9]+' margins.log
!lualatex -interaction=nonstopmode -halt-on-error -file-line-error margins.tex > /dev/null
grep -E ':[0-9]+: |^l\.[0-9]+' margins.log
!typst compile margins.typ
sed 's/margins=2cm/margin=2cm/' margins.tex > fixed.tex && pdflatex -interaction=nonstopmode -halt-on-error fixed.tex > /dev/null && ls fixed.pdf
lualatex -interaction=nonstopmode -halt-on-error fixed.tex > /dev/null && ls fixed.pdf
sed 's/margins: 2cm/margin: 2cm/' margins.typ > fixed.typ && typst compile fixed.typ fixed-typst.pdf && ls fixed-typst.pdf
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ pdflatex -interaction=nonstopmode -halt-on-error -file-line-error margins.tex > /dev/null
[exit status 1]
$ grep -E ':[0-9]+: |^l\.[0-9]+' margins.log
…/texmf-dist/tex/latex/geometry/geometry.sty:1017: Package keyval Error: margins undefined.
l.1017 \ProcessOptionsKV[p]{Gm}
…/texmf-dist/tex/latex/geometry/geometry.sty:1017:  ==> Fatal error occurred, no output PDF file produced!
$ lualatex -interaction=nonstopmode -halt-on-error -file-line-error margins.tex > /dev/null
[exit status 1]
$ grep -E ':[0-9]+: |^l\.[0-9]+' margins.log
…/texmf-dist/tex/latex/geometry/geometry.sty:1017: Package keyval Error: margins undefined.
l.1017 \ProcessOptionsKV[p]{Gm}
…/texmf-dist/tex/latex/geometry/geometry.sty:1017:  ==> Fatal error occurred, no output PDF file produced!
$ typst compile margins.typ
error: unexpected argument: margins
  ┌─ margins.typ:2:23
  │
2 │ #set page(paper: "a4", margins: 2cm)
  │                        ^^^^^^^^^^^^

[exit status 1]
$ sed 's/margins=2cm/margin=2cm/' margins.tex > fixed.tex && pdflatex -interaction=nonstopmode -halt-on-error fixed.tex > /dev/null && ls fixed.pdf
fixed.pdf
$ lualatex -interaction=nonstopmode -halt-on-error fixed.tex > /dev/null && ls fixed.pdf
fixed.pdf
$ sed 's/margins: 2cm/margin: 2cm/' margins.typ > fixed.typ && typst compile fixed.typ fixed-typst.pdf && ls fixed-typst.pdf
fixed-typst.pdf

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py

Both LaTeX engines stopped inside geometry.sty, at line 1017 of the package, with Package keyval Error: margins undefined, and the context line was the package’s own code, not line 3 of your file. Typst named margins.typ, line 2, column 23, and underlined the argument. With margin=2cm and margin: 2cm, all three built.

Breadth goes the other way. CTAN, the central archive of TeX material, lists more than 7,000 packages, and this report loads seven of them; its Typst version imports none, because tables, figures, CSV files and bibliographies are built into Typst, which its guide calls “batteries included”.

Key idea

Requirements decide first; the build time and the error messages, measured on your own document, decide only between the systems the requirements leave.

Checkpoint

Check: Build time and error messages

  1. Question 1 of 2 In the timing session, how many times did latexmk run pdfLaTeX for one full build of the report from a clean folder?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 3 runs

    timings.py counted 3 pdflatex runs and 2 BibTeX runs in latexmk's log, the runs that the references and citations needed. Typst compiled once.

  2. Question 2 of 2 margins.tex loads geometry with margins=2cm, a misspelt option. Where did pdfLaTeX report the error?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: In geometry.sty, at line 1017 of the package, as Package keyval Error, margins undefined

    Both LaTeX engines reported the error inside the package that received the option, while Typst named the file, line and column of the argument. The fix was the same word in both: margin.

An audit before converting counts the work

A migration costs what pandoc does not carry over as it is, and those places can be counted before anything is converted. The conversion lesson found them one by one in a short note; audit.py counts them in the whole report, report.tex, four pages with seven sections, three equations, two tables, three figures and three citations:

The report to migrate, and the audit

report/report.tex

% report.tex: an invented field report, the LaTeX project that this lesson migrates to Typst.
\documentclass[11pt,a4paper]{article}
\usepackage{amsmath}
\usepackage{booktabs}
\usepackage{graphicx}
\usepackage{tikz}
\usepackage{pgfplotstable}
\pgfplotsset{compat=1.18}
\usepackage[numbers]{natbib}
\usepackage{hyperref}
\newcommand{\mm}{\,mm}
\title{Rain on two flat roofs: a field report}
\author{Meera Pillai and Ravi Kumar}
\date{}
\begin{document}
\maketitle

\section{Introduction}\label{sec:intro}
Two housing blocks on the same street collect rain from their flat roofs into one shared tank.
The residents asked whether the two roofs collect the same amount, because the tank's upkeep is split between the blocks by roof area alone.
For two weeks of the monsoon we read a gauge on each roof every morning.
Section~\ref{sec:site} describes the roofs, Section~\ref{sec:method} the gauges and the two quantities we computed, and Section~\ref{sec:totals} the totals with their errors.

\section{The two roofs}\label{sec:site}
Block A is a four-storey building with a roof of 410 square metres, open on every side.
Block B is one storey lower, its roof is 395 square metres, and the water tank stands on its east edge.
Both roofs drain through two outlets into the same pipe.
Figure~\ref{fig:site} shows where the gauges stood \citep{pillai2026site}.
\begin{figure}[htb]
  \centering
  \begin{tikzpicture}
    \draw (0,0) rectangle (3,1.5) node[midway] {Block A};
    \draw (4,0) rectangle (7,1.5) node[midway] {Block B};
  \end{tikzpicture}
  \caption{The two roofs seen from above.}\label{fig:site}
\end{figure}

\section{Method}\label{sec:method}
Each gauge is a funnel of 20 centimetres over a graduated cylinder, read to the nearest half millimetre.
We read both gauges between 7 and 8 in the morning and emptied them after each reading.
The daily mean of roof $r$ over $n$ days is
\begin{equation}\label{eq:mean}
  \bar{x}_r = \frac{1}{n}\sum_{i=1}^{n} x_{r,i},
\end{equation}
and the difference between the roofs on day $i$ is
\begin{equation}\label{eq:diff}
  d_i = x_{A,i} - x_{B,i}.
\end{equation}
Section~\ref{sec:totals} uses Equation~\eqref{eq:mean} and Equation~\eqref{eq:diff} \citep{rao2025gauges}.
\vspace{2mm}

\section{Daily readings}\label{sec:daily}
Table~\ref{tab:daily} lists the readings of the first week, read from \texttt{data/rain.csv}.
\begin{table}[htb]
  \centering
  \caption{Daily readings in millimetres.}\label{tab:daily}
  \pgfplotstabletypeset[col sep=comma, string type, every head row/.style={before row=\toprule, after row=\midrule},
    every last row/.style={after row=\bottomrule}]{data/rain.csv}
\end{table}
Figure~\ref{fig:daily} draws the same readings.
Day 3 was dry, and on day 7 both gauges read the same.
\begin{figure}[htb]
  \centering
  \includegraphics[width=0.7\linewidth]{figures/daily}
  \caption{Daily rain on both roofs.}\label{fig:daily}
\end{figure}
\newpage

\section{Totals and errors}\label{sec:totals}
Table~\ref{tab:totals} gives the totals of the two weeks, and Figure~\ref{fig:totals} compares them.
\begin{table}[htb]
  \centering
  \caption{Totals of the two weeks.}\label{tab:totals}
  \begin{tabular}{lr}
    \toprule
    Roof & Rain (mm) \\
    \midrule
    Block A & 83.0 \\
    Block B & 76.5 \\
    \bottomrule
  \end{tabular}
\end{table}
\begin{figure}[htb]
  \centering
  \includegraphics[width=0.5\linewidth]{figures/totals}
  \caption{Total rain on each roof.}\label{fig:totals}
\end{figure}
The error of the total $T_r$ of each gauge is
\begin{equation}\label{eq:error}
  \epsilon_r = 0.05\,T_r,
\end{equation}
so Equation~\eqref{eq:error} gives 4.2\mm\ for Block A and 3.8\mm\ for Block B.
Together they exceed the difference of 6.5\mm, as Section~\ref{sec:discussion} explains \citep{iyer2024calibration}.
\vspace{2mm}

\section{Discussion}\label{sec:discussion}
Block A collected 6.5\mm\ more than Block B, but the errors of the two gauges add up to 8.0\mm, so the difference may come from the gauges alone.
The tank on Block B shades part of its roof in the morning, which would lower its readings on sunny days, not on rainy ones.

\section{Conclusion}\label{sec:conclusion}
Two weeks of readings do not show that one roof collects more rain than the other.
The blocks can keep splitting the upkeep by roof area until a longer series says otherwise.

\bibliographystyle{plainnat}
\bibliography{refs}
\end{document}

audit.py

"""Counts, before any conversion, the places in a LaTeX file that pandoc does not carry over to Typst as they are.

    python3 audit.py report.tex

Each row is one kind of construct, how often the document body uses it, whether pandoc 3.12 names it in its --verbose
messages, and what pandoc did with it in this lesson's migration. A row with a count above 0 is work to plan; the last
lines add the rows up, and count the places that pandoc converts without a message. Comments are ignored, and the
definitions of custom macros are read from the preamble, then their uses counted in the body.
"""

import re
import sys
from pathlib import Path

# Text after an unescaped % is a comment.
source = "\n".join(re.split(r"(?<!\\)%", line)[0] for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines())
preamble, _, body = source.partition("\\begin{document}")
macros = re.findall(r"\\(?:re)?newcommand\{?\\([A-Za-z]+)", preamble)
uses = {name: len(re.findall(rf"\\{name}(?![A-Za-z])", body)) for name in macros}


def count(pattern):
    return len(re.findall(pattern, body))


rows = [
    ("uses of custom macros", sum(uses.values()), "silent", "expanded where used; the macro is gone"),
    ("tikzpicture", count(r"\\begin\{tikzpicture\}"), "named", "skipped; an empty figure keeps the caption"),
    ("\\pgfplotstabletypeset", count(r"\\pgfplotstabletypeset"), "named", "skipped with its caption and label"),
    ("word before \\ref", count(r"\b(?:Table|Figure|Section|Chapter|Appendix)~\\ref\{"), "silent", "the word printed twice"),
    ("\\eqref", count(r"\\eqref\{"), "silent", "a link whose text is the label"),
    ("\\vspace, \\newpage", count(r"\\(?:vspace|newpage|clearpage)\b"), "named", "skipped"),
]
print(f"{'construct':<24} {'count':>5}  {'--verbose':<9}  what pandoc 3.12 did")
for name, n, verbose, effect in rows:
    print(f"{name:<24} {n:>5}  {verbose:<9}  {effect}")
print(f"{'places to fix or check':<24} {sum(r[1] for r in rows):>5}")
print(f"{'of them silent':<24} {sum(r[1] for r in rows if r[2] == 'silent'):>5}")
print("macros:", ", ".join(f"\\{name} ({n} uses)" for name, n in uses.items()) or "none")
The audit of report.tex, before any conversion Python · audit_session.py
from session import files, needs, run

# Before anything is converted: audit.py counts the places in report.tex that pandoc will not carry over as they are.
needs("python3")
files("report/report.tex", "audit.py")
run("""
python3 audit.py report.tex
""")

Output

$ python3 audit.py report.tex
construct                count  --verbose  what pandoc 3.12 did
uses of custom macros        5  silent     expanded where used; the macro is gone
tikzpicture                  1  named      skipped; an empty figure keeps the caption
\pgfplotstabletypeset        1  named      skipped with its caption and label
word before \ref            10  silent     the word printed twice
\eqref                       3  silent     a link whose text is the label
\vspace, \newpage            3  named      skipped
places to fix or check      23
of them silent              18
macros: \mm (5 uses)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 audit_session.py

The migration audit: A count, made before anything is converted, of the places in a source that a conversion will not carry over as they are, such as custom macros, TikZ pictures and words written before references. Each place becomes a fix to plan or a check to make. found 23 places. pandoc’s --verbose messages name 5 of them, the TikZ picture, the table that pgfplotstable typesets from data/rain.csv and three spacing commands, because pandoc skips them. The other 18 convert without a word: the 5 uses of the macro \mm are expanded into a thin space and mm, the 10 words written before \ref print twice once Typst adds its own, and the 3 \eqref become links whose text is the label.

Not every place is a fix. The expanded macro prints correctly and only the shorthand is gone, and the three spacing commands were placed for LaTeX’s page breaks, so they are left to Typst’s layout. That leaves 15 fixes: 10 words, 3 references, 1 drawing and 1 table, the estimate the next section puts to the test.

Turn counts into hours with a pilot

Migrate one chapter first, time the fixes of each kind, then multiply by the audit’s counts for the rest. A count of 40 references and 6 drawings means little until you know that a drawing takes you half an hour.

Checkpoint

Check: Auditing before converting

  1. Question 1 of 2 audit.py found 23 places in report.tex. How many of them does pandoc convert without any message?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 18 places

    pandoc names the TikZ picture, the pgfplotstable table and the 3 spacing commands, 5 places, because it skips them. The 5 macro uses, the 10 words before a reference and the 3 \eqref, 18 places, convert silently.

  2. Question 2 of 2 The audit counted 23 places, but the plan had 15 fixes. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The 5 expanded macro uses print correctly, and the 3 spacing commands are left to Typst's layout

    An audit lists the places to fix or check. Each place is then sorted into a fix or a check, and the fixes are the estimate: here 10 words, 3 references, 1 drawing and 1 table.

The migration converts the body, scripts the fixes and proves the result

The migration follows the rule of the conversion lesson: pandoc converts the body, and a frame written by hand, main.typ, sets the numbering, the title and the bibliography. The fixes are a script, fixes.py, so that they can run again if report.tex changes before the switch:

The hand-written frame and the scripted fixes

report/main.typ

// main.typ: the Typst frame of the migrated report, written by hand. pandoc converts the body, into body.typ.
#set document(title: [Rain on two flat roofs: a field report], author: ("Meera Pillai", "Ravi Kumar"))
#set page(paper: "a4")
#set text(size: 11pt)
#set heading(numbering: "1")
#set math.equation(numbering: "(1)")
#show figure.where(kind: table): set figure.caption(position: top)
#align(center)[
  #text(17pt)[Rain on two flat roofs: a field report] \
  Meera Pillai and Ravi Kumar
]

#include "body.typ"

#bibliography("refs.bib", title: [References])

fixes.py

"""Makes the fixes that pandoc's body.typ of the report needs, the ones a person would make by hand, as a script, so
that the migration can be run again after the LaTeX changes.

    python3 fixes.py body.typ

1. Typst's references print their own word ("Table 1"), so the word written before each one goes: Table~@tab:totals
   becomes @tab:totals.
2. pandoc writes \\eqref as a link whose text is the label: Equation~#link(<eq:mean>)[\\[eq:mean\\]] becomes @eq:mean.
3. The TikZ drawing arrived as an empty figure with its caption; it is drawn again with Typst's own shapes.
4. The table that pgfplotstable read from data/rain.csv was skipped with its caption and label; Typst reads the same
   CSV file with csv() and builds the table again.

Each fix prints how often it applied, and the script stops with exit status 1 if one of them finds nothing to fix.
"""

import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
counts = {}

text, counts["word before a reference removed"] = re.subn(r"\b(?:Table|Figure|Section|Equation)~(?=@)", "", text)
text, counts["\\eqref link turned into a reference"] = re.subn(
    r"\b(?:Equation)~#link\(<([^>]+)>\)\[\\\[[^\]]*\\\]\]", r"@\1", text)

EMPTY = "#figure([],\n  caption: [\n    The two roofs seen from above.\n  ]\n)"
DRAWING = """#figure(
  grid(columns: 2, gutter: 1cm,
    rect(width: 3cm, height: 1.5cm)[#align(center + horizon)[Block A]],
    rect(width: 3cm, height: 1.5cm)[#align(center + horizon)[Block B]]),
  caption: [The two roofs seen from above.],
)"""
counts["TikZ figure drawn again"] = text.count(EMPTY)
text = text.replace(EMPTY, DRAWING)

ANCHOR = "`data/rain.csv`.\n"
TABLE = """`data/rain.csv`.

#figure(
  {
    let rows = csv("data/rain.csv")
    table(columns: 3, align: (left, right, right), stroke: none,
      table.hline(), table.header(..rows.first()), table.hline(stroke: 0.5pt),
      ..rows.slice(1).flatten(), table.hline())
  },
  caption: [Daily readings in millimetres.],
) <tab:daily>
"""
counts["CSV table built again"] = text.count(ANCHOR)
text = text.replace(ANCHOR, TABLE)

path.write_text(text, encoding="utf-8")
for what, count in counts.items():
    print(f"{count:>3}  {what}")
if not all(counts.values()):
    sys.exit("a fix found nothing to change")
The migration: convert, compile, fix, compile, count Python · migrate_session.py
from session import files, needs, run

# The migration of the report: pandoc converts the body, Typst compiles it inside the hand-written main.typ, fixes.py
# makes the fixes the audit predicted, and the parts of both builds are counted.
needs("pandoc", "typst", "pdflatex", "latexmk", "pdftotext", "git", "python3")
files("report", "figures.py", "fixes.py")
run("""
python3 figures.py report
cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1
cd report && pandoc --verbose report.tex -t typst -o body.typ 2>&1 | grep -c Skipped
grep -c -E '(Table|Figure|Section|Equation)~(@|#link)' report/body.typ
cp report/body.typ report/body-pandoc.typ
!cd report && typst compile main.typ report-typst.pdf
python3 fixes.py report/body.typ
cd report && typst compile main.typ report-typst.pdf
!git diff --no-index --stat report/body-pandoc.typ report/body.typ
grep -c '^.section' report/report.tex
cd report && typst eval 'query(heading.where(numbering: "1")).len()' --in main.typ
grep -c 'begin.equation' report/report.tex
cd report && typst eval 'query(math.equation.where(block: true)).len()' --in main.typ
grep -c 'begin.table' report/report.tex
cd report && typst eval 'query(figure.where(kind: table)).len()' --in main.typ
grep -c 'begin.figure' report/report.tex
cd report && typst eval 'query(figure.where(kind: image)).len()' --in main.typ
pdfinfo report/report.pdf | grep Pages
pdfinfo report/report-typst.pdf | grep Pages
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ python3 figures.py report
$ cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1
$ cd report && pandoc --verbose report.tex -t typst -o body.typ 2>&1 | grep -c Skipped
13
$ grep -c -E '(Table|Figure|Section|Equation)~(@|#link)' report/body.typ
13
$ cp report/body.typ report/body-pandoc.typ
$ cd report && typst compile main.typ report-typst.pdf
error: label `<tab:daily>` does not exist in the document
   ┌─ body.typ:40:6
   │
40 │ Table~@tab:daily lists the readings of the first week, read from
   │       ^^^^^^^^^^

[exit status 1]
$ python3 fixes.py report/body.typ
 10  word before a reference removed
  3  \eqref link turned into a reference
  1  TikZ figure drawn again
  1  CSV table built again
$ cd report && typst compile main.typ report-typst.pdf
$ git diff --no-index --stat report/body-pandoc.typ report/body.typ
 report/{body-pandoc.typ => body.typ} | 45 ++++++++++++++++++++++--------------
 1 file changed, 28 insertions(+), 17 deletions(-)
[exit status 1]
$ grep -c '^.section' report/report.tex
7
$ cd report && typst eval 'query(heading.where(numbering: "1")).len()' --in main.typ
7
$ grep -c 'begin.equation' report/report.tex
3
$ cd report && typst eval 'query(math.equation.where(block: true)).len()' --in main.typ
3
$ grep -c 'begin.table' report/report.tex
2
$ cd report && typst eval 'query(figure.where(kind: table)).len()' --in main.typ
2
$ grep -c 'begin.figure' report/report.tex
3
$ cd report && typst eval 'query(figure.where(kind: image)).len()' --in main.typ
3
$ pdfinfo report/report.pdf | grep Pages
Pages:           4
$ pdfinfo report/report-typst.pdf | grep Pages
Pages:           3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 migrate_session.py

pandoc printed 13 messages about skipped commands and none about the 13 references written the LaTeX way. The first compile stopped at label <tab:daily> does not exist: the table typeset from the CSV file had been dropped with its label, so the sentence that referred to it pointed nowhere. fixes.py then made the 15 fixes of the estimate, and the second compile passed. Follow the steps:

Interactive · stage by stage Migrating the report, step by step Step through the migration as the session ran it, from the audit to the day the Typst files become the primary copy.
Migrating the report, step by step, step 7 of 7. Showing Audit report.tex (15 fixes to plan), Convert the body (13 skipped, 13 references), Compile main.typ (compiled, 3 pages), Run fixes.py (10 + 3 + 1 + 1 applied), Count both sides (7, 3, 2 and 3 on both), Switch the primary copy. In focus: Switch the primary copy.compile againAudit report.tex15 fixes to planConvert the body13 skipped, 13 referencesCompile main.typcompiled, 3 pagesRun fixes.py10 + 3 + 1 + 1 appliedCount both sides7, 3, 2 and 3 on bothSwitch the primary copyedit only the Typst

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Audit before converting. audit.py counted 23 places in report.tex that pandoc would not carry over as they are, 18 of them without any message, and left 15 fixes to plan: 10 words, 3 references, 1 drawing and 1 table.
  2. Convert only the body. pandoc wrote body.typ and printed 13 messages about commands it skipped, among them the TikZ picture and the table read from the CSV file. It printed nothing about the 13 references written the LaTeX way.
  3. Compile it in its frame. main.typ numbers the headings and equations and loads refs.bib. The first compile stopped at label <tab:daily> does not exist, because the table typeset from the CSV file had been dropped with its label.
  4. Make the fixes with a script. fixes.py removed 10 words before references, turned 3 links into references, and built the drawing and the table again: the 15 fixes of the estimate, each printed with how often it applied.
  5. Compile again. With the fixed body.typ the second compile passed, and the Typst PDF had 3 pages.
  6. Count what both should have. grep on report.tex and query() in Typst counted the same 7 sections, 3 display equations, 2 tables and 3 figures. The LaTeX PDF has 4 pages against 3, which is layout, not lost content.
  7. One primary copy from now on. Compare the text of both PDFs as the conversion lesson does. When it matches, tag the LaTeX version in Git and edit only main.typ and body.typ from then on.

Both sides counted 7 sections, 3 display equations, 2 tables and 3 figures. The page count differed, 4 against 3, which is layout: other fonts, other margins, and the skipped \newpage. Counts prove that nothing was lost; they do not prove that the words are the same, so compare the text of both PDFs as the conversion lesson does before the Typst files become the primary copy: The one set of source files that every other format of a document is made from and that every edit goes into. Copies in other formats are outputs: they are rebuilt from it and never edited by hand. More in “Collaboration, version control and tracked changes”.

Two copies in use

Wrong: keeping report.tex and main.typ both in use after the switch, and editing whichever is open. Right: on the day the counts and the texts match, tag the LaTeX version in Git and edit only the Typst files from then on.

Checkpoint

Check: Migrating and proving it

  1. Question 1 of 2 Why did the first typst compile of the converted report stop?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: pandoc had dropped the table typeset from the CSV file with its label, so @tab:daily pointed nowhere

    A compile error points at the first fault, and here it was a reference to content that the conversion had lost. The faults that compile, such as the doubled words, still need the audit.

  2. Question 2 of 2 Both builds counted 7 sections, 3 display equations, 2 tables and 3 figures, but the LaTeX PDF has 4 pages and the Typst PDF 3. What follows?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing was lost by count; the pages differ by layout, and the text still has to be compared

    Counts of parts and a comparison of the text answer two different questions: whether anything is missing, and whether what is there says the same.

Compiling catches a wrong key, not an invented reference

Assistants in editors and chat tools now suggest markup and references, and both need two checks: compile the suggestion, and check its facts against a source. The misspelt margins above is the kind of slip that compiling catches. A suggested citation shows what it cannot. Here a sentence about shading gained the key kumar2024shade and a .bib entry for it:

A paragraph with a suggested citation, and its suggested entry

cite/cite.tex

% cite.tex: one paragraph with two citations. The second key, kumar2024shade, came with a suggested reference
% that is not in refs.bib yet.
\documentclass{article}
\usepackage[numbers]{natbib}
\begin{document}
A funnel gauge on a flat roof reads less than one on open ground \citep{rao2025gauges},
and a tank that shades the roof lowers the reading further \citep{kumar2024shade}.
\bibliographystyle{plainnat}
\bibliography{refs}
\end{document}

cite/cite.typ

// cite.typ: the same paragraph in Typst, with the same two keys.
A funnel gauge on a flat roof reads less than one on open ground @rao2025gauges,
and a tank that shades the roof lowers the reading further @kumar2024shade.

#bibliography("refs.bib")

cite/suggested.bib

@article{kumar2024shade,
  author  = {Kumar, Ravi and Shah, Nisha},
  title   = {Shading and Rain Capture on Urban Roofs},
  journal = {Journal of Example Hydrology},
  year    = {2024},
  volume  = {11},
  pages   = {118--127},
  doi     = {10.5555/jeh.2024.118}
}
The suggestion built before and after its entry is added Python · assistant_session.py
from session import files, needs, run

# A suggested citation, built before and after its suggested entry is added to refs.bib: what the builds catch, and
# the list of DOIs that only a person can check.
needs("pdflatex", "latexmk", "typst", "pdftotext")
files("cite/cite.tex", "cite/cite.typ", "cite/refs.bib", "cite/suggested.bib")
run("""
latexmk -pdf -silent cite.tex > /dev/null 2>&1; echo "latexmk: exit status $?"
grep -E 'Citation .* undefined' cite.log
pdftotext cite.pdf - | head -2
!typst compile cite.typ cite-typst.pdf
cat suggested.bib >> refs.bib
latexmk -pdf -silent cite.tex > /dev/null 2>&1; echo "latexmk: exit status $?"
!grep -c -E 'Citation .* undefined' cite.log
typst compile cite.typ cite-typst.pdf
pdftotext cite-typst.pdf - | grep -A 1 Kumar
grep -E '^@|doi' refs.bib
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ latexmk -pdf -silent cite.tex > /dev/null 2>&1; echo "latexmk: exit status $?"
latexmk: exit status 0
$ grep -E 'Citation .* undefined' cite.log
Package natbib Warning: Citation `kumar2024shade' on page 1 undefined on input line 7.
$ pdftotext cite.pdf - | head -2
A funnel gauge on a flat roof reads less than one on open ground [1], and a
tank that shades the roof lowers the reading further [? ].
$ typst compile cite.typ cite-typst.pdf
error: label `<kumar2024shade>` does not exist in the document
  ┌─ cite.typ:3:59
  │
3 │ and a tank that shades the roof lowers the reading further @kumar2024shade.
  │                                                            ^^^^^^^^^^^^^^^

[exit status 1]
$ cat suggested.bib >> refs.bib
$ latexmk -pdf -silent cite.tex > /dev/null 2>&1; echo "latexmk: exit status $?"
latexmk: exit status 0
$ grep -c -E 'Citation .* undefined' cite.log
0
[exit status 1]
$ typst compile cite.typ cite-typst.pdf
$ pdftotext cite-typst.pdf - | grep -A 1 Kumar
[2] R. Kumar and N. Shah, “Shading and Rain Capture on Urban Roofs,” Journal of Example
Hydrology, vol. 11, pp. 118–127, 2024, doi: 10.5555/jeh.2024.118.
$ grep -E '^@|doi' refs.bib
@article{rao2025gauges,
  doi     = {10.5555/jeh.2025.040}
@article{kumar2024shade,
  doi     = {10.5555/jeh.2024.118}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assistant_session.py

Suggestion What the builds did What no build can check
A key missing from refs.bib latexmk ended with exit status 0, [?] and a natbib warning; Typst stopped at line 3, column 59 Nothing: the builds found it
The entry for that key Both built; Typst printed it as reference [2] Whether the paper exists, with this title, these authors and pages

Once the entry was appended, both builds passed and Typst printed it like any real reference. Only its source can show whether it is real, so the last command lists every DOI: Digital Object Identifier: a persistent identifier of a work, a prefix and a suffix separated by a slash, such as 10.1000/182. https://doi.org/ followed by it leads to the work. More in “Bibliography files and reference managers” in refs.bib; each is opened as https://doi.org/ followed by the DOI, which, the DOI Foundation explains, redirects to the work the DOI points to, and what is found there is compared with the entry before the citation stays. The DOIs in this lesson are invented, like every reference of its examples.

Citation Generator Paste a DOI to fetch its record from doi.org, then compare it with the entry in your .bib file.

Journals ask for the same care. The ICMJE recommendations tell authors to check what AI tools produce, because it “can be incorrect, incomplete, or biased”, and ask journals to require authors to say how they used them.

Some text never goes into such a tool

ICMJE’s recommendations say that a referee’s duty of confidentiality may rule out uploading a manuscript to AI tools where confidentiality cannot be assured. Treat your group’s unpublished results the same way, and use only tools your institution allows for them.

Checkpoint

Check: Checking suggested markup and references

  1. Question 1 of 2 What did latexmk do with \citep{kumar2024shade} while the key was missing from refs.bib?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It ended with exit status 0, printed [?] in the PDF and left a natbib warning in the log

    This is why the build lesson of this module makes warnings fail a build: with $warnings_as_errors = 1, latexmk would have stopped here too.

  2. Question 2 of 2 After the suggested entry for kumar2024shade was added, both builds passed. What still has to happen before the citation stays?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Look up its DOI at doi.org and compare the record with the entry's title, authors and pages

    A build checks that the markup is valid and that every key has an entry. Whether the entry describes a real work is a fact, and facts are checked against their source.

Data, references and figures in plain files keep a project portable

The migration was cheap wherever the report kept something in a plain file. refs.bib was used unchanged, because Typst’s bibliography function reads BibLaTeX .bib files as well as Hayagriva files. data/rain.csv was read by pgfplotstable in LaTeX and by Typst’s csv(). The two charts were PNG files that figures.py draws from the same CSV file, and both systems include images. The one drawing written in TikZ had to be drawn again. This session builds both versions, corrects one reading in the CSV file, and builds both again:

One correction in rain.csv, read by both builds Python · portable_session.py
from session import files, needs, run

# The same data and references feed both builds. Day 2 of Block A is corrected once, in data/rain.csv; the figure
# script, pdfLaTeX and Typst all read the corrected value.
needs("pdflatex", "latexmk", "pandoc", "typst", "pdftotext", "python3")
files("report", "figures.py", "fixes.py")
run("""
python3 figures.py report
cd report && pandoc report.tex -t typst -o body.typ && python3 ../fixes.py body.typ > /dev/null
cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1 && typst compile main.typ report-typst.pdf
pdftotext -layout report/report.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
pdftotext -layout report/report-typst.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
perl -pi -e 's/^2,12\\.0,/2,12.5,/' report/data/rain.csv
python3 figures.py report
cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1 && typst compile main.typ report-typst.pdf
pdftotext -layout report/report.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
pdftotext -layout report/report-typst.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
grep -c '^@' report/refs.bib
pdftotext report/report.pdf - | grep -c -E '^\\[[0-9]\\]'
pdftotext report/report-typst.pdf - | grep -c -E '^\\[[0-9]\\]'
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ python3 figures.py report
$ cd report && pandoc report.tex -t typst -o body.typ && python3 ../fixes.py body.typ > /dev/null
$ cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1 && typst compile main.typ report-typst.pdf
$ pdftotext -layout report/report.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
 2 12.0 11.5
$ pdftotext -layout report/report-typst.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
 2 12.0 11.5
$ perl -pi -e 's/^2,12\.0,/2,12.5,/' report/data/rain.csv
$ python3 figures.py report
$ cd report && latexmk -pdf -silent report.tex > /dev/null 2>&1 && typst compile main.typ report-typst.pdf
$ pdftotext -layout report/report.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
 2 12.5 11.5
$ pdftotext -layout report/report-typst.pdf - | grep -E '^ +2 +[0-9]' | tr -s ' '
 2 12.5 11.5
$ grep -c '^@' report/refs.bib
3
$ pdftotext report/report.pdf - | grep -c -E '^\[[0-9]\]'
3
$ pdftotext report/report-typst.pdf - | grep -c -E '^\[[0-9]\]'
3

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 portable_session.py

Both PDFs printed day 2 as 12.0 and 11.5, and after the one edit both printed 12.5; the bibliography of each listed the 3 entries of refs.bib. Step through what each shared file feeds:

Interactive · step by step The files both builds of the report read Step through what each shared file feeds, then follow one corrected reading into both PDFs.
The files both builds of the report read, step 6 of 6. Showing data/rain.csv (day 2: 12.5), figures.py (two PNG charts), refs.bib (3 entries, unchanged), report.tex (pgfplotstable reads it), main.typ and body.typ (csv() reads it). In focus: data/rain.csv, main.typ and body.typ.PNGPNGdata/rain.csvday 2: 12.5figures.pytwo PNG chartsrefs.bib3 entries, unchangedreport.texpgfplotstable reads itmain.typ and body.typcsv() reads it2,12.5,11.5

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The data is read by both. data/rain.csv holds the daily readings. pgfplotstable typesets it in report.tex, and csv() builds the same table in Typst.
  2. The charts are drawn once. figures.py draws both charts from the same CSV file as PNG files, and both systems include PNG images.
  3. The references are not converted. refs.bib is read unchanged: by BibTeX through natbib for report.tex, and by Typst's bibliography function, which reads BibLaTeX .bib files.
  4. One part belonged to one system. The site drawing was TikZ code inside report.tex, the only part written in one system's language, and the only part that had to be drawn again.
  5. One reading is corrected, once. Day 2 of Block A changes from 12.0 to 12.5 in rain.csv and nowhere else. On the next build, figures.py and pdfLaTeX read the new value.
  6. Both PDFs show it. Typst reads the same file, so both PDFs printed day 2 as 12.5, and both bibliographies listed the 3 entries of refs.bib.

Typst reads JSON, YAML, TOML and XML as well as CSV: Comma-separated values: a plain-text table with one line per row, its cells separated by commas, often with a first line of column headings. Spreadsheets export it, and Typst's csv() reads it. More in “Long, wide and data-driven tables”, so data of any of these kinds stays outside the markup. Drawings that must work in both systems are kept as files too, with one catch: Typst’s image reference says PDF images cannot be used when exporting with a PDF standard such as PDF/UA-1, and suggests SVG, while pdfLaTeX reads PDF but not SVG, as Images, figures and alt text showed. Make the SVG and the PDF from one source with one command.

Key idea

Keep the text in markup, the data in CSV or JSON, the references in a .bib file and the figures as files made by a script: a migration then converts the markup and leaves everything else as it is.

Checkpoint

Check: Portable data, references and figures

  1. Question 1 of 2 Day 2 of Block A was corrected from 12.0 to 12.5. Which files had to change for both PDFs and the chart to show 12.5?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only data/rain.csv

    Data kept in its own file is corrected once, and every build that reads it follows, in either system.

  2. Question 2 of 2 A vector drawing must appear in a pdfLaTeX paper and in its Typst twin, which is exported as PDF/UA-1. What do you keep?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One source, exported as PDF for pdfLaTeX and as SVG for Typst by one command

    A figure kept as a file moves between systems when each system gets a format it reads; one source and one command keep the two files the same drawing.

Practise: decide for six projects

Turn the four requirements into a function, so that a group applies them in the same order to every new document. The sample tests are six invented projects, and the first hint is open:

Exercise · Easy · Python

Decide LaTeX or Typst from a project's requirements

A research group writes down four facts about every new document before anyone starts it. Write decide(project) in decide.py. It receives those facts as a dictionary and returns a pair: the system, "latex", "typst" or "either", and the name of the fact that decided it. Check the facts in this order; the first one that applies decides, and the later ones are not read:

  1. "venue" is "latex-source": the venue builds LaTeX source or gives a LaTeX class. Return ("latex", "venue").
  2. "standard" is "ua-2": the PDF must meet PDF/UA-2. Return ("latex", "standard").
  3. "latex_only" is a list of features that only a LaTeX package provides. If it is not empty, return ("latex", "latex_only").
  4. "writers" is "latex" or "typst": every writer already works in that system. Return that system and "writers".

If none of them applies, return ("either", "none"): the choice then rests on the measured differences of the lesson. A fact that is missing from the dictionary counts as not applying, the same as None or an empty list.

Starter code · decide.py

def decide(project):
    """Return (system, deciding fact) for a project's requirements, the first rule that applies deciding."""
    # Replace this line: test the venue, the standard, the LaTeX-only features and the writers, in that order.
    return ("either", "none")
The sample tests · test_decide.py
from decide import decide


def test_venue_first():
    """a journal that builds its own LaTeX class decides, even for a group that writes in Typst"""
    project = {"venue": "latex-source", "standard": None, "latex_only": [], "writers": "typst"}
    assert decide(project) == ("latex", "venue")


def test_standard():
    """a public report that must meet PDF/UA-2 goes to LaTeX"""
    project = {"venue": "pdf", "standard": "ua-2", "latex_only": [], "writers": "typst"}
    assert decide(project) == ("latex", "standard")


def test_latex_only_feature():
    """a thesis that needs a department's LaTeX class goes to LaTeX"""
    project = {"venue": "pdf", "standard": None, "latex_only": ["department thesis class"], "writers": None}
    assert decide(project) == ("latex", "latex_only")


def test_writers():
    """a preprint that goes out as a PDF follows its writers, who all write Typst"""
    project = {"venue": "pdf", "standard": None, "latex_only": [], "writers": "typst"}
    assert decide(project) == ("typst", "writers")


def test_nothing_decides():
    """lab notes with no fixed requirement are open to either system"""
    project = {"venue": None, "standard": None, "latex_only": [], "writers": None}
    assert decide(project) == ("either", "none")


def test_missing_facts():
    """facts that are missing count as not applying"""
    assert decide({"writers": "latex"}) == ("latex", "writers")
    assert decide({}) == ("either", "none")
A hint

Read each fact with project.get(...), so that a missing key gives None, and test the four rules with if in the order the prompt gives, returning as soon as one applies. An empty list is false in Python, so if latex_only: is enough for rule 3.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Last, six ideas to test, from the venue rule to the shared rain.csv. The questions start where your answers so far were weakest, and every choice explains itself, right or wrong:

Practice that adapts to you

Practise this lesson

15 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Which requirement is checked first when a group chooses a system?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Whether the venue builds LaTeX source or gives its own LaTeX class

    The order is the venue, the accessibility standard, a feature only a LaTeX package has, then the writers' system; measured differences come after all four.

  2. Question 2 of 15 A paper written in Typst will go only to arXiv. Does arXiv force the authors to use LaTeX?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: No; a Typst PDF has no TeX source, so arXiv takes the PDF itself if its fonts are embedded and it has no JavaScript

    The venue requirement is about what the venue builds. arXiv builds TeX source when it gets it, but takes other PDFs as they are, so here the requirement does not apply.

  3. Question 3 of 15 A report needs wider margins for half a page of a wide table and its normal margins straight after, on the same page. What does Typst's guide for LaTeX users say?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: Changing margins in Typst forces a page break; the pad function with negative padding can stretch a few paragraphs instead

    A documented limit becomes a requirement when the document needs exactly that feature; here a workaround may be enough, so try it on the real table before deciding.

  4. Question 4 of 15 In the timing session, a full build took 7.80 s with latexmk and pdfLaTeX and 0.16 s with Typst. How many times longer did pdfLaTeX take? Round to a whole number.

    Type a number.

    Show the answer: Question 4 of 15

    Answer: 49 times (anything from 48 to 50 counts)

    7.80 ÷ 0.16 = 48.75, so about 49 times. The ratio comes from one computer under one load; time your own document before you rely on it.

  5. Question 5 of 15 Which column of the timing table would be the same on another computer?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: The runs and the pages

    Measure the seconds on your own machine; the runs tell you why LaTeX takes longer, since every extra run reads the whole document again.

  6. Question 6 of 15 Which construct of report.tex does pandoc convert without any message?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Section~\ref{sec:totals}, which becomes Section~@sec:totals and prints Section twice

    What pandoc skips, it says it skipped; what it converts into something that compiles and reads wrongly, it does not mention. The audit finds both kinds.

  7. Question 7 of 15 An audit of one thesis chapter counts 40 words before \ref, 12 \eqref, 6 TikZ pictures and 2 pgfplotstable tables. A pilot showed 1 minute per reference fix and 25 minutes per drawing or table. How many minutes of fixes do you plan?

    Type a number.

    Show the answer: Question 7 of 15

    Answer: 252 minutes

    40 + 12 = 52 references at 1 minute is 52 minutes; 6 + 2 = 8 drawings and tables at 25 minutes is 200 minutes; 52 + 200 = 252 minutes, a little over four hours.

  8. Question 8 of 15 Why is main.typ written by hand instead of being converted?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: It holds the numbering, the title and the bibliography, so pandoc converts only the body and a new conversion changes none of them

    The frame is yours and the body is pandoc's; keeping them apart lets the body be converted again while the layout stays as you wrote it.

  9. Question 9 of 15 Why were the fixes written as a script, fixes.py, rather than made by hand in body.typ?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: So that the migration can run again if report.tex changes before the switch, with each fix printing how often it applied

    While both copies exist, the LaTeX one may still change. A scripted migration is cheap to repeat, so the switch can wait until the counts and the text match.

  10. Question 10 of 15 Which of these show that the migration lost nothing? Select every one that does.

    Choose every answer that is right.

    Show the answer: Question 10 of 15

    Answer:

    • The same text in both PDFs, apart from differences you can name
    • The same counts of sections, display equations, tables and figures on both sides

    Compiling is necessary and proves only validity. Counts prove completeness, and the text comparison proves the content.

  11. Question 11 of 15 Which of these faults in a suggestion does a build catch by itself? Select every one.

    Choose every answer that is right.

    Show the answer: Question 11 of 15

    Answer:

    • An option name that does not exist, such as margins for margin
    • A citation key that is missing from the .bib file, in Typst

    A build checks markup and keys. Anything that is a fact about the world, such as a paper's existence or its pages, has to be checked against the source.

  12. Question 12 of 15 You were sent a manuscript to referee, and a colleague offers to tidy its LaTeX with an online assistant. What do the ICMJE recommendations say?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Confidentiality may rule out uploading a manuscript to AI tools where it cannot be assured, and the journal must be asked before AI is used

    Text you were trusted with stays inside tools whose handling of it you know and are allowed to use; tidy LaTeX is never a reason to send it elsewhere.

  13. Question 13 of 15 An assistant offers a corrected .bib entry for a real paper you cite: the same DOI, but other page numbers than your file has. What do you do?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Look up the DOI's record and the article itself, and keep what the publisher's record says

    The rule is the same for every fact in a suggestion: the source decides, and the DOI is the quickest way to the source.

  14. Question 14 of 15 Which bibliography files does Typst's bibliography function read?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: BibLaTeX .bib files and Hayagriva YAML files

    Because both systems read the same .bib file, the references are the one part of a migration that needs no conversion at all.

  15. Question 15 of 15 A department may move its lab handbook from LaTeX to Typst in two years. Which layout keeps that move cheapest?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Text in markup, data in CSV files, references in one .bib file and figures as files made by a script

    The cost of a migration sits in whatever is written in one system's language. Keep that part small, and keep everything else in files that both systems read.

Interview questions

Warm-up (fresher to 2–5 years): Which requirements make a project stay in LaTeX, whatever its writers prefer?

Three requirements decide it by themselves. A venue that builds LaTeX source or gives its own LaTeX class needs .tex files, though arXiv accepts a PDF that was not made from TeX. A PDF that must meet PDF/UA-2 today needs LaTeX, because Typst’s accessibility guide lists PDF/UA-1 as supported and PDF/UA-2 as planned; build it with LuaLaTeX, the preferred engine for tagging. And something the document needs that exists only as a LaTeX package, such as a department’s thesis class or a PGF/TikZ drawing, keeps it in LaTeX. Only when none applies do the writers’ own system and the measured differences decide: in this lesson’s session, Typst built a report in 0.16 seconds where latexmk with pdfLaTeX took 7.80, and it named the line and column of a misspelt option.

Two harder questions follow: a thesis that its author wants to move to Typst, and the rules a research group needs for suggested markup and references. Write your first move for each before you read the first step it gives:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A doctoral student has a 160-page LaTeX thesis, eight months before submission, and wants to move it to Typst for faster builds. The university supplies a LaTeX template but accepts any PDF that follows its layout rules. Decide whether to migrate, estimate the work, and plan it so that the thesis can be built at every step.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    Your research group has started taking LaTeX and Typst markup and references from assistants. Write the group's rules: what is checked before a suggestion goes into a paper, how each check is done, and which text never goes into such a tool.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Requirements that nobody can change decide first: a venue that builds LaTeX source or its own class, PDF/UA-2 today, or a feature that only a LaTeX package has points to LaTeX, and the writers’ own system comes next.
  • On one report, Typst compiled in 0.16 seconds where latexmk took 7.80 with pdfLaTeX and 8.40 with LuaLaTeX, and it named the line and column of a misspelt option that both LaTeX engines reported inside geometry.sty.
  • The audit counted 23 places in the report before any conversion, 18 of which pandoc converts without a message, and left an estimate of 15 fixes.
  • The migration converted the body inside a hand-written main.typ, made the 15 fixes with a script, and matched 7 sections, 3 equations, 2 tables and 3 figures on both sides.
  • A build catches a citation key that is missing, but not a reference that does not exist, so every DOI a suggestion adds is looked up before it stays.
  • Data in CSV, references in .bib and figures as image files moved unchanged, and one edit to rain.csv reached both PDFs.

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which requirements decide for LaTeX by themselves?

    A venue that builds LaTeX source or gives its own LaTeX class, a PDF that must meet PDF/UA-2 today, and a feature that exists only as a LaTeX package, such as a thesis class or a PGF/TikZ drawing.

  2. Which two limits named in Typst's guide for LaTeX users can decide for LaTeX?

    Its plotting ecosystem does not yet offer the breadth of PGF/TikZ, and a change of the page margins forces a page break, where LaTeX can change them anywhere.

  3. Where did each system report the misspelt margins option?

    Both LaTeX engines reported Package keyval Error: margins undefined inside geometry.sty, while Typst named margins.typ, line 2, column 23, and underlined the argument.

  4. What is a migration audit?

    A count, before converting, of the places pandoc drops or converts wrongly: the report had 23, 18 of them silent, which left an estimate of 15 fixes.

  5. How was the report migrated so that the fixes could run again?

    pandoc converted only the body, which a hand-written main.typ includes, and fixes.py applied the 15 fixes, so the whole migration can run again; then the section, equation, table and figure counts were the same on both sides.

  6. What does a build catch in a suggested citation, and what not?

    A key missing from the .bib file, which Typst stops on and latexmk prints as [?] with exit status 0, but not an invented work: look each DOI up through https://doi.org/ and check the record against the entry.

  7. Which files of the report moved to Typst unchanged?

    refs.bib, which Typst reads as BibLaTeX, data/rain.csv, which csv() reads, and the PNG charts; only the TikZ drawing had to be drawn again.

  8. Why make a figure as both SVG and PDF from one source?

    pdfLaTeX takes PDF figures but not SVG, and Typst cannot embed a PDF image in an export to PDF/UA-1 or another PDF standard, where it asks for SVG.

Terms in this lesson

migration audit
A count, made before anything is converted, of the places in a source that a conversion will not carry over as they are, such as custom macros, TikZ pictures and words written before references. Each place becomes a fix to plan or a check to make.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.