Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

mdbook-swirly

An mdBook preprocessor that renders Swirly diagrams to SVG while your book builds.

Write a diagram in a fenced block tagged swirly:

```swirly
@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |     | 'b' |     | 'c' |
to = 5

> s1 |     | 'b' |     | 'c' |     |
```

and the page gets this in its place:

t012345‘a’‘b’‘c’c‘b’‘c’s1

No client-side JavaScript, no image files to keep in step with the prose, and nothing fetched when the page loads. The diagram above is an SVG that was rendered when this page was built — and it follows your theme, so try the paint roller in the toolbar.

What Swirly draws

Swirly has two diagram species:

  • Marble diagrams — the familiar timeline of events flowing through an operator, from the RxJS tradition.
  • Grid diagrams — rows sharing one discrete, labelled transaction axis, mixing event streams with held-value cells. These are the diagrams the Functional Reactive Programming book uses, and the reason this fork exists.

Both are written as plain text you can diff, review and edit in place.

Where to start

This documentation follows Diátaxis, which sorts documentation by what you are trying to do:

TutorialNever used it? Start here and build a book with a diagram in it.
How-to guidesYou know what you want; these are the recipes.
ReferenceEvery syntax form, configuration key and command.
ExplanationWhy it works the way it does.
ExamplesEvery example in the Swirly repository, rendered.

Install

cargo install mdbook-swirly
cd my-book
mdbook-swirly install

The tutorial walks through that from an empty directory.

Your first diagram

This tutorial takes you from an empty directory to a book with a working diagram in it. It should take about ten minutes, and you will end up with something you can keep building on.

You need Rust installed. Everything else, we install as we go.

By the end you will have:

  • a book that renders swirly code blocks to SVG,
  • a stream diagram and a cell diagram you wrote yourself,
  • and diagrams that follow the reader’s theme.

We are going to build things up one piece at a time, and some steps will look like more typing than they need to be. That is on purpose — the point is to see each part before it gets hidden behind a shortcut.

Start with Setting up.

Setting up

We need two programs: mdBook itself, and this preprocessor.

cargo install mdbook
cargo install mdbook-swirly

Now make a book to put diagrams in:

mdbook init frp-notes --title "FRP notes"
cd frp-notes

Answer n when it offers to create a .gitignore, and y to the title it suggests. You should have a directory holding book.toml and a src/ with two markdown files in it.

Check it works before we change anything:

mdbook serve --open

That builds the book and opens it in a browser, and it keeps running — it will rebuild every time you save a file. Leave it going; we will lean on that for the rest of the tutorial.

Wiring in the preprocessor

In a second terminal, from the same directory:

mdbook-swirly install

It prints what it did:

Wrote ./assets/swirly.css
Updated ./book.toml (previous copy at ./book.toml.bak)

Two things happened. book.toml gained the entries that make mdBook run us:

[preprocessor.swirly]
command = "mdbook-swirly"
after = ["links"]

[output.html]
additional-css = ["./assets/swirly.css"]

And a stylesheet appeared in assets/. That stylesheet is not decoration — it is what lets a diagram follow the reader’s theme. We come back to it in Themes.

The after = ["links"] is there so that mdBook expands any {{#include}} before we go looking for diagrams. It matters later, when you want to keep a diagram in its own file.

Checking it took

Open src/chapter_1.md and replace its contents with:

# Chapter 1

```swirly
@ t | 0 | 1 | 2

> s |  |  |
```

Save. The browser should reload and show a line with three dashed dividers crossing it:

t012s

If you see that, everything is connected. If you still see the source text in a grey code box, the preprocessor is not running — check that mdbook-swirly --version works and that book.toml has the [preprocessor.swirly] section.

That diagram is an SVG in the page. View source and you will find it inline: no image file was written, and nothing is fetched when the page loads.

Next: what that diagram actually says.

A stream

The diagram you just made has two lines in it, and each one does a different job. Here it is again with the pieces named:

@ t | 0 | 1 | 2        <- the axis: what the columns are
                          (blank line separates blocks)
> s |  |  |  |          <- a stream row

Every block is separated by a blank line, the way paragraphs are. That is how Swirly knows where one row ends and the next begins.

The axis

@ t | 0 | 1 | 2

@ declares the time axis. The first cell, t, is the label that appears in the gutter on the left. Everything after it is a column, and each column is one transaction — a single instant in which the whole system updates at once.

This is the part that makes a grid diagram different from a marble diagram. Time is not a continuous line each row measures for itself; it is a set of labelled columns that every row shares. Read a column downwards and you see what happened in that instant.

A stream row

> s |  |  |

> declares a stream row, s is its gutter label, and then there is one slot per column. All three slots are empty, so the stream never fires — the line just runs through:

t012s

Count the pipes. One | is one slot, and the axis line above has exactly the same number — three pipes on both. An empty last slot is a bare trailing |, not an extra one. Get the count wrong and the build stops with a message saying so, which you will see for yourself in Diagnose a failing diagram.

Making it fire

Put values in the slots:

> s | 'a' |  | 'b'

t012‘a’‘b’s

The stream fires 'a' in transaction 0, nothing in 1, and 'b' in 2. Notice that the value sits on the line rather than in a bubble on it, and that the line runs through it. An empty slot is not a gap in the line; it just means nothing happened.

Notice too that the quotes are yours. A slot is drawn exactly as you typed it, so 'a' is a string, 42 is a number, and return 'a' is an expression. Try replacing 'a' with something longer and watch the column widen to fit.

Two streams

Add a second row. Remember the blank line between blocks:

@ t | 0 | 1 | 2

> s1 | 'a' |     | 'b'

> s2 |     | 'x' | 'y'

t012‘a’‘b’s1‘x’‘y’s2

Both rows hang off the same axis, so transaction 2 is a single column and you can see at a glance that s1 and s2 both fire there. That alignment is the whole point of the grid.

The extra spaces are for you, not the parser — slots are trimmed, so line them up however reads best in the source.

Next: a cell, which remembers.

A cell

A stream fires and is gone. A cell holds a value — it always has one, and it keeps it until something replaces it. That difference is most of what FRP diagrams are about, and Swirly draws it as a box.

Replace your chapter’s diagram with this:

@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |  | 'b' |  | 'c' |

t012345‘a’‘b’‘c’c

= declares a cell row. The slots work exactly as they did for a stream — one per column — but they mean something different. A value changes what the cell holds, and a blank slot means keep holding what you had.

So the box is divided in three, and the dividers fall at 0, 2 and 4: exactly where you wrote a value. You did not draw those dividers, and you could not have put them in the wrong place. They are derived from the slots.

Notice the box opens before column 0. The cell already held 'a' when the diagram starts, because a cell always has a value — there is no moment where it has none.

Where the value comes from

A cell usually gets its values from a stream. Put both rows in and you can see the relationship:

@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |     | 'b' |     | 'c' |

> s |     | 'b' |     | 'c' |     |

t012345‘a’‘b’‘c’c‘b’‘c’s

Read a column at a time. s fires 'b' in transaction 1, and c changes to 'b' in transaction 2 — one column later. Same for 'c' at 3 and 4.

That one-column lag is not a drawing convention; it is the thing being drawn. A cell updated by a stream takes its new value in the next transaction, so that everything reading the cell during transaction 1 agrees on what it holds. Lining the rows up on a shared axis is what makes that visible.

Stopping early

A cell does not have to run to the end of the diagram. Add a to line directly beneath the row — no blank line, because it belongs to the same block:

= c | 'a' |     | 'b' |     | 'c' |
to = 5

t012345‘a’‘b’‘c’c‘b’‘c’s

The box now closes at transaction 5 and the line carries on to the same arrowhead every other row reaches. from does the same at the other end, for a cell that does not exist until partway through.

What you have

You can now draw the two things FRP is made of and the relationship between them. That is most of the notation. The syntax reference has the rest — annotation rows, references between rows, nested transactions — and it is short.

One thing left: making diagrams follow the reader’s theme.

Themes

Find the paint roller in the toolbar at the top of this page and switch to Coal or Ayu. The diagram below changes with it:

t012345‘a’‘b’‘c’c‘b’‘c’s

Your own book does the same. Switch themes in the browser tab you left running and the diagram you just drew will follow.

This is worth a moment, because it should not be possible. The SVG was rendered once, when the book was built, long before anyone chose a theme. It cannot know what colours the page is using.

How it does it

The trick is to not name any colours. The default theme, adaptive, draws every stroke and every glyph in currentColor — a CSS keyword meaning “whatever the surrounding text colour is” — and paints no background at all. The stylesheet mdbook-swirly install dropped in your assets/ directory is what supplies that colour, and it does so from mdBook’s own variables:

svg.swirly      { color: var(--fg); }
svg.swirly rect { fill: var(--bg); }

Those two variables are how mdBook itself paints every page, so the diagram is now using the same palette as the prose around it. Custom themes work too, as long as they set --fg and --bg.

The second rule is why cell boxes stay opaque: without it the dashed grid would show straight through them.

Picking a different one

If you would rather have a fixed look, set it in book.toml:

[preprocessor.swirly]
theme = "sodium"

sodium is black-on-white line art, matching the printed figures in the Functional Reactive Programming book:

t012345‘a’‘b’‘c’c‘b’‘c’s

It stays black on white whatever the reader picks, which is right for print and wrong for a reader using Coal — try switching now and see. That is the trade: adaptive follows the page, the others are fixed.

You can also override a single diagram without changing the book default, which is what produced the one above:

```swirly theme=sodium
@ t | 0 | 1
```

The full list is in Themes and styles.

Done

You have a book that renders diagrams, you can draw streams and cells, and you know why the colours behave the way they do.

From here:

  • The how-to guides cover adding this to a book you already have, keeping diagrams in separate files, and building in CI.
  • The syntax reference has the forms this tutorial skipped.
  • The examples are every diagram in the Swirly repository, which is the fastest way to see what the notation can do.

How-to guides

Recipes for specific jobs, assuming you already know roughly what you want. If you are new to mdbook-swirly, the tutorial is a better starting point.

Add diagrams to an existing book

Install into the book

From the book’s root — the directory holding book.toml:

mdbook-swirly install

Or name the directory:

mdbook-swirly install path/to/book

This writes assets/swirly.css and adds two sections to book.toml. It is additive and safe to re-run: comments, key order and any existing additional-css entries survive, an existing stylesheet is left alone unless you pass --force, and the previous book.toml is kept as book.toml.bak.

To see the changes without making them:

mdbook-swirly install --print

If your book disables the default preprocessors

A book with

[build]
use-default-preprocessors = false

must list every preprocessor it wants, including links — the one that handles {{#include}}. install adds after = ["links"] to our section, which is a declaration of ordering, not a dependency: if links is not enabled, ours simply runs whenever it likes. But {{#include}} will not be expanded at all, so the technique below will not work.

Including a diagram from a file

Keeping a diagram in its own file means you can render it with the CLI, keep it under test, or share it between books. Point {{#include}} at it from inside a swirly fence:

```swirly
{{#include ../diagrams/hold.txt}}
```

Paths are relative to the markdown file doing the including, and they may reach outside src/.

The ordering is what makes this work. mdBook runs links first, which replaces the {{#include}} with the file’s contents; by the time we look at the chapter, the fence contains a diagram specification. That is exactly what after = ["links"] buys you, and it is why install sets it.

You can show the same file as source and as a picture by including it twice:

```swirly
{{#include ../diagrams/hold.txt}}
```

```text
{{#include ../diagrams/hold.txt}}
```

Every page under Examples is built this way, from files in the Swirly repository.

Migrating from images

If the book currently has diagrams as checked-in PNGs or SVGs, you can replace them one at a time — there is no flag day. A swirly block and an ![](image.png) can sit in the same chapter indefinitely.

What you get for converting: the diagram is diffable, it follows the reader’s theme, and it cannot fall out of step with the prose without the build noticing.

Choose a theme

Set the book’s default

[preprocessor.swirly]
theme = "adaptive"
ThemeLooks likeUse it when
adaptivefollows the pagethe default; the reader can switch mdBook themes
sodiumblack on white line artyou want the printed-book look, fixed
lightSwirly’s light palette, coloured marblesmarble diagrams where colour distinguishes values
darkSwirly’s dark palettea book that is only ever dark

An unknown name stops the build and lists the valid ones, so a typo cannot quietly give you the default.

Override one diagram

Put the option in the fence’s info string:

```swirly theme=sodium
@ t | 0 | 1
```

This wins over the book default for that block only. It is useful when most of a book is adaptive but one figure is meant to match a printed original.

Which to pick

Use adaptive unless you have a reason not to. It is the only one that stays legible when a reader switches to Coal or Ayu, and it is the only one that follows a custom theme.

Its one limitation is that it is monochrome. It renders every stroke and glyph in the page’s text colour, so it cannot distinguish values by colour the way light does with marble diagrams. For grid diagrams that costs you nothing — they are line art either way. For marble diagrams with many distinct values, light may read better, at the price of looking wrong on dark themes.

You can have both: set theme = "adaptive" as the book default and tag the few marble diagrams that need colour with theme=light.

Making adaptive match a custom mdBook theme

adaptive reads two CSS variables, --fg and --bg, which every built-in mdBook theme defines. If you ship a custom theme that defines them too, the diagrams follow it with no further work. If it uses different variable names, override the rules in assets/swirly.css — see Restyle diagrams.

Restyle diagrams

There are two levels to reach for, and they solve different problems.

Level 1: CSS, for anything about colour

assets/swirly.css is yours to edit — install will not overwrite it unless you pass --force. Every diagram carries class="swirly" on its <svg>, so you have a handle on all of them at once.

The stylesheet as installed is only a few rules:

svg.swirly {
  display: block;
  margin: 1.25em auto;
  max-width: 100%;
  height: auto;
  color: var(--fg);
}

svg.swirly rect {
  fill: var(--bg);
}

CSS rules beat the presentation attributes on the elements, so you can override anything the renderer emitted. To tint diagrams away from the body text colour:

svg.swirly { color: var(--links); }

To make them full-bleed rather than centred:

svg.swirly { margin-inline: 0; max-width: none; }

To leave cell boxes transparent, so the transaction grid shows through:

svg.swirly rect { fill: none; }

This level only works with the adaptive theme, which emits currentColor rather than literal colours. The fixed themes bake their palette into the SVG, and while you can still override it with CSS, you are then fighting the theme rather than using it.

Level 2: style keys, for anything about geometry

Sizes, spacings, stroke widths, fonts and column sizing are decided when the SVG is generated, so CSS cannot reach them. Those come from Swirly’s style keys, set in a [styles] block inside the diagram itself:

t012‘a’‘b’‘c’s

That block applies to the diagram it appears in. The full list of keys is in Themes and styles.

Common ones:

KeyDoes
axis_column_sizinguniform (default), content or fixed
axis_column_widthcolumn width when sizing is fixed
axis_column_min_widthfloor for measured columns
grid_row_heightheight of a stream row
grid_cell_heightheight of a cell box
row_label_widthminimum gutter width

Which level

If you are changing a colour, use CSS — it survives a theme switch and applies to the whole book at once. If you are changing a size, use style keys. If you find yourself setting the same style keys on every diagram, that is a sign the book wants its own theme, which is a Swirly-side change rather than one you can make from here.

Diagnose a failing diagram

The build stopped

A diagram that cannot be rendered stops the build by default, naming the chapter and the line the fence starts on:

mdbook-swirly: a swirly diagram failed to render (set on-error = "warn" to continue): appendix/semantics.md:42: Grid row `s1` has 5 slot(s) but the axis has 6 column(s); they must correspond one to one.

This is deliberate. The alternative — dropping the diagram and carrying on — produces a book that builds green with a figure silently missing, which is a worse day than a failed build.

To keep going anyway, leaving the block as source and warning on stderr:

[preprocessor.swirly]
on-error = "warn"

That is useful while converting a pile of diagrams at once, when you want to see all the failures rather than the first.

Common causes

Slot count. The most frequent by far. Every row must have exactly one slot per axis column. Count the pipes — a row carries the same number as the axis above it. With three columns you want > s | a | b | c, and if the last slot is empty, > s | a | b |, which is still three pipes.

from or to naming a column that does not exist. These take a column label, not an index. If your axis is @ t | zero | one, then to = 1 is an error and to = one is what you meant.

A marble row in a grid diagram. A block containing an @ axis puts the whole diagram in grid mode, where --a--b--| has no meaning and is rejected rather than half-parsed.

A theme name typo. Caught before any rendering starts, with the valid names listed.

Rendering one diagram on its own

The fastest way to iterate is to take mdBook out of the loop:

mdbook-swirly render diagram.txt > /tmp/out.svg

It reads stdin if you give it no file, so you can paste a block straight in:

mdbook-swirly render <<'EOF'
@ t | 0 | 1 | 2

> s | 'a' |  | 'b'
EOF

Add --theme to check how it looks under a different palette.

Getting more out of an error

Errors from the renderer carry a JavaScript stack, hidden by default because the bundle is minified and the trace is one uninformative offset. If a message looks like a bug in the renderer rather than a mistake in your diagram, turn it on:

MDBOOK_SWIRLY_DEBUG=1 mdbook build

It works for render too, which is usually the quicker way to look at one diagram.

The diagram did not render, but nothing failed

If a swirly block comes out as a grey code box, the preprocessor never ran. Check, in order:

  1. book.toml has a [preprocessor.swirly] section.
  2. The command in it resolves — try running it by hand.
  3. The command answers the handshake: mdbook-swirly supports html must exit 0. mdBook asks this before every build, and if the answer is anything but a clean exit 0 it skips the preprocessor without saying so. A wrapper script that prints a banner, or a cargo run that emits a warning, will do this to you.

That last one is the failure mode worth remembering, because the symptom is silence.

The diagram rendered but looks wrong on a dark theme

The stylesheet is missing. Check that book.toml has

[output.html]
additional-css = ["./assets/swirly.css"]

and that the file exists. Without it, adaptive diagrams fall back to black ink on a white box. Re-run mdbook-swirly install to restore it.

Build a book in CI

mdbook-swirly is a single self-contained binary with no runtime dependencies, so CI needs the binary and nothing else. No Node, no browser, no network access at build time.

GitHub Actions

name: Book

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install mdBook and the preprocessor
        run: |
          cargo install mdbook --locked
          cargo install mdbook-swirly --locked

      - run: mdbook build

cargo install compiles from source, which is slow on a cold runner. Two ways to avoid that.

Prebuilt binaries. Every release ships binaries for Linux, macOS and Windows:

      - name: Install mdbook-swirly
        run: |
          VERSION=0.1.0
          curl -sSL "https://github.com/RadicalZephyr/mdbook-swirly/releases/download/v${VERSION}/mdbook-swirly-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz" \
            | tar xz --strip-components=1 -C /usr/local/bin --wildcards '*/mdbook-swirly'

Or cache the cargo install:

      - uses: Swatinem/rust-cache@v2
      - uses: taiki-e/install-action@v2
        with:
          tool: mdbook,mdbook-swirly

Failing the build on a broken diagram

This is the default and you should keep it. A broken diagram exits non-zero with the chapter and line in the message, so CI catches a diagram that stopped parsing the same way it catches a broken link.

If you have set on-error = "warn" for local work, make sure CI does not inherit it.

Diagrams in a submodule

If your diagram files live in another repository pulled in as a submodule — which is how the examples in this book work — CI needs to check it out:

      - uses: actions/checkout@v4
        with:
          submodules: true

Without that, {{#include}} finds an empty directory, and mdBook reports a missing file rather than a missing submodule, which is a confusing five minutes.

Publishing to GitHub Pages

The workflow this book is built with is in .github/workflows/pages.yml and is a working example of all of the above: submodule checkout, a cached build of the preprocessor, and deployment to Pages.

Reference

Complete descriptions of what exists. For how to use these, see the how-to guides.

Diagram syntax

A specification is a sequence of blocks separated by blank lines. A line beginning with % is a comment and is discarded before parsing.

The first line of a block decides what it is. Blocks are matched in this order, and the first match wins:

First line matchesBlock is
[styles]diagram styles
[styles.X]message styles
@ followed by space or end of linea time axis
>, = or ., a single-token label, then |a grid row
>an operator
anything elsea marble stream

Lines after the first in a block are configuration, written key = value.

Two modes

A diagram containing a time axis is in grid mode; one without it is in frame mode. The difference is what a horizontal position means: grid mode looks up a discrete labelled column shared by every row, frame mode scales a continuous frame number.

Marble rows are rejected in grid mode rather than half-parsed, so you cannot mix the two by accident.

Time axis

@ <label> | <column> | <column> | ...

t012

The segment before the first | is the gutter label and may be empty. Each segment after it is one column — one transaction.

One | declares one column, and the axis draws one dashed boundary per column with none after the last, so the line above is three pipes, three columns and three dashed lines. There is no special case for a trailing pipe: the count of pipes on a line is the count of columns it declares, and a grid row carries exactly the same number.

A final | with nothing after it therefore declares an unlabelled column, which is how a diagram asks for a closing boundary:

t01

A column label may be prefixed with one > per level of nesting, for diagrams that split a transaction:

@ t | [0] | >[0,0] | >[0,1] | [1]

t[0][0,0][0,1][1]

Nesting lightens the grid line that opens the column.

A diagram may declare at most one axis, and it need not come first.

Configuration: title overrides the gutter label.

Grid rows

All three kinds share a shape: a sigil, a single-token label, then one slot per column.

<sigil> <label> | <slot> | <slot> | ...

Slots are trimmed, so you may pad them to line up with the axis. A slot’s text is drawn verbatim — the quotes in 'a' are yours, not the notation’s.

Every row must have exactly as many slots as the axis has columns, which means exactly as many pipes. A mismatch stops the build.

A row whose last slot is empty ends in a bare | and needs no extra one, so an all-empty three-column row is > s | | |.

Stream rows — >

A line running the width of the axis, ending in an arrowhead. An empty slot means the stream did not fire in that transaction.

> s1 | 'a' |  | 'b'

t012‘a’‘b’s1

Cell rows — =

A box holding a value across an interval. A non-empty slot changes the held value; an empty one keeps it. Dividers are derived from that, not written.

= c | 'a' |  | 'b'

t012‘a’‘b’c

Configuration:

KeyMeaning
fromcolumn label whose opening boundary the box starts at. Default: before column 0
tocolumn label whose opening boundary the box closes at. Default: after the last column

Both take a column label, not an index. The box overhangs the boundary that bounds it rather than sitting flush against it, so a cell reads as holding its value through that transaction.

t01234‘a’‘b’c

Annotation rows — .

A label and values with no line of their own, for commenting on a transaction.

. a1 |  | 'a' |

t012‘a’‘b’c‘a’a1

References

A slot whose text matches another row’s label is a reference to that row rather than a literal — the switch case, where a cell holds another cell or a stream.

t0123‘a’‘b’‘c’‘d’s1‘W’‘X’‘Y’‘Z’s2s1s2c

Resolution happens after the whole diagram is parsed, so the row being named does not have to be declared first. A row naming itself stays a literal.

Marble streams

Frame mode uses RxJS marble-testing syntax- for a frame of time, a letter or digit for a value, | for completion, # for an error, and () to group events into one frame.

--a--b--|

Configuration:

KeyMeaning
titlethe label drawn to the left of the line
ghostscomma-separated value names to draw faded
X := valuedraw marble X with this text instead of X

Leading whitespace offsets the stream in time.

Named streams

A block of the form x = <marbles>, where x is one character, defines a stream instead of drawing one. Using that character in a later marble line nests the stream inside the event, which is how higher-order diagrams are drawn:

x = --a--b--|

-x----|

Operators

A line beginning with > that is not a grid row — that is, one without a pipe right after its first word — is an operator band:

> concatAll

Backticks inside the title embed a marble diagram in it:

> debounce(() => `--|`)

Configuration: X := value, as for marble streams.

Diagram styles

[styles]
axis_column_sizing = fixed
axis_column_width = 160

Applies to the diagram it appears in. See Themes and styles for the keys.

Message styles

[styles.a]
fill_color = red

Applies to one marble value — the single character after the dot — in frame mode.

Configuration

book.toml

[preprocessor.swirly]
command = "mdbook-swirly"
after = ["links"]
theme = "adaptive"
on-error = "fail"

[output.html]
additional-css = ["./assets/swirly.css"]

mdbook-swirly install writes all of this.

Preprocessor keys

KeyValuesDefaultMeaning
themeadaptive, sodium, light, darkadaptivepalette for every diagram in the book
on-errorfail, warnfailwhat to do when a diagram will not render

on_error is accepted as a spelling of on-error.

An unrecognised theme name stops the build and lists the valid ones. An unrecognised value for on-error does the same.

mdBook keys that matter

KeyWhy
commandhow mdBook invokes the preprocessor
after = ["links"]makes {{#include}} expand before we look for diagrams
additional-cssloads the stylesheet the adaptive theme needs

after is an ordering declaration, not a dependency: if the links preprocessor is disabled, ours still runs, but {{#include}} will not have been expanded.

Per-block options

Options go in the fence’s info string, after the word swirly:

```swirly theme=sodium
@ t | 0 | 1
```
OptionValuesMeaning
themea theme nameoverrides the book default for this block

An unrecognised option is an error rather than being ignored, so a typo does not silently do nothing.

The first word of the info string must be exactly swirly. A block tagged swirlyish or rust is left alone.

Per-diagram styles

Geometry is decided when the SVG is generated, so it cannot be changed with CSS. Use a [styles] block inside the diagram:

[styles]
axis_column_sizing = fixed
axis_column_width = 160

See Themes and styles for the full key list.

Precedence

For a given diagram, lowest to highest:

  1. the theme’s defaults,
  2. the book’s theme in book.toml,
  3. the block’s theme= in the info string,
  4. a [styles] block inside the diagram.

CSS applies over all of it for anything colour-related, because a CSS rule beats a presentation attribute on the element.

Command line

mdbook-swirly                      run as a preprocessor (mdBook does this)
mdbook-swirly install [DIR]        add the preprocessor to a book
mdbook-swirly render [FILE]        render one specification to stdout
mdbook-swirly supports <RENDERER>  preprocessor protocol handshake
mdbook-swirly --help
mdbook-swirly --version

No arguments — preprocess

Reads the mdBook preprocessor payload ([context, book] as JSON) on stdin and writes the modified book on stdout. mdBook invokes this; you would not normally run it yourself, and it reports as much if you do.

Exits non-zero if a diagram fails to render, unless on-error = "warn".

supports <renderer>

Exits 0 for every renderer. mdBook asks this before each build.

Answering anything but a clean exit 0 makes mdBook skip the preprocessor without reporting anything, so this arm is deliberately incapable of failing — it returns before reading configuration, starting the engine, or touching stdin.

install [DIR]

Writes assets/swirly.css and adds the [preprocessor.swirly] and additional-css entries to book.toml. DIR defaults to the current directory and must contain a book.toml.

FlagEffect
--forcereplace assets/swirly.css if it already exists
--printwrite the resulting book.toml and stylesheet to stdout, change nothing
--dry-runsame as --print

Additive and idempotent. Comments, key order and existing additional-css entries are preserved; the previous book.toml is kept as book.toml.bak. A command you have already set is left alone, so pointing it at a local build survives a re-run.

render [FILE]

Renders one specification to SVG on stdout. Reads stdin if FILE is omitted.

FlagEffect
--theme <NAME>adaptive (default), sodium, light or dark
mdbook-swirly render diagram.txt > diagram.svg
mdbook-swirly render --theme sodium diagram.txt
echo '@ t | 0 | 1' | mdbook-swirly render

The SVG carries class="swirly", so dropping it into a page that has swirly.css loaded gives the same theme-following behaviour as inside a book. Without that stylesheet an adaptive rendering inherits whatever color is in effect where you put it.

Environment

VariableEffect
MDBOOK_SWIRLY_DEBUGappend the JavaScript stack to errors raised by the renderer

Set to anything other than empty or 0. It is an environment variable rather than a flag because mdBook owns the invocation, so this way one build can be debugged without editing book.toml:

MDBOOK_SWIRLY_DEBUG=1 mdbook build

The stack is hidden by default because the bundled renderer is minified: the trace is a single offset into one very long line, which tells a reader nothing and makes an ordinary message — a slot count, an unknown theme — look like a crash. It is still the only view into the bundle when something there genuinely breaks.

Exit codes

CodeMeaning
0success
1anything else — the message is on stderr, prefixed mdbook-swirly:

Themes and styles

Themes

NameColoursBackgroundColumn sizing
adaptivecurrentColor throughoutnoneuniform
sodiumblack, everything italicwhitecontent
lightSwirly’s light palettewhiteuniform
darkSwirly’s dark paletteblackuniform

adaptive is the default. It is derived from sodium by replacing every colour with currentColor and dropping the background, which is what lets one rendering follow the reader’s mdBook theme — see How adaptive theming works.

The stylesheet

mdbook-swirly install writes assets/swirly.css:

svg.swirly {
  display: block;
  margin: 1.25em auto;
  max-width: 100%;
  height: auto;
  color: var(--fg);
}

svg.swirly rect {
  fill: var(--bg);
}

@media print {
  svg.swirly { color: #000; }
  svg.swirly rect { fill: #fff; }
}

--fg and --bg are mdBook’s own variables, defined by every built-in theme. The rect rule keeps cell boxes opaque so the dashed grid stops at their edges; without it the grid shows through.

The file is yours to edit and is not overwritten unless you pass --force.

Style keys

Set in a [styles] block inside a diagram. Values are numbers, colours or font values, depending on the key.

Naming is regular: a key is <group>_<property>. These are the groups a grid diagram uses.

Axis and columns — axis_

Key
axis_column_sizinguniform (default), content or fixed
axis_column_widththe width used when sizing is fixed
axis_column_min_widthfloor for a measured column
axis_column_paddingadded to measured content to give the column width
axis_header_heightheight of the label row
axis_label_color, axis_label_font_{family,size,style,weight}the column labels

Sizing decides how a column’s width is found. uniform measures every column’s contents and gives them all the widest; content gives each its own, which is what varies column widths within one diagram; fixed ignores the measurements.

Grid lines — grid_line_

grid_line_color, grid_line_stroke_width, grid_line_dash_width, grid_line_bleed, grid_line_depth_stroke_width_step

bleed is how far the dashes extend past the first and last row. depth_stroke_width_step thins the line for each level of column nesting.

Stream rows — grid_row_

grid_row_height, grid_row_lead, grid_row_tail, grid_row_value_color, grid_row_value_font_{family,size,style,weight}

lead and tail are how far the line runs before the first column boundary and past the last.

Cell rows — grid_cell_

grid_cell_height, grid_cell_overhang, grid_cell_fill_color, grid_cell_stroke_color, grid_cell_stroke_width, grid_cell_divider_stroke_width, grid_cell_value_padding, grid_cell_value_color, grid_cell_value_font_{family,size,style,weight}

overhang is how far the box extends past the last column when to is unset. value_padding insets a held value from the edge of its run.

Annotation rows — grid_annotation_

grid_annotation_height, grid_annotation_value_color, grid_annotation_value_font_{family,size,style,weight}

Row labels — row_label_

row_label_width, row_label_gap, row_label_color, row_label_font_{family,size,style,weight}

width is a minimum: the gutter grows to fit the widest label.

Frame-mode groups

Marble diagrams use arrow_, event_, operator_, stream_, range_, barrier_, completion_ and error_, plus frame_width, stacking_height, higher_order_angle and ghost_opacity.

Diagram-wide

background_color, canvas_padding, minimum_width, minimum_height

Setting background_color to an empty string suppresses the background rectangle entirely, which is what adaptive does.

Getting the exhaustive list

There are 103 keys. They are the fields of DiagramStyles in the Swirly sources, which is the only place guaranteed to be current:

git submodule update --init
awk '/^export type DiagramStyles = \{/,/^\}/' \
  vendor/swirly/packages/swirly-types/src/styles.ts \
  | grep -oE '^  [a-z_]+' | tr -d ' ' | sort

The awk range matters: the same file declares a smaller type per style group, and a plain grep over the whole file returns those too.

Explanation

Background and design reasoning. None of this is needed to use the tool; it is here for when you want to know why it behaves as it does, or you are about to change it.

Why render at build time

There are three places a diagram can be turned into pixels, and the choice shapes everything else.

The three options

In the browser. Ship the specification to the reader along with a renderer, and draw the diagram after the page loads. This is how mdbook-mermaid works — and why a book using it loads a couple of megabytes of JavaScript before any diagram appears.

By hand, ahead of time. Run a tool, commit the SVG, reference it with ![](diagram.svg). No runtime cost, but the picture and the prose are now two files that can disagree, and nothing notices when they do.

At build time. Turn the specification into an SVG while the book is being built, and inline it into the page.

What build time buys

The page is finished when it arrives. No JavaScript, no layout shift, no second request. A diagram is as cheap as the paragraph next to it.

The source is the artifact. There is no generated file to regenerate, forget to regenerate, or commit stale. The specification in the markdown is the diagram; they cannot drift because there is only one of them.

A broken diagram breaks the build. This is the one that matters most in practice. If the notation changes, or a diagram is edited into something that no longer parses, you find out from a failed build with a chapter and line number — not from a reader, months later, looking at a blank space. It is the same argument as compiling rather than discovering type errors at runtime, and it is why on-error defaults to fail.

Diagrams are reviewable. A change to a diagram shows up in a diff as the lines that changed, in a notation a reviewer can read. A change to a committed SVG shows up as several thousand unreadable lines.

What it costs

The theme problem. A diagram rendered at build time cannot know what colours the reader will choose, and mdBook lets them choose at runtime. Solving that is the whole of adaptive theming, and it is the single most interesting constraint in this tool.

No interactivity. A build-time SVG cannot animate, respond to hover, or let the reader scrub a timeline. For marble diagrams that is no loss; if you wanted an interactive playground, you would want something else entirely.

A renderer in the build. Something has to run Swirly, which is JavaScript, inside a process that mdBook can invoke. That is the subject of Architecture, and it is the reason this tool is 1.7 MB rather than a shell script.

Why not just commit the SVGs

This is the closest competitor, and it deserves a straight answer: committing SVGs works, and for a handful of diagrams it is completely reasonable.

It stops being reasonable at the point where you have enough diagrams that nobody can remember which ones are current. A book with twenty figures and a notation still under development — which is exactly the situation this was built for — regenerates all of them every time the renderer improves. Doing that by hand is a chore that gets skipped, and a skipped chore is a book with some figures from an older renderer and no way to tell which.

Moving the render into the build makes that failure impossible rather than merely unlikely.

How adaptive theming works

A diagram is rendered once, when the book is built. The reader picks a theme later, in the browser, and can change it at any time. Those two facts look irreconcilable: the renderer cannot know what colours the page will be using.

Here is the diagram from the tutorial. Switch themes with the paint roller and watch it follow:

t012345‘a’‘b’‘c’c‘b’‘c’s

The move

Do not name any colours.

Every stroke and glyph is drawn in currentColor, a CSS keyword meaning “whatever the surrounding text colour happens to be”. No background rectangle is drawn at all. The result is a diagram with no opinion about its own palette — it inherits one from wherever it lands.

The stylesheet then supplies that colour from mdBook’s own variables:

svg.swirly      { color: var(--fg); }
svg.swirly rect { fill: var(--bg); }

--fg and --bg are what mdBook paints every page with, and each built-in theme redefines them. So the diagram is not following our idea of what Coal looks like; it is using the same two values the prose around it is using. A custom theme that defines them works with no further effort.

Why it is a theme, not a feature

adaptive is not special-cased anywhere in the renderer. It is an ordinary Swirly theme whose colour values happen to all be the string currentColor:

const ink = Object.fromEntries(
  Object.keys(base)
    .filter((key) => /_color$/.test(key) && key !== 'background_color')
    .map((key) => [key, 'currentColor'])
)

Twenty-two colour keys, rewritten mechanically. The renderer writes them into the SVG exactly as it would write #333, and the browser resolves them. No renderer change was needed to make any of this work, which is a good sign that currentColor is the right level to be solving the problem at.

The one thing that cannot be currentColor

A cell box has to be opaque, so the dashed transaction grid stops at its edges rather than showing through. Opaque means the page background — which is the one colour that is emphatically not the ink colour.

That is what the second CSS rule is for. The <rect> carries a literal #fff as a presentation attribute, and the stylesheet overrides it with var(--bg). A CSS rule beats a presentation attribute in the cascade, so the rule wins whenever the stylesheet is loaded, and the literal is what you fall back to if it is not.

You might reasonably ask why the attribute is not var(--bg) directly. It could be: var() does resolve inside presentation attributes in current browsers. But that support arrived late and unevenly, whereas var() in an ordinary CSS rule has been universal for years. Putting the variable in the stylesheet and a plain colour in the attribute means the diagram degrades to legible rather than to invisible.

What it costs

adaptive is monochrome. It has exactly one ink colour, because currentColor is exactly one colour.

For grid diagrams that is free — they were line art to begin with. For marble diagrams it is a real loss: Swirly’s light theme gives each value its own marble colour, derived from a hash of the value, and that is genuinely useful when a diagram has a dozen distinct events. adaptive renders those as outlines.

If a particular diagram needs colour, tag it:

```swirly theme=light
--a--b--c--|
```

and accept that it will look like a light-theme diagram sitting in a dark page. The book default and the per-block override exist precisely so you can make that trade one diagram at a time.

Architecture

Swirly is JavaScript. mdBook preprocessors are programs that speak JSON on stdin and stdout. Getting from one to the other is most of what this tool is.

mdBook ──[context, book] as JSON──> mdbook-swirly (Rust)
                                       │
                                       ├── pulldown-cmark: find `swirly` fences
                                       ├── QuickJS: evaluate the Swirly bundle
                                       │     └── swirlyRender(spec, theme) -> SVG
                                       └── splice the SVG back into the markdown
                                       │
       <──────── modified book ────────┘

Rust outside, JavaScript inside

The split is: Rust owns the command line, the preprocessor protocol, the markdown and book.toml; JavaScript owns turning a specification into an SVG.

That boundary is chosen so the JavaScript side is a pure, synchronous string -> string function. It does no I/O, starts no timers, loads no modules and reads no arguments. Everything stateful is on the Rust side.

This matters more than it sounds, because it decides what has to run the JavaScript.

Why QuickJS and not a JavaScript runtime

The obvious move is to embed a runtime — Node, or Deno as a library. Both were measured before this was built:

deno compileRust + QuickJS
Binary81.3 MB1.7 MB
Startup and render88 ms46 ms

A runtime’s value is its event loop, module system, Node compatibility, permissions and networking. A pure function needs none of that. Embedding Deno would have meant shipping eighty megabytes of infrastructure to call one function that does not use any of it — and, because the function is synchronous, would not have been faster either.

QuickJS is about a megabyte of C implementing ES2020, which is precisely the job. The bundle is evaluated once when the preprocessor starts and the engine is kept warm, so a book with twenty diagrams pays for the engine once.

The bundle

src/swirly-bundle.js is Swirly’s parser, renderer and themes compiled by esbuild into one file with no imports and no Node built-ins, embedded into the binary with include_str!. It needs exactly one thing from its host — globalThis.self — which Rust supplies in a one-line shim before evaluating it.

It is a generated file that is committed, which is a trade: users get cargo install with no Node anywhere, at the cost of an artifact that can go stale. Two things guard it. The Swirly submodule in vendor/ pins the exact revision it was built from, and CI rebuilds it there and fails if the result differs byte for byte.

Making that check possible required the bundle to be reproducible, which it initially was not: unminified esbuild output records each module’s path as a comment, so the bytes depended on where the Swirly checkout happened to sit. Minifying removes the paths — and takes the bundle from 286 KB to 111 KB.

Finding the diagrams

Fences are located with pulldown-cmark, the same CommonMark parser mdBook itself uses, rather than a regular expression. Tilde fences, indented fences inside list items, and longer fences containing shorter ones all then behave the way mdBook will treat them, because it is the same code making the decision.

Replacements are spliced back in from the end of the chapter forwards, so each byte range stays valid as earlier ones are rewritten.

Walking the book

The book payload is traversed as generic JSON, looking for objects with a Chapter key, rather than deserialised into typed models. mdBook renamed the root collection from sections to items in 0.5 and has added chapter fields over time; walking structurally means both spellings work and unknown fields are carried through untouched.

The handshake

mdBook runs mdbook-swirly supports <renderer> before each build. If that exits non-zero it skips the preprocessor and says nothing — the book builds successfully with every diagram missing.

Because the cost of getting this wrong is silent and the benefit of nuance is nil, the supports arm is the first thing main matches and it returns before reading configuration, starting the engine or touching stdin. There is nothing in it that can fail.

Examples

Every example in the Swirly repository, rendered by this preprocessor.

These pages are not copies. The Swirly repository is a git submodule of this one, and each example is pulled in with mdBook’s {{#include}} — so what you see below is the current contents of those files, rendered by the version of Swirly this crate embeds. If an example changes upstream, this page changes with it.

The mechanism is worth knowing about in its own right; see Add diagrams to an existing book.

Grid diagrams

The transaction-aligned species: rows sharing one discrete, labelled time axis, mixing event streams with held-value cells. See the syntax reference for the notation.

Each diagram below is rendered from the file named beneath it, pulled straight out of the Swirly submodule.

gridAxis

The apparatus on its own: a labelled transaction axis, a dashed boundary opening each column, and a stream that never fires.

t012s

examples/gridAxis.txt
% A transaction grid. Grid mode replaces the continuous marble timeline with a
% discrete, labelled axis that every row shares: `@` declares the columns, and
% one dashed boundary opens each of them.
%
% `>` declares a stream row. Its slots are empty here, so the line just runs
% through — Sodium streams never complete, so there is no `|` to write.

@ t | 0 | 1 | 2

> s |  |  |

gridStreams

Three streams sharing one axis. Where two of them fire in the same transaction, that is a single column, so you can see it at a glance.

t0123402s1102030s2

examples/gridStreams.txt
% Stream rows carry one slot per transaction, so the source lines up with the
% diagram. A value is typeset on the line rather than inside a marble, and a
% blank slot means the stream did not fire in that transaction.

@ t | 0 | 1 | 2 | 3 | 4

> s1 | 0 |    | 2  |    |

> s2 |   | 10 | 20 | 30 |

gridCell

A cell holds a value until something replaces it. The dividers are derived from the slots, falling wherever a slot carries a value.

t012345‘a’‘b’‘c’c

examples/gridCell.txt
% A cell row holds a value across an interval. `=` declares one, and its box is
% divided wherever the held value changes: a non-empty slot opens a new run and
% a blank one extends the run before it, so the dividers are derived rather
% than written out.

@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |  | 'b' |  | 'c' |

gridHold

hold turns a stream into a cell, one transaction behind, and to closes the box before the axis ends.

t012345‘a’‘b’‘c’c‘b’‘c’s1

examples/gridHold.txt
% `hold` turns a stream into a cell, one transaction behind: s1 fires 'b' in
% transaction 1 and c starts holding it in transaction 2.
%
% `to` closes the box early, at the boundary that opens the column it names.
% The line carries on to the same arrowhead every other row reaches.

@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |  | 'b' |  | 'c' |
to = 5

> s1 |  | 'b' |  | 'c' |  |

gridSwitch

switch: a slot naming another row refers to that row rather than being a literal, so c3 holds first c1 and then c2.

t01234‘a’‘b’‘c’‘d’‘e’c1‘V’‘W’‘X’‘Y’‘Z’c2c1c2c3‘a’‘b’‘X’‘Y’‘Z’c4

examples/gridSwitch.txt
% `switch` — a cell whose value is another cell. A slot naming another row in
% the diagram resolves to a reference to that row rather than to a literal, so
% c3 holds c1 and then c2, and c4 is what the switched cell yields.

@ t | 0 | 1 | 2 | 3 | 4

= c1 | 'a' | 'b' | 'c' | 'd' | 'e'

= c2 | 'V' | 'W' | 'X' | 'Y' | 'Z'

= c3 | c1 |  | c2 |  |

= c4 | 'a' | 'b' | 'X' | 'Y' | 'Z'

gridAnnotations

Annotation rows address the same columns as everything else but draw no line, for commenting on a transaction.

t012345‘a’‘b’c‘a’a1‘b’a2

examples/gridAnnotations.txt
% Annotation rows, declared with `.`, address the same columns as every other
% row but draw no line of their own — they comment on a transaction rather than
% carrying a stream or a cell.

@ t | 0 | 1 | 2 | 3 | 4 | 5

= c | 'a' |  | 'b' |  |  |
to = 3

. a1 |  | 'a' |  |  |  |

. a2 |  |  | 'b' |  |  |

gridNested

Split transactions. A column label can name a nested transaction, and each level of nesting lightens the line that opens it.

t[0][0,0][0,1][1][1,0][‘a’,‘b’][‘c’]s1‘a’‘b’‘c’s2

examples/gridNested.txt
% Split transactions. A column label may name a nested transaction, and each
% leading `>` on it marks one more level of nesting, which lightens the grid
% line that opens that column.

@ t | [0] | >[0,0] | >[0,1] | [1] | >[1,0]

> s1 | ['a','b'] |  |  | ['c'] |

> s2 |  | 'a' | 'b' |  | 'c'

Marble diagrams

The RxJS-style timelines Swirly started as. These use marble-testing syntax with a few additions of Swirly’s own – named streams, operator bands, value substitutions – described in the syntax reference.

They are drawn here with the adaptive theme, so they are monochrome. Swirly’s light theme gives every value its own marble colour, which reads better when a diagram has many distinct events; see Choose a theme.

concatAll

Higher-order: each event on the outer stream is itself a stream, drawn skewed, and concatAll plays them one after another.

fedcbaconcatAllfedcba

examples/concatAll.txt
% An example application of the concatAll operator.
% Showcases styles and higher-order observables.
% Based on RxJS's concatAll diagram:
% https://github.com/ReactiveX/rxjs/blob/fc3d4264395d88887cae1df2de1b931964f3e684/spec/operators/concatAll-spec.ts

[styles]
frame_width = 20
completion_height = 20
higher_order_angle = 30
arrow_fill_color = black

x = ----a------b------|

y = ---c-d---|

z = ---e--f-|

-x---y----z------|

> concatAll

-----a------b---------c-d------e--f-|

debounce

An operator whose title carries a marble diagram of its own, written in backticks.

dcba
debounce(() => 
)
dca

examples/debounce.txt
% An example application of the debounce operator.
% Showcases marble diagrams inside operators.
% Based on RxJS's debounce diagram:
% https://github.com/ReactiveX/rxjs/blob/fc3d4264395d88887cae1df2de1b931964f3e684/spec/operators/debounce-spec.ts

-a--bc--d---|

> debounce(() => `--|`)

---a---c--d-|

exhaustAll

Events dropped while an inner stream is still running are drawn faded, via the ghosts configuration key.

ihgfedcbaexhaustAllihgcba

examples/exhaustAll.txt
% An example application of the exhaustAll operator.
% Showcases ghost notifications.

x = --a---b---c--|

y = ---d--e---f---|

z = ---g--h---i---|

------x-------y------z--|
ghosts = y

> exhaustAll

--------a---b---c-------g--h---i---|

onErrorResumeNext

# marks an error. The title key labels each input line.

sourcebanextdconErrorResumeNextoutputdcba

examples/onErrorResumeNext.txt
% An example application of the onErrorResumeNext operator.
% Showcases titles and error notifications.
% Based on RxJS's onErrorResumeNext diagram:
% https://github.com/ReactiveX/rxjs/blob/86dfe3c78dd37c6828a08b45364f030796879cc0/spec/operators/onErrorResumeNext-spec.ts

--a--b--#
title = source

--c--d--|
title = next

> onErrorResumeNext

--a--b----c--d--|
title = output

pluck

Values given longer names with :=, so a marble can display something wider than one character.

{v:3}{v:2}{v:1}pluck(‘v’)321

examples/pluck.txt
% An example application of the pluck operator.
% Showcases styles and notification values.
% Based on RxJS's pluck diagram:
% https://github.com/ReactiveX/rxjs/blob/fc3d4264395d88887cae1df2de1b931964f3e684/spec/operators/pluck-spec.ts

[styles]
event_radius = 30
operator_height = 60

--a--b--c--|
a := {v:1}
b := {v:2}
c := {v:3}

> pluck('v')

--x--y--z--|
x := 1
y := 2
z := 3

skipUntil

Two inputs and one output, with the second stream deciding when the first starts passing through.

edcbaxskipUntiled

examples/skipUntil.txt
% An example application of the skipUntil operator.
% Showcases notification-level styling.
% Based on RxJS's skipUntil diagram:
% https://github.com/ReactiveX/rxjs/blob/fc3d4264395d88887cae1df2de1b931964f3e684/spec/operators/skipUntil-spec.ts

[styles]
event_value_color = white

[styles.a]
fill_color = #FF0000

[styles.b]
fill_color = green

[styles.c]
fill_color = rgb(0, 0, 255)

[styles.d]
fill_color = yellow
value_color = black

[styles.e]
fill_color = magenta

[styles.x]
fill_color = rgb(63, 63, 63)

--a--b--c--d--e----|

---------x------|

> skipUntil

-----------d--e----|

zipAll

Named streams defined with x = ... and nested into the outer line, then combined pairwise.

21bazipAllb2a1

examples/zipAll.txt
% An example application of the zipAll operator.
% Showcases higher-order observables and notification values.
% Based on RxJS's zipAll diagram:
% https://github.com/ReactiveX/rxjs/blob/fc3d4264395d88887cae1df2de1b931964f3e684/spec/operators/zipAll-spec.ts

x = -a-----b-|

y = --1-2-----

-x----y--------|

> zipAll

-----------------A----B-|
A := a1
B := b2