19 changed files with 48 additions and 205 deletions
-9
View File
@@ -1,14 +1,5 @@
# Changelog
## [Unreleased]
- Added `description-width` config option, wrapping descriptions and values instead of drawing them on a single line
## [v0.3.1] - 2026-06-14
- Support for non-consecutive ranges ([#13](https://git.kb28.ch/HEL/rivet-typst/pulls/13))
- Prevent drawing separators on starting lines of fields ([#17](https://git.kb28.ch/HEL/rivet-typst/pulls/17))
- Fix end line on non-consecutive ranges ending at the highest bit not being draw ([#18](https://git.kb28.ch/HEL/rivet-typst/pulls/18))
- Add documentation for `start` property of structures ([61f13df815f](https://git.kb28.ch/HEL/rivet-typst/commit/6f13df815f99fd6be8b6b3a02b4d53bcc8fe0aff))
## [v0.3.0] - 2025-05-03
- updated CeTZ to 0.3.4
- updated to Typst 0.13.1
+4 -4
View File
@@ -9,7 +9,7 @@ It is based on the [homonymous Python script](https://git.kb28.ch/HEL/rivet/)
<tr>
<td>
<a href="./gallery/example1.typ">
<img src="./gallery/example1.png" width="1000px" alt="A black on white diagram showing the bit structure of a machine instruction, detailing operands, flags and selectors.">
<img src="./gallery/example1.png" width="1000px">
</a>
</td>
</tr>
@@ -19,7 +19,7 @@ It is based on the [homonymous Python script](https://git.kb28.ch/HEL/rivet/)
<tr>
<td>
<a href="./gallery/example2.typ">
<img src="./gallery/example2.png" width="1000px" alt="A white on blue diagram showing the bit structure of RISC-V memory instructions, detailing operands, flags and selectors.">
<img src="./gallery/example2.png" width="1000px">
</a>
</td>
</tr>
@@ -35,7 +35,7 @@ For more information, see the [manual](manual.pdf)
To use this package, simply import `schema` from [rivet](https://typst.app/universe/package/rivet) and call `schema.load` to parse a schema description. Then use `schema.render` to render it, et voilà !
```typ
#import "@preview/rivet:0.3.1": schema
#let doc = schema.load(yaml("path/to/schema.yaml"))
#import "@preview/rivet:0.3.0": schema
#let doc = schema.load("path/to/schema.yaml")
#schema.render(doc)
```
+1 -3
View File
@@ -26,7 +26,6 @@
/// - full-page (bool): If true, the page will be resized to fit the diagram and take the background color
/// - all-bit-i (bool): If true, all bit indices will be rendered, otherwise, only the ends of each range will be displayed
/// - ltr-bits (bool): If true, bits are placed with the LSB on the left instead of the right
/// - description-width (float | none): If set, descriptions and values wrap at this width instead of being drawn on a single line
/// -> dictionary
#let config(
default-font-family: "Ubuntu Mono",
@@ -54,8 +53,7 @@
height: 800,
full-page: false,
all-bit-i: true,
ltr-bits: false,
description-width: none
ltr-bits: false
) = {}
/// Dark theme config
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
#import "@preview/rivet:0.3.1": schema, config
#import "../src/lib.typ": schema, config
#let example = schema.load(yaml("./example1.yaml"))
#let example = schema.load("/gallery/example1.yaml")
#schema.render(example, config: config.config(
full-page: true
))
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
#import "@preview/rivet:0.3.1": schema, config
#import "../src/lib.typ": schema, config
#let example = schema.load(yaml("./example2.yaml"))
#let example = schema.load("/gallery/example2.yaml")
#schema.render(example, config: config.blueprint(
full-page: true,
left-labels: true
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
#import "@preview/rivet:0.3.1": schema, config
#import "../src/lib.typ": schema, config
#let example = schema.load(yaml("/gallery/example1.yaml"))
#let example = schema.load("/gallery/example1.yaml")
//#schema.render(example)
= Chapter 1
+4 -4
View File
@@ -1,14 +1,14 @@
#import "@preview/rivet:0.3.1": *
#import "../../src/lib.typ": *
#let conf = config.config(
full-page: true,
left-labels: true
)
#let alu = schema.load(yaml("./alu_instr.yaml"))
#let alu = schema.load("/gallery/riscv/alu_instr.yaml")
#schema.render(alu, config: conf)
#let branch = schema.load(yaml("./branch_instr.yaml"))
#let branch = schema.load("/gallery/riscv/branch_instr.yaml")
#schema.render(branch, config: conf)
#let mem = schema.load(yaml("./mem_instr.yaml"))
#let mem = schema.load("/gallery/riscv/mem_instr.yaml")
#schema.render(mem, config: conf)
BIN
View File
Binary file not shown.
+4 -5
View File
@@ -1,17 +1,16 @@
#import "@preview/rivet:0.3.1": *
#import "../src/lib.typ": *
#let test-yaml = schema.load(yaml("./test.yaml"))
#let test-yaml = schema.load("/gallery/test.yaml")
#schema.render(test-yaml, config: config.config(
full-page: true
))
#let test-json = schema.load(json("./test.json"))
#let test-json = schema.load("/gallery/test.json")
#schema.render(test-json, config: config.blueprint(
full-page: true
))
#let test-xml-raw = schema.xml-loader.parse(xml("./test.xml").first())
#let test-xml = schema.load(test-xml-raw)
#let test-xml = schema.load("/gallery/test.xml")
#schema.render(test-xml, config: config.dark(
full-page: true
))
BIN
View File
Binary file not shown.
+2 -60
View File
@@ -105,7 +105,7 @@ Since the XML format is quite different from the other, you might find it helpfu
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]
It can also optionnaly contain a "colors" dictionary. More details about this in #link(<format-colors>)[Colors]
```json
{
@@ -152,34 +152,6 @@ The range name (or key) defines the left- and rightmost bits (e.g. `7-4` goes fr
}
```
=== 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.
@@ -203,36 +175,6 @@ For values depending on other ranges, see #link(<format-dependencies>)[Dependenc
}
```
=== 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>
@@ -393,7 +335,7 @@ structures:
#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 x = schema.xml-loader.parse(yaml("schema.yaml").first())
#let s = schema.load(x)
// Raw block
#let s = schema.load(```xml
-2
View File
@@ -25,7 +25,6 @@
full-page: false,
all-bit-i: true,
ltr-bits: false,
description-width: none,
) = {
return (
default-font-family: default-font-family,
@@ -54,7 +53,6 @@
full-page: full-page,
all-bit-i: all-bit-i,
ltr-bits: ltr-bits,
description-width: description-width,
)
}
+1 -1
View File
@@ -1,4 +1,4 @@
#let version = version(0,3,1)
#let version = version(0,3,0)
#import "config.typ"
#import "schema.typ"
+22 -106
View File
@@ -41,24 +41,6 @@
)
}
// A wrapped label is built as plain content so that its height can be measured
// before it is placed, and the label after it offset by what it really takes.
#let text-block(txt, color, width, font: none, italic: false, size: 1em) = {
let text-params = (:)
if font != none {
text-params.insert("font", font)
}
if italic {
text-params.insert("style", "italic")
}
box(width: width * 1pt, text(txt, fill: color, size: size, ..text-params))
}
#let draw-block(block, x, y) = {
draw.content((x, -y), block, anchor: "north-west", stroke: none)
}
#let draw-line(color, a, b) = {
let (x0, y0) = a
let (x1, y1) = b
@@ -131,26 +113,15 @@
for (val, desc) in values.pairs().sorted(key: p => p.first()) {
desc-y += gap
let txt = val + " = " + desc
if config.description-width == none {
shapes += draw-text(
txt, txt-col, desc-x + bit-w / 2, desc-y,
anchor: "north-west",
font: config.italic-font-family,
italic: true,
size: config.italic-font-size
)
shapes += draw-text(
txt, txt-col, desc-x + bit-w / 2, desc-y,
anchor: "north-west",
font: config.italic-font-family,
italic: true,
size: config.italic-font-size
)
desc-y += config.italic-font-size / 1.2pt
} else {
let block = text-block(
txt, txt-col, config.description-width - bit-w / 2,
font: config.italic-font-family,
italic: true,
size: config.italic-font-size
)
shapes += draw-block(block, desc-x + bit-w / 2, desc-y)
desc-y += measure(block).height.pt()
}
desc-y += config.italic-font-size / 1.2pt
}
return (shapes, desc-x, desc-y)
@@ -178,47 +149,22 @@
shapes += draw-underbracket(config, start-x, start-x + width, start-y)
let mid-x = start-x + width / 2
let link-y = if config.description-width == none {
desc-y
} else {
// wrapped labels hang from desc-y, so aim at the middle of their first line
desc-y + config.default-font-size.pt() / 2 - bit-h / 2
}
shapes += draw-link(config, mid-x, start-y, desc-x, link-y)
shapes += draw-link(config, mid-x, start-y, desc-x, desc-y)
let txt-x = desc-x
if config.left-labels {
// desc-x is the right edge of the label when they are on the left, so shift
// to its left edge: exactly the box width when wrapping, an estimate from
// the character count otherwise
txt-x -= if config.description-width == none {
range_.description.len() * config.default-font-size / 2pt
} else {
config.description-width
}
txt-x -= range_.description.len() * config.default-font-size / 2pt
}
if config.description-width == none {
shapes += draw-text(
range_.description,
config.text-color,
txt-x, desc-y + bit-h / 2,
anchor: "west"
)
shapes += draw-text(
range_.description,
config.text-color,
txt-x, desc-y + bit-h / 2,
anchor: "west"
)
desc-y += config.default-font-size / 0.75pt
} else {
let block = text-block(
range_.description,
config.text-color,
config.description-width,
font: config.default-font-family,
size: config.default-font-size
)
shapes += draw-block(block, txt-x, desc-y)
desc-y += measure(block).height.pt()
}
desc-y += config.default-font-size / 0.75pt
if range_.values != none and range_.depends-on == none {
let shapes_
@@ -441,10 +387,8 @@
}
let range-boundaries = ()
for r in struct.ranges.values() {
let start-i = to-real-i(if config.ltr-bits {r.start} else {r.end})
let end-i = to-real-i(if config.ltr-bits {r.end} else {r.start}) + 1
range-boundaries.push(start-i)
range-boundaries.push(end-i)
let i = to-real-i(if config.ltr-bits {r.start} else {r.end})
range-boundaries.push(i)
}
// Draw colors
@@ -463,11 +407,6 @@
indices.push(r.start)
indices.push(r.end)
}
// ensure first and last bits are included
if not indices.contains(0) { indices.insert(0, struct.start) }
if not indices.contains(struct.bits + struct.start - 1) {
indices.push(struct.bits + struct.start - 1)
}
}
for i in range(struct.bits) {
@@ -514,9 +453,8 @@
let desc-y = bits-y + bit-h * 2
// Names + simple descriptions
for (i, range_) in ranges.enumerate() {
for range_ in ranges {
let start-i = to-real-i(if config.ltr-bits {range_.start} else {range_.end})
let end-i = to-real-i(if config.ltr-bits {range_.end} else {range_.start})
let start-x = bits-x + start-i * bit-w
let width = rng.bits(range_) * bit-w
@@ -526,28 +464,6 @@
let line-x = if config.ltr-bits {start-x + width} else {start-x}
shapes += draw-line(border-col, (line-x, bits-y), (line-x, bits-y + bit-h))
shapes += draw-text(range_.name, txt-col, name-x, name-y, fill: bg-col)
// paint end line only if needed
let is-not-limit = if config.ltr-bits { start-i != 0 } else { end-i != struct.bits - 1 }
if (
is-not-limit
and ranges
.at(
i
+ {
if config.ltr-bits and i < ranges.len() - 1 { 1 } else { -1 }
},
)
.end
!= range_.start - 1
) {
line-x += if config.ltr-bits { -width } else { width }
shapes += draw-line(
border-col,
(line-x, bits-y),
(line-x, bits-y + bit-h),
)
}
if range_.description != "" {
let shapes_
@@ -573,7 +489,7 @@
return (shapes, desc-y)
}
#let render(config, schema, width: 100%) = context {
#let render(config, schema, width: 100%) = {
set text(
font: config.default-font-family,
size: config.default-font-size
@@ -652,4 +568,4 @@
config: config,
render: render.with(config)
)
}
}
+2 -3
View File
@@ -76,9 +76,8 @@
}
return (
bits: int(elmt.attrs.bits),
ranges: ranges,
start: elmt.attrs.at("start", default: 0)
bits: elmt.attrs.bits,
ranges: ranges
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "rivet"
version = "0.3.1"
version = "0.3.0"
compiler = "0.13.1"
repository = "https://git.kb28.ch/HEL/rivet-typst"
entrypoint = "src/lib.typ"
@@ -9,6 +9,6 @@ authors = [
]
categories = [ "visualization" ]
license = "Apache-2.0"
description = "Register / Instruction Visualizer & Explainer Tool, using CeTZ"
description = "Register / Instruction Visualizer & Explainer Tool with Typst, using CeTZ"
keywords = [ "assembly", "instruction", "binary" ]
exclude = [ "gallery", "justfile", "docs" ]