md_kroki/lib.rs
1//! [](https://crates.io/crates/md-kroki)
2//! [](https://docs.rs/md-kroki)
3//! [](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//! 
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}