Your country

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

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

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

Collaboration, version control and tracked changes

Keep LaTeX and Typst in Git with diffs that show sentences, mark changes between drafts with latexdiff, merge Word tracked changes, keep notes out of PDFs.

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

What you will learn

  • Organise LaTeX and Typst sources in Git with one sentence per line, so that diffs and merges show changed sentences
  • Generate a PDF that marks every change between two LaTeX drafts with latexdiff, and check it
  • Compare two Typst drafts through the text a reader sees
  • Merge a co-author's tracked changes in a Word file back into the source with pandoc
  • Remove notes for co-authors from the final PDF without editing the source, and choose an online editor by its features

Before you start

What you’ll learn · Before you start

What you will learn

  • Organise LaTeX and Typst sources in Git with one sentence per line, so that diffs and merges show changed sentences
  • Generate a PDF that marks every change between two LaTeX drafts with latexdiff, and check it
  • Compare two Typst drafts through the text a reader sees
  • Merge a co-author's tracked changes in a Word file back into the source with pandoc
  • Remove notes for co-authors from the final PDF without editing the source, and choose an online editor by its features

Before you start

On this page

Two co-authors who each change one sentence of the same paragraph should not have to merge their work by hand. In this lesson’s session they did have to, because the paragraph was a single line of the file; with one sentence per line, Git merged the same two edits by itself.

Below, two people write a short report together. You will lay its source out so that Git shows and merges sentences, keep the files that builds write out of the repository, give a supervisor a PDF with every change between two drafts marked, compare Typst drafts, bring a Word user’s tracked changes back into the source, and keep notes for co-authors out of the final PDF. Every session was recorded with Git 2.54, TeX Live 2026, Typst 0.15.1 and pandoc 3.12.

One sentence per line lets Git merge two people’s changes

Git compares files line by line. A diff lists the lines that changed, and a merge combines two branches’ changes as long as they touch different lines; when both change the same lines, the result is a merge conflict: What Git reports when two branches changed the same lines of a file, or lines next to each other: it stops and writes both versions between conflict markers for a person to resolve.. In this session two co-authors change one paragraph of three sentences: Ravi, on the branch ravi, the last sentence, and Meera, on main, the first. two_edits.sh makes both commits, and the merge is recorded three times, with the paragraph on one line, with one sentence per line, and with Ravi’s change moved to the second sentence, next to Meera’s:

Two branches, one paragraph

merge/two_edits.sh

# two_edits.sh FOLDER OLD1 NEW1 OLD2 NEW2
# In FOLDER, commit method.tex, then change it on two branches, as two co-authors would:
#   on the branch ravi, OLD1 becomes NEW1;
#   on main, OLD2 becomes NEW2.
# It stops on main, ready for git merge ravi, and stops at the first command that fails.
set -e
cd "$1"
git init -q -b main .
git add method.tex
git commit -q -m 'Method, first draft'
git switch -q -c ravi
python3 ../edit.py method.tex "$2" "$3"
git commit -q -am 'Mark missing readings'
git switch -q main
python3 ../edit.py method.tex "$4" "$5"
git commit -q -am 'Two weeks of data'

edit.py

"""Replaces one piece of text in a file, the way a co-author changes one sentence in an editor.

    python3 edit.py FILE OLD NEW

The first OLD in FILE becomes NEW, and the file is saved. It stops with exit status 1, and changes nothing, when FILE
does not contain OLD, so a session never goes on as if an edit had been made.
"""

import sys
from pathlib import Path

path, old, new = Path(sys.argv[1]), sys.argv[2], sys.argv[3]
text = path.read_text(encoding="utf-8")
if old not in text:
    sys.exit(f"{path}: no {old!r} in the file")
path.write_text(text.replace(old, new, 1), encoding="utf-8")
The same two edits, merged three ways Python · merge_session.py
from session import files, needs, run

# Two co-authors change one paragraph on two branches, and Git merges them: first with the paragraph on one line,
# then with one sentence per line, then with the two changes in neighbouring sentences.
needs("git", "python3")
files("edit.py", "merge/two_edits.sh", "merge/paragraph.tex=paragraph/method.tex",
      "merge/sentences.tex=sentences/method.tex", "merge/sentences.tex=neighbours/method.tex")
run("""
# 1. The whole paragraph on one line; Ravi changes the last sentence, Meera the first.
sh two_edits.sh paragraph 'left blank.' 'left blank and marked as missing.' 'for a week.' 'for two weeks.'
!cd paragraph && git merge ravi -m 'Merge ravi'
cat paragraph/method.tex
# 2. One sentence per line, the same two changes.
sh two_edits.sh sentences 'left blank.' 'left blank and marked as missing.' 'for a week.' 'for two weeks.'
cd sentences && git merge ravi -m 'Merge ravi'
cat sentences/method.tex
# 3. One sentence per line, but Ravi changes the second sentence, next to Meera's first.
sh two_edits.sh neighbours 'at 8 a.m.' 'at 8 a.m. and 6 p.m.' 'for a week.' 'for two weeks.'
!cd neighbours && git merge ravi -m 'Merge ravi'
cat neighbours/method.tex
""")

Output

# 1. The whole paragraph on one line; Ravi changes the last sentence, Meera the first.
$ sh two_edits.sh paragraph 'left blank.' 'left blank and marked as missing.' 'for a week.' 'for two weeks.'
$ cd paragraph && git merge ravi -m 'Merge ravi'
Auto-merging method.tex
CONFLICT (content): Merge conflict in method.tex
Automatic merge failed; fix conflicts and then commit the result.
[exit status 1]
$ cat paragraph/method.tex
<<<<<<< HEAD
Two gauges logged the rain for two weeks. They were read at 8 a.m. every day. A reading that could not be taken was left blank.
=======
Two gauges logged the rain for a week. They were read at 8 a.m. every day. A reading that could not be taken was left blank and marked as missing.
>>>>>>> ravi
# 2. One sentence per line, the same two changes.
$ sh two_edits.sh sentences 'left blank.' 'left blank and marked as missing.' 'for a week.' 'for two weeks.'
$ cd sentences && git merge ravi -m 'Merge ravi'
Auto-merging method.tex
Merge made by the 'ort' strategy.
 method.tex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ cat sentences/method.tex
Two gauges logged the rain for two weeks.
They were read at 8 a.m. every day.
A reading that could not be taken was left blank and marked as missing.
# 3. One sentence per line, but Ravi changes the second sentence, next to Meera's first.
$ sh two_edits.sh neighbours 'at 8 a.m.' 'at 8 a.m. and 6 p.m.' 'for a week.' 'for two weeks.'
$ cd neighbours && git merge ravi -m 'Merge ravi'
Auto-merging method.tex
CONFLICT (content): Merge conflict in method.tex
Automatic merge failed; fix conflicts and then commit the result.
[exit status 1]
$ cat neighbours/method.tex
<<<<<<< HEAD
Two gauges logged the rain for two weeks.
They were read at 8 a.m. every day.
=======
Two gauges logged the rain for a week.
They were read at 8 a.m. and 6 p.m. every day.
>>>>>>> ravi
A reading that could not be taken was left blank.

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

With the paragraph on one line, both branches had changed line 1, so Git stopped with CONFLICT (content) and wrote the whole paragraph twice between conflict markers. With one sentence per line, the changes sat on lines 1 and 3, and Git answered Merge made by the 'ort' strategy with both sentences edited. Changes to neighbouring lines conflict too, as the third merge shows, but its markers held two sentences instead of a paragraph. Step through the merges:

Interactive · step by step Two edits to one paragraph, merged by Git Step through the same two changes in both layouts and watch where Git can merge them and where it stops.
Two edits to one paragraph, merged by Git, step 6 of 6. Showing One line per paragraph (1 line, 3 sentences), One line per sentence (3 lines), Ravi edits his branch (changes line 1), Ravi edits his branch (changes line 2), Meera edits main (changes line 1), Meera edits main (changes line 1), git merge ravi (conflict: whole paragraph), git merge ravi (conflict: lines 1 and 2). In focus: git merge ravi.One line per paragraph1 line, 3 sentencesOne line per sentence3 linesRavi edits his branchchanges line 1Ravi edits his branchchanges line 2Meera edits mainchanges line 1Meera edits mainchanges line 1git merge raviconflict: whole paragraphgit merge raviconflict: lines 1 and 2smaller conflict

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

Every step as text
  1. The same three sentences. Both files hold the method paragraph of three sentences. On the left it is one line of the file; on the right each sentence has a line of its own.
  2. Ravi changes the last sentence. On his branch, Ravi adds and marked as missing to the last sentence. On the left that changes line 1, the only line; on the right it changes line 3.
  3. Meera changes the first sentence. On main, Meera changes for a week to for two weeks. That is line 1 in both files.
  4. One line changed twice. Git merges by lines. On the left both branches changed line 1, so Git cannot choose: it stopped with CONFLICT (content) and wrote both versions of the whole paragraph between conflict markers.
  5. Two different lines. On the right the branches changed lines 1 and 3, with line 2 untouched between them, so Git took both changes: Merge made by the ort strategy, and the file holds both sentences as edited.
  6. Neighbouring lines still collide. In the third recorded merge Ravi changed line 2 instead. Changes to neighbouring lines conflict too, but the markers held only the two changed sentences, not the paragraph.

The same layout makes diffs readable. This session commits two drafts of report.tex, each sentence on its own line, and asks Git what changed; then it builds the report and asks what Git sees in the folder:

report.tex, two drafts

draft 1 · drafts/v1.tex

\documentclass{article}
\begin{document}
\section{Method}
Two gauges logged the rain on the roofs of Block~A and Block~B for a week.
The gauges were read at 8 a.m. every day.
A reading that could not be taken was left blank.
\section{Result}
Block~A received 41.5\,mm and Block~B 38.0\,mm, a difference of 3.5\,mm.
\end{document}

draft 2 · drafts/v2.tex

\documentclass{article}
\begin{document}
\section{Method}
Two gauges logged the rain on the roofs of Block~A and Block~B for two weeks.
The gauges were read at 8 a.m. every day.
A reading that could not be taken was left blank and marked as missing.
\section{Result}
Block~A received 83.0\,mm and Block~B 76.5\,mm, a difference of 6.5\,mm.
The gauges agree to within their stated error.
\end{document}
git diff on the drafts, then git status after a build Python · git_session.py
from session import files, needs, run

# The two drafts of report.tex as two commits: what git diff shows, with and without the tex diff driver of
# .gitattributes and with --word-diff; then a build, and what git status says before and after a .gitignore.
needs("git", "pdflatex", "latexmk")
files("drafts/v1.tex=report.tex", "drafts/gitignore.txt")
run("""
git init -q -b main . && git add report.tex && git commit -q -m 'Draft 1'
""")
files("drafts/v2.tex=v2.tex")
run("""
mv v2.tex report.tex
git diff --stat
git diff -U0
echo '*.tex diff=tex' > .gitattributes
git diff -U0
git diff --word-diff -U0
git add .gitattributes report.tex && git commit -q -m 'Draft 2'
latexmk -pdf -silent report.tex > /dev/null 2>&1
git status --short
cp gitignore.txt .gitignore && rm gitignore.txt
git status --short
git check-ignore -v report.log report.pdf
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ git init -q -b main . && git add report.tex && git commit -q -m 'Draft 1'
$ mv v2.tex report.tex
$ git diff --stat
 report.tex | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
$ git diff -U0
diff --git a/report.tex b/report.tex
index f1f6bfe..e334a5a 100644
--- a/report.tex
+++ b/report.tex
@@ -4 +4 @@
-Two gauges logged the rain on the roofs of Block~A and Block~B for a week.
+Two gauges logged the rain on the roofs of Block~A and Block~B for two weeks.
@@ -6 +6 @@ The gauges were read at 8 a.m. every day.
-A reading that could not be taken was left blank.
+A reading that could not be taken was left blank and marked as missing.
@@ -8 +8,2 @@ A reading that could not be taken was left blank.
-Block~A received 41.5\,mm and Block~B 38.0\,mm, a difference of 3.5\,mm.
+Block~A received 83.0\,mm and Block~B 76.5\,mm, a difference of 6.5\,mm.
+The gauges agree to within their stated error.
$ echo '*.tex diff=tex' > .gitattributes
$ git diff -U0
diff --git a/report.tex b/report.tex
index f1f6bfe..e334a5a 100644
--- a/report.tex
+++ b/report.tex
@@ -4 +4 @@ \section{Method}
-Two gauges logged the rain on the roofs of Block~A and Block~B for a week.
+Two gauges logged the rain on the roofs of Block~A and Block~B for two weeks.
@@ -6 +6 @@ \section{Method}
-A reading that could not be taken was left blank.
+A reading that could not be taken was left blank and marked as missing.
@@ -8 +8,2 @@ \section{Result}
-Block~A received 41.5\,mm and Block~B 38.0\,mm, a difference of 3.5\,mm.
+Block~A received 83.0\,mm and Block~B 76.5\,mm, a difference of 6.5\,mm.
+The gauges agree to within their stated error.
$ git diff --word-diff -U0
diff --git a/report.tex b/report.tex
index f1f6bfe..e334a5a 100644
--- a/report.tex
+++ b/report.tex
@@ -4 +4 @@ \section{Method}
Two gauges logged the rain on the roofs of Block~A and Block~B for [-a week-]{+two weeks+}.
@@ -6 +6 @@ \section{Method}
A reading that could not be taken was left blank {+and marked as missing+}.
@@ -8 +8,2 @@ \section{Result}
Block~A received [-41-]{+83+}.[-5-]{+0+}\,mm and Block~B [-38-]{+76+}.[-0-]{+5+}\,mm, a difference of [-3-]{+6+}.5\,mm.
{+The gauges agree to within their stated error.+}
$ git add .gitattributes report.tex && git commit -q -m 'Draft 2'
$ latexmk -pdf -silent report.tex > /dev/null 2>&1
$ git status --short
?? gitignore.txt
?? report.aux
?? report.fdb_latexmk
?? report.fls
?? report.log
?? report.pdf
$ cp gitignore.txt .gitignore && rm gitignore.txt
$ git status --short
?? .gitignore
$ git check-ignore -v report.log report.pdf
.gitignore:7:*.log	report.log
.gitignore:12:*.pdf	report.pdf

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

Each changed sentence showed as one changed line. The line after @@ is the header of a hunk: One block of changed lines in a diff, introduced by a header line that starts with @@ and gives where the block starts in the old and in the new file.: by default Git took the nearest line that starts with a letter, a sentence that tells you nothing, but with *.tex diff=tex in .gitattributes, one of the built-in patterns the gitattributes manual lists, it named the section, \section{Method}. --word-diff showed the changed words inside each line, [-a week-]{+two weeks+}; with the tex pattern on, it split numbers at the full stop, so 41.5 became two changes.

Commit what you write, never what the build writes

After the build, git status --short listed five files that nobody wrote: report.aux, report.fdb_latexmk, report.fls, report.log and the PDF, besides the gitignore.txt the session had copied in. Committed, they would change on every build and conflict on every merge. With a .gitignore that lists them, only the .gitignore itself was left to add, and git check-ignore -v named the line that matched each file:

gitignore.txt: save as .gitignore Text · drafts/gitignore.txt
# Save as .gitignore: files that LaTeX, BibTeX, latexmk and the editors write, never the sources.
*.aux
*.bbl
*.blg
*.fdb_latexmk
*.fls
*.log
*.out
*.synctex.gz
*.toc
# The PDFs are built from the sources; CI keeps them as artifacts.
*.pdf

Hard wrapping at 80 columns

Wrong: an editor that rewraps a paragraph at a fixed width, so that one changed word moves the line breaks of every line after it and the diff shows the whole paragraph. Right: one sentence per line, and let the editor wrap long lines on screen without changing the file.

Checkpoint

Check: One sentence per line in Git

  1. Question 1 of 2 Ravi changed the last sentence and Meera the first sentence of one paragraph, on two branches. When did git merge succeed by itself?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When each sentence was on a line of its own

    Git merges by lines, so the layout of the source decides what counts as the same place. One sentence per line turns two edits in one paragraph into edits on two lines.

  2. Question 2 of 2 What did *.tex diff=tex in .gitattributes change in the output of git diff -U0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Each hunk header named the section of the change, such as \section{Method}

    Without the attribute, Git took the nearest line that starts with a letter, a sentence of the text, as each hunk's context; with it, the section name.

latexdiff marks every change between two drafts

A supervisor who reads PDFs does not want a diff of markup. latexdiff, whose CTAN entry calls it a Perl script for visual mark up and revision of significant differences between two LaTeX files, writes a third .tex file in which every deletion and addition is marked; pdflatex then builds it like any other file. This session takes the last commit but one from Git, compares it with the current file, builds the result and checks the PDF:

latexdiff between the last two commits, built and checked Python · latexdiff_session.py
from session import files, needs, run

# Draft 1 and draft 2 of report.tex as two commits; latexdiff marks what changed between the last commit but one and
# the file as it is now, pdflatex builds the marked file, and changes.py lists the coloured text of its PDF.
# latexdiff-vc, which comes with latexdiff, does the same from the commit's name; its temporary file is named after
# the process, which the page shows as [process id].
needs("git", "pdflatex", "latexdiff", "mutool")
files("drafts/v1.tex=report.tex", "changes.py")
run("""
git init -q -b main . && git add report.tex && git commit -q -m 'Draft 1'
""")
files("drafts/v2.tex=v2.tex")
run("""
mv v2.tex report.tex && git commit -q -am 'Draft 2'
git show HEAD~1:report.tex > old.tex
latexdiff old.tex report.tex > diff.tex
sed -n '/^.begin{document}/,$p' diff.tex
pdflatex -interaction=nonstopmode -halt-on-error diff.tex > /dev/null
python3 changes.py diff.pdf
latexdiff-vc --git -r HEAD~1 report.tex
""", mask=[(r"oldtmp-\d+", "oldtmp-[process id]")], env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ git init -q -b main . && git add report.tex && git commit -q -m 'Draft 1'
$ mv v2.tex report.tex && git commit -q -am 'Draft 2'
$ git show HEAD~1:report.tex > old.tex
$ latexdiff old.tex report.tex > diff.tex
$ sed -n '/^.begin{document}/,$p' diff.tex
\begin{document}
\section{Method}
Two gauges logged the rain on the roofs of Block~A and Block~B for \DIFdelbegin \DIFdel{a week}\DIFdelend \DIFaddbegin \DIFadd{two weeks}\DIFaddend .
The gauges were read at 8 a.m. every day.
A reading that could not be taken was left blank \DIFaddbegin \DIFadd{and marked as missing}\DIFaddend .
\section{Result}
Block~A received \DIFdelbegin \DIFdel{41.5}\DIFdelend \DIFaddbegin \DIFadd{83.0}\DIFaddend \,mm and Block~B \DIFdelbegin \DIFdel{38.0}\DIFdelend \DIFaddbegin \DIFadd{76.5}\DIFaddend \,mm, a difference of \DIFdelbegin \DIFdel{3.5}\DIFdelend \DIFaddbegin \DIFadd{6.5}\DIFaddend \,mm\DIFaddbegin \DIFadd{.
The gauges agree to within their stated error}\DIFaddend .
\end{document}
$ pdflatex -interaction=nonstopmode -halt-on-error diff.tex > /dev/null
$ python3 changes.py diff.pdf
deleted  a week
added    two weeks
added    and marked as missing
deleted  41.5
added    83.0
deleted  38.0
added    76.5
deleted  3.5
added    6.5
added    . The gauges agree to within their stated error
$ latexdiff-vc --git -r HEAD~1 report.tex
Working on  report.tex 
patching file report.tex
Running: latexdiff   "report-oldtmp-[process id].tex" "report.tex" > "report-diffHEAD~1.tex"
Generated difference file report-diffHEAD~1.tex

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

In diff.tex, a week became \DIFdelbegin \DIFdel{a week}\DIFdelend and two weeks the matching \DIFadd. latexdiff compares words, not lines, so each changed number was marked alone, and the full stop of the old last sentence moved into the added text. Follow the session’s steps:

Interactive · stage by stage From two drafts to a PDF that shows every change Step through the recorded session and watch what each program reads and what it writes.
From two drafts to a PDF that shows every change, step 6 of 6. Showing old.tex (draft 1, from Git), latexdiff, diff.tex (\DIFdel, \DIFadd marks), pdflatex, diff.pdf, changes.py (4 deleted, 6 added). In focus: changes.py, fix.a change missingold.texdraft 1, from Gitlatexdiffold.tex and report.texdiff.tex\DIFdel, \DIFadd markspdflatexbuilds diff.texdiff.pdfblue addedchanges.py4 deleted, 6 added

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

Every step as text
  1. The old draft from Git. git show HEAD~1:report.tex prints the file as the last commit but one held it, and the session saves it as old.tex. Any commit, tag or branch name works the same way.
  2. latexdiff compares the words. latexdiff reads old.tex and the current report.tex and compares them word by word, not line by line, so a changed number inside a sentence is marked alone.
  3. A new .tex file with the markup. It writes diff.tex: the new text with each deletion wrapped in \DIFdel and each addition in \DIFadd, and a preamble that loads ulem and color to draw them.
  4. An ordinary build. pdflatex builds diff.tex like any other file. Your own report.tex and its PDF are not touched.
  5. Colours a co-author can read. In diff.pdf added words are blue with a wavy line under them and deleted words red and struck through, in the default style called UNDERLINE.
  6. Check the PDF itself. changes.py reads the colour of every character with MuPDF and lists the runs: deleted a week, added two weeks and eight more. A change that should be there and is not sends you back to latexdiff.

A PDF with marks should be checked like any other output. changes.py reads every character’s colour with MuPDF, and listed four deleted runs and six added ones, a week deleted and two weeks added first. latexdiff-vc, installed with latexdiff, does the Git step itself: latexdiff-vc --git -r HEAD~1 report.tex wrote report-diffHEAD~1.tex. For a project of several files, --flatten reads \input and \include files into the comparison.

changes.py: the coloured text of a latexdiff PDF Python · changes.py
"""Lists the marked text of a latexdiff PDF: blue text is what was added, red text what was deleted.

    python3 changes.py diff.pdf

MuPDF's structured text (mutool draw -F stext) gives every character of the PDF with its colour. Neighbouring
characters of the same colour become one run, and a black character ends a run. latexdiff's default style draws the
wavy line under an addition with characters too (each one a ":" in blue), so those are skipped.
"""

import subprocess
import sys
import xml.etree.ElementTree as ET

KINDS = {"#0000ff": "added", "#ff0000": "deleted"}

stext = subprocess.run(["mutool", "draw", "-q", "-F", "stext", "-o", "-", sys.argv[1]], capture_output=True,
                       check=True).stdout
runs = []
for line in ET.fromstring(stext).iter("line"):
    for char in line.iter("char"):
        kind, text = KINDS.get(char.get("color")), char.get("c")
        if kind == "added" and text == ":":
            continue
        if kind is None:
            if runs and runs[-1][0] is not None:
                runs.append((None, ""))
        elif runs and runs[-1][0] == kind:
            runs[-1] = (kind, runs[-1][1] + text)
        else:
            runs.append((kind, text))
    if runs and runs[-1][0] is not None:
        runs[-1] = (runs[-1][0], runs[-1][1] + " ")
for kind, text in runs:
    if kind and text.strip():
        print(f"{kind:8} {' '.join(text.split())}")

A marked-up PDF is a working copy

latexdiff’s default style draws its marks with the ulem package, and the LaTeX Project’s tagging status list marks ulem, like todonotes, as currently incompatible with tagged PDF. Send the marked PDF to co-authors, and the clean build to the journal or the thesis office.

Checkpoint

Check: Marked-up PDFs with latexdiff

  1. Question 1 of 2 Draft 2 changed 41.5 to 83.0 inside a sentence. How did latexdiff mark it in diff.tex?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: \DIFdel{41.5} followed by \DIFadd{83.0}, with the rest of the sentence unmarked

    Word-level marks are what makes a latexdiff PDF readable: a co-author sees the four changed numbers, not three rewritten sentences.

  2. Question 2 of 2 After latexdiff old.tex report.tex > diff.tex and a pdflatex run, which file holds the marked-up PDF?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: diff.pdf, built from diff.tex; report.tex and its PDF are unchanged

    The marked file is a working copy for co-authors; the clean build of report.tex is what you submit.

A Typst draft is compared through the text a reader sees

Typst has no latexdiff. Its source is plain text, so the Git diffs above work the same way, but a co-author who reads PDFs needs the change in the words of the page. This session builds the two drafts of the report in Typst, extracts their text with pdftotext and compares the two texts word by word with git diff --no-index, which compares two files outside any repository:

report.typ, two drafts

draft 1 · drafts/v1.typ

#set heading(numbering: "1")
= Method
Two gauges logged the rain on the roofs of Block A and Block B for a week.
The gauges were read at 8 a.m. every day.
A reading that could not be taken was left blank.

= Result
Block A received 41.5 mm and Block B 38.0 mm, a difference of 3.5 mm.

draft 2 · drafts/v2.typ

#set heading(numbering: "1")
= Method
Two gauges logged the rain on the roofs of Block A and Block B for two weeks.
The gauges were read at 8 a.m. every day.
A reading that could not be taken was left blank and marked as missing.

= Result
Block A received 83.0 mm and Block B 76.5 mm, a difference of 6.5 mm.
The gauges agree to within their stated error.
Two Typst drafts compared through their extracted text Python · typst_compare_session.py
from session import files, needs, run

# Typst has no latexdiff. The two drafts are compiled, pdftotext extracts what a reader sees, and git diff compares
# the two texts word by word (--no-index: two files, no repository).
needs("typst", "pdftotext", "git")
files("drafts/v1.typ", "drafts/v2.typ")
run("""
typst compile v1.typ && typst compile v2.typ
pdftotext v1.pdf v1.txt && pdftotext v2.pdf v2.txt
!git diff --no-index --word-diff -U0 v1.txt v2.txt
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ typst compile v1.typ && typst compile v2.typ
$ pdftotext v1.pdf v1.txt && pdftotext v2.pdf v2.txt
$ git diff --no-index --word-diff -U0 v1.txt v2.txt
diff --git a/v1.txt b/v2.txt
index aab0e2e..9a22d1b 100644
--- a/v1.txt
+++ b/v2.txt
@@ -2,2 +2,2 @@
Two gauges logged the rain on the roofs of Block A and Block B for [-a week.-]{+two weeks.+} The gauges were
read at 8 a.m. every day. A reading that could not be taken was left [-blank.-]{+blank and marked as missing.+}
@@ -6 +6,2 @@ Two gauges logged the rain on the roofs of Block A and Block B for a week. The g
Block A received [-41.5-]{+83.0+} mm and Block B [-38.0-]{+76.5+} mm, a difference of [-3.5-]{+6.5+} mm. {+The gauges agree to within+}
{+their stated error.+}
[exit status 1]

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

The comparison found the same six changes as latexdiff, [-a week.-]{+two weeks.+} first, and ended with exit status 1, which git diff --no-index uses when the files differ. The extracted text follows the PDF’s line breaks, not the source’s, so one changed sentence can show as two lines, as the last addition did. MySmartCoPilot’s Text Diff shows the same comparison side by side, and its PDF Compare tool also marks the areas of each page that look different:

Compare PDF Files (Text & Visual) Load the two Typst PDFs to see every changed word and every page area that differs.

Checkpoint

Check: Comparing Typst drafts

  1. One question Why did the session compare the Typst drafts with pdftotext and git diff --no-index --word-diff?

    Choose one answer.

    Show the answer: One question

    Answer: Typst has no latexdiff, and the extracted text shows the changes in the words a reader sees

    For a co-author who reads PDFs, a word diff of the extracted text is the closest thing Typst has to latexdiff, and MySmartCoPilot's PDF Compare adds a view of the pages.

A co-author’s tracked changes come back through pandoc

Some co-authors work only in a word processor, with tracked change: An insertion or a deletion that a word processor records with its author instead of making it silently, so that others can see it and accept or reject it.. A .docx file is a ZIP package of XML parts, so Git can store it but not show what changed. ECMA-376, the Office Open XML standard, stores each tracked change in the document’s XML: inserted text in a w:ins element, deleted text in a w:del element, each with its author. This session writes the report as a Word file, lets coauthor_edits.py make three tracked changes as an invented co-author would, and reads the file back with pandoc:

Word tracked changes, read back by pandoc Python · word_session.py
from session import files, needs, run

# report.tex goes to a co-author as a Word file. coauthor_edits.py plays the co-author, who makes three tracked
# changes; pandoc then reads them back, first with every change shown, then with the changes accepted (its default).
needs("pandoc", "python3")
files("drafts/v2.tex=report.tex", "coauthor_edits.py")
run("""
pandoc report.tex -o report.docx
python3 coauthor_edits.py report.docx report-ravi.docx
pandoc report-ravi.docx --track-changes=all -t markdown --wrap=none
pandoc report-ravi.docx -t markdown --wrap=none
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ pandoc report.tex -o report.docx
$ python3 coauthor_edits.py report.docx report-ravi.docx
report-ravi.docx: 3 tracked changes by Ravi Kumar
$ pandoc report-ravi.docx --track-changes=all -t markdown --wrap=none
# Method

Two gauges logged the rain on the roofs of Block A and Block B for [two weeks]{.deletion author="Ravi Kumar"}[14 days]{.insertion author="Ravi Kumar"}. The gauges were read at 8 a.m. every day. A reading that could not be taken was left blank and marked as missing. [Missing readings were not estimated.]{.insertion author="Ravi Kumar"}

# Result

Block A received 83.0 mm and Block B 76.5 mm, a difference of 6.5 mm. [The gauges agree to within their stated error.]{.deletion author="Ravi Kumar"}
$ pandoc report-ravi.docx -t markdown --wrap=none
# Method

Two gauges logged the rain on the roofs of Block A and Block B for 14 days. The gauges were read at 8 a.m. every day. A reading that could not be taken was left blank and marked as missing. Missing readings were not estimated.

# Result

Block A received 83.0 mm and Block B 76.5 mm, a difference of 6.5 mm.

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

Pandoc’s manual describes --track-changes with three values. all kept every change as a span with its kind and author, [two weeks]{.deletion author="Ravi Kumar"}[14 days]{.insertion author="Ravi Kumar"}, a list to work through; the default, accept, printed the text with the three changes made; reject gives the text without them. Step through the round trip:

Interactive · message by message A co-author's tracked changes, back into the LaTeX source Step through the round trip and see where the changes are visible, where they are applied, and who applies them.
A co-author's tracked changes, back into the LaTeX source, step 6 of 6. Messages so far: You to pandoc 3.12: report.tex; pandoc 3.12 to Ravi: report.docx; Ravi to You: report-ravi.docx; You to pandoc 3.12: --track-changes=all; pandoc 3.12 to You: accepted text.Youreport.tex, commit 3pandoc 3.122 insertions, 2 deletionsRavi3 tracked changesreport.texreport.docxreport-ravi.docx--track-changes=allaccepted text

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

Every step as text
  1. A Word file for Ravi. pandoc report.tex -o report.docx turns the sections into Heading 1 paragraphs and the sentences into body text. The .tex file stays the primary copy.
  2. Ravi edits with tracking on. Ravi changes two weeks to 14 days, adds a sentence and deletes another. Each change is stored in the file with his name, as w:del and w:ins elements.
  3. The file comes back. Ravi returns report-ravi.docx. Opened in Word it shows his three changes; to Git it is a ZIP file, so a diff of it says nothing useful.
  4. Every change, with its author. pandoc --track-changes=all writes each change as a span with its kind and author, such as [14 days]{.insertion author="Ravi Kumar"}, a list you can work through.
  5. The changes accepted. Without the option, pandoc accepts every change, its default, and prints the text as Ravi left it. With reject it prints the text as it was.
  6. You apply them to the source. You make the changes you agree with in report.tex and commit them. The LaTeX markup, such as \,mm and the labels, never went through Word, so nothing of it is lost.

The .tex file stays 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.: you make the changes you agree with in report.tex and commit them, so the LaTeX markup that never went through Word, such as \,mm and the labels, is never lost.

Text Diff Paste the accepted text and your report's text to see Ravi's changes word by word.

Key idea

Every comparison works on the text it can read: Git on source lines, latexdiff on LaTeX words, pdftotext on the words of a page, and pandoc on the changes stored inside a Word file.

Checkpoint

Check: Tracked changes from Word

  1. Question 1 of 2 Which pandoc option listed each of Ravi's tracked changes with its kind and author?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: --track-changes=all

    all keeps the changes visible as a list to work through; accept and reject give the two clean versions.

  2. Question 2 of 2 Ravi returns report-ravi.docx with three tracked changes. What keeps your LaTeX labels and \,mm spacing safe?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Applying the changes you accept to report.tex by hand, so the .tex file stays the primary copy

    pandoc --track-changes=all gives the list of changes; the decisions are made in the source, where they are also committed.

Notes for co-authors stay in the source, out of the final PDF

A question for a co-author is easiest to answer where it belongs, in the text. todonotes, which its CTAN entry says lets you mark things to do later, draws a note in the margin or, with inline, in the text. In Typst a short function does the same, and sys.inputs, which reads values given with --input on the command line, turns the notes off for the final build:

The same two notes in LaTeX and in Typst

notes/notes.tex

% notes.tex: notes for co-authors with todonotes. A build that passes the option disable to todonotes leaves them out.
\documentclass{article}
\usepackage{todonotes}
\begin{document}
\section{Method}
Two gauges logged the rain for two weeks.\todo{Ravi: add the gauge model.}
They were read at 8 a.m. every day.
\todo[inline]{Meera: say who read them on Sundays.}
\end{document}

notes/notes.typ

// notes.typ: notes for co-authors. typst compile notes.typ shows them;
// typst compile --input final=yes notes.typ leaves them out.
#let final = sys.inputs.at("final", default: "no") == "yes"
#let note(who, body) = if not final {
  box(fill: yellow.lighten(60%), inset: 2pt)[*#who:* #body]
}

#set heading(numbering: "1")
= Method
Two gauges logged the rain for two weeks. #note("Ravi")[Add the gauge model.]
They were read at 8 a.m. every day.
#note("Meera")[Say who read them on Sundays.]
Working builds with notes, final builds without, counted Python · notes_session.py
from session import files, needs, run

# The same notes in both systems: a working build that shows them, and a final build that leaves them out, checked
# by counting the co-authors' names in the text pdftotext extracts.
needs("pdflatex", "typst", "pdftotext")
files("notes/notes.tex", "notes/notes.typ")
run(r"""
pdflatex -interaction=nonstopmode -halt-on-error notes.tex > /dev/null
pdftotext notes.pdf - | grep -c -E 'Ravi|Meera'
pdflatex -interaction=nonstopmode -halt-on-error -jobname=notes-final '\PassOptionsToPackage{disable}{todonotes}\input{notes}' > /dev/null
!pdftotext notes-final.pdf - | grep -c -E 'Ravi|Meera'
typst compile notes.typ
pdftotext notes.pdf - | grep -c -E 'Ravi|Meera'
typst compile --input final=yes notes.typ notes-final.pdf
!pdftotext notes-final.pdf - | grep -c -E 'Ravi|Meera'
""", env={"SOURCE_DATE_EPOCH": "1767225600"})

Output

$ pdflatex -interaction=nonstopmode -halt-on-error notes.tex > /dev/null
$ pdftotext notes.pdf - | grep -c -E 'Ravi|Meera'
2
$ pdflatex -interaction=nonstopmode -halt-on-error -jobname=notes-final '\PassOptionsToPackage{disable}{todonotes}\input{notes}' > /dev/null
$ pdftotext notes-final.pdf - | grep -c -E 'Ravi|Meera'
0
[exit status 1]
$ typst compile notes.typ
$ pdftotext notes.pdf - | grep -c -E 'Ravi|Meera'
2
$ typst compile --input final=yes notes.typ notes-final.pdf
$ pdftotext notes-final.pdf - | grep -c -E 'Ravi|Meera'
0
[exit status 1]

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

grep -c counted the co-authors’ names in the extracted text. The working builds held 2; the final builds, pdflatex with \PassOptionsToPackage{disable}{todonotes} given before the file and Typst with --input final=yes, held 0, and grep ended with exit status 1 because it found nothing. Neither final build needed a change to the source, so a note that nobody remembered to delete still stays out of the final PDF, and the same grep can fail a CI job when it finds one.

Name the person in the note

Start every note with the name of the person who should answer it, as both files do. A search for Ravi: then finds every open question for Ravi, in the source and in the working PDF.

Checkpoint

Check: Notes out of the final PDF

  1. One question How did the final LaTeX build leave out the todonotes without a change to notes.tex?

    Choose one answer.

    Show the answer: One question

    Answer: pdflatex was given \PassOptionsToPackage{disable}{todonotes} before \input{notes}

    Passing the option on the command line, or --input final=yes in Typst, means a forgotten note cannot reach the final PDF.

The online editor’s plan decides history, tracked changes and Git

Some groups write in the browser. Overleaf edits LaTeX and the Typst web app edits Typst, and what each offers depends on the plan. Overleaf’s plans page lists these differences:

Feature Without a subscription With a subscription
Co-authors 1 collaborator per project 10 per project, or unlimited
History The previous 24 hours only Complete, every version restorable
Tracked changes Not included Included
Git and other tools No integrations Git, GitHub, Dropbox, Zotero, Mendeley and ReadCube

The Typst web app’s plans page lists these:

Feature Without a subscription With a subscription
Co-authors Share projects and work on them together The same, and invitations by email
Comments Not listed Threaded comments, marked resolved
Git Not listed Experimental sync with GitHub or GitLab
Storage 200 MB, 100 files per project 2 GB, 1,000 files per project

So without a subscription, an Overleaf project has one collaborator and a day of history, and no tracked changes; the Typst app lets a group edit together but lists comments and Git sync only with its subscription.

Companies and projects named here

MySmartCoPilot is independent of Overleaf, Typst, the Git project and Ecma International. The plan features and formats described here are quoted from each one’s own pages and standards, cited at the end of the lesson.

A Git repository with the latexdiff and pdftotext comparisons of this lesson keeps full history and gives marked PDFs whatever editor each co-author uses, so the plans decide only what the browser itself can do.

Choose by the feature, then check the page

Write down the two or three features your group cannot do without, such as history older than a day or Git, then read the plans page itself before you choose: plans change, and the page is the only source that is always current.

Checkpoint

Check: Choosing an online editor

  1. Question 1 of 2 Without a subscription, how much document history does Overleaf's plans page list?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The previous 24 hours only

    A day of history is enough to undo a slip, not to see what changed since last month's draft; Git history has no such limit.

  2. Question 2 of 2 A group writes in the Typst web app and wants its project in a GitHub repository. What do the plans pages say?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Git sync with GitHub or GitLab is listed with the subscription, as experimental

    Choose by the feature the group needs, then read the current plans page, because plans change.

Practise: one sentence per line

Breaking a paragraph into sentences is the one step of this lesson that you would otherwise do by hand. Write it once, against sample tests made from this lesson’s report; the first hint is open:

Exercise · Medium · Python

Put one sentence on each line of a paragraph

Git compares and merges files line by line, so a paragraph kept as one sentence per line shows exactly which sentences changed. Write one_per_line(paragraph) in sentences.py. Given a paragraph of LaTeX or Typst text on one line, it returns the same text with a line break after each sentence:

  • a sentence ends at ., ? or ! that is followed by a space and then an upper-case letter, a digit or a backslash (the start of a command such as \textbf);
  • the space after the end of a sentence becomes one line break, and nothing else in the text changes;
  • these words end with a full stop but do not end a sentence: e.g., i.e., cf., vs., al. (as in et al.), Fig., Eq., Sec., Dr. and Prof., and so does a single capital letter with a full stop, an initial such as the A. of A. Rao;
  • a full stop inside a number, as in 41.5, is never followed by a space, so it ends nothing.

Spaces at the start and the end of the paragraph are removed.

Starter code · sentences.py

def one_per_line(paragraph):
    """Return the paragraph with each sentence on a line of its own."""
    # Replace this line: find each sentence end and put a line break in place of the space after it.
    return paragraph.strip()
The sample tests · test_sentences.py
from sentences import one_per_line


def test_three_sentences():
    """breaks a paragraph of three sentences into three lines"""
    text = "Two gauges logged the rain for a week. They were read at 8 a.m. every day. A reading that could not be taken was left blank."
    assert one_per_line(text) == (
        "Two gauges logged the rain for a week.\n"
        "They were read at 8 a.m. every day.\n"
        "A reading that could not be taken was left blank."
    )


def test_abbreviations():
    """keeps e.g. and Fig. inside their sentences"""
    text = "Two gauges, e.g. the ones on Block A, logged the rain. Fig. 2 shows them."
    assert one_per_line(text) == "Two gauges, e.g. the ones on Block A, logged the rain.\nFig. 2 shows them."


def test_initials_and_et_al():
    """keeps initials and et al. inside their sentences"""
    text = "The method of A. Rao and D. Kulkarni was used. Rao et al. tested it on 12 roofs. It worked."
    assert one_per_line(text) == (
        "The method of A. Rao and D. Kulkarni was used.\n"
        "Rao et al. tested it on 12 roofs.\n"
        "It worked."
    )


def test_questions_numbers_and_commands():
    """ends sentences at ? and !, before a digit or a command, and never inside a number"""
    text = "Block A received 41.5 mm. Why less in Block B? 38.0 mm is low! \\textbf{Block B} was shaded."
    assert one_per_line(text) == (
        "Block A received 41.5 mm.\n"
        "Why less in Block B?\n"
        "38.0 mm is low!\n"
        "\\textbf{Block B} was shaded."
    )


def test_lower_case_after_full_stop():
    """does not break before a word in lower case"""
    assert one_per_line("They were read at 8 a.m. every day.") == "They were read at 8 a.m. every day."


def test_one_sentence_and_spaces():
    """returns a single sentence unchanged, without its outer spaces"""
    assert one_per_line("  The totals are in Table~\\ref{tab:totals}.  ") == "The totals are in Table~\\ref{tab:totals}."
A hint

Walk through the text with re.finditer(r"[.?!] +(?=[A-Z0-9\\])", text): each match is a possible sentence end. Look at the word just before the match (the characters after the last space before it) and skip the match when that word, with its full stop, is one of the abbreviations or a single capital letter.

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, a short self-check on the six ideas of the lesson. Questions on what you know least come first, and every choice you pick is explained on the spot:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 14 With one sentence per line, Ravi changed sentence 2 and Meera sentence 1. What did git merge do?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: It stopped with a conflict, but the markers held only the two changed sentences

    One sentence per line does not end conflicts, but it makes them small: two sentences to read instead of a paragraph.

  2. Question 2 of 14 Which of these files belongs in the repository of a LaTeX report?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: report.tex

    The session's .gitignore listed .aux, .bbl, .blg, .fdb_latexmk, .fls, .log, .out, .synctex.gz, .toc and the PDFs, and git check-ignore -v named the line that matched each.

  3. Question 3 of 14 An editor rewraps every paragraph at 80 columns when you save. What does the diff of a one-word change look like?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: Every line after the change may show as changed, because the line breaks move

    One sentence per line keeps each change on its own line; let the editor wrap long lines on screen only.

  4. Question 4 of 14 How many deleted runs did changes.py find in the latexdiff PDF of the two drafts?

    Type a number.

    Show the answer: Question 4 of 14

    Answer: 4 runs

    a week, 41.5, 38.0 and 3.5 were red and struck through; the six added runs were blue with a wavy line.

  5. Question 5 of 14 Your supervisor wants the changes since the draft you tagged v1. Which commands give the marked PDF?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: git show v1:report.tex > old.tex, then latexdiff old.tex report.tex > diff.tex, then pdflatex diff.tex

    latexdiff needs both versions as files; Git gives you the old one, or latexdiff-vc fetches it for you.

  6. Question 6 of 14 Should the latexdiff PDF go to the journal as the final version?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: No; it is a working copy with marks, and ulem, which draws them, is marked currently incompatible with tagged PDF

    A marked PDF serves one purpose, showing changes to people; everything else gets the clean build.

  7. Question 7 of 14 What does --no-index let git diff do?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: Compare two files that are not in a repository

    git diff --no-index --word-diff is a word diff of any two text files, which is what two extracted PDF texts are.

  8. Question 8 of 14 The comparison of the two Typst drafts ended with exit status 1. What did that mean?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: The two texts differed

    Exit status 0 means the same text, 1 means a difference; a CI job can use that to tell whether a PDF changed.

  9. Question 9 of 14 Where does a .docx file keep a tracked insertion?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: In word/document.xml, as a w:ins element with its author

    Because the changes are in the XML, pandoc can read them back, with --track-changes choosing what to do with them.

  10. Question 10 of 14 What does pandoc report-ravi.docx --track-changes=reject -t markdown print for the first sentence?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: ... for two weeks.

    accept, reject and all give the three views of one file: the co-author's text, yours, and the list of changes.

  11. Question 11 of 14 In notes.typ, what decides whether a note is drawn?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: sys.inputs, which holds final = yes only when the command gives --input final=yes

    Values given with --input reach a document as sys.inputs, so one source gives a working and a final build.

  12. Question 12 of 14 How can a CI job make sure no note for a co-author reaches the final PDF?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Build the final version with notes turned off, then fail if the extracted text still contains a name such as Ravi

    Check the output, not only the setting: the final PDF's text is what a reader would see.

  13. Question 13 of 14 What does Overleaf's plans page list as the collaborators per project without a subscription?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: 1

    Count the people who must edit the same project at the same time before you choose a plan.

  14. Question 14 of 14 Three co-authors want tracked changes and history older than a day, and nobody has a subscription. What setup gives them both?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: A Git repository, with latexdiff PDFs between commits for the marked changes

    The features a group needs can come from the editor's plan or from the tools around the files; this lesson's tools need no plan at all.

Interview questions

Warm-up (fresher to 2–5 years): Why keep one sentence per line in a LaTeX or Typst file that lives in Git?

Because Git diffs and merges by lines. With one sentence per line, a diff shows which sentences changed, and two co-authors who change different sentences of a paragraph get a clean merge. With a whole paragraph on one line, any change marks the paragraph as changed, and two changes anywhere in it conflict. In this lesson’s session, the same two edits to the first and last sentence of a paragraph conflicted when the paragraph was one line and merged by themselves with one sentence per line; edits to neighbouring sentences still conflicted, but only over those two lines. Neither LaTeX nor Typst cares where a line ends inside a paragraph, so the PDF stays the same, and the editor can still wrap long lines on screen.

Two harder questions come from this lesson: a thesis committee that wants the changes between two versions, and a group whose members write in three different tools. Plan your opening move for each before you read its first step:

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

  1. Fresher to mid

    Your thesis committee wants a PDF that marks every change between the version they read at the last meeting and the revised one, a 140-page LaTeX thesis in twelve chapter files. How do you produce it, check it, and keep it apart from the PDF you submit?

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

  2. Mid to senior

    Four authors write one paper: two in LaTeX in Git, one in an online LaTeX editor, and one who only uses a word processor with tracked changes. Set up the way you work so that no change is lost, every change can be traced to its author, and the submitted PDF has no notes in it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Git diffs and merges by lines, so one sentence per line turned a paragraph-wide conflict into a clean merge, and *.tex diff=tex made each hunk name its section.
  • Commit the sources and a .gitignore, never the .aux, .log, .bbl or PDF files that every build writes again.
  • latexdiff writes a .tex file with every change marked, which pdflatex builds into a PDF a co-author can read; check the PDF’s marks, as changes.py did by colour.
  • Typst drafts are compared through their extracted text, word by word, with git diff --no-index --word-diff.
  • pandoc’s --track-changes=all lists a Word co-author’s changes with their authors, and the .tex file stays the primary copy in which you apply them.
  • Notes for co-authors stay out of the final PDF when the final build turns them off, todonotes with disable and Typst through sys.inputs, and the plans pages say which online editor gives history, tracked changes and Git.

Flashcards · 8 cards

Recall it, then check

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

  1. Why keep one sentence per line in a source file under Git?

    Git diffs and merges by lines, so co-authors' edits to different sentences merge by themselves, while the same edits to a paragraph kept on one line conflicted.

  2. How do you make Git's hunk headers name the LaTeX section?

    Add *.tex diff=tex to .gitattributes, one of Git's built-in patterns; the header then showed \section{Method}.

  3. Which files belong in a LaTeX project's .gitignore?

    Everything a build writes again: .aux, .log, .fls, .fdb_latexmk, .bbl and the PDF. git check-ignore -v names the line that matched each file.

  4. What does latexdiff produce?

    A third .tex file in which every deletion and addition between two drafts is marked word by word, which pdflatex builds into a PDF; latexdiff-vc --git -r HEAD~1 takes the old draft from Git.

  5. How do you compare two Typst drafts the way a reader sees them?

    Extract the text of both PDFs with pdftotext and compare it word by word with git diff --no-index --word-diff.

  6. How does pandoc read a Word co-author's tracked changes?

    --track-changes=all keeps each insertion and deletion as a span with its author; accept, the default, applies them, and reject drops them.

  7. Why does the .tex file stay the primary copy when a co-author edits in Word?

    You apply the changes you agree with in report.tex, so the LaTeX markup that never went through Word, such as \,mm and the labels, is never lost.

  8. How do you keep notes for co-authors out of the final PDF without editing the source?

    Pass todonotes the disable option from the command line, with \PassOptionsToPackage{disable}{todonotes}; in Typst, read --input final=yes from sys.inputs.

Terms in this lesson

hunk
One block of changed lines in a diff, introduced by a header line that starts with @@ and gives where the block starts in the old and in the new file.
merge conflict
What Git reports when two branches changed the same lines of a file, or lines next to each other: it stops and writes both versions between conflict markers for a person to resolve.
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.
tracked change
An insertion or a deletion that a word processor records with its author instead of making it silently, so that others can see it and accept or reject it.

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.