Files
rivet-typst/manual.typ
T
dromell ab3ccfccea feat: add description-width config option
Descriptions and values are drawn as a single line that never wraps.
Because a diagram is scaled to fit the width it is rendered into, the
longest label is what decides that scale: one long description makes the
whole diagram -- bit cells, indices and every other label with it --
shrink until that line fits. A register whose fields carry a sentence
each is then unreadable at any page width, and since each diagram is
measured on its own, a document full of them renders every one at a
different size.

Setting description-width wraps labels at that width instead, so the
length of a description no longer decides how large the diagram is
drawn. Values wrap at the same right edge as the description they belong
to, and the option composes with left-labels, force-descs-on-side and
ltr-bits.

Wrapping means a label is no longer one line tall, so labels can no
longer be spaced by a fixed line height. Each is now offset by the
height it actually occupies, measured before it is placed, which is why
render gained a context block.

The option defaults to none, which keeps the single-line behaviour on
the original code paths. With it unset every diagram in the gallery
renders byte-identical to before.
2026-08-18 10:30:17 +02:00

445 lines
11 KiB
Typst

#import "@preview/tidy:0.4.2"
#import "@preview/codly:1.3.0": codly-init, codly
#import "@preview/codly-languages:0.1.8": codly-languages
#import "@preview/showybox:2.0.4": showybox
#import "src/lib.typ"
#import "src/schema.typ"
#import "docs/examples.typ"
#show: codly-init
#codly(languages: codly-languages)
#set heading(numbering: (..num) => if num.pos().len() < 4 {
numbering("1.1", ..num)
})
#set page(numbering: "1/1", header: align(right)[rivet #sym.dash.em v#lib.version])
#let doc-ref(target, full: false, var: false) = {
let (module, func) = target.split(".")
let label-name = module + "-" + func
let display-name = func
if full {
display-name = target
}
if not var {
label-name += "()"
display-name += "()"
}
link(label(label-name), raw(display-name))
}
#let note(it) = showybox(
title: "Note",
title-style: (
color: white,
weight: "bold"
),
frame: (
title-color: blue.lighten(30%),
border-color: blue.darken(40%)
),
it
)
#show link: set text(blue)
#let sch = schema.load(```yaml
structures:
main:
bits: 5
ranges:
4:
name: R
description: Register
3:
name: I
description: Instruction
2:
name: V
description: Visualizer
1:
name: E
description: Explainer
0:
name: T
description: Tool
```)
#align(center, schema.render(sch, width: 50%, config: lib.config.config(left-labels: true)))
#v(1fr)
#box(
width: 100%,
stroke: black,
inset: 1em,
outline(indent: auto, depth: 3)
)
#pagebreak(weak: true)
= Introduction
This package provides a way to make beautiful register diagrams using the CeTZ package. It can be used to document Assembly instructions or binary registers
This is a port of the #link("https://git.kb28.ch/HEL/rivet")[homonymous Python script] for Typst.
= Usage
#let import-stmt = "#import \"@preview/rivet:" + str(lib.version) + "\""
Simply import the `schema` module and call `schema.load` to parse a schema description. Then use `schema.render` to render it, et voilà !
#raw(block: true, lang: "typ", ```typ
$import: schema
#let doc = schema.load(yaml("path/to/schema.yaml"))
#schema.render(doc)
```.text.replace("$import", import-stmt))
Please read the #link(<loading>)[Loading] chapter for more detailed explanations on how to load schema descriptions.
= Format
This section describes the structure of a schema definition. The examples given use the JSON syntax. For examples in different formats, see #link("https://git.kb28.ch/HEL/rivet-typst/src/branch/main/gallery/test.yaml")[test.yaml], #link("https://git.kb28.ch/HEL/rivet-typst/src/branch/main/gallery/test.json")[test.json] and #link("https://git.kb28.ch/HEL/rivet-typst/src/branch/main/gallery/test.xml")[test.xml]. You can also directly define a schema using Typst dictionaries and arrays.
Since the XML format is quite different from the other, you might find it helpful to look at the examples in the #link("https://git.kb28.ch/HEL/rivet-typst/src/branch/main/gallery/")[Gitea repo] to get familiar with it.
== Main layout
A schema contains a dictionary of structures. There must be at least one defined structure named "main".
It can also optionally contain a "colors" dictionary. More details about this in #link(<format-colors>)[Colors]
```json
{
"structures": {
"main": {
...
},
"struct1": {
...
},
"struct2": {
...
},
...
}
}
```
#pagebreak(weak: true)
== Structure <format-structure>
A structure has a given number of bits and one or multiple ranges. Each range of bits can have a name, a description and / or values with special meaning (see #link(<format-range>)[Range]). A range's structure can also depend on another range's value (see #link(<format-dependencies>)[Dependencies]).
The range name (or key) defines the left- and rightmost bits (e.g. `7-4` goes from bit 7 down to bit 4). The order in which you write the range is not important, meaning `7-4` is equivalent to `4-7`. Bits are displayed in big-endian, i.e. the leftmost bit has the highest value, except if you enable the `ltr-bits` #doc-ref("config.config") option.
```json
"main": {
"bits": 8,
"ranges": {
"7-4": {
...
},
"3-2": {
...
},
"1": {
...
},
"0": {
...
}
}
}
```
=== Start <format-start>
By default, structures start at bit 0, but you may want to number bits from 1, or another arbitrary value. To do this, you can set the `start` property of a structure to the desired start value. For example,
```json
"main": {
"bits": 8,
"start": 4,
"ranges": {
"11-7": { ... },
"6-4": { ... }
}
}
```
#let start-schema = (structures: (main: (bits: 8, start: 4, ranges: ("11-7": (name: ""), "6-4": (name: "")))))
would render as
#align(
center,
schema.render(
schema.load(start-schema),
width: 50%
)
)
== Range <format-range>
A range represents a group of consecutive bits. It can have a name (displayed in the bit cells), a description (displayed under the structure) and / or values.
For values depending on other ranges, see #link(<format-dependencies>)[Dependencies].
#note[
In YAML, make sure to wrap values in quotes because some values can be interpreted as octal notation (e.g. 010 #sym.arrow.r 8)
]
```json
"3-2": {
"name": "op",
"description": "Logical operation",
"values": {
"00": "AND",
"01": "OR",
"10": "XOR",
"11": "NAND"
}
}
```
=== Wrapping descriptions <format-description-width>
Descriptions and values are drawn on a single line. A diagram is scaled to fit
the width it is given, so a long description makes the whole diagram, bit cells
and all, shrink until it fits:
#let long-schema = (structures: (main: (bits: 16, ranges: (
"15-8": (name: "op", description: "Operation to perform on the two operands"),
"7-0": (
name: "flags",
description: "Flags applied to the operation, see the table below for the meaning of each bit",
values: ("00000001": "carry in, added to the result before the flags are evaluated")
)
))))
#let long-sch = schema.load(long-schema)
#align(center, schema.render(long-sch, width: 75%))
Setting the #doc-ref("config.config") option `description-width` wraps them at
that width instead, leaving the diagram at its natural size. The width is given
in the same units as `bit-width`, and values wrap at the same right edge as the
description they belong to. Each label is offset by the height it actually
occupies, so wrapping onto several lines does not make labels overlap.
```typ
schema.render(sch, config: config.config(description-width: 300))
```
#align(center, schema.render(long-sch, width: 75%, config: lib.config.config(description-width: 300)))
#pagebreak(weak: true)
== Dependencies <format-dependencies>
The structure of one range may depend on the value of another. To represent this situation, first indicate on the child range the range on which it depends.
Then, in its values, indicate which structure to use. A description can also be added (displayed below the horizontal dependency arrow)
```json
"7-4": {
...
"depends-on": "0",
"values": {
"0": {
"description": "immediate value",
"structure": "immediateValue"
},
"1": {
"description": "value in register",
"structure": "registerValue"
}
}
}
```
Finally, add the sub-structures to the structure dictionary:
```json
{
"structures": {
"main": {
...
},
"immediateValue": {
"bits": 4,
...
},
"registerValue": {
"bits": 4,
...
},
...
}
}
```
#pagebreak(weak: true)
== Colors <format-colors>
You may want to highlight some ranges to make your diagram more readable. For this, you can use colors. Colors may be defined in a separate dictionary, at the same level as the "structures" dictionary:
```json
{
"structures": {
...
},
"colors": {
...
}
}
```
It can contain color definitions for any number of ranges. For each range, you may then define a dictionary mapping bit ranges to a particular color:
```json
"colors": {
"main": {
"31-28": "#ABCDEF",
"27-20": "12,34,56"
},
"registerValue": {
"19-10": [12, 34, 56]
}
}
```
Valid color formats are:
- hex string starting with `#`, e.g. `"#23fa78"`
- array of three integers (only JSON, YAML and Typst), e.g. `[35, 250, 120]`
- string of three comma-separated integers (useful for XML), e.g. `"35,250,120"`
- a Typst color (only Typst), e.g. `colors.green` or `rgb(35, 250, 120)`
#note[
The XML format implements colors a bit differently. Instead of having a "colors" dictionary, color definitions are directly put on the same level as structure definitions. For this, you can use a `color` node with the attributes "structure", "color", "start" and "end", like so:
```xml
<schema>
<structure id="main" bits="8">
...
</structure>
...
<color structure="main" color="#FF0000" start="4" end="7" />
<color structure="main" color="255,0,0" start="0" end="3" />
</schema>
```
]
#pagebreak(weak: true)
= Loading <loading>
Due to current limitations of the Typst compiler, the package can only access its own files, unless directly included in your project. For this reason, rivet cannot load a schema from a path, and you will need to read the files yourself to pass their contents to the package.
Here are a number of ways you can load your schemas:
== JSON Format
````typ
// From file (ONLY IF PACKAGE INSTALLED IN PROJECT)
#let s = schema.load("schema.json")
// From file
#let s = schema.load(json("schema.json"))
// Raw block
#let s = schema.load(```json
{
"structures": {
"main": {
...
}
}
}
```)
````
== YAML Format
````typ
// From file (ONLY IF PACKAGE INSTALLED IN PROJECT)
#let s = schema.load("schema.yaml")
// From file
#let s = schema.load(yaml("schema.yaml"))
// Raw block
#let s = schema.load(```yaml
structures:
main:
...
```)
````
== Typst Format
```typ
#let s = schema.load((
structures: (
main: (
...
)
)
))
```
#pagebreak(weak: true)
== XML Format
````typ
// From file (ONLY IF PACKAGE INSTALLED IN PROJECT)
#let x = schema.xml-loader.load("schema.xml")
#let s = schema.load(x)
// From file
#let x = schema.xml-loader.parse(xml("schema.xml").first())
#let s = schema.load(x)
// Raw block
#let s = schema.load(```xml
<schema>
<structure id="main" bits="32">
...
</structure>
</schema>
```)
````
#pagebreak(weak: true)
= Config presets
Aside from the default config, some example presets are also provided:
- #doc-ref("config.config", full: true): the default theme, black on white
#examples.config-config
- #doc-ref("config.dark", full: true): a dark theme, with white text and lines on a black background
#examples.config-dark
- #doc-ref("config.blueprint", full: true): a blueprint theme, with white text and lines on a blue background
#examples.config-blueprint
#pagebreak(weak: true)
= Reference
#let doc-config = tidy.parse-module(
read("docs/config.typ"),
name: "config",
old-syntax: true,
scope: (
doc-ref: doc-ref
)
)
#tidy.show-module(doc-config, sort-functions: false)
#pagebreak()
#let doc-schema = tidy.parse-module(
read("docs/schema.typ"),
name: "schema",
old-syntax: true,
scope: (
schema: schema,
doc-ref: doc-ref
)
)
#tidy.show-module(doc-schema, sort-functions: false)