diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7ca8837 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## [v0.0.2] - 2024-06-15 +### Added +- `width` parameter to `schema.render` for easier integration +- `all-bit-i` config option +- colored ranges +- format specification in the manual + +## [v0.0.1] - 2024-05-19 +- initial version +- ported all features from the [python package](https://git.kb28.ch/HEL/rivet/) \ No newline at end of file diff --git a/docs/config.typ b/docs/config.typ index 54c8c3d..3023224 100644 --- a/docs/config.typ +++ b/docs/config.typ @@ -24,6 +24,7 @@ /// - width (float): TODO -> remove /// - height (float): TODO -> remove /// - 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 /// -> dictionary #let config( default-font-family: "Ubuntu Mono", @@ -49,7 +50,8 @@ left-labels: false, width: 1200, height: 800, - full-page: false + full-page: false, + all-bit-i: true ) = {} /// Dark theme config diff --git a/docs/schema.typ b/docs/schema.typ index db1a811..4a56136 100644 --- a/docs/schema.typ +++ b/docs/schema.typ @@ -2,13 +2,15 @@ /// This function returns a dictionary of structures /// /// Supported formats: #schema.valid-extensions.map(e => raw("." + e)).join(", ") -/// - path-or-schema (str, raw): If it is a string, defines the path to load. \ -/// If it is a raw block, its content is directly parsed (the block's language will define the format to use) +/// - path-or-schema (str, raw, dictionary): If it is a string, defines the path to load. \ +/// If it is a raw block, its content is directly parsed (the block's language will define the format to use) \ +/// If it is a dictionary, it directly defines the schema structure /// -> dictionary #let load(path-or-schema) = {} /// Renders the given schema /// This functions -/// - structures (dictionary): A schema dictionary, as returned by #doc-ref("schema.load") +/// - schema (dictionary): A schema dictionary, as returned by #doc-ref("schema.load") /// - config (auto, dictionary): The configuration parameters, as returned by #doc-ref("config.config") -#let render(structures, config: auto) \ No newline at end of file +/// - width (ratio, length): The width of the generated figure +#let render(schema, config: auto, width: 100%) = {} \ No newline at end of file diff --git a/gallery.bash b/gallery.bash new file mode 100644 index 0000000..54c17e8 --- /dev/null +++ b/gallery.bash @@ -0,0 +1,40 @@ +#!/bin/bash + +PDFS=false + +while getopts "p" flag +do + case "${flag}" in + p) PDFS=true;; + esac +done + +echo "Generating gallery images" + +set -- ./gallery/example*.typ +cnt="$#" +i=1 +for f +do + f2="${f/typ/png}" + echo "($i/$cnt) $f -> $f2" + typst c --root ./ "$f" "$f2" + i=$((i+1)) +done + +if [ "$PDFS" = true ] +then + echo + echo "Generating gallery PDFs" + + set -- ./gallery/*.typ + cnt="$#" + i=1 + for f + do + f2="${f/typ/pdf}" + echo "($i/$cnt) $f -> $f2" + typst c --root ./ "$f" "$f2" + i=$((i+1)) + done +fi \ No newline at end of file diff --git a/gallery/example1.pdf b/gallery/example1.pdf index eeb4e1e..032a0f7 100644 Binary files a/gallery/example1.pdf and b/gallery/example1.pdf differ diff --git a/gallery/example1.png b/gallery/example1.png index 70bf2ae..17a1e7a 100644 Binary files a/gallery/example1.png and b/gallery/example1.png differ diff --git a/gallery/example2.pdf b/gallery/example2.pdf index a6beb04..953d011 100644 Binary files a/gallery/example2.pdf and b/gallery/example2.pdf differ diff --git a/gallery/example2.png b/gallery/example2.png index 33c0ef3..f5091e4 100644 Binary files a/gallery/example2.png and b/gallery/example2.png differ diff --git a/gallery/example3.pdf b/gallery/example3.pdf new file mode 100644 index 0000000..601bc47 Binary files /dev/null and b/gallery/example3.pdf differ diff --git a/gallery/example3.png b/gallery/example3.png new file mode 100644 index 0000000..e075d7f Binary files /dev/null and b/gallery/example3.png differ diff --git a/gallery/example3.typ b/gallery/example3.typ new file mode 100644 index 0000000..cf04b43 --- /dev/null +++ b/gallery/example3.typ @@ -0,0 +1,23 @@ +#import "../src/lib.typ": schema, config + +#let example = schema.load("/gallery/example1.yaml") +//#schema.render(example) + += Chapter 1 +#lorem(50) + += Chapter 2 +#lorem(50) + +== Section 2.1 + +#lorem(20) + +#figure( + schema.render(example, config: config.config(all-bit-i: false)), + caption: "Test schema" +) + +#lorem(20) + += Chapter 3 \ No newline at end of file diff --git a/gallery/example3.yaml b/gallery/example3.yaml new file mode 100644 index 0000000..1cc11ba --- /dev/null +++ b/gallery/example3.yaml @@ -0,0 +1,12 @@ +structures: + main: + bits: 32 + ranges: + 31-24: + name: op + 23-16: + name: r1 + 15-8: + name: r2 + 7-0: + name: r3 \ No newline at end of file diff --git a/gallery/test.json b/gallery/test.json index fc15e6d..f247bea 100644 --- a/gallery/test.json +++ b/gallery/test.json @@ -101,5 +101,11 @@ } } } + }, + "colors": { + "main": { + "31-28": "#FF0000", + "11-4": [34, 176, 43] + } } } diff --git a/gallery/test.pdf b/gallery/test.pdf index 1c90a5a..d08058f 100644 Binary files a/gallery/test.pdf and b/gallery/test.pdf differ diff --git a/gallery/test.typ b/gallery/test.typ index c8005f2..515a846 100644 --- a/gallery/test.typ +++ b/gallery/test.typ @@ -25,4 +25,115 @@ structures: ```) #schema.render(test-raw, config: config.config( full-page: true +)) + +#let test-typ = schema.load(( + structures: ( + main: ( + bits: 32, + ranges: ( + "31-28": (name: "cond"), + "27": (name: "0"), + "26": (name: "1"), + "25": (name: "I"), + "24": ( + name: "P", + description: "pre / post indexing bit", + values: ( + "0": "post, add offset after transfer", + "1": "pre, add offset before transfer" + ) + ), + "23": ( + name: "U", + description: "up / down bit", + values: ( + "0": "down, subtract offset from base", + "1": "up, addition offset to base" + ) + ), + "22": ( + name: "B", + description: "byte / word bit", + values: ( + "0": "transfer word quantity", + "1": "transfer byte quantity" + ) + ), + "21": ( + name: "W", + description: "write-back bit", + values: ( + "0": "no write-back", + "1": "write address into base" + ) + ), + "20": ( + name: "L", + description: "load / store bit", + values: ( + "0": "store to memory", + "1": "load from memory" + ) + ), + "19-16": ( + name: "Rn", + description: "base register" + ), + "15-12": ( + name: "Rd", + description: "source / destination register" + ), + "11-0": ( + name: "offset", + depends-on: "25", + values: ( + "0": ( + description: "offset is an immediate value", + structure: "immediateOffset" + ), + "1": ( + description: "offset is a register", + structure: "registerOffset" + ) + ) + ) + ) + ), + immediateOffset: ( + bits: 12, + ranges: ( + "11-0": ( + name: "12-bit immediate offset", + description: "unsigned number" + ) + ) + ), + registerOffset: ( + bits: 12, + ranges: ( + "11-4": ( + name: "shift", + description: "shift applied to Rm" + ), + "3-0": ( + name: "Rm", + description: "offset register" + ) + ) + ) + ), + colors: ( + main: ( + "31-28": red, + "11-4": green + ), + registerOffset: ( + "11-4": rgb(240, 140, 80) + ) + ) +)) + +#schema.render(test-typ, config: config.config( + full-page: true )) \ No newline at end of file diff --git a/gallery/test.xml b/gallery/test.xml index ab92e22..4a8b716 100644 --- a/gallery/test.xml +++ b/gallery/test.xml @@ -65,4 +65,7 @@ offset register + + + \ No newline at end of file diff --git a/gallery/test.yaml b/gallery/test.yaml index 2ca7964..f7119ae 100644 --- a/gallery/test.yaml +++ b/gallery/test.yaml @@ -71,3 +71,11 @@ structures: 3-0: name: Rm description: offset register + +colors: + main: + 31-28: "#3EFA6B" + 25-23: + - 100 + - 150 + - 200 \ No newline at end of file diff --git a/manual.pdf b/manual.pdf index 414b565..2031103 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index 8124525..214fdfe 100644 --- a/manual.typ +++ b/manual.typ @@ -1,4 +1,6 @@ #import "@preview/tidy:0.3.0" +#import "@preview/codelst:2.0.1": sourcecode +#import "@preview/showybox:2.0.1": showybox #import "src/lib.typ" #import "src/schema.typ" #import "docs/examples.typ" @@ -26,13 +28,26 @@ link(label(label-name))[#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) = 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. For more information on the schema format, please check out the original project's #link("https://git.kb28.ch/HEL/rivet/src/branch/main/format.md")[format.md] +This is a port of the #link("https://git.kb28.ch/HEL/rivet")[homonymous Python script] for Typst. = Usage @@ -43,6 +58,185 @@ Simply import `schema` from #link("src/lib.typ") and call `schema.load` to parse #schema.render(doc) ```] += 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 on GitHub to get familiar with it. + +== Main layout + +A schema contains a dictionary of structures. The must be at least one defined structure named "main". + +It can also optionnaly contain a "colors" dictionary. More details about this in #link()[Colors] + +#sourcecode[```json +{ + "structures": { + "main": { + ... + }, + "struct1": { + ... + }, + "struct2": { + ... + }, + ... + } +} +```] + +#pagebreak(weak: true) + +== 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()[Range]). A range's structure can also depend on another range's value (see #link()[Dependencies]). + +The range name (or key) defines the left- and rightmost bits (e.g. `7-4` goes from bit 7 down to bit 4). Bits are displayed in big-endian, i.e. the leftmost bit has the highest value. + +#sourcecode[```json +"main": { + "bits": 8, + "ranges": { + "7-4": { + ... + }, + "3-2": { + ... + }, + "1": { + ... + }, + "0": { + ... + } + } +} +```] + +== 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()[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) +] + +#sourcecode[```json +"3-2": { + "name": "op", + "description": "Logical operation", + "values": { + "00": "AND", + "01": "OR", + "10": "XOR", + "11": "NAND" + } +} +```] + +#pagebreak(weak: true) + +== 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 above the horizontal dependency arrow) + +#sourcecode[```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: + +#sourcecode[```json +{ + "structures": { + "main": { + ... + }, + "immediateValue": { + "bits": 4, + ... + }, + "registerValue": { + "bits": 4, + ... + }, + ... + } +} +```] + +#pagebreak(weak: true) + +== 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: + +#sourcecode[```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: + +#sourcecode[```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: + #sourcecode[```xml + + + ... + + ... + + + + ```] +] + +#pagebreak(weak: true) + = Config presets Aside from the default config, some example presets are also provided: diff --git a/src/config.typ b/src/config.typ index f835eb4..f3868ad 100644 --- a/src/config.typ +++ b/src/config.typ @@ -22,7 +22,8 @@ left-labels: false, width: 1200, height: 800, - full-page: false + full-page: false, + all-bit-i: true ) = { return ( default-font-family: default-font-family, @@ -48,7 +49,8 @@ left-labels: left-labels, width: width, height: height, - full-page: full-page + full-page: full-page, + all-bit-i: all-bit-i ) } diff --git a/src/lib.typ b/src/lib.typ index fa6c0a3..8b19e84 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,4 +1,4 @@ -#let version = version((0,0,1)) +#let version = version((0,0,2)) #import "config.typ" #import "schema.typ" \ No newline at end of file diff --git a/src/renderer.typ b/src/renderer.typ index 238a0fb..e07c4f5 100644 --- a/src/renderer.typ +++ b/src/renderer.typ @@ -32,20 +32,12 @@ if italic { text-params.insert("style", "italic") } - - let content-params = (:) - if fill != none { - content-params.insert("fill", fill) - content-params.insert("frame", "rect") - content-params.insert("padding", (4pt, 0pt)) - } draw.content( (x, -y), text(txt, fill: color, size: size, ..text-params), anchor: anchor, - stroke: none, - ..content-params + stroke: none ) } @@ -253,7 +245,7 @@ #let draw-dependency( draw-struct, config, - struct, structures, bits-x, bits-y, range_, desc-x, desc-y + struct, schema, bits-x, bits-y, range_, desc-x, desc-y ) = { let shapes = () @@ -301,7 +293,7 @@ val-struct = structure.load("", val-struct) let shapes_ - (shapes_, ..) = draw-struct(config, val-struct, structures, ox: depend-start-x, oy: desc-y) + (shapes_, ..) = draw-struct(config, val-struct, schema, ox: depend-start-x, oy: desc-y) shapes += shapes_ let y = desc-y + bit-h * 1.5 @@ -332,7 +324,7 @@ prev-range-y = prev-depend-y depend-range.last-value-y = prev-depend-y - (shapes_, desc-y) = draw-struct(config, structures.at(data.structure), structures, ox: start-x, oy: desc-y) + (shapes_, desc-y) = draw-struct(config, schema.structures.at(data.structure), schema, ox: start-x, oy: desc-y) shapes += shapes_ } @@ -341,8 +333,9 @@ return (shapes, desc-x, desc-y, struct) } -#let draw-structure(config, struct, structures, ox: 0, oy: 0) = { +#let draw-structure(config, struct, schema, ox: 0, oy: 0) = { let shapes + let colors = schema.at("colors", default: (:)) let bg-col = config.background let txt-col = config.text-color let border-col = config.border-color @@ -352,22 +345,60 @@ let (bits-x, bits-y) = (ox, oy + bit-h) let bits-width = struct.bits * bit-w let start-bit = struct.start + let bit-colors = (:) + for i in range(struct.bits) { + bit-colors.insert(str(i), bg-col) + } + if struct.name in colors { + for (s, col) in colors.at(struct.name) { + let (start, end) = rng.parse-span(s) + for i in range(start, end + 1) { + let real-i = struct.bits - i - 1 + start-bit + bit-colors.insert(str(real-i), col) + } + } + } + let range-boundaries = () + for r in struct.ranges.values() { + let i = struct.bits - r.end - 1 + start-bit + range-boundaries.push(i) + } + + // Draw colors + for i in range(struct.bits) { + let bit-x = ox + i * bit-w + shapes += draw-rect(bit-colors.at(str(i)), bit-x, bits-y, bit-w+1, bit-h) + } // Draw rectangle around structure shapes += draw-rect(border-col, bits-x, bits-y, bits-width, bit-h, thickness: 2) + let indices = range(struct.bits) + if not config.all-bit-i { + indices = () + for r in struct.ranges.values() { + indices.push(r.start) + indices.push(r.end) + } + } + for i in range(struct.bits) { let bit-x = ox + i * bit-w - shapes += draw-text( - str(struct.bits - i - 1 + start-bit), - txt-col, - bit-x + bit-w / 2, - oy + bit-h / 2 - ) + let real-i = struct.bits - i - 1 + start-bit + + if real-i in indices { + shapes += draw-text( + str(real-i), + txt-col, + bit-x + bit-w / 2, + oy + bit-h / 2 + ) + } // Draw separator - if i != 0 { - shapes += draw-line(border-col, (bit-x, bits-y), (bit-x, bits-y + bit-h)) + if i != 0 and not i in range-boundaries { + shapes += draw-line(border-col, (bit-x, bits-y), (bit-x, bits-y + bit-h * 0.2)) + shapes += draw-line(border-col, (bit-x, bits-y + bit-h * 0.8), (bit-x, bits-y + bit-h)) } } @@ -400,13 +431,7 @@ let name-x = start-x + width / 2 let name-y = bits-y + bit-h / 2 - shapes += draw-rect( - bg-col, - start-x + bit-w / 2, - name-y - bit-h * 0.3, - width - bit-w, - bit-h * 0.6 - ) + shapes += draw-line(border-col, (start-x, bits-y), (start-x, bits-y + bit-h)) shapes += draw-text(range_.name, txt-col, name-x, name-y, fill: bg-col) if range_.description != "" { @@ -424,7 +449,7 @@ let shapes_ (shapes_, desc-x, desc-y, struct) = draw-dependency( draw-structure, config, - struct, structures, bits-x, bits-y, range_, desc-x, desc-y + struct, schema, bits-x, bits-y, range_, desc-x, desc-y, ) shapes += shapes_ } @@ -433,13 +458,13 @@ return (shapes, desc-y) } -#let render(config, structures) = { +#let render(config, schema, width: 100%) = { set text( font: config.default-font-family, size: config.default-font-size ) - let main = structures.main + let main = schema.structures.main let ox = config.margins.at(3) if config.left-labels { ox = config.width - ox - main.bits * config.bit-width @@ -458,9 +483,9 @@ set page(..params) - canvas(length: 1pt, background: config.background, { + let cnvs = canvas(length: 1pt, background: config.background, { let (shapes, _) = draw-structure( - config, main, structures, + config, main, schema, ox: ox, oy: config.margins.at(0) ) @@ -475,6 +500,36 @@ fill: none ) }) + + if config.full-page { + cnvs + } else { + layout(size => { + let m = measure(cnvs) + let w = m.width + let h = m.height + let base-w = if type(width) == ratio { + size.width * width + } else { + width + } + let r = if w == 0 { + 0 + } else { + base-w / w + } + + let new-w = w * r + let new-h = h * r + r *= 100% + + box( + width: new-w, + height: new-h, + scale(x: r, y: r, cnvs, reflow: true) + ) + }) + } } #let make(config) = { diff --git a/src/schema.typ b/src/schema.typ index a3328ba..5c1f650 100644 --- a/src/schema.typ +++ b/src/schema.typ @@ -44,22 +44,49 @@ #let load(path-or-schema) = { let schema = if type(path-or-schema) == str { parse-file(path-or-schema) + } else if type(path-or-schema) == dictionary { + path-or-schema } else { parse-raw(path-or-schema) } + if "colors" in schema { + for struct in schema.colors.keys() { + for (span, col) in schema.colors.at(struct) { + if type(col) == str { + if col.starts-with("#") { + col = rgb(col) + } else { + let (r, g, b) = col.split(",").map(v => int(v)) + col = rgb(r, g, b) + } + } else if type(col) == array { + col = rgb(..col) + } else if type(col) != color { + panic("Invalid color format") + } + schema.colors.at(struct).at(span) = col + } + } + } else { + schema.insert("colors", (:)) + } + let structures = (:) for (id, data) in schema.structures { id = str(id) structures.insert(id, structure.load(id, data)) } - return structures + return ( + structures: structures, + colors: schema.at("colors", default: (:)) + ) } -#let render(structures, config: auto) = { +#let render(schema, width: 100%, config: auto) = { if config == auto { config = conf.config() } let renderer_ = renderer.make(config) - (renderer_.render)(structures) + (renderer_.render)(schema, width: width) } \ No newline at end of file diff --git a/src/xml-loader.typ b/src/xml-loader.typ index 1f00c42..9513766 100644 --- a/src/xml-loader.typ +++ b/src/xml-loader.typ @@ -83,8 +83,10 @@ #let parse(content) = { let struct-elmts = content.children.filter(e => "tag" in e and e.tag == "structure") + let color-elmts = content.children.filter(e => "tag" in e and e.tag == "color") let structures = (:) + let colors = (:) for struct-elmt in struct-elmts { structures.insert( @@ -93,8 +95,19 @@ ) } + for color-elmt in color-elmts { + let struct = color-elmt.attrs.structure + if not struct in colors { + colors.insert(struct, (:)) + } + + let span = color-elmt.attrs.end + "-" + color-elmt.attrs.start + colors.at(struct).insert(span, color-elmt.attrs.color) + } + return ( - structures: structures + structures: structures, + colors: colors ) } diff --git a/typst.toml b/typst.toml index a10dcb7..0145bae 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] name = "rivet" -version = "0.0.1" +version = "0.0.2" compiler = "0.11.0" repository = "https://git.kb28.ch/HEL/rivet-typst" entrypoint = "src/lib.typ"