diff --git a/packages/preview/diagraph/0.2.1/LICENSE b/packages/preview/diagraph/0.2.1/LICENSE new file mode 100644 index 000000000..25dc23975 --- /dev/null +++ b/packages/preview/diagraph/0.2.1/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Robotechnic + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/preview/diagraph/0.2.1/README.md b/packages/preview/diagraph/0.2.1/README.md new file mode 100644 index 000000000..ae46bfffb --- /dev/null +++ b/packages/preview/diagraph/0.2.1/README.md @@ -0,0 +1,122 @@ +# diagraph + +A simple Graphviz binding for Typst using the WebAssembly plugin system. + +## Usage + +### Basic usage + +This plugin is quite simple to use, you just need to import it: + +```typ +#import "@preview/diagraph:0.2.1": * +``` + +You can render a Graphviz Dot string to a SVG image using the `render` function: + +```typ +#render("digraph { a -> b }") +``` + +Alternatively, you can use `raw-render` to pass a `raw` instead of a string: + +````typ +#raw-render( + ```dot + digraph { + a -> b + } + ``` +) +```` + +You can see an example of this in [`examples/`](https://github.com/Robotechnic/diagraph/tree/main/examples). + +For more information about the Graphviz Dot language, you can check the [official documentation](https://graphviz.org/documentation/). + +### Arguments + +`render` and `raw-render` accept multiple arguments that help you customize your graphs. + +- `engine` (`str`) is the name of the engine to generate the graph with. Available engines are circo, dot, fdp, neato, nop, nop1, nop2, osage, patchwork, sfdp, and twopi. Defaults to `"dot"`. + +- `width` and `height` (`length` or `auto`) are the dimensions of the image to display. If set to `auto` (the default), will be the dimensions of the generated SVG. If a `length`, cannot be expressed in `em`. + +- `clip` (`bool`) determines whether to hide parts of the graph that extend beyond its frame. Defaults to `true`. + +- `background` (`none` or `color` or `gradient`) describes how to fill the background. If set to `none` (the default), the background will be transparent. + +- `labels` (`dict`) is a list of labels to use to override the defaults labels. This is discussed in depth in the next section. Defaults to `(:)`. + +### Labels + +By default, all node labels are rendered by Typst. If a node has no explicitly set label (using the `[label="..."]` syntax), its name is used as its label, and interpreted as math if possible. This means a node named `n_0` will render as 𝑛0. + +If you want a node label to contain a more complex mathematical equation, or more complex markup, you can use the `labels` argument: pass a dictionary that maps node names to Typst `content`. Each node with a name within the dictionary will have its label overridden by the corresponding content. + +````typ +#raw-render( + ``` + digraph { + rankdir=LR + node[shape=circle] + Hmm -> a_0 + Hmm -> big + a_0 -> "a'" -> big [style="dashed"] + big -> sum + } + ```, + labels: (: + big: [_some_#text(2em)[ big ]*text*], + sum: $ sum_(i=0)^n 1/i $, + ), +) +```` + +See [`examples/`](https://github.com/Robotechnic/diagraph/tree/main/examples) for the rendered graph. + +## Build + +This project was built with emscripten `3.1.46`. Apart from that, you just need to run `make wasm` to build the wasm file. All libraries are downloaded and built automatically to get the right version that works. + +There are also some other make commands: + +- `make link`: Link the project to the typst plugin folder +- `make clean`: Clean the build folder and the link +- `make clean-link`: Only clean the link +- `make compile_database`: Generate the compile_commands.json file +- `make module`: It copy the files needed to run the plugin in a folder called `graphviz` in the current directory +- `make wasi-stub`: Build the wasi stub executable, it require a rust toolchain properly configured + +### Wasi stub + +Somme functions need to be stubbed to work with the webassembly plugin system. The `wasi-stub` executable is a spetial one fitting the needs of the typst plugin system. You can find the source code [here](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master). It is important to use this one as the default subbed functions are not the same and the makefile is suited for this one. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details + +## Changelog + +### 0.2.1 + +- Added support for relative lenghts in the `width` and `height` arguments +- Fix various bugs + +### 0.2.0 + +- Node labels are now handled by Typst + +### 0.1.2 + +- Graphs are now scaled to make the graph text size match the document text size + +### 0.1.1 + +- Remove the `raw-render-rule` show rule because it doesn't allow use of custom font and the `render` / `raw-render` functions are more flexible +- Add the `background` parameter to the `render` and `raw-render` typst functions and default it to `transparent` instead of `white` +- Add center attribute to draw graph in the center of the svg in the `render` c function + +### 0.1.0 + +Initial working version diff --git a/packages/preview/diagraph/0.2.1/diagraph.wasm b/packages/preview/diagraph/0.2.1/diagraph.wasm new file mode 100755 index 000000000..d6e7ba585 Binary files /dev/null and b/packages/preview/diagraph/0.2.1/diagraph.wasm differ diff --git a/packages/preview/diagraph/0.2.1/internals.typ b/packages/preview/diagraph/0.2.1/internals.typ new file mode 100644 index 000000000..ca44a3745 --- /dev/null +++ b/packages/preview/diagraph/0.2.1/internals.typ @@ -0,0 +1,283 @@ +#let plugin = plugin("diagraph.wasm") + + +#let double-precision = 1000 + +#let length-to-int(value) = { + calc.round(value * double-precision / 1pt) +} + +#let int-to-length(value) = { + value / double-precision * 1pt +} + + +/// Encodes a 32-bytes integer into big-endian bytes. +#let encode-int(value) = { + bytes(( + calc.rem(calc.quo(value, 0x1000000), 0x100), + calc.rem(calc.quo(value, 0x10000), 0x100), + calc.rem(calc.quo(value, 0x100), 0x100), + calc.rem(calc.quo(value, 0x1), 0x100), + )) +} + +/// Decodes a big-endian integer from the given bytes. +#let decode-int(bytes) = { + let result = 0 + for byte in array(bytes) { + result = result * 256 + byte + } + return result +} + +/// Encodes an array of integers into bytes. +#let encode-int-array(arr) = { + bytes( + arr + .map(encode-int) + .map(array) + .flatten() + ) +} + +/// Encodes an array of strings into bytes. +#let encode-string-array(strings) = { + bytes(strings.map(string => array(bytes(string)) + (0,)).flatten()) +} + +/// Transforms bytes into an array whose elements are all `bytes` with the +/// specified length. +#let group-bytes(buffer, group-len) = { + assert(calc.rem(buffer.len(), group-len) == 0) + array(buffer).fold((), (acc, x) => { + if acc.len() != 0 and acc.last().len() < group-len { + acc.last().push(x) + acc + } else { + acc + ((x,),) + } + }).map(bytes) +} + +/// Group elements of the array in pairs. +#let array-to-pairs(arr) = { + assert(calc.even(arr.len())) + arr.fold((), (acc, x) => { + if acc.len() != 0 and acc.last().len() < 2 { + acc.last().push(x) + acc + } else { + acc + ((x,),) + } + }) +} + +/// Get an array of evaluated labels from a graph. +#let get-labels(manual-label-names, dot) = { + let encoded-labels = plugin.get_labels( + encode-int(manual-label-names.len()), + encode-string-array(manual-label-names), + bytes(dot), + ) + let encoded-label-array = array(encoded-labels).split(0).slice(0, -1).map(bytes) + encoded-label-array.map(encoded-label => { + let mode = str(encoded-label.slice(0, 1)) + let label-str = str(encoded-label.slice(1)) + if mode == "t" { + [#label-str] + } else if mode == "m" { + math.equation(eval(mode: "math", label-str)) + } else { + panic("Internal Diagraph error: Unsopported mode: `" + mode + "`") + } + }) +} + +/// Encodes the dimensions of labels into bytes. +#let encode-label-dimensions(styles, labels) = { + encode-int-array( + labels + .map(label => { + let dimensions = measure(label, styles) + ( + length-to-int(dimensions.width), + length-to-int(dimensions.height), + ) + }) + .flatten() + ) +} + +/// Converts any relative length to an absolute length. +#let relative-to-absolute(value, styles, container-dimension) = { + if type(value) == relative { + let absolute-part = relative-to-absolute(value.length, styles, container-dimension) + let ratio-part = relative-to-absolute(value.ratio, styles, container-dimension) + return absolute-part + ratio-part + } + if type(value) == length { + return value.abs + value.em * measure(line(length: 1em), styles).width + } + if type(value) == ratio { + return value * container-dimension + } + panic("Expected relative length, found " + str(type(value))) +} + +/// Renders a graph with Graphviz. +#let render( + /// A string containing Dot code. + dot, + /// Nodes whose name appear in this dictionary will have their label + /// overridden with the corresponding content. Defaults to an empty + /// dictionary. + labels: (:), + /// The name of the engine to generate the graph with. Defaults to `"dot"`. + engine: "dot", + /// The width of the image to display. If set to `auto` (the default), will be + /// the width of the generated SVG or, if the height is set to a value, it + /// will be scaled to keep the aspect ratio. + width: auto, + /// The height of the image to display. If set to `auto` (the default), will + /// be the height of the generated SVG or if the width is set to a value, it + /// will be scaled to keep the aspect ratio. + height: auto, + /// Whether to hide parts of the graph that extend beyond its frame. Defaults + /// to `true`. + clip: true, + /// A color or gradient to fill the background with. If set to `none` (the + /// default), the background will be transparent. + background: none, +) = { + let manual-labels = labels.values() + let manual-label-names = labels.keys() + let manual-label-count = manual-labels.len() + + let native-labels = get-labels(manual-label-names, dot) + let native-label-count = native-labels.len() + + layout(((width: container-width, height: container-height)) => style(styles => { + let font-size = measure(line(length: 1em), styles).width + + let output = plugin.render( + encode-int(length-to-int(font-size)), + bytes(dot), + encode-label-dimensions(styles, native-labels), + encode-label-dimensions(styles, manual-labels), + encode-string-array(manual-label-names), + bytes(engine), + ) + + if output.at(0) != 0 { + return { + show: highlight.with(fill: red) + set text(white) + raw(block: true, str(output)) + } + } + + let integer-size = output.at(1) + output = output.slice(2) + + // Get native label coordinates. + let native-label-coordinates-size = 2 * native-label-count * integer-size + let native-label-coordinates = array-to-pairs( + group-bytes(output.slice(0, native-label-coordinates-size), integer-size) + .map(decode-int) + .map(int-to-length) + ) + output = output.slice(native-label-coordinates-size) + + // Get manual label coordinates. + let manual-label-coordinate-sets = () + for manual-label-index in range(manual-label-count) { + let coordinate-set = () + let use-count = decode-int(output.slice(0, integer-size)) + output = output.slice(integer-size) + for i in range(use-count) { + coordinate-set.push( + (output.slice(0, integer-size), output.slice(integer-size, 2 * integer-size)) + .map(decode-int) + .map(int-to-length) + ) + output = output.slice(integer-size * 2) + } + manual-label-coordinate-sets.push(coordinate-set) + } + + // Get SVG dimensions. + let svg-width = int-to-length(decode-int(output.slice(0, integer-size))) + let svg-height = int-to-length(decode-int(output.slice(integer-size + 1, integer-size * 2))) + output = output.slice(integer-size * 2) + + let final-width = if width == auto { + svg-width + } else { + relative-to-absolute(width, styles, container-width) + } + let final-height = if height == auto { + svg-height + } else { + relative-to-absolute(height, styles, container-height) + } + + if width == auto and height != auto { + let ratio = final-height / svg-height + final-width = svg-width * ratio + } else if width != auto and height == auto { + let ratio = final-width / svg-width + final-height = svg-height * ratio + } + + + // Rescale the final image to the desired size. + show: block.with( + width: final-width, + height: final-height, + clip: clip, + breakable: false, + ) + show: scale.with( + origin: top + left, + x: final-width / svg-width * 100%, + y: final-height / svg-height * 100%, + ) + + // Construct the graph and its labels. + show: block.with(width: svg-width, height: svg-height, fill: background) + + // Display SVG. + image.decode( + output, + format: "svg", + width: svg-width, + height: svg-height, + ) + + // Place native labels. + for (label, coordinates) in native-labels.zip(native-label-coordinates) { + let (x, y) = coordinates + let label-dimensions = measure(label, styles) + place( + top + left, + dx: x - label-dimensions.width / 2, + dy: final-height - y - label-dimensions.height / 2 - (final-height - svg-height), + label, + ) + } + + // Place manual labels. + for (label, coordinate-set) in manual-labels.zip(manual-label-coordinate-sets) { + let label-dimensions = measure(label, styles) + for (x, y) in coordinate-set { + place( + top + left, + dx: x - label-dimensions.width / 2, + dy: final-height - y - label-dimensions.height / 2 - (final-height - svg-height), + label, + ) + } + } + })) +} diff --git a/packages/preview/diagraph/0.2.1/lib.typ b/packages/preview/diagraph/0.2.1/lib.typ new file mode 100644 index 000000000..be3a27655 --- /dev/null +++ b/packages/preview/diagraph/0.2.1/lib.typ @@ -0,0 +1,14 @@ +#import "internals.typ": render + +/// Renders a graph with Graphviz. +/// +/// See `render`'s documentation in `internals.typ` for a list of valid +/// arguments and their descriptions. +#let raw-render( + /// A `raw` element containing Dot code. + raw, + ..args, +) = { + assert(raw.has("text"), message: "`raw-render` expects a `raw` element") + return render(raw.text, ..args) +} diff --git a/packages/preview/diagraph/0.2.1/typst.toml b/packages/preview/diagraph/0.2.1/typst.toml new file mode 100644 index 000000000..09f7c7d9c --- /dev/null +++ b/packages/preview/diagraph/0.2.1/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "diagraph" +version = "0.2.1" +entrypoint = "lib.typ" +authors = ["Robotechnic", "MDLC01"] +license = "MIT" +repository = "https://github.com/Robotechnic/diagraph.git" +description = "Graphviz bindings for Typst" +compiler = "0.8.0" +exclude = ["graphviz_interface/*", "examples/*", ".gitignore", "Makefile"] +keywords = ["graphviz", "graph", "diagram"] \ No newline at end of file