forked from HEL/rivet-typst
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab3ccfccea | ||
|
|
5ac65afaff | ||
|
|
472562596f
|
||
|
|
39e6acaed8
|
||
|
|
6cc3cfcd00
|
||
|
|
861c25026b
|
||
|
|
768e63fd46
|
||
|
|
2678aee584
|
||
|
|
2228f22352
|
||
|
|
a909398fbd
|
||
|
|
05de90890f | ||
|
|
9b83b4c4a8 | ||
|
|
df7899ee4b | ||
|
|
b6f2fd99b8 |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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
|
## [v0.3.0] - 2025-05-03
|
||||||
- updated CeTZ to 0.3.4
|
- updated CeTZ to 0.3.4
|
||||||
- updated to Typst 0.13.1
|
- updated to Typst 0.13.1
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ It is based on the [homonymous Python script](https://git.kb28.ch/HEL/rivet/)
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="./gallery/example1.typ">
|
<a href="./gallery/example1.typ">
|
||||||
<img src="./gallery/example1.png" width="1000px">
|
<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.">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -19,7 +19,7 @@ It is based on the [homonymous Python script](https://git.kb28.ch/HEL/rivet/)
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="./gallery/example2.typ">
|
<a href="./gallery/example2.typ">
|
||||||
<img src="./gallery/example2.png" width="1000px">
|
<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.">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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à !
|
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
|
```typ
|
||||||
#import "@preview/rivet:0.3.0": schema
|
#import "@preview/rivet:0.3.1": schema
|
||||||
#let doc = schema.load(yaml("path/to/schema.yaml"))
|
#let doc = schema.load(yaml("path/to/schema.yaml"))
|
||||||
#schema.render(doc)
|
#schema.render(doc)
|
||||||
```
|
```
|
||||||
+3
-1
@@ -26,6 +26,7 @@
|
|||||||
/// - full-page (bool): If true, the page will be resized to fit the diagram and take the background color
|
/// - 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
|
/// - 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
|
/// - 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
|
/// -> dictionary
|
||||||
#let config(
|
#let config(
|
||||||
default-font-family: "Ubuntu Mono",
|
default-font-family: "Ubuntu Mono",
|
||||||
@@ -53,7 +54,8 @@
|
|||||||
height: 800,
|
height: 800,
|
||||||
full-page: false,
|
full-page: false,
|
||||||
all-bit-i: true,
|
all-bit-i: true,
|
||||||
ltr-bits: false
|
ltr-bits: false,
|
||||||
|
description-width: none
|
||||||
) = {}
|
) = {}
|
||||||
|
|
||||||
/// Dark theme config
|
/// Dark theme config
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
#import "../src/lib.typ": schema, config
|
#import "@preview/rivet:0.3.1": schema, config
|
||||||
|
|
||||||
#let example = schema.load("/gallery/example1.yaml")
|
#let example = schema.load(yaml("./example1.yaml"))
|
||||||
#schema.render(example, config: config.config(
|
#schema.render(example, config: config.config(
|
||||||
full-page: true
|
full-page: true
|
||||||
))
|
))
|
||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
#import "../src/lib.typ": schema, config
|
#import "@preview/rivet:0.3.1": schema, config
|
||||||
|
|
||||||
#let example = schema.load("/gallery/example2.yaml")
|
#let example = schema.load(yaml("./example2.yaml"))
|
||||||
#schema.render(example, config: config.blueprint(
|
#schema.render(example, config: config.blueprint(
|
||||||
full-page: true,
|
full-page: true,
|
||||||
left-labels: true
|
left-labels: true
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
#import "../src/lib.typ": schema, config
|
#import "@preview/rivet:0.3.1": schema, config
|
||||||
|
|
||||||
#let example = schema.load("/gallery/example1.yaml")
|
#let example = schema.load(yaml("/gallery/example1.yaml"))
|
||||||
//#schema.render(example)
|
//#schema.render(example)
|
||||||
|
|
||||||
= Chapter 1
|
= Chapter 1
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#import "../../src/lib.typ": *
|
#import "@preview/rivet:0.3.1": *
|
||||||
|
|
||||||
#let conf = config.config(
|
#let conf = config.config(
|
||||||
full-page: true,
|
full-page: true,
|
||||||
left-labels: true
|
left-labels: true
|
||||||
)
|
)
|
||||||
#let alu = schema.load("/gallery/riscv/alu_instr.yaml")
|
#let alu = schema.load(yaml("./alu_instr.yaml"))
|
||||||
#schema.render(alu, config: conf)
|
#schema.render(alu, config: conf)
|
||||||
|
|
||||||
#let branch = schema.load("/gallery/riscv/branch_instr.yaml")
|
#let branch = schema.load(yaml("./branch_instr.yaml"))
|
||||||
#schema.render(branch, config: conf)
|
#schema.render(branch, config: conf)
|
||||||
|
|
||||||
#let mem = schema.load("/gallery/riscv/mem_instr.yaml")
|
#let mem = schema.load(yaml("./mem_instr.yaml"))
|
||||||
#schema.render(mem, config: conf)
|
#schema.render(mem, config: conf)
|
||||||
|
|||||||
Binary file not shown.
+5
-4
@@ -1,16 +1,17 @@
|
|||||||
#import "../src/lib.typ": *
|
#import "@preview/rivet:0.3.1": *
|
||||||
|
|
||||||
#let test-yaml = schema.load("/gallery/test.yaml")
|
#let test-yaml = schema.load(yaml("./test.yaml"))
|
||||||
#schema.render(test-yaml, config: config.config(
|
#schema.render(test-yaml, config: config.config(
|
||||||
full-page: true
|
full-page: true
|
||||||
))
|
))
|
||||||
|
|
||||||
#let test-json = schema.load("/gallery/test.json")
|
#let test-json = schema.load(json("./test.json"))
|
||||||
#schema.render(test-json, config: config.blueprint(
|
#schema.render(test-json, config: config.blueprint(
|
||||||
full-page: true
|
full-page: true
|
||||||
))
|
))
|
||||||
|
|
||||||
#let test-xml = schema.load("/gallery/test.xml")
|
#let test-xml-raw = schema.xml-loader.parse(xml("./test.xml").first())
|
||||||
|
#let test-xml = schema.load(test-xml-raw)
|
||||||
#schema.render(test-xml, config: config.dark(
|
#schema.render(test-xml, config: config.dark(
|
||||||
full-page: true
|
full-page: true
|
||||||
))
|
))
|
||||||
|
|||||||
+9930
-8910
File diff suppressed because it is too large
Load Diff
+32
-2
@@ -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".
|
A schema contains a dictionary of structures. There must be at least one defined structure named "main".
|
||||||
|
|
||||||
It can also optionnaly contain a "colors" dictionary. More details about this in #link(<format-colors>)[Colors]
|
It can also optionally contain a "colors" dictionary. More details about this in #link(<format-colors>)[Colors]
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -203,6 +203,36 @@ 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)
|
#pagebreak(weak: true)
|
||||||
|
|
||||||
== Dependencies <format-dependencies>
|
== Dependencies <format-dependencies>
|
||||||
@@ -363,7 +393,7 @@ structures:
|
|||||||
#let x = schema.xml-loader.load("schema.xml")
|
#let x = schema.xml-loader.load("schema.xml")
|
||||||
#let s = schema.load(x)
|
#let s = schema.load(x)
|
||||||
// From file
|
// From file
|
||||||
#let x = schema.xml-loader.parse(yaml("schema.yaml").first())
|
#let x = schema.xml-loader.parse(xml("schema.xml").first())
|
||||||
#let s = schema.load(x)
|
#let s = schema.load(x)
|
||||||
// Raw block
|
// Raw block
|
||||||
#let s = schema.load(```xml
|
#let s = schema.load(```xml
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
full-page: false,
|
full-page: false,
|
||||||
all-bit-i: true,
|
all-bit-i: true,
|
||||||
ltr-bits: false,
|
ltr-bits: false,
|
||||||
|
description-width: none,
|
||||||
) = {
|
) = {
|
||||||
return (
|
return (
|
||||||
default-font-family: default-font-family,
|
default-font-family: default-font-family,
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
full-page: full-page,
|
full-page: full-page,
|
||||||
all-bit-i: all-bit-i,
|
all-bit-i: all-bit-i,
|
||||||
ltr-bits: ltr-bits,
|
ltr-bits: ltr-bits,
|
||||||
|
description-width: description-width,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#let version = version(0,3,0)
|
#let version = version(0,3,1)
|
||||||
|
|
||||||
#import "config.typ"
|
#import "config.typ"
|
||||||
#import "schema.typ"
|
#import "schema.typ"
|
||||||
+78
-22
@@ -41,6 +41,24 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 draw-line(color, a, b) = {
|
||||||
let (x0, y0) = a
|
let (x0, y0) = a
|
||||||
let (x1, y1) = b
|
let (x1, y1) = b
|
||||||
@@ -113,15 +131,26 @@
|
|||||||
for (val, desc) in values.pairs().sorted(key: p => p.first()) {
|
for (val, desc) in values.pairs().sorted(key: p => p.first()) {
|
||||||
desc-y += gap
|
desc-y += gap
|
||||||
let txt = val + " = " + desc
|
let txt = val + " = " + desc
|
||||||
shapes += draw-text(
|
if config.description-width == none {
|
||||||
txt, txt-col, desc-x + bit-w / 2, desc-y,
|
shapes += draw-text(
|
||||||
anchor: "north-west",
|
txt, txt-col, desc-x + bit-w / 2, desc-y,
|
||||||
font: config.italic-font-family,
|
anchor: "north-west",
|
||||||
italic: true,
|
font: config.italic-font-family,
|
||||||
size: config.italic-font-size
|
italic: true,
|
||||||
)
|
size: config.italic-font-size
|
||||||
|
)
|
||||||
|
|
||||||
desc-y += config.italic-font-size / 1.2pt
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (shapes, desc-x, desc-y)
|
return (shapes, desc-x, desc-y)
|
||||||
@@ -149,22 +178,47 @@
|
|||||||
shapes += draw-underbracket(config, start-x, start-x + width, start-y)
|
shapes += draw-underbracket(config, start-x, start-x + width, start-y)
|
||||||
|
|
||||||
let mid-x = start-x + width / 2
|
let mid-x = start-x + width / 2
|
||||||
shapes += draw-link(config, mid-x, start-y, desc-x, desc-y)
|
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)
|
||||||
|
|
||||||
let txt-x = desc-x
|
let txt-x = desc-x
|
||||||
|
|
||||||
if config.left-labels {
|
if config.left-labels {
|
||||||
txt-x -= range_.description.len() * config.default-font-size / 2pt
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shapes += draw-text(
|
if config.description-width == none {
|
||||||
range_.description,
|
shapes += draw-text(
|
||||||
config.text-color,
|
range_.description,
|
||||||
txt-x, desc-y + bit-h / 2,
|
config.text-color,
|
||||||
anchor: "west"
|
txt-x, desc-y + bit-h / 2,
|
||||||
)
|
anchor: "west"
|
||||||
|
)
|
||||||
|
|
||||||
desc-y += config.default-font-size / 0.75pt
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
if range_.values != none and range_.depends-on == none {
|
if range_.values != none and range_.depends-on == none {
|
||||||
let shapes_
|
let shapes_
|
||||||
@@ -387,8 +441,10 @@
|
|||||||
}
|
}
|
||||||
let range-boundaries = ()
|
let range-boundaries = ()
|
||||||
for r in struct.ranges.values() {
|
for r in struct.ranges.values() {
|
||||||
let i = to-real-i(if config.ltr-bits {r.start} else {r.end})
|
let start-i = to-real-i(if config.ltr-bits {r.start} else {r.end})
|
||||||
range-boundaries.push(i)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw colors
|
// Draw colors
|
||||||
@@ -472,9 +528,9 @@
|
|||||||
shapes += draw-text(range_.name, txt-col, name-x, name-y, fill: bg-col)
|
shapes += draw-text(range_.name, txt-col, name-x, name-y, fill: bg-col)
|
||||||
|
|
||||||
// paint end line only if needed
|
// paint end line only if needed
|
||||||
|
let is-not-limit = if config.ltr-bits { start-i != 0 } else { end-i != struct.bits - 1 }
|
||||||
if (
|
if (
|
||||||
start-i != 0
|
is-not-limit
|
||||||
and end-i != struct.bits - 1
|
|
||||||
and ranges
|
and ranges
|
||||||
.at(
|
.at(
|
||||||
i
|
i
|
||||||
@@ -517,7 +573,7 @@
|
|||||||
return (shapes, desc-y)
|
return (shapes, desc-y)
|
||||||
}
|
}
|
||||||
|
|
||||||
#let render(config, schema, width: 100%) = {
|
#let render(config, schema, width: 100%) = context {
|
||||||
set text(
|
set text(
|
||||||
font: config.default-font-family,
|
font: config.default-font-family,
|
||||||
size: config.default-font-size
|
size: config.default-font-size
|
||||||
|
|||||||
+3
-2
@@ -76,8 +76,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
bits: elmt.attrs.bits,
|
bits: int(elmt.attrs.bits),
|
||||||
ranges: ranges
|
ranges: ranges,
|
||||||
|
start: elmt.attrs.at("start", default: 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rivet"
|
name = "rivet"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
compiler = "0.13.1"
|
compiler = "0.13.1"
|
||||||
repository = "https://git.kb28.ch/HEL/rivet-typst"
|
repository = "https://git.kb28.ch/HEL/rivet-typst"
|
||||||
entrypoint = "src/lib.typ"
|
entrypoint = "src/lib.typ"
|
||||||
@@ -9,6 +9,6 @@ authors = [
|
|||||||
]
|
]
|
||||||
categories = [ "visualization" ]
|
categories = [ "visualization" ]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
description = "Register / Instruction Visualizer & Explainer Tool with Typst, using CeTZ"
|
description = "Register / Instruction Visualizer & Explainer Tool, using CeTZ"
|
||||||
keywords = [ "assembly", "instruction", "binary" ]
|
keywords = [ "assembly", "instruction", "binary" ]
|
||||||
exclude = [ "gallery", "justfile", "docs" ]
|
exclude = [ "gallery", "justfile", "docs" ]
|
||||||
Reference in New Issue
Block a user