md_kroki/
lib.rs

1//! [![Crates.io](https://img.shields.io/crates/v/md-kroki.svg)](https://crates.io/crates/md-kroki)
2//! [![Docs.rs](https://docs.rs/md-kroki/badge.svg)](https://docs.rs/md-kroki)
3//! [![CI](https://github.com/JoelCourtney/md-kroki/workflows/CI/badge.svg)](https://github.com/JoelCourtney/md-kroki/actions)
4//!
5//! This crate provides a tool for rendering [Kroki](https://kroki.io) diagrams inside markdown strings.
6//! The input diagram code can either be inlined in the markdown or referenced via and external file, but
7//! for now the output is always inlined back into the markdown.
8//!
9//! # Usage
10//!
11//! ## Creating a renderer
12//!
13//! You can create a default renderer easily:
14//!
15//! ```rust
16//! # use md_kroki::MdKroki;
17//! # tokio_test::block_on(async {
18//! # let my_markdown_string: String = String::new();
19//! // This default renderer uses the kroki.io API and only allows inlined diagrams.
20//! let renderer = MdKroki::default();
21//!
22//! renderer.render(my_markdown_string).await
23//! # });
24//! ```
25//!
26//! The renderer also provides a synchronous `render_sync` method for sync contexts.
27//!
28//! You can configure the endpoint and enable external file references with the builder:
29//!
30//! ```rust
31//! # use md_kroki::MdKroki;
32//! # tokio_test::block_on(async {
33//! # let my_markdown_string: String = String::new();
34//! let renderer = MdKroki::builder()
35//!
36//!    // Use your own deployment of Kroki.
37//!    .endpoint("http://localhost/")
38//!
39//!    // Resolve file references and read their contents.
40//!    // See builder docs for more details.
41//!    .path_resolver(|path| Ok(std::fs::read_to_string(path)?))
42//!
43//!    .build();
44//!
45//! renderer.render(my_markdown_string).await
46//! # });
47//! ```
48//!
49//! ## Inlining diagrams
50//!
51//! You can write the diagram code directly in the markdown using the custom `<kroki>` tag:
52//!
53//! ```md
54//! <kroki type="erd">
55//!   [Person]
56//!   *name
57//!   height
58//!   weight
59//!   +birth_location_id
60//!
61//!   [Location]
62//!   *id
63//!   city
64//!   state
65//!   country
66//!
67//!   Person *--1 Location
68//! </kroki>
69//! ```
70//!
71//! The `type` attribute tells kroki what renderer to use and is required.
72//!
73//! If you want to use traditional markdown elements, you can inline the diagram source with a fenced code block.
74//!
75//! ``````markdown
76//! ```kroki-mermaid
77//! graph TD
78//!   A[ Anyone ] -->|Can help | B( Go to github.com/yuzutech/kroki )
79//!   B --> C{ How to contribute? }
80//!   C --> D[ Reporting bugs ]
81//!   C --> E[ Sharing ideas ]
82//!   C --> F[ Advocating ]
83//! ```
84//! ``````
85//!
86//! Here the code block language takes the place of the `type` attribute: it must be of the form `kroki-<diagram type>`.
87//! Otherwise it will be treated like a normal code block.
88//!
89//! ## Referencing external files
90//!
91//! If the input code of a diagram is too big to inline nicely in your markdown, you can reference an external file:
92//!
93//! ```md
94//! Using the kroki tag:
95//! <kroki type="excalidraw" path="my/file.excalidraw" />
96//!
97//! Or using a traditional markdown image tag:
98//! ![my excalidrawing](kroki-excalidraw:my/file.excalidraw)
99//! ```
100//!
101//! When using the markdown tag, the path must be prefixed with `kroki-<diagram type>:`. Otherwise it will be treated
102//! like a normal image tag.
103//!
104//! You must provide a path resolver to the builder if you want to use file references.
105
106#![deny(missing_docs)]
107
108mod render;
109#[cfg(test)]
110mod test;
111
112use anyhow::Result;
113use std::path::PathBuf;
114
115/// Kroki diagram renderer.
116pub struct MdKroki {
117    endpoint: String,
118    path_resolver: PathResolver,
119}
120
121impl MdKroki {
122    /// Create a builder.
123    pub fn builder() -> MdKrokiBuilder {
124        MdKrokiBuilder {
125            md_kroki: MdKroki::default(),
126        }
127    }
128}
129
130/// Options for resolving paths in tags that reference external files.
131///
132/// It will cause an error if you use a path without providing an appropriate resolver.
133#[allow(clippy::type_complexity)]
134#[derive(Default)]
135enum PathResolver {
136    #[default]
137    None,
138    Path(Box<dyn Fn(PathBuf) -> Result<String> + Send>),
139    PathAndRoot(Box<dyn Fn(PathBuf, Option<&str>) -> Result<String> + Send>),
140}
141
142/// Builder for configuring the renderer.
143pub struct MdKrokiBuilder {
144    md_kroki: MdKroki,
145}
146
147impl MdKrokiBuilder {
148    /// Sets the endpoint url. Use if you'd like to target your own deployment of Kroki.
149    ///
150    /// Default is <https://kroki.io>.
151    pub fn endpoint(mut self, endpoint: impl std::fmt::Display) -> Self {
152        self.md_kroki.endpoint = endpoint.to_string();
153        self
154    }
155
156    /// Sets a basic path resolver. Unnecessary if all your diagrams are inline. Example:
157    ///
158    /// ```
159    /// # use std::path::Path;
160    /// # use md_kroki::MdKroki;
161    /// let resolver = |path| {
162    ///     let base_path = Path::new("path/to/files");
163    ///     Ok(std::fs::read_to_string(base_path.join(path))?)
164    /// };
165    /// let md_kroki = MdKroki::builder()
166    ///     .path_resolver(resolver)
167    ///     .build();
168    /// ```
169    pub fn path_resolver<F>(mut self, path_resolver: F) -> Self
170    where
171        F: Fn(PathBuf) -> Result<String> + Send + 'static,
172    {
173        self.md_kroki.path_resolver = PathResolver::Path(Box::new(path_resolver));
174        self
175    }
176
177    /// Path resolver with optional root parameter.
178    ///
179    /// If none of your diagrams use a root attribute, just use [path_resolver][Self::path_resolver].
180    /// There is no need to provide both [path_resolver][Self::path_resolver] and [path_and_root_resolver][Self::path_and_root_resolver].
181    ///
182    /// This option is only available on external file references on the
183    /// `<kroki>` tag. Using the `root` attribute will send that value to the resolver:
184    ///
185    /// ```xml
186    /// <kroki type="mermaid" path="file.mermaid" root="assets" />
187    /// ```
188    ///
189    /// In most cases this option will be unnecessary. Example:
190    ///
191    /// ```
192    /// # use std::path::Path;
193    /// # use md_kroki::MdKroki;
194    /// # use anyhow::bail;
195    /// let resolver = |path, root: Option<&str>| {
196    ///     let base_path = match root {
197    ///         None => Path::new(""),
198    ///         Some("assets") => Path::new("static/assets"),
199    ///         Some(r) => bail!("unrecognized root: {r}")
200    ///     };
201    ///     Ok(std::fs::read_to_string(base_path.join(path))?)
202    /// };
203    /// let md_kroki = MdKroki::builder()
204    ///     .path_and_root_resolver(resolver)
205    ///     .build();
206    /// ```
207    ///
208    /// Due to limitations in Rust's type inference, you need to specify `Option<&str>` as the
209    /// type of the `root` argument. It can't be inferred.
210    pub fn path_and_root_resolver<F>(mut self, path_resolver: F) -> Self
211    where
212        F: Fn(PathBuf, Option<&str>) -> Result<String> + Send + 'static,
213    {
214        let wrapped = move |path, root: Option<&str>| path_resolver(path, root);
215        self.md_kroki.path_resolver = PathResolver::PathAndRoot(Box::new(wrapped));
216        self
217    }
218
219    /// Consume self and build a renderer.
220    pub fn build(self) -> MdKroki {
221        self.md_kroki
222    }
223}
224
225impl Default for MdKroki {
226    fn default() -> Self {
227        Self {
228            endpoint: "https://kroki.io".to_string(),
229            path_resolver: PathResolver::None,
230        }
231    }
232}