viz_js/
lib.rs

1//! Rust bindings for viz-js
2
3#![warn(missing_docs)]
4
5use js_sys::Array;
6use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue, UnwrapThrowExt};
7use web_sys::SvgsvgElement;
8
9#[wasm_bindgen]
10extern "C" {
11    #[wasm_bindgen (extends = js_sys::Object, js_name = Window)]
12    #[derive(Debug, Clone, PartialEq, Eq)]
13    type WindowWithViz;
14
15    /// Graphviz instance
16    #[wasm_bindgen (extends = js_sys::Object, js_name = Object)]
17    #[derive(Debug, Clone, PartialEq, Eq)]
18    pub type VizInstance;
19
20    #[wasm_bindgen(method, js_name = instance)]
21    async fn instance_raw(viz: &Viz) -> JsValue;
22}
23
24/// Graphviz renderer options
25#[wasm_bindgen(getter_with_clone)]
26pub struct Options {
27    /// Output format. See [VizInstance::formats] for available options
28    pub format: String,
29
30    /// Graphviz engine. See [VizInstance::engines] for available options
31    pub engine: String,
32
33    /// Invert y coordinates in output
34    #[wasm_bindgen(js_name = yInvert)]
35    pub y_invert: bool,
36}
37
38impl Default for Options {
39    fn default() -> Self {
40        Self {
41            format: "dot".to_string(),
42            engine: "dot".to_string(),
43            y_invert: false,
44        }
45    }
46}
47
48#[wasm_bindgen(module = "/js/viz-standalone.js")]
49extern "C" {
50    #[wasm_bindgen(catch)]
51    async fn graphviz_dummy_hack() -> Result<JsValue, JsValue>;
52
53    #[wasm_bindgen(js_namespace = window, js_name = Viz, getter, method)]
54    fn viz(this: &WindowWithViz) -> Viz;
55
56    #[wasm_bindgen (extends = js_sys::Object, js_name = Viz)]
57    #[derive(Debug, Clone, PartialEq, Eq)]
58    type Viz;
59
60    /// Get current Graphviz version
61    #[wasm_bindgen(js_name = graphvizVersion, getter, method)]
62    pub fn graphviz_version(viz_instance: &VizInstance) -> String;
63
64    #[wasm_bindgen(js_name = engines, getter, method)]
65    fn engines_raw(viz_instance: &VizInstance) -> Array;
66
67    #[wasm_bindgen(js_name = formats, getter, method)]
68    fn formats_raw(viz_instance: &VizInstance) -> Array;
69
70    #[wasm_bindgen(js_name = renderString, method, catch)]
71    fn render_string_raw(
72        viz_instance: &VizInstance,
73        src: String,
74        options: Options,
75    ) -> Result<JsValue, JsValue>;
76
77    #[wasm_bindgen(js_name = renderSVGElement, method, catch)]
78    fn render_svg_element_raw(
79        viz_instance: &VizInstance,
80        src: String,
81        options: Options,
82    ) -> Result<JsValue, JsValue>;
83
84    /// Render to JSON. [Options::format] is always `"json"`
85    #[wasm_bindgen(js_name = renderJSON, method, catch)]
86    pub fn render_json(
87        viz_instance: &VizInstance,
88        src: String,
89        options: Options,
90    ) -> Result<JsValue, JsValue>;
91}
92
93impl Viz {
94    async fn instance(&self) -> VizInstance {
95        // HACK: I have no idea why it's needed here. This function does nothing. But when
96        // you remove this call, it stops working and `undefined`s appear everywhere. Maybe some
97        // wasm-bindgen quirk that I don't understand.
98        let _ = graphviz_dummy_hack().await;
99
100        Viz::instance_raw(self)
101            .await
102            .dyn_into::<VizInstance>()
103            .expect_throw("Could not intialize Graphviz")
104    }
105}
106
107impl VizInstance {
108    /// Create new Graphviz instance.
109    pub async fn new() -> VizInstance {
110        js_sys::global()
111            .dyn_into::<WindowWithViz>()
112            .expect_throw("Could not intialize Graphviz")
113            .viz()
114            .instance()
115            .await
116    }
117
118    /// List available layout engines
119    pub fn engines(&self) -> Vec<String> {
120        VizInstance::engines_raw(self)
121            .into_iter()
122            .map(|js| js.as_string().expect_throw("Engine name is not a string"))
123            .collect()
124    }
125
126    /// List available rendering image formats
127    pub fn formats(&self) -> Vec<String> {
128        VizInstance::formats_raw(self)
129            .into_iter()
130            .map(|js| js.as_string().expect_throw("Format is not a string"))
131            .collect()
132    }
133
134    /// Render to a string
135    pub fn render_string(&self, src: String, options: Options) -> Result<String, JsValue> {
136        VizInstance::render_string_raw(self, src, options).map(|res| {
137            res.as_string()
138                .expect_throw("Rendered object is not a string")
139        })
140    }
141
142    /// Render to SVG element. [Options::format] is always `"svg"`
143    pub fn render_svg_element(
144        &self,
145        src: String,
146        options: Options,
147    ) -> Result<SvgsvgElement, JsValue> {
148        VizInstance::render_svg_element_raw(self, src, options).map(|res| {
149            res.try_into()
150                .expect_throw("Rendered object is not an svg element")
151        })
152    }
153}