Rheo

rheo-context

When Rheo compiles a project, it prepends a small binding to the top of every vertebra:

#let rheo-context() = (
  handle: "chapters:intro",
  ..sys.inputs.rheo-context,
)

Only the handle is per-file; it is composed with the format-global values (spine, spine-flat, target, ext) spread from sys.inputs.rheo-context. This exposes Rheo’s view of the project to your Typst code: which file this is, and every file in the spine alongside it. Rheo injects a distinct binding into each file, so every file sees its own values.

rheo-context() is a zero-arg function returning a dictionary; call it to read Rheo’s view. It is not a Typst context value, and it needs no #context keyword (sys.inputs is non-contextual). Any Typst code in the file can use it directly:

This page's handle is #rheo-context().handle.

Fields

The shape of rheo-context is designed to be extensible — more fields may be added in future versions.

FieldDescription
handleThis file’s handle — its :-separated identifier, the same handle used for cross-file links.
spine

A tree (a forest — an array of top-level nodes) mirroring the project’s directory and section structure. Each node is a dictionary with five fields:

  • title — the node’s title.
  • handle — the vertebra’s handle, or none for a group node (a directory or section with no landing file).
  • path — the vertebra’s path relative to the project root, or none for a group node.
  • metadata — the vertebra’s document metadata dictionary (an empty dictionary for a group node, which has no source file).
  • children — an array of child nodes, recursing to arbitrary depth (empty for a node with no descendants).

A group node is not itself clickable — there’s nothing to link to — but its title still labels the section. Walk children to build nested navigation.

spine-flat

A flat list of every clickable vertebra in spine order (group nodes are omitted). Each entry is a dictionary with four fields:

  • handle — the vertebra’s handle.
  • path — its path, relative to the project root.
  • title — its title.
  • metadata — the vertebra’s document metadata dictionary.

Reach for it when a flat sequence beats walking the tree — prev/next navigation, page counts, and the like. A file reads its own metadata by finding its handle in spine-flat.

targetThe output format Rheo is compiling for — "html" or "epub". It is absent for PDF, where Typst’s native target() returns "paged". The value is the same for every vertebra. In authored files, prefer Typst’s own target() (which Rheo polyfills to return this value) over reading the field directly — target() works everywhere, e.g. #if target() == "epub" [ ... ].
extThe output file extension for the format being compiled — "html" for HTML, "xhtml" for EPUB — sourced from the format plugin. Like target, it is format-global (the same for every vertebra) and absent for PDF, where there are no per-page files. This is the value Rheo itself uses to build depth-relative cross-vertebra link hrefs.

Vertebra metadata

Every vertebra in spine and spine-flat carries a metadata dictionary harvested from its #set document(...) rule. It is format-global — the same values are present across PDF, HTML, and EPUB. Each key appears only when the corresponding argument is set; a file with no #set document(...) gets an empty dictionary.

The date key is present only for a static date — #set document(date: datetime(year: ..., ...)). It is absent when the date is none, auto, datetime.today(), or a partial datetime (the same resolution rules the Atom feed uses for entry timestamps).

// This file's own metadata, looked up by handle.
#let me = rheo-context().spine-flat.find(v => v.handle == rheo-context().handle)
#if "date" in me.metadata [
  Published #me.metadata.date.display("[year]-[month]-[day]").
]

Example: a table of contents

Because spine-flat lists every clickable vertebra in order, a template or package can build a flat table of contents from it directly:

#for entry in rheo-context().spine-flat [
  - #link(label(entry.handle))[#entry.title]
]

Each file receives the same spine-flat, so this produces a consistent list across the whole project, while rheo-context().handle lets a template highlight the current page.

A nested table of contents — one that reflects the project’s directory and section groups — has to walk spine instead, recursing into children:

#let toc(nodes) = {
  for node in nodes [
    - #if node.handle != none [
        #link(label(node.handle))[#node.title]
      ] else [
        #node.title
      ]
      #if node.children.len() > 0 [
        #toc(node.children)
      ]
  ]
}
#toc(rheo-context().spine)