Typst Examples Book

This book provides an extended tutorial and lots of Typst snippets that can help you to write better Typst code.

This is an unofficial book. Some snippets & suggestions here may be outdated or useless (please let me know if you find some).

However, all of them should compile on last version of Typst1.

CAUTION: currently the book is a WIP, so don't rely on it.

If you like it, consider giving a star on GitHub!

This will greatly help me to stay motivated and continue working on this book!

The book consists of several chapters, each with its own goal:

  1. Typst Basics
  2. Typst Snippets
  3. Typst Packages
  4. Typstonomicon

Contributions

Any contributions are very welcome! If you have a good code snippet that you want to share, feel free to submit an issue with snippet or make a PR in the repository.

I will especially appreciate submissions of active community members and compiler contributors! However, I will also very appreciate feedback from beginners to make the book as comprehensible as possible!

Acknowledgements

Thanks to everyone in the community who published their code snippets!

If someone doesn't like their code and/or name being published, please contact me.

1

When a new version launches, it may take a few days to update the book, but not much more.

Typst Basics

This is a chapter that consistently introduces you to the most things you need to know when writing with Typst.

It show much more things than official tutorial, so maybe it will be interesting to read for some of the experienced users too.

Some examples are taken from Official Tutorial and Official Reference. Most are created and edited specially for this book.

Important: in most cases there will be used "clipped" examples of your rendered documents (no margins, smaller width and so on).

To set up the spacing as you want, see corresponding example section WIP and Official Page Setup Guide.

Tutorial by Examples

The first section of Typst Basics is very similar to Official Tutorial, with more specialized examples and less words. It is highly recommended to read the official tutorial anyway.

Markup language

Starting

Starting typing in Typst is easy.
No including packages or other weird things.

Blank line will move text to new paragraph.

Btw, you can use any language and unicode symbols
with no problems by default: ßçœ̃ɛ̃ø∀αβёыა😆…

(as long as current font supports it)
Rendered image

Markup

= Markup

This was a heading. Number of `=` in front of name corresponds to heading level.

===== Fifth-level heading

Okay, let's move to _emphasis_ and *bold* text.
Markup syntax is generally similar to
`AsciiDoc` (this was `raw` for monospace text!)
Rendered image

New lines & Escaping

You can break \
line anywhere you \
want using \\ symbol.

Also you can use that symbol to
escape \_all the symbols you want\_,
if you don't want it to be interpreted as markup
or other special symbols.
Rendered image

Comments & codeblocks

You can write comments with `//` and `/* comment */`:
// Like this
/* Or even like
this */

```typ
Just in case you didn't read source,
this is how it is written:

// Like this
/* Or even like
this */

By the way, I'm writing it all in a _fenced code block_ with *syntax highlighting*!
```
Rendered image

Smart quotes

== What else?

There are not much things in basic "markup" syntax,
but we will see much more interesting things very soon!
I hope you noticed auto-matched "smart quotes" there.
Rendered image

Lists

- Writing lists in a simple way is great.
- Nothing complex, start your points with `-`
  and this will become a list.
  - Indented lists are created via indentation.

+ Numbered lists start with `+` instead of `-`.
+ There is no alternative markup syntax for lists
+ So just remember `-` and `+`, all other symbols
  wouldn't work in an unintended way.
  + That is a general property of Typst's markup.
  + Unlike Markdown, there is only one way
    to write something with it.
Rendered image

Notice:

Typst doesn't support markdown-like syntax for lists:
1. Apple
1. Pair
1. Soup
Rendered image

Math

I will just mention math ($a + b/c = sum_i x^i$) 
is possible there and is quite pretty:

$
7.32 beta +
  sum_(i=0)^nabla
    (Q_i (a_i - epsilon)) / 2
$

To learn more about math, see corresponding chapter.
Rendered image

Functions

Functions

Okay, let's now move to more complex things.

First of all, there are *lots of magic* in Typst.
And it major part of it is called "scripting".

To go to scripting mode, type `#` and *some function name*
after that. We will start with _something dull_:

#lorem(50)

_That *function* just generated 50 "Lorem Ipsum" words!_
Rendered image

More functions

#underline[functions can do everything!]

#text(orange)[L]ike #text(size: 0.8em)[Really] #sub[E]verything!

#figure(
  caption: [
    This is a screenshot from one of first theses written in Typst. \
    _All these things are written with #text(blue)[custom functions] too._
  ],
  image("../boxes.png", width: 80%)
)

In fact, you can #strong[forget] about markup
and #emph[just write] functions everywhere!

#list[
  All that markup is just a #emph[syntax sugar] over functions!
]
Rendered image

How to call functions

First, start with `#`. Then write a name.
Finally, write some parentheses and maybe something inside.

You can navigate lots of built-in functions 
in #link("https://typst.app/docs/reference/")[Official Reference].

#quote(block: true, attribution: "Typst Examples Book")[
  That's right, links, quotes and lots of
  other document elements are created with functions.
]
Rendered image

Function arguments

There are _two types_ of function arguments:

+ *Positional.* Like `50` in `lorem(50)`.
  Just write them in parentheses and it will be okay.
  If you have many, use commas.
+ *Named.* Like in `#quote(attribution: "Whoever")`.
  Write the value after a name and a colon.

If argument is named, it has some _default value_.
To find out what it is, see
#link("https://typst.app/docs/reference/")[Official Typst Reference].
Rendered image

Content

The most "universal" type in Typst language is *content*.
Everything you write in the document becomes content.

#[
  But you can explicitly create it with
  _scripting mode_ and *square brackets*.

  There, in square brackets, you can use any markup
  functions or whatever you want.
]
Rendered image

Markup and code modes

When you use `#`, you are "switching" to code mode.
When you use `[]`, you turn back:

// +-- going from markup (the default mode) to scripting for that function
// |                 +-- scripting mode: calling `text`, the last argument is markup
// |     first arg   |
// v     vvvvvvvvv   vvvv
   #rect(width: 5cm, text(red)[hello *world*])
//  ^^^^                       ^^^^^^^^^^^^^ just a markup argument for `text`
//  |
//  +-- calling `rect` in scripting mode, with two arguments: width and other content
Rendered image

Passing content into functions

So why do we need square braces?

The main answer is that if you *write content right after
function, it will be passed as positional argument there*.

#quote(block: true)[
  So #text(red)[_that_] allows me to write
  _literally anything in things
  I pass to #underline[functions]!_
]
Rendered image

Passing content, part II

So, just to make it clear, when I write

```typ
- #text(red)[red text]
- #text([red text], red)
- #text("red text", red) 
  Quotes there mean a plain string, not a content!
  You can't use markup inside.
```

It all will result in a #text([red text], red).
Rendered image

Basic styling

Let's start with justifying

#set page(width: 15cm, margin: (left: 4cm, right: 4cm))

Okay, that was great, but using functions everywhere,
specifying all arguments every time is awfully cumbersome.

That's way Typst has _rules_. No, not for you, for the document.

#set par(justify: true)

And the first rule we will consider there is `set` rule.
As you see, I've just used it on `par` (which is short from paragraph)
and now all paragraphs became _justified_.

It will apply to all paragraphs after the rule,
but will work only in it's _scope_ (we will discuss them later).

#par(justify: false)[
  Of course, you can override a `set` rule.
  This rule just sets the _default value_
  of an argument of an element.
]

And in start of this document (snippet) 
I've reduced page size to make justifying more visible,
and also increased margins to add blank space on edges. 
Rendered image

A bit about length units

There are several absolute length units in Typst:

#set rect(height: 1em)

#table(
  columns: 2,
  [Points], rect(width: 72pt),
  [Millimeters], rect(width: 25.4mm),
  [Centimeters], rect(width: 2.54cm),
  [Inches], rect(width: 1in),
  [Relative to font size], rect(width: 6.5em)
)

`1 em` = current font size. \
It is a very convenient unit,
so we are going to use it a lot
Rendered image

Setting something else

Of course, you can use set rule with all built-in functions and all their named arguments to make something "default".

For example, let's make all quotes there authored by that book:

#set quote(block: true, attribution: [Typst Examples Book])

#quote[
  Typst is great!
]

#quote[
  The problem with quotes on the internet is
  that it is hard to verify their authenticity.
]
Rendered image

Opinionated defaults

That allows you to set the defaults for the document in general as you want:

#set par(justify: true)
#set list(indent: 1em)
#set enum(indent: 1em)
#set page(numbering: "1")

- List item
- List item

+ Enum item
+ Enum item
Rendered image

Don't complain about bad defaults! Set your own.

Numbering

= Numbering
Some of elements have a property called "numbering".
They accept so-called "numbering patterns" and
are very useful with set rules. Let's see what I mean.

#set heading(numbering: "I.1:")

= This is first level
= Another first
== Second
== Another second
=== Now third
== And second again
= Now returning to first
= These are actual romanian numerals
Rendered image

Of course, there are lots of other cool properties that can be set, so feel free to dive into Reference and explore them!

And we are now moving to something much more interesting…

Advanced styling

The show rule

Advanced styling comes with another rule. The _`show` rule_.

#show "Be careful": strong[Play]

This is a very powerful thing, sometimes even too powerful.
Be careful with it.

#show "it is holding me hostage": text(green)[I'm fine]

Wait, what? I told you "Be careful!", not "Play!".

Help, it is holding me hostage.
Rendered image

Now a bit more serious

Show rule is a powerful thing that takes a _selector_
and what to apply to it. After that it will apply to
all elements it can find.

It may be extremely useful like that:

#show emph: set text(blue)

Now if I want to _emphasize_ something,
it will be both _emphasized_ and _blue_.
Isn't that cool?
Rendered image

Blocks

One of the most important usages is that you can set up all spacing using blocks. Like every element with text contains text that can be set up, every block element contains blocks:

Text before
= Heading
Text after

#show heading: set block(spacing: 0.5em)

Text before
= Heading
Text after
Rendered image

Selector

So show rule can accept selectors.
There are lots of different selector types,
for example
- element functions
- strings
- regular expressions
- field filters

Let's see example of the latter:

#show heading.where(level: 1): set align(center)

= Title
== Small title

Of course, you can set align by hand,
no need to use show rules
(but they are very handy!):

#align(center)[== Centered small title]
Rendered image

Custom formatting

Let's try now writing custom functions. 
It is very easy, see yourself:

// "it" is a heading, we take it and output things in braces
#show heading: it => {
  // center it
  set align(center)
  // set size and weight
  set text(12pt, weight: "regular")
  // see more about blocks and boxes
  // in corresponding chapter
  block(smallcaps(it.body))
}

= Smallcaps heading
Rendered image

Setting spacing

TODO: explain block spacing for common elements

Formatting to get an "article look"

#set page(
  // Header is that small thing on top
  header: align(
    right + horizon,
    [Some header there]
  ),
  height: 12cm
)

#align(center, text(17pt)[
  *Important title*
])

#grid(
  columns: (1fr, 1fr),
  align(center)[
    Some author \
    Some Institute \
    #link("mailto:some@mail.edu")
  ],
  align(center)[
    Another author \
    Another Institute \
    #link("mailto:another@mail.edu")
  ]
)

Now let's split text into two columns:

#show: rest => columns(2, rest)

#show heading.where(
  level: 1
): it => block(width: 100%)[
  #set align(center)
  #set text(12pt, weight: "regular")
  #smallcaps(it.body)
]

#show heading.where(
  level: 2
): it => text(
  size: 11pt,
  weight: "regular",
  style: "italic",
  it.body + [.],
)

// Now let's fill it with words:

= Heading 
== Small heading
#lorem(10)
== Second subchapter
#lorem(10)
= Second heading
#lorem(40)

== Second subchapter
#lorem(40)
Rendered image

Templates

Templates

If you want to reuse styling in other files, you can use the template idiom. Because set and show rules are only active in their current scope, they will not affect content in a file you imported your file into. But functions can circumvent this in a predictable way:

// define a function that:
// - takes content
// - applies styling to it
// - returns the styled content
#let apply-template(body) = [
  #show heading.where(level: 1): emph
  #set heading(numbering: "1.1")
  // ...
  #body
]

This is equivalent to:

// we can reduce the number of hashes needed here by using scripting mode
// same as above but we exchanged `[...]` for `{...}` to switch from markup
// into scripting mode
#let apply-template(body) = {
  show heading.where(level: 1): emph
  set heading(numbering: "1.1")
  // ...
  body
}

Then in your main file:

#import "template.typ": apply-template
#show: apply-template

This will apply a "template" function to the rest of your document!

Passing arguments

// add optional named arguments
#let apply-template(body, name: "My document") = {
  show heading.where(level: 1): emph
  set heading(numbering: "1.1")

  align(center, text(name, size: 2em))

  body
}

Then, in template file:

#import "template.typ": apply-template

// `func.with(..)` applies the arguments to the function and returns the new
// function with those defaults applied
#show: apply-template.with(name: "Report")

// it is functionally the same as this
#let new-template(..args) = apply-template(name: "Report", ..args)
#show: new-template

Writing templates is fairly easy if you understand scripting.

See more information about writing templates in Official Tutorial.

There is no official repository for templates yet, but there are a plenty community ones in awesome-typst.

Must-know

This section contains things, that are not general enough to be part of "tutorial", but still are very important to know for proper typesetting.

Feel free to skip through things you are sure you will not use.

Boxing & Blocking

You can use boxes to wrap anything
into text: #box(image("../tiger.jpg", height: 2em)).

Blocks will always be "separate paragraphs".
They will not fit into a text: #block(image("../tiger.jpg", height: 2em))
Rendered image

Both have similar useful properties:

#box(stroke: red, inset: 1em)[Box text]
#block(stroke: red, inset: 1em)[Block text]
Rendered image

rect

There is also rect that works like block, but has useful default inset and stroke:

#rect[Block text]
Rendered image

Figures

For the purposes of adding a figure to your document, use figure function. Don't try to use boxes or blocks there.

Figures are that things like centered images (probably with captions), tables, even code.

@tiger shows a tiger. Tigers
are animals.

#figure(
  image("../tiger.jpg", width: 80%),
  caption: [A tiger.],
) <tiger>
Rendered image

In fact, you can put there anything you want:

They told me to write a letter to you. Here it is:

#figure(
  {text(size: 5em)[I]},
  caption: [I'm cool, right?],
) 
Rendered image

Using spacing

Most time you will pass spacing into functions. There are special function fields that take only size. They are usually called like width, length, in(out)set, spacing and so on.

Like in CSS, one of the ways to set up spacing in Typst is setting margins and padding of elements. However, you can also insert spacing directly using functions h (horizontal spacing) and v (vertical spacing).

Links to reference: h, v.

Horizontal #h(1cm) spacing.
#v(1cm)
And some vertical too!
Rendered image

Absolute length units

Link to reference

Absolute length (aka just "length") units are not affected by outer content and size of parent.

#set rect(height: 1em)
#table(
  columns: 2,
  [Points], rect(width: 72pt),
  [Millimeters], rect(width: 25.4mm),
  [Centimeters], rect(width: 2.54cm),
  [Inches], rect(width: 1in),
)
Rendered image

Relative to current font size

1em = 1 current font size:

#set rect(height: 1em)
#table(
  columns: 2,
  [Centimeters], rect(width: 2.54cm),
  [Relative to font size], rect(width: 6.5em)
)

Double font size: #box(stroke: red, baseline: 40%, height: 2em, width: 2em)
Rendered image

It is a very convenient unit, so it is used a lot in Typst.

Combined

Combined: #box(rect(height: 5pt + 1em))

#(5pt + 1em).abs
#(5pt + 1em).em
Rendered image

Ratio length

Link to reference

1% = 1% from parent size in that dimension

This line width is 50% of available page size (without margins):

#line(length: 50%)

This line width is 50% of the box width: #box(stroke: red, width: 4em, inset: (y: 0.5em), line(length: 50%))
Rendered image

Relative length

Link to reference

You can combine absolute and ratio lengths into relative length:

#rect(width: 100% - 50pt)

#(100% - 50pt).length \
#(100% - 50pt).ratio
Rendered image

Fractional length

Link to reference

Single fraction length just takes maximum size possible to fill the parent:

Left #h(1fr) Right

#rect(height: 1em)[
  #h(1fr)
]
Rendered image

There are not many places you can use fractions, mainly those are h and v.

Several fractions

If you use several fractions inside one parent, they will take all remaining space proportional to their number:

Left #h(1fr) Left-ish #h(2fr) Right
Rendered image

Nested layout

Remember that fractions work in parent only, don't rely on them in nested layout:

Word: #h(1fr) #box(height: 1em, stroke: red)[
  #h(2fr)
]
Rendered image

Placing, Moving, Scale & Hide

This is a very important section if you want to do arbitrary things with layout, create custom elements and hacking a way around current Typst limitations.

TODO: WIP, add text and better examples

Place

Ignore layout, just put some object somehow relative to parent and current position. The placed object will not affect layouting

Link to reference

#set page(height: 60pt)
Hello, world!

#place(
  top + right, // place at the page right and top
  square(
    width: 20pt,
    stroke: 2pt + blue
  ),
)
Rendered image

Basic floating with place

#set page(height: 150pt)
#let note(where, body) = place(
  center + where,
  float: true,
  clearance: 6pt,
  rect(body),
)

#lorem(10)
#note(bottom)[Bottom 1]
#note(bottom)[Bottom 2]
#lorem(40)
#note(top)[Top]
#lorem(10)
Rendered image
Rendered image

dx, dy

Manually change position by (dx, dy) relative to intended.

#set page(height: 100pt)
#for i in range(16) {
  let amount = i * 4pt
  place(center, dx: amount - 32pt, dy: amount)[A]
}
Rendered image

Move

Link to reference

#rect(inset: 0pt, move(
  dx: 6pt, dy: 6pt,
  rect(
    inset: 8pt,
    fill: white,
    stroke: black,
    [Abra cadabra]
  )
))
Rendered image

Scale

Scale content without affecting the layout.

Link to reference

#scale(x: -100%)[This is mirrored.]
Rendered image
A#box(scale(75%)[A])A \
B#box(scale(75%, origin: bottom + left)[B])B
Rendered image

Hide

Don't show content, but leave empty space there.

Link to reference

Hello Jane \
#hide[Hello] Joe
Rendered image

Tables and grids

While tables are not that necessary to know if you don't plan to use them in your documents, grids may be very useful for document layout. We will use both of them them in the book later.

Let's not bother with copying examples from official documentation. Just make sure to skim through it, okay?

Basic snippets

Spreading

Spreading operators (see there) may be especially useful for the tables:

#set text(size: 9pt)

#let yield_cells(n) = {
  for i in range(0, n + 1) {
    for j in range(0, n + 1) {
      let product = if i * j != 0 {
        // math is used for the better look 
        if j <= i { $#{ j * i }$ } 
        else {
          // upper part of the table
          text(gray.darken(50%), str(i * j))
        }
      } else {
        if i == j {
          // the top right corner 
          $times$
        } else {
          // on of them is zero, we are at top/left
          $#{i + j}$
        }
      }
      // this is an array, for loops merge them together
      // into one large array of cells
      (
        table.cell(
          fill: if i == j and j == 0 { orange } // top right corner
          else if i == j { yellow } // the diagonal
          else if i * j == 0 { blue.lighten(50%) }, // multipliers
          product,),
      )
    }
  }
}

#let n = 10
#table(
  columns: (0.6cm,) * (n + 1), rows: (0.6cm,) * (n + 1), align: center + horizon, inset: 3pt, ..yield_cells(n),
)
Rendered image

Highlighting table row

#table(
  columns: 2,
  fill: (x, y) => if y == 2 { highlight.fill },
  [A], [B],
  [C], [D],
  [E], [F],
  [G], [H],
)
Rendered image

For individual cells, use

#table(
  columns: 2,
  [A], [B],
  table.cell(fill: yellow)[C], table.cell(fill: yellow)[D],
  [E], [F],
  [G], [H],
)
Rendered image

Splitting tables

Tables are split between pages automatically.

#set page(height: 8em)
#(
table(
  columns: 5,
  [Aligner], [publication], [Indexing], [Pairwise alignment], [Max. read length  (bp)],
  [BWA], [2009], [BWT-FM], [Semi-Global], [125],
  [Bowtie], [2009], [BWT-FM], [HD], [76],
  [CloudBurst], [2009], [Hashing], [Landau-Vishkin], [36],
  [GNUMAP], [2009], [Hashing], [NW], [36]
  )
)
Rendered image
Rendered image

However, if you want to make it breakable inside other element, you'll have to make that element breakable too:

#set page(height: 8em)
// Without this, the table fails to split upon several pages
#show figure: set block(breakable: true)
#figure(
table(
  columns: 5,
  [Aligner], [publication], [Indexing], [Pairwise alignment], [Max. read length  (bp)],
  [BWA], [2009], [BWT-FM], [Semi-Global], [125],
  [Bowtie], [2009], [BWT-FM], [HD], [76],
  [CloudBurst], [2009], [Hashing], [Landau-Vishkin], [36],
  [GNUMAP], [2009], [Hashing], [NW], [36]
  )
)
Rendered image
Rendered image

Scripting

Typst has a complete interpreted language inside. One of key aspects of working with your document in a nicer way

Basics

Variables I

Let's start with variables.

The concept is very simple, just some value you can reuse:

#let author = "John Doe"

This is a book by #author. #author is a great guy.

#quote(block: true, attribution: author)[
  \<Some quote\>
]
Rendered image

Variables II

You can store any Typst value in variable:

#let block_text = block(stroke: red, inset: 1em)[Text]

#block_text

#figure(caption: "The block", block_text)
Rendered image

Functions

We have already seen some "custom" functions in Advanced Styling chapter.

Functions are values that take some values and output some values:

// This is a syntax that we have seen earlier
#let f = (name) => "Hello, " + name

#f("world!")
Rendered image

Alternative syntax

You can write the same shorter:

// The following syntaxes are equivalent
#let f = (name) => "Hello, " + name
#let f(name) = "Hello, " + name

#f("world!")
Rendered image

Braces, brackets and default

Square brackets

You may remember that square brackets convert everything inside to content.

#let v = [Some text, _markup_ and other #strong[functions]]
#v
Rendered image

We may use same for functions bodies:

#let f(name) = [Hello, #name]
#f[World] // also don't forget we can use it to pass content!
Rendered image

Important: It is very hard to convert content to plain text, as content may contain anything! Sp be careful when passing and storing content in variables.

Braces

However, we often want to use code inside functions. That's when we use {}:

#let f(name) = {
  // this is code mode

  // First part of our output
  "Hello, "

  // we check if name is empty, and if it is,
  // insert placeholder
  if name == "" {
      "anonym"
  } else {
      name
  }

  // finish sentence
  "!"
}

#f("")
#f("Joe")
#f("world")
Rendered image

Scopes

This is a very important thing to remember.

You can't use variables outside of scopes they are defined (unless it is file root, then you can import them). Set and show rules affect things in their scope only.

#{
  let a = 3;
}
// can't use "a" there.

#[
  #show "true": "false"

  This is true.
]

This is true.
Rendered image

Return

Important: by default braces return anything that "returns" into them. For example,

#let change_world() = {
  // some code there changing everything in the world
  str(4e7)
  // another code changing the world
}

#let g() = {
  "Hahaha, I will change the world now! "
  change_world()
  " So here is my long evil monologue..."
}

#g()
Rendered image

To avoid returning everything, return only what you want explicitly, otherwise everything will be joined:

#let f() = {
  "Some long text"
  // Crazy numbers
  "2e7"
  return none
}

// Returns nothing
#f()
Rendered image

Default values

What we made just now was inventing "default values".

They are very common in styling, so there is a special syntax for them:

#let f(name: "anonym") = [Hello, #name!]

#f()
#f(name: "Joe")
#f(name: "world")
Rendered image

You may have noticed that the argument became named now. In Typst, named argument is an argument that has default value.

Types, part I

Each value in Typst has a type. You don't have to specify it, but it is important.

Content (content)

Link to Reference.

We have already seen it. A type that represents what is displayed in document.

#let c = [It is _content_!]

// Check type of c
#(type(c) == content)

#c

// repr gives an "inner representation" of value
#repr(c)
Rendered image

Important: It is very hard to convert content to plain text, as content may contain anything! So be careful when passing and storing content in variables.

None (none)

Nothing. Also known as null in other languages. It isn't displayed, converts to empty content.

#none
#repr(none)
Rendered image

String (str)

Link to Reference.

String contains only plain text and no formatting. Just some chars. That allows us to work with chars:

#let s = "Some large string. There could be escape sentences: \n,
 line breaks, and even unicode codes: \u{1251}"
#s \
#type(s) \
`repr`: #repr(s)

#let s = "another small string"
#s.replace("a", sym.alpha) \
#s.split(" ") // split by space
Rendered image

You can convert other types to their string representation using this type's constructor (e.g. convert number to string):

#str(5) // string, can be worked with as string
Rendered image

Boolean (bool)

Link to Reference.

true/false. Used in if and many others

#let b = false
#b \
#repr(b) \
#(true and not true or true) = #((true and (not true)) or true) \
#if (4 > 3) {
  "4 is more than 3"
}
Rendered image

Integer (int)

Link to Reference.

A whole number.

The number can also be specified as hexadecimal, octal, or binary by starting it with a zero followed by either x, o, or b.

#let n = 5
#n \
#(n += 1) \
#n \
#calc.pow(2, n) \
#type(n) \
#repr(n)
Rendered image
#(1 + 2) \
#(2 - 5) \
#(3 + 4 < 8)
Rendered image
#0xff \
#0o10 \
#0b1001
Rendered image

You can convert a value to an integer with this type's constructor (e.g. convert string to int).

#int(false) \
#int(true) \
#int(2.7) \
#(int("27") + int("4"))
Rendered image

Float (float)

Link to Reference.

Works the same way as integer, but can store floating point numbers. However, precision may be lost.

#let n = 5.0

// You can mix floats and integers, 
// they will be implicitly converted
#(n += 1) \
#calc.pow(2, n) \
#(0.2 + 0.1) \
#type(n) 
Rendered image
#3.14 \
#1e4 \
#(10 / 4)
Rendered image

You can convert a value to a float with this type's constructor (e.g. convert string to float).

#float(40%) \
#float("2.7") \
#float("1e5")
Rendered image

Types, part II

In Typst, most of things are immutable. You can't change content, you can just create new using this one (for example, using addition).

Immutability is very important for Typst since it tries to be as pure language as possible. Functions do nothing outside of returning some value.

However, purity is partly "broken" by these types. They are super-useful and not adding them would make Typst much pain.

However, using them adds complexity.

Arrays (array)

Link to Reference.

Mutable object that stores data with their indices.

Working with indices

#let values = (1, 7, 4, -3, 2)

// take value at index 0
#values.at(0) \
// set value at 0 to 3
#(values.at(0) = 3)
// negative index => start from the back
#values.at(-1) \
// add index of something that is even
#values.find(calc.even)
Rendered image

Iterating methods

#let values = (1, 7, 4, -3, 2)

// leave only what is odd
#values.filter(calc.odd) \
// create new list of absolute values of list values
#values.map(calc.abs) \
// reverse
#values.rev() \
// convert array of arrays to flat array
#(1, (2, 3)).flatten() \
// join array of string to string
#(("A", "B", "C")
 .join(", ", last: " and "))
Rendered image

List operations

// sum of lists:
#((1, 2, 3) + (4, 5, 6))

// list product:
#((1, 2, 3) * 4)
Rendered image

Empty list

#() \ // this is an empty list
#(1,) \  // this is a list with one element
BAD: #(1) // this is just an element, not a list!
Rendered image

Dictionaries (dict)

Link to Reference.

Dictionaries are objects that store a string "key" and a value, associated with that key.

#let dict = (
  name: "Typst",
  born: 2019,
)

#dict.name \
#(dict.launch = 20)
#dict.len() \
#dict.keys() \
#dict.values() \
#dict.at("born") \
#dict.insert("city", "Berlin ")
#("name" in dict)
Rendered image

Empty dictionary

This is an empty list: #() \
This is an empty dict: #(:)
Rendered image

Conditions & loops

Conditions

See official documentation.

In Typst, you can use if-else statements. This is especially useful inside function bodies to vary behavior depending on arguments types or many other things.

#if 1 < 2 [
  This is shown
] else [
  This is not.
]
Rendered image

Of course, else is unnecessary:

#let a = 3

#if a < 4 {
  a = 5
}

#a
Rendered image

You can also use else if statement (known as elif in Python):

#let a = 5

#if a < 4 {
  a = 5
} else if a < 6 {
  a = -3
}

#a
Rendered image

Booleans

if, else if, else accept only boolean values as a switch. You can combine booleans as described in types section:

#let a = 5

#if (a > 1 and a <= 4) or a == 5 [
    `a` matches the condition
]
Rendered image

Loops

See official documentation.

There are two kinds of loops: while and for. While repeats body while the condition is met:

#let a = 3

#while a < 100 {
    a *= 2
    str(a)
    " "
}
Rendered image

for iterates over all elements of sequence. The sequence may be an array, string or dictionary (for iterates over its key-value pairs).

#for c in "ABC" [
  #c is a letter.
]
Rendered image

To iterate to all numbers from a to b, use range(a, b+1):

#let s = 0

#for i in range(3, 6) {
    s += i
    [Number #i is added to sum. Now sum is #s.]
}
Rendered image

Because range is end-exclusive this is equal to

#let s = 0

#for i in (3, 4, 5) {
    s += i
    [Number #i is added to sum. Now sum is #s.]
}
Rendered image
#let people = (Alice: 3, Bob: 5)

#for (name, value) in people [
    #name has #value apples.
]
Rendered image

Break and continue

Inside loops can be used break and continue commands. break breaks loop, jumping outside. continue jumps to next loop iteration.

See the difference on these examples:

#for letter in "abc nope" {
  if letter == " " {
    // stop when there is space
    break
  }

  letter
}
Rendered image
#for letter in "abc nope" {
  if letter == " " {
    // skip the space
    continue
  }

  letter
}
Rendered image

Advanced arguments

Spreading arguments from list

#let func(a, b, c, d, e) = [#a #b #c #d #e]
#func(..(([hi],) * 5))
Rendered image

This may be super useful in tables:

#let a = ("hi", "b", "c")

#table(columns: 3,
  [test], [x], [hello],
  ..a
)
Rendered image

Key arguments

The same idea works with key arguments:

#let text-params = (fill: blue, size: 0.8em)

Some #text(..text-params)[text].
Rendered image

Managing arbitrary arguments

Typst allows taking as many arbitrary positional and key arguments as you want. In that case function is given special arguments object that stores in it positional and named arguments.

Link to reference

#let f(..args) = [
  #args.pos()\
  #args.named()
]

#f(1, "a", width: 50%, block: false)
Rendered image

You can combine them with other arguments. Spreading operator will "eat" all remaining arguments:

#let format(title, ..authors) = {
  let by = authors
    .pos()
    .join(", ", last: " and ")

  [*#title* \ _Written by #by;_]
}

#format("ArtosFlow", "Jane", "Joe")
Rendered image

States & Query

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Typst tries to be a pure language as much as possible.

That means, a function can't change anything outside of it. That also means, if you call function, the result should be always the same.

Unfortunately, our world (and therefore our documents) isn't pure. If you create a heading №2, you want the next to be number three.

That section will guide you to using impure Typst. Don't overuse it!

States

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Before we start something practical, it is important to understand states in general.

Here is a good explanation of why do we need them: Official Reference about states. It is highly recommended to read it first.

So instead of

#let x = 0
#let compute(expr) = {
  // eval evaluates string as Typst code
  x = eval(
    expr.replace("x", str(x))
  )
  [New value is #x.]
}

#compute("10") \
#compute("x + 3") \
#compute("x * 2") \
#compute("x - 5")

DOES NOT COMPILE: Variables from outside the function are read-only and cannot be modified

You should write

#let s = state("x", 0)
#let compute(expr) = [
  #s.update(x =>
    eval(expr.replace("x", str(x)))
  )
  New value is #s.display().
]

#compute("10") \
#compute("x + 3") \
#compute("x * 2") \
#compute("x - 5")

The computations will be made _in order_ they are located in the document:

#let more = [
  #compute("x * 2") \
  #compute("x - 5")
]

#compute("10") \
#compute("x + 3") \
#more
Rendered image

Operations with states

Creating new state

#let x = state("state-id")
#let y = state("state-id", 2)

#x, #y

#x.display() \
#y.display(n => "State is " + str(n))
Rendered image

Update

Updating is a content that tells that in this place of document the state should be updated.

#let x = state("x", 0)
#x.display() \
#let _ = x.update(3)
// nothing happens, we don't put `update` into the document flow
#x.display() \
#repr(x.update(3)) \ // this is how that content looks \
#x.update(3)
#x.display() // Finally!
Rendered image

ID collision

TLDR; Never allow colliding states.

States are described by their id-s, if they are the same, the code will break.

So, if you write functions or loops that are used several times, be careful!

#let f(x) = {
  // return new state…
  // …but their id-s are the same!
  // so it will always be the same state!
  let y = state("x", 0)
  y.update(y => y + x)
  y.display()
}

#let a = f(2)
#let b = f(3)

#a, #b \
#repr(a), #repr(b)
Rendered image

However, this may seem okay:

// locations in code are different!
#let x = state("state-id")
#let y = state("state-id", 2)

#x, #y
Rendered image

But in fact, it isn't:

#let x = state("state-id")
#let y = state("state-id", 2)

#x.display(), #y.display()

#x.update(3)

#x.display(), #y.display()
Rendered image

Locate

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Link to reference

Many things should be recompiled depending on some external things. To understand, what those external things are, it should be a content that is put into a document. It works roughly the same way as state.update.

Locate takes a function that, when that locate is put in the document and given a location in the document, returns some content instead of that locate.

Location

Link to reference

#locate(loc => [
  My location: \
  #loc.position()!
])
Rendered image

state.at(loc)

Given a location, returns value of state in that location. That allows kind of time travel, you can get location at any place of document.

state.display is roughly equivalent to

#let display(state) = locate(location => {
  state.at(location)
})

#let x = state("x", 0)
#x.display() \
#x.update(n => n + 3)
#display(x)
Rendered image

Final

Calculates the final value of state.

The location there is needed to restrict what content will change within recompilations. That greatly increases speed and better resolves "conflicts".

#let x = state("x", 5)
x = #x.display() \

#locate(loc => [
  The final x value is #x.final(loc)
])

#x.update(-3)
x = #x.display()

#x.update(n => n + 1)
x = #x.display()
Rendered image

Convergence

Sometimes layout will not converge. For example, imagine this:

#let x = state("x", 5)
x = #x.display() \

#locate(loc => [
  // let's set x to final x + 1
  // and see what will go on?
  #x.update(x.final(loc) + 1)
  #x.display()
])
Rendered image

WARNING: layout did not converge within 5 attempts

It is impossible to resolve that layout, so Typst gives up and gives you a warning.

That means you should be careful with states!

This is a dark, dark magic that requires large sacrifices!

Counters

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Counters are special states that count elements of some type. As with states, you can create your own with identifier strings.

Important: to initiate counters of elements, you need to set numbering for them.

States methods

Counters are states, so they can do all things states can do.

#set heading(numbering: "1.")

= Background
#counter(heading).update(3)
#counter(heading).update(n => n * 2)

== Analysis
Current heading number: #counter(heading).display().
Rendered image
#let mine = counter("mycounter")
#mine.display()

#mine.step()
#mine.display()

#mine.update(c => c * 3)
#mine.display()
Rendered image

Displaying counters

#set heading(numbering: "1.")

= Introduction
Some text here.

= Background
The current value is:
#counter(heading).display()

Or in roman numerals:
#counter(heading).display("I")
Rendered image

Counters also support displaying both current and final values out-of-box:

#set heading(numbering: "1.")

= Introduction
Some text here.

#counter(heading).display(both: true) \
#counter(heading).display("1 of 1", both: true) \
#counter(heading).display(
  (num, max) => [#num of #max],
   both: true
)

= Background
The current value is: #counter(heading).display()
Rendered image

Step

That's quite easy, for counters you can increment value using step. It works the same way as update.

#set heading(numbering: "1.")

= Introduction
#counter(heading).step()

= Analysis
Let's skip 3.1.
#counter(heading).step(level: 2)

== Analysis
At #counter(heading).display().
Rendered image

You can use counters in your functions:

#let c = counter("theorem")
#let theorem(it) = block[
  #c.step()
  *Theorem #c.display():*
  #it
]

#theorem[$1 = 1$]
#theorem[$2 < 3$]
Rendered image

Measure, Layout

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Style & Measure

Style documentation.

Measure documentation.

measure returns the element size. This command is extremely helpful when doing custom layout with place.

However, there is a catch. Element size depends on styles, applied to this element.

#let content = [Hello!]
#content
#set text(14pt)
#content
Rendered image

So if we will set the big text size for some part of our text, to measure the element's size, we have to know where the element is located. Without knowing it, we can't tell what styles should be applied.

So we need a scheme similar to locate.

This is what styles function is used for. It is a content, which, when located in document, calls a function inside on current styles.

Now, when we got fixed styles, we can get the element's size using measure:

#let thing(body) = style(styles => {
  let size = measure(body, styles)
  [Width of "#body" is #size.width]
})

#thing[Hey] \
#thing[Welcome]
Rendered image

Layout

Layout is similar to measure, but it returns current scope parent size.

If you are putting elements in block, that will be block's size. If you are just putting right on the page, that will be page's size.

As parent's size depends on it's place in document, it uses the similar scheme to locate and style:

#layout(size => {
  let half = 50% * size.width
  [Half a page is #half wide.]
})
Rendered image

It may be extremely useful to combine layout with measure, to get width of things that depend on parent's size:

#let text = lorem(30)
#layout(size => style(styles => [
  #let (height,) = measure(
    block(width: size.width, text),
    styles,
  )
  This text is #height high with
  the current page width: \
  #text
]))
Rendered image

Query

This section is outdated. It may be still useful, but it is strongly recommended to study new context system (using the reference).

Link there

Query is a thing that allows you getting location by selector (this is the same thing we used in show rules).

That enables "time travel", getting information about document from its parts and so on. That is a way to violate Typst's purity.

It is currently one of the the darkest magics currently existing in Typst. It gives you great powers, but with great power comes great responsibility.

Time travel

#let s = state("x", 0)
#let compute(expr) = [
  #s.update(x =>
    eval(expr.replace("x", str(x)))
  )
  New value is #s.display().
]

Value at `<here>` is
#context s.at(
  query(<here>)
    .first()
    .location()
)

#compute("10") \
#compute("x + 3") \
*Here.* <here> \
#compute("x * 2") \
#compute("x - 5")
Rendered image

Getting nearest chapter

#set page(header: context {
  let elems = query(
    selector(heading).before(here()),
    here(),
  )
  let academy = smallcaps[
    Typst Academy
  ]
  if elems == () {
    align(right, academy)
  } else {
    let body = elems.last().body
    academy + h(1fr) + emph(body)
  }
})

= Introduction
#lorem(23)

= Background
#lorem(30)

= Analysis
#lorem(15)
Rendered image

Metadata

Metadata is invisible content that can be extracted using query or other content. This may be very useful with typst query to pass values to external tools.

// Put metadata somewhere.
#metadata("This is a note") <note>

// And find it from anywhere else.
#context {
  query(<note>).first().value
}
Rendered image

Math

Math is a special environment that has special features related to... math.

Syntax

To start math environment, $. The spacing around $ will make it either inline math (smaller, used in text) or display math (used on math equations on their own).

// This is inline math
Let $a$, $b$, and $c$ be the side
lengths of right-angled triangle.
Then, we know that:

// This is display math
$ a^2 + b^2 = c^2 $

Prove by induction:

// You can use new lines as spacing too!
$
sum_(k=1)^n k = (n(n+1)) / 2
$
Rendered image

Math.equation

The element that math is displayed in is called math.equation. You can use it for set/show rules:

#show math.equation: set text(red)

$
integral_0^oo (f(t) + g(t))/2
$
Rendered image

Any symbol/command that is available in math, is also available in code mode using math.command:

#math.integral, #math.underbrace([a + b], [c])
Rendered image

Letters and commands

Typst aims to have as simple and effective syntax for math as possible. That means no special symbols, just using commands.

To make it short, Typst uses several simple rules:

  • All single-letter words turn into variables. That includes any unicode symbols too!

  • All multi-letter words turn into commands. They may be built-in commands (available with math.something outside of math environment). Or they may be user-defined variables/functions. If the command isn't defined, there will be compilation error.

    If you use kebab-case or snake_case for variables you want to use in math, you will have to refer to them as #snake-case-variable.
  • To write simple text, use quotes:

    $a "equals to" 2$
    Rendered image
    Spacing matters there!
    $a "=" 2$, $a"="2$
    Rendered image
  • You can turn it into multi-letter variables using italic:

    $(italic("mass") v^2)/2$
    Rendered image

Commands see there (go to the links to see the commands).

All symbols see there.

Multiline equations

To create multiline display equation, use the same symbol as in markup mode: \:

$
a = b\
a = c
$
Rendered image

Escaping

Any symbol that is used may be escaped with \, like in markup mode. For example, you can disable fraction:

a  / b \
a \/ b
Rendered image

The same way it works with any other syntax.

Wrapping inline math

Sometimes, when you write large math, it may be too close to text (especially for some long letter tails).

#lorem(17) $display(1)/display(1+x^n)$ #lorem(20)
Rendered image

You may easily increase the distance it by wrapping into box:

#lorem(17) #box($display(1)/display(1+x^n)$, inset: 0.2em) #lorem(20)
Rendered image

Symbols

Multiletter words in math refer either to local variables, functions, text operators, spacing or special symbols. The latter are very important for advanced math.

$
forall v, w in V, alpha in KK: alpha dot (v + w) = alpha v + alpha w
$
Rendered image

You can write the same with unicode:

$
v, wV, α𝕂: α(v + w) = α v + α w
$
Rendered image

Symbols naming

See all available symbols list there.

General idea

Typst wants to define some "basic" symbols with small easy-to-remember words, and build complex ones using combinations. For example,

$
// cont — contour
integral, integral.cont, integral.double, integral.square, sum.integral\

// lt — less than, gt — greater than
lt, lt.circle, lt.curly, lt.eq, lt.eq.curly, lt.not, lt.eq.not, lt.eq.not.curly, gt, lt.gt.eq, lt.gt.not
$
Rendered image

I highly recommend using WebApp/Typst LSP when writing math with lots of complex symbols. That helps you to quickly choose the right symbol within all combinations.

Sometimes the names are not obvious, for example, sometimes it is used prefix n- instead of not:

$
gt.nequiv, gt.napprox, gt.ntilde, gt.tilde.not
$
Rendered image

Common modifiers

  • .b, .t, .l, .r: bottom, top, left, right. Change direction of symbol.

    $arrow.b, triangle.r, angle.l$

    Rendered image
  • .bl, tr: bottom-left, top-right and so on. Where diagonal directions are possible.

  • .bar, .circle, .times, ...: adds corresponding element to symbol

  • .double, .triple, .quad: combine symbol 2, 3 or 4 times

  • .not crosses the symbol

  • .cw, .ccw: clock-wise and counter-clock-wise. For arrows and other things.

  • .big, .small:

    $plus.circle.big plus.circle, times.circle.big plus.circle$

    Rendered image
  • .filled: fills the symbol

    $square, square.filled, diamond.filled, arrow.filled$

    Rendered image

Greek letters

Lower case letters start with lower case letter, upper case start with upper case.

For different versions of letters, use .alt

$
alpha, Alpha, beta, Beta, beta.alt, gamma, pi, Pi,\
pi.alt, phi, phi.alt, Phi, omicron, kappa, kappa.alt, Psi,\
theta, theta.alt, xi, zeta, rho, rho.alt, kai, Kai,
$
Rendered image

Blackboard letters

Just use double of them. If you want to make some other symbol blackboard, use bb:

$bb(A), AA, bb(1)$
Rendered image

Fonts issues

Default font is New Computer Modern Math. It is a good font, but there are some inconsistencies.

Typst maps symbol names to unicode, so if the font has wrong symbols, Typst will display wrong ones.

Empty set

See example:

// nothing in default math font is something bad
$nothing, nothing.rev, diameter$

#show math.equation: set text(font: "Fira Math")

// Fira math is more consistent
$nothing, nothing.rev, diameter$
Rendered image

However, you can fix this with font feature:

#show math.equation: set text(features: ("cv01",))

$nothing, nothing.rev, diameter$
Rendered image

Or simply using "show" rule:

#show math.nothing: math.diameter

$nothing, nothing.rev, diameter$
Rendered image

Grouping

Every grouping can be (currently) done by parenthesis. So the parenthesis may be both "real" parenthesis and grouping ones.

For example, these parentheses specify nominator of the fraction:

$ (a^2 + b^2)/2 $
Rendered image

Left-right

See official documentation.

If there are two matching braces of any kind, they will be wrapped as lr (left-right) group.

$
{[((a + b)/2) + 1]_0}
$
Rendered image

You can disable it by escaping.

You can also match braces of any kind by using lr directly:

$
lr([a/2, b)) \
lr([a/2, b), size: #150%)
$
Rendered image

Fences

Fences are not matched automatically because of large amount of false-positives.

You can use abs or norm to match them:

$
abs(a + b), norm(a + b), floor(a + b), ceil(a + b), round(a + b)
$
Rendered image

Alignment

General alignment

By default display math is center-aligned, but that can be set up with show rule:

#show math.equation: set align(right)

$
(a + b)/2
$
Rendered image

Or using align element:

#align(left, block($ x = 5 $))
Rendered image

Alignment points

When equations include multiple alignment points (&), this creates blocks of alternatingly right- and left- aligned columns.

In the example below, the expression (3x + y) / 7 is right-aligned and = 9 is left-aligned.

$ (3x + y) / 7 &= 9 && "given" \
  3x + y &= 63 & "multiply by 7" \
  3x &= 63 - y && "subtract y" \
  x &= 21 - y/3 & "divide by 3" $
Rendered image

The word "given" is also left-aligned because && creates two alignment points in a row, alternating the alignment twice.

& & and && behave exactly the same way. Meanwhile, "multiply by 7" is left-aligned because just one & precedes it.

Each alignment point simply alternates between right-aligned/left-aligned.

Setting limits

Sometimes we want to change how the default attaching should work.

Limits

For example, in many countries it is common to write definite integrals with limits below and above. To set this, use limits function:

$
integral_a^b\
limits(integral)_a^b
$
Rendered image

You can set this by default using show rule:

#show math.integral: math.limits

$
integral_a^b
$

This is inline equation: $integral_a^b$
Rendered image

Only display mode

Notice that this will also affect inline equations. To enable limits for display math only, use limits(inline: false):

#show math.integral: math.limits.with(inline: false)

$
integral_a^b
$

This is inline equation: $integral_a^b$.
Rendered image

Of course, it is possible to move them back as bottom attachments:

$
sum_a^b, scripts(sum)_a^b
$
Rendered image

Operations

The same scheme works for operations. By default, they are attached to the bottom and top:

$a =_"By lemme 1" b, a scripts(=)_+ b$
Rendered image

Operators

See reference.

There are lots of built-in "text operators" in Typst math. This is a symbol that behaves very close to plain text. Nevertheless, it is different:

$
lim x_n, "lim" x_n, "lim"x_n
$
Rendered image

Predefined operators

Here are all text operators Typst has built-in:

$
arccos, arcsin, arctan, arg, cos, cosh, cot, coth, csc,\
csch, ctg, deg, det, dim, exp, gcd, hom, id, im, inf, ker,\
lg, lim, liminf, limsup, ln, log, max, min, mod, Pr, sec,\
sech, sin, sinc, sinh, sup, tan, tanh, tg "and" tr
$
Rendered image

Creating custom operator

Of course, there always will be some text operators you will need that are not in the list.

But don't worry, it is very easy to add your own:

#let arcsinh = math.op("arcsinh")

$
arcsinh x
$
Rendered image

Limits for operators

When creating operators (upright text with proper spacing), you can set limits for display mode at the same time:

$
op("liminf")_a, op("liminf", limits: #true)_a
$
Rendered image

This is roughly equivalent to

$
limits(op("liminf"))_a
$
Rendered image

Everything can be combined to create new operators:

#let liminf = math.op(math.underline(math.lim), limits: true)
#let limsup = math.op(math.overline(math.lim), limits: true)
#let integrate = math.op($integral dif x$)

$
liminf_(x->oo)\
limsup_(x->oo)\
integrate x^2
$
Rendered image

Location and sizes

We talked already about display and inline math. They differ not only by aligning and spacing, but also by size and style:

Inline: $a/(b + 1/c), sum_(n=0)^3 x_n$

$
a/(b + 1/c), sum_(n=0)^3 x_n
$
Rendered image

The size and style of current environment is described by Math Size, see reference.

There are for sizes:

  • Display math size (display)
  • Inline math size (inline)
  • Script math size (script)
  • Sub/super script math size (sscript)

Each time thing is used in fraction, script or exponent, it is moved several "levels lowers", becoming smaller and more "crapping". sscript isn't reduced father:

$
"display:" 1/("inline:" a + 1/("script:" b + 1/("sscript:" c + 1/("sscript:" d + 1/("sscript:" e + 1/f)))))
$
Rendered image

Setting sizes manually

Just use the corresponding command:

Inine: $sum_0^oo e^x^a$\
Inline with limits: $limits(sum)_0^oo e^x^a$\
Inline, but like true display: $display(sum_0^oo e^x^a)$
Rendered image

Vectors, matrices, semicolumn syntax

Vectors

By vector we mean a column there.
To write arrow notations for letters, use $arrow(v)$
I recommend to create shortcut for this, like #let arr = math.arrow

To write columns, use vec command:

$
vec(a, b, c) + vec(1, 2, 3) = vec(a + 1, b + 2, c + 3)
$
Rendered image

Delimiter

You can change parentheses around the column or even remove them:

$
vec(1, 2, 3, delim: "{") \
vec(1, 2, 3, delim: "||") \
vec(1, 2, 3, delim: #none)
$
Rendered image

Gap

You can change the size of gap between rows:

$
vec(a, b, c)
vec(a, b, c, gap:#0em)
vec(a, b, c, gap:#1em)
$
Rendered image

Making gap even

You can easily note that the gap isn't necessarily even or the same in different vectors:

$
vec(a/b, a/b, a/b) = vec(1, 1, 1)
$
Rendered image

That happens because gap refers to spacing between elements, not the distance between their centers.

To fix this, you can use this snippet.

Matrix

See official reference

Matrix is very similar to vec, but accepts rows, separated by ;:

$
mat(
    1, 2, ..., 10;
    2, 2, ..., 10;
    dots.v, dots.v, dots.down, dots.v;
    10, 10, ..., 10; // `;` in the end is optional
)
$
Rendered image

Delimiters and gaps

You can specify them the same way as for vectors.

Specify the arguments either before the content, or after the semicolon. The code will panic if there is no semicolon!
$
mat(
    delim: "|",
    1, 2, ..., 10;
    2, 2, ..., 10;
    dots.v, dots.v, dots.down, dots.v;
    10, 10, ..., 10;
    gap: #0.3em
)
$
Rendered image

Semicolon syntax

When you use semicolons, the arguments between the semicolons are merged into arrays. See yourself:

#let fun(..args) = {
    args.pos()
}

$
fun(1, 2;3, 4; 6, ; 8)
$
Rendered image

If you miss some of elements, they will be replaced by none-s.

You can mix semicolon syntax and named arguments, but be careful!

#let fun(..args) = {
    repr(args.pos())
    repr(args.named())
}

$
fun(1, 2; gap: #3em, 4)
$
Rendered image

For example, this will not work:

$
//         ↓ there is no `;`, so it tries to add (gap:) to array
mat(1, 2; 4, gap: #3em)
$

Classes

See official documentation

Each math symbol has its own "class", the way it behaves. That's one of the main reasons why they are layouted differently.

Classes

$
a b c\
a class("normal", b) c\
a class("punctuation", b) c\
a class("opening", b) c\
a lr(b c]) c\
a lr(class("opening", b) c ]) c // notice it is moved vertically \
a class("closing", b) c\
a class("fence", b) c\
a class("large", b) c\
a class("relation", b) c\
a class("unary", b) c\
a class("binary", b) c\
a class("vary", b) c\
$
Rendered image

Setting class for symbol

Default:

$square circle square$

With `#h(0)`:

$square #h(0pt) circle #h(0pt) square$

With `math.class`:

#show math.circle: math.class.with("normal")
$square circle square$
Rendered image

Special symbols

Important: I'm not great with special symbols, so I would additionally appreciate additions and corrections.

Typst has a great support of unicode. That also means it supports special symbols. They may be very useful for typesetting.

In most cases, you shouldn't use these symbols directly often. If possible, use them with show rules (for example, replace all -th with \u{2011}th, a non-breaking hyphen).

Non-breaking symbols

Non-breaking symbols can make sure the word/phrase will not be separated. Typst will try to put them as a whole.

Non-breaking space

Important: As it is spacing symbols, copy-pasting it will not help. Typst will see it as just a usual spacing symbol you used for your source code to look nicer in your editor. Again, it will interpret it as a basic space.

This is a symbol you should't use often (use Typst boxes instead), but it is a good demonstration of how non-breaking symbol work:

#set page(width: 9em)

// Cruel and world are separated.
// Imagine this is a phrase that can't be split, what to do then?
Hello cruel world

// Let's connect them with a special space!

// No usual spacing is allowed, so either use semicolumn...
Hello cruel#sym.space.nobreak;world

// ...parentheses...
Hello cruel#(sym.space.nobreak)world

// ...or unicode code
Hello cruel\u{00a0}world

// Well, to achieve the same effect I recommend using box:
Hello #box[cruel world]
Rendered image

Non-breaking hyphen

#set page(width: 8em)

This is an $i$-th element.

This is an $i$\u{2011}th element.

// the best way would be
#show "-th": "\u{2011}th"

This is an $i$-th element.
Rendered image

Connectors and separators

World joiner

Initially, world joiner indicates that no line break should occur at this position. It is also a zero-width symbol (invisible), so it can be used as a space removing thing:

#set page(width: 9em)
#set text(hyphenate: true)

Thisisawordthathastobreak

// Be careful, there is no line break at all now!
Thisi#sym.wj;sawordthathastobreak

// code from `physica` package
// word joiner here is used to avoid extra spacing
#let just-hbar = move(dy: -0.08em, strike(offset: -0.55em, extent: -0.05em, sym.planck))
#let hbar = (sym.wj, just-hbar, sym.wj).join()

$ a #just-hbar b, a hbar b$
Rendered image

Zero width space

Similar to word-joiner, but this is a space. It doesn't prevent word break. On the contrary, it breaks it without any hyphen at all!

#set page(width: 9em)
#set text(hyphenate: true)

// There is a space inside!
Thisisa#sym.zws;word

// Be careful, there is no hyphen at all now!
Thisisawo#sym.zws;rdthathastobreak
Rendered image

Extra

Bibliography

Typst supports bibliography using BibLaTex .bib file or its own Hayagriva .yml format.

BibLaTex is wider supported, but Hayagriva is easier to work with.

Link to Hayagriva documentation and some examples.

Citation Style

The style can be customized via CSL, citation style language, with more than 10 000 styles available online. See official repository.

Typst Snippets

Useful snippets for common (and not) tasks.

Demos

Resume (using template)

#import "@preview/modern-cv:0.1.0": *

#show: resume.with(
  author: (
      firstname: "John", 
      lastname: "Smith",
      email: "js@example.com", 
      phone: "(+1) 111-111-1111",
      github: "DeveloperPaul123",
      linkedin: "Example",
      address: "111 Example St. Example City, EX 11111",
      positions: (
        "Software Engineer",
        "Software Architect"
      )
  ),
  date: datetime.today().display()
)

= Education

#resume-entry(
  title: "Example University",
  location: "B.S. in Computer Science",
  date: "August 2014 - May 2019",
  description: "Example"
)

#resume-item[
  - #lorem(20)
  - #lorem(15)
  - #lorem(25)
]
Rendered image

Book cover

// author: bamdone
#let accent  = rgb("#00A98F")
#let accent1 = rgb("#98FFB3")
#let accent2 = rgb("#D1FF94")
#let accent3 = rgb("#D3D3D3")
#let accent4 = rgb("#ADD8E6")
#let accent5 = rgb("#FFFFCC")
#let accent6 = rgb("#F5F5DC")

#set page(paper: "a4",margin: 0.0in, fill: accent)

#set rect(stroke: 4pt)
#move(
  dx: -6cm, dy: 1.0cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent1,
)))

#set rect(stroke: 4pt)
#move(
  dx: -2cm, dy: -1.0cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent2,
)))

#set rect(stroke: 4pt)
#move(
  dx: 8cm, dy: -10cm,
  rotate(-45deg,
    rect(
      width: 100cm,
      height: 1cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent3,
)))

#set rect(stroke: 4pt)
#move(
  dx: 7cm, dy: -8cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent4,
)))

#set rect(stroke: 4pt)
#move(
  dx: 0cm, dy: -0cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 2cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent1,
)))

#set rect(stroke: 4pt)
#move(
  dx: 9cm, dy: -7cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 1.5cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent6,
)))

#set rect(stroke: 4pt)
#move(
  dx: 16cm, dy: -13cm,
  rotate(-45deg,
    rect(
      width: 1000cm,
      height: 1cm,
      radius: 50%,
      stroke: 0pt,
      fill:accent2,
)))

#align(center)[
  #rect(width: 30%,
    fill: accent4,
    stroke:none,
    [#align(center)[
      #text(size: 60pt,[Title])
    ]
    ])
]

#align(center)[
  #rect(width: 30%,
    fill: accent4,
    stroke:none,
    [#align(center)[
      #text(size: 20pt,[author])
    ]
    ])
]
Rendered image

Logos & Figures

Using SVG-s images is totally fine. Totally. But if you are lazy and don't want to search for images, here are some logos you can just copy-paste in your document.

Important: Typst in text doesn't need a special writing (unlike LaTeX). Just write "Typst", maybe "Typst", and it is okay.

TeX and LaTeX


#let TeX = {
  set text(font: "New Computer Modern", weight: "regular")
  box(width: 1.7em, {
    [T]
    place(top, dx: 0.56em, dy: 0.22em)[E]
    place(top, dx: 1.1em)[X]
  })
}

#let LaTeX = {
  set text(font: "New Computer Modern", weight: "regular")
  box(width: 2.55em, {
    [L]
    place(top, dx: 0.3em, text(size: 0.7em)[A])
    place(top, dx: 0.7em)[#TeX]
  })
}

Typst is not that hard to learn when you know #TeX and #LaTeX.
Rendered image

Typst guy

// author: fenjalien
#import "@preview/cetz:0.1.2": *

#set page(width: auto, height: auto)

#canvas(length: 1pt, {
  import draw: *
  let color = rgb("239DAD")
  scale((y: -1))
  set-style(fill: color, stroke: none,)

  // body
  merge-path({
    bezier(
      (112.847, 134.007),
      (114.835, 143.178),
      (112.847, 138.562),
      (113.509, 141.619),
      name: "b"
    )
    bezier(
      "b.end",
      (122.063, 145.515),
      (116.16, 144.736),
      (118.569, 145.515),
      name: "b"
    )
    bezier(
      "b.end",
      (135.977, 140.121),
      (125.677, 145.515),
      (130.315, 143.717)
    )
    bezier(
      (139.591, 146.055),
      (113.389, 159.182),
      (128.99, 154.806),
      (120.256, 159.182),
      name: "b"
    )
    bezier(
      "b.end",
      (97.1258, 154.327),
      (106.522, 159.182),
      (101.101, 157.563),
      name: "b"
    )
    bezier(
      "b.end",
      (91.1626, 136.704),
      (93.1503, 150.97),
      (91.1626, 145.096),
      name: "b"
    )
    line(
      (rel: (0, -47.1126), to: "b.end"),
      (rel: (-9.0352, 0)),
      (80.6818, 82.9381),
      (91.1626, 79.7013),
      (rel: (0, -8.8112)),
      (112.847, 61),
      (rel: (0, 19.7802)),
      (134.17, 79.1618),
      (132.182, 90.8501),
      (112.847, 90.1309)
    )
  })

  // left pupil
  merge-path({
    bezier(
      (70.4667, 65.6833),
      (71.9727, 70.5068),
      (71.4946, 66.9075),
      (71.899, 69.4091)
    )
    bezier(
      (71.9727, 70.5068),
      (75.9104, 64.5912),
      (72.9675, 69.6715),
      (75.1477, 67.319)
    )
    bezier(
      (75.9104, 64.5912),
      (72.0556, 60.0005),
      (76.8638, 61.1815),
      (74.4045, 59.7677)
    )
    bezier(
      (72.0556, 60.0005),
      (66.833, 64.3859),
      (70.1766, 60.1867),
      (67.7909, 63.0017)
    )
    bezier(
      (66.833, 64.3859),
      (70.4667, 65.6833),
      (67.6159, 64.3083),
      (69.4388, 64.4591)
    )
  })

  // right pupil
  merge-path({
    bezier(
      (132.37, 61.668),
      (133.948, 66.7212),
      (133.447, 62.9505),
      (133.87, 65.5712)
    )
    bezier(
      (133.948, 66.7212),
      (138.073, 60.5239),
      (134.99, 65.8461),
      (137.274, 63.3815)
    )
    bezier(
      (138.073, 60.5239),
      (134.034, 55.7145),
      (139.066, 56.9513),
      (136.495, 55.4706)
    )
    bezier(
      (134.034, 55.7145),
      (128.563, 60.3087),
      (132.066, 55.9066),
      (129.567, 58.8586),
    )
    bezier(
      (128.563, 60.3087),
      (132.37, 61.668),
      (129.383, 60.2274),
      (131.293, 60.3855),
    )
  })

  set-style(
    stroke: (paint: rgb("239DAD"), thickness: 6pt, cap: "round"),
    fill: none,
  )

  // left eye
  merge-path({
    bezier(
      (58.5, 64.7273),
      (73.6136, 52),
      (58.5, 58.3636),
      (64.0682, 52.7955),
      name: "b"
    )
    bezier(
      "b.end",
      (84.75, 64.7273),
      (81.5682, 52),
      (84.75, 57.5682),
      name: "b"
    )
    bezier(
      "b.end",
      (71.2273, 76.6591),
      (84.75, 71.8864),
      (79.1818, 76.6591),
      name: "b"
    )
    bezier(
      "b.end",
      (58.5, 64.7273),
      (63.2727, 76.6591),
      (58.5, 71.0909)
    )
  })
  // eye lash
  line(
    (62.5, 55),
    (59.5, 52),
  )

  merge-path({
    bezier(
      (146.5, 61.043),
      (136.234, 49),
      (146.5, 52.7634),
      (141.367, 49)
    )
    bezier(
      (136.234, 49),
      (121.569, 62.5484),
      (125.969, 49),
      (120.836, 54.2688)
    )
    bezier(
      (121.569, 62.5484),
      (134.034, 72.3333),
      (122.302, 70.8279),
      (128.168, 72.3333)
    )
    bezier(
      (134.034, 72.3333),
      (146.5, 61.043),
      (139.901, 72.3333),
      (146.5, 69.3225)
    )
  })

  set-style(stroke: (thickness: 4pt))

  // right arm
  merge-path({
    bezier(
      (109.523, 115.614),
      (127.679, 110.918),
      (115.413, 115.3675),
      (122.283, 113.112)
    )
    bezier(
      (127.679, 110.918),
      (137, 106.591),
      (130.378, 109.821),
      (132.708, 108.739)
    )
  })

  // right first finger
  bezier(
    (137, 106.591),
    (140.5, 98.0908),
    (137.385, 102.891),
    (138.562, 99.817)
  )

  // right second finger
  bezier(
    (137, 106.591),
    (146, 101.591),
    (139.21, 103.799),
    (142.425, 101.713)
  )

  // right third finger
  line(
    (137, 106.591),
    (148, 106.591)
  )

  //right forth finger
  bezier(
    (137, 106.591),
    (146, 111.091),
    (140.243, 109.552),
    (143.119, 110.812)
  )

  // left arm
  bezier(
    (95.365, 116.979),
    (73.5, 107.591),
    (88.691, 115.549),
    (80.587, 112.887)
  )

  // left first finger
  line(
    (73.5, 107.591),
    (rel: (0, -9.5))
  )
  // left second finger
  line(
    (73.5, 107.591),
    (65.396, 100.824)
  )
  // left third finger
  line(
    (73.5, 107.591),
    (63.012, 105.839)
  )
  // left fourth finger
  bezier(
    (73.5, 107.591),
    (63.012, 111.04),
    (70.783, 109.121),
    (67.214, 111.255)
  )
})
Rendered image

Labels

Get chapter of label

#let ref-heading(label) = context {
  let elems = query(label)
  if elems.len() != 1 {
    panic("found multiple elements")
  }
  let element = elems.first()
  if element.func() != heading {
    panic("label must target heading")
  }
  link(label, element.body)
}

= Design <design>
#lorem(20)

= Implementation
In #ref-heading(<design>), we discussed...
Rendered image

Outlines

Outlines

Lots of outlines examples are already available in official reference

Table of contents

#outline()

= Introduction
#lorem(5)

= Prior work
#lorem(10)
Rendered image

Outline of figures

#outline(
  title: [List of Figures],
  target: figure.where(kind: table),
)

#figure(
  table(
    columns: 4,
    [t], [1], [2], [3],
    [y], [0.3], [0.7], [0.5],
  ),
  caption: [Experiment results],
)
Rendered image

You can use arbitrary selector there, so you can do any crazy things.

Ignore low-level headings

#set heading(numbering: "1.")
#outline(depth: 2)

= Yes
Top-level section.

== Still
Subsection.

=== Nope
Not included.
Rendered image

Set indentation

#set heading(numbering: "1.a.")

#outline(
  title: [Contents (Automatic)],
  indent: auto,
)

#outline(
  title: [Contents (Length)],
  indent: 2em,
)

#outline(
  title: [Contents (Function)],
  indent: n => [] * n,
)

= About ACME Corp.
== History
=== Origins
#lorem(10)

== Products
#lorem(10)
Rendered image

Replace default dots

#outline(fill: line(length: 100%), indent: 2em)

= First level
== Second level
Rendered image

Make different outline levels look different

#set heading(numbering: "1.")

#show outline.entry.where(
  level: 1
): it => {
  v(12pt, weak: true)
  strong(it)
}

#outline(indent: auto)

= Introduction
= Background
== History
== State of the Art
= Analysis
== Setup
Rendered image

Long and short captions for the outline

// author: laurmaedje
// Put this somewhere in your template or at the start of your document.
#let in-outline = state("in-outline", false)
#show outline: it => {
  in-outline.update(true)
  it
  in-outline.update(false)
}

#let flex-caption(long, short) = context if in-outline.get() { short } else { long }

// And this is in the document.
#outline(title: [Figures], target: figure)

#figure(
  rect(),
  caption: flex-caption(
    [This is my long caption text in the document.],
    [This is short],
  )
)
Rendered image

Page setup

See Official Page Setup guide

#set page(
  width: 3cm,
  margin: (x: 0cm),
)

#for i in range(3) {
  box(square(width: 1cm))
}
Rendered image
#set page(columns: 2, height: 4.8cm)
Climate change is one of the most
pressing issues of our time, with
the potential to devastate
communities, ecosystems, and
economies around the world. It's
clear that we need to take urgent
action to reduce our carbon
emissions and mitigate the impacts
of a rapidly changing climate.
Rendered image
#set page(fill: rgb("444352"))
#set text(fill: rgb("fdfdfd"))
*Dark mode enabled.*
Rendered image
#set par(justify: true)
#set page(
  margin: (top: 32pt, bottom: 20pt),
  header: [
    #set text(8pt)
    #smallcaps[Typst Academcy]
    #h(1fr) _Exercise Sheet 3_
  ],
)

#lorem(19)
Rendered image
#set page(foreground: text(24pt)[🥸])

Reviewer 2 has marked our paper
"Weak Reject" because they did
not understand our approach...
Rendered image

Hiding things

// author: GeorgeMuscat
#let redact(text, fill: black, height: 1em) = {
  box(rect(fill: fill, height: height)[#hide(text)])
}

Example:
  - Unredacted text
  - Redacted #redact("text")
Rendered image

Multiline detection

Detects if figure caption (may be any other element) has more than one line.

If the caption is multiline, it makes it left-aligned.

Breaks on manual linebreaks.
#show figure.caption: it => {
  layout(size => style(styles => [
    #let text-size = measure(
      it.supplement + it.separator + it.body,
      styles,
    )

    #let my-align

    #if text-size.width < size.width {
      my-align = center
    } else {
      my-align = left
    }

    #align(my-align, it)

  ]))
}

#figure(caption: lorem(6))[
    ```rust
    pub fn main() {
        println!("Hello, world!");
    }
    ```
]

#figure(caption: lorem(20))[
    ```rust
    pub fn main() {
        println!("Hello, world!");
    }
    ```
]
Rendered image

Duplicate content

Notice that this implementation will mess up with labels and similar things. For complex cases see one below.
```typ #set page(paper: "a4", flipped: true) #show: body => grid( columns: (1fr, 1fr), column-gutter: 1cm, body, body, ) #lorem(200) ```

Advanced

/// author: frozolotl
#set page(paper: "a4", flipped: true)
#set heading(numbering: "1.1")
#show ref: it => {
  if it.element != none {
    it
  } else {
    let targets = query(it.target, it.location())
    if targets.len() == 2 {
      let target = targets.first()
      if target.func() == heading {
        let num = numbering(target.numbering, ..counter(heading).at(target.location()))
        [#target.supplement #num]
      } else if target.func() == figure {
        let num = numbering(target.numbering, ..target.counter.at(target.location()))
        [#target.supplement #num]
      } else {
        it
      }
    } else {
      it
    }
  }
}
#show link: it => context {
  let dest = query(it.dest)
  if dest.len() == 2 {
    link(dest.first().location(), it.body)
  } else {
    it
  }
}
#show: body => context grid(
  columns: (1fr, 1fr),
  column-gutter: 1cm,
  body,
  {
    let reset-counter(kind) = counter(kind).update(counter(kind).get())
    reset-counter(heading)
    reset-counter(figure.where(kind: image))
    reset-counter(figure.where(kind: raw))
    set heading(outlined: false)
    set figure(outlined: false)
    body
  },
)

#outline()

= Foo <foo>
See @foo and @foobar.

#figure(rect[This is an image], caption: [Foobar], kind: raw) <foobar>

== Bar
== Baz
#link(<foo>)[Click to visit Foo]
Rendered image

Lines between list items

/// author: frozolotl
#show enum.where(tight: false): it => {
  it.children
    .enumerate()
    .map(((n, item)) => block(below: .6em, above: .6em)[#numbering("1.", n + 1) #item.body])
    .join(line(length: 100%))
}

+ Item 1

+ Item 2

+ Item 3
Rendered image

The same approach may be easily adapted to style the enums as you want.

Shaped boxes with text

(I guess that will make a package eventually, but let it be a snippet for now)

/// author: JustForFun88
#import "@preview/oxifmt:0.2.0": strfmt

#let shadow_svg_path = `
<svg
    width="{canvas-width}"
    height="{canvas-height}"
    viewBox="{viewbox}"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:svg="http://www.w3.org/2000/svg">
    <!-- Definitions for reusable components -->
    <defs>
        <filter id="shadowing" >
            <feGaussianBlur in="SourceGraphic" stdDeviation="{blur}" />
        </filter>
    </defs>

    <!-- Drawing the rectangle with a fill and feGaussianBlur effect -->
    <path
        style="fill: {flood-color}; opacity: {flood-opacity}; filter:url(#shadowing)"
        d="{vertices} Z" />
</svg>
`.text

#let parallelogram(width: 20mm, height: 5mm, angle: 30deg) = {
	let δ = height * calc.tan(angle)
	(
    (      + δ,     0pt   ),
    (width + δ * 2, 0pt   ),
    (width + δ,     height),
    (0pt,           height),
	)
}

#let hexagon(width: 100pt, height: 30pt, angle: 30deg) = {
  let dy = height / 2;
	let δ = dy * calc.tan(angle)
	(
    (0pt,           dy    ),
    (      + δ,     0pt   ),
    (width + δ,     0pt   ),
    (width + δ * 2, dy    ),
    (width + δ,     height),
    (      + δ,     height),
	)
}

#let shape_size(vertices) = {
    let x_vertices = vertices.map(array.first);
    let y_vertices = vertices.map(array.last);

    (
      calc.max(..x_vertices) - calc.min(..x_vertices),
      calc.max(..y_vertices) - calc.min(..y_vertices)
    )
}

#let shadowed_shape(shape: hexagon, fill: none,
  stroke: auto, angle: 30deg, shadow_fill: black, alpha: 0.5, 
  blur: 1.5, blur_margin: 5, dx: 0pt, dy: 0pt, ..args, content
) = layout(size => context {
    let named = args.named()
    for key in ("width", "height") {
      if key in named and type(named.at(key)) == ratio {
        named.insert(key, size.at(key) * named.at(key))
      }
    }

    let opts = (blur: blur, flood-color: shadow_fill.to-hex())
       
    let content = box(content, ..named)
    let size = measure(content)

    let vertices = shape(..size, angle: angle)
    let (shape_width, shape_height) = shape_size(vertices)
    let margin = opts.blur * blur_margin * 1pt

    opts += (
      canvas-width:  shape_width  + margin,
      canvas-height: shape_height + margin,
      flood-opacity: alpha
    )

    opts.viewbox = (0, 0, opts.canvas-width.pt(), opts.canvas-height.pt()).map(str).join(",")

    opts.vertices = "";
    let d = margin / 2;
    for (i, p) in vertices.enumerate() {
        let prefix = if i == 0 { "M " } else { " L " };
        opts.vertices += prefix + p.map(x => str((x + d).pt())).join(", ");
    }

    let svg-shadow = image.decode(strfmt(shadow_svg_path, ..opts))
    place(dx: dx, dy: dy, svg-shadow)
    place(path(..vertices, fill: fill, stroke: stroke, closed: true))
    box(h((shape_width - size.width) / 2) + content, width: shape_width)
})

#set text(3em);

#shadowed_shape(shape: hexagon,
    inset: 1em, fill: teal,
    stroke: 1.5pt + teal.darken(50%),
    shadow_fill: red,
    dx: 0.5em, dy: 0.35em, blur: 3)[Hello there!]
#shadowed_shape(shape: parallelogram,
    inset: 1em, fill: teal,
    stroke: 1.5pt + teal.darken(50%),
    shadow_fill: red,
    dx: 0.5em, dy: 0.35em, blur: 3)[Hello there!]
Rendered image

Code formatting

Inline highlighting

#let r = raw.with(lang: "r")

This can then be used like: #r("x <- c(10, 42)")
Rendered image

Tab size

#set raw(tab-size: 8)
```tsv
Year	Month	Day
2000	2	3
2001	2	1
2002	3	10
```
Rendered image

Theme

See reference

Enable ligatures for code

#show raw: set text(ligatures: true, font: "Cascadia Code")

Then the code becomes `x <- a`
Rendered image

Advanced formatting

See packages section.

Scripting

Unflatten arrays

// author: PgSuper
#let unflatten(arr, n) = {
  let columns = range(0, n).map(_ => ())
  for (i, x) in arr.enumerate() {
    columns.at(calc.rem(i, n)).push(x)
  }
  array.zip(..columns)
}

#unflatten((1, 2, 3, 4, 5, 6), 2)
#unflatten((1, 2, 3, 4, 5, 6), 3)
Rendered image

Create an abbreviation

#let full-name = "Federal University of Ceará"

#let letts = {
  full-name
    .split()
    .map(word => word.at(0)) // filter only capital letters
    .filter(l => upper(l) == l)
    .join()
}
#letts
Rendered image

Split the string retrieving separators

#",this, is a a a a; a. test? string!".matches(regex("(\b[\P{Punct}\s]+\b|\p{Punct})")).map(x => x.captures).join()
Rendered image

Numbering

"Clean" numbering

// original author: tromboneher

// Number sections according to a number of schemes, omitting previous leading elements.
// For example, where the numbering pattern "A.I.1." would produce:
//
// A. A part of the story
//   A.I. A chapter
//   A.II. Another chapter
//     A.II.1. A section
//       A.II.1.a. A subsection
//       A.II.1.b. Another subsection
//     A.II.2. Another section
// B. Another part of the story
//   B.I. A chapter in the second part
//   B.II. Another chapter in the second part
//
// clean_numbering("A.", "I.", "1.a.") would produce:
//
// A. A part of the story
//   I. A chapter
//   II. Another chapter
//     1. A section
//       1.a. A subsection
//       1.b. Another subsection
//     2. Another section
// B. Another part of the story
//   I. A chapter in the second part
//   II. Another chapter in the second part
//
#let clean_numbering(..schemes) = {
  (..nums) => {
    let (section, ..subsections) = nums.pos()
    let (section_scheme, ..subschemes) = schemes.pos()

    if subsections.len() == 0 {
      numbering(section_scheme, section)
    } else if subschemes.len() == 0 {
      numbering(section_scheme, ..nums.pos())
    }
    else {
      clean_numbering(..subschemes)(..subsections)
    }
  }
}

#set heading(numbering: clean_numbering("A.", "I.", "1.a."))

= Part
== Chapter
== Another chapter
=== Section
==== Subsection
==== Another subsection
= Another part of the story
== A chapter in the second part
== Another chapter in the second part
Rendered image

Math numbering

See there.

Numbering each paragraph

// author: roehlichA
// Legal formatting of enumeration
#show enum: it => context {
  // Retrieve the last heading so we know what level to step at
  let headings = query(selector(heading).before(here()))
  let last = headings.at(-1)

  // Combine the output items
  let output = ()
  for item in it.children {
    output.push([
      #counter(heading).step(level: last.level + 1)
      #counter(heading).display()
    ])
    output.push([
      #text(item.body)
      #parbreak()
    ])
  }

  // Display in a grid
  grid(
    columns: (auto, 1fr),
    column-gutter: 1em,
    row-gutter: 1em,
    ..output
  )
}

= Some heading
+ Paragraphs here are preceded with a number so they can be referenced directly.
+ _#lorem(100)_
+ _#lorem(100)_

== A subheading
+ Paragraphs are also numbered correctly in subheadings.
+ _#lorem(50)_
+ _#lorem(50)_
Rendered image

Numbering

Number by current heading

See also built-in numbering in math package section

/// author: laurmaedje
#set heading(numbering: "1.")

// reset counter at each chapter
#show heading.where(level:1): it => {
  counter(math.equation).update(0)
  it
}

#set math.equation(numbering: n => {
  let h1 = counter(heading).get().first()
  numbering("(1.1)", h1, n)
})

= Section
== Subsection

$ 5 + 3 = 8 $
$ 5 + 3 = 8 $

= New Section
== Subsection
$ 5 + 3 = 8 $
== Subsection
$ 5 + 3 = 8 $
Rendered image

Number only labeled equations

Simple code

// author: shampoohere
#show math.equation:it => {
  if it.fields().keys().contains("label"){
    math.equation(block: true, numbering: "(1)", it)
    // change your numbering style in `numbering`
  } else {
    it
  }
}

$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-2>
$ sum_x^2 $
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-3>
Rendered image

Make the hacked references clickable again

// author: gijsleb
#show math.equation:it => {
  if it.has("label"){
    math.equation(block:true, numbering: "(1)", it)
  } else {
    it
  }
}

#show ref: it => {
  let el = it.element
  if el != none and el.func() == math.equation {
    link(el.location(), numbering(
      "(1)",
      counter(math.equation).at(el.location()).at(0) + 1
    ))
  } else {
    it
  }
}

$ sum_x^2 $ 
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-2>
$ sum_x^2 $ 
$ dif/(dif x)(A(t)+B(x))=dif/(dif x)A(t)+dif/(dif x)B(t) $ <ep-3>
In @ep-2 and @ep-3 we see equations
Rendered image

Operations

Fractions

$
p/q, p slash q, p\/q
$
Rendered image

Slightly moved:

#let mfrac(a, b) = move(a, dy: -0.2em) + "/" + move(b, dy: 0.2em, dx: -0.1em)
$A\/B, #mfrac($A$, $B$)$,
Rendered image

Large fractions

#let dfrac(a, b) = $display(frac(#a, #b))$

$(x + y)/(1/x + 1/y) quad (x + y)/(dfrac(1,x) + dfrac(1, y))$
Rendered image

Scripts

To set scripts and limits see Typst Basics section

Make every character upright when used in subscript

// author: emilyyyylime

$f_a, f_b, f^a, f_italic("word")$
#show math.attach: it => {
  import math: *
  if it.has("b") and it.b.func() != upright[].func() and it.b.has("text") and it.b.text.len() == 1 {
    let args = it.fields()
    let _ = args.remove("base")
    let _ = args.remove("b")
    attach(it.base, b: upright(it.b), ..args)
  } else {
    it
  }
}

$f_a, f_b, f^a, f_italic("word")$

Vectors & Matrices

You can easily note that the gap isn't necessarily even or the same in different vectors and matrices:

$
mat(0, 1, -1; -1, 0, 1; 1, -1, 0) vec(a/b, a/b, a/b) = vec(c, d, e)
$
Rendered image

That happens because gap refers to spacing between elements, not the distance between their centers.

To fix this, you can use this snippet:

// Fixed height vector
#let fvec(..children, delim: "(", gap: 1.5em) = { // change default gap there
  style(styles => 
    math.vec(
      delim: delim,
      gap: 0em,
      ..for el in children.pos() {
        ({
          box(
            width: measure(el, styles).width,
            height: gap, place(horizon, el)
          )
        },) // this is an array
        // `for` merges all these arrays, then we pass it to arguments
      }
    )
  )
}

// fixed hight matrix
// accepts also row-gap, column-gap and gap
#let fmat(..rows, delim: "(", augment: none) = {
  let args = rows.named()
  let (gap, row-gap, column-gap) = (none,)*3;

  if "gap" in args {
    gap = args.at("gap")
    row-gap = args.at("row-gap", default: gap)
    column-gap = args.at("row-gap", default: gap)
  }
  else {
    // change default vertical there
    row-gap = args.at("row-gap", default: 1.5em) 
    // and horizontal there
    column-gap = rows.named().at("column-gap", default: 0.5em)
  }

  style(styles =>
    math.mat(
      delim: delim,
      row-gap: 0em,
      column-gap: column-gap,
      ..for row in rows.pos() {
        (for el in row {
          ({
          box(
            width: measure(el, styles).width,
            height: row-gap, place(horizon, el)
          )
        },)
        }, )
      }
    )
  )
}

$
"Before:"& vec(((a/b))/c, a/b, c) = vec(1, 1, 1)\
"After:"& fvec(((a/b))/c, a/b, c) = fvec(1, 1, 1)\

"Before:"& mat(a, b; c, d) vec(e, dot) = vec(c/d, e/f)\
"After:"& fmat(a, b; c, d) fvec(e, dot) = fvec(c/d, e/f)
$
Rendered image

Fonts

Set math font

Important: The font you want to set for math should contain necessary math symbols. That should be a special font with math. If it isn't, you are very likely to get an error (remember to set fallback: false and check typst fonts to debug the fonts).

#show math.equation: set text(font: "Fira Math", fallback: false)

$
emptyset \

integral_a^b sum (A + B)/C dif x \
$
Rendered image

Calligraphic letters

#let scr(it) = math.class("normal",
  text(font: "", stylistic-set: 1, $cal(it)$) + h(0em)
)

$ scr(A) scr(B) + scr(C), -scr(D) $
Rendered image

Unfortunately, currently just stylistic-set for math creates bad spacing. Math engine detects if the letter should be correctly spaced by whether it is the default font. However, just making it "normal" isn't enough, because than it can be reduced. That's way the snippet is as hacky as it is (probably should be located in Typstonomicon, but it's not large enough).

Color & Gradients

Gradients

Gradients may be very cool for presentations or just a pretty look.

/// author: frozolotl
#set page(paper: "presentation-16-9", margin: 0pt)
#set text(fill: white, font: "Inter")

#let grad = gradient.linear(rgb("#953afa"), rgb("#c61a22"), angle: 135deg)

#place(horizon + left, image(width: 60%, "../img/landscape.png"))

#place(top, polygon(
  (0%, 0%),
  (70%, 0%),
  (70%, 25%),
  (0%, 29%),
  fill: white,
))
#place(bottom, polygon(
  (0%, 100%),
  (100%, 100%),
  (100%, 30%),
  (60%, 30% + 60% * 4%),
  (60%, 60%),
  (0%, 64%),
  fill: grad,
))

#place(top + right, block(inset: 7pt, image(height: 19%, "../img/tub.png")))

#place(bottom, block(inset: 40pt)[
  #text(size: 30pt)[
    Presentation Title
  ]

  #text(size: 16pt)[#lorem(20) | #datetime.today().display()]
])
Rendered image

Pretty things

Set bar to the text's left

(also known as quote formatting)

+ #lorem(10) \
  #rect(fill: luma(240), stroke: (left: 0.25em))[
    *Solution:* #lorem(10)

    $ a_(n+1)x^n = 2... $
  ]
Rendered image

Text on box top

// author: gaiajack
#let todo(body) = block(
  above: 2em, stroke: 0.5pt + red,
  width: 100%, inset: 14pt
)[
  #set text(font: "Noto Sans", fill: red)
  #place(
    top + left,
    dy: -6pt - 14pt, // Account for inset of block
    dx: 6pt - 14pt,
    block(fill: white, inset: 2pt)[*DRAFT*]
  )
  #body
]

#todo(lorem(100))
Rendered image

Individual language fonts

A cat แปลว่า แมว

#show regex("\p{Thai}+"): text.with(font: "Noto Serif Thai")

A cat แปลว่า แมว
Rendered image

Fake italic & Text shadows

Skew

// author: Enivex
#set page(width: 21cm, height: 3cm)
#set text(size:25pt)
#let skew(angle,vscale: 1,body) = {
  let (a,b,c,d)= (1,vscale*calc.tan(angle),0,vscale)
  let E = (a + d)/2
  let F = (a - d)/2
  let G = (b + c)/2
  let H = (c - b)/2
  let Q = calc.sqrt(E*E + H*H)
  let R = calc.sqrt(F*F + G*G)
  let sx = Q + R
  let sy = Q - R
  let a1 = calc.atan2(F,G)
  let a2 = calc.atan2(E,H)
  let theta = (a2 - a1) /2
  let phi = (a2 + a1)/2

  set rotate(origin: bottom+center)
  set scale(origin: bottom+center)

  rotate(phi,scale(x: sx*100%, y: sy*100%,rotate(theta,body)))
}

#let fake-italic(body) = skew(-12deg,body)
#fake-italic[This is fake italic text]

#let shadowed(body) = box(place(skew(-50deg, vscale: 0.8, text(fill:luma(200),body)))+place(body))
#shadowed[This is some fancy text with a shadow]
Rendered image

Special documents

Signature places

#block(width: 150pt)[
  #line(length: 100%)
  #align(center)[Signature]
]
Rendered image

Presentations

See polylux.

Forms

Form with placeholder

#grid(
  columns: 2,
  rows: 4,
  gutter: 1em,

  [Student:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [Teacher:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [ID:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
  [School:],
  [#block()#align(bottom)[#line(length: 10em, stroke: 0.5pt)]],
)
Rendered image

Interactive

Presentation interactive forms are coming! They are currently under heavy work by @tinger.

Use with external tools

Currently the best ways to communicate is using

  1. Preprocessing. The tool should generate Typst file
  2. Typst Query (CLI). See the docs there.
  3. WebAssembly plugins. See the docs there.

In some time there will be examples of successful usage of first two methods. For the third one, see packages.

Packages

Once the Typst Universe was launched, this chapter has become almost redundant. This is actually a very cool place to look for packages.

However, there are still some cool examples of interesting package usage.

General

Typst has packages, but, unlike LaTeX, you need to remember:

  • You need them only for some specialized tasks, basic formatting can be totally done without them.
  • Packages are much lighter and much easier "installed" than LaTeX ones.
  • Packages are just plain Typst files (and sometimes plugins), so you can easily write your own!

To use mighty package, just write, like this:

#import "@preview/cetz:0.1.2": canvas, plot

#canvas(length: 1cm, {
  plot.plot(size: (8, 6),
    x-tick-step: none,
    x-ticks: ((-calc.pi, $-pi$), (0, $0$), (calc.pi, $pi$)),
    y-tick-step: 1,
    {
      plot.add(
        style: plot.palette.blue,
        domain: (-calc.pi, calc.pi), x => calc.sin(x * 1rad))
      plot.add(
        hypograph: true,
        style: plot.palette.blue,
        domain: (-calc.pi, calc.pi), x => calc.cos(x * 1rad))
      plot.add(
        hypograph: true,
        style: plot.palette.blue,
        domain: (-calc.pi, calc.pi), x => calc.cos((x + calc.pi) * 1rad))
    })
})
Rendered image

Contributing

If you are author of a package or just want to make a fair overview, feel free to make issues/PR-s!

Drawing

cetz

Cetz is an analogue of LaTeX's tikz. Maybe it is not as powerful yet, but certainly easier to learn and use.

It is the best choice in most of cases you want to draw something in Typst.

#import "@preview/cetz:0.1.2"

#cetz.canvas(length: 1cm, {
  import cetz.draw: *
  import cetz.angle: angle
  let (a, b, c) = ((0,0), (-1,1), (1.5,0))
  line(a, b)
  line(a, c)
  set-style(angle: (radius: 1, label-radius: .5), stroke: blue)
  angle(a, c, b, label: $alpha$, mark: (end: ">"), stroke: blue)
  set-style(stroke: red)
  angle(a, b, c, label: n => $#{n/1deg} degree$,
    mark: (end: ">"), stroke: red, inner: false)
})
Rendered image
#import "@preview/cetz:0.1.2": canvas, draw

#canvas(length: 1cm, {
  import draw: *
  intersections(name: "demo", {
    circle((0, 0))
    bezier((0,0), (3,0), (1,-1), (2,1))
    line((0,-1), (0,1))
    rect((1.5,-1),(2.5,1))
  })
  for-each-anchor("demo", (name) => {
    circle("demo." + name, radius: .1, fill: black)
  })
})
Rendered image
#import "@preview/cetz:0.1.2": canvas, draw

#canvas(length: 1cm, {
  import draw: *
  let (a, b, c) = ((0, 0), (1, 1), (2, -1))
  line(a, b, c, stroke: gray)
  bezier-through(a, b, c, name: "b")
  // Show calculated control points
  line(a, "b.ctrl-1", "b.ctrl-2", c, stroke: gray)
})
Rendered image
#import "@preview/cetz:0.1.2": canvas, draw

#canvas(length: 1cm, {
  import draw: *
  group(name: "g", {
    rotate(45deg)
    rect((0,0), (1,1), name: "r")
    copy-anchors("r")
  })
  circle("g.top", radius: .1, fill: black)
})
Rendered image

Graphs

cetz

Cetz comes with quite built-in support of drawing basic graphs. It is much more customizable and extensible then packages like plotst, so it is recommended to skim through its possibilities.

See full manual there.

#let data = (
  [A], ([B], [C], [D]), ([E], [F])
)

#import "@preview/cetz:0.1.2": canvas, draw, tree

#canvas(length: 1cm, {
  import draw: *

  set-style(content: (padding: .2),
    fill: gray.lighten(70%),
    stroke: gray.lighten(70%))

  tree.tree(data, spread: 2.5, grow: 1.5, draw-node: (node, _) => {
    circle((), radius: .45, stroke: none)
    content((), node.content)
  }, draw-edge: (from, to, _) => {
    line((a: from, number: .6, abs: true, b: to),
         (a: to, number: .6, abs: true, b: from), mark: (end: ">"))
  }, name: "tree")

  // Draw a "custom" connection between two nodes
  let (a, b) = ("tree.0-0-1", "tree.0-1-0",)
  line((a: a, number: .6, abs: true, b: b), (a: b, number: .6, abs: true, b: a), mark: (end: ">", start: ">"))
})
Rendered image
#import "@preview/cetz:0.1.2": canvas, draw

#canvas({
    import draw: *
    circle((90deg, 3), radius: 0, name: "content")
    circle((210deg, 3), radius: 0, name: "structure")
    circle((-30deg, 3), radius: 0, name: "form")
    for (c, a) in (
    ("content", "bottom"),
    ("structure", "top-right"),
    ("form", "top-left")
    ) {
    content(c, box(c + " oriented", inset: 5pt), anchor:
    a)
    }
    stroke(gray + 1.2pt)
    line("content", "structure", "form", close: true)
    for (c, s, f, cont) in (
    (0.5, 0.1, 1, "PostScript"),
    (1, 0, 0.4, "DVI"),
    (0.5, 0.5, 1, "PDF"),
    (0, 0.25, 1, "CSS"),
    (0.5, 1, 0, "XML"),
    (0.5, 1, 0.4, "HTML"),
    (1, 0.2, 0.8, "LaTeX"),
    (1, 0.6, 0.8, "TeX"),
    (0.8, 0.8, 1, "Word"),
    (1, 0.05, 0.05, "ASCII")
    ) {
    content((bary: (content: c, structure: s, form:
    f)),cont)
    }
})
Rendered image
#import "@preview/cetz:0.1.2": canvas, chart

#let data2 = (
  ([15-24], 18.0, 20.1, 23.0, 17.0),
  ([25-29], 16.3, 17.6, 19.4, 15.3),
  ([30-34], 14.0, 15.3, 13.9, 18.7),
  ([35-44], 35.5, 26.5, 29.4, 25.8),
  ([45-54], 25.0, 20.6, 22.4, 22.0),
  ([55+],   19.9, 18.2, 19.2, 16.4),
)

#canvas({
  chart.barchart(mode: "clustered",
                 size: (9, auto),
                 label-key: 0,
                 value-key: (..range(1, 5)),
                 bar-width: .8,
                 x-tick-step: 2.5,
                 data2)
})
Rendered image

Draw a graph in polar coords

#import "@preview/cetz:0.1.2": canvas, plot

#figure(
canvas(length: 1cm, {
  plot.plot(size: (5, 5),
    x-tick-step: 5,
    y-tick-step: 5,
    x-max: 20,
    y-max: 20,
    x-min: -20,
    y-min: -20,
    x-grid: true,
    y-grid: true,
    {
      plot.add(
        domain: (0,2*calc.pi),
        samples: 100,
        t => (13*calc.cos(t)-5*calc.cos(2*t)-2*calc.cos(3*t)-calc.cos(4*t), 16*calc.sin(t)*calc.sin(t)*calc.sin(t))
        )
    })
}), caption: "Plot made with cetz",)
Rendered image

diagraph

Test

#import "@preview/diagraph:0.2.0": *
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph {
    rankdir=LR;
    f -> B
    B -> f
    C -> D
    D -> B
    E -> F
    f -> E
    B -> F
  }
  ```
)
Rendered image

Eating

#import "@preview/diagraph:0.2.0": *
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph {
    orange -> fruit
    apple -> fruit
    fruit -> food
    carrot -> vegetable
    vegetable -> food
    food -> eat
    eat -> survive
  }
  ```
)
Rendered image

FFT

Labels are overridden manually.

#import "@preview/diagraph:0.2.0": *
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph {
    node [shape=none]
    1
    2
    3
    r1
    r2
    r3
    1->2
    1->3
    2->r1 [color=red]
    3->r2 [color=red]
    r1->r3 [color=red]
    r2->r3 [color=red]
  }
  ```
)
Rendered image

State Machine

#import "@preview/diagraph:0.2.0": *
#set page(width: auto)
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph finite_state_machine {
    rankdir=LR
    size="8,5"

    node [shape=doublecircle]
    LR_0
    LR_3
    LR_4
    LR_8

    node [shape=circle]
    LR_0 -> LR_2 [label="SS(B)"]
    LR_0 -> LR_1 [label="SS(S)"]
    LR_1 -> LR_3 [label="S($end)"]
    LR_2 -> LR_6 [label="SS(b)"]
    LR_2 -> LR_5 [label="SS(a)"]
    LR_2 -> LR_4 [label="S(A)"]
    LR_5 -> LR_7 [label="S(b)"]
    LR_5 -> LR_5 [label="S(a)"]
    LR_6 -> LR_6 [label="S(b)"]
    LR_6 -> LR_5 [label="S(a)"]
    LR_7 -> LR_8 [label="S(b)"]
    LR_7 -> LR_5 [label="S(a)"]
    LR_8 -> LR_6 [label="S(b)"]
    LR_8 -> LR_5 [label="S(a)"]
  }
  ```
)
Rendered image

Clustering

See docs.

#import "@preview/diagraph:0.2.0": *
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph G {

    subgraph cluster_0 {
      style=filled;
      color=lightgrey;
      node [style=filled,color=white];
      a0 -> a1 -> a2 -> a3;
      label = "process #1";
    }

    subgraph cluster_1 {
      node [style=filled];
      b0 -> b1 -> b2 -> b3;
      label = "process #2";
      color=blue
    }

    start -> a0;
    start -> b0;
    a1 -> b3;
    b2 -> a3;
    a3 -> a0;
    a3 -> end;
    b3 -> end;

    start [shape=Mdiamond];
    end [shape=Msquare];
  }
  ```
)
Rendered image

HTML

#import "@preview/diagraph:0.2.0": *
#let renderc(code) = render(code.text)

#renderc(
  ```
  digraph structs {
      node [shape=plaintext]
      struct1 [label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
    <TR><TD>left</TD><TD PORT="f1">mid dle</TD><TD PORT="f2">right</TD></TR>
  </TABLE>>];
      struct2 [label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
    <TR><TD PORT="f0">one</TD><TD>two</TD></TR>
  </TABLE>>];
      struct3 [label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">
    <TR>
      <TD ROWSPAN="3">hello<BR/>world</TD>
      <TD COLSPAN="3">b</TD>
      <TD ROWSPAN="3">g</TD>
      <TD ROWSPAN="3">h</TD>
    </TR>
    <TR>
      <TD>c</TD><TD PORT="here">d</TD><TD>e</TD>
    </TR>
    <TR>
      <TD COLSPAN="3">f</TD>
    </TR>
  </TABLE>>];
      struct1:f1 -> struct2:f0;
      struct1:f2 -> struct3:here;
  }
  ```
)
Rendered image

Overridden labels

Labels for nodes big and sum are overridden.

#import "@preview/diagraph:0.2.0": *
#set page(width: auto)

#raw-render(
  ```
  digraph {
    rankdir=LR
    node[shape=circle]
    Hmm -> a_0
    Hmm -> big
    a_0 -> "a'" -> big [style="dashed"]
    big -> sum
  }
  ```,
  labels: (:
    big: [_some_#text(2em)[ big ]*text*],
    sum: $ sum_(i=0)^n 1/i $,
  ),
)
Rendered image

bob-draw

WASM plugin for svgbob to draw easily with ASCII,.

#import "@preview/bob-draw:0.1.0": *
#render(```
         /\_/\
bob ->  ( o.o )
         \ " /
  .------/  /
 (        | |
  `====== o o
```)
Rendered image
#import "@preview/bob-draw:0.1.0": *
#show raw.where(lang: "bob"): it => render(it)

#render(
    ```
      0       3  
       *-------* 
    1 /|    2 /| 
     *-+-----* | 
     | |4    | |7
     | *-----|-*
     |/      |/
     *-------*
    5       6
    ```,
    width: 25%,
)

```bob
"cats:"
 /\_/\  /\_/\  /\_/\  /\_/\ 
( o.o )( o.o )( o.o )( o.o )
```

```bob
       +10-15V           ___0,047R
      *---------o-----o-|___|-o--o---------o----o-------.
    + |         |     |       |  |         |    |       |
    -===-      _|_    |       | .+.        |    |       |
    -===-      .-.    |       | | | 2k2    |    |       |
    -===-    470| +   |       | | |        |    |      _|_
    - |       uF|     '--.    | '+'       .+.   |      \ / LED
      +---------o        |6   |7 |8    1k | |   |      -+-
             ___|___   .-+----+--+--.     | |   |       |
              -═══-    |            |     '+'   |       |
                -      |            |1     |  |/  BC    |
               GND     |            +------o--+   547   |
                       |            |      |  |`>       |
                       |            |     ,+.   |       |
               .-------+            | 220R| |   o----||-+  IRF9Z34
               |       |            |     | |   |    |+->
               |       |  MC34063   |     `+'   |    ||-+
               |       |            |      |    |       |  BYV29     -12V6
               |       |            |      '----'       o--|<-o----o--X OUT
 6000 micro  - | +     |            |2                  |     |    |
 Farad, 40V ___|_____  |            |--o                C|    |    |
 Capacitor  ~ ~ ~ ~ ~  |            | GND         30uH  C|    |   --- 470
               |       |            |3      1nF         C|    |   ###  uF
               |       |            |-------||--.       |     |    | +
               |       '-----+----+-'           |      GND    |   GND
               |            5|   4|             |             |
               |             |    '-------------o-------------o
               |             |                           ___  |
               `-------------*------/\/\/------------o--|___|-'
                                     2k              |       1k0
                                                    .+.
                                                    | | 5k6 + 3k3
                                                    | | in Serie
                                                    '+'
                                                     |
                                                    GND
```
Rendered image

wavy

finite

Finite automata. See the manual for a full documentation.

#import "@preview/finite:0.3.0": automaton

#automaton((
  q0: (q1:0, q0:"0,1"),
  q1: (q0:(0,1), q2:"0"),
  q2: (),
))
Rendered image

Custom boxes

Showbox

#import "@preview/showybox:2.0.1": showybox

#showybox(
  [Hello world!]
)
Rendered image
#import "@preview/showybox:2.0.1": showybox

// First showybox
#showybox(
  frame: (
    border-color: red.darken(50%),
    title-color: red.lighten(60%),
    body-color: red.lighten(80%)
  ),
  title-style: (
    color: black,
    weight: "regular",
    align: center
  ),
  shadow: (
    offset: 3pt,
  ),
  title: "Red-ish showybox with separated sections!",
  lorem(20),
  lorem(12)
)

// Second showybox
#showybox(
  frame: (
    dash: "dashed",
    border-color: red.darken(40%)
  ),
  body-style: (
    align: center
  ),
  sep: (
    dash: "dashed"
  ),
  shadow: (
	  offset: (x: 2pt, y: 3pt),
    color: yellow.lighten(70%)
  ),
  [This is an important message!],
  [Be careful outside. There are dangerous bananas!]
)
Rendered image
#import "@preview/showybox:2.0.1": showybox

#showybox(
  title: "Stokes' theorem",
  frame: (
    border-color: blue,
    title-color: blue.lighten(30%),
    body-color: blue.lighten(95%),
    footer-color: blue.lighten(80%)
  ),
  footer: "Information extracted from a well-known public encyclopedia"
)[
  Let $Sigma$ be a smooth oriented surface in $RR^3$ with boundary $diff Sigma equiv Gamma$. If a vector field $bold(F)(x,y,z)=(F_x (x,y,z), F_y (x,y,z), F_z (x,y,z))$ is defined and has continuous first order partial derivatives in a region containing $Sigma$, then

  $ integral.double_Sigma (bold(nabla) times bold(F)) dot bold(Sigma) = integral.cont_(diff Sigma) bold(F) dot dif bold(Gamma) $
]
Rendered image
#import "@preview/showybox:2.0.1": showybox

#showybox(
  title-style: (
    weight: 900,
    color: red.darken(40%),
    sep-thickness: 0pt,
    align: center
  ),
  frame: (
    title-color: red.lighten(80%),
    border-color: red.darken(40%),
    thickness: (left: 1pt),
    radius: 0pt
  ),
  title: "Carnot cycle's efficiency"
)[
  Inside a Carnot cycle, the efficiency $eta$ is defined to be:

  $ eta = W/Q_H = frac(Q_H + Q_C, Q_H) = 1 - T_C/T_H $
]
Rendered image
#import "@preview/showybox:2.0.1": showybox

#showybox(
  footer-style: (
    sep-thickness: 0pt,
    align: right,
    color: black
  ),
  title: "Divergence theorem",
  footer: [
    In the case of $n=3$, $V$ represents a volume in three-dimensional space, and $diff V = S$ its surface
  ]
)[
  Suppose $V$ is a subset of $RR^n$ which is compact and has a piecewise smooth boundary $S$ (also indicated with $diff V = S$). If $bold(F)$ is a continuously differentiable vector field defined on a neighborhood of $V$, then:

  $ integral.triple_V (bold(nabla) dot bold(F)) dif V = integral.surf_S (bold(F) dot bold(hat(n))) dif S $
]
Rendered image
#import "@preview/showybox:2.0.1": showybox

#showybox(
  frame: (
    border-color: red.darken(30%),
    title-color: red.darken(30%),
    radius: 0pt,
    thickness: 2pt,
    body-inset: 2em,
    dash: "densely-dash-dotted"
  ),
  title: "Gauss's Law"
)[
  The net electric flux through any hypothetical closed surface is equal to $1/epsilon_0$ times the net electric charge enclosed within that closed surface. The closed surface is also referred to as Gaussian surface. In its integral form:

  $ Phi_E = integral.surf_S bold(E) dot dif bold(A) = Q/epsilon_0 $
]
Rendered image

Colorful boxes

#import "@preview/colorful-boxes:1.2.0": colorbox, slantedColorbox, outlinebox, stickybox

#colorbox(
  title: lorem(5),
  color: "blue",
  radius: 2pt,
  width: auto
)[
  #lorem(50)
]

#slantedColorbox(
  title: lorem(5),
  color: "red",
  radius: 0pt,
  width: auto
)[
  #lorem(50)
]

#outlinebox(
  title: lorem(5),
  color: none,
  width: auto,
  radius: 2pt,
  centering: false
)[
  #lorem(50)
]

#outlinebox(
  title: lorem(5),
  color: "green",
  width: auto,
  radius: 2pt,
  centering: true
)[
  #lorem(50)
]

#stickybox(
  rotation: 3deg,
  width: 7cm
)[
  #lorem(20)
]
Rendered image

Theorems

See math

Math

General

physica

Physica (Latin for natural sciences) provides utilities that simplify otherwise complex and repetitive mathematical expressions in natural sciences.

Its manual provides a full set of demonstrations of how the package could be helpful.

Common notations

  • Calculus: differential, ordinary and partial derivatives
    • Optional function name,
    • Optional order number or an array of thereof,
    • Customizable "d" symbol and product joiner (say, exterior product),
    • Overridable total order calculation,
  • Vectors and vector fields: div, grad, curl,
  • Taylor expansion,
  • Dirac braket notations,
  • Tensors with abstract index notations,
  • Matrix transpose and dagger (conjugate transpose).
  • Special matrices: determinant, (anti-)diagonal, identity, zero, Jacobian, Hessian, etc.

Below is a preview of those notations.

#import "@preview/physica:0.9.1": * // Symbol names annotated below

#table(
  columns: 4, align: horizon, stroke: none, gutter: 1em,

  // vectors: bold, unit, arrow
  [$ vb(a), vb(e_i), vu(a), vu(e_i), va(a), va(e_i) $],
  // dprod (dot product), cprod (cross product), iprod (innerproduct)
  [$ a dprod b, a cprod b, iprod(a, b) $],
  // laplacian (different from built-in laplace)
  [$ dot.double(u) = laplacian u =: laplace u $],
  // grad, div, curl (vector fields)
  [$ grad phi, div vb(E), \ curl vb(B) $],
)
Rendered image
#import "@preview/physica:0.9.1": * // Symbol names annotated below

#table(
  columns: 4, align: horizon, stroke: none, gutter: 1em,

  // Row 1.
  // dd (differential), var (variation), difference
  [$ dd(f), var(f), difference(f) $],
  // dd, with an order number or an array thereof
  [$ dd(x,y), dd(x,y,2), \ dd(x,y,[1,n]), dd(vb(x),t,[3,]) $],
  // dd, with custom "d" symbol and joiner
  [$ dd(x,y,p:and), dd(x,y,d:Delta), \ dd(x,y,z,[1,1,n+1],d:d,p:dot) $],
  // dv (ordinary derivative), with custom "d" symbol and joiner
  [$ dv(phi,t,d:Delta), dv(phi,t,d:upright(D)), dv(phi,t,d:delta) $],

  // Row 2.
  // dv, with optional function name and order
  [$ dv(,t) (dv(x,t)) = dv(x,t,2) $],
  // pdv (partial derivative)
  [$ pdv(f,x,y,2), pdv(,x,y,[k,]) $],
  // pdv, with auto-added overridable total
  [$ pdv(,x,y,[i+2,i+1]), pdv(,y,x,[i+1,i+2],total:3+2i) $],
  // In a flat form
  [$ dv(u,x,s:slash), \ pdv(u,x,y,2,s:slash) $],
)
Rendered image
#import "@preview/physica:0.9.1": * // Symbol names annotated below

#table(
  columns: 3, align: horizon, stroke: none, gutter: 1em,

  // tensor
  [$ tensor(T,+a,-b,-c) != tensor(T,-b,-c,+a) != tensor(T,+a',-b,-c) $],
  // Set builder notation
  [$ Set(p, {q^*, p} = 1) $],
  // taylorterm (Taylor series term)
  [$ taylorterm(f,x,x_0,1) \ taylorterm(f,x,x_0,(n+1)) $],
)
Rendered image
#import "@preview/physica:0.9.1": * // Symbol names annotated below

#table(
  columns: 3, align: horizon, stroke: none, gutter: 1em,

  // expval (mean/expectation value), eval (evaluation boundary)
  [$ expval(X) = eval(f(x)/g(x))^oo_1 $],
  // Dirac braket notations
  [$
    bra(u), braket(u), braket(u, v), \
    ket(u), ketbra(u), ketbra(u, v), \
    mel(phi, hat(p), psi) $],
  // Superscript show rules that need to be enabled explicitly.
  // If put in a content block, they only control that block's scope.
  [
    #show: super-T-as-transpose // "..^T" just like handwriting
    #show: super-plus-as-dagger // "..^+" just like handwriting
    $ op("conj")A^T =^"def" A^+ \
      e^scripts(T), e^scripts(+) $ ], // Override with scripts()
)
Rendered image

Matrices

In addition to Typst's built-in mat() to write a matrix, physica provides a number of handy tools to make it even easier.

#import "@preview/physica:0.9.1": TT, mdet

$
// Matrix transpose with "TT", though it is recommended to
// use super-T-as-transpose so that "A^T" also works (more on that later).
A^TT,
// Determinant with "mdet(...)".
det mat(a, b; c, d) := mdet(a, b; c, d)
$
Rendered image

Diagonal matrix dmat(...), antidiagonal matrix admat(...), identity matrix imat(n), and zero matrix zmat(n).

#import "@preview/physica:0.9.1": dmat, admat, imat, zmat

$ dmat(1, 2)  dmat(1, a_1, xi, fill:0)               quad
  admat(1, 2) admat(1, a_1, xi, fill:dot, delim:"[") quad
  imat(2)     imat(3, delim:"{",fill:*) quad
  zmat(2)     zmat(3, delim:"|") $
Rendered image

Jacobian matrix with jmat(func; ...) or the longer name jacobianmatrix, Hessian matrix with hmat(func; ...) or the longer name hessianmatrix, and finally xmat(row, col, func) to build a matrix.

#import "@preview/physica:0.9.1": jmat, hmat, xmat

$
jmat(f_1,f_2; x,y) jmat(f,g; x,y,z; delim:"[") quad
hmat(f; x,y)       hmat(; x,y; big:#true)      quad

#let elem-ij = (i,j) => $g^(#(i - 1)#(j - 1)) = #calc.pow(i,j)$
xmat(2, 2, #elem-ij)
$
Rendered image

mitex

MiTeX provides LaTeX support powered by WASM in Typst, including real-time rendering of LaTeX math equations. You can also use LaTeX syntax to write \ref and \label.

Please refer to the manual for more details.

#import "@preview/mitex:0.2.0": *

Write inline equations like #mi("x") or #mi[y].

Also block equations:

#mitex(`
  \newcommand{\f}[2]{#1f(#2)}
  \f\relax{x} = \int_{-\infty}^\infty
    \f\hat\xi\,e^{2 \pi i \xi x}
    \,d\xi
`)

Text mode:

#mitext(`
  \iftypst
    #set math.equation(numbering: "(1)", supplement: "equation")
  \fi

  An inline equation $x + y$ and block \eqref{eq:pythagoras}.

  \begin{equation}
    a^2 + b^2 = c^2 \label{eq:pythagoras}
  \end{equation}
`)

i-figured

Configurable equation numbering per section in Typst. There is also figure numbering per section, see more examples in its manual.

#import "@preview/i-figured:0.2.3"

// make sure you have some heading numbering set
#set heading(numbering: "1.1")

// apply the show rules (these can be customized)
#show heading: i-figured.reset-counters
#show math.equation: i-figured.show-equation.with(
  level: 1,
  zero-fill: true,
  leading-zero: true,
  numbering: "(1.1)",
  prefix: "eqt:",
  only-labeled: false,  // numbering all block equations implicitly
  unnumbered-label: "-",
)


= Introduction

You can write inline equations such as $x + y$, and numbered block equations like:

$ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>

To reference a math equation, please use the `eqt:` prefix. For example, with @eqt:ratio, we have:

$ F_n = floor(1 / sqrt(5) phi.alt^n) $


= Appdendix

Additionally, you can use the <-> tag to indicate that a block formula should not be numbered:

$ y = integral_1^2 x^2 dif x $ <->

Subsequent math equations will continue to be numbered as usual:

$ F_n = floor(1 / sqrt(5) phi.alt^n) $
Rendered image

Theorems

ctheorem

A numbered theorem environment in Typst. See more examples in its manual.

#import "@preview/ctheorems:1.1.0": *
#show: thmrules

#set page(width: 16cm, height: auto, margin: 1.5cm)
#set heading(numbering: "1.1")

#let theorem = thmbox("theorem", "Theorem", fill: rgb("#eeffee"))
#let corollary = thmplain("corollary", "Corollary", base: "theorem", titlefmt: strong)
#let definition = thmbox("definition", "Definition", inset: (x: 1.2em, top: 1em))

#let example = thmplain("example", "Example").with(numbering: none)
#let proof = thmplain(
  "proof", "Proof", base: "theorem",
  bodyfmt: body => [#body #h(1fr) $square$]
).with(numbering: none)

= Prime Numbers
#lorem(7)
#definition[ A natural number is called a #highlight[_prime number_] if ... ]
#example[
  The numbers $2$, $3$, and $17$ are prime. See @cor_largest_prime shows that
  this list is not exhaustive!
]
#theorem("Euclid")[There are infinitely many primes.]
#proof[
  Suppose to the contrary that $p_1, p_2, dots, p_n$ is a finite enumeration
  of all primes. ... a contradiction.
]
#corollary[
  There is no largest prime number.
] <cor_largest_prime>
#corollary[There are infinitely many composite numbers.]
Rendered image

lemmify

Lemmify is another theorem evironment generator with many selector and numbering capabilities. See documentations in its readme.

#import "@preview/lemmify:0.1.5": *

#let my-thm-style(
  thm-type, name, number, body
) = grid(
  columns: (1fr, 3fr),
  column-gutter: 1em,
  stack(spacing: .5em, [#strong(thm-type) #number], emph(name)),
  body
)
#let my-styling = ( thm-styling: my-thm-style )
#let (
  definition, theorem, proof, lemma, rules
) = default-theorems("thm-group", lang: "en", ..my-styling)
#show: rules
#show thm-selector("thm-group"): box.with(inset: 0.8em)
#show thm-selector("thm-group", subgroup: "theorem"): it => box(
  it, fill: rgb("#eeffee"))

#set heading(numbering: "1.1")

= Prime numbers
#lorem(7) @proof and @thm[theorem]
#definition[ A natural number is called a #highlight[_prime number_] if ... ]
#theorem(name: "Theorem name")[There are infinitely many primes.]<thm>
#proof[
  Suppose to the contrary that $p_1, p_2, dots, p_n$ is a finite enumeration
  of all primes. ... #highlight[_a contradiction_].]<proof>
#lemma[There are infinitely many composite numbers.]
Rendered image

Physics

physica

Physica (Latin for natural sciences) provides utilities that simplify otherwise complex and repetitive mathematical expressions in natural sciences.

Its manual provides a full set of demonstrations of how the package could be helpful.

Mathematical physics

The packages/math.md page has more examples on its math capabilities. Below is a preview that may be of particular interest in the domain of physics:

  • Calculus: differential, ordinary and partial derivatives
    • Optional function name,
    • Optional order number or array of order numbers,
    • Customizable "d" symbol and product joiner (say, exterior product),
    • Overridable total order calculation,
  • Vectors and vector fields: div, grad, curl,
  • Taylor expansion,
  • Dirac braket notations,
  • Tensors with abstract index notations,
  • Matrix transpose and dagger (conjugate transpose).
  • Special matrices: determinant, (anti-)diagonal, identity, zero, Jacobian, Hessian, etc.

A partial glimpse:

#import "@preview/physica:0.9.1": *
#show: super-T-as-transpose // put in a #[...] to limit its scope...
#show: super-plus-as-dagger // ...or use scripts() to manually override

$ dd(x,y,2) quad dv(f,x,d:Delta)      quad pdv(,x,y,[2i+1,2+i]) quad
  vb(a) va(a) vu(a_i)  quad mat(laplacian, div; grad, curl)     quad
  tensor(T,+a,-b,+c)   quad ket(phi)  quad A^+ e^scripts(+) A^T integral^T $
Rendered image

Isotopes

#import "@preview/physica:0.9.1": isotope

// a: mass number A
// z: the atomic number Z
$
isotope(I, a:127), quad isotope("Fe", z:26), quad
isotope("Tl",a:207,z:81) --> isotope("Pb",a:207,z:82) + isotope(e,a:0,z:-1)
$
Rendered image

Reduced Planck constant (hbar)

In the default font, the Typst built-in symbol planck.reduce looks a bit off: on letter "h" there is a slash instead of a horizontal bar, contrary to the symbol's colloquial name "h-bar". This package offers hbar to render the symbol in the familiar form⁠. Contrast:

#import "@preview/physica:0.9.1": hbar

$ E = planck.reduce omega => E = hbar omega, wide
  frac(planck.reduce^2, 2m) => frac(hbar^2, 2m), wide
  (pi G^2) / (planck.reduce c^4) => (pi G^2) / (hbar c^4), wide
  e^(frac(i(p x - E t), planck.reduce)) => e^(frac(i(p x - E t), hbar)) $
Rendered image

quill: quantum diagrams

See documentation.

#import "@preview/quill:0.2.0": *
#quantum-circuit(
  lstick($|0$), gate($H$), ctrl(1), rstick($(|00+|11)/2$, n: 2), [\ ],
  lstick($|0$), 1, targ(), 1
)
Rendered image
#import "@preview/quill:0.2.0": *

#let ancillas = (setwire(0), 5, lstick($|0$), setwire(1), targ(), 2, [\ ],
setwire(0), 5, lstick($|0$), setwire(1), 1, targ(), 1)

#quantum-circuit(
  scale-factor: 80%,
  lstick($|ψ$), 1, 10pt, ctrl(3), ctrl(6), $H$, 1, 15pt, 
    ctrl(1), ctrl(2), 1, [\ ],
  ..ancillas, [\ ],
  lstick($|0$), 1, targ(), 1, $H$, 1, ctrl(1), ctrl(2), 
    1, [\ ],
  ..ancillas, [\ ],
  lstick($|0$), 2, targ(),  $H$, 1, ctrl(1), ctrl(2), 
    1, [\ ],
  ..ancillas
)
Rendered image
#import "@preview/quill:0.2.0": *

#quantum-circuit(
  lstick($|psi$),  ctrl(1), gate($H$), 1, ctrl(2), meter(), [\ ],
  lstick($|beta_00〉$, n: 2), targ(), 1, ctrl(1), 1, meter(), [\ ],
  3, gate($X$), gate($Z$),  midstick($|psi$)
)
Rendered image

Tables

Tablex: general purpose tables library

#import "@preview/tablex:0.0.7": tablex, rowspanx, colspanx

#tablex(
  columns: 4,
  align: center + horizon,
  auto-vlines: false,

  // indicate the first two rows are the header
  // (in case we need to eventually
  // enable repeating the header across pages)
  header-rows: 2,

  // color the last column's cells
  // based on the written number
  map-cells: cell => {
    if cell.x == 3 and cell.y > 1 {
      cell.content = {
        let value = int(cell.content.text)
        let text-color = if value < 10 {
          red.lighten(30%)
        } else if value < 15 {
          yellow.darken(13%)
        } else {
          green
        }
        set text(text-color)
        strong(cell.content)
      }
    }
    cell
  },

  /* --- header --- */
  rowspanx(2)[*Username*], colspanx(2)[*Data*], (), rowspanx(2)[*Score*],
  (),                 [*Location*], [*Height*], (),
  /* -------------- */

  [John], [Second St.], [180 cm], [5],
  [Wally], [Third Av.], [160 cm], [10],
  [Jason], [Some St.], [150 cm], [15],
  [Robert], [123 Av.], [190 cm], [20],
  [Other], [Unknown St.], [170 cm], [25],
)
Rendered image
#import "@preview/tablex:0.0.7": tablex, hlinex, vlinex, colspanx, rowspanx

#pagebreak()
#v(80%)

#tablex(
  columns: 4,
  align: center + horizon,
  auto-vlines: false,
  repeat-header: true,

  /* --- header --- */
  rowspanx(2)[*Names*], colspanx(2)[*Properties*], (), rowspanx(2)[*Creators*],
  (),                 [*Type*], [*Size*], (),
  /* -------------- */

  [Machine], [Steel], [5 $"cm"^3$], [John p& Kate],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Robert],
  [Frog], [Animal], [6 $"cm"^3$], [Rodbert],
)
Rendered image
Rendered image
#import "@preview/tablex:0.0.7": tablex, gridx, hlinex, vlinex, colspanx, rowspanx

#tablex(
  columns: 4,
  auto-lines: false,

  // skip a column here         vv
  vlinex(), vlinex(), vlinex(), (), vlinex(),
  colspanx(2)[a], (),  [b], [J],
  [c], rowspanx(2)[d], [e], [K],
  [f], (),             [g], [L],
  //   ^^ '()' after the first cell are 100% ignored
)

#tablex(
  columns: 4,
  auto-vlines: false,
  colspanx(2)[a], (),  [b], [J],
  [c], rowspanx(2)[d], [e], [K],
  [f], (),             [g], [L],
)

#gridx(
  columns: 4,
  (), (), vlinex(end: 2),
  hlinex(stroke: yellow + 2pt),
  colspanx(2)[a], (),  [b], [J],
  hlinex(start: 0, end: 1, stroke: yellow + 2pt),
  hlinex(start: 1, end: 2, stroke: green + 2pt),
  hlinex(start: 2, end: 3, stroke: red + 2pt),
  hlinex(start: 3, end: 4, stroke: blue.lighten(50%) + 2pt),
  [c], rowspanx(2)[d], [e], [K],
  hlinex(start: 2),
  [f], (),             [g], [L],
)
Rendered image
#import "@preview/tablex:0.0.7": tablex, colspanx, rowspanx

#tablex(
  columns: 3,
  map-hlines: h => (..h, stroke: blue),
  map-vlines: v => (..v, stroke: green + 2pt),
  colspanx(2)[a], (),  [b],
  [c], rowspanx(2)[d], [ed],
  [f], (),             [g]
)
Rendered image
#import "@preview/tablex:0.0.7": tablex, colspanx, rowspanx

#tablex(
  columns: 4,
  auto-vlines: true,

  // make all cells italicized
  map-cells: cell => {
    (..cell, content: emph(cell.content))
  },

  // add some arbitrary content to entire rows
  map-rows: (row, cells) => cells.map(c =>
    if c == none {
      c  // keeping 'none' is important
    } else {
      (..c, content: [#c.content\ *R#row*])
    }
  ),

  // color cells based on their columns
  // (using 'fill: (column, row) => color' also works
  // for this particular purpose)
  map-cols: (col, cells) => cells.map(c =>
    if c == none {
      c
    } else {
      (..c, fill: if col < 2 { blue } else { yellow })
    }
  ),

  colspanx(2)[a], (),  [b], [J],
  [c], rowspanx(2)[dd], [e], [K],
  [f], (),             [g], [L],
)
Rendered image
#import "@preview/tablex:0.0.7": gridx

#gridx(
  columns: 3,
  rows: 6,
  fill: (col, row) => (blue, red, green).at(calc.rem(row + col - 1, 3)),
  map-cols: (col, cells) => {
    let last = cells.last()
    last.content = [
      #cells.slice(0, cells.len() - 1).fold(0, (acc, c) => if c != none { acc + eval(c.content.text) } else { acc })
    ]
    last.fill = aqua
    cells.last() = last
    cells
  },
  [0], [5], [10],
  [1], [6], [11],
  [2], [7], [12],
  [3], [8], [13],
  [4], [9], [14],
  [s], [s], [s]
)
Rendered image

Tada: data manipulation

#import "@preview/tada:0.1.0"

#let column-data = (
  name: ("Bread", "Milk", "Eggs"),
  price: (1.25, 2.50, 1.50),
  quantity: (2, 1, 3),
)
#let record-data = (
  (name: "Bread", price: 1.25, quantity: 2),
  (name: "Milk", price: 2.50, quantity: 1),
  (name: "Eggs", price: 1.50, quantity: 3),
)
#let row-data = (
  ("Bread", 1.25, 2),
  ("Milk", 2.50, 1),
  ("Eggs", 1.50, 3),
)

#import tada: TableData, to-tablex
#let td = TableData(data: column-data)
// Equivalent to:
#let td2 = tada.from-records(record-data)
// _Not_ equivalent to (since field names are unknown):
#let td3 = tada.from-rows(row-data)

#to-tablex(td)
#to-tablex(td2)
#to-tablex(td3)
Rendered image

Tablem: markdown tables

See documentation there

Render markdown tables in Typst.

#import "@preview/tablem:0.1.0": tablem

#tablem[
  | *Name* | *Location* | *Height* | *Score* |
  | ------ | ---------- | -------- | ------- |
  | John   | Second St. | 180 cm   |  5      |
  | Wally  | Third Av.  | 160 cm   |  10     |
]
Rendered image

Custom render

#import "@preview/tablex:0.0.6": tablex, hlinex
#import "@preview/tablem:0.1.0": tablem

#let three-line-table = tablem.with(
  render: (columns: auto, ..args) => {
    tablex(
      columns: columns,
      auto-lines: false,
      align: center + horizon,
      hlinex(y: 0),
      hlinex(y: 1),
      ..args,
      hlinex(),
    )
  }
)

#three-line-table[
  | *Name* | *Location* | *Height* | *Score* |
  | ------ | ---------- | -------- | ------- |
  | John   | Second St. | 180 cm   |  5      |
  | Wally  | Third Av.  | 160 cm   |  10     |
]
Rendered image

Tbl: compact syntax

Compact syntax and some new features:

#import "@preview/tbl:0.0.4"
#show: tbl.template.with(box: true, tab: "|")

```tbl
      R | L
      R   N.
software|version
_
     AFL|2.39b
    Mutt|1.8.0
    Ruby|1.8.7.374
TeX Live|2015
```
Rendered image

Code

codly

See docs there

#import "@preview/codly:0.1.0": codly-init, codly, disable-codly
#show: codly-init.with()

#codly(languages: (
        typst: (name: "Typst", color: rgb("#41A241"), icon: none),
    ),
    breakable: false
)

```typst
#import "@preview/codly:0.1.0": codly-init
#show: codly-init.with()
```

// Still formatted!
```rust
pub fn main() {
    println!("Hello, world!");
}
```

#disable-codly()
Rendered image

Codelst

#import "@preview/codelst:2.0.0": sourcecode

#sourcecode[```typ
#show "ArtosFlow": name => box[
  #box(image(
    "logo.svg",
    height: 0.7em,
  ))
  #name
]

This report is embedded in the
ArtosFlow project. ArtosFlow is a
project of the Artos Institute.
```]
Rendered image

Presentations

Polylux

See polylux book

// Get Polylux from the official package repository
#import "@preview/polylux:0.3.1": *

// Make the paper dimensions fit for a presentation and the text larger
#set page(paper: "presentation-16-9")
#set text(size: 25pt)

// Use #polylux-slide to create a slide and style it using your favourite Typst functions
#polylux-slide[
  #align(horizon + center)[
    = Very minimalist slides

    A lazy author

    July 23, 2023
  ]
]

#polylux-slide[
  == First slide

  Some static text on this slide.
]

#polylux-slide[
  == This slide changes!

  You can always see this.
  // Make use of features like #uncover, #only, and others to create dynamic content
  #uncover(2)[But this appears later!]
]
Rendered image
Rendered image
Rendered image
Rendered image

Slydst

See the documentation there.

Much more simpler and less powerful than polulyx:

#import "@preview/slydst:0.1.0": *

#show: slides.with(
  title: "Insert your title here", // Required
  subtitle: none,
  date: none,
  authors: (),
  layout: "medium",
  title-color: none,
)

== Outline

#outline()

= First section

== First slide

#figure(rect(width: 60%), caption: "Caption")

#v(1fr)

#lorem(20)

#definition(title: "An interesting definition")[
  #lorem(20)
]
Rendered image
Rendered image
Rendered image
Rendered image

Layouting

General useful things.

Pinit: relative place by pins

The idea of pinit is pinning pins on the normal flow of the text, and then placing the content relative to pins.

#import "@preview/pinit:0.1.3": *
#set page(height: 6em, width: 20em)

#set text(size: 24pt)

A simple #pin(1)highlighted text#pin(2).

#pinit-highlight(1, 2)

#pinit-point-from(2)[It is simple.]
Rendered image

More complex example:

#import "@preview/pinit:0.1.3": *

// Pages
#set page(paper: "presentation-4-3")
#set text(size: 20pt)
#show heading: set text(weight: "regular")
#show heading: set block(above: 1.4em, below: 1em)
#show heading.where(level: 1): set text(size: 1.5em)

// Useful functions
#let crimson = rgb("#c00000")
#let greybox(..args, body) = rect(fill: luma(95%), stroke: 0.5pt, inset: 0pt, outset: 10pt, ..args, body)
#let redbold(body) = {
  set text(fill: crimson, weight: "bold")
  body
}
#let blueit(body) = {
  set text(fill: blue)
  body
}

// Main body
#block[
  = Asymptotic Notation: $O$

  Use #pin("h1")asymptotic notations#pin("h2") to describe asymptotic efficiency of algorithms.
  (Ignore constant coefficients and lower-order terms.)

  #greybox[
    Given a function $g(n)$, we denote by $O(g(n))$ the following *set of functions*:
    #redbold(${f(n): "exists" c > 0 "and" n_0 > 0, "such that" f(n) <= c dot g(n) "for all" n >= n_0}$)
  ]

  #pinit-highlight("h1", "h2")

  $f(n) = O(g(n))$: #pin(1)$f(n)$ is *asymptotically smaller* than $g(n)$.#pin(2)

  $f(n) redbold(in) O(g(n))$: $f(n)$ is *asymptotically* #redbold[at most] $g(n)$.

  #pinit-line(stroke: 3pt + crimson, start-dy: -0.25em, end-dy: -0.25em, 1, 2)

  #block[Insertion Sort as an #pin("r1")example#pin("r2"):]

  - Best Case: $T(n) approx c n + c' n - c''$ #pin(3)
  - Worst case: $T(n) approx c n + (c' \/ 2) n^2 - c''$ #pin(4)

  #pinit-rect("r1", "r2")

  #pinit-place(3, dx: 15pt, dy: -15pt)[#redbold[$T(n) = O(n)$]]
  #pinit-place(4, dx: 15pt, dy: -15pt)[#redbold[$T(n) = O(n)$]]

  #blueit[Q: Is $n^(3) = O(n^2)$#pin("que")? How to prove your answer#pin("ans")?]

  #pinit-point-to("que", fill: crimson, redbold[No.])
  #pinit-point-from("ans", body-dx: -150pt)[
    Show that the equation $(3/2)^n >= c$ \
    has infinitely many solutions for $n$.
  ]
]
Rendered image

Margin notes

#import "@preview/drafting:0.1.1": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

= Margin Notes
== Setup
Unfortunately `typst` doesn't expose margins to calling functions, so you'll need to set them explicitly. This is done using `set-page-properties` *before you place any content*:

// At the top of your source file
// Of course, you can substitute any margin numbers you prefer
// provided the page margins match what you pass to `set-page-properties`

== The basics
#lorem(20)
#margin-note(side: left)[Hello, world!]
#lorem(10)
#margin-note[Hello from the other side]

#lorem(25)
#margin-note[When notes are about to overlap, they're automatically shifted]
#margin-note(stroke: aqua + 3pt)[To avoid collision]
#lorem(25)

#let caution-rect = rect.with(inset: 1em, radius: 0.5em, fill: orange.lighten(80%))
#inline-note(rect: caution-rect)[
  Be aware that notes will stop automatically avoiding collisions if 4 or more notes
  overlap. This is because `typst` warns when the layout doesn't resolve after 5 attempts
  (initial layout + adjustment for each note)
]
Rendered image
#import "@preview/drafting:0.1.1": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

== Adjusting the default style
All function defaults are customizable through updating the module state:

#lorem(4) #margin-note(dy: -2em)[Default style]
#set-margin-note-defaults(stroke: orange, side: left)
#lorem(4) #margin-note[Updated style]


Even deeper customization is possible by overriding the default `rect`:

#import "@preview/colorful-boxes:1.1.0": stickybox

#let default-rect(stroke: none, fill: none, width: 0pt, content) = {
  stickybox(rotation: 30deg, width: width/1.5, content)
}
#set-margin-note-defaults(rect: default-rect, stroke: none, side: right)

#lorem(20)
#margin-note(dy: -25pt)[Why not use sticky notes in the margin?]

// Undo changes from last example
#set-margin-note-defaults(rect: rect, stroke: red)

== Multiple document reviewers
#let reviewer-a = margin-note.with(stroke: blue)
#let reviewer-b = margin-note.with(stroke: purple)
#lorem(20)
#reviewer-a[Comment from reviewer A]
#lorem(15)
#reviewer-b(side: left)[Comment from reviewer B]

== Inline Notes
#lorem(10)
#inline-note[The default inline note will split the paragraph at its location]
#lorem(10)
/*
// Should work, but doesn't? Created an issue in repo.
#inline-note(par-break: false, stroke: (paint: orange, dash: "dashed"))[
  But you can specify `par-break: false` to prevent this
]
*/
#lorem(10)
Rendered image
#import "@preview/drafting:0.1.1": *

#let (l-margin, r-margin) = (1in, 2in)
#set page(
  margin: (left: l-margin, right: r-margin, rest: 0.1in),
)
#set-page-properties(margin-left: l-margin, margin-right: r-margin)

== Hiding notes for print preview
#set-margin-note-defaults(hidden: true)

#lorem(20)
#margin-note[This will respect the global "hidden" state]
#margin-note(hidden: false, dy: -2.5em)[This note will never be hidden]

= Positioning
== Precise placement: rule grid
Need to measure space for fine-tuned positioning? You can use `rule-grid` to cross-hatch
the page with rule lines:

#rule-grid(width: 10cm, height: 3cm, spacing: 20pt)
#place(
  dx: 180pt,
  dy: 40pt,
  rect(fill: white, stroke: red, width: 1in, "This will originate at (180pt, 40pt)")
)

// Optionally specify divisions of the smallest dimension to automatically calculate
// spacing
#rule-grid(dx: 10cm + 3em, width: 3cm, height: 1.2cm, divisions: 5, square: true,  stroke: green)

// The rule grid doesn't take up space, so add it explicitly
#v(3cm + 1em)

== Absolute positioning
What about absolutely positioning something regardless of margin and relative location? `absolute-place` is your friend. You can put content anywhere:

#context {
  let (dx, dy) = (25%, here().position().y)
  let content-str = (
    "This absolutely-placed box will originate at (" + repr(dx) + ", " + repr(dy) + ")"
    + " in page coordinates"
  )
  absolute-place(
    dx: dx, dy: dy,
    rect(
      fill: green.lighten(60%),
      radius: 0.5em,
      width: 2.5in,
      height: 0.5in,
      [#align(center + horizon, content-str)]
    )
  )
}
#v(1in)

The "rule-grid" also supports absolute placement at the top-left of the page by passing `relative: false`. This is helpful for "rule"-ing the whole page.
Rendered image

Dropped capitals

Get more info here

Basic usage

#import "@preview/droplet:0.1.0": dropcap

#dropcap(gap: -2pt, hanging-indent: 8pt)[
  #lorem(42)
]
Rendered image

Extended styling

#import "@preview/droplet:0.1.0": dropcap

#dropcap(
  height: 2,
  justify: true,
  gap: 6pt,
  transform: letter => style(styles => {
    let height = measure(letter, styles).height

    grid(columns: 2, gutter: 6pt,
      align(center + horizon, text(blue, letter)),
      // Use "place" to ignore the line's height when
      // the font size is calculated later on.
      place(horizon, line(
        angle: 90deg,
        length: height + 6pt,
        stroke: blue.lighten(40%) + 1pt
      )),
    )
  })
)[
  #lorem(42)
]
Rendered image

Headings for actual current chapter

See hydra

#import "@preview/hydra:0.2.0": hydra

#set page(header: hydra() + line(length: 100%))
#set heading(numbering: "1.1")
#show heading.where(level: 1): it => pagebreak(weak: true) + it

= Introduction
#lorem(750)

= Content
== First Section
#lorem(500)
== Second Section
#lorem(250)
== Third Section
#lorem(500)

= Annex
#lorem(10)
Rendered image
Rendered image
Rendered image
Rendered image
Rendered image

Wrapping figures

The better native support for wrapping is planned, however, something is already possible via package:

#import "@preview/wrap-it:0.1.0": wrap-content, wrap-top-bottom

#set par(justify: true)
#let fig = figure(
  rect(fill: teal, radius: 0.5em, width: 8em),
  caption: [A figure],
)
#let body = lorem(40)
#wrap-content(fig, body)

#wrap-content(
  fig,
  body,
  align: bottom + right,
  column-gutter: 2em
)

#let boxed = box(fig, inset: 0.5em)
#wrap-content(boxed)[
  #lorem(40)
]

#let fig2 = figure(
  rect(fill: lime, radius: 0.5em),
  caption: [Another figure],
)
#wrap-top-bottom(boxed, fig2, lorem(60))
Rendered image
Limitations: non-ideal spacing near warping, only top-bottom left/right are supported.

Misc

Formatting strings

oxifmt, general purpose string formatter

#import "@preview/oxifmt:0.2.0": strfmt
#strfmt("I'm {}. I have {num} cars. I'm {0}. {} is {{cool}}.", "John", "Carl", num: 10) \
#strfmt("{0:?}, {test:+012e}, {1:-<#8x}", "hi", -74, test: 569.4) \
#strfmt("{:_>+11.5}", 59.4) \
#strfmt("Dict: {:!<10?}", (a: 5))
Rendered image
#import "@preview/oxifmt:0.2.0": strfmt
#strfmt("First: {}, Second: {}, Fourth: {3}, Banana: {banana} (brackets: {{escaped}})", 1, 2.1, 3, label("four"), banana: "Banana!!")\
#strfmt("The value is: {:?} | Also the label is {:?}", "something", label("label"))\
#strfmt("Values: {:?}, {1:?}, {stuff:?}", (test: 500), ("a", 5.1), stuff: [a])\
#strfmt("Left5 {:_<5}, Right6 {:*>6}, Center10 {centered: ^10?}, Left3 {tleft:_<3}", "xx", 539, tleft: "okay", centered: [a])\
Rendered image
#import "@preview/oxifmt:0.2.0": strfmt
#repr(strfmt("Left-padded7 numbers: {:07} {:07} {:07} {3:07}", 123, -344, 44224059, 45.32))\
#strfmt("Some numbers: {:+} {:+08}; With fill and align: {:_<+8}; Negative (no-op): {neg:+}", 123, 456, 4444, neg: -435)\
#strfmt("Bases (10, 2, 8, 16(l), 16(U):) {0} {0:b} {0:o} {0:x} {0:X} | W/ prefixes and modifiers: {0:#b} {0:+#09o} {0:_>+#9X}", 124)\
#strfmt("{0:.8} {0:.2$} {0:.potato$}", 1.234, 0, 2, potato: 5)\
#strfmt("{0:e} {0:E} {0:+.9e} | {1:e} | {2:.4E}", 124.2312, 50, -0.02)\
#strfmt("{0} {0:.6} {0:.5e}", 1.432, fmt-decimal-separator: ",")
Rendered image

name-it, integer to text

#import "@preview/name-it:0.1.0": name-it

- #name-it(2418345)
Rendered image

nth, Nth element

#import "@preview/nth:0.2.0": nth
#nth(3), #nth(5), #nth(2421)
Rendered image

Counting words

Wordometr

#import "@preview/wordometer:0.1.0": word-count, total-words

#show: word-count

In this document, there are #total-words words all up.

#word-count(total => [
  The number of words in this block is #total.words
  and there are #total.characters letters.
])
Rendered image

Excluding elements

You can exclude elements by name (e.g., "caption"), function (e.g., figure.caption), where-selector (e.g., raw.where(block: true)), or label (e.g., <no-wc>).

#import "@preview/wordometer:0.1.0": word-count, total-words

#show: word-count.with(exclude: (heading.where(level: 1), strike))

= This Heading Doesn't Count
== But I do!

In this document #strike[(excluding me)], there are #total-words words all up.

#word-count(total => [
  You can exclude elements by label, too.
  #[That was #total-words, excluding this sentence!] <no-wc>
], exclude: <no-wc>)
Rendered image

External

These are not official packages. Maybe once they will become one.

However, they may be very useful.

Treemap display

Code Link

Treemap diagram

Typstonomicon, or The Code You Should Not Write

Totally cursed examples with lots of quires, measure and other things to hack around current Typst limitations. Generally you should use this code only if you really need it.

Code in this chapter may break in lots of circumstances and debugging it will be very painful. You are warned.

I think that this chapter will slowly die as Typst matures.

Image with original size

This function renders image with the size it "naturally" has.

Note: starting from v0.11, Typst tries using default image size when width and height are auto. It only uses container's size if the image doesn't fit. So this code is more like a legacy, but still may be useful.

This works because measure conceptually places the image onto a page with infinite size and then the image defaults to 1pt per pixel instead of becoming infinitely larger itself.

// author: laurmaedje
#let natural-image(..args) = style(styles => {
  let (width, height) = measure(image(..args), styles)
  image(..args, width: width, height: height)
})

#image("../tiger.jpg")
#natural-image("../tiger.jpg")
Rendered image

Word count

This chapter is deprecated now. It will be removed soon.
## Recommended solution

Use wordometr package:

#import "@preview/wordometer:0.1.0": word-count, total-words

#show: word-count

In this document, there are #total-words words all up.

#word-count(total => [
  The number of words in this block is #total.words
  and there are #total.characters letters.
])
Rendered image

Just count all words in document

// original author: laurmaedje
#let words = counter("words")
#show regex("\p{L}+"): it => it + words.step()

== A heading
#lorem(50)

=== Strong chapter
#strong(lorem(25))

// it is ignoring comments

#align(right)[(#words.display() words)]
Rendered image

Count only some elements, ignore others

// original author: jollywatt
#let count-words(it) = {
    let fn = repr(it.func())
    if fn == "sequence" { it.children.map(count-words).sum() }
    else if fn == "text" { it.text.split().len() }
    else if fn in ("styled") { count-words(it.child) }
    else if fn in ("highlight", "item", "strong", "link") { count-words(it.body) }
    else if fn in ("footnote", "heading", "equation") { 0 }
    else { 0 }
}

#show: rest => {
    let n = count-words(rest)
    rest + align(right, [(#n words)])
}

== A heading (shouldn't be counted)
#lorem(50)

=== Strong chapter
#strong(lorem(25)) // counted too!
Rendered image

Try & Catch

// author: laurmaedje
// Renders an image or a placeholder if it doesn't exist.
// Don’t try this at home, kids!
#let maybe-image(path, ..args) = locate(loc => {
  let path-label = label(path)
  let first-time = query(locate(_ => {}).func(), loc).len() == 0
  if first-time or query(path-label, loc).len() > 0 {
    [#image(path, ..args)#path-label]
  } else {
    rect(width: 50%, height: 5em, fill: luma(235), stroke: 1pt)[
      #set align(center + horizon)
      Could not find #raw(path)
    ]
  }
})

#maybe-image("../tiger.jpg")
#maybe-image("../tiger1.jpg")
Rendered image

Breakpoints on broken blocks

Implementation with table headers & footers

See a demo project (more comments, I stripped some of them) there.

/// author: wrzian

// Underlying counter and zig-zag functions
#let counter-family(id) = {
  let parent = counter(id)
  let parent-step() = parent.step()
  let get-child() = counter(id + str(parent.get().at(0)))
  return (parent-step, get-child)
}

// A fun zig-zag line!
#let zig-zag(fill: black, rough-width: 6pt, height: 4pt, thick: 1pt, angle: 0deg) = {
  layout((size) => {
    // Use layout to get the size and measure our horizontal distance
    // Then get the per-zigzag width with some maths.
    let count = int(calc.round(size.width / rough-width))
    // Need to add extra thickness since we join with `h(-thick)`
    let width = thick + (size.width - thick) / count
    // One zig and one zag:
    let zig-and-zag = {
      let line-stroke = stroke(thickness: thick, cap: "round", paint: fill)
      let top-left = (thick/2, thick/2)
      let bottom-mid = (width/2, height - thick/2)
      let top-right = (width - thick/2, thick/2)
      let zig = line(stroke: line-stroke, start: top-left, end: bottom-mid)
      let zag = line(stroke: line-stroke, start: bottom-mid, end: top-right)
      box(place(zig) + place(zag), width: width, height: height, clip: true)
    }
    let zig-zags = ((zig-and-zag,) * count).join(h(-thick))
    rotate(zig-zags, angle)
  })
}

// ---- Define split-box ---- //

// Customizable options for a split-box border:
#let default-border = (
  // The starting and ending lines
  above: line(length: 100%),
  below: line(length: 100%),
  // Lines to put between the box over multiple pages
  btwn-above: line(length: 100%, stroke: (dash:"dotted")),
  btwn-below: line(length: 100%, stroke: (dash:"dotted")),
  // Left/right lines
  // These *must* use `grid.vline()`, otherwise you will get an error.
  // To remove the lines, set them to: `grid.vline(stroke: none)`.
  // You could probably configure this better with a rowspan, but I'm lazy.
  left: grid.vline(),
  right: grid.vline(),
)

// Create a box for content which spans multiple pages/columns and
// has custom borders above and below the column-break.
#let split-box(
  // Set the border dictionary, see `default-border` above for options
  border: default-border,
  // The cell to place content in, this should resolve to a `grid.cell`
  cell: grid.cell.with(inset: 5pt),
  // The last positional arg or args are your actual content
  // Any extra named args will be sent to the underlying grid when called
  // This is useful for fill, align, etc.
  ..args
) = {
  // See `utils.typ` for more info.
  let (parent-step, get-child) = counter-family("split-box-unique-counter-string")
  parent-step() // Place the parent counter once.
  // Keep track of each time the header is placed on a page.
  // Then check if we're at the first placement (for header) or the last (footer)
  // If not, we'll use the 'between' forms of the  border lines.
  let border-above = context {
    let header-count = get-child()
    header-count.step()
    context if header-count.get() == (1,) { border.above } else { border.btwn-above }
  }
  let border-below = context {
    let header-count = get-child()
    if header-count.get() == header-count.final() { border.below } else { border.btwn-below }
  }
  // Place the grid!
  grid(
    ..args.named(),
    columns: 3,
    border.left,
    grid.header(border-above , repeat: true),
    ..args.pos().map(cell),
    grid.footer(border-below, repeat: true),
    border.right,
  )
}

// ---- Examples ---- //

#set page(width: 7.2in, height: 3in, columns: 6)

// Tada!
#split-box[
  #lorem(20)
]

// And here's a fun example:

#let fun-border = (
  // gradients!
  above: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow)),
  below: line(length: 100%, stroke: 2pt + gradient.linear(..color.map.rainbow, angle: 180deg)),
  // zig-zags!
  btwn-above: move(dy: +2pt, zig-zag(fill: blue, angle: 3deg)),
  btwn-below: move(dy: -2pt, zig-zag(fill: orange, angle: 177deg)),
  left: grid.vline(stroke: (cap: "round", paint: purple)),
  right: grid.vline(stroke: (cap: "round", paint: purple)),
)

#split-box(border: fun-border)[
  #lorem(25)
]

// And some more tame friends:

#split-box(border: (
  above: move(dy: -0.5pt, line(length: 100%)),
  below: move(dy: +0.5pt, line(length: 100%)),
  // zig-zags!
  btwn-above: move(dy: -1.1pt, zig-zag()),
  btwn-below: move(dy: +1.1pt, zig-zag(angle: 180deg)),
  left: grid.vline(stroke: (cap: "round")),
  right: grid.vline(stroke: (cap: "round")),
))[
  #lorem(10)
]

#split-box(
  border: (
    above: line(length: 100%, stroke: luma(50%)),
    below: line(length: 100%, stroke: luma(50%)),
    btwn-above: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    btwn-below: line(length: 100%, stroke: (dash: "dashed", paint: luma(50%))),
    left: grid.vline(stroke: none),
    right: grid.vline(stroke: none),
  ),
  cell: grid.cell.with(inset: 5pt, fill: color.yellow.saturate(-85%))
)[
  #lorem(20)
]
Rendered image

Implementation via headers, footers and stated

Limitations: works only with one-column layout and one break.
#let countBoundaries(loc, fromHeader) = {
  let startSelector = selector(label("boundary-start"))
  let endSelector = selector(label("boundary-end"))

  if fromHeader {
    // Count down from the top of the page
    startSelector = startSelector.after(loc)
    endSelector = endSelector.after(loc)
  } else {
    // Count up from the bottom of the page
    startSelector = startSelector.before(loc)
    endSelector = endSelector.before(loc)
  }

  let startMarkers = query(startSelector, loc)
  let endMarkers = query(endSelector, loc)
  let currentPage = loc.position().page

  let pageStartMarkers = startMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  let pageEndMarkers = endMarkers.filter(elem =>
    elem.location().position().page == currentPage)

  (start: pageStartMarkers.len(), end: pageEndMarkers.len())
}

#set page(
  margin: 2em,
  // ... other page setup here ...
  header: locate(loc => {
    let boundaryCount = countBoundaries(loc, true)

    if boundaryCount.end > boundaryCount.start {
      // Decorate this header with an opening decoration
      [Block break top: $-->$]
    }
  }),
  footer: locate(loc => {
    let boundaryCount = countBoundaries(loc, false)

    if boundaryCount.start > boundaryCount.end {
      // Decorate this footer with a closing decoration
      [Block break end: $<--$]
    }
  })
)

#let breakable-block(body) = block({
  [
    #metadata("boundary") <boundary-start>
  ]
  stack(
    // Breakable list content goes here
    body
  )
  [
    #metadata("boundary") <boundary-end>
  ]
})

#set page(height: 10em)

#breakable-block[
    #([Something \ ]*10)
]
Rendered image
Rendered image

Extracting plain text

// original author: ntjess
#let stringify-by-func(it) = {
  let func = it.func()
  return if func in (parbreak, pagebreak, linebreak) {
    "\n"
  } else if func == smartquote {
    if it.double { "\"" } else { "'" } // "
  } else if it.fields() == (:) {
    // a fieldless element is either specially represented (and caught earlier) or doesn't have text
    ""
  } else {
    panic("Not sure how to handle type `" + repr(func) + "`")
  }
}

#let plain-text(it) = {
  return if type(it) == str {
    it
  } else if it == [ ] {
    " "
  } else if it.has("children") {
    it.children.map(plain-text).join()
  } else if it.has("body") {
    plain-text(it.body)
  } else if it.has("text") {
    if type(it.text) == "string" {
      it.text
    } else {
      plain-text(it.text)
    }
  } else {
    // remove this to ignore all other non-text elements
    stringify-by-func(it)
  }
}

#plain-text(`raw inline text`)

#plain-text(highlight[Highlighted text])

#plain-text[List
  - With
  - Some
  - Elements

  + And
  + Enumerated
  + Too
]

#plain-text(underline[Underlined])

#plain-text($sin(x + y)$)

#for el in (
  circle,
  rect,
  ellipse,
  block,
  box,
  par,
  raw.with(block: true),
  raw.with(block: false),
  heading,
) {
  plain-text(el(repr(el)))
  linebreak()
}

// Some empty elements
#plain-text(circle())
#plain-text(line())

#for spacer in (linebreak, pagebreak, parbreak) {
  plain-text(spacer())
}
Rendered image

Horizontally align something with something

// author: tabiasgeehuman
#let inline-with(select, content) = context {
  let target = query(
    selector(select)
  ).last().location().position().x
  let current = here().position().x

  box(inset: (x: target - current + 0.3em), content)
}

#let inline-label(name) = [#line(length: 0%) #name]

#inline-with(selector(<start-c>))[= Common values]
#align(left, box[$
    #inline-label(<start-c>) "Circles"(0) =& 0 \
    lim_(x -> 1) "Circles"(0) =& 0
$])
Rendered image

Create zero-level chapters

// author: tinger

#let chapter = figure.with(
  kind: "chapter",
  // same as heading
  numbering: none,
  // this cannot use auto to translate this automatically as headings can, auto also means something different for figures
  supplement: "Chapter",
  // empty caption required to be included in outline
  caption: [],
)

// emulate element function by creating show rule
#show figure.where(kind: "chapter"): it => {
  set text(22pt)
  counter(heading).update(0)
  if it.numbering != none { strong(it.counter.display(it.numbering)) } + [ ] + strong(it.body)
}

// no access to element in outline(indent: it => ...), so we must do indentation in here instead of outline
#show outline.entry: it => {
  if it.element.func() == figure {
    // we're configuring chapter printing here, effectively recreating the default show impl with slight tweaks
    let res = link(it.element.location(), 
      // we must recreate part of the show rule from above once again
      if it.element.numbering != none {
        numbering(it.element.numbering, ..it.element.counter.at(it.element.location()))
      } + [ ] + it.element.body
    )

    if it.fill != none {
      res += [ ] + box(width: 1fr, it.fill) + [ ] 
    } else {
      res += h(1fr)
    }

    res += link(it.element.location(), it.page)
    strong(res)
  } else {
    // we're doing indenting here
    h(1em * it.level) + it
  }
}

// new target selector for default outline
#let chapters-and-headings = figure.where(kind: "chapter", outlined: true).or(heading.where(outlined: true))

//
// start of actual doc prelude
//

#set heading(numbering: "1.")

// can't use set, so we reassign with default args
#let chapter = chapter.with(numbering: "I")

// an example of a "show rule" for a chapter
// can't use chapter because it's not an element after using .with() anymore
#show figure.where(kind: "chapter"): set text(red)

//
// start of actual doc
//

// as you can see these are not elements like headings, which makes the setup a bit harder
// because the chapters are not headings, the numbering does not include their chapter, but could using a show rule for headings

#outline(target: chapters-and-headings)

#chapter[Chapter]
= Chap Heading
== Sub Heading

#chapter[Chapter again]
= Chap Heading
= Chap Heading
== Sub Heading
=== Sub Sub Heading
== Sub Heading

#chapter[Chapter yet again]
Rendered image

Make all math display math

May slightly interfere with math blocks.
// author: eric1102
#show math.equation: it => {
  if it.body.fields().at("size", default: none) != "display" {
    return math.display(it)
  }
  it
}

Inline math: $sum_(n=0)^oo e^(x^2 - n/x^2)$\
Some other text on new line.


$
sum_(n=0)^oo e^(x^2 - n/x^2)
$
Rendered image