Introduction

Welcome to Elembic's docs! Elembic is a framework that lets you create your own elements and types in Typst, including support for type-checking and casting on the fly.

Elembic supports Typst 0.11.0 or later.

Elements

Elembic allows you to create custom elements, which are reusable visual components of the document with configurable properties (through styling).

Elembic's elements support styling through show and set rules, which allow changing the default values of element properties in bulk. They are scoped (that is, lose effect at the end of the current #[] block) and do not use state or counter by default, making it comparatively fast.

However, there are some important limitations, so make sure to read the "Limitations" page, which explains them in detail.

In addition, Elembic includes some extras not natively available such as revoke and reset rules (which can be used to temporarily "undo" the effect of an earlier set rule, or group of set rules, for a limited scope). Also, Elembic can guarantee type-safety and more helpful errors by typechecking inputs to elements' fields.

Elembic also supports custom reference and outline support for your element, per-element counter support, get rules (accessing the current defaults of fields, considering set rules so far); folding (e.g. setting a stroke to 4pt and, later, to red will result in a combined stroke of 4pt + red); scopes (you can have some constants and functions attached to your element); and so on. Read the chapters under "Elements" to learn more.

Types

Elembic ships with a considerably flexible type system through its types module, with its main purpose being to typecheck fields, but it can be used anywhere you want through the e.types.cast(value, type) function.

Not only does Elembic support using and combining Typst-native types (e.g. a field can take e.types.union(int, stroke) to support integers or strokes), but it also supports declaring your own custom, nominal types. They are represented as dictionaries, but are strongly-typed, such that a dictionary with the same fields won't cast to your custom type and vice-versa by default (unless you specify otherwise). For example, a data type representing a person won't cast to another type owner even if they share the same fields.

Custom types optionally support arbitrary casting from other types. For example, you may allow automatic casts from integers and floats to your custom type. This is mostly useful when creating "ad-hoc" types for certain elements with fully customized behavior. You can read more in the "Custom types" chapter.

License

Elembic is licensed under MIT or Apache-2.0, at your option.

About

Some basic information about Elembic.

Installation

Through the package manager

Just import the latest elembic version and you're ready to go!

#import "@preview/elembic:1.1.0" as e

#let element = e.element.declare(...)
#show: e.set_(element, data: 20)
// ...

For testing and development

If you'd like to contribute or try out the latest development version, Elembic may be installed as a local package (or by copying it to your project in the web app).

To install Elembic as a local package in your system, see https://github.com/typst/packages?tab=readme-ov-file#local-packages for instructions.

In particular, it involves downloading Elembic's files from either GitHub (pgbiel/elembic) or Codeberg (pgbiel/elembic) and then copying it to $LOCAL_PACKAGES_DIR/elembic/1.1.0.

If you're using a Linux platform, there is the following one-liner command to install the latest development version (note: does not remove a prior installation):

pkgbase="${XDG_DATA_HOME:-$HOME/.local/share}/typst/packages/local/elembic" && mkdir -p "$pkgbase/1.1.0" && curl -L https://github.com/PgBiel/elembic/archive/main.tar.gz | tar xz --strip-components=1 --directory="$pkgbase/1.1.0"

Elembic can then be imported with import "@local/elembic:1.1.0" as e.

Limitations

Please keep in mind the following limitations when using Elembic.

Rule limit

Elembic, in its default and most efficient mode, has a limit of up to 30 non-consecutive rules within the same scope, by default. This is due to a limitation in the Typst compiler regarding maximum function call depth, as we use show rules with context { } for set rules to work without using state (see also typst#4171 for more information).

Usually, this isn't a problem unless you hit the infamous "max show rule depth exceeded" error. If you ever receive it, you may have to switch to stateful mode, which has no set rule limit, however it is slower as it uses state.

However, there are some easy things to keep in mind that will let you avoid this error very easily, which include:

  1. Grouping rules together with apply
  2. Scoping temporary rules and not misusing revoke
  3. (As a last resort) Switching to either of the other styling modes (leaky or stateful)

Grouping rules together with apply

First of all, note that, for the purposes of this limit, rule may be a set rule, but also an apply rule, which has the power of applying multiple consecutive rules at the cost of one. That is, an apply rule, by itself, can hold way more than 30 rules (maybe 100, 500, you name it!) at once without a problem as it only counts for one towards the limit.

Therefore, it is recommended to always group set rules together into a single apply rule whenever possible.

Note that elembic is smart enough to do this automatically whenever it is absolutely safe to do so - that is, when they are consecutive (there are no elements or text between them, only whitespace or show/set rules).

That is, doing either of these is OK:

#import "@preview/elembic:1.1.0" as e

// OK: Explicitly paying for the cost of only a single rule
#show: e.apply(
  e.set_(elem, fieldA: 1)
  e.set_(elem, fieldB: 2)
  e.set_(elem, fieldC: 3)
)

// OR

// OK: elembic automatically merges consecutive rules
// (with nothing or only whitespace / set and show rules inbetween)
#show: e.set_(elem, fieldA: 1)
#show: e.set_(elem, fieldB: 1)

#show: e.set_(elem, fieldC: 1)

but please avoid adding text and other elements between them - elembic does not merge them as it may be unsafe (text may be converted into custom elements by show rules, and other elements may contain custom elements within them, for example):

#import "@preview/elembic:1.1.0" as e

// AVOID THIS! Paying for 3 rules instead of 1
// (Cannot safely move down the text between the rules
// automatically)
#show: e.set_(elem, fieldA: 1)

Some text (please move me below the rules)

#show: e.set_(elem, fieldB: 2)

Some text (please move me below the rules)

#show: e.set_(elem, fieldC: 3)

As a general rule of thumb, prefer using explicit apply rules whenever possible. It's not only safer (it's easy to accidentally disable the automatic merging by adding text like above), it's also easier to write and much cleaner!

Scoping temporary rules and not misusing revoke

Are you only using a set rule for a certain part of your document? Please, scope it instead of using it and revoking it. The latter will permanently cost two rules from the limit, while the former will only cost one and only during the scope.

That is, do this:

// Default color
#superbox()

#[
  #show: e.set_(superbox, color: red)

  // All superboxes are temporarily red now
  #superbox()
  #superbox()
  #superbox()
]

// Back to default color!
// (The set rule is no longer in effect.)
#superbox()

But do not do this:

// Default color
#superbox()

#show: e.named("red", e.set_(superbox, color: red))

// All superboxes are red from here onwards
#superbox()
#superbox()
#superbox()

// AVOID THIS!
// While the rule was revoked and the color is back
// to the default, BOTH rules are still unnecessarily
// active and counting towards the limit.
#show: e.revoke("red")

// Back to default color!
// However, that is because both rules are in effect.
#superbox()

A good usage of revoke is to only temporarily (for a certain scope) undo a previous set rule:

// Default color
#superbox()

#show: e.named("red", e.set_(superbox, color: red))

// All superboxes are red from here onwards
#superbox()
#superbox()
#superbox()

#[
  // OK: This is scoped and only temporary
  #show: e.revoke("red")

  // Back to default color!
  // (Only temporary)
  // Both rules are in effect here
  // (the second nullifies the first).
  #superbox()
]

// This is red again now (the "red" rule is back).
// The revoke rule is no longer in effect.
// Only the set rule.
#superbox()

Switching to other styling modes

If those measures are still not enough to fix the error, then you will have to switch to another styling mode.

There are three styling modes available for set rules (as well as apply rules, revoke rules and so on):

  1. Normal mode (default): The safest mode, uses show: set rules on existing elements to store set rule data, and then immediately restores the current value of those rules, so it is fast (as it only uses show / set, so it causes no document relayouts) and hygienic (the inner workings of Elembic have no effect on your document in that case).
    • In this mode, you are limited to around 30 simultaneous rules in the same scope as each set rule has two nested function calls (contributing twice towards the limit of 64, minus 3 due to elements themselves).
    • It is worth the reminder that this number refers to ungrouped, non-consecutive set rules. If you group set rules together, they are unlimited in number.
    • This is the default mode when using e.set_(...), e.revoke(...) and others.
  2. Leaky mode: It is as fast as normal mode, but has double the limit of rules (around 60). However, it is not hygienic as it resets existing #set bibliography(title: ...) rules to their first known values. (That is, any bibliography.title set rules after the first set rule are lost, and the initial value is reset with each leaky rule.) If this is acceptable to you, then feel free to use leaky rules to easily increase the limit.
    • This mode can be used by packages' internal elements, for example, since that set rule is probably unimportant then.
    • To switch to this mode, you can simply write #show: e.leaky.enable() at the top of your document, or within any #[ scope ] to temporarily apply this change.
      • Alternatively, you can also replace all set rules with their e.leaky counterparts. For example, you can replace e.set_(element, field: value) with e.leaky.set_(element, field: value). (You can create an alias such as import e: leaky as lk and then lk.set_(...) for ease of use.)
      • The alternative is more useful to package authors who may want more control. For general users, e.leaky.enable() is enough and recommended. (The same can't be said for stateful mode mentioned below.)
  3. Stateful mode: Rules in this mode do not have any usage limits. You can nest them as much as you want, even if you don't group them. However, the downside is that this mode uses state, which can cause document relayouts and be slower.
    • Note that you can restrict this mode change to a single scope and use other modes elsewhere in the document.
    • Other rule modes are compatible with stateful mode. You can still use non-stateful set rules when stateful mode is enabled; while they will still have a limit, they will read and update set rule data using state as well, so they stay in sync. In addition, the limit of normal-mode rules is doubled just by enabling stateful mode in the containing scope, since they will automatically switch to a more lightweight code path. Therefore, package authors can use normal-mode rules without problems.

The easiest solution is often to just switch to leaky mode with #show: e.leaky.enable(), which will double the non-consecutive rule limit, at the cost of pinning bibliography.title to its first known (to elembic) value.

But if you need even more nested rules, you may have to switch to stateful mode, which uses state to keep track of set rules. It is slower and may trigger document relayouts in some cases, but has no usage limits (other than nesting many elements inside each other, which is a limitation shared by native elements as well and is unavoidable).

To do this, there are two steps (unlike the single step for leaky mode):

  1. Enabling stateful mode: Writing #show: e.stateful.enable() (note the parentheses) for either a certain scope (to only enable it for a certain portion of your document) or at the very top of the document (to enable it for all of the document);

    • This is used to inform rules in normal mode that they should read and write to the state. It also increases their limits by doing so.
    • Without this step, stateful-only set rules (below) don't do anything.
  2. Replacing existing set rules with their stateful-only counterparts. That is, replace all occurrences of e.set_, e.apply, e.revoke etc. with e.stateful.set_, e.stateful.apply and e.stateful.revoke, respectively, in the scopes where stateful mode was enabled. (The previous rules remain working even under stateful mode, but still have usage limits, albeit a bit larger.)

    • While a Ctrl+F fixes it, it's a bit of a mouthful, so you may want to consider aliasing e.stateful to a variable such as #import e: stateful as st, then you can write st.set_, st.apply etc. instead.
    • Stateful-only rules have no usage limits, but they only work in scopes where stateful mode is enabled. Otherwise, they cause an error, since the state changes would be ignored.

The two steps above should be enough to fix the error for good, provided you understand the potential performance consequences. Of course, it just might not be at all significant for your document.

Performance

Elembic's performance is, in general, satisfactory, and is logged on CI, but may get worse if you use more features. Here's a brief discussion about potential offenders:

  • Elements with contextual: true have reduced benefits from memoization. As such, if too many of them are placed (say, in the hundreds), they can cause a considerable performance penalty. However, it is not a problem if only a few of them are placed.
  • Filtered rules and ancestry tracking are also heavy (although less) if there are too many matching elements (in the order of hundreds or thousands) / elements with ancestry tracking enabled. Be mindful when using these, and try to apply them to less used elements.
  • Typechecking of fields can add some overhead on each element instantiation, however this is expected to be mostly irrelevant in the average case (when using mostly native types or combinations of them) compared to Elembic's other features. Still, you can improve this by overriding Elembic's default argument parser, or even disabling typechecking altogether with the typecheck: false option on elements, if that proves to be a bottleneck for your particular element.

The only way to know whether Elembic is a good fit for you is to give it a try on your own document!

Frequently Asked Questions

Where does the name "Elembic" come from?

Elembic's name comes from "element" + "alembic", to indicate that one of Elembic's goals is to experiment with different approaches for this functionality, and to help shape a future update to Typst that includes native custom elements, which will eventually remove the need for using this package.

Changelog

v1.1.0 (2025-06-19)

What's changed

  • Critical fix: Fix joining in show rules on e.selector and labeled elements (PR GH#54)
  • Elements can now opt into support for outer labels to work just like native elements with labelable: true (PR GH#52)
    • Elements with labelable: true can be labeled with #elem(...) <label-here> (as well as with the old syntax below).
    • However, the element will then require #show: e.prepare() by the user to work.
    • In addition, the element can no longer be inline.
    • The default of labelable: auto works as before and only accepts labels specified by parameter: #elem(..., label: <label-here>).
    • Setting labelable: false removes the special label field altogether.

Full Changelog: https://github.com/PgBiel/elembic/compare/v1.1.0...v1.0.0

v1.0.0 (2025-06-09)

Elembic is now available on the Typst package manager! Use it today with:

#import "@preview/elembic:1.0.0" as e

#let my-element = e.element.declare(...)
#show: e.set_(my-element, field: 2, ...)

In addition, the docs have been updated (and are still receiving more updates) - read them here: https://pgbiel.github.io/elembic

What's Changed

  • Add doc: property to elements to describe what they do
  • Optimization: When an element display function is defined as display: it => e.get(get => x) (instantly returns e.get), the body is simplified to just x (get => ... is called internally by the element code), reducing amount of context {} nesting.
  • Custom types now have fold: auto by default, merging their fields between set rules
  • types.wrap now has an additional safeguard to error when fold: needs to be updated
  • Moved ok, err, is-ok from e.types to e.result.{ok, err, is-ok}
  • Created examples/ folder for examples on how to use elembic
  • Update docs for v1.0 (PR GH#48)
    • Now using mdbook-admonish
    • Improve "Creating" section
    • New "Examples" section
    • New "Styling" section
    • New "Scripting" section
    • Show how to import from the package manager
    • Several other improvements

Full Changelog: https://github.com/PgBiel/elembic/compare/v1.0.0-rc2...v1.0.0

v1.0.0-rc2 (2025-06-03)

What's Changed

  • Add folding to dicts by default (PR GH#47)
  • There is now a perma-unstable internal module which exposes some internal stuff for debugging. Relying on it is not recommended, but might be needed to test for implementation bugs at some point.

Full Changelog: https://github.com/PgBiel/elembic/compare/v1.0.0-rc1...v1.0.0-rc2

v1.0.0-rc1 (2025-05-31)

NOTE: Docs are currently outdated; they will receive a brief update throughout the week (before and after 1.0 is released).

What's Changed

  • Fixed e.select accidentally producing clashing labels in trivial cases
    • NOTE: since alpha 3, it requires a mandatory prefix: argument as clashing labels are inevitable when two selects are not nested, but instead siblings. So make sure to add something like e.select(prefix: "@preview/your-package/level 1" ...) to ensure each select is distinct. There is no risk of clashing when one select is inside the other.
  • Enabled folding of smart(option(T)) when T is a foldable type (e.g. array, stroke)
  • Enabled folding of union types when not ambiguous (PR GH#46)
  • Added more safeguards to e.types.wrap, a utility to create new types wrapping old types, to avoid further cases of creation of invalid types
  • Added some forward-compatibility to e.ref
  • Store custom type arguments with e.data(custom type).type-args, and similarly for custom elements with e.data(custom element).elem-args
    • Those were the arguments passed to .declare, can be used for reflection purposes

Full Changelog: https://github.com/PgBiel/elembic/compare/v0.0.1-alpha3...v1.0.0-rc1

v0.0.1-alpha3 (2025-05-23)

We're very close to v1!

What's Changed

  • New rules:

    • Show: the new recommended approach for show rules, support custom elembic filters (PR GH#27)
      • show: e.show_(wock.with(color: red), it => [hello #it]) will add "hello" before all red wocks
      • These show rules are nameable and revokable, allowing you to cancel its application temporarily, for elements in a scope, using show: e.revoke("its name")
    • Filtered: apply to all children of matching elements (PR GH#17)
      • show: e.filtered(wock.with(color: red), e.set_(wock, color: purple)) will set all wocks children of red wocks to purple
    • Conditional set: change fields of all matching elements
      • show: e.cond-set(wock.with(kind: "monster"), color: purple) will set all monster wocks to purple color
  • New filters: (usable in those rules - PR GH#23)

    • AND, OR, NOT, XOR combiners
      • e.filters.and_(my-rect, e.filters.not_(my-rect.with(fill: blue))) matches all rectangles without blue fill
      • e.filters.or_(thm.with(kind: "lemma"), thm.with(kind: "theorem")) will match theorems or lemmas
      • e.filters.xor(thm.with(kind: "lemma"), thm.with(stroke: blue)) will match a lemma or a theorem with a blue stroke, but not a lemma with a blue stroke
      • Note that NOT must be in an and to avoid unlimited matching
    • Custom filters
      • e.filters.and_(person, e.filters.custom((it, ..) => it.age > 5)) will match all person instances with age greater than 5
      • Same disclaimer as NOT
  • Within filter (ancestry tracking - PR GH#38)

    • Check descendant
      • e.filters.and_(wock, e.within(blocky.with(fill: red))) matches all wocks in red-filled blocky at any depth
    • Check depth
      • e.filters.and_(wock, e.within(blocky, depth: 1)) matches all wocks which are direct children of blocky among elembic elements with ancestry tracking enabled (see caveat below)
    • Check max depth
      • e.filters.and_(wock, e.within(blocky, max-depth: 2)) matches all wocks which are either direct children of blocky or inside a direct child of blocky (same disclaimer)
    • NOTE: Ancestry tracking is lazy and restricted to elembic elements. This means that:
      • If you never write a rule with e.within(elem), any other .within will completely ignore elem. For example, in blocky(elem(wock)), if we never wrote a rule with e.within(elem), wock will still be considered a direct child (depth 1). So ancestry tracking is only enabled for an element if at least one .within(elem) rule is used above it.
      • You can force elements to track ancestry without rules using show: e.settings(track-ancestry: (elem1, elem2)). (Applies only to elements in the same scope / below).
        • You can even force all elements to do it with show: e.settings(track-ancestry: "any")
        • However, this will degrade your performance when applied to too many elements so make sure to test it appropriately first
        • It may also change the meaning of within rules depending on depth.
  • Added e.query(filter) (#34)

    • Query instances of your element with a certain filter, as specified above
    • within filters will only work if the element is set to track ancestry (lazy), or just store (avoid some of the performance losses)
      • Force with #show: e.settings(store-ancestry: (elem,)) or (to track and store) track-ancestry: (elem,).
  • Lots of forward- and backward-compatibility fixes (e.g. #37)

  • none is now valid input for content type fields (see #5 for discussion)

  • Added support for automatic casts from dictionary to custom types by enabling with the casts: ((from: dictionary),) option (Issue GH#15)

Other features

  • Use e.eq(elem1, elem2) or e.eq(mytype1, mytype2) to properly compare two custom elements or custom type instances. (PR GH#29)

    • If this returns true, it will remain so even if you update elembic to another version later, or change some data about the type or element.
    • This is important as those changes would cause == to turn false between elements created before the change and elements created after.
    • However, note that e.eq is potentially slower as it will recursively compare fields.
    • Only a concern for package authors (since there can be concurrent versions of a package simultaneously).
  • Use #show: e.leaky.enable() to quickly enable leaky mode for rules under it without having to add the e.leaky prefix to each one.

  • Rules can now have multiple names to revoke them by (PR GH#25).

    • For example, #show: e.named("a1", "b1", e.set_(blocky, fill: red))
    • Useful for templates (e.g. use names to "group together" revokable rules).
    • Then, if downstream users don't like those rules, they can revoke them easily with #show: e.revoke("a1") in the scope where the template is used.
  • Added e.stateful.get()

    • Stateful mode-only feature, allows you to get(custom element) (get values set by set rules) without having to wrap your code in a callback get => ...
    • E.g. let fields = e.stateful.get(wock)
  • Added field metadata, internal: bool, folds: bool

    • Set e.field(internal: true) to indicate this field is internal and users shouldn't care about it
    • Set e.field(folds: false) to disable folding for this field (e.g. set rule on an array field with fold: false will override the previous array instead of just appending to it)
    • Set e.field(metadata: (a: 5, b: 6)) to attach arbitrary metadata to a field

Full Changelog: https://github.com/PgBiel/elembic/compare/v0.0.1-alpha2...v0.0.1-alpha3

v0.0.1-alpha2 (2025-03-18)

This was the version that was shipped with lilaq 0.1.0.

v0.0.1-alpha1 (2025-01-13)

Initial alpha testing release

Examples

This chapter contains useful sample usages of elembic, with more to be added over time.

Simple Thesis Template

This example demonstrates the usage of elembic for configurable and flexible templates.

We could have the following template.typ for a thesis. Note that we can retrieve thesis settings with get rules, and style individual components through show / set rules:

#import "@preview/elembic:1.1.0" as e

#let settings = e.element.declare(
  "thesis-settings",
  doc: "Settings for the best thesis template.",
  prefix: "@preview/thesis-template,v1",

  // Not meant to be displayed, only receives set rules
  display: it => panic("This element cannot be shown"),

  // The fields need defaults to be settable.
  fields: (
    e.field("title", str, doc: "The thesis title.", default: "My thesis"),
    e.field("author", str, doc: "The thesis author.", default: "Unspecified Author"),
    e.field("advisor", e.types.option(str), doc: "The advisor's name."),
    e.field("coadvisors", e.types.array(str), doc: "The co-advisors' names."),
  )
)

// Make title page an element so it is configurable
#let title-page = e.element.declare(
  "title-page",
  doc: "Title page for the thesis.",
  prefix: "@preview/thesis-template,v1",

  fields: (
    e.field("page-fill", e.types.option(e.types.paint), doc: "Optional page fill", default: none),
  ),

  // Default, overridable show-set rules
  template: it => {
    set align(center)
    set text(weight: "bold")
    it
  },

  display: it => e.get(get => {
    // Have a dedicated page with configurable fill
    show: page.with(fill: it.page-fill)

    // Retrieve thesis settings
    let opts = get(settings)
    block(text(32pt)[#opts.title])
    block(text(20pt)[#opts.author])

    if opts.advisor != none {
      [Advised by #opts.advisor \ ]
    }

    for coadvisor in opts.coadvisors {
      [Co-advised by #coadvisor \ ]
    }
  }),
)

#let template(doc) = e.get(get => {
  // Apply settings to document metadata
  set document(
    title: get(settings).title,
    author: get(settings).author,
  )

  // Apply some styles
  set heading(numbering: "1.")
  set par(first-line-indent: (all: true, amount: 2em))

  title-page()

  // Place the document, now with styles applied
  doc
})

We can then use this template in main.typ as follows:

#import "@preview/elembic:1.1.0" as e
#import "template.typ" as thesis

// Configure template
#show: e.set_(
  thesis.settings,
  title: "Cool template",
  author: "Kate",
  advisor: "Robert",
  coadvisors: ("John", "Ana"),
)

// Have a red background in the title page
#show: e.set_(thesis.title-page, page-fill: red.lighten(50%))

// Override the bold text set rule
#show e.selector(thesis.title-page, outer: true): set text(weight: "regular")

// Apply italic text formatting in the title page
#show: e.show_(thesis.title-page, emph)

// Apply the template
#show: thesis.template

= Introduction

#lorem(80)

This will produce the following pages of output: Title page - red and italics Introduction page

Simple Theorems Package

This example is a work in progress, but in the meantime, check out the "Outline" page for an example.

Creating custom elements

This chapter will explain everything you need to know about the creation of an element using Elembic. It is useful not only for package authors, but interested users who would like to make reusable components in their document.

Elements, in their essence, are reusable components of the document which can be used to ensure certain parts of it share the same appearance - for example, headings, figures, as well as blocks, which are available by default.

In addition, however, elements have other properties. They can be configured by users through styles, that is, show and set rules, which can be used to, respectively, replace an element's whole appearance with some other, or change the values of some of that element's fields, if they are unspecified when the element is created.

Last but not least, Elembic can guarantee some level of type-safety by typechecking user input to element fields. Whenever you create a field, you must specify its type, which will allow Elembic to do this check.

Throughout the chapter, we will create and manipulate a sample element named "theorem".

Declaring a custom element

Want to make a reusable and stylable component for your users? You can start by creating your own element as described below.

First steps

You can use the element.declare function to create a custom element. It will return the constructor for this element, which you should export from your package or file:

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  // Element name.
  "theorem",

  // A prefix to disambiguate from elements with the same name.
  // Elements with the same name and prefix are treated as equal
  // by the library, which may cause bugs when mixing them up,
  // so conflicts should be avoided.
  prefix: "@preview/my-package,v1",

  // Some documentation for your element, describing what it does.
  // Accessible later through `e.data(theorem).doc`.
  doc: "Formats a theorem statement.",

  // Default show rule: how this element displays itself.
  display: it => [Hello world!],

  // No fields for now.
  fields: ()
)

// Place it in the document:
#theorem()

This will display "Hello world!" in the document. Great!

Note that the element prefix is fundamental for distinguishing elements with the same name. The idea is for the element name to be simple (as that's what is displayed in errors and such), but the element prefix should be as unique as possible. (However, try to not make it way too long either!)

Importantly, if you ever change the prefix (say, after a major update to your package), users of the element with the old prefix (i.e. in older versions) will not be compatible with the element with the new prefix (that is, their set rules won't target them and such). While this could be frustrating at first, it is necessary if you change up your element's fields in a breaking way to avoid bugs and incompatibility problems. Therefore, you may want to consider adding a version number to the prefix (could be your library's major version number or just a number specific to that element) which is changed on each breaking change to the element's fields.

Adding fields

You may want to have your element's appearance be configurable by end users through fields. Let's add a color field to change the fill of text inside our theorem:

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  "theorem",
  prefix: "@preview/my-package,v1",
  doc: "Formats a theorem statement.",

  // Default show rule receives the constructed element.
  display: it => text(fill: it.fill)[Hello world!],

  fields: (
    // Specify field name, type, brief description and default.
    // This allows us to override the color if desired.
    e.field("fill", e.types.paint, doc: "The text fill.", default: red),
  )
)

// This theorem will display "Hello world!" in red.
#theorem()

// This theorem will display "Hello world!" in blue.
#theorem(fill: blue)

Here we use e.types.paint instead of just color because the fill could be a gradient or tiling as well, for example; paint is a shorthand for e.types.union(color, gradient, tiling).

To read more about the types that can be used for fields, read the Type system chapter.

Note that omitting default: red in the field creation would have caused an error, as Elembic cannot infer a default value for the color type.

However, that isn't a problem if, say, we add a required field with required: true. These fields do not need a default.

By default, required fields are positional, although one can also force them to be named through named: true (and vice-versa: you can have non-required fields be positional with named: false).

Let's give it a shot by allowing the user to customize what goes inside the element:

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  "theorem",
  prefix: "@preview/my-package,v1",
  doc: "Formats a theorem statement.",

  display: it => text(fill: it.fill)[#it.body],

  fields: (
    // Force this field to be specified.
    e.field("body", content, required: true),
    e.field("fill", e.types.paint, doc: "The text fill.", default: red),
  )
)

// This theorem will display "Wowzers!" in red.
#theorem[Wowzers!]

// This theorem will display "Some content" in blue.
#theorem(fill: blue)[Some content]

Note that this also allows users to override the default values of fields through set rules (see "Set rules" for more information):

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  "theorem",
  prefix: "@preview/my-package,v1",
  doc: "Formats a theorem statement.",

  display: it => text(fill: it.fill)[#it.body],

  fields: (
    // Force this field to be specified.
    e.field("body", content, required: true),
    e.field("fill", e.types.paint, doc: "The text fill.", default: red),
  )
)

#show: e.set_(theorem, fill: green)

// This theorem will display "Impressed!" in green.
#theorem[Impressed!]

Note on folding

By default, folding is enabled for compatible field types - most commonly, arrays, dictionaries and strokes.

For fields with those types, this means consecutive set rules don't override each other, but have their values joined (see the linked page for details).

If this is not desired for a specific field, set e.field("that field", folds: false).

Tip: field options

folds is just one example of how you can configure a field. For a full list of field options, as well as more details on them, check out "Specifying fields".

Overridable set rules

Instead of applying set rules at the top of your display function, apply set rules through template: instead. This allows overriding them with show-set on the element's selector.

For example, this doesn't work (note how the set rules cannot be overridden):

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  "theorem",
  // Don't do this!
  display: it => {
    // Oh no: these set rules cannot be overridden!
    set text(red)
    set align(center)
    it.body
  },

  prefix: "@preview/my-package,v1",
  doc: "Formats a theorem statement.",
  fields: (e.field("body", content, required: true),)
)

// Let's try to override these set rules:
#show e.selector(theorem): set text(blue)
#show e.selector(theorem): set align(left)

// Didn't work!
#theorem[Still red and centered...]

theorem still red and centered

Do the following instead. With template, they can now be overridden:

#import "@preview/elembic:1.1.0" as e

#let theorem = e.element.declare(
  "theorem",

  template: it => {
    // This is ok!
    // They can be overridden!
    set text(red)
    set align(center)
    it
  },

  display: it => {
    it.body
  },

  prefix: "@preview/my-package,v1",
  doc: "Formats a theorem statement.",
  fields: (e.field("body", content, required: true),)
)

// Let's try to override these set rules:
#show e.selector(theorem): set text(blue)
#show e.selector(theorem): set align(left)

// It worked!
#theorem[Blue and left-aligned at last!]

theorem blue and left-aligned

Element reflection

If you need to access data about the element within display (or other element functions receiving fields), you can use e.data or its related functions. In particular, e.counter(it) provides the element's counter, whereas e.func(it) provides the element constructor itself. Check the page about custom references for more information.

Accessing context

Read "Accessing context" for details on how to access contextual values in your display function.

Specifying fields

The previous section on declaring custom elements provided examples on how to declare fields to specify user-configurable data for your element. Here, we will go more in depth.

When specifying an element's fields, you should use the field function for each field, which supports a number of options.

Required and named fields

By default, fields are optional and named (specified as element(field-name: value), but can be omitted). Omitted optional fields are set to a type-specific default (e.g. none for e.types.option(int), empty array for array), but you can specify a different default to e.field with default: ("not", "empty") for example.

Setting required: true will cause the field to become required and positional (specified as element(value), no default).

You can use named: for other combinations: required: true, named: true for required and named and required: false, named: false for optional and positional fields.

Type

The field's type is a fundamental piece of information. Elembic uses this to automatically check for invalid input by the user when constructing your element. The type system chapter has more information on what you can do with types.

You can use e.types.any to disable typechecking for a single field, or typechecks: false on the element to disable it for all fields.

Tip

To check for a custom data structure (usually dictionary-based) in a field, consider creating your own custom type.

Type combinators

There are several type combinators you can use:

  • Use e.types.union(typeA, typeB, ...) to indicate a field can have more than one type. The first matching type is used.
  • Use e.types.option(type) to indicate that a field can be set to (or defaults to) none.
  • Use e.types.smart(type) to indicate that a field can be set to auto to mean a smart default.
  • Use e.types.array(type) and e.types.dict(type) for arrays and dictionaries with only a specific type of value, such as e.types.array(int) for an array of integers.

Changing types

To change existing types slightly, check out type wrapping with e.types.wrap. This can be used to:

  1. Add custom folding behavior to your field (override fold with a function);

  2. Add a custom check to your field with e.g. e.types.wrap(int, check: prev => i => i > 0) instead of just int to only accept positive integers

    • Here prev can be ignored since it is none (int has no checks by default), but make sure to invoke it as needed.
  3. Add a custom cast with cast.

Metadata

Several options may be specified to attach metadata to fields. This metadata can be retrieved later with e.fields(element).all-fields.FIELD-NAME, so it is useful for docs generators, and otherwise has no practical effects:

  1. doc (string): Field documentation.
  2. internal (bool): If set to true, should be hidden in the documentation.
  3. meta (dictionary): Arbitrary key/value pairs to attach to this field.

Synthesizing fields

Some elements will have conveniently auto-generated fields, which are created after set rules are applied, but before show rules. To do this, there are two steps:

  1. List those fields as "synthesized" fields in the fields array. To do this, just specify e.field(..., synthesized: true).

    • Such fields cannot be manually specified by users, however they can be matched on by filters.
  2. Create a synthesis step for your element with synthesize: fields => updated fields. Here, you can, for example, access Typst context, as well as use e.counter(it) to read the counter, and so on.

Here's a bit of an artificial example just to showcase the functionality: we combine a stroke field and a body field to generate an applied field, which is what is effectively displayed.

#import "@preview/elembic:1.1.0" as e: field, types

#let frame = e.element.declare(
  "frame",
  prefix: "@preview/my-package,v1",
  doc: "Displays its body in a frame.",
  display: it => it.applied,
  fields: (
    field("body", content, doc: "Body to display.", required: true),
    field("stroke", types.option(stroke), doc: "Stroke to add around the body."),
    field("applied", content, doc: "Frame applied to the body.", synthesized: true)
  ),
  synthesize: it => {
    it.applied = block(stroke: it.stroke, it.body)
    it
  }
)

#show: e.show_(frame, it => {
  let applied = e.fields(it).applied
  [The applied field was #raw(repr(applied)), as seen below:]
  it
})

#frame(stroke: red + 2pt)[abc]

"The applied field was block(stroke: 2pt + rgb("#ff4136"), body: [abc]), as seen below:", followed by "abc" inside a box with red stroke

All field options

e.field has the following arguments:

  1. name (string, positional): the field name, by which it will be accessed in the dictionary returned by e.fields(element instance).

  2. type_ (type / typeinfo, positional): the field's type, defines how to check whether user input to this field is valid (read more about types in the Type system chapter).

  3. doc (string, named, optional): documentation for the field. It is recommended to always set this. The documentation (and other field metadata) can later be retrieved by accessing e.data(elem).all-fields, and can be used to auto-generate documentation, for example.

  4. required (boolean, named, optional): whether this field is required (and thus can only be specified at the constructor, as it will have no default). This defaults to false.

  5. named (boolean or auto, named, optional): whether this field should be specified positionally (false - that is, without its name) to the constructor and set rules, or named (true - the user must specify the field by name). By default, this is auto, which is true for optional fields and false for required fields (but you can have required named fields and optional positional fields by changing both parameters accordingly).

  6. default (any, named, optional): the default value for this field if required is false. If this argument is omitted, the type's default is used (as explained below), or otherwise the field has no default (only possible if it is required). If a value is specified, that value becomes the field's default value (it will be cast to the given field type). It was done this way so you can also specify none as a default (which is common).

    Note that many types have their own default values, so you can usually omit default entirely, as the field will then just use that type's default. For example, if the type is int, then default will automatically be set to 0 for such optional fields. If the type doesn't have its own default and the field is optional, you must specify your own default.

  7. synthesized (boolean, named, optional): this is used to indicate that this is an automatically generated field during synthesis. A synthesized field cannot be manually set by the user - be it through the constructor or through set rules. However, it can be matched on in filters, such as for selecting elements through e.select. For example, if you have a synthesized field called level, a user can match on elem.with(level: 10).

    Therefore, you should always list automatically generated fields as synthesized fields. Elembic won't forbid you from adding "hidden fields" during synthesis, but then they will be seen as inexistent by the framework, so it is recommended to always list auto-generated fields as synthesized fields, with proper documentation for them.

    Read more about synthesis above.

  8. folds (boolean, named, optional): if false, set rules and arguments changing this field will always completely override the previous value instead of joining. This only has an effect on foldable types, such as arrays, dictionaries and strokes. For other types, that is already what happens: no joining.

  9. internal (boolean, named, optional): if true, indicates this field should be hidden from documentation. Has no effect on elembic itself and is only meant to be read by external tools.

  10. meta (dictionary, named, optional): an optional dictionary with arbitrary keys and values to be read by external tools, such as documentation generators.

Accessing context

Within your element's display function, you may sometimes need to access contextual values. For example, you may have to:

  1. Read the current text font (with text.font), or some other Typst set rule.
  2. Read an elembic element's set rule.
  3. Read the value of a certain state variable.
  4. Read a counter.

How to do this with elembic?

There are a few options. The simplest and safest option is just display: it => e.get(get => context { ... }).

Here are all the options:

  1. Use the default context: more efficient, but ignores show/show-set rules not applying to the outer selector, so usually not recommended.

  2. Use e.get: if you need to read an elembic set rule, you can use a get rule.

    Tip

    Ensure e.get is the first step in your display for greater performance, as elembic will avoid creating a needless nested context then:

    e.element.declare(
      display: it => e.get(get => {
        // Do everything else here
        // including a nested context block if needed
      })
    )
    
  3. Use explicit context: to read other contextual values, you can use a nested context to guarantee all show-set rules are considered.

Compare each option below.

Default context

By default, without an explicit context block, your display function runs under the element's original context.

This means that the values above, by default, can be read, but ignoring show / show-set rules on this element as those are applied after display() is called.

However, show / show-set rules on the element's outer selector can be read without an additional context.

Note

The outer selector is applied before any rules - set rules, show rules and so on -, which is why this works.

Its downside is that it cannot be used for filtering in a show rule, precisely because the element's fields are not yet known.

If it can be used, however, it is the most lightweight option.

Consider this example using default context:

#import "@preview/elembic:1.1.0" as e
#let elem1 = e.element.declare(
  "elem1",
  prefix: "@example",
  fields: (),
  display: _ => text.size,
)
#set text(size: 2pt)
#show e.selector(elem1, outer: true): set text(size: 8pt)
#show e.selector(elem1): set text(size: 12pt)
#show: e.show_(elem1, it => { set text(size: 15pt); it })

// Displays "8pt" (non-outer rules ignored)
#elem1()

Explicit context

Less efficient, but more likely to be what you are looking for.

Consider this example with explicit context, it will display "12pt" (non-outer show-set is considered):

#import "@preview/elembic:1.1.0" as e
#let elem2 = e.element.declare(
  "elem2",
  prefix: "@example",
  fields: (),
  display: _ => e.get(_ => context text.size),
)
#set text(size: 2pt)
#show e.selector(elem2, outer: true): set text(size: 8pt)
#show e.selector(elem2): set text(size: 12pt)
#show: e.show_(elem2, it => { set text(size: 15pt); it })

// Displays "12pt" (non-outer rules considered)
#elem2()

Overriding the constructor and argument parsing

Disabling typechecking

You can use typecheck: false to generate an argument parser that doesn't check fields' types. This is useful to retain type information but disable checking if that's needed. The performance difference is likely to not be too significant, so that likely wouldn't be enough of a reason, unless too many advanced typesystem features are used.

Custom constructor

You can use construct: default-constructor => (..args) => value to override the default constructor for your custom type. You should use construct: rather than creating a wrapper function to ensure that data retrieval functions, such as e.data(func), still work.

Custom argument parsing

You can use parse-args: (default arg parser, fields: dictionary, typecheck: bool) => (args, include-required: bool) => (true, dictionary with fields) or (false, error message) to override the built-in argument parser. This is used both for the constructor and for set rules.

Here, args is an arguments and include-required: true indicates the function is being called in the constructor, so required fields must be parsed and enforced.

However, include-required: false indicates a call in set rules, so required fields must not be parsed and forbidden.

In addition, the default arg parser function can be used as a base for the function's implementation, of signature (arguments, include-required: bool) => (true, fields) or (false, error).

Warning

Note that the custom args parsing function should not panic on invalid input, but rather return (false, "error message") in that case.

This is consistent with the default arg parser function.

Argument sink

Here's how you'd use this to implement a positional argument sink:

#let sunk = e.element.declare(
  "sunk",
  display: it => {
    (it.run)(it)
  },
  fields: (
    field("values", e.types.array(stroke), required: true),
    field("run", function, required: true, named: true),
    field("color", color, default: red),
    field("inner", content, default: [Hello!]),
  ),
  parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => {
    let args = if include-required {
      // Convert positional arguments into a single 'values' argument
      let values = args.pos()
      arguments(values, ..args.named())
    } else if args.pos() == () {
      args
    } else {
      // Return errors the correct way
      return (false, "element 'sunk': unexpected positional arguments\n  hint: these can only be passed to the constructor")
    }

    default-parser(args, include-required: include-required)
  },
  prefix: ""
)

// Use 'run: func' as an example to test and ensure we received the correct fields
#sunk(
  5pt, 10pt, black, 5pt + black,
  run: it => assert.eq(it.values, (5pt, 10pt, black, 5pt + black).map(stroke))
)

Labels and references

Labeling elements

Elements can be labeled with #elem(label: <label-name>) (unless they set labeled: false.

Compared to what would be a more usual syntax (#elem() <label-name>, which should not be used), using label as an argument has multiple benefits:

  1. It works as a field, and so #show: e.show_(elem.with(label: <label-name>), it => ...) works (as well as in filtered rules and so on), and the label is available through e.fields.
  2. #show <label-name>: it => ... will have full field data available. (However, this show rule style is not revokable.)
  3. It allows custom references to work, as outlined below.

Referencing elements

To add reference support to an element, add reference: (...) in the element's declaration. It requires the keys supplement and numbering, which can be their usual values (content and string) or functions final fields => value, if you want the user to be able to override those values through supplement and numbering fields in the element. However, your reference can also be fully customized with (custom: fields => content).

Then, you must tell your user to call #show: e.prepare() at the top of their own document, so that references will work properly.

By default, the number used by references is the element's own counter (accessible with e.counter(elem)), stepped by one for each element. You can use e.g. count: counter => counter.update(n => n + 2) or even count: counter => fields => (something using fields) to change this behavior.

#import "@preview/elembic:1.1.0" as e: field

// The line below must be written by the END USER for references to work!
#show: e.prepare()

#let theorem = e.element.declare(
  "theorem",
  prefix: "my-package",

  display: it => [*Theorem #e.counter(it).display("1"):* #text(fill: it.fill)[#it.body]],

  reference: (
    supplement: [Theorem],
    numbering: "1"
  ),

  fields: (
    e.field("body", content, required: true),
    e.field("fill", e.types.paint, doc: "The text fill.", default: red),
  )
)

#theorem(label: <my-thm>)[*Hello world*]
#theorem(fill: blue, label: <other-thm>)[*1 + 1 = 2*]

Here is @my-thm

Here is @other-thm

"Theorem 1: Hello world" (in red), "Theorem 2: 1 + 1 = 2" (in blue), "Here is Theorem 1", "Here is Theorem 2"

Outline

To enable outline support, you may either use outline: auto on an element that already supports references - in which case it will simply reuse the reference supplement and numbering in the outline - or use outline: (caption: fields => content), which will show an extra caption beside supplement and numbering if they exist, otherwise (if the element doesn't support references, or uses (custom: ...) for references) it will simply display the caption by itself.

Note that the user doesn't need #show: e.prepare() for outline support to work, but it's good practice since it's needed for references.

The user may then display the element's outline using #outline(target: e.selector(elem, outline: true)).

#import "@preview/elembic:1.1.0" as e: field

#show: e.prepare()

#let theorem = e.element.declare(
  "theorem",
  prefix: "my-package",

  display: it => [*Theorem #e.counter(it).display("1"):* #text(fill: it.fill)[#it.body]],

  reference: (
    supplement: [Theorem],
    numbering: "1"
  ),

  outline: auto,

  fields: (
    e.field("body", content, required: true),
    e.field("fill", e.types.paint, doc: "The text fill.", default: red),
  )
)

#outline(target: e.selector(theorem, outline: true))

#theorem(label: <my-thm>)[*Hello world*]
#theorem(fill: blue, label: <other-thm>)[*1 + 1 = 2*]

Outline shows: "Contents", "Theorem 1 ... 1", "Theorem 2 ... 1", referring to the two theorems below

Extra declaration options

Setting overridable default styles with template

You can have a custom template for your element with the template option. It's a function displayed element => content where you're supposed to apply default styles, such as #set par(justify: true), which the user can then override using the element's outer selector (e.selector(elem, outer: true)) in a show-set rule:

#import "@preview/elembic:1.1.0" as e: field

#let elem = e.element.declare(
  "elem",
  // ...
  display: _ => [Hello world!]
  template: it => {
    set par(justify: true)
    it
  },
)

// Par justify is enabled
#elem()

// Overriding:
#show e.selector(elem, outer: true): set par(justify: false)

// Par justify is disabled
#elem()

Extra preparation with prepare

If your element needs some special, document-wide preparation (in particular, show and set rules) to function, you can specify prepare: (elem, doc) => ... to declare.

Then, the end user will need to write #show: e.prepare(your-elem, /* any other elems... */) at the top of their document to apply those rules.

Note that e.prepare, with or without arguments, is also used to enable references to custom elements, as noted in the relevant page.

#import "@preview/elembic:1.1.0" as e: field

#let elem = e.element.declare(
  "elem",
  // ...
  display: it => {
    figure(supplement: [Special Figure], numbering: "1.", kind: "some special figure created by your element")[abc]
  },
  prepare: (elem, it) => {
    // As an example, ensure some special figure you create has some properties
    show figure.where(kind: "some special figure created by your element"): set text(red)
    it
  },
)

// End user:
#show: e.prepare(elem)

// Now the generated figure has red text
#elem()

Making more context available with contextual: true

Some elements may need to access values from other elements' set rules in their display functions or in the functions used to generate reference supplements or outline captions, for example. If that is the case, you will need to enable contextual: true, which enables the usage of (e.ctx(it).get)(elem).field-name to get the latest value for that field considering set rules.

In addition, this option might also need to be enabled particularly in the off-chance you need to access the currently set value specifically of bibliography.title from context, due to how Elembic uses that property in its inner workings. Other values - such as text.fill or par.justify -, however, are already available to display and other functions by default, so you do not have to enable this option in those cases.

Regardless, this option should be avoided if possible: it decreases the benefits from memoization, which can lead to a significant performance penalty in the case where the element is used too many times (say, several hundred). If the element isn't meant to be used too many times (maybe once or twice, in the case of template-internal elements, for example), then that shouldn't be a concern. (Otherwise, this option would be enabled by default.)

Styling elements

This chapter teaches you useful information on how to best customize elements created with elembic.

Preparation

One important tip before using custom elements is to check in the custom element's documentation if it requires preparation. This is the case if:

  1. You intend to use a @reference on that element.
  2. The element says it needs it, e.g. to apply some basic show rules.

In that case, write the following at the top of your document:

#show: e.prepare(elem1, elem2, ...)

If only custom references are needed, just #show: e.prepare() is enough.

Constructing elements

A package you import will expose the constructor function for an element, used to display a new instance of that component in your document. This function may receive some arguments (fields) to configure the element's appearance, and creates content which you can then place in your document.

For example, a package might expose a container element with a single required field, its body. Required fields are usually specified positionally, without their names. It might also have a few optional fields, such as the box's width, which is auto (to adjust with the body) by default. Such fields are usually specified by their names.

Here's how we'd place a container into the document, in order to display it as it was defined by its package:

// Sample package name (it doesn't actually exist!)
#import "@preview/container-package:0.0.1": container

#container([Hello world!], width: 1cm)

// OR (syntactically equivalent)

#container(width: 1cm)[Hello world!]

Tip

You can retrieve the arguments you specified above later with e.fields, obtained by importing elembic (or e for short):

#import "@preview/elembic:1.1.0" as e

#let my-container = container(fill: red)[Body]

#assert(e.fields(my-container.fill) == red)
#assert(e.fields(my-container.body) == [Body])

Arguments not specified above (not fill or body) won't be available (at least, outside of show rules), as those will depend on set rules which are not evaluated immediately (until you place my-container somewhere).

If you're repeating yourself a lot, always creating the same containers or with similar arguments, one strategy to make that easier is with a variable:

#let red-container = container.with(fill: red)

#red-container[This is already red!]

However, a better idea for templates and such is to use set rules to configure default values for each field.

Set rules

Often, you will want to have a common style for a particular element across your document, without repeating that configuration by hand all the time. For example, you might want that all container instances have a red border. You might also want them to have a fixed height of 1cm. Let's assume that element has two fields, border and height, which configure exactly those properties.

You may then use elembic's set rules through #show: e.set_(element, field: value, ...), which are similar to Typst's own set rules: they change the values of unspecified fields for all instances of an element within the nearest #[ scope ]. When they are not within a scope, they apply to the whole document.

For this and other operations with custom elembic elements, you will have to import elembic. It is common to alias it to just e for simplicity.

Tip

Make sure to group your set rules together at the start of the document, if possible (or wherever they are going to be applied). That is, avoid adding text and other elements between them. This causes elembic to group them up and apply them in one go, avoiding one of its main limitations in its default style mode: a limit of up to ~30 non-consecutive set rules. (The limitation is circumventable, but at the cost of reduced performance. Read more at the Limitations page.)

Here's how you would set the default borders of all container instances to red:

#import "@preview/elembic:1.1.0" as e
#import "@preview/container-package:0.0.1": container

#show: e.set_(container, border: red)

// This will implicitly have a red border
#container(width: 1cm)[Hello world!]

// But the set rule is just a default
// This will override it with a blue border
#container(width: 1cm, border: blue)[Hello world!]

Tip: Applying multiple rules at once

Use e.apply(rule 1, rule 2, ...) to conveniently and safely apply multiple rules at once (set rules, but also show rules and anything else provided by elembic). This is what elembic implicitly converts consecutive set rules to for efficiency, but it's nice and convenient to do it explicitly.

For example, let's set all containers' heights to 1cm as well. This will require two set rules, which are best grouped together as such:

#import "@preview/elembic:1.1.0" as e
#import "@preview/container-package:0.0.1": container

#show: e.apply(
  e.set_(container, border: red)
  e.set_(container, height: 1cm)
)

// The above is equivalent to:
// #show: e.set_(container, border: red)
// #show: e.set_(container, height: 1cm)

// This will implicitly have a red border and a height of 1cm
#container(width: 1cm)[Hello world!]

Scoping set rules

It is useful to always restrict temporary set rules to a certain scope so they don't apply to the whole document. This not only avoids unintended behavior and signals intent, but also ensures you will keep a minimal amount of set rules active at once.

You can create a scope with #[]:

// This container has the default border
#container[Hello world!]

#[
  #show: e.set_(container, border: red)

  // These containers have a red border
  #container[Hello world!]
  #container[Hello world!]
  #container[Hello world!]
]

// This container has the default border again
// (The set rule is no longer in effect)
#container[Hello world!]

Folding

Some types have support for folding. When applying multiple set rules for fields with these types, their values are joined instead of overridden. This applies, for example, to arrays, dictionaries and strokes. Note the example below:

// More on show rules in the show chapter
#show: e.show_(theorem, it => [Authors: #e.fields(it).authors.join(", ")])

#show: e.set_(theorem, authors: ("Robson", "Jane"))
#show: e.set_(theorem, authors: ("Kate",))

// Prints "Authors: Robson, Jane, Kate, Josef, Euler"
#theorem(authors: ("Josef", "Euler"))

The set rules and arguments do not override each other.

To disable folding behavior for a specific field, the package author has to disable folding for their element with e.field("name", ..., fold: false). If the package author disabled folding for authors, it'd then print just Authors: Josef, Euler instead, as set rules would then override instead of joining.

How are types joined during folding?

Folding is a property, fold, of each type ("typeinfo") in elembic's type system. It may be a function of the form (outer, inner) => new value where outer is the previous value and inner is the next (such as ("Robson", "Jane") and ("Kate",) respectively for the second set rule above), returning a joined version of the two values (in the example, ("Robson", "Jane", "Kate")). A function that always returns inner is equivalent to setting fold: none (type has no folding).

There is more information in the Type system chapter. The element's author can customize the fold: function for any type, even types which don't usually have folding, with e.types.wrap(type, fold: prev-fold => (outer, inner) => new value) (see more at "Wrapping types").

Show rules

You can fully override the appearance of an element using show rules. They work similarly to Typst's own show rules, but use elembic rules.

Tip

All usage tips from set rules apply here too: show rules are also scoped, and they should be grouped together in your template to avoid counting too heavily towards the set rule limit. You can also use e.apply(rule 1, rule2, ...) to explicitly group them in a visually clean way.

A show rule has the form e.show_(element or filter, it => replacement), where you specify the element which should be visually replaced, and then pass a function which receives the replaced element and returns the replacement.

In this function, use e.fields(it) to retrieve the replaced element's final field values. This can be used to decide what to replace the element by based on its fields, or to display some fields for debugging and so on.

For example, here's a show rule that would display the width field alongside each container.

#import "@preview/elembic:1.1.0" as e
#import "@preview/container-package:0.0.1": container

#show: e.show_(container, it => {
  let fields = e.fields(it)
  [Here's a container with width #fields.width: #it]
})

// This will display:
// "Here's a container with width 1cm: Hello world!"
#container(width: 1cm)[Hello world!]

Conditional show rules

You can also only apply show rules to elements with certain values for fields with filters. For example, you may want to remove all containers with a red or blue fill property. For this, you can use the simplest filters, which just compare field values: with filters, akin to Typst elements' where filters. Here's how you'd use them:

#import "@preview/elembic:1.1.0" as e
#import "@preview/container-package:0.0.1": container

// Remove red and blue fill containers
#show: e.show_(container.with(fill: red), none)
#show: e.show_(container.with(fill: blue), none)

// This container is removed.
#container(fill: red)[Hello world!]
// This container is also removed.
#container(fill: blue)[Hello world!]

// But this container is kept (fill isn't red or blue).
#container(fill: yellow)[Hello world!]

For more information on filters, see the dedicated chapter. They can be much more advanced and allow more fine-grained selections of elements.

Filtered rules

Sometimes, you need to apply certain rules only to the children of certain elements. For example, you may want to add a blue background to all thmref elements inside theorem elements. For this, e.filtered(filter, rule) can be used. This is a special rule that applies an elembic rule on the children of each element matching filter.

Warning

This rule has a potential performance impact if the filter matches too many elements, in the hundreds.

Note

The filtered rule is not applied to elements matching the filter. Only to their children (output of the display function and any show rules).

This means e.filtered(theorem, e.set_(theorem, supplement: [Abc])) will only change the supplements of theorems inside other theorems, for example.

// Only apply to thmref inside theorem
#show: e.filtered(theorem, e.show_(thmref, block.with(fill: red)))

// This one does not have a red fill: it is outside a theorem.
#thmref(<abc>)


#theorem[
  // The 'thmref' here will have red fill.
  *A Theorem:* theorem #thmref(<abc>) also applies to rectangles.
]

Conditional set rules

Some set rules that modify certain fields should only be applied if other fields have specific values. For example, if a theorem has kind: "lemma", you may want to set its supplement field to display as [Lemma].

In this case, you can use e.cond-set(filter, field1: value1, field2: value2, ...). filter determines which element instances should be changed, and what comes after are the fields to set.

The filter must be restricted to matching a single element, or it will be rejected. This is only a problem for certain filters, such as NOT filters, custom filters and e.within filters. In that case, you can use e.filters.and_(element, filter) to force it to only apply to that element.

Note

e.cond-set(element.with(...), field: value) is not recursive. This means that a separate element nested inside a matched element will not be affected.

This makes it differ from Typst's show-set rule, such as show heading.where(level: 1): set heading(supplement: [Chapter]), which would also affect any nested heading.

To also affect children and descendants similarly to Typst's show-set, use e.filtered(element.with(...), e.set_(element, field: value)) together with e.cond-set.

For example:

#show: e.cond-set(theorem.with(kind: "lemma"), supplement: "Lemma")

// This will display "Theorem 1: The fact is true."
#theorem[The fact is true.]

// This will display "Lemma 1: This fact is also true."
#theorem(kind: "lemma")[This fact is also true.]

Revoking rules

Most rules can be temporarily revoked in a certain scope. This is especially useful for show rules, which often aren't easy to undo, unlike set rules. This can also be used to place an element while ignoring set rules if necessary. There are lots of possibilities!

This is also useful for templates: they can specify a default set of rules which the user can then revoke if they want without changing the template's source code.

Naming rules

The first step to revoking a rule is giving it a name by using e.named(name, rule). This is the name we'll use to indicate what to revoke:

#import "@preview/elembic:1.1.0" as e
#let elem = e.element.declare(/* ... */)
#show: e.named("removes elems", e.show_(elem, none))

// This is removed
#elem()

// This is removed
#elem()

Tip

You can assign multiple names to the same rule (e.named(name1, name2, ..., rule)). Revoking any of them will revoke the rule.

In addition, the same name can be shared by multiple rules. Revoking that name revokes all of them. This can be used to create "groups" of revokable rules.

Naming filtered rules

Filtered rules need some extra attention regarding naming:

  1. e.named("name", e.filtered(filter, rule)) will assign "name" to e.filtered, but not to each rule created with this filter.

    • In this case, revoking "name" will stop any new rule from being applied, but will not revoke an already applied rule in this scope.
  2. e.filtered(filter, e.named("name", rule)) will assign "name" to each new rule created, but not to e.filtered. This is fairly unusual.

  3. e.named("name", e.filtered(filter, e.named("name", rule))) will assign "name" to both filtered and each new copy of rule.

    • This is usually recommended, as revoking "name" will both stop rule from being applied and revoke an already active rule.

revoke rules

Next, let's say we want to stop a rule from being applied in a certain limited scope. We can then use e.revoke("name") to temporarily revoke all rules with that name.

For example, let's temporarily cancel the show rule from the previous example to show just one element:

#import "@preview/elembic:1.1.0" as e
#let elem = e.element.declare(/* ... */)
#show: e.named("removes elems", e.show_(elem, none))

// This is removed
#elem()

#[
  #show: e.revoke("removes elems")

  // This is shown!
  #elem()
]

// This is removed
#elem()

reset rules

Reset rules are more drastic and allow temporarily revoking all previous rules - named or not - in a certain scope.

The effect can be restricted to rules targeting only a single element, or (if nothing is specified) applied to all elements at once. Use with caution, as that may have unintended consequences to 3rd-party elements.

Effect on filtered rules

Reset rules targeting specific elements will stop active filtered rules targeting those elements from applying new rules, but will not revoke already applied rules unless they also target the same elements.

Consider the example below:

#show: e.set_(container, fill: red)
#show: e.set_(container, stroke: 5pt)

#container[This has red fill with a large black stroke.]

#[
  #show: e.reset(container)
  #show: e.set_(container, stroke: blue)

  #container[This has no fill with a normal-sized (not 5pt) blue stroke.]
]

#container[Back to red fill with large black stroke.]

Creating Typst selectors

Sometimes, you might need to use Typst selectors - not elembic filters - to match custom elements, such as:

  1. To match built-in elements and elembic elements simultaneously, e.g. in a show rule or built-in query;
  2. To use show elembic-elem: set native-typst-elem() (show-set on native elements inside custom elements).

You can obtain native Typst selectors which match elements in two ways:

Using e.selector

e.selector(element) returns a selector which matches all instances of that element for show rules and show-set rules.

You can also use e.selector(element, outer: true) specifically for show-set rules. This only matters if the element needs to read the final value of the set rule for its display logic, and is optional otherwise.

For example:

#show e.selector(theorem): set text(red)

#theorem[This text is red!!!]

Using e.select

Since e.selector(elem) matches all instances, it does not take a filter. To pick which elements should be matched by a selector, use e.select(filters..., (selectors...) => body, prefix: "...") to create a scope where any new elements (inside body) matching an elembic filter will be matched by the corresponding selectors passed by parameter. This function effectively converts a filter to a Typst selector in a scope.

The prefix is used to avoid conflicts between separate calls to e.select. There is no conflict between nested e.select calls regardless of prefix, but with the same prefix, sibling calls (or in totally separate places) will clash.

Note

Since e.select can only match elements placed inside it, it may be wise to use it at the very top of the document, perhaps as part of your template, to match as many elements as possible.

#e.select(
  container.with(fill: red),
  container.with(fill: blue),
  prefix: "@preview/my-package/1",
  (red-container, blue-container) => [
    #show red-container: set text(red)
    #show blue-container: set text(red)

    #container(fill: red)[This text is red!]
    #container(fill: blue)[This text is red!]
    #container(fill: yellow)[This text is not red.]

    // "Matched red: 1"
    #context [Matched red: #query(red-container).len()]
  ]
)

// This one is outside that `select` and not picked up
#container(fill: red)

Scripting

elembic has several utilities for using elements in scripting.

The main utility is e.data and its helpers, which provide most or all data known to elembic about a custom element, an element instance, a custom type, and so on. This is explained in more detail in "Fields and reflection".

However, there are other useful functions, such as e.query to query element instances.

In addition, e.get is the main way to introspect the style chain and read the latest values defined through set rules.

Warning

To compare element instances for equality, especially if you're a package author, use e.eq, as described in "Fields and reflection".

Fields and reflection

Elements naturally contain data, such as the fields specified for them, as well as their names, unique IDs (eid), counters, and so on.

You can retrieve this data using the dedicated data-extraction functions made available by Elembic. They are all based around e.data, the main function which returns a data dictionary for lots of different types. Here are some of the most useful functions and tasks:

Accessing fields

You can use e.fields(instance). Note that the returned dictionary will be incomplete if the element was just created. It is only complete in show rules, when set rules and default fields have been resolved.

#import "@preview/elembic:1.1.0" as e

#show: e.set_(elem, field-c: 10)

#let instance = elem("abc", field-a: 5, field-b: 6)

// Field information incomplete: set rules not yet resolved
#assert.eq(e.fields(instance), (pos-field: "abc", field-a: 5, field-b: 6))

#show: e.show_(elem, it => {
  // Field information is complete in show rules
  assert.eq(e.fields(it), (some-non-required-field: "default value", field-c: 10, pos-field: "abc", field-a: 5, field-b: 6))
})

#instance

Accessing element ID and constructor

You can use e.eid(instance) or e.eid(elem) to obtain the corresponding unique element ID. This is always the same for types produced from the same element.

Similarly, e.func(instance) will give you the constructor used to create this instance.

However, it is not recommended to compare e.func because note that the constructor may change between versions of a package without changing the element ID. That is, e.eid(a) == e.eid(b) might hold (they come from the same element), but e.func(a) == e.func(b) might not, if a came from package version 0.0.1 and b, from version 0.0.2.

Therefore, to check if two element instances belong to the same element, write e.eid(a) == e.eid(b).

Checking if elements are equal

To check if two element instances a and b are equal (same type of element and have the same fields), it is recommended to use e.eq:

#let my-elem-instance = elem(field: 5)

// AVOID (in packages): my-elem-instance == elem(field: 5)
#if e.eq(my-elem-instance, elem(field: 5)) {
  [They are equal!]
} else {
  [Nope, not equal]
}

Without e.eq, if the two elements come from different versions of the same package, a == b will be false, even if they have the same eid and fields.

Get rules

Set rules are not only useful to set default parameters. They are also useful for configuration more broadly. This is particularly useful for templates, which can use elements for fine-grained configuration.

Get rules (e.get) allow you to read the currently set values for each element's fields. Here's a basic example:

#import "@preview/elembic:1.1.0" as e

#show: e.set_(elem, count: 1, names: ("Robert",))
#show: e.set_(elem, count: 5, names: ("John", "Kate"))

// Output:
// "The chosen count is 5."
// "The chosen names are Robert, John, Kate."
#e.get(get => {
  [The chosen count is #get(elem).count.]
  [The chosen names are #get(elem).names.join[, ].]
})

Usage in templates

A template can use get rules for fine-grained settings. Check out the "Simple Thesis Template" example for a sample.

Query

Typst provides query(selector) for built-in Typst elements. The equivalent for custom elembic elements is e.query(filter), which, similarly, must be used within context { ... }. It returns a list of elements matching filter. Check "Filters" for information on filters.

For example:

#import "@preview/elembic:1.1.0" as e

#elem(fill: red, name: "A")
#elem(fill: red, name: "B")
#elem(fill: blue, name: "C")

#context {
  let red-elems = e.query(elem.with(fill: red))

  // This will be:
  // "Red element names: A, B"
  [Red element names: #red-elems.map(it => e.fields(it).name).join[, ]]
}

Restricting filter domains

Filters must be restricted to a finite set of potentially matching elements to be used with e.query.

This is only a problem with NOT and within filters, which could potentially match any elements. They can be restricted to certain elements with e.filters.and_(e.filters.or_(elem1, elem2), e.filters.not_(elem1.with(field: 5))) for example.

In addition, using e.within with e.query won't work as expected without using e.settings to manually enable ancestry tracking; see the last section of this page for details.

before and after

In Typst, for built-in elements, you can write query(selector(element).before(here())) to get all element instances before the current location, but not after. Similarly, using .after(here()) will restrict the query to elements after the current location, but not before.

For elembic elements, e.query has the parameters before: location and after: location (can be used simultaneously) for the same effect.

#import "@preview/elembic:1.1.0" as e

#elem()

#elem()

// Before: 2
// After: 1
#context [
  Before: #e.query(elem, before: here()).len()
  After: #e.query(elem, after: here()).len()
]

#elem()

Using e.within with query

The e.within filter, used to match nested elements, will not work with e.query unless both the queried element and its expected parent track ancestry, as per the rules in "Lazy ancestry tracking".

That is, e.query(e.filters.and_(elem1, e.within(elem2))) will return an empty list unless both elem1 and elem2 had ancestry tracking enabled before they were placed, e.g. due to the usage of rules containing e.within(elem1) and e.within(elem2). Otherwise, those elements will not provide the information e.query needs!

However, remember that ancestry tracking can be manually enabled by adding e.settings at the top of your document:

#import "@preview/elembic:1.1.0" as e

// Without the following, the query would return 0 results
#show: e.settings(track-ancestry: (child, parent))

#parent(child(name: "A"))
#child(name: "B")

#context {
  let nested-child = e.query(e.filters.and_(child, e.within(parent)))

  // "Nested child elements are A"
  // (Requires 'e.settings' at the top to work)
  [Nested child elements are #nested-child.map(it => e.fields(it).name).join[, ]]
}

Filters

Filters are used by rules such as show rules and filtered rules. They allow specifying which elements those rules should apply to.

They are mostly similar to Typst's selectors, including some of its operators (and_, or_), but with some additional operators: within to match children, not_ for negation, xor for either/or (not both), and custom to apply any condition.

Field filter

The most basic kind of filter, only matches elements with equal field values (checked with ==).

Create this filter with element.with(field: expected value, other field: expected value). All field values must match.

Note

Since == is used for comparisons, this means element.with(field: 5) will match both element(field: 5) and element(field: 5.0) as 5 == 5.0 (type conversions are possible).

For example, to change the supplement of theorems authored exclusively by Robert:

#show: e.cond-set(theorem.with(authors: ("Robert",)), supplement: [Robert Theorem])

// Uses the default supplement, e.g. just "Theorem"
#theorem(authors: ("John", "Kate"))[First Theorem]

// Uses "Robert Theorem" as supplement
#theorem(authors: ("Robert",))[Second Theorem]

Logic operators

Filters can be combined with logic operators. They are:

  1. e.filters.and_(filter 1, filter 2, ...): matches elements which match all filters at once.
  2. e.filters.or_(filter 1, filter 2, ...): matches elements which match at least one of the given filters.
  3. e.filters.xor(filter 1, filter 2): matches elements which match exactly one of the two given filters.
  4. e.filters.not_(filter): matches elements which do not match the given filter.

Consider the example below:

#import "@preview/elembic:1.1.0" as e
#import "package": container, theorem

#show: e.show_(  // replace elements...
  e.filters.or_( // ...matching at least one of the following filters:
    e.filters.and_( // Either satisfies all of the two conditions below:
      container, // 1. Is a container;
      e.filters.not_(container.with(fill: red)) // 2. Does not have a red fill;
    ),
    theorem.with(supplement: [Lemma]), // Or is a theorem tagged as a Lemma...
  ),

  // ...with the sentence "Matched (element name)!"
  it => [Matched #e.func-name(it)!]
)

// Not replaced: is a container, but has a red fill; and is not a theorem.
#container(fill: red)

// Replaced with "Matched container!"
#container(fill: blue)

Restricting NOT's domain

A NOT filter cannot be used with elembic filtered rules by default as it could match any element, e.g. e.filters.not_(elem.with(field: 5)) would not only match elem with a field different from 5, but also match any element that isn't elem, which is not viable for elembic.

To solve this, each NOT filter must be ultimately (directly or indirectly) wrapped within another filter operator that restricts its matching domain (usually AND).

For example, e.filters.and_(e.filters.or_(elem1, elem2), e.filters.not_(elem1.with(fill: red))) will match any elem2 instances, and elem1 instances with a non-red fill, but won't match elem3 for example, even though it would satisfy the NOT filter on its own. This filter can now be used in filtered rules!

Something similar occurs with custom filters and nested element filters.

Nested elements

Filters created with e.within(parent) can be used to match elements inside other elements, that is, returned at some level by the element's display() function, or by any show rules on it.

For example, e.filters.and_(theorem, e.within(container)) will match all theorem under container at any (known) depth.

Performance warning

This feature can have a potentially significant performance impact on elements repeated hundreds or thousands of times, being more or less equivalent to filtered rules in performance. Be mindful when using it. We do mitigate the performance impact through "lazy ancestry tracking", explained shortly below.

Restricting the domain

Similarly to NOT filters, e.within(parent) filters may be applied to any elements within parent in principle. Therefore, to use them in filtered rules and query, they must be used within filter operators which restrict which elements they may apply to, usually AND.

For example, e.filters.and_(e.filters.or_(elem1, elem2), e.within(parent)) will only match elem1 and elem2 instances within parent, which is something that elembic can work with for e.filtered and other rules taking filters.

Matching exact and max depth

You can choose to only match descendants at a certain exact depth, or at a maximum depth. This can be specified with e.within(elem, depth: 2) and e.within(elem, max-depth: 2).

For example, within parent(container(theorem())) and assuming container has ancestry tracking enabled (see Lazy ancestry tracking for when that might not be the case):

  • e.within(parent, depth: 1) matches only the container.
  • e.within(parent, depth: 2) matches only the theorem.
  • e.within(parent, max-depth: 1) matches only the container.
  • e.within(parent, max-depth: 2) matches both the container and the theorem.

On the other hand, if container does not have ancestry tracking enabled, it is effectively "invisible" and theorem is considered to have depth 1.

Lazy ancestry tracking

By default, elements do not keep track of ancestry (list of ancestor elements, used to match these filters) unless a rule using e.within is used. This is lazy ancestry tracking for short.

This means that descendants of elem are not known until the first usage of e.within(elem). Therefore:

  • If no e.within(elem) rules were used so far, it is not tracked by ancetsry, so theorem is considered as depth 1 in parent(elem(theorem())).
  • Queries with e.within(elem) don't work if no rules were used with e.within(elem) (and won't match elements under elem coming before those rules).

This is because ancestry tracking has a notable performance impact for repeated elements and has to be disabled by default.

To globally enable ancestry tracking without any rules, use e.settings(track-ancestry: (elem1, elem2, ...)) with a list of elements to enable it for:

// Force ancestry tracking for all instances of those elements in this scope
#show: e.settings(track-ancestry: (parent, container, theorem))

// This will now match properly even though `e.within(container)` isn't used
#show: e.show_(
  e.filters.and_(theorem, e.within(parent, depth: 2)),
  none
)

// Theorem here is hidden
#parent(container(theorem[Where did I go?]))

// This one is kept
#parent(theorem[I survived!])

// Same here
#theorem[I survived!]

Custom filters

These filters can be used to implement any arbitrary logic when matching elements by passing a simple function.

For example, e.filters.and_(mytable, e.filters.custom((it, ..) => it.cells.len() > 5)) will match any mytable with more than 5 cells.

The filtering function must return a boolean (true to match this element), and receives the following parameters for each potential element:

  1. Fields (positional).
  2. eid: "...": the element's eid. Useful if more than one kind of element is being tested by this filter. Use e.eid(elem) to retrieve the eid of an element for comparison.
  3. ancestry: (...): the ancestors of the current element, including (fields: (...), eid: "...")
  4. Extra named arguments which must be ignored with .. for forwards-compatibility with future elembic versions.

Warning

Don't forget the .. at the parameter list to ignore any potentially unused parameters (there can be more in future elembic updates).

Restricting the domain

Similarly to NOT filters, custom filters may be applied to any elements in principle. Therefore, they must be used within filter operators which restrict which elements they may apply to, usually AND.

For example, the following filter only applies to elem1 and elem2 instances, and checks different fields depending on which one it is:

e.filters.and_(
  e.filters.or_(elem1, elem2),
  e.filters.custom((it, eid: "", ..) => (
    eid == e.eid(elem1) and it.count > 5
    or eid == e.eid(elem2) and it.cells.len() < 10
  ))
)

Type system

In order to ensure type-safety for your element's fields, Elembic has its own type system which is worth being aware about. It not only allows you to customize how types are checked for each field, but even create your own, brand new types, much like you can create elements!

Purpose

Types are an important guarantee that users of your elements will specify the correct types for each field. However, note that Elembic's type-checking utilities can be used anywhere, not only for elements!

Note that, for elements, when a field is created with field, it is necessary to specify a field name and a type, or any to accept any type:

#let elem = e.element.declare(
  // ...
  fields: (
    field("amount", int /* <--- here! */, required: true),
    field("anything", e.types.any, default: none)  // <--- anything goes!
  )
)

#elem(5)  // OK!
// #elem("abc")  // Error: "expected integer, found string"
#elem(5, anything: "string")  // OK!
#elem(5, anything: 20pt)  // OK!

Native types (such as int) or any are not the only types which can be specified for element fields. In general, anything that is representable in the typeinfo format, described below, can be used as a field type.

Typeinfo

"Typeinfo" is the structure (represented as a Typst dictionary) that describes, in each field:

  1. type-kind: The kind of a type - "native" for native types, "custom" for custom types, and other values for other special types (such as "any", "never", "union" for the output of types.union, "wrapped" as the output of types.wrap, "collection" for types.array, and so on);
  2. name: the name of a type;
  3. input: which basic types may be cast to it (e.g.: integer or string). This uses the type ID format for custom types, obtained with types.typeid(value), of the form (tid: ..., name: ...). For native types, the output of type(value) is used;
  4. output: which basic types the input may be cast to (e.g.: just string). This uses the same format as input;
  5. check: an extra check (function input value => true / false) that tells whether a basic input type may be cast to this type, or none for no such check;
  6. cast: an optional function to cast an input value that passed the check to an output value (function input value => output value). Must always succeed, or panic. If none, the input type is kept unchanged as an output after casting to this type. An integer would remain an integer without a cast, for example (which may be the intention).
  7. error: an optional function that is called when the check fails (input value => string), indicating why the check may have failed.
  8. default: the default value for the type when using it for a field. This is used if the field isn't required and the element author didn't specify a default. This must either be an empty array () for no default, or a singleton array (value,) (note the trailing comma) to specify a default.
  9. fold: an optional function (outer output value, inner output value) => new output value to indicate how to combine two consecutive values of this type, where inner is more prioritized over outer. For example, for the stroke type, one would have (5pt, black) => 5pt + black, but (5pt + black, red) => 5pt + red since red is the inner value in that case. If this is none, the inner value always completely overrides the outer value (the default behavior). This is used in set rules: they don't completely override the value of the field if its type has fold.
    • Note that fold may also be auto to mean (a, b) => a + b, for performance reasons.
  10. data: some extra data giving an insight into how this typeinfo was generated. What this is varies per kind of typeinfo. For example, for native types such as int, this will be the native type itself.

Special types

There are some special types which don't correspond precisely to a single native or custom type, but have special behavior.

types.any: Accepting any values

You can use types.any to ensure a field can receive any value (disables typechecking, folding, and any kind of special behavior).

types.never: Accepts no values

This exists more for completeness, but this is a type with no input types, so it is impossible to cast to it.

types.literal: Accepting only a single value of a type

You can use types.literal("abc") to only accept values equal to "abc". As a shorthand, you can write just "abc" as the type for the field (unless it is a dictionary or function, as it'd then be ambiguous with other types).

To accept more than one literal, you can use types.union, e.g. types.union("abc", "def") for either "abc" or "def".

Note that casting is preserved: types.literal(5.0) will accept either the integer 5 or the float 5.0. To accept just the float 5.0, you can use types.exact(types.literal(5.0)), for example.

types.custom-type: Accept custom types themselves

To accept any custom type as a value, you can use types.custom-type. However, note that, due to ambiguity when passing functions, the user will have to pass e.data(the-custom-type-constructor) instead of just the constructor itself for the cast to work / for the field to typecheck.

The same goes for literals: to receive one of multiple custom types, you'll have to use union(literal(e.data(type1)), literal(e.data(type2))). Note that you have to use literal to disambiguate.

For native types, you can just use type as the type, and they will be accepted without any special ceremony by the user (they can pass int to a type field and it works). However, note that writing literal(int) explicitly, for example, is still required to only accept certain native types' constructors.

Native and custom elements are not currently supported on their own, although you may have some success in accepting function.

Type combinators

Here's some information about some special types combining other types.

types.union: Either of multiple types

You can use types.union(int, str) to indicate that a field depends on either an integer or a string.

Note

Unions are ordered. This means that types.union(int, float) != types.union(float, int).

This is relevant when two or more types in the union can accept the same native type, with differing checks or casts. In the case of int and float, the integer 5 will remain the integer 5 when casting to types.union(int, float), but will be casted to the float 5.0 when casted to types.union(float, int). (Of course, a float such as 4.0 will remain a float in both cases, since it isn't accepted by int).

Optional and smart types

For fields that can be set to none to indicate absence, use types.option(typ). This is the same as types.union(none, typ).

For fields with a smart default indicated by auto, use types.smart(typ). This is the same as types.union(auto, typ).

You can also combine both: types.option(e.types.smart(typ)) is the same as types.union(none, auto, typ).

Folding in unions

Folding is preserved in unions unless it's ambiguous. For example, it is preserved for types.union(int, stroke, array): two arrays of this type are joined, a length and a color are cast to stroke and combined into a single length + color stroke, and integers have no folding and stay that way (the latest integer has priority).

However, if you have types.union(types.array(int), types.array(float)), folding is disabled (the latest array overrides the previous) as it is not straightforward to tell to which type an array could belong, so we avoid creating an invalid instance of this type (which could happen if we joined an int array with a float array).

types.exact: Disable casting for a type

You can use types.exact(typ) to ensure there is no casting involved for this type. For example, types.exact(float) ensures integers won't cast to floats (they are normally accepted). Also, types.exact(stroke) ensures only stroke(5pt) can be passed to a field with that type, not 5pt itself. Finally, types.exact(my-custom-type), where my-custom-type has custom casts from existing types, disables those casts, allowing only an instance of my-custom-type itself to be used for a field with that type.

types.array: Array of a type

You can use types.array(typ) to accept arrays of elements of the same type.

types.dict: Dictionary with values of a type

You can use types.dict(typ) to accept dictionaries with values of the same type. (Note that dictionary keys are all strings.)

For example, (a: 5, b: 6) is a valid dict(int), but not a valid dict(str).

Wrapping types

You may use the types.wrap(type, ..overrides) to override certain behaviors and properties of a type.

For each override, if you pass a function, you receive the old value and must return the new value.

Some examples:

  1. Positive integers: You can use let pos-int = types.wrap(int, check: old-check => value => value > 0) to only accept positive integers.
  2. New default: You can use types.wrap(int, default: (5,)) to have an int type with a default value of 5 (note that we use an array with a single element, as opposed to () (empty array) which means no default);
    • If you're using this for a single field, considering specifying e.field(..., default: new default) instead.
  3. Always casting integers: Use types.wrap(int, output: (float,), cast: old-cast => float) to only accept integers, but cast all of them to floats.
    • Note that we overrode output to indicate that only floats can be returned now (notably, not integers).

Overriding a function with another

If an override must set a property to a function, due to ambiguity with the notation above, it must be a function that returns the new function, e.g. cast: old-cast => new-cast where new-cast can be some-input => casted output value.

Warning

Make sure to follow the typeinfo format in the chapter's top-level page. Invalid overrides, such as malformed casts, may lead to elembic behaving incorrectly.

There are safeguards for the most common potential mistakes, but some mistakes cannot be caught, such as misbehaving cast and fold functions.

In particular:

  • If you override cast and/or fold, make sure to also override output: (type 1, type 2, ...). Anything that can be returned by cast or fold must be listed as an output type, e.g. output: (int, float) if both can only return integers or floats. Do not return a type outside output.

    • You can also use output: ("any",) in an extreme case, but this is discouraged.
  • If you override check or input, make sure to also adjust output like above, especially if your new check is more permissive than the previous one.

Native types

Typst-native types, such as int and str, are internally represented by typeinfos of "native" type-kind, which can be obtained with e.types.native.typeinfo(type). They can generally be specified directly on type positions (e.g. types.union(int, float)) without using that function, as Elembic will automatically convert them.

Tip

For fill-like fields, there is also e.types.paint, an alias for types.union(color, gradient, tiling).

Casting

Of note, some native types, such as float, stroke and content, supporting casting, e.g. str | none => content, int => float and length => stroke. This means you can pass a string to a content-type field and it will be accepted and converted to content.

You can use e.types.exact to disable casting for a type.

Folding

In addition, some native types support folding, a special behavior when specifying consecutive set rules over the same field with that type. The most notable one is stroke: specifying a stroke of 4pt and then black generates 4pt + black. There is also array: specifying an array (2, 3) and then (4, 5) on set rules generates (2, 3, 4, 5) at the end. Finally, alignment is worthy of mention: specifying left + bottom, right and then top, in that order, generates the final value of right + top.

You can disable folding with e.types.wrap, setting fold: none.

Elements as types

Custom elements

Custom elements can be used directly as types (in fields etc.) to specify that you only want to accept a certain custom element as input. Note that you can use a union to accept more than one custom element.

#import "@preview/elembic:1.1.0" as e

#let elem = e.element.declare(...)

#assert.eq(
  e.types.cast(
    elem(field: 5),
    elem
  ),
  (true, elem(field: 5))
)

Native elements

You can use e.types.native-elem(native element function) to only accept instances of a particular native element.

For example, e.types.native-elem(heading) only accepts headings. (You can use a union to accept more than one native element.)

#import "@preview/elembic:1.1.0" as e

#assert.eq(
  e.types.cast(
    [= hello!],
    e.types.native-elem(heading)
  ),
  (true, [= hello!])
)

Helper functions

You can use types.cast(element, type) to try to cast an element to a type; this will return either (true, casted-value) or (false, error-message).

There are also types.typeid(value) to obtain the "type ID" of this value (its type if it's a native type instance, or (tid: ..., name: ...) if it's an instance of a custom type, as well as "custom type" if it's a custom type literal obtained with e.data(custom type)), which is the format used in input and output.

In addition, types.typename(value) returns the name of the type of that value as a string, similar to str(type(native type)) but extended to custom types.

Finally, types.typeinfo(type) will try to obtain a typeinfo object from that type (always succeeds if it's a typeinfo object by itself), returning (true, typeinfo) on success and (false, error-message) on failure.

Custom types

Elembic supports creating your own custom types, which are used to represent data structures with specific formats and fields. They do not compare equal to existing types, not even to dictionaries, even though they are themselves represented by dictionaries. They have their own unique ID based on prefix and name, similar to custom elements. It is assumed that custom types with the same unique ID are equal, so it should be changed if breaking changes ensue.

Custom types can be used as the types of fields in elements, or on their own through types.cast.

Custom types have typechecked fields in the constructor and support casting from other types, meaning you can accept e.g. an integer for a field taking a custom type.

Declaring a custom type

You can use e.types.declare. Make sure to specify a unique prefix to distinguish your type from others with the same name.

You should specify fields created with e.field. They can have an optional documentation with doc.

#import "@preview/elembic:1.1.0" as e: field, types

#let person = e.types.declare(
  "person",
  prefix: "@preview/my-package,v1",
  doc: "Relevant data for a person.",
  fields: (
    field("name", str, doc: "Person's name", required: true),
    field("age", int, doc: "Person's age", default: 40),
    field("preference", types.any, doc: "Anything the person likes", default: none)
  ),
)

#assert.eq(
  e.repr(person("John", age: 50, preference: "soup")),
  "person(age: 50, preference: \"soup\", name: \"John\")"
)

Your type, in this case person, can then be used as the type of an element's field, or used with e.types.cast in other scenarios.

Take a look at the following chapters, such as Casts, to read about more options that can be used to customize your new type.

Equality

To check if two instances of your custom type are equal, consider using:

  1. e.tid(a) == e.tid(b) to check if both variables belong to the same custom type.
  2. e.eq(a, b) to check if both variables have the exact same type and fields.
    • This checks only tid and fields recursively, ignoring changes to other custom type data between package versions, and so is safer.
    • Although it is slower than == for very complex types, so you can use a == b instead for private types, or for templates.

Casts

You can add casts from native types (or any types supported by the type system, such as literals) to your custom type, allowing fields receiving your type to also accept the casted-from types.

Dictionary cast

The simplest cast, allows casting dictionaries to your type when they have the correct structure. In summary, the dictionary's keys must correspond to named fields in your type. To use its default implementation, simply add casts: ((from: dictionary),) and the rest is sorted out. (You can add other casts, as explained below, by adding more casts to that list.)

Fails if there are required positional fields.

For example:

#import "@preview/elembic:1.1.0" as e: field, types

#let person = e.types.declare(
  "person",
  prefix: "@preview/my-package,v1",
  doc: "Relevant data for a person.",
  fields: (
    // All fields named, one required
    field("name", str, doc: "Person's name", required: true, named: true),
    field("age", int, doc: "Person's age", default: 40),
    field("preference", types.any, doc: "Anything the person likes", default: none)
  ),
  casts: ((from: dictionary),) // <-- note the comma!
)

#assert.eq(
  types.cast((name: "Johnson", age: 20, preference: "ice cream"), person),
  // Same as using the default constructor
  (true, person(name: "Johnson", age: 20, preference: "ice cream"))
)

Custom casts

Additional casts are given by the casts: (cast1, cast2, ...) parameter. Each cast takes at least (from: typename, with: constructor => value => constructor(...)), where value was already casted to typename beforehand (e.g. if typename is float, then value will always have type float, even if the user passes an integer). It may optionally take check: value => bool as well to only accept that typename if check(value) is true.

In the future, automatic casting from dictionaries will be supported (although it can already be manually implemented).

Note

Casts are ordered. This means that specifying a cast from int and then float is different from specifying a cast from float followed by int, for example.

This is relevant when two or more types in the union can accept the same native type as input, with differing checks or casts. In the case of int and float, the integer 5 will trigger the cast from int as you'd expect if the int cast comes first, but will converted to 5.0 before triggering the cast from float if the float cast is specified first. (Of course, a float such as 4.0 will trigger the cast from float in both cases, since it isn't accepted by int).

These principles are made evident in the example below:

#import "@preview/elembic:1.1.0" as e: field, types

#let person = e.types.declare(
  "person",
  prefix: "@preview/my-package,v1",
  doc: "Relevant data for a person.",
  fields: (
    field("name", str, doc: "Person's name", required: true),
    field("age", int, doc: "Person's age", default: 40),
    field("preference", types.any, doc: "Anything the person likes", default: none)
  ),
  casts: (
    (from: "Johnson", with: person => name => person(name, age: 45)),
    (from: str, check: name => name.starts-with("Alfred "), with: person => name => person(name, age: 30)),
    (from: str, with: person => name => person(name)),
  )
)

// Manually invoke typechecking and cast
// Notice how the first succeeding cast is always made
#assert.eq(
  types.cast("Johnson", person),
  (true, person("Johnson", age: 45))
)
#assert.eq(
  types.cast("Alfred Notexistent", person),
  (true, person("Alfred Notexistent", age: 30))
)
#assert.eq(
  types.cast("abc", person),
  (true, person("abc", age: 40))
)

Wait, that sounds a lot like a union!

That's right: most of the casting generation code is shared with union! The union code also contains optimizations for simple types, which we take advantage of here.

The main difference here is that these casts become part of the custom type itself. This means that they will always be there when using this custom type as the type of a field.

However, it's possible to override this behavior: users of the type can disable the casts by wrapping the custom type in the types.exact combinator.

Other custom type options

Folding

If all of your fields may be omitted (for example), or if you just generally want to be able to combine fields, you could consider adding folding to your custom type with fold: auto, which will combine each field individually using their own fold methods. You can also use fold: default constructor => (outer, inner) => combine inner with outer, giving priority to inner for full customization.

Custom constructor and argument parsing

Much like elements, you can use construct: default-constructor => (..args) => value to override the default constructor for your custom type. You should use construct: rather than create a wrapper function to ensure that data retrieval functions, such as e.data(func), still work.

You can use parse-args: (default arg parser, fields: dictionary, typecheck: bool) => (args, include-required: true) => dictionary with fields to override the built-in argument parser to the constructor (instead of overriding the entire constructor). include-required is always true and is simply a remnant from elements' own argument parser (which share code with the one used for custom types).

Argument sink

Here's how you'd use this to implement a positional argument sink (receiving a variable amount of positional arguments):

#let sunk = e.types.declare(
  "sunk",
  doc: "A test type to showcase argument sink",
  fields: (
    field("values", e.types.array(stroke), required: true),
    field("color", color, default: red),
    field("inner", content, default: [Hello!]),
  ),
  parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: true) => {
    let args = if include-required {
      // Convert positional arguments into a single 'values' argument
      let values = args.pos()
      arguments(values, ..args.named())
    } else if args.pos() == () {
      // 'include-required' is always true for types, but keeping these here
      // just for completeness
      args
    } else {
      assert(false, message: "element 'sunk': unexpected positional arguments\n  hint: these can only be passed to the constructor")
    }

    default-parser(args, include-required: include-required)
  },
  prefix: ""
)

#assert.eq(
  e.fields(sunk(5pt, black, 5pt + black, inner: [Inner])),
  (values: (stroke(5pt), stroke(black), 5pt + black), inner: [Inner], color: red)
)

Reference

This chapter contains information about top-level constants and functions exported by each module in elembic.

This chapter is a work in progress. PRs appreciated.

Element functions

At the moment, all of the functions in this module are exported exclusively at the top-level of the package, other than declare which must be used as e.element.declare.

Declaration

e.element.declare

Creates a new element, returning its constructor. Read the "Creating custom elements" chapter for more information.

Signature:

#e.declare(
  name,
  prefix: str,
  doc: none | str,
  display: function,
  fields: array,
  parse-args: auto | function(arguments, include-required: bool) -> dictionary = auto,
  typecheck: bool = true,
  allow-unknown-fields: bool = false,
  template: none | function(displayed element) -> content = none,
  prepare: none | function(elem, document) -> content = none,
  construct: none | function(constructor) -> function(..args) -> content = none,
  scope: none | dictionary | module = none,
  count: none | function(counter) -> content | function(counter) -> function(fields) -> content = counter.step,
  labelable: auto | bool = auto,
  reference: none | (supplement: none | content | function(fields) -> content, numbering: none | function(fields) -> str | function, custom: none | function(fields) -> content) = none,
  outline: none | auto | (caption: content) = none,
  synthesize: none | function(fields) -> synthesized fields,
  contextual: bool = false,
) -> function

Arguments:

  • name: The element's name.
  • prefix: The element's prefix, used to distinguish it from elements with the same name. This is usually your package's name alongside a (major) version.
  • doc: The element's documentation, if any.
  • display: Function fields => content to display the element.
  • fields: Array with this element's fields. They must be created with e.field(...).
  • parse-args: Optional override for the built-in argument parser (or auto to keep as is). Must be in the form function(args, include-required: bool) => dictionary, where include-required: true means required fields are enforced (constructor), while include-required: false means they are forbidden (set rules).
  • typecheck: Set to false to disable field typechecking.
  • allow-unknown-fields: Set to true to allow users to specify unknown fields to your element. They are not typechecked and are simply forwarded to the element's fields by the argument parser.
  • template: Optional function displayed element => content to define overridable default set rules for your elements, such as paragraph settings. Users can override these settings with show-set rules on elements.
  • prepare: Optional function (element, document) => content to define show and set rules that should be applied to the whole document for your element to properly function.
  • construct: Optional function that overrides the default element constructor, returning arbitrary content. This should be used over manually wrapping the returned constructor as it ensures set rules and data extraction from the constructor still work.
  • scope: Optional scope with associated data for your element. This could be a module with constructors for associated elements, for instance. This value can be accessed with e.scope(elem), e.g. #import e.scope(elem): sub-elem.
  • count: Optional function counter => (content | function fields => content) which inserts a counter step before the element. Ensures the element's display function has updated context to get the latest counter value (after the step / update) with e.counter(it).get(). Defaults to counter.step to step the counter once before each element placed.
  • labelable: Set this to true to support outer label syntax: #elem(...) <label-here>. The downsides are that #show: e.prepare() becomes required to use the element, the element can no longer be inline, and show rules on the individual labels no longer have access to final fields. Defaults to auto, which still allows labeling without those downsides by specifying #element(label: <abc>), ensuring show rules on that label work and have access to the element's final fields. In both cases, also allows referring to labeled elements with @chosen-label (requires #show: e.prepare() to work), but the element may not have its own settable field named label.
  • reference: When not none, allows referring to the new element with Typst's built-in @ref syntax. Requires the user to execute #show: e.prepare() at the top of their document (it is part of the default rules, so prepare needs no arguments there). Specify either a supplement and numbering for references looking like "Name 2", and/or custom to show some fully customized content for the reference instead.
  • outline: When not none, allows creating an outline for the element's appearances with #outline(target: e.selector(elem, outline: true)). When set to auto, the entries will display "Name 2" based on reference information. When a caption is specified via a dictionary, it will display as "Name 2: caption", unless supplement and numbering for reference are both none, in which case it will only display caption.
  • synthesize: Can be set to a function to override final values of fields, or create new fields based on final values of fields, before the first show rule. When computing new fields based on other fields, please specify those new fields in the fields array with synthesized: true. This forbids the user from specifying them manually, but allows them to filter based on that field.
  • contextual: When set to true, functions fields => something for other options, including display, will be able to access the current values of set rules with (e.ctx(fields).get)(other-elem). In addition, an additional context block is created, so that you may access the correct values for native-elem.field in the context. In practice, this is a bit expensive, and so this option shouldn't be enabled unless you need precisely bibliography.title, or you really need to get set rule information from other elements within functions such as synthesize or display.

Example:

#import "@preview/elembic:1.1.0" as e: field

// For references to apply
#show: e.prepare()

#let elem = e.element.declare(
  "elem",
  prefix: "@preview/my-package,v1",
  display: it => {
    [== #it.title]
    block(fill: it.fill)[#it.inner]
  },
  fields: (
    field("fill", e.types.option(e.types.paint)),
    field("inner", content, default: [Hello!]),
    field("title", content, default: [Hello!]),
  ),
  reference: (
    supplement: [Elem],
    numbering: "1"
  ),
  outline: (caption: it => it.title),
)

#outline(target: e.selector(elem, outline: true))

#elem()
#elem(title: [abc], label: <abc>)
@abc

Rules and styles

e.apply

Apply multiple rules (set rules, etc.) at once.

These rules do not count towards the "set rule limit" observed in Limitations; apply itself will always count as a single rule regardless of the amount of rules inside it (be it 5, 50, or 500). Therefore, it is recommended to group rules together under apply whenever possible.

Note that Elembic will automatically wrap consecutive rules (only whitespace or native set/show rules inbetween) into a single apply, bringing the same benefit.

Signature:

#e.apply(
  ..rules: e.apply(...) | e.set_(...) | e.revoke(...) | e.reset(...),
  mode: auto | style-modes.normal | style-modes.leaky | style-modes.stateful = auto
) -> function

Example:

#show: e.apply(
  set_(superbox, fill: red),
  set_(superbox, width: 100)
)

e.get

Reads the current values of element fields after applying set rules.

The callback receives a 'get' function which can be used to read the values for a given element. The content returned by the function, which depends on those values, is then placed into the document.

Signature:

#e.get(
  receiver: function(function) -> content
) -> content

Example:

#show: e.set_(elem, fill: green)
// ...
#e.get(get => {
  // OK
  assert(get(elem).fill == green)
})

e.named

Name a certain rule. Use e.apply to name multiple rules at once. This is used to be able to revoke the rule later with e.revoke.

Please note that, at the moment, each rule can only have one name. This means that applying multiple named on the same set of rules will simply replace the previous names.

However, more than one rule can have the same name, allowing both to be revoked at once if needed.

Signature:

#e.named(
  name: str,
  rule: e.apply(...) | e.set_(...) | e.revoke(...) | e.reset(...),
) -> function

Example:

#show: e.named("cool rule", e.set_(elem, fields))

e.prepare

Applies necessary show rules to the entire document so that custom elements behave properly. This is usually only needed for elements which have custom references, since, in that case, the document-wide rule #show ref: e.ref is required. It is recommended to always use e.prepare when using Elembic.

However, some custom elements also have their own prepare functions. (Read their documentation to know if that's the case.) Then, you may specify their constructors as parameters to this function, and this function will run the prepare function of each element. Not specifying any elements will just run the default rules, which may still be important.

As an example, an element may use its own prepare function to apply some special behavior to its outline.

Signature:

#e.prepare(
  ..elems: function
) -> function

Example:

// Apply default rules + special rules for these elements (if they need it)
#show: e.prepare(elemA, elemB)

// Apply default rules only (enable custom references for all elements)
#show: e.prepare()

e.ref

This is meant to be used in a show rule of the form #show ref: e.ref to ensure references to custom elements work properly.

Please use e.prepare as it does that automatically, and more if necessary.

Signature:

#e.ref(
  ref: content
) -> content

Example:

#show ref: e.ref

e.reset

Temporarily revoke all active set rules for certain elements (or even all elements, if none are specified). Applies only to the current scope, like other rules.

Signature:

#e.reset(
  ..elems: function,
  mode: auto | style-modes.normal | style-modes.leaky | style-modes.stateful = auto
) -> function

Example:

#show: e.set_(element, fill: red)
#[
  // Revoke all previous set rules on 'element' for this scope
  #show: e.reset(element)
  #element[This is using the default fill (not red)]
]

// Rules not revoked outside the scope
#element[This is using red fill]

e.revoke

Revoke all rules with a certain name, temporarily disabling their effects within the current scope. This is supported for named set rules, reset rules and even revoke rules themselves (which prompts the originally revoked rules to temporarily apply again).

This is intended to be used temporarily, in a specific scope. This means you are supposed to only revoke the rule for a short portion of the document. If you wish to do the opposite, that is, only apply the rule for a short portion for the document (and have it never apply again afterwards), then please just scope the set rule itself instead.

You should use e.named to add names to rules.

Signature:

#e.revoke(
  name: str,
  mode: auto | style-modes.normal | style-modes.leaky | style-modes.stateful = auto
) -> function

Example:

#show: e.named("name", set_(element, fields))
...
#[
  #show: e.revoke("name")
  // rule 'name' doesn't apply here
  ...
]

// Applies here again
...

e.select

Prepare Typst-native selectors which only match elements with a certain set of values for their fields. Receives filters in the format element.with(field: A, other-field: B). Note that the fields must be specified through their names, even if they are usually positional. These filters are similar in spirit to native elements' element.where(..fields) selectors.

For each filter specified, an additional selector argument is passed to the callback function. These selectors can be used for show-set rules. Note that #show sel: set (...) will only apply to the element's body (which is usually fine). In addition, rules applied as #show sel: e.set_(...) are applied in reverse due to how Typst works, so consider using filtered rules for that instead.

You must wrap the remainder of the document that depends on those selectors as the value returned by the callback.

It is thus recommended to only use this function once, at the very top of the document, to get all the needed selectors. This is because this function can only match elements within the returned callback. Elements outside it are not matched by the selectors, even if their fields' values match.

Signature:

#e.select(
  ..filters: element.with(one-field: expected-value, another-field: expected-value),
  receiver: function(..selectors) -> content,
  prefix: str
) -> content

Example:

#e.select(prefix: "@preview/my-package/1", superbox.with(fill: red), superbox.with(width: auto), (red-superbox, auto-superbox) => {
  show red-superbox: set text(red)
  show auto-superbox: set text(red)

  #superbox(fill: red)[Red text]
  #superbox(width: auto)[Red text]
  #superbox(fill: green, width: 5pt)[Not red text]
})

e.set_

Apply a set rule to a custom element. Check out the Styling guide for more information.

Note that this function only accepts non-required fields (that have a default). Any required fields must always be specified at call site and, as such, are always be prioritized, so it is pointless to have set rules for those.

Keep in mind the limitations when using set rules, as well as revoke, reset and apply rules.

As such, when applying many set rules at once, please use e.apply instead (or specify them consecutively so elembic does that automatically).

Signature:

#e.set_(
  elem: function,
  ..fields
)

Example:

#show: e.set_(superbox, fill: red)
#show: e.set_(superbox, optional-pos-arg1, optional-pos-arg2)

// This call will be equivalent to:
// #superbox(required-arg, optional-pos-arg1, optional-pos-arg2, fill: red)
#superbox(required-arg)

e.style-modes

Dictionary with an integer for each style mode:

  • normal (normal mode - default): limit of ~30 non-consecutive rules.
  • leaky (leaky mode): limit of ~60 non-consecutive rules.
  • stateful (stateful mode): no rule limit, but slower.

You will normally not use these values directly, but rather e.g. use e.stateful.set_(...) to use a stateful-only rule.

Read limitations for more information.

Data retrieval functions

Functions used to retrieve data from custom elements, custom types, and their instances.

Main functions

e.data

This is the main function used to retrieve data from custom elements and custom types and their instances. The other functions listed under "Helper functions" are convenient wrappers over this function to reduce typing. It receives any input and returns a dictionary with one of the following values for the data-kind key:

  1. "element": dictionary with an element's relevant parameters and data generated it was declared. This is the data kind returned by e.data(constructor). Importantly, it contains information such as eid for the element's unique ID (combining its prefix and name, used to distinguish it from elements with the same name), sel for the element's selector, outer-sel for the outer selector (used exclusively for show-set rules), counter for the element's counter, func for the element's constructor, fields for a parsed list of element fields, and more.

  2. "custom-type-data": conceptually similar to "element" but for custom types, including their data at declaration time. This is returned by e.data(custom-type-constructor). Contains tid for the type's unique ID (combining prefix and name), as well as typeinfo for the type's typeinfo, fields with field information and func for the constructor.

  3. "element-instance": returned from e.data(it) in a show rule on custom elements, or when using e.data(some-element(...)). Relevant keys here include eid for the element ID (prefix + name), func for the element's constructor, as well as fields, dictionary containing the values specified for each field for this instance.

    It also contains body, which, in show rules, contains the value returned by the element's display function (the element's effective body), but note that the body shouldn't be placed directly; you should return it from the show rule to preserve the element. You usually will never need to use body. In addition, outside of show rules, it is rather meaningless and only contains the element itself.

    Note that, in show rules, the returned data will have fields-known: true which indicates that the final values of all fields, after synthesizing and set rules are applied, are known and stored in fields. Outside of show rules, however (e.g. on recently-constructed elements), the dictionary will have fields-known: false indicating that the dictionary of fields is incomplete and only contains the arguments initially passed to the constructor, as set rules and default values for missing fields haven't been applied yet.

    Note also that this data kind is also returned when applying to elements matched in a show rule on filtered selectors returned by e.select.

  4. "type-instance": similar to "element-instance", except fields are always known and complete since there are no show or set rules for custom types, so e.data(my-type(...)) will always have a complete set of fields, as well as tid for the type's ID and func for its constructor.

  5. "incomplete-element-instance": this is only obtained when trying to e.data(it) on a show rule on an element's outer selector (obtained from e.selector(elem, outer: true) or e.data(elem).outer-sel). The only relevant information it contains is the eid of the matched element. The rest is all unknown.

  6. "content": returned when some arbitrary content with native elements is received. In this case, eid will be none, but func will be set to it.func(), fields will be set to it.fields() and body will be set to it (the given parameter) itself.

  7. "unknown": something that wasn't content, an element, a custom type, or their instances was given. For example, an integer (no data to extract).

Signature:

#e.data(
  any
) -> dictionary

Example:

// Element
// (Equivalent to e.data(elem).sel)
#let sel = e.selector(elem)

// Instance
#show sel: it => {
  // (Equivalent to e.data(it).fields)
  let fields = e.fields(it)
  [Given color: #fields.color]

  [Counter value is #e.counter(elem).display("1.")]

  // (Equivalent to e.data(it).eid, ...)
  assert.eq(e.eid(it), e.eid(elem))
  // (Equivalent to e.data(it).func)
  assert.eq(e.func(it), elem)

  it
}

// Custom type data
#let name = e.data(my-type).name
#let typeinfo = e.data(my-type).typeinfo

// Custom type instance data
#assert.eq(e.func(my-type(...)), my-type)

// Equivalent to e.data(my-type(a: 5, b: 6)).fields
#assert.eq(e.fields(my-type(a: 5, b: 6)), (a: 5, b: 6))

e.repr

This is used to obtain a debug representation of custom types.

In the future, this will support elements as well.

Also supports native types (just calls repr() for them).

Signature:

#e.repr(
  any
) -> str

Example:

// -> "person(name: \"John\", age: 40)"
#e.repr(person(name: "John", age: 40))

Helper functions

Functions which simply wrap and return some specific data.

e.counter

Helper function to obtain an element's associated counter.

This is a wrapper over e.data(arg).counter.

Signature:

#e.counter(
  element
) -> counter

Example:

#e.counter(elem).step()

#context e.counter(elem).display("1.")

e.ctx

Helper function to obtain context from element instances within show rules and functions used by e.element.declare.

This is only available if the element was declared with contextual: true, as it is otherwise expensive to store. When available, it is a dictionary containing (get: function), where get(elem) returns the current default values of fields for elem considering set rules.

This is a wrapper over e.data(arg).ctx.

Signature:

#e.ctx(
  any
) -> dictionary | none

Example:

// For references to work
#show: e.prepare()

#let elem = e.element.declare(
  "elem",
  prefix: "@preview/my-package,v1",
  contextual: true,
  reference: (
    custom: it => [#(e.ctx(it).get)(other-elem).field]
  )
)

#elem(label: <a>)

// Will display the value of the other element's field
@a

e.eid

Helper function to obtain an element's unique ID (modified combination of prefix + name).

Can also be used on element instances.

This is a wrapper over e.data(arg).eid.

Signature:

#e.eid(
  any
) -> str | none

Example:

e.fields

Helper function to obtain an element or custom type instance's fields as a dictionary.

For elements, this will be incomplete outside of show rules. For custom types, it is always complete.

This is a wrapper over e.data(arg).fields.

Signature:

#e.fields(
  any
) -> dictionary | none

Example:

#assert.eq(e.fields(my-elem(a: 5)), (a: 5))

e.func

Helper function to obtain the constructor used to create an element or custom type instance.

Can also be used on elements or custom types themselves.

This is a wrapper over e.data(arg).func.

Signature:

#e.func(
  any
) -> function | none

e.func-name

Get the name of a content's constructor function as a string.

Returns none on invalid input.

Signature:

#e.func-name(
  content | custom element function
) -> function | none

Example:

assert.eq(func-name(my-elem()), "my-elem")
assert.eq(func-name([= abc]), "heading")

e.scope

Helper function to obtain an element or custom type's associated scope.

This is a wrapper over e.data(arg).scope.

Signature:

#e.scope(
  any
) -> module | dictionary | none

Example:

#import e.scope(elem): sub-elem

#sub-elem(...)

e.selector

Returns one of the element's selectors:

  • By default, returns its main selector, used for show rules and querying. This is equivalent to e.data(arg).sel.
  • With outer: true, returns a selector that can be used for show-set rules. This is equivalent to e.data(arg).outer-sel.
  • With outline: true, returns a selector that can be used in outline(target: /* here */) for outlinable elements. This is equivalent to e.data(arg).outline-sel.

Signature:

#e.selector(
  element,
  outer: bool = false,
  outline: bool = false,
) -> label | selector | none

Example:

#show: e.show_(elem, strong)
#show e.selector(elem, outer: true): set par(justify: false)

#outline(target: e.selector(elem, outline: true))

e.tid

Helper function to obtain the type ID of a custom type, or of an instance's custom type.

This is a wrapper over e.data(arg).tid.

Signature:

#e.tid(
  any
) -> str | none