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 8 – Theses, reports and long documents

Build a thesis template from your university's rules

Turn a thesis manual into rules a script can measure, implement them once as a LaTeX class and a Typst template, and check every PDF against them.

  • Advanced
  • 20 minutes
  • Examples run with TeX Live 2026, TinyTeX 2026.10, Typst 0.15.1, Python 3.14.8, Pyodide 314.0.7, qpdf 12.4.1 and mupdf 1.28.3
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answer to its interview question.

What you will learn

  • Convert a written thesis manual into a list of rules, each with a number that can be measured
  • Implement the rules once, as a LaTeX class built on report and as a Typst template function
  • Check a compiled PDF against the rules with a script instead of by eye, and read what it reports
  • Use, adapt and share thesis templates responsibly, marked unofficial, licensed and checked against the current manual

Before you start

What you’ll learn · Before you start

What you will learn

  • Convert a written thesis manual into a list of rules, each with a number that can be measured
  • Implement the rules once, as a LaTeX class built on report and as a Typst template function
  • Check a compiled PDF against the rules with a script instead of by eye, and read what it reports
  • Use, adapt and share thesis templates responsibly, marked unofficial, licensed and checked against the current manual

Before you start

On this page

A thesis manual is written in sentences, for people, and a thesis office may check a PDF against it with a ruler. In this lesson’s run, a thesis on the report class’s own settings broke 5 of the 13 rules of an invented manual: wrong paper, 10 pt type, single spacing, page numbers too high and arabic numbers from the first page.

Here you turn that manual into rules a program can measure, write the checker before the template, and then implement the rules once as a LaTeX class and once as a Typst template, until both pass all 13. The last section is about using and sharing such templates honestly: who made them, under which licence, and against which edition of the manual.

A rule you can measure is a rule you can check

The manual of Example University, invented for this lesson, says “space the lines one and a half times the type size apart”. That sentence becomes checkable once it is a number: with 12 pt type, baseline: The line that letters stand on. Line spacing is measured from the baseline of one line to the baseline of the next, so 12 pt type set one and a half lines apart has baselines 18 pt apart. 18 pt apart. Every rule of the manual can be written that way, as a quantity, a limit and a way to measure it in the PDF:

Rule The manual says Measured in the PDF as
paper A4, 210 by 297 mm the size of every page
margins at least 35 mm left, 25 mm elsewhere where the ink of the text starts and ends
body size, line spacing 12 pt type, 18 pt from baseline to baseline the commonest type size, and the gap between baselines
chapter titles bold, at least 16 pt the font of each title line
page numbers centred at the foot, 12 to 20 mm up the last line of each page
numbering, order roman to chapter 1, then arabic; the manual’s order page labels, and the first line of each page
citations by number, as [3] numbers in brackets, and no author and year

rules.toml holds the 13 rules, each with the sentence it comes from, so that a reader can trace any number back to the manual. TOML is a plain format that Python reads with its standard tomllib module:

rules.toml: the invented manual as 13 rules TOML · rules.toml
# rules.toml: the thesis manual of Example University, invented for this lesson, as rules that a program can check.
# Each rule keeps the sentence of the manual it comes from (manual), the kind of measurement (check) and its numbers.
# spec_check.py reads this file and a PDF and says, rule by rule, what it measured and whether the rule holds.

[[rule]]
id = "paper"
manual = "Print on A4 paper, 210 by 297 mm."
check = "page-size"
width_mm = 210
height_mm = 297

[[rule]]
id = "margin-left"
manual = "Leave at least 35 mm on the left of every page, for the binding."
check = "margin"
side = "left"
min_mm = 35

[[rule]]
id = "margin-right"
manual = "Leave at least 25 mm on the right."
check = "margin"
side = "right"
min_mm = 25

[[rule]]
id = "margin-top"
manual = "Leave at least 25 mm above the text."
check = "margin"
side = "top"
min_mm = 25

[[rule]]
id = "margin-bottom"
manual = "Leave at least 25 mm below the text; the page number goes in this margin."
check = "margin"
side = "bottom"
min_mm = 25

[[rule]]
id = "body-size"
manual = "Set the text in 12 pt type."
check = "body-size"
pt = 12
tolerance_pt = 0.1

[[rule]]
id = "line-spacing"
manual = "Space the lines of the text one and a half times the type size apart: 18 pt from baseline to baseline."
check = "line-pitch"
pt = 18
tolerance_pt = 0.2

[[rule]]
id = "chapter-titles"
manual = "Set chapter titles in bold type of at least 16 pt."
check = "chapter-titles"
min_pt = 16

[[rule]]
id = "page-numbers"
manual = "Number every page but the title page at its foot, centred under the text, 12 to 20 mm above the bottom edge."
check = "page-numbers"
min_mm = 12
max_mm = 20
tolerance_mm = 1

[[rule]]
id = "numbering"
manual = "Number the pages before chapter 1 in lowercase roman, counting the title page, and from chapter 1 on in arabic from 1."
check = "labels"

[[rule]]
id = "front-matter"
manual = "Give the certificate, declaration, acknowledgements, abstract and contents in that order."
check = "order"
titles = ["Certificate", "Declaration", "Acknowledgements", "Abstract", "Contents"]

[[rule]]
id = "chapter-pages"
manual = "Start every chapter on a new page."
check = "chapter-pages"

[[rule]]
id = "citations"
manual = "Cite sources by number in square brackets, as [3], never by author and year."
check = "citations"

Some rules resist numbers: “use a clear, professional font” is a judgement. Ask the thesis office what it means, or write it down as a list of fonts that are accepted, and keep a short list of the rules you still check by eye.

Checkpoint

Check: Rules written as numbers

  1. Question 1 of 2 How does "space the lines one and a half times the type size apart" become a rule a script can check, for 12 pt type?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Baselines 18 pt apart, measured as the gap between body lines of one paragraph

    Write the quantity, the limit and where to measure it: 18 pt from baseline to baseline, with a tolerance for rounding.

  2. Question 2 of 2 The manual also says "use a clear, professional font". What do you do with that rule?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Ask the thesis office what it accepts, write the answer down as a list of fonts, or keep the rule on a short list checked by eye

    Most rules turn into numbers; the few that do not are named, so nobody believes the checker covers them.

The checker comes before the template

Write the check before the template, as a programmer writes a test before the code: then the template is done when the checker says so, not when the pages look right. spec_check.py reads a PDF with two programs, MuPDF’s mutool draw -F stext, which gives every line of text with its box, its baseline and the size and name of its font, and qpdf --json, which gives the page labels, and it prints one line per rule. Step through one rule from the manual to the verdict:

Interactive · stage by stage From a sentence of the manual to PASS or FAIL Step through one rule, the 18 pt line spacing, from the manual to the verdict, then see where the other rules take their numbers.
From a sentence of the manual to PASS or FAIL, step 6 of 6. Showing The manual's sentence, A rule in rules.toml, The compiled PDF, Lines from mutool, The measurement, PASS or FAIL. In focus: PASS or FAIL.The manual's sentence1.5 times the type sizeA rule in rules.tomlpt = 18, tolerance 0.2The compiled PDFthesis.pdfLines from mutoolsize, font, box, baselineThe measurementgaps between baselinesPASS or FAILone line per rule

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

Every step as text
  1. Start from the sentence. The manual asks for lines one and a half times the type size apart. For 12 pt type that is a number you can measure, 18 pt from one baseline to the next.
  2. Write it as a rule. rules.toml keeps the sentence beside the number: check = "line-pitch", pt = 18, and a tolerance of 0.2 pt for rounding. The rule says nothing about LaTeX or Typst.
  3. Build the PDF. The class or the template turns the thesis into a PDF. The checker reads only that file, so it judges what a reader and the thesis office will see.
  4. Read every line. mutool draw -F stext gives each line of text with the size and the name of its font, its box and its baseline, in points. qpdf gives the page labels.
  5. Measure. Body text is the type size used most, 12 pt here. The checker takes the gaps between baselines of body lines in one paragraph and keeps the most common one.
  6. Decide. The class measured 17.9 pt and Typst 18 pt, both within 0.2 pt of 18: PASS. The plain report measured 12 pt: FAIL. The same steps give every other rule its number.

spec_check.py: 13 measurements, one per rule Python · spec_check.py
"""Checks a thesis PDF against the rules of a manual, written as rules.toml, and prints one line per rule: what it
measured, and PASS or FAIL. Nothing is judged by eye: every number comes from the PDF.

    python3 spec_check.py rules.toml thesis.pdf

The PDF is read with two command-line tools: MuPDF's mutool draw -F stext gives each page's size and every line of
text with its box, its baseline and the size and name of its font, in points (72 to the inch), and qpdf --json gives
the page labels. The title page, the first page, is left out of the margin and page-number checks, since its layout is
the manual's own. A page number is the last line of a page when that line is the page's label. Margins are measured
from the ink of the text, from the first to the last visible character of each line (a PDF may keep a space after the
last word of a line, which is not ink), with 1 mm of tolerance, because glyphs such as a full stop may hang a little
into the margin. A line counts as body text when its type is the size used most in the document, and the spacing of
lines is the most common distance between the baselines of two body lines that follow each other in one paragraph.
"""

import json
import re
import subprocess
import sys
import tomllib
import xml.etree.ElementTree as ET
from collections import Counter

MM = 25.4 / 72  # millimetres per point
MARGIN_TOLERANCE_MM = 1.0
BOLD = re.compile(r"Bold|BX|Black|Heavy|Semibold", re.I)


def roman(number):
    """4 -> 'iv'."""
    out = ""
    for value, letters in [(1000, "m"), (900, "cm"), (500, "d"), (400, "cd"), (100, "c"), (90, "xc"), (50, "l"),
                           (40, "xl"), (10, "x"), (9, "ix"), (5, "v"), (4, "iv"), (1, "i")]:
        while number >= value:
            out, number = out + letters, number - value
    return out


def read_pages(pdf):
    """Every page as {width, height, lines}, each line {text, size, bold, x0, y0, x1, y1, baseline, block}."""
    xml = subprocess.run(["mutool", "draw", "-F", "stext", "-o", "-", pdf], capture_output=True, text=True,
                         check=True).stdout
    pages = []
    for page in ET.fromstring(xml).iter("page"):
        lines = []
        for number, block in enumerate(page.iter("block")):
            for line in block.iter("line"):
                fonts = line.findall("font")
                chars = [char for font in fonts for char in font.findall("char")]
                if not chars or not line.get("text", "").strip():
                    continue
                main = max(fonts, key=lambda font: len(font.findall("char")))
                _, y0, _, y1 = (float(v) for v in line.get("bbox").split())
                # The ink of the line runs from its first to its last visible character; a space that a PDF keeps
                # after the last word is not part of it.
                ink = [char for char in chars if char.get("c", "").strip()]
                corners = [float(v) for v in ink[0].get("quad").split()[0::2] + ink[-1].get("quad").split()[0::2]]
                lines.append({"text": line.get("text").strip(), "size": float(main.get("size")),
                              "bold": bool(BOLD.search(main.get("name", ""))), "x0": min(corners[:4]),
                              "y0": y0, "x1": max(corners[4:]), "y1": y1, "baseline": float(chars[0].get("y")),
                              "block": number})
        pages.append({"width": float(page.get("width")), "height": float(page.get("height")), "lines": lines})
    return pages


def read_labels(pdf, count):
    """The label of every page from the PDF's /PageLabels, or None when it has none."""
    found = json.loads(subprocess.run(["qpdf", "--json", "--json-key=pagelabels", pdf], capture_output=True,
                                      text=True, check=True).stdout)["pagelabels"]
    if not found:
        return None
    styles = {"/D": str, "/r": roman, "/R": lambda n: roman(n).upper()}
    out = []
    for index in range(count):
        start = max((r for r in found if r["index"] <= index), key=lambda r: r["index"])
        label = start["label"]
        number = label.get("/St", 1) + index - start["index"]
        out.append(styles[label["/S"]](number) if "/S" in label else "")
    return out


class Thesis:
    def __init__(self, pdf):
        self.pages = read_pages(pdf)
        self.labels = read_labels(pdf, len(self.pages))
        sizes = Counter()
        for page in self.pages:
            for line in page["lines"]:
                sizes[round(line["size"], 1)] += len(line["text"])
        self.body_size = sizes.most_common(1)[0][0]
        first_lines = [page["lines"][0]["text"] if page["lines"] else "" for page in self.pages]
        self.first_lines = first_lines
        self.chapter_one = next((i for i, line in enumerate(first_lines) if re.fullmatch(r"Chapter 1", line)), None)

    def number_line(self, index):
        """The page number line of page `index`, or None."""
        lines = self.pages[index]["lines"]
        if not lines or self.labels is None:
            return None
        last = max(lines, key=lambda line: line["baseline"])
        return last if last["text"] == self.labels[index] else None

    def text_lines(self, index):
        number = self.number_line(index)
        return [line for line in self.pages[index]["lines"] if line is not number]


def check_page_size(thesis, rule):
    sizes = {(round(p["width"] * MM, 1), round(p["height"] * MM, 1)) for p in thesis.pages}
    ok = all(abs(w - rule["width_mm"]) <= 1 and abs(h - rule["height_mm"]) <= 1 for w, h in sizes)
    return ", ".join(f"{w} × {h} mm" for w, h in sorted(sizes)), ok


def check_margin(thesis, rule):
    side = rule["side"]
    values = []
    for index in range(1, len(thesis.pages)):
        page, lines = thesis.pages[index], thesis.text_lines(index)
        if not lines:
            continue
        values.append({"left": min(l["x0"] for l in lines), "right": page["width"] - max(l["x1"] for l in lines),
                       "top": min(l["y0"] for l in lines), "bottom": page["height"] - max(l["y1"] for l in lines)}[side])
    least = min(values) * MM
    return f"{least:.1f} mm at the least", least >= rule["min_mm"] - MARGIN_TOLERANCE_MM


def check_body_size(thesis, rule):
    return f"{thesis.body_size:g} pt", abs(thesis.body_size - rule["pt"]) <= rule["tolerance_pt"]


def check_line_pitch(thesis, rule):
    gaps = Counter()
    for page in thesis.pages:
        body = [l for l in page["lines"] if abs(l["size"] - thesis.body_size) < 0.05]
        for before, after in zip(body, body[1:]):
            if before["block"] == after["block"]:
                gaps[round(after["baseline"] - before["baseline"], 1)] += 1
    if not gaps:
        return "no two body lines in a row", False
    pitch = gaps.most_common(1)[0][0]
    return f"{pitch:g} pt between baselines", abs(pitch - rule["pt"]) <= rule["tolerance_pt"]


def check_chapter_titles(thesis, rule):
    titles = []
    for index, page in enumerate(thesis.pages[1:], 1):
        lines = thesis.text_lines(index)
        if not lines or lines[0]["size"] <= thesis.body_size:
            continue
        opening = [lines[0]] + ([lines[1]] if re.fullmatch(r"Chapter \d+", lines[0]["text"]) and len(lines) > 1 else [])
        titles.extend(opening)
    if not titles:
        return "no chapter titles found", False
    smallest = min(t["size"] for t in titles)
    bold = sum(t["bold"] for t in titles)
    return f"{len(titles)} title lines, {bold} bold, {smallest:.1f} pt at the least", bold == len(titles) and \
        smallest >= rule["min_pt"]


def check_page_numbers(thesis, rule):
    found, good = 0, 0
    lefts = [min(l["x0"] for l in thesis.text_lines(i)) for i in range(1, len(thesis.pages)) if thesis.text_lines(i)]
    rights = [max(l["x1"] for l in thesis.text_lines(i)) for i in range(1, len(thesis.pages)) if thesis.text_lines(i)]
    middle = (min(lefts) + max(rights)) / 2
    heights = []
    for index in range(1, len(thesis.pages)):
        line = thesis.number_line(index)
        if line is None:
            continue
        found += 1
        above = (thesis.pages[index]["height"] - line["baseline"]) * MM
        centred = abs((line["x0"] + line["x1"]) / 2 - middle) * MM <= rule["tolerance_mm"]
        heights.append(above)
        good += centred and rule["min_mm"] <= above <= rule["max_mm"]
    pages = len(thesis.pages) - 1
    if not heights:
        return f"0 of {pages} pages numbered", False
    return f"{found} of {pages} numbered, {good} in place, {min(heights):.1f} to {max(heights):.1f} mm up", \
        found == good == pages


def check_labels(thesis, rule):
    if thesis.labels is None:
        return "no page labels", False
    if thesis.chapter_one is None:
        return "no page starts with Chapter 1", False
    expected = [roman(n) for n in range(1, thesis.chapter_one + 1)]
    expected += [str(n) for n in range(1, len(thesis.pages) - thesis.chapter_one + 1)]
    for index, (label, want) in enumerate(zip(thesis.labels, expected), 1):
        if label != want:
            return f"page {index} is {label or '(empty)'}, not {want}", False
    return f"{thesis.labels[0]} to {thesis.labels[thesis.chapter_one - 1]}, then 1 to {thesis.labels[-1]}", True


def check_order(thesis, rule):
    places = []
    for title in rule["titles"]:
        if title not in thesis.first_lines:
            return f"{title} missing", False
        places.append(thesis.first_lines.index(title))
    return f"{len(places)} parts on pages {', '.join(str(p + 1) for p in places)}", places == sorted(places)


def check_chapter_pages(thesis, rule):
    openings = [(i, line) for i, page in enumerate(thesis.pages) for line in page["lines"]
                if re.fullmatch(r"Chapter \d+", line["text"])]
    first = sum(line is thesis.pages[i]["lines"][0] for i, line in openings)
    return f"{first} of {len(openings)} chapters open a page", bool(openings) and first == len(openings)


def check_citations(thesis, rule):
    start = thesis.chapter_one or 0
    text = " ".join(line["text"] for page in thesis.pages[start:] for line in page["lines"])
    numbered = len(re.findall(r"\[\d+(?:[,–-] ?\d+)*\]", text))
    named = len(re.findall(r"\([A-Z][a-z]+(?: and [A-Z][a-z]+| et al\.)?,? \d{4}[a-z]?\)", text))
    return f"{numbered} numbered, {named} by author and year", numbered > 0 and named == 0


CHECKS = {"page-size": check_page_size, "margin": check_margin, "body-size": check_body_size,
          "line-pitch": check_line_pitch, "chapter-titles": check_chapter_titles, "page-numbers": check_page_numbers,
          "labels": check_labels, "order": check_order, "chapter-pages": check_chapter_pages,
          "citations": check_citations}


def main(rules_file, pdf):
    with open(rules_file, "rb") as handle:
        rules = tomllib.load(handle)["rule"]
    thesis = Thesis(pdf)
    passed = 0
    print(f"{'rule':<15}{'measured':<52}result")
    for rule in rules:
        measured, ok = CHECKS[rule["check"]](thesis, rule)
        passed += ok
        print(f"{rule['id']:<15}{measured:<52}{'PASS' if ok else 'FAIL'}")
    print(f"{len(rules)} rules: {passed} pass, {len(rules) - passed} fail")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

Its first run is on plain.tex, the thesis on the report class’s own settings, with nothing of the manual applied:

The thesis before the template, checked Python · rules_session.py
from session import files, run

# The test before the template: plain.tex, the thesis on the report class's own settings, built and checked against
# rules.toml with spec_check.py.
files("rules.toml", "spec_check.py", "latex/plain.tex=plain.tex", "latex/chapters.tex=chapters.tex")
run(r"""
latexmk -pdf -interaction=nonstopmode -silent plain.tex > /dev/null 2>&1
python3 spec_check.py rules.toml plain.pdf
""")

Output

$ latexmk -pdf -interaction=nonstopmode -silent plain.tex > /dev/null 2>&1
$ python3 spec_check.py rules.toml plain.pdf
rule           measured                                            result
paper          215.9 × 279.4 mm                                    FAIL
margin-left    47.2 mm at the least                                PASS
margin-right   47.4 mm at the least                                PASS
margin-top     45.1 mm at the least                                PASS
margin-bottom  42.4 mm at the least                                PASS
body-size      10 pt                                               FAIL
line-spacing   12 pt between baselines                             FAIL
chapter-titles 10 title lines, 10 bold, 20.7 pt at the least       PASS
page-numbers   9 of 9 numbered, 0 in place, 31.5 to 31.5 mm up     FAIL
numbering      page 1 is 1, not i                                  FAIL
front-matter   5 parts on pages 2, 3, 4, 5, 6                      PASS
chapter-pages  2 of 2 chapters open a page                         PASS
citations      4 numbered, 0 by author and year                    PASS
13 rules: 8 pass, 5 fail

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

Five rules failed, each with the number that broke it: the report class’s default paper is US letter, 215.9 by 279.4 mm; its default type is 10 pt, with baselines 12 pt apart; the page numbers stood 31.5 mm above the bottom edge; and the first page was labelled 1. The margins passed, but only because letter paper and the default text block leave wide ones. Eight passes are not eight rules that a template implements, which is why the template’s own run must pass all 13.

A checker you have not checked

Wrong: trusting a checker you have never run on a PDF whose numbers you know. A checker that measures the right margin from each line’s box finds Typst’s justified lines a millimetre in the margin, because in Typst’s PDFs that box includes a space after the last word. Right: measure the ink, from the first to the last visible character, as spec_check.py does, and look into any number that surprises you.

Checkpoint

Check: A checker written before the template

  1. Question 1 of 2 Why did plain.pdf fail the paper rule?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The report class's default paper is US letter, 215.9 by 279.4 mm

    A class or a template must set the paper; a4paper is the class's first option.

  2. Question 2 of 2 Eight of the 13 rules passed for plain.tex. What does that say about a template?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing yet; the margins passed only because letter paper and the default text block leave wide ones, and the template must pass all 13

    Run the checker first to see the gap, and again after every change, until every rule passes for the template's own PDF.

A class implements every rule once

A thesis layout is a class: the LaTeX guide for class authors says that commands which could be used with any class belong in a package, and the rest in a class. exu-thesis.cls builds on report with \LoadClass, takes two key-value option: A package or class option written as key=value, such as course=Hydrology lab, which a LaTeX file creates with \DeclareKeys and reads with \ProcessKeyOptions. An option without a value can set a switch. More in “Writing LaTeX classes and Typst templates”, degree and draft, and names in comments the rules each block implements; a later lesson of this track, on writing classes and Typst templates, goes through every line of such a file:

The class and a thesis that uses it

latex/exu-thesis.cls

% exu-thesis.cls: a thesis class for Example University, whose manual is invented for this lesson. Unofficial: no
% institution publishes it. Each block names the rules of rules.toml that it implements.
\NeedsTeXFormat{LaTeX2e}[2022-06-01]
\ProvidesClass{exu-thesis}[2026/10/01 v0.1 Thesis layout for the invented Example University manual (unofficial)]

% Options: the degree printed on the title page, and draft, which builds without images (graphicx's draft mode).
\DeclareKeys[exu]{
  degree.store = \exu@degree,
  degree.usage = load,
  draft.if     = exu@draft,
  draft.usage  = load,
}
\SetKeys[exu]{degree = Doctor of Philosophy}
\DeclareUnknownKeyHandler[exu]{\PassOptionsToClass{\CurrentOption}{report}}
\ProcessKeyOptions[exu]

% paper, body-size: A4 and 12 pt type; oneside, so the binding margin is on the left of every page.
\LoadClass[a4paper,12pt,oneside]{report}

% margin-left, margin-right, margin-top, margin-bottom: the text block, with the page number below it.
\RequirePackage[left=35mm,right=25mm,top=25mm,bottom=25mm]{geometry}
% line-spacing: setspace's one-and-a-half spacing.
\RequirePackage{setspace}
\onehalfspacing
\RequirePackage{graphicx}
\ifexu@draft \setkeys{Gin}{draft} \fi
% numbering, front-matter: the lists in the contents, page labels in the PDF.
\RequirePackage[nottoc]{tocbibind}
\RequirePackage{hyperref}

% The title page counts as page i and prints no number (rule numbering).
\newcommand{\department}[1]{\gdef\exu@department{#1}}
\newcommand{\exu@department}{}
\renewcommand{\maketitle}{%
  \pagenumbering{roman}%
  \thispagestyle{empty}%
  \begin{center}
    {\LARGE\bfseries\@title\par}
    \vspace{2cm}
    A thesis submitted for the degree of \exu@degree\par
    \vspace{1cm}
    {\large\@author\par}
    \vfill
    Department of \exu@department\\ Example University
  \end{center}
  \clearpage
}

% front-matter: one command for each unnumbered part, listed in the contents at the page where it opens.
\newcommand{\frontpart}[1]{\chapter*{#1}\addcontentsline{toc}{chapter}{#1}}
% numbering: chapter 1 starts the arabic count at 1, on a new page.
\newcommand{\mainpart}{\clearpage\pagenumbering{arabic}}

latex/thesis.tex

% thesis.tex: the invented thesis, built with exu-thesis.cls.
\documentclass[degree={Doctor of Philosophy}]{exu-thesis}
\title{Rooftop Rainwater Harvesting in Example District}
\author{Nisha Varghese}
\department{Civil Engineering}
\usepackage{lipsum}   % filler paragraphs, so that chapter 1 fills whole pages
\begin{document}
\maketitle
\frontpart{Certificate}
This is to certify that this thesis was carried out by Nisha Varghese under my supervision. (Invented wording.)
\frontpart{Declaration}
I declare that this thesis is my own work and that every source I used is cited. (Invented wording.)
\frontpart{Acknowledgements}
I thank the staff of the district library and of the school, who looked after the two gauges.
\frontpart{Abstract}
Two rain gauges on flat roofs in Example District logged every 0.2 mm of rain from June to November. This thesis
reports what they recorded and how much of it a roof could store.
\tableofcontents
\mainpart
\input{chapters}
\end{document}
The thesis built with the class, checked Python · class_session.py
from session import files, run

# The class at work: thesis.tex, built with exu-thesis.cls and checked against the same rules.
files("rules.toml", "spec_check.py", "latex/exu-thesis.cls=exu-thesis.cls", "latex/thesis.tex=thesis.tex",
      "latex/chapters.tex=chapters.tex")
run(r"""
latexmk -pdf -interaction=nonstopmode -silent thesis.tex > /dev/null 2>&1
python3 spec_check.py rules.toml thesis.pdf
""")

Output

$ latexmk -pdf -interaction=nonstopmode -silent thesis.tex > /dev/null 2>&1
$ python3 spec_check.py rules.toml thesis.pdf
rule           measured                                            result
paper          210.0 × 297.0 mm                                    PASS
margin-left    35.0 mm at the least                                PASS
margin-right   25.0 mm at the least                                PASS
margin-top     26.3 mm at the least                                PASS
margin-bottom  25.0 mm at the least                                PASS
body-size      12 pt                                               PASS
line-spacing   17.9 pt between baselines                           PASS
chapter-titles 10 title lines, 10 bold, 24.8 pt at the least       PASS
page-numbers   9 of 9 numbered, 9 in place, 14.5 to 14.5 mm up     PASS
numbering      i to vi, then 1 to 4                                PASS
front-matter   5 parts on pages 2, 3, 4, 5, 6                      PASS
chapter-pages  2 of 2 chapters open a page                         PASS
citations      4 numbered, 0 by author and year                    PASS
13 rules: 13 pass, 0 fail

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

All 13 rules passed. The page is A4, the text block: The part of a page that holds the running text, inside the margins. Page numbers and running headers sit outside it, in the margins. leaves 35 mm on the left and 25 mm on the right, the type is 12 pt, and setspace’s \onehalfspacing put the baselines 17.9 pt apart, inside the 0.2 pt tolerance of 18. The page numbers stood 14.5 mm above the bottom edge on all 9 numbered pages, and the labels ran i to vi, then 1 to 4. The front-matter commands of the earlier lesson are now part of the class: \frontpart{Declaration} writes the heading and its contents entry together, and \mainpart starts the arabic count, so a student cannot place either one wrongly.

Defaults in the class, choices in the options

Put the manual’s numbers into the class, not into each thesis: a student who writes \usepackage{geometry} with other margins in their own preamble is overriding the manual. What really differs between students, such as the degree or a draft build without images, becomes an option.

Checkpoint

Check: A LaTeX class that implements the rules

  1. Question 1 of 2 What does \frontpart{Declaration} do in exu-thesis.cls?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It writes \chapter*{Declaration} and its \addcontentsline in one step, so the entry always has the right page

    A class that names each step of the manual removes the chance of doing that step in the wrong order.

  2. Question 2 of 2 A student asks the class for an option to make the side margins 30 mm. What do you answer?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; the margins are the manual's numbers and stay in the class, and options are for what differs between students

    exu-thesis.cls has two options, degree and draft, because those are what really differ from thesis to thesis.

A Typst template is one function with the same rules

Typst’s tutorial builds a template as a function that takes the document’s content as its last parameter and is applied with #show: conf.with(…), its other parameters filled in by name. exu-thesis.typ does that, with title, author, degree and department, and checks the two it cannot do without. Line spacing needs care here. The paragraph reference says leading: In Typst, the space between the bottom edge of one line and the top edge of the next, set with par(leading: …). Together with the text's top and bottom edges it decides how far apart the baselines are. is the space between the bottom edge of one line and the top edge of the next, and the text reference says the top edge is, by default, the height of the capital letters. So the template makes each line 1 em high, from top-edge: 1em to bottom-edge: "baseline", and adds 6 pt of leading: 12 pt plus 6 pt is 18 pt from baseline to baseline.

Two versions of the template, and the thesis that uses them

first version · typst/first/exu-thesis.typ

// exu-thesis.typ, first version: a thesis template for Example University, whose manual is invented for this lesson. Unofficial: no
// institution publishes it. Each part names the rules of rules.toml that it implements.
#let exu-thesis(
  title: none,
  author: none,
  degree: "Doctor of Philosophy",
  department: none,
  body,
) = {
  assert(title != none, message: "exu-thesis: give the thesis a title, as in title: [Rainfall]")
  assert(type(author) == str, message: "exu-thesis: give the author as a string, as in author: \"N. Varghese\"")
  set document(title: title, author: author)
  // paper, margin-left, margin-right, margin-top, margin-bottom; page-numbers: roman until chapter 1, centred at the
  // foot of the page, in the bottom margin.
  set page(paper: "a4", margin: (left: 35mm, right: 25mm, top: 25mm, bottom: 25mm), numbering: "i")
  // body-size and line-spacing: 12 pt type whose lines are 1em high from baseline to baseline before leading, so that
  // 6 pt of leading puts the baselines 18 pt apart.
  set text(size: 12pt, top-edge: 1em, bottom-edge: "baseline")
  set par(leading: 6pt, spacing: 18pt, justify: true)
  set heading(numbering: none)
  // chapter-titles, chapter-pages: every part and chapter on a new page, its title bold at 20 pt.
  show heading.where(level: 1): it => {
    pagebreak(weak: true)
    set text(size: 20pt, weight: "bold")
    if it.numbering != none {
      block(context [Chapter #counter(heading).display()])
    }
    block(below: 24pt, it.body)
  }
  // numbering: the title page counts as page i and shows no number.
  page(numbering: none, align(center)[
    #text(size: 20pt, weight: "bold", title)
    #v(2cm)
    A thesis submitted for the degree of #degree
    #v(1cm)
    #text(size: 14pt, author)
    #v(1fr)
    Department of #department \ Example University
  ])
  body
}

// numbering: chapter 1 starts the arabic count at 1, at the top of a new page, and the chapters are numbered.
#let main-part(body) = {
  pagebreak()
  set page(numbering: "1")
  counter(page).update(1)
  set heading(numbering: "1.1")
  body
}

second version · typst/exu-thesis.typ

// exu-thesis.typ: a thesis template for Example University, whose manual is invented for this lesson. Unofficial: no
// institution publishes it. Each part names the rules of rules.toml that it implements.
#let exu-thesis(
  title: none,
  author: none,
  degree: "Doctor of Philosophy",
  department: none,
  body,
) = {
  assert(title != none, message: "exu-thesis: give the thesis a title, as in title: [Rainfall]")
  assert(type(author) == str, message: "exu-thesis: give the author as a string, as in author: \"N. Varghese\"")
  set document(title: title, author: author)
  // paper, margin-left, margin-right, margin-top, margin-bottom; page-numbers: roman until chapter 1, centred at the
  // foot of the page, in the bottom margin.
  set page(paper: "a4", margin: (left: 35mm, right: 25mm, top: 25mm, bottom: 25mm), numbering: "i")
  // body-size and line-spacing: 12 pt type whose lines are 1em high from baseline to baseline before leading, so that
  // 6 pt of leading puts the baselines 18 pt apart. margin-right: no glyph hangs into the margin, which the manual
  // measures from the text.
  set text(size: 12pt, top-edge: 1em, bottom-edge: "baseline", overhang: false)
  set par(leading: 6pt, spacing: 18pt, justify: true)
  set heading(numbering: none)
  // chapter-titles, chapter-pages: every part and chapter on a new page, its title bold at 20 pt.
  show heading.where(level: 1): it => {
    pagebreak(weak: true)
    set text(size: 20pt, weight: "bold")
    if it.numbering != none {
      block(context [Chapter #counter(heading).display()])
    }
    block(below: 24pt, it.body)
  }
  // numbering: the title page counts as page i, and its label in the PDF is i, but its footer shows no number.
  page(footer: none, align(center)[
    #text(size: 20pt, weight: "bold", title)
    #v(2cm)
    A thesis submitted for the degree of #degree
    #v(1cm)
    #text(size: 14pt, author)
    #v(1fr)
    Department of #department \ Example University
  ])
  body
}

// numbering: chapter 1 starts the arabic count at 1, at the top of a new page, and the chapters are numbered.
#let main-part(body) = {
  pagebreak()
  set page(numbering: "1")
  counter(page).update(1)
  set heading(numbering: "1.1")
  body
}

typst/thesis.typ

// thesis.typ: the invented thesis, built with the exu-thesis template.
#import "exu-thesis.typ": exu-thesis, main-part
#show: exu-thesis.with(
  title: [Rooftop Rainwater Harvesting in Example District],
  author: "Nisha Varghese",
  department: [Civil Engineering],
)

= Certificate
This is to certify that this thesis was carried out by Nisha Varghese under my supervision. (Invented wording.)

= Declaration
I declare that this thesis is my own work and that every source I used is cited. (Invented wording.)

= Acknowledgements
I thank the staff of the district library and of the school, who looked after the two gauges.

= Abstract
Two rain gauges on flat roofs in Example District logged every 0.2 mm of rain from June to November. This thesis
reports what they recorded and how much of it a roof could store.

#outline()

#show: main-part

= Introduction <chap:intro>
Rooftop rainwater harvesting stores the rain that falls on a roof for later use, in a tank or in the ground below the
building. In Example District most houses have flat concrete roofs, which shed water quickly and evenly, so a roof of
ninety square metres could gather a large part of a household's water in the wettest months of the year. Earlier work
measured storage tanks in small towns @rao2021tanks, but no record of rainfall on the roofs themselves has been kept in
the district, so this thesis sets two gauges on two roofs and reads them through a whole season of rain.

#for _ in range(6) [#lorem(110)\ ]

= Results <chap:results>
The library gauge recorded its wettest month in August, with 701 mm, and the school gauge recorded 528 mm in the same
month. The two gauges agreed on the order of the months, from the wettest to the driest, and on the dry spell that
followed the first week of September. Their totals differed most in July, when a storm passed north of the school and
over the library, which is why the yield tables of the district office give a range rather than one number for each
month @menon2023yield.

#bibliography("refs.bib", style: "ieee")
The thesis with each version of the template, checked Python · typst_session.py
from session import files, run

# The template at work: typst/thesis.typ compiled with the first version of the exu-thesis template, then with the
# second, and each PDF checked against the same rules.
files("rules.toml", "spec_check.py", "typst/first/exu-thesis.typ=first/exu-thesis.typ",
      "typst/exu-thesis.typ=exu-thesis.typ", "typst/thesis.typ=thesis.typ", "typst/refs.bib=refs.bib")
run(r"""
sed 's#"exu-thesis.typ"#"first/exu-thesis.typ"#' thesis.typ > first.typ
typst compile first.typ
python3 spec_check.py rules.toml first.pdf
typst compile thesis.typ
python3 spec_check.py rules.toml thesis.pdf
""")

Output

$ sed 's#"exu-thesis.typ"#"first/exu-thesis.typ"#' thesis.typ > first.typ
$ typst compile first.typ
$ python3 spec_check.py rules.toml first.pdf
rule           measured                                            result
paper          210.0 × 297.0 mm                                    PASS
margin-left    35.0 mm at the least                                PASS
margin-right   24.2 mm at the least                                PASS
margin-top     25.4 mm at the least                                PASS
margin-bottom  27.7 mm at the least                                PASS
body-size      12 pt                                               PASS
line-spacing   18 pt between baselines                             PASS
chapter-titles 10 title lines, 10 bold, 20.0 pt at the least       PASS
page-numbers   9 of 9 numbered, 9 in place, 13.3 to 13.3 mm up     PASS
numbering      page 1 is (empty), not i                            FAIL
front-matter   5 parts on pages 2, 3, 4, 5, 6                      PASS
chapter-pages  2 of 2 chapters open a page                         PASS
citations      4 numbered, 0 by author and year                    PASS
13 rules: 12 pass, 1 fail
$ typst compile thesis.typ
$ python3 spec_check.py rules.toml thesis.pdf
rule           measured                                            result
paper          210.0 × 297.0 mm                                    PASS
margin-left    35.0 mm at the least                                PASS
margin-right   25.0 mm at the least                                PASS
margin-top     25.4 mm at the least                                PASS
margin-bottom  27.7 mm at the least                                PASS
body-size      12 pt                                               PASS
line-spacing   18 pt between baselines                             PASS
chapter-titles 10 title lines, 10 bold, 20.0 pt at the least       PASS
page-numbers   9 of 9 numbered, 9 in place, 13.3 to 13.3 mm up     PASS
numbering      i to vi, then 1 to 4                                PASS
front-matter   5 parts on pages 2, 3, 4, 5, 6                      PASS
chapter-pages  2 of 2 chapters open a page                         PASS
citations      4 numbered, 0 by author and year                    PASS
13 rules: 13 pass, 0 fail

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

The first version passed 12 rules and failed one: its title page had an empty label, because numbering: none on that page leaves the label out, while the manual counts the title page as i. The second version keeps the roman numbering on the title page and removes only its footer, page(footer: none, …), so the label is i and no number is printed. It also turns off overhang, which the text reference says lets certain glyphs hang into the margin in justified text: with it, the right margin measured 24.2 mm, inside the checker’s 1 mm tolerance; without it, 25.0 mm. A stricter office would measure without tolerance, and the template should not depend on one.

Key idea

One rules file and one checker judge both implementations by their PDFs: the checker found the Typst template’s empty title label, which no page showed.

Checkpoint

Check: A Typst template that implements the rules

  1. Question 1 of 2 With top-edge: 1em and bottom-edge: "baseline", each line of 12 pt type is 12 pt high. How much leading puts the baselines 18 pt apart?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 6 pt

    Leading is the space between the bottom edge of one line and the top edge of the next, so 12 pt of line plus 6 pt of leading is 18 pt from baseline to baseline, as the checker measured.

  2. Question 2 of 2 Why did the first Typst template fail the numbering rule?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: numbering: none on the title page left its label empty, where the manual counts it as i

    page(footer: none, …) hides the number and keeps the count's label; numbering: none removes both.

A template is unofficial until the institution publishes it

Templates are easy to find. In Typst’s package index, read for this lesson, 226 of the 1,684 packages on Typst Universe mention a thesis or a dissertation in their name or description, and 223 of those are templates. A name proves nothing about whose rules a template follows. The package unofficial-iitkgp-thesis says so in its name and in its disclaimer, that it is not affiliated with, endorsed by or maintained by the institute; many others say nothing.

Not affiliated

Based on public information about the Indian Institute of Technology Kharagpur. MySmartCoPilot is not affiliated with the Indian Institute of Technology Kharagpur, and the Indian Institute of Technology Kharagpur’s real thesis rules may differ from what this lesson describes.

Before you use or publish a template, check four things:

Check Why
Which edition of the manual it follows Manuals change between years; a margin or the order of the front matter may have moved
What the checker says about its PDF Every rule of your manual, measured on a thesis built with it
Who made it, and whether the institution publishes it Only the institution can call a template official; call yours unofficial
Its licence Without one, others have no stated permission to copy or adapt it; the Typst team’s own templates carry the MIT-0 licence

When you share your own, publish the rules file and the checker with it, so a student next year can rerun the checks against the new manual, and mark every invented or example text as such, as this lesson’s certificate is marked.

A template handed down

When a template passes from one batch of students to the next, rerun the checker on it every year against the current manual, and keep the rules file in the template’s folder, so that anyone who receives it can check what it promises.

Checkpoint

Check: Using and sharing templates honestly

  1. One question A template on Typst Universe is named after your university. What does the name tell you?

    Choose one answer.

    Show the answer: One question

    Answer: Nothing about whose rules it follows; check its disclaimer, the edition of the manual it follows and what the checker says about its PDF

    Judge a template by your manual and your checker, not by its name.

Practise: measure line spacing and check page labels

Two of the checker’s measurements make good exercises: the line spacing, from the baselines mutool reports, and the numbering rule, from the page labels. Each runs in the page against sample tests taken from this lesson’s PDFs, and its first hint is open:

Code lab · 2 steps

Two measurements of a thesis checker

Measure the line spacing of a page from its baselines, then report the first page whose label breaks the manual's numbering.

  1. Measure the line spacing of a PDF To do
  2. Check the page labels against the manual To do

Exercise · Medium · Python

Measure the line spacing of a PDF

spec_check.py decided the line-spacing rule from the lines that mutool read out of each PDF. Write line_pitch(lines, body_size) in pitch.py, the measurement behind that rule.

lines lists the lines of a page in reading order, each as (block, size, baseline): the number of the paragraph block the line belongs to, its type size in points, and the height of its baseline in points, counted down from the top of the page. Return the most common gap between the baselines of two lines that follow each other in the list, when both belong to the same block and both have the body size (within 0.05 pt). Round each gap to 0.1 pt before you count it. When two gaps are equally common, return the smaller one; when no pair of lines qualifies, return None.

Three body lines of one paragraph whose baselines are at 100.0, 118.0 and 136.0 give 18.0.

Starter code · pitch.py

from collections import Counter


def line_pitch(lines, body_size):
    """Return the most common baseline-to-baseline gap of body lines that follow each other in one block."""
    # Replace this line: count the rounded gaps between body lines of the same block.
    return None
The sample tests · test_pitch.py
from pitch import line_pitch


def test_one_paragraph():
    """measures three lines 18 pt apart"""
    assert line_pitch([(0, 12.0, 100.0), (0, 12.0, 118.0), (0, 12.0, 136.0)], 12.0) == 18.0


def test_rounding():
    """rounds each gap to 0.1 pt, as LaTeX's one-and-a-half spacing measured 17.9 pt"""
    lines = [(0, 12.0, 100.0), (0, 12.0, 117.93), (0, 12.0, 135.86), (0, 12.0, 153.79)]
    assert line_pitch(lines, 12.0) == 17.9


def test_other_blocks_do_not_count():
    """leaves out the gap between two paragraphs"""
    lines = [(0, 12.0, 100.0), (0, 12.0, 118.0), (1, 12.0, 160.0), (1, 12.0, 178.0)]
    assert line_pitch(lines, 12.0) == 18.0


def test_other_sizes_do_not_count():
    """leaves out headings and footnotes"""
    lines = [(0, 20.0, 60.0), (0, 20.0, 85.0), (0, 20.0, 110.0), (1, 12.0, 140.0), (1, 12.0, 152.0)]
    assert line_pitch(lines, 12.0) == 12.0


def test_ties_take_the_smaller_gap():
    """returns the smaller of two equally common gaps"""
    lines = [(0, 12.0, 100.0), (0, 12.0, 118.0), (0, 12.0, 130.0)]
    assert line_pitch(lines, 12.0) == 12.0


def test_no_pair():
    """returns None when no two body lines follow each other in one block"""
    assert line_pitch([(0, 12.0, 100.0), (1, 12.0, 118.0)], 12.0) is None
    assert line_pitch([], 12.0) is None
A hint

Keep only the body lines first, then pair each line with the next one: zip(body, body[1:]). Count the rounded gaps of pairs in the same block with collections.Counter, and choose with min(counts, key=lambda gap: (-counts[gap], gap)).

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.

Exercise · Medium · Python

Check the page labels against the manual

The numbering rule caught the first version of the Typst template: its title page had an empty label. Write numbering_problem(labels, first_lines) in numbering.py, the check behind that rule.

labels holds the label of every page, in the order of the PDF ("" for an empty label), and first_lines the first line of every page. Chapter 1 opens on the first page whose first line is exactly "Chapter 1". The manual wants the pages before it labelled i, ii, iii and so on, counting from the first page, and the pages from it on labelled 1, 2, 3 and so on.

Return None when every label is right. Otherwise return one string: "no page starts with Chapter 1", or, for the first page whose label is wrong, "page <n> is <label>, not <expected>", where <n> counts pages from 1 and an empty label is written (empty). The first Typst template gives "page 1 is (empty), not i".

Starter code · numbering.py

def numbering_problem(labels, first_lines):
    """Return None when the labels follow the manual, else the first problem as a string."""
    # Replace this line: find chapter 1, build the labels the manual expects and compare.
    return None
The sample tests · test_numbering.py
from numbering import numbering_problem

FIRST = ["Rooftop Rainwater Harvesting", "Certificate", "Declaration", "Acknowledgements", "Abstract", "Contents",
         "Chapter 1", "Rain fell all week", "Chapter 2", "Bibliography"]
GOOD = ["i", "ii", "iii", "iv", "v", "vi", "1", "2", "3", "4"]


def test_the_class():
    """accepts the labels of the LaTeX class's thesis"""
    assert numbering_problem(GOOD, FIRST) is None


def test_empty_title_label():
    """reports the first Typst template's title page"""
    assert numbering_problem([""] + GOOD[1:], FIRST) == "page 1 is (empty), not i"


def test_no_roman_front_matter():
    """reports the plain report, numbered 1, 2, 3 from the start"""
    assert numbering_problem([str(n) for n in range(1, 11)], FIRST) == "page 1 is 1, not i"


def test_no_restart_at_chapter_one():
    """reports a chapter 1 that does not start the arabic count"""
    labels = GOOD[:6] + ["7", "8", "9", "10"]
    assert numbering_problem(labels, FIRST) == "page 7 is 7, not 1"


def test_a_late_mistake():
    """reports the first wrong page only"""
    labels = GOOD[:8] + ["4", "5"]
    assert numbering_problem(labels, FIRST) == "page 9 is 4, not 3"


def test_larger_roman_numbers():
    """writes roman numbers past viii correctly"""
    first = ["Title"] + ["Part"] * 10 + ["Chapter 1"]
    labels = ["i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "1"]
    assert numbering_problem(labels, first) is None
    assert numbering_problem(labels[:8] + ["viiii"] + labels[9:], first) == "page 9 is viiii, not ix"


def test_no_chapter_one():
    """says when no page starts with Chapter 1"""
    assert numbering_problem(GOOD, ["Chapter 2" if line == "Chapter 1" else line for line in FIRST]) == \
        "no page starts with Chapter 1"
A hint

Build the list of labels the manual expects, roman numbers up to chapter 1 and arabic numbers after it, then compare it with labels page by page with enumerate(zip(labels, expected), 1). A loop over the pairs (1000, "m"), (900, "cm") … (1, "i") writes a roman number.

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.

PDF Metadata Viewer & Editor See the size of every page of your thesis PDF in millimetres, the first rule of almost every manual.

Then work through the questions. Each answer is explained once you check it, and the practice comes back to the ideas you have missed:

Practice that adapts to you

Practise this lesson

15 questions about 5 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 rule of a manual is already measurable as it is written?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Leave at least 35 mm on the left of every page

    A measurable rule names a quantity and a limit, and says where in the PDF to measure it.

  2. Question 2 of 15 For 11 pt type, how far apart are the baselines under the rule "one and a half times the type size apart"?

    Type a number.

    Show the answer: Question 2 of 15

    Answer: 16.5 pt (anything from 16.45 to 16.55 counts)

    1.5 times 11 pt is 16.5 pt. Write that number into the rules file when the manual allows 11 pt type.

  3. Question 3 of 15 The current manual says "double spacing", and last year's said "24 pt between lines". What goes into rules.toml?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The number the thesis office confirms for this year, with the current manual's sentence and edition beside it

    Keep the edition of the manual in the rules file, so a later check knows which rules it is applying.

  4. Question 4 of 15 Which program gave spec_check.py the size and font of every line of the PDF?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: MuPDF's mutool draw -F stext

    Reading the PDF itself means the same checker judges a thesis from any system, including a word processor.

  5. Question 5 of 15 Why does the checker measure margins from the first and last visible characters of each line, not from the line's box?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: A PDF may keep a space after the last word, which put Typst's justified lines a millimetre into the margin

    Check the checker on PDFs you know, and look into every number that surprises you.

  6. Question 6 of 15 With overhang on, the right margin measured 24.2 mm and passed within the checker's 1 mm tolerance. The thesis office measures with no tolerance. What do you do?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: Fix the cause, overhang: false, so that the ink ends at 25.0 mm, and keep the tolerance for rounding only

    A tolerance is for measurement noise; a known cause of a narrower margin is fixed in the template.

  7. Question 7 of 15 What does \LoadClass[a4paper,12pt,oneside]{report} do in exu-thesis.cls?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: It builds the thesis class on the report class, with A4 paper, 12 pt type and one-sided printing

    The paper and the type size are the first two rules, so they are written once, where the class loads report.

  8. Question 8 of 15 Which line of exu-thesis.cls made the line-spacing rule pass?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: \onehalfspacing, from the setspace package

    setspace's one-and-a-half spacing came out at 17.9 pt for 12 pt type, close to 18 but not exactly, which is why the rule has a tolerance.

  9. Question 9 of 15 Next year the manual asks for 40 mm on the left. What changes?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: The left margin in the class's geometry line and in rules.toml; then rebuild and rerun the checker on every thesis

    Keep the class and the rules file in step, and date them by the edition of the manual they follow.

  10. Question 10 of 15 How does thesis.typ apply the exu-thesis template?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: #show: exu-thesis.with(title: …, author: …, department: …)

    import brings the function in by name, and the show rule passes the rest of the document to it.

  11. Question 11 of 15 What does overhang: false change in the second template?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: No glyph hangs into the margin in justified text, so the right margin measured 25.0 mm instead of 24.2 mm

    Overhang looks good on a printed page and costs a millimetre against a manual that measures from the text.

  12. Question 12 of 15 A student calls the template with author: ("N. Varghese", "A. Rao"). What happens?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: The compile stops at the template's assert, with its message to give the author as a string

    A template's users rarely read its code, so it checks its arguments first and says how to fix them.

  13. Question 13 of 15 Who can make a thesis template official?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Only the institution whose manual it follows, by publishing it as its own

    Call a template you share unofficial, and say which edition of the manual it follows.

  14. Question 14 of 15 Which licence do the templates that the Typst team maintains carry?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: MIT-0

    A licence tells others what they may do with your template; choose one before you share it.

  15. Question 15 of 15 You are about to publish your department's template. What do you put in the same folder?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: The rules file, the checker, a licence, and a note that it is unofficial and which edition of the manual it follows

    A template that ships with its own checks can be trusted for exactly as long as the checks pass.

Interview questions

Warm-up (fresher to 2–5 years): why write a thesis manual’s rules as numbers before you write any template code?

Because a number can be checked and a sentence cannot. “One and a half spacing” becomes 18 pt from baseline to baseline for 12 pt type, and “a wide left margin” becomes at least 35 mm, so a script can measure every page of the PDF and say which rule fails and by how much. Written first, the rules are the test the template must pass: in this lesson the report class’s own settings failed 5 of 13 rules, and the class and the second Typst template passed all 13. The numbers also expose what the words hide: LaTeX’s one-and-a-half spacing measured 17.9 pt, and Typst needed explicit text edges and 6 pt of leading to reach 18 pt. Rules that stay judgements, such as a clear font, go on a short list that someone checks by eye.

Two harder questions follow: building templates for a whole department, and a thesis rejected for its margins although it was built from a template named after the university. Plan your first step before you open each one:

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

  1. Fresher to mid

    A student built their thesis from a popular online template named after your university, and the thesis office rejected it: the left margin is 30 mm, and the manual asks for 35. What went wrong, and what process stops it from happening to the next student?

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

Key takeaways

  • Rewrite each rule of a thesis manual as a quantity with a limit and a way to measure it in the PDF, and keep the manual’s sentence beside it.
  • Write the checker before the template, check the checker on a PDF you know, and treat a template as done only when every rule passes.
  • A LaTeX class built on report puts the manual’s numbers in one file, with options only for what differs between students.
  • A Typst template is one function applied with a show rule; its line spacing needs explicit text edges and leading, and its title page needs roman numbering without a footer.
  • A template is unofficial until the institution publishes it: check its edition of the manual, its PDF and its licence, and share yours with the rules and the checker.

Flashcards · 8 cards

Recall it, then check

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

  1. How do you make "one and a half spacing" checkable?

    Turn it into a number you can measure in the PDF: with 12 pt type, baselines 18 pt apart.

  2. What do you do with a rule that resists numbers, such as "a clear, professional font"?

    Ask the thesis office what it means or write down the accepted fonts, and keep a short list of the rules you check by eye.

  3. Why write the checker before the template?

    The template is then done when every rule passes, not when the pages look right; the report class's own settings failed 5 of the 13 rules.

  4. Which defaults of the report class broke the invented manual?

    US letter paper, 10 pt type with baselines 12 pt apart, page numbers 31.5 mm above the bottom edge, and arabic numbers from the first page.

  5. What belongs in a thesis class, and what in its options?

    The manual's numbers belong in the class; only what really differs between students, such as the degree or a draft build, becomes an option.

  6. How does a Typst template get 18 pt between baselines with 12 pt type?

    It makes each line 1 em high, with top-edge: 1em and bottom-edge: "baseline", and adds 6 pt of leading.

  7. Why did the first Typst template fail the numbering rule?

    numbering: none on the title page left its label empty. Keeping roman numbering and removing only the footer labels it i without printing a number.

  8. What do you check before using a thesis template found online?

    Which edition of the manual it follows, what a checker says about its PDF, whether the institution publishes it, and its licence: a template is unofficial until the institution publishes it.

Terms in this lesson

baseline
The line that letters stand on. Line spacing is measured from the baseline of one line to the baseline of the next, so 12 pt type set one and a half lines apart has baselines 18 pt apart.
leading
In Typst, the space between the bottom edge of one line and the top edge of the next, set with par(leading: …). Together with the text's top and bottom edges it decides how far apart the baselines are.
text block
The part of a page that holds the running text, inside the margins. Page numbers and running headers sit outside it, in the margins.

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.