diff --git a/packages/preview/diagraph/0.3.1/LICENSE b/packages/preview/diagraph/0.3.1/LICENSE
new file mode 100644
index 0000000000..a85204e590
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2025 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.3.1/README.md b/packages/preview/diagraph/0.3.1/README.md
new file mode 100644
index 0000000000..19743093aa
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/README.md
@@ -0,0 +1,100 @@
+# diagraph
+
+A simple Graphviz binding for Typst using the WebAssembly plugin system.
+
+## Usage
+
+### Basic usage
+
+
+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:
+
+<!--EXAMPLE(raw-render)-->
+````typ
+#raw-render(
+  ```dot
+  digraph {
+    a -> b
+  }
+  ```
+)
+````
+![raw-render](https://raw.githubusercontent.com/Robotechnic/diagraph/main/images/raw-render1.png)
+
+For more information about the Graphviz Dot language, you can check the [official documentation](https://graphviz.org/documentation/).
+
+### Advanced usage
+
+Check the [manual](https://raw.githubusercontent.com/Robotechnic/diagraph/main/doc/manual.pdf) for more information about the plugin.
+
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
+
+## Changelog
+
+### 0.3.1
+
+- Updated graphviz version to 12.2.1
+- Fixed a bug with the font being incorrectly set
+- Added adjacency lists to the graph rendering possibilities
+
+### 0.3.0
+
+- Added support for edge labels
+- Added a manual generated with Typst
+- Updated graphviz version
+- Fix an error in math mode detection
+
+### 0.2.5
+
+- If the shape is point, the label isn't displayed
+- Now a minimum size is not enforced if the node label is empty
+- Added support for font alternatives
+
+### 0.2.4
+
+- Added support for xlabels which are now rendered by Typst
+- Added support for cluster labels which are now rendered by Typst
+- Fix a margin problem with the clusters
+
+### 0.2.3
+
+- Updated to typst 0.11.0
+- Added support for `fontcolor`, `fontsize` and `fontname` nodes attributes
+- Diagraph now uses a protocol generator to generate the wasm interface
+
+### 0.2.2
+
+- Fix an alignment issue
+- Added a better mathematic formula recognition for node labels
+
+### 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.3.1/adjacency.typ b/packages/preview/diagraph/0.3.1/adjacency.typ
new file mode 100644
index 0000000000..0ce65b8384
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/adjacency.typ
@@ -0,0 +1,191 @@
+#import "internals.typ": render
+
+#let inches = (
+  "width",
+  "height",
+  "len",
+  "lheight",
+  "lwidth",
+  "margin",
+  "nodesep",
+  "page",
+  "size",
+  "vertices",
+)
+
+#let value-to-str(key, value) = {
+  if type(value) == length {
+    if key in inches {
+      str(value.to-absolute().inches())
+    } else {
+      str(value.to-absolute().pt())
+    }
+  } else if type(value) == color {
+    "\"" + value.to-hex() + "\""
+  } else if value == none {
+    "none"
+  } else if value == true {
+    "true"
+  } else if value == false {
+    "false"
+  } else {
+    "\"" + str(value) + "\""
+  }
+}
+
+#let dict-to-graph-args(args, sep: ";", parent: "") = {
+  for (key, value) in args {
+    if type(value) == dictionary {
+      if sep == "," {
+        panic("Invalid argument for " + parent + "[" + key + "=...]")
+      }
+      key + "[" + dict-to-graph-args(value, sep: ",", parent: key) + "]"
+    } else {
+      key + "=" + if type(value) == array {
+        "\"(" + value.map(value-to-str.with(key)).join(",") + ")\""
+      } else {
+        value-to-str(key, value)
+      }
+    } + sep
+  }
+}
+
+#let create-nodes(min, max) = {
+  range(min, max).map(str).join(";")
+}
+
+#let create-attributes(labels) = {
+  labels
+    .enumerate()
+    .map(label => {
+        if type(label.at(1)) == dictionary {
+          let _ = label.at(1).remove("label", default: "")
+          _ = label.at(1).remove("xlabel", default: "")
+          str(label.at(0)) + "[" + dict-to-graph-args(label.at(1), sep: ",") + "]"
+        }
+      })
+    .join("")
+}
+
+#let create-clusters(clusters) = {
+  clusters
+    .enumerate()
+    .map(cluster => {
+        let id = str(cluster.at(0))
+        let nodes = cluster.at(1)
+        "subgraph cluster_" + id + "{"
+        if type(nodes) == dictionary {
+          let _ = nodes.remove("label", default: "")
+          let subnodes = nodes.remove("nodes")
+          dict-to-graph-args(nodes)
+          subnodes.map(str).join(";")
+        } else {
+          nodes.map(str).join(";")
+        } 
+        "};"
+      })
+    .join("")
+}
+
+#let build-edges(adjacency, directed) = {
+  adjacency
+    .enumerate()
+    .map(edges-list => {
+        edges-list
+          .at(1)
+          .enumerate()
+          .map(edge => {
+              if edge.at(1) == none {
+                ""
+              } else {
+                str(edges-list.at(0))
+                if directed {
+                  " -> "
+                } else {
+                  " -- "
+                }
+                str(edge.at(0)) + ";"
+              }
+            })
+          .join("")
+      })
+    .join("")
+}
+
+#let adjacency(..args) = context {
+  if args.pos().len() != 1 {
+    panic("adjacency() requires one argument: an adjacency matrix")
+  }
+  let adjacency = args.at(0)
+  let graph-params = args.named()
+  let vertex-labels = graph-params.remove("vertex-labels", default: ())
+  let directed = graph-params.remove("directed", default: true)
+  let clusters = graph-params.remove("clusters", default: ())
+	let debug = graph-params.remove("debug", default: false)
+	let clip = graph-params.remove("clip", default: true)
+
+  render(
+    if directed {
+      "digraph"
+    } else {
+      "graph"
+    } + "{" + dict-to-graph-args(graph-params) + create-nodes(
+      0,
+      adjacency.len(),
+    ) + ";" + create-clusters(clusters) + create-attributes(vertex-labels) + build-edges(adjacency, directed) + "}",
+		debug: debug,
+		clip: clip,
+    labels: name => {
+      let id = int(name)
+      if id < vertex-labels.len() {
+        let label = vertex-labels.at(id)
+        if type(label) == dictionary {
+          label.at("label", default: "")
+        } else {
+          label
+        }
+      } else {
+        ""
+      }
+    },
+    xlabels: name => {
+      let id = int(name)
+      if id < vertex-labels.len() {
+        let label = vertex-labels.at(id)
+        if type(label) == dictionary {
+          label.at("xlabel", default: none)
+        } else {
+          none
+        }
+      } else {
+        none
+      }
+    },
+    edges: (name, edges) => {
+      let id = int(name)
+      let result = (:)
+      for edge in edges {
+        let edge-id = int(edge)
+        let label = adjacency.at(id).at(edge-id)
+        if label != none {
+          result.insert(edge, [#label])
+        }
+      }
+      result
+    },
+		clusters: (name) => {
+			let id = int(name.split("_").at(1))
+			if id < clusters.len() {
+				let cluster = clusters.at(id)
+				if type(cluster) == dictionary {
+					cluster.at("label", default: none)
+				} else {
+					none
+				}
+			} else {
+				none
+			}
+		}
+  )
+}
+
diff --git a/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm b/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm
new file mode 100755
index 0000000000..c76b2caf35
Binary files /dev/null and b/packages/preview/diagraph/0.3.1/graphviz_interface/diagraph.wasm differ
diff --git a/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ b/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ
new file mode 100644
index 0000000000..16d512bef0
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/graphviz_interface/protocol.typ
@@ -0,0 +1,390 @@
+/// 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.slice(0,4)) {
+    result = result * 256 + byte
+  }
+  (result, 4)
+}
+
+/// Encodes a string into bytes.
+#let encode-string(value) = {
+	bytes(value) + bytes((0x00,))
+}
+
+/// Decodes a string from the given bytes.
+#let decode-string(bytes) = {
+	let length = 0
+	for byte in array(bytes) {
+		length = length + 1
+		if byte == 0x00 {
+			break
+		}
+	}
+	if length == 0 {
+		("", 1)
+	} else { 
+		(str(bytes.slice(0, length - 1)), length)
+	}
+	//(array(bytes.slice(0, length - 1)), length)
+}
+
+/// Encodes a boolean into bytes
+#let encode-bool(value) = {
+  if value {
+	bytes((0x01,))
+  } else {
+	bytes((0x00,))
+  }
+}
+
+/// Decodes a boolean from the given bytes
+#let decode-bool(bytes) = {
+  if bytes.at(0) == 0x00 {
+	(false, 1)
+  } else {
+	(true, 1)
+  }
+}
+
+/// Encodes a character into bytes
+#let encode-char(value) = {
+  bytes(value)
+}
+
+/// Decodes a character from the given bytes
+#let decode-char(bytes) = {
+  (bytes.at(0), 1)
+}
+
+#let fractional-to-binary(fractional_part, max_dec, zero) = {
+	let result = 0
+	let i = 22 - max_dec
+	let first_one = 0
+	if zero {
+		while fractional_part < 1 {
+			fractional_part *= 2
+			first_one += 1
+		}
+		fractional_part -= 1
+		i = 23
+	}
+	while i > 0 and fractional_part > 0 {
+		fractional_part *= 2
+		if fractional_part >= 1 {
+			result += calc.pow(2, i - 1)
+			fractional_part -= 1
+		}
+		i -= 1
+	}
+	(result, first_one)
+}
+
+#let float-to-int(value) = {
+	if value == 0 {
+		return 0
+	}
+	let sign = if value < 0.0 { 1 } else { 0 }
+	let value = calc.abs(value)
+	let mantissa = calc.trunc(value)
+	let fractional_part = calc.fract(value)
+	let exponent = if mantissa == 0 {
+		0
+	} else {
+		calc.floor(calc.log(base: 2, mantissa)) - 1
+	}
+	let (fractional_part, first_one) = fractional-to-binary(fractional_part, exponent, mantissa == 0)
+	mantissa *= calc.pow(2, 22 - exponent)
+	mantissa += fractional_part
+	if exponent == 0 {
+		exponent = -first_one
+	}
+	exponent += 127
+	return  sign * calc.pow(2, 31) + exponent * calc.pow(2, 23) + mantissa
+}
+
+#let mantissa-to-float(mantissa) = {
+	let result = 1.0
+	for i in range(0,23) {
+		if calc.rem(mantissa, 2) == 1 {
+			result += 1.0/calc.pow(2, 23 - i)
+		}
+		mantissa = calc.quo(mantissa, 2)
+	}
+	result
+}
+
+#let int-to-float(value) = {
+	if value == 0 {
+		return 0.0
+	}
+	let sign = if value >= calc.pow(2, 31) {
+		value -= calc.pow(2, 31)
+		 -1 
+	} else { 
+		1
+	}
+	let exponent = calc.rem(calc.quo(value, calc.pow(2, 23)), calc.pow(2, 8))
+	let mantissa = calc.rem(value, calc.pow(2, 23))
+	sign * calc.pow(2, exponent - 127) * mantissa-to-float(mantissa)
+}
+
+/// Encodes a float into bytes
+#let encode-float(value) = {
+	encode-int(float-to-int(value))
+}
+
+#let encode-point(value) = {
+	encode-float(value.pt())
+}
+
+/// Decodes a float from the given bytes
+#let decode-float(bytes) = {
+	let (decoded, size) = decode-int(bytes)
+	(int-to-float(decoded), size)
+}
+
+#let decode-point(bytes) = {
+	let (value, size) = decode-float(bytes)
+	(value * 1pt, size)
+}
+
+/// Encodes a list of elements into bytes
+#let encode-list(arr, encoder) = {
+	let length = encode-int(arr.len())
+	let encoded = bytes(arr.map(encoder).map(array).flatten())
+	length + encoded
+}
+
+/// Decodes a list of elements from the given bytes
+#let decode-list(bytes, decoder) = {
+	let (length, length_size) = decode-int(bytes)
+	let result = ()
+	let offset = length_size
+	for i in range(0, length) {
+		let (element, size) = decoder(bytes.slice(offset, bytes.len()))
+		result.push(element)
+		offset += size
+	}
+	(result, offset)
+}
+#let decode-EdgeLabelInfo(bytes) = {
+  let offset = 0
+  let (f_to, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_index, size) = decode-int(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_label, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_label_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xlabel, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_headlabel, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_headlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_taillabel, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_taillabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_color, size) = decode-int(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  ((
+    to: f_to,
+    index: f_index,
+    label: f_label,
+    label_math_mode: f_label_math_mode,
+    xlabel: f_xlabel,
+    xlabel_math_mode: f_xlabel_math_mode,
+    headlabel: f_headlabel,
+    headlabel_math_mode: f_headlabel_math_mode,
+    taillabel: f_taillabel,
+    taillabel_math_mode: f_taillabel_math_mode,
+    color: f_color,
+    font_name: f_font_name,
+    font_size: f_font_size,
+  ), offset)
+}
+#let decode-NodeLabelInfo(bytes) = {
+  let offset = 0
+  let (f_name, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_label, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xlabel, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xlabel_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_color, size) = decode-int(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_edges_infos, size) = decode-list(bytes.slice(offset, bytes.len()), decode-EdgeLabelInfo)
+  offset += size
+  ((
+    name: f_name,
+    label: f_label,
+    math_mode: f_math_mode,
+    xlabel: f_xlabel,
+    xlabel_math_mode: f_xlabel_math_mode,
+    color: f_color,
+    font_name: f_font_name,
+    font_size: f_font_size,
+    edges_infos: f_edges_infos,
+  ), offset)
+}
+#let decode-ClusterLabelInfo(bytes) = {
+  let offset = 0
+  let (f_name, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_label, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_math_mode, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_color, size) = decode-int(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_name, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_font_size, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  ((
+    name: f_name,
+    label: f_label,
+    math_mode: f_math_mode,
+    color: f_color,
+    font_name: f_font_name,
+    font_size: f_font_size,
+  ), offset)
+}
+#let encode-SizedEdgeLabel(value) = {
+  encode-bool(value.at("overwrite")) + encode-point(value.at("width")) + encode-point(value.at("height")) + encode-bool(value.at("xoverwrite")) + encode-point(value.at("xwidth")) + encode-point(value.at("xheight")) + encode-bool(value.at("headoverwrite")) + encode-point(value.at("headwidth")) + encode-point(value.at("headheight")) + encode-bool(value.at("tailoverwrite")) + encode-point(value.at("tailwidth")) + encode-point(value.at("tailheight"))
+}
+#let encode-SizedNodeLabel(value) = {
+  encode-bool(value.at("overwrite")) + encode-bool(value.at("xoverwrite")) + encode-point(value.at("width")) + encode-point(value.at("height")) + encode-point(value.at("xwidth")) + encode-point(value.at("xheight")) + encode-list(value.at("edges_size"), encode-SizedEdgeLabel)
+}
+#let encode-SizedClusterLabel(value) = {
+  encode-point(value.at("width")) + encode-point(value.at("height"))
+}
+#let decode-EdgeCoordinates(bytes) = {
+  let offset = 0
+  let (f_x, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_y, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xx, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xy, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_headx, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_heady, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_tailx, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_taily, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  ((
+    x: f_x,
+    y: f_y,
+    xx: f_xx,
+    xy: f_xy,
+    headx: f_headx,
+    heady: f_heady,
+    tailx: f_tailx,
+    taily: f_taily,
+  ), offset)
+}
+#let decode-NodeCoordinates(bytes) = {
+  let offset = 0
+  let (f_x, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_y, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xx, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_xy, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_edges, size) = decode-list(bytes.slice(offset, bytes.len()), decode-EdgeCoordinates)
+  offset += size
+  ((
+    x: f_x,
+    y: f_y,
+    xx: f_xx,
+    xy: f_xy,
+    edges: f_edges,
+  ), offset)
+}
+#let decode-ClusterCoordinates(bytes) = {
+  let offset = 0
+  let (f_x, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_y, size) = decode-point(bytes.slice(offset, bytes.len()))
+  offset += size
+  ((
+    x: f_x,
+    y: f_y,
+  ), offset)
+}
+#let encode-GetGraphInfo(value) = {
+  encode-string(value.at("dot"))
+}
+#let encode-renderGraph(value) = {
+  encode-point(value.at("font_size")) + encode-string(value.at("dot")) + encode-list(value.at("labels"), encode-SizedNodeLabel) + encode-list(value.at("cluster_labels"), encode-SizedClusterLabel) + encode-string(value.at("engine"))
+}
+#let decode-Engines(bytes) = {
+  let offset = 0
+  let (f_engines, size) = decode-list(bytes.slice(offset, bytes.len()), decode-string)
+  offset += size
+  ((
+    engines: f_engines,
+  ), offset)
+}
+#let decode-graphInfo(bytes) = {
+  let offset = 0
+  let (f_error, size) = decode-bool(bytes.slice(offset, bytes.len()))
+  offset += size
+  let (f_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-NodeCoordinates)
+  offset += size
+  let (f_cluster_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-ClusterCoordinates)
+  offset += size
+  let (f_svg, size) = decode-string(bytes.slice(offset, bytes.len()))
+  offset += size
+  ((
+    error: f_error,
+    labels: f_labels,
+    cluster_labels: f_cluster_labels,
+    svg: f_svg,
+  ), offset)
+}
+#let decode-GraphInfo(bytes) = {
+  let offset = 0
+  let (f_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-NodeLabelInfo)
+  offset += size
+  let (f_cluster_labels, size) = decode-list(bytes.slice(offset, bytes.len()), decode-ClusterLabelInfo)
+  offset += size
+  ((
+    labels: f_labels,
+    cluster_labels: f_cluster_labels,
+  ), offset)
+}
diff --git a/packages/preview/diagraph/0.3.1/internals.typ b/packages/preview/diagraph/0.3.1/internals.typ
new file mode 100644
index 0000000000..18c34d302b
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/internals.typ
@@ -0,0 +1,561 @@
+#import "graphviz_interface/protocol.typ": *
+#let plugin = plugin("graphviz_interface/diagraph.wasm")
+
+
+/// Converts a string containing escape sequences to content.
+#let parse-string(s) = {
+  let result = []
+  let row = ""
+  let is-escaped = false
+
+  for cluster in s {
+    if is-escaped {
+      is-escaped = false
+
+      if cluster == "l" {
+        result += align(left, row)
+        row = ""
+      } else if cluster == "n" {
+        result += align(center, row)
+        row = ""
+      } else if cluster == "r" {
+        result += align(right, row)
+        row = ""
+      } else {
+        row += cluster
+      }
+    } else if cluster == "\\" {
+      is-escaped = true
+    } else {
+      row += cluster
+    }
+  }
+
+  set block(spacing: 0.65em)
+  result + align(center, row)
+}
+
+/// Convert a number to a string with a fixed number of digits.
+/// The number is padded with zeros on the left if necessary.
+#let int-to-string(n, digits, base: 10) = {
+  let n-str = str(n, base: base)
+  let n-len = n-str.len()
+  let zeros = "0" * (digits - n-len)
+  zeros + n-str
+}
+
+/// Return a buffer in readable format.
+#let buffer-repr(buffer) = [
+  #repr(array(buffer).map(x => "0x" + int-to-string(x, 2, base: 16)).join(", "))
+]
+
+/// COnvert a string to math or text mode
+#let convert-label(label, math-mode) = {
+  if label == "" {
+    return ""
+  }
+  if math-mode {
+    math.equation(eval(mode: "math", label))
+  } else {
+    parse-string(label)
+  }
+}
+
+/// Return a formatted label based on its color, font and content.
+#let label-format(color, font, fontsize, label) = {
+  if label == "" {
+    return ""
+  }
+  set text(fill: rgb(int-to-string(color, 8, base: 16)), bottom-edge: "bounds")
+  set text(size: fontsize) if fontsize.pt() != 0
+	set text(font: font) if font != ("",)
+	text(label)
+}
+/// Check that all edges in the overwrite dictionary are present in the encoded label edges.
+#let check-overwrite(encoded-label, edge-overwrite) = {
+  for to-node in edge-overwrite.keys() {
+    if encoded-label.at("edges_infos").find(edge => edge.at("to") == to-node) == none {
+      panic("Node \"" + encoded-label.at("name") + "\" does not have an edge to node \"" + to-node + "\"")
+    }
+  }
+}
+
+#let edge-label-format(edge-overwrite, edge-label, color, font, fontsize, name) = {
+  let overwrite-list = edge-overwrite.at(edge-label.at("to"), default: none)
+  if overwrite-list != none {
+    let index = edge-label.at("index")
+    if type(overwrite-list) == array and index < overwrite-list.len() {
+			let overwrite = overwrite-list.at(index)
+			if type(overwrite) != dictionary {
+				if name == "label" {
+					return overwrite
+				}
+			} else if name in overwrite {
+      	return overwrite.at(name)
+			}
+    } else if index == 0 {
+			if type(overwrite-list) != dictionary {
+				if name == "label" {
+					return overwrite-list
+				}
+			} else if name in overwrite-list {
+				return overwrite-list.at(name)
+			}
+    }
+  }
+  let label-content = edge-label.at(name)
+  if label-content == "" {
+    return ""
+  }
+  label-format(color, font, fontsize, convert-label(label-content, edge-label.at(name + "_math_mode")))
+}
+
+#let format-edge-labels(encoded-label, edge-overwrite) = {
+  let formatted-edge-labels = ()
+  for edge-label in encoded-label.at("edges_infos") {
+    let font-size = text.size
+    if edge-label.at("font_size").pt() != 0 {
+      font-size = edge-label.at("font_size")
+    }
+    let font-name = edge-label.at("font_name").split(",")
+    let font-color = edge-label.at("color")
+
+    // panic(edge-label, edge-overwrite)
+
+    formatted-edge-labels.push((
+      native: "label" in edge-overwrite,
+      label: edge-label-format(edge-overwrite, edge-label, edge-label.at("color"), font-name, font-size, "label"),
+      xnative: "xlabel" in edge-overwrite,
+      xlabel: edge-label-format(edge-overwrite, edge-label, edge-label.at("color"), font-name, font-size, "xlabel"),
+      tailnative: "taillabel" in edge-overwrite,
+      taillabel: edge-label-format(
+        edge-overwrite,
+        edge-label,
+        edge-label.at("color"),
+        font-name,
+        font-size,
+        "taillabel",
+      ),
+      headnative: "headlabel" in edge-overwrite,
+      headlabel: edge-label-format(
+        edge-overwrite,
+        edge-label,
+        edge-label.at("color"),
+        font-name,
+        font-size,
+        "headlabel",
+      ),
+    ))
+  }
+  formatted-edge-labels
+}
+
+/// Return 
+/// - the label content depending on the overwrite method.
+/// - a boolean indicating if the label was overwritten.
+#let label-overwrite(label-type, label, overwrite-method, font-name, font-size) = {
+	let name = label.at("name")
+	if type(overwrite-method) == dictionary and name in overwrite-method {
+		return (overwrite-method.at(name), true)
+	} 
+	
+	if type(overwrite-method) == function {
+		let overwrite = overwrite-method(name)
+		if overwrite != none {
+			return (overwrite, true)
+		}
+	}
+
+	let label-content = label.at(label-type)
+	if label-content != "" {
+		label-content = convert-label(label-content, label.at("math_mode"))
+		label-content = label-format(label.at("color"), font-name, font-size, label-content)
+		return (label-content, true)
+	}
+	
+	return ("", false)
+}
+
+/// Get an array of evaluated labels from a graph.
+#let get-labels(labels, xlabels, clusters, edges, dot) = {
+  let overridden-labels = (
+    "dot": dot,
+  )
+  // panic(buffer-repr(encode-GetGraphInfo(overridden-labels)))
+  let encoded-labels = plugin.get_labels(encode-GetGraphInfo(overridden-labels))
+  let (graph-labels, _) = decode-GraphInfo(encoded-labels)
+  // panic(graph-labels)
+  (
+    graph-labels.at("labels").map(encoded-label => {
+      let font-size = text.size
+      if encoded-label.at("font_size").pt() != 0 {
+        font-size = encoded-label.at("font_size")
+      }
+      let font-name = encoded-label.at("font_name").split(",")
+
+			let (label, overwrite) = label-overwrite("label", encoded-label, labels, font-name, font-size)
+
+			let (xlabel, xoverwrite) = label-overwrite("xlabel", encoded-label, xlabels, font-name, font-size)
+
+			let edges-overwrite = if type(edges) == function {
+				edges(encoded-label.at("name"), encoded-label.at("edges_infos").map(edge => edge.at("to")))
+			} else {
+				edges.at(encoded-label.at("name"), default: (:))
+			}
+      check-overwrite(encoded-label, edges-overwrite)
+      let edge-labels = format-edge-labels(encoded-label, edges-overwrite)
+      (
+        overwrite: overwrite,
+        label: label,
+        xoverwrite: xoverwrite,
+        xlabel: xlabel,
+        edges_infos: edge-labels,
+      )
+    }),
+    graph-labels.at("cluster_labels").map(encoded-label => {
+			let font-name = encoded-label.at("font_name").split(",")
+			let font-size = text.size
+			if encoded-label.at("font_size").pt() != 0 {
+				font-size = encoded-label.at("font_size")
+			}
+			let (label, overwrite) = label-overwrite("label", encoded-label, clusters, font-name, font-size)
+			(
+				overwrite: overwrite,
+				label: label,
+			)
+    }),
+  )
+}
+
+
+#let label-dimensions(color, font, fontsize, label) = {
+  if label == "" {
+    (
+      width: 0pt,
+      height: 0pt,
+    )
+  } else {
+    let label = label-format(color, font, fontsize, label)
+    measure(label)
+  }
+}
+
+#let measure-label(edge, name, margin: 0pt) = {
+  let dim = measure(edge.at(name))
+  (
+    width: dim.width + margin,
+    height: dim.height + margin,
+  )
+}
+
+/// Encodes the dimensions of labels into bytes.
+#let encode-label-dimensions(labels, overridden-labels, overridden-xlabels) = {
+  let edges-margin = 5pt
+  labels.map(label => {
+    let edges-size = label.at("edges_infos").map(edge => {
+      let label = measure-label(edge, "label", margin: edges-margin)
+      let xlabel = measure-label(edge, "xlabel", margin: edges-margin)
+      let taillabel = measure-label(edge, "taillabel", margin: edges-margin)
+      let headlabel = measure-label(edge, "headlabel", margin: edges-margin)
+      (
+        overwrite: edge.at("label") != "",
+        width: label.width,
+        height: label.height,
+        xoverwrite: edge.at("xlabel") != "",
+        xwidth: xlabel.width,
+        xheight: xlabel.height,
+        tailoverwrite: edge.at("taillabel") != "",
+        tailwidth: taillabel.width,
+        tailheight: taillabel.height,
+        headoverwrite: edge.at("headlabel") != "",
+        headwidth: headlabel.width,
+        headheight: headlabel.height,
+      )
+    })
+
+    let dimensions = if label.at("overwrite") {
+      measure(label.at("label"))
+    } else {
+      (width: 0pt, height: 0pt)
+    }
+    let xdimensions = if label.at("xoverwrite") {
+      measure(label.at("xlabel"))
+    } else {
+      (width: 0pt, height: 0pt)
+    }
+    (
+      overwrite: label.at("label") != "" or label.at("overwrite"),
+      xoverwrite: label.at("xlabel") != "" or label.at("xoverwrite"),
+      width: dimensions.width,
+      height: dimensions.height,
+      xwidth: xdimensions.width,
+      xheight: xdimensions.height,
+      edges_size: edges-size,
+    )
+
+  })
+}
+
+#let encode-cluster-label-dimensions(clusters-labels-infos, clusters) = {
+  clusters-labels-infos.map(label => {
+    let dim = measure(label.at("label"))
+    (
+      width: dim.width,
+      height: dim.height,
+    )
+  })
+}
+
+/// Converts any relative length to an absolute length.
+#let relative-to-absolute(value, container-dimension) = {
+  if type(value) == relative {
+    let absolute-part = relative-to-absolute(value.length, container-dimension)
+    let ratio-part = relative-to-absolute(value.ratio, container-dimension)
+    return absolute-part + ratio-part
+  }
+  if type(value) == length {
+    return value.to-absolute()
+  }
+  if type(value) == ratio {
+    return value * container-dimension
+  }
+  panic("Expected relative length, found " + str(type(value)))
+}
+
+#let debug-rectangle(x, y, width, height) = {
+  place(
+    top + left,
+    dx: x,
+    dy: y,
+    rect(height: height, width: width, fill: none, stroke: red),
+  )
+}
+
+/// 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: (:),
+	/// Nodes whose name appear in this dictionary will have their xlabel
+	/// overridden with the corresponding content. Defaults to an empty
+	/// dictionary.
+	xlabels: (:),
+	/// Nodes whose name appear in this dictionary will have their
+	/// edge label overridden with the corresponding content.
+	/// Each vale mut be a list of dictionaries, one for each edge.
+	/// Each dictionary can have the following keys:
+	/// - `label`: the content of the label
+	/// - `xlabel`: the content of the xlabel
+	/// - `taillabel`: the content of the taillabel
+	/// - `headlabel`: the content of the headlabel
+	edges: (:),
+	/// Cluster names whose name appear in this dictionary will have their
+	/// label overridden with the corresponding content. Defaults to an empty
+	/// dictionary.
+	clusters: (:),
+  /// 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,
+	/// Display a red rectangle around each label to help with debugging.
+	debug: false,
+  /// A color or gradient to fill the background with. If set to `none` (the
+  /// default), the background will be transparent.
+  background: none,
+) = {
+  set math.equation(numbering: none)
+	if type(dot) != str {
+		panic("The dot code must be a string")
+	}
+
+  layout(((width: container-width, height: container-height)) => (
+    context {
+      let (labels-infos, clusters-labels-infos) = get-labels(
+        labels,
+        xlabels,
+        clusters,
+        edges,
+        dot,
+      )
+      // return [#repr(labels-infos)]
+      // return [#repr(clusters-labels-infos)]
+      let labels-info-count = labels-infos.len()
+
+      let encoded-data = (
+        "font_size": text.size.to-absolute(),
+        "dot": dot,
+        "labels": encode-label-dimensions(labels-infos, labels, xlabels),
+        "cluster_labels": encode-cluster-label-dimensions(clusters-labels-infos, clusters),
+        "engine": engine,
+      )
+
+      // return [#repr(encoded-data)]
+      // return [#buffer-repr(encode-renderGraph(encoded-data))]
+
+      let output = plugin.render(encode-renderGraph(encoded-data))
+
+      if output.at(0) != 0 {
+        return {
+          show: highlight.with(fill: red)
+          set text(white)
+          raw(block: true, str(output))
+        }
+      }
+
+      let output = decode-graphInfo(output).at(0)
+
+      // return [#repr(output)]
+
+      // Get SVG dimensions.
+      let (width: svg-width, height: svg-height) = measure(image.decode(output.at("svg"), format: "svg"))
+
+      let final-width = if width == auto {
+        svg-width
+      } else {
+        relative-to-absolute(width, container-width)
+      }
+      let final-height = if height == auto {
+        svg-height
+      } else {
+        relative-to-absolute(height, 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,
+      )
+
+      set align(top + left)
+
+      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.at("svg"),
+        format: "svg",
+        width: svg-width,
+        height: svg-height,
+      )
+
+      let place-label(dx, dy, label) = {
+        let dimensions = measure(label)
+        place(
+          top + left,
+          dx: dx - dimensions.width / 2,
+          dy: final-height - dy - dimensions.height / 2 - (final-height - svg-height),
+          label,
+        )
+        if debug {
+          debug-rectangle(
+            dx - dimensions.width / 2,
+            final-height - dy - dimensions.height / 2 - (final-height - svg-height),
+            dimensions.width,
+            dimensions.height,
+          )
+        }
+      }
+
+      let place-edge-label(dx, dy, name, edge-info) = {
+        if edge-info.at(name) == "" {
+          return
+        }
+        place-label(
+          dx,
+          dy,
+          edge-info.at(name),
+        )
+      }
+
+      // Place labels.
+      for (label-info, label-coordinates) in labels-infos.zip(output.at("labels")) {
+        for (edge-info, edge-coordinates) in label-info.at("edges_infos").zip(label-coordinates.at("edges")) {
+          place-edge-label(
+            edge-coordinates.at("x"),
+            edge-coordinates.at("y"),
+            "label",
+            edge-info,
+          )
+          place-edge-label(
+            edge-coordinates.at("xx"),
+            edge-coordinates.at("xy"),
+            "xlabel",
+            edge-info,
+          )
+          place-edge-label(
+            edge-coordinates.at("headx"),
+            edge-coordinates.at("heady"),
+            "headlabel",
+            edge-info,
+          )
+          place-edge-label(
+            edge-coordinates.at("tailx"),
+            edge-coordinates.at("taily"),
+            "taillabel",
+            edge-info,
+          )
+        }
+
+        if label-info.at("overwrite") {
+          place-label(
+            label-coordinates.at("x"),
+            label-coordinates.at("y"),
+            label-info.at("label"),
+          )
+        }
+        if label-info.at("xoverwrite") {
+          place-label(
+            label-coordinates.at("xx"),
+            label-coordinates.at("xy"),
+            label-info.at("xlabel"),
+          )
+        }
+      }
+
+
+      for (clusters-infos, cluster-coordinates) in clusters-labels-infos.zip(output.at("cluster_labels")) {
+        if clusters-infos.at("overwrite") {
+          place-label(
+            cluster-coordinates.at("x"),
+            cluster-coordinates.at("y"),
+            clusters-infos.at("label"),
+          )
+        }
+      }
+    }
+  ))
+}
+
+#let engine-list() = {
+	let list = plugin.engine_list()
+  let (engines, _) = decode-Engines(plugin.engine_list())
+	engines
+}
\ No newline at end of file
diff --git a/packages/preview/diagraph/0.3.1/lib.typ b/packages/preview/diagraph/0.3.1/lib.typ
new file mode 100644
index 0000000000..fa69497f22
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/lib.typ
@@ -0,0 +1,15 @@
+#import "internals.typ": render, engine-list
+#import "adjacency.typ": adjacency
+
+/// 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.3.1/typst.toml b/packages/preview/diagraph/0.3.1/typst.toml
new file mode 100644
index 0000000000..19b6a12297
--- /dev/null
+++ b/packages/preview/diagraph/0.3.1/typst.toml
@@ -0,0 +1,12 @@
+[package]
+name = "diagraph"
+version = "0.3.1"
+entrypoint = "lib.typ"
+authors = ["Robotechnic <@Robotechnic>", "Malo <@MDLC01>"]
+license = "MIT"
+description = "Draw graphs with Graphviz. Use mathematical formulas as labels."
+repository = "https://github.com/Robotechnic/diagraph.git"
+keywords = ["graphviz", "graph", "diagram"]
+categories = ["components", "visualization", "integration"]
+compiler = "0.11.0"
+exclude = ["graphviz_interface/src/*", "graphviz_interface/.*", "graphviz_interface/Makefile","graphviz_interface/*.prot", "examples/*", ".gitignore", "Makefile"]