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:
- Grouping rules together with
apply
- Scoping temporary rules and not misusing
revoke
- (As a last resort) Switching to either of the other styling modes (
leaky
orstateful
)
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):
- 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.
- 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, anybibliography.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 replacee.set_(element, field: value)
withe.leaky.set_(element, field: value)
. (You can create an alias such asimport e: leaky as lk
and thenlk.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.)
- Alternatively, you can also replace all set rules with their
- 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):
-
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.
- This is used to inform rules in normal mode that they should read and write to the
-
Replacing existing set rules with their stateful-only counterparts. That is, replace all occurrences of
e.set_
,e.apply
,e.revoke
etc. withe.stateful.set_
,e.stateful.apply
ande.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 writest.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.
- While a Ctrl+F fixes it, it's a bit of a mouthful, so you may want to consider aliasing
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 speciallabel
field altogether.
- Elements with
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 asdisplay: it => e.get(get => x)
(instantly returnse.get
), the body is simplified to justx
(get => ...
is called internally by the element code), reducing amount ofcontext {}
nesting. - Custom types now have
fold: auto
by default, merging their fields between set rules types.wrap
now has an additional safeguard to error whenfold:
needs to be updated- Moved
ok
,err
,is-ok
frome.types
toe.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
- Now using
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 twoselect
s are not nested, but instead siblings. So make sure to add something likee.select(prefix: "@preview/your-package/level 1" ...)
to ensure eachselect
is distinct. There is no risk of clashing when oneselect
is inside the other.
- NOTE: since alpha 3, it requires a mandatory
- Enabled folding of
smart(option(T))
whenT
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 withe.data(custom element).elem-args
- Those were the arguments passed to
.declare
, can be used for reflection purposes
- Those were the arguments passed to
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
- Show: the new recommended approach for show rules, support custom elembic filters (PR GH#27)
-
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 fille.filters.or_(thm.with(kind: "lemma"), thm.with(kind: "theorem"))
will match theorems or lemmase.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 allperson
instances with age greater than 5- Same disclaimer as NOT
- AND, OR, NOT, XOR combiners
-
Within filter (ancestry tracking - PR GH#38)
- Check descendant
e.filters.and_(wock, e.within(blocky.with(fill: red)))
matches all wocks in red-filledblocky
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 ignoreelem
. For example, inblocky(elem(wock))
, if we never wrote a rule withe.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.
- You can even force all elements to do it with
- If you never write a rule with
- Check descendant
-
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,)
.
- Force with
-
Lots of forward- and backward-compatibility fixes (e.g. #37)
-
none
is now valid input forcontent
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)
ore.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).
- If this returns
-
Use
#show: e.leaky.enable()
to quickly enable leaky mode for rules under it without having to add thee.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.
- For example,
-
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 callbackget => ...
- E.g.
let fields = e.stateful.get(wock)
- Stateful mode-only feature, allows you to
-
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
- Set
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:
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!]
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)
.
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...]
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!]
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.
To check for a custom data structure (usually dictionary-based) in a field, consider creating your own custom type.
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 toauto
to mean a smart default. - Use
e.types.array(type)
ande.types.dict(type)
for arrays and dictionaries with only a specific type of value, such ase.types.array(int)
for an array of integers.
To change existing types slightly, check out type wrapping with e.types.wrap
. This can be used to:
-
Add custom folding behavior to your field (override
fold
with a function); -
Add a custom check to your field with e.g.
e.types.wrap(int, check: prev => i => i > 0)
instead of justint
to only accept positive integers- Here
prev
can be ignored since it isnone
(int
has no checks by default), but make sure to invoke it as needed.
- Here
-
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:
doc
(string): Field documentation.internal
(bool): If set totrue
, should be hidden in the documentation.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:
-
List those fields as "synthesized" fields in the
fields
array. To do this, just specifye.field(..., synthesized: true)
.- Such fields cannot be manually specified by users, however they can be matched on by filters.
-
Create a synthesis step for your element with
synthesize: fields => updated fields
. Here, you can, for example, access Typst context, as well as usee.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]
All field options
e.field
has the following arguments:
-
name
(string, positional): the field name, by which it will be accessed in the dictionary returned bye.fields(element instance)
. -
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). -
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 accessinge.data(elem).all-fields
, and can be used to auto-generate documentation, for example. -
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. -
named
(boolean orauto
, 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 isauto
, which istrue
for optional fields andfalse
for required fields (but you can have required named fields and optional positional fields by changing both parameters accordingly). -
default
(any, named, optional): the default value for this field ifrequired
isfalse
. 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 specifynone
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 isint
, thendefault
will automatically be set to0
for such optional fields. If the type doesn't have its own default and the field is optional, you must specify your own default. -
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 throughe.select
. For example, if you have a synthesized field calledlevel
, a user can match onelem.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.
-
folds
(boolean, named, optional): iffalse
, 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. -
internal
(boolean, named, optional): iftrue
, indicates this field should be hidden from documentation. Has no effect onelembic
itself and is only meant to be read by external tools. -
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:
- Read the current text font (with
text.font
), or some other Typst set rule. - Read an elembic element's set rule.
- Read the value of a certain
state
variable. - 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:
-
Use the default context: more efficient, but ignores show/show-set rules not applying to the outer selector, so usually not recommended.
-
Use
e.get
: if you need to read an elembic set rule, you can use a get rule. -
Use explicit
context
: to read other contextual values, you can use a nestedcontext
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
.
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)
.
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:
- 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 throughe.fields
. #show <label-name>: it => ...
will have full field data available. (However, this show rule style is not revokable.)- 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
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*]
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
.
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:
- You intend to use a
@reference
on that element. - 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!]
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.
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!]
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.
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.
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
.
This rule has a potential performance impact if the filter matches too many elements, in the hundreds.
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.
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()
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.
Filtered rules need some extra attention regarding naming:
-
e.named("name", e.filtered(filter, rule))
will assign"name"
toe.filtered
, but not to eachrule
created with this filter.- In this case, revoking
"name"
will stop any newrule
from being applied, but will not revoke an already appliedrule
in this scope.
- In this case, revoking
-
e.filtered(filter, e.named("name", rule))
will assign"name"
to each newrule
created, but not toe.filtered
. This is fairly unusual. -
e.named("name", e.filtered(filter, e.named("name", rule)))
will assign"name"
to bothfiltered
and each new copy ofrule
.- This is usually recommended, as revoking
"name"
will both stoprule
from being applied and revoke an already activerule
.
- This is usually recommended, as revoking
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.
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:
- To match built-in elements and elembic elements simultaneously, e.g. in a show rule or built-in query;
- 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.
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.
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[, ].]
})
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[, ]]
}
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.
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:
e.filters.and_(filter 1, filter 2, ...)
: matches elements which match all filters at once.e.filters.or_(filter 1, filter 2, ...)
: matches elements which match at least one of the given filters.e.filters.xor(filter 1, filter 2)
: matches elements which match exactly one of the two given filters.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)
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.
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.
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 thecontainer
.e.within(parent, depth: 2)
matches only thetheorem
.e.within(parent, max-depth: 1)
matches only thecontainer
.e.within(parent, max-depth: 2)
matches both thecontainer
and thetheorem
.
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, sotheorem
is considered as depth 1 inparent(elem(theorem()))
. - Queries with
e.within(elem)
don't work if no rules were used withe.within(elem)
(and won't match elements underelem
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:
- Fields (positional).
eid: "..."
: the element's eid. Useful if more than one kind of element is being tested by this filter. Usee.eid(elem)
to retrieve theeid
of an element for comparison.ancestry: (...)
: the ancestors of the current element, including(fields: (...), eid: "...")
- Extra named arguments which must be ignored with
..
for forwards-compatibility with future elembic versions.
Don't forget the ..
at the parameter list to ignore any potentially unused parameters (there can be more in future elembic updates).
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:
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 oftypes.union
,"wrapped"
as the output oftypes.wrap
,"collection"
fortypes.array
, and so on);name
: the name of a type;input
: which basic types may be cast to it (e.g.: integer or string). This uses the type ID format for custom types, obtained withtypes.typeid(value)
, of the form(tid: ..., name: ...)
. For native types, the output oftype(value)
is used;output
: which basic types the input may be cast to (e.g.: just string). This uses the same format asinput
;check
: an extra check (functioninput value => true / false
) that tells whether a basic input type may be cast to this type, ornone
for no such check;cast
: an optional function to cast an input value that passed the check to an output value (functioninput value => output value
). Must always succeed, or panic. Ifnone
, 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).error
: an optional function that is called when the check fails (input value => string
), indicating why the check may have failed.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.fold
: an optional function(outer output value, inner output value) => new output value
to indicate how to combine two consecutive values of this type, whereinner
is more prioritized overouter
. For example, for thestroke
type, one would have(5pt, black) => 5pt + black
, but(5pt + black, red) => 5pt + red
sincered
is the inner value in that case. If this isnone
, 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 hasfold
.- Note that
fold
may also beauto
to mean(a, b) => a + b
, for performance reasons.
- Note that
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 asint
, 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.
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:
- Positive integers: You can use
let pos-int = types.wrap(int, check: old-check => value => value > 0)
to only accept positive integers. - 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.
- If you're using this for a single field, considering specifying
- 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).
- Note that we overrode
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
.
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/orfold
, make sure to also overrideoutput: (type 1, type 2, ...)
. Anything that can be returned bycast
orfold
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 outsideoutput
.- You can also use
output: ("any",)
in an extreme case, but this is discouraged.
- You can also use
-
If you override
check
orinput
, make sure to also adjustoutput
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.
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:
e.tid(a) == e.tid(b)
to check if both variables belong to the same custom type.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 usea == b
instead for private types, or for templates.
- This checks only
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).
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
: Functionfields => content
to display the element.fields
: Array with this element's fields. They must be created withe.field(...)
.parse-args
: Optional override for the built-in argument parser (orauto
to keep as is). Must be in the formfunction(args, include-required: bool) => dictionary
, whereinclude-required: true
means required fields are enforced (constructor), whileinclude-required: false
means they are forbidden (set rules).typecheck
: Set tofalse
to disable field typechecking.allow-unknown-fields
: Set totrue
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 functiondisplayed 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 withe.scope(elem)
, e.g.#import e.scope(elem): sub-elem
.count
: Optional functioncounter => (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) withe.counter(it).get()
. Defaults tocounter.step
to step the counter once before each element placed.labelable
: Set this totrue
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 toauto
, 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 namedlabel
.reference
: When notnone
, 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, soprepare
needs no arguments there). Specify either asupplement
andnumbering
for references looking like "Name 2", and/orcustom
to show some fully customized content for the reference instead.outline
: When notnone
, allows creating an outline for the element's appearances with#outline(target: e.selector(elem, outline: true))
. When set toauto
, the entries will display "Name 2" based on reference information. When acaption
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 displaycaption
.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 withsynthesized: true
. This forbids the user from specifying them manually, but allows them to filter based on that field.contextual
: When set totrue
, functionsfields => something
for other options, includingdisplay
, 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 fornative-elem.field
in the context. In practice, this is a bit expensive, and so this option shouldn't be enabled unless you need preciselybibliography.title
, or you really need to get set rule information from other elements within functions such assynthesize
ordisplay
.
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:
-
"element"
: dictionary with an element's relevant parameters and data generated it was declared. This is the data kind returned bye.data(constructor)
. Importantly, it contains information such aseid
for the element's unique ID (combining itsprefix
andname
, 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. -
"custom-type-data"
: conceptually similar to"element"
but for custom types, including their data at declaration time. This is returned bye.data(custom-type-constructor)
. Containstid
for the type's unique ID (combiningprefix
andname
), as well astypeinfo
for the type's typeinfo,fields
with field information andfunc
for the constructor. -
"element-instance"
: returned frome.data(it)
in a show rule on custom elements, or when usinge.data(some-element(...))
. Relevant keys here includeeid
for the element ID (prefix + name),func
for the element's constructor, as well asfields
, 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'sdisplay
function (the element's effective body), but note that thebody
shouldn't be placed directly; you should returnit
from the show rule to preserve the element. You usually will never need to usebody
. 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 infields
. Outside of show rules, however (e.g. on recently-constructed elements), the dictionary will havefields-known: false
indicating that the dictionary offields
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
. -
"type-instance"
: similar to"element-instance"
, exceptfields
are always known and complete since there are no show or set rules for custom types, soe.data(my-type(...))
will always have a complete set of fields, as well astid
for the type's ID andfunc
for its constructor. -
"incomplete-element-instance"
: this is only obtained when trying toe.data(it)
on a show rule on an element's outer selector (obtained frome.selector(elem, outer: true)
ore.data(elem).outer-sel
). The only relevant information it contains is theeid
of the matched element. The rest is all unknown. -
"content"
: returned when some arbitrarycontent
with native elements is received. In this case,eid
will benone
, butfunc
will be set toit.func()
,fields
will be set toit.fields()
andbody
will be set toit
(the given parameter) itself. -
"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 toe.data(arg).outer-sel
. - With
outline: true
, returns a selector that can be used inoutline(target: /* here */)
for outlinable elements. This is equivalent toe.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