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.
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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:
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:
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. 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
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:
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") 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
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:
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") 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:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- Compile again. With the fixed body.typ the second compile passed, and the Typst PDF had 3 pages.
- 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.
- 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
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:
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}
} 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.
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
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:
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- The charts are drawn once. figures.py draws both charts from the same CSV file as PNG files, and both systems include PNG images.
- 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.
- 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.
- 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.
- 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
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:
"venue"is"latex-source": the venue builds LaTeX source or gives a LaTeX class. Return("latex", "venue")."standard"is"ua-2": the PDF must meet PDF/UA-2. Return("latex", "standard")."latex_only"is a list of features that only a LaTeX package provides. If it is not empty, return("latex", "latex_only")."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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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.
-
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.
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
.biband figures as image files moved unchanged, and one edit torain.csvreached both PDFs.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
- Guide for LaTeX users (limitations, Pandoc, packages) (Typst)
- Accessibility guide (PDF/UA-1 and PDF/UA-2) (Typst)
- LaTeX tagging project: usage instructions (The LaTeX Project)
- arXiv: submitting a PDF (arXiv)
- arXiv: TeX Live versions and processors (arXiv)
- CTAN, the Comprehensive TeX Archive Network (CTAN)
- geometry: flexible and complete interface to document dimensions (CTAN)
- Typst reference: page (the margin parameter) (Typst)
- Pandoc user’s guide (--verbose, the Typst writer) (pandoc)
- pgfplots: function and data plots in TeX (CTAN)
- pgfplotstable: loads, rounds, formats and postprocesses numerical tables (CTAN)
- Typst reference: bibliography (BibLaTeX and Hayagriva files) (Typst)
- Typst reference: data loading (csv, json, yaml, toml, xml) (Typst)
- Typst reference: image (formats, PDF images and PDF standards) (Typst)
- What is a DOI? (DOI Foundation)
- ICMJE recommendations: use of AI by authors (International Committee of Medical Journal Editors)
- ICMJE recommendations: responsibilities in the submission and peer-review process (International Committee of Medical Journal Editors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress