Typtex!

Created: August 29, 2025
Last updated: September 3, 2025
Author: Jeremy Gao

Messing typst with macros.

Typst vs Latex

Both Typst and Latex are powerful typesetting tools. Typst, the newborn, uses a rust-like[1] syntax and rules on variable declaration, while latex, the powerful, uses a macro based system that looks like this:

% dogs go woof
\documentclass{doggies}
\usepackage{dogs_are_good}
\title{it is a dog!}
\a \b \c \something \thatthing

This, of course, is not real Latex code. Anyways, backslashes are usually used as the escape character in modern systems. Typst also use the backslash as an escape character, but only for entering special characters. For code and function calls, Typst uses “#”, the pound.

#function-call(arg1: 114, arg2: 514)
// my favourite space character - the ideographic
// space character U+3000
#other-function-call(str: "\u{3000}")
// enters code mode
#{
    1 + 1
}

I’ve never used latex before, neither do I know the stuff happening under the hood. Nevertheless, I do know something about general text macros. Text macros only replace text. While they remain a good choice to reduce boilerplate, it is impossible to ensure the contents generated by text macros can be parsed into valid ASTs. On the other hand, this also means they are extremely general, and can serve for almost any purposes, it’s just that it becomes the user’s obligation to validate the output, not the macro processor’s.

Macro Processors

A lot of programming language have macros. C has macros, C++ has macros, Rust has macros, Nim has macros, Latex has macros, and the entire language is built using \defines. Sadly, Typst doesn’t have builtin macros, and the authors determined that functions can already cover all scenarios where we do need macros.

…And they are quite right. A lot of times macros act as barriers for analyzers. Expanding them means compiling some code ahead of time, especially those recursive ones.

Though typst doesn’t have builtin macros, we can still use external macros in typst. The first thing to note is Typst has a very strict lifetime rule, much like that of rust’s. Once a variable is out of its lifetime, or functioning zone [2]. Even when using macros we still have to follow Typst’s rules. Next, we would need to use a macro processor. Something like GNU M4 would work very well, the standard C/C++ preprocessor is also good. Some might remember jinja2 from python, but that’s an overkill for our example.

GPP

Introducing GPP, the generic preprocessor. It’s modern, its syntax is even simpler than M4’s, while having the same flexibility. It’s standalone, so there’s no compiler overhead that comes with the preprocessor like when you decide to install gcc.

One major advantage of GPP is its customizable syntax. By default, it has 6 modes, simulating the syntax seen in five different languages (two are for C/C++). The one we’re using is, of course, the TeX mode.

Defining and calling something under this mode is easy;

% define a macro
\define{foo}{bar}

% this line evaluates to `bar`
\foo

% execute a command and reading from stdout
\exec{python3 -c 'print("Do you wanna build a snowman?")'}

% output the current line
\line

So it’s possible to modify typst as follows:[3]

\define{cmd}{##1}
\define{page}{page(#1)}
\define{text}{text(#1)}
\define{set}{set #1}
\cmd{\set{\page{width: auto, height: auto}}}
\cmd{\set{\text{fill: red}}}
lorem ipsum

Typst doesn’t support calling external commands during compilation, nor getting the current time down to the second. Those could be bypassed using the exec macro - this just calls an external command and inserts the results back to the output stream

// now we have nanoseconds - even better!
\exec{date --iso-8601=ns}
\exec{python3 -c 'import itertools; print(list(itertools.combinations("马冬梅", 2)))'}

Doing Everything in One Run

gpp outputs everything to stdout, the standard output. Since typst supporting compiling from the standard input stdin, We can redirect the text stream to the typst compiler’s standard input using |, the pipe operator, in the command line.

gpp -T -x input.typ | typst c - output.pdf

…and that should be it.


  1. I have to be honest with you: I haven’t written that much rust code yet. Don’t take my words seriously as if they are from a rust professionals. ↩︎

  2. I read too many rust tutorials in Chinese and I completely forgot the correct English name. ↩︎

  3. Outputs:

    #set page(width: auto, height: auto)
    #set text(fill: red)
    lorem ipsum
    2025-08-29T23:40:11,634448127-07:00
    [('马', '冬'), ('马', '梅'), ('冬', '梅')]
    

    The execution result of the date command depends on the current date. ↩︎