Papyri Tutorial

Papyri is a programmable markup language. Being a “markup” language means that you can write your content mostly as normal, and you only need to use Papyri’s syntax when you want to “mark up” things that should be presented differently.

Papyri compiles to HTML, and almost all HTML syntax is also valid in Papyri:

You <em>can</em> write Papyri like this, but HTML is a bit clunky.

However, it’s much more convenient when writing Papyri to use functions to mark up your content:

It's better to write Papyri like @emph this, instead.


Here are a few built-in functions:

These functions apply simply to the next thing you write after the function name. If you want to apply a function to more than one word, you can use braces to group multiple words together:

For example, with no braces only @emph one word is emphasised, but you can use
braces to emphasise @emph {multiple words} like this.

The item which the function applies to is called its contents. The contents of a function may be the result of another function:

If you absolutely @underline @b must stress something...

Braces are also needed if a word contains things that aren’t letters in the Latin alphabet, such as apostrophes, hyphens, or accents. You can group anything else, too, including other functions:

@h1 {A @i {top-level heading} with nested markup}


Some functions accept more arguments than just their “contents”. For example, here’s how you can write a link in Papyri:

@href(``) {Click here!}

Arguments to a function are written in parentheses. In this case, the @href function has one argument: the link URL, a string literal written in backticks (the ` symbol).

Some functions have named arguments:

@image(alt={Initech corporate logo}) `resources/logo.png`

For the @image function, both the alt argument and the contents are strings, although the alt argument here is written as text inside braces instead of backticks. Simple string values can also be written without backticks:

@image(alt=Logo) `resources/logo.png`

Here, the string Logo is a single word, so it doesn’t need backticks to count as a string. Still, it’s recommended to use backticks for a string value, as this is more explicit.

Page Template

An HTML page needs some structure around the main content — a doctype, <html>, <head> and <body> tags, and a <title>, at least. Fortunately, you can wrap your page in this boilerplate code using the @page function. This function takes a named title argument, and the “contents” are the page contents:

@page(title={A Very Interesting Page})...

If there was anything here, then this page would be very interesting.

The @page function builds a page using the default Papyri theme, which is the one you see on this page.

This example introduces another piece of syntax: when used for a function’s “contents”, an ellipsis ... means the remainder of the document (or the remainder of the current group or HTML tag).


You can declare variables using the keyword @let. The syntax for @let is like a function: it takes any number of named arguments, which become variables you can refer to as $var_name within the “contents” of the @let expression.

In practice, you usually want to use the ... syntax for the “contents”, so the new variables stay in scope for the remainder of the document (or the remainder of the current group or HTML tag).

@let(position={Director of Redundancies Director}, company={Initech})...

Please submit your TPS reports to the $position at $company.

Variables can appear within text, or can also be used as function arguments, including the “contents” argument:

@let(url=`../index.html`, link_text={Back to the home page})...

@href($url) $link_text

String Templates

Additionally, string variables can be used in string templates, to build a string value out of fixed and variable parts:

@image(alt="$company corporate logo") `resources/logo.png`

String templates can also be used directly in HTML tags:


<div class="panel $panel_kind">
    You may find this information useful.

String templates are delimited either by single-quotes ', or double-quotes ". It’s recommended to only use single-quotes if you want to build a string containing double-quote characters; for a string containing both kinds of quotes, you can use the escape sequences \" or \'.

In case you need to join variables together with literal text that looks like part of a variable name, you must use braces to disambiguate:

<div class="{$panel_kind}_panel"> ... </div>

More Formatting

Besides functions, there are a few other bits of Papyri syntax which can be used to format content in particular ways. The most basic of these is a paragraph break, which is achieved just by writing a blank line:

This content has two paragraphs.

They get split apart because of the blank line in between.

Bulleted lists can be written using square brackets: the list items will normally need to be contained in braces, and they must have commas between them.

Items on the menu include:
    {Spam and eggs},
    {Eggs and spam},

For numbered lists, simply use the built-in @numbered function:

@numbered [
    {Acquire underpants},

Code Formatting

Papyri makes it easy for documents to include properly-formatted source code. The @code and @code_block functions format inline code and code listings respectively; however, you normally don’t need to explicitly call these functions, because when you write a string literal with backticks ` anywhere that text is expected, these functions are automatically called. For example:

This paragraph is text, but `this part` is formatted as inline code. There is
also a block-level code listing below it:

    This part is a code listing, because it has three backticks instead of one.
    The indentation will be stripped automatically, based on how the first
    non-empty line is indented.

For syntax highlighting, put the following line⁠@implicit is like @let, except it also allows the variable to be passed implicitly to the @code and @code_block functions which know to accept it. Implicit parameters are beyond the scope of this tutorial. somewhere near the start of your Papyri source file, before any of the code you want highlighted, with the name of the language the code is written in:


For formatting program input or output (as opposed to source code), use the built-in functions @kbd and @samp respectively. These output <kbd> and <samp> tags instead of <code> tags, and do not use syntax highlighting.

Papyri allows you to use as many backticks as you want to delimit string literals, so if you need to write code which contains backticks `like this`, you can just use sufficiently wide delimiters, `` `like this` ``. (The surrounding whitespace will be trimmed.)

Three or more backticks make a code listing; if you need that many backticks but you don’t want a block-level listing, you can call @code explicitly to make it formatted inline:

Calling `@code` with triple-backticks to format inline code containing
double-backticks: @code ``` `` `like this` `` ```.

For documents with code in multiple languages, you can select the language in a code listing by writing the language name on the first line of the code block, immediately after the opening backticks.⁠This can also be done to get syntax highlighting only in code listings, and not in inline code. If you need inline code to have a different language, use @code with a named language argument, like @code(language=`java`) `;`. This overrides the “implicit” language declaration, if there is one.


Most of the code in this document, like `[2 * x for x in range(5)]`, is going
to be written in Python, but the following listing shows how that might be
written in Java:

    List<Integer> numbers = new ArrayList<>();
    for(int x = 0; x < 5; x++) {
        numbers.add(2 * x);


Papyri has two kinds of comments:

Comments can be used to make remarks about the document, or to temporarily suppress parts of it from the output. Comments don’t work inside string literals, but they do work inside string templates.

Comments are never preserved in the compiled HTML, even if they are HTML-style multiline comments.

Line comments (those beginning with #) also suppress whitespace at the start of the next line. This can be useful when you want content written across multiple lines to be compiled without a space being inserted in between: for example, when you write a footnote⁠Yes, these aren’t really “footnotes”, because they appear in the middle of the document instead of in a footer. But this is the web, not a printed document, so we can get away with that by hiding the “footnote” until the user clicks to see it. using the built-in @footnote function, it makes sense to put it on a separate line, and write # at the end of the previous line to prevent a space being inserted before the footnote marker:

This sentence is true.#
    @footnote {Actually, it's more complicated than that.}

Special Characters

Nice-looking formatting often requires symbols that you can’t easily type on a keyboard, such as dashes and fancy quote marks. When writing regular text in Papyri, you can get some of the most useful ones by character substitutions:

Some characters like $, @ and braces have special meanings in Papyri, but sometimes you may want to write them literally as text. You can do this by escaping them with backslashes: \$ and \@ give the raw characters, and \\ gives a raw backslash if you want that. The same works for preventing text substitutions: for example, \~ gives a literal tilde.

Other characters can be written with hexadecimal Unicode escape sequences or HTML entities:

Escape sequences and entities don’t work inside string literals (with backticks), but they do work in string templates.

Declaring Functions

So far we’ve seen that Papyri is a markup language, but here’s where we start to see that it’s a programmable markup language. You can declare your own functions, to mark up your documents in customisable ways:

@fn panel($_panel_kind: str) $v -> {
    <div class="panel $_panel_kind">$v</div>

The @fn keyword is used to declare a function. Its parameters are declared inside the parentheses, except for the “contents” parameter which is declared afterwards. In this example, there is one parameter named $_panel_kind which must be of type str (i.e. a string), plus the “contents” parameter, which is named $v. There is no type annotation for $v, which is equivalent to a type annotation $v: any (i.e. any type is allowed). The function body then appears after the arrow, ->.

Now that the function is declared, it can be called like any other:

@panel(`info`) {
    You may find this information useful.

In this example, $_panel_kind is a positional parameter because its name begins with an underscore. If we wrote it as $panel_kind then it would be a named parameter, so we would call the function like this:

@panel(panel_kind=`info`) { ... }

It’s recommended to use named parameters unless it will be obvious from context what the argument is for.


In the previous section we mentioned two types: str and any. Papyri has the following primitive types:

Additionally, there are three kinds of compound type:

Here T can be any type, primitive or otherwise; so for example, str list means a list of strings, int dict means a dictionary of integers, and any list? means an optional list which can contain values of any type (and possibly of different types to each other).

Type annotations for parameters are used to check the arguments and coerce them to the appropriate types, where possible. For example, int and bool values can be coerced to str, and values of any type can be coerced to html, or to block (by wrapping inline content in a <p> tag if necessary).

To explicitly convert from one type to another, you can use the @block::from, @inline::from and @str::from functions, which are built-in. For example, @str::from 123 will make the string `123`, and @block::from {Foo bar} will make the HTML tag <p>Foo bar</p>. These functions only perform the same coercions as are done for any other arguments, so e.g. @str::from <p>Foo bar</p> will not work.

Optional Parameters and Default Values

Function parameters can have default values:

@fn greet($greeting: inline = {Hello}) $name: inline -> {
    $greeting, $name!

This way we can call the function with or without the greeting argument:

Note that greeting must be passed as a named argument, since its name doesn’t begin with an underscore. Note also that when calling the function with no argument (other than the “contents”), you don’t need to write it like @greet() Alice with parentheses; that is allowed, but discouraged.

A parameter with a default value is optional; a parameter can also be made optional by declaring it with a question mark ? — note that this goes before the type annotation, if there is one:

@fn say_goodbye($extra?: inline) $v -> {
    Goodbye, $name! $extra

A parameter made optional this way has a default value of ‘.’ (the null/empty value), and its type is implicitly an optional type (i.e. str? instead of str). One way to use parameters like this is as optional tag attributes: the syntax ?= in an HTML tag means the attribute will only be present if the value is not null/empty.

@fn filler_text($css_class?: str) $v: inline -> {
    <span class?=$css_class>
        Lorem ipsum, dolor sit amet.

A function’s “contents” parameter cannot have a default value and cannot be optional. However, you can use an optional type like html? to allow the function to be called with either some contents like @my_function {Contents...}, or no contents like @my_function..


Parameters and arguments can be spread. A single asterisk * spreads positional arguments, and a double asterisk ** spreads named arguments. These both work when declaring a function and also when calling it:

Spread parameters can also have type declarations; for example, the following function accepts any number of positional arguments, but they must all be integers.

@fn my_function(*$_args: int list) $v -> { ... }

Note that a positional spread parameter must still have a name beginning with an underscore, because it’s positional.

Pattern Matching

Except for function calls, Papyri has only one control flow structure, called @match. It can be used to create functions which do different things depending on their arguments, and it can also be used to extract parts out strings, lists or HTML tags. For example, we can use @match to pluralise a word depending on a number:

@let(thing={cow}, amount=2)...

@match $amount {
    0 -> {No {$thing}s.},
    1 -> {One $thing.},
    _ -> {$amount {$thing}s.},

The match rules must be written inside braces, with commas between them; each rule is a pattern, an arrow ->, and a replacement. The first matching pattern applies; you must ensure that some pattern always does apply (the compiler reports a warning if $amount doesn’t match any of them). In this example, the patterns 0 and 1 are integer literals, and the pattern _ is a “wildcard” which always matches.

Other literal values can be used as patterns: integers, strings with backticks, True and False, and ‘.’ which matches the the null/empty value. We can also match based on a value’s type:

@match $v {
    _: none -> {Nothing.},
    _: bool -> {A Boolean.},
    _: int -> {An integer.},
    _: string -> {A string.},
    _: html -> {Some HTML content.},

Things get more interesting when we start matching on parts of a value. For example, here’s a recursive function which reverses a list:

@fn reverse_list $a: any list -> {
    @match $a {
        [] -> [],
        [$head, *$tail] -> [*@reverse_list($tail), $head],

The pattern [] matches an empty list, for the base case of the recursion. The other pattern matches a list with at least one item; the first item will be bound to the variable $head, and the spread pattern *$tail binds the remaining items to the variable $tail, as another list.

We can also match on parts of strings, using template patterns:

@match `Hello, world!` {
    "Hello, $thing!" -> $thing,
    _ -> @raise `No greeting found`,

In case the pattern does not match, we can use the built-in @raise function to report an error message.

Patterns can be combined in nearly arbitrary ways: the example below matches a list with two items, but only if the first one is an integer and the second one is a string with a colon in the middle:

@match $v {
    [$x: int, "$a:$b"] -> {
        Found an integer $x and two string parts "$a" and "$b".

HTML Tag Patterns

Because values in Papyri can also represent HTML content, we can write patterns which match HTML tags, and extract parts of them. Here’s an example:

@let(tag=<span id=`some_span`> ... </span>)...

@match $tag {
    <span id=$id> _ </span> -> {The id is "$id".},
    <span> _ </span> -> {No id found.},
    _ -> @raise `Doesn't seem to be a <span> tag.`,

The first pattern here matches a <span> tag with an id attribute, and any contents. The second pattern matches a <span> tag with no attributes, and any contents. If we wanted these patterns to also match tags with other attributes, we could use a spread pattern:

@match $tag {
    <span id=$id **_> _ </span> -> {The id is "$id".},
    <span **_> _ </span> -> {No id found.},
    _ -> @raise `Not a <span> tag.`,

Here, the attribute spread pattern is **_ because we don’t care what other attributes the tag has, so we just ignore them with the wildcard pattern _.

We can get more general than this: it’s possible to match any kind of tag by matching the tag name as a wildcard:

@match $tag {
    <_ **_> _ </> -> {Some HTML tag, don't know what.},
    _ -> @raise `Not an HTML tag.`,

This pattern matches a tag of any name with any attributes, and any contents, i.e. it always matches any HTML tag. Note that the closing tag must be written as </>, with the tag name omitted, since we don’t know what it is!⁠In this context this syntax is necessary, but the same syntax is also allowed as a shorthand for a closing tag in other contexts. For example, <blockquote> ... </> is valid in Papyri. (It would not be valid in pure HTML.)

If we want to make use of the tag name, attributes and/or contents, we can go further and bind them to variables. This allows us to transform tags in arbitrary ways:

@match $tag {
    <$tag_name **$attrs> $contents </> -> { ... },
    _ -> @raise `Not an HTML tag.`,

Using the variables captured by the match pattern, we can construct an altered version of the tag. For example:

In practice, this capability is rarely needed. For many purposes, you can use built-in functions instead:

Imports and Exports

If you are writing a set of documents, there may be some functions or other declarations which you want to reuse across multiple documents. Or, if a single document is quite long you might want to split it across several source files while still compiling it to a single HTML file.

To export from the current file, use the @export keyword:

To import another Papyri source file:


That’s all for now. There are a few other things, but they aren’t that important.

Papyri is still under development, so this tutorial will get updated as new features are added to the language. In the meantime, if you want to see more examples of how to write documents in Papyri, the Papyri source for this page is a good place to look next.