Authoring Prequeries
Prequery is not just meant for people who want to download images; its real purpose is to make it easy to create any kind of preprocessing for Typst documents, without having to leave the document for configuring that preprocessing.
We call the units that provide information to preprocessors outside the document, and process the information given back to the document, prequeries. The name is a bit overloaded—Prequery is the overall project and a Typst package, prequery
prequery
is a command line tool, and "a prequery" is a kind of Typst function—but it's ultimately simpler than using different names for these related units.
While the package does not actually contain a lot of code, describing how the image()
image()
prequery is implemented might help—especially because it relies on a peculiar behavior regarding file path resolution. Here is the actual code:
This function shadows a built-in one, which is of course not technically necessary. It does require us to call the original function by prefixing the std
std
module, though. The Parameters to the used prequery()
prequery()
function are as follows:
- The first two parameters specify the metadata made available for querying: the value, a dictionary with keys
url
url
andpath
path
, will be put into ametadata
metadata
element with label<web-resource>
<web-resource>
. - The last one is also simple, it just specifies what to display if prequery is in fallback mode: the Unicode character "Frame with Picture" 🖼.
- The third parameter, written as
std.image.with(..args)
std.image.with(..args)
is the most involved and warrants its own section.
If you looked very closely, you may have spotted something strange in one of the previous examples:
The image()
image()
function is part of a package, but the path example.png
example.png
refers to a file in a user's document. Packages can't read users' files, what's happening here?
The answer lies in a peculiarity of Typst's arguments
arguments
type. When you forward an arguments
arguments
value as a whole to another function, it remembers where the contained individual values came from.
Consider these two similar functions:
While they seem to be equivalent (the path
path
parameter of image()
image()
is mandatory anyway), they behave differently:
With my-image
my-image
, passing path
path
to image()
image()
resolves the path relative to the file xy/lib.typ
xy/lib.typ
, resulting in "xy/assets/foo.png"
"xy/assets/foo.png"
. With my-image2
my-image2
on the other hand, the path is relative to where the arguments
arguments
containing it were constructed, and that happens in main.typ
main.typ
, at the call site. The path is thus resolved as "assets/foo.png"
"assets/foo.png"
.
This is of course very useful for prequeries, which are all about specifying the files into which external data should be saved, and then successfully reading from these files! As long as the file name remains in an arguments
arguments
value, it can be passed on and still treated as relative to the caller of the package.
A prequery thus basically consists of the following parts:
- Some metadata, to be read by preprocessors
- Instructions on how to display the preprocessed information, if present
- Some fallback content for when it isn't present
- The logic for deciding when to show the fallback or attempt to display the real content
Your prequery has to provide the first three pieces; the Prequery package ties it together. As an example, let's attempt to write a prequery that executes a Python script and renders its output. Using it should look like this:
The result should be a raw block containing hello world
hello world
, and the fallback also a raw block with ...
...
in it.
Question 1: what will a preprocessor need to produce the hello world
hello world
result in a form our Typst code can access it? Well, the code to run, and the file name to save the output to:
Question 2: given the file path with the output in it, how can it be displayed? The answer is to first read()
read()
the file, then put its content into a raw
raw
element.
And we also know what we want as a fallback:
… and that's it! It may still require some tinkering, but you should be able to use a shell
shell
preprocessor job to execute the code contained in this prequery.
We started with defining that our prequery would be called like this:
If you want to use Prequery to create something like an executable notebook, specifying a file name for each code snippet will be pretty distracting. Better would be something like this:
The bad news is, you have to specify a file name for the read-outside-the-package trick to work; the good news is, doing it once is enough. So, let's store the file name in a state, as arguments
arguments
:
We're also generating a metadata
metadata
element here that the preprocessor can pick up for output.
All prequeries' results need to be written there, so a plain text file is no longer sufficient. We could instead use a JSON file containing an array of output strings.
Let's look at how to display the preprocessor results again: The outputs are in a file. We need to know what the file name is, and which array element is the correct one. Each prequery is preceded by its own metadata, and we have also generated one extra <python>
<python>
metadata at the start, so our overall prequery will look like this:
This lets us call our function as shown above. For even better results, we can use a show rule:
(The first show rule is due to this issue, which affects content generated by applying a show rule to raw blocks. Note that the font itself is also affected, not just the font size, but that doesn't matter in our particular case.)
We now can configure our python()
python()
prequery once, and then write code to be automatically evaluated with minimal overhead.