doc_image_embed/
lib.rs

1//! Embed images in documentation.
2//!
3//! This crate enables the portable embedding of images in
4//! `rustdoc`-generated documentation. Standard
5//! web-compatible image formats should be supported. Please [file an issue][issue-tracker]
6//! if you have problems. Read on to learn how it works.
7//!
8//! # Usage
9//! #### How to embed images in documentation
10//!
11//! First, add this crate to your `cargo.toml`:
12//!
13//! ```toml
14//! [dependencies]
15//! // Replace x.x with the latest version
16//! doc-image-embed = "x.x"
17//! ```
18//! or
19//! ```sh
20//! cargo add doc-image-embed
21//! ```
22//!
23//! Note: all image paths are relative to the **crate root**.
24//!
25//! ## Embedding images in outer attribute documentation
26//!
27//! Outer attribute documentation is typically used for documenting functions, structs, traits,
28//! macros and so on. Let's consider documenting a function and embedding an image into its
29//! documentation:
30//!
31//! ```
32//! // Import the attribute macro
33//! use doc_image_embed::embed_image;
34//!
35//! /// Foos the bar.
36//! ///
37//! /// Let's drop an image below this text.
38//! ///
39//! // You still have to use the image
40//! /// ![Alt text goes here][myimagelabel]
41//! ///
42//! /// And another one.
43//! ///
44//! /// ![A Foobaring][foobaring]
45//! ///
46//! /// We can include any number of images in the above fashion. The important part is that
47//! /// you match the label ("myimagelabel" or "foobaring" in this case) with the label in the
48//! /// below attribute macro.
49//! // Paths are always relative to the **crate root**
50//! #[cfg_attr(doc, doc = embed_image!("myimagelabel", "embed-doc-image-showcase/images/rustacean-flat-gesture-tiny.png"))]
51//! #[cfg_attr(doc, doc = embed_image!("foobaring", "embed-doc-image-showcase/images/dancing-ferris-tiny.gif"))]
52//! fn foobar() {}
53//! ```
54//!
55//! And that's it! If you run `cargo doc`, you should hopefully be able to see your images
56//! in the documentation for `foobar`, and it should also work on `docs.rs` without trouble.
57//!
58//! ## Embedding images in inner attribute documentation
59//!
60//! We'll also locally be able to properly embed the images as long as we're using Rust >= 1.54
61//! (or nightly). Here's how you can embed images in crate-level or module-level documentation:
62//!
63//! ```rust
64//! //! My awesome crate for fast foobaring in latent space.
65//! //!
66//! // Important: note the blank line of documentation on each side of the image lookup table.
67//! // The "image lookup table" can be placed anywhere, but we place it here together with the
68//! // warning if the `doc-images` feature is not enabled.
69//! #![cfg_attr(doc,
70//! doc = embed_doc_image::embed_image!("myimagelabel", "images/foo.png"),
71//! doc = embed_doc_image::embed_image!("foobaring", "assets/foobaring.png")
72//! )]
73//! //!
74//! //! Let's use our images:
75//! //! ![Alt text goes here][myimagelabel] ![A Foobaring][foobaring]
76//! ```
77//!
78//! # How it works
79//!
80//! The crux of the issue is that `rustdoc` does not have a mechanism for tracking locally stored
81//! images referenced by documentation and carry them over to the final documentation. Therefore
82//! currently images on `docs.rs` can only be included if you host the image somewhere on the
83//! internet and include the image with its URL. However, this has a number of issues:
84//!
85//! - You need to host the image, which incurs considerable additional effort on the part of
86//!   crate authors.
87//! - The image is only available for as long as the image is hosted.
88//! - Images in local documentation will not work without internet access.
89//! - Images are not *versioned*, unless carefully done so manually by the crate author. That is,
90//!   the author must carefully provide *all* versions of the image across all versions of the
91//!   crate with a consistent naming convention in order to ensure that documentation of
92//!   older versions of the crate display the image consistent with that particular version.
93//!
94//! The solution employed by this crate is based on a remark made in an old
95//! [reddit comment from 2017][reddit-comment]. In short, Rustdoc allows images to be provided
96//! inline in the Markdown as `base64` encoded binary blobs in the following way:
97//!
98//! ```txt
99//! ![Alt text][myimagelabel]
100//!
101//! [myimagelabel]: data:image/png;base64,BaSe64EnCoDeDdAtA
102//! ```
103//!
104//! Basically we can use the "reference" feature of Markdown links/images to provide the URL
105//! of the image in a different location than the image itself, but instead of providing an URL
106//! we can directly provide the binary data of the image in the Markdown documentation.
107//!
108//! However, doing this manually with images would terribly clutter the documentation, which
109//! seems less than ideal. Instead, we do this programmatically. The macros available in this
110//! crate essentially follow this idea:
111//!
112//! - Take a label and image path relative to the crate root as input.
113//! - Determine the MIME type (based on extension) and `base64` encoding of the image.
114//! - Produce an appropriate doc string and inject it into the Markdown documentation for the
115//!   crate/function/struct/etc.
116//!
117//! Clearly, this is still quite hacky, but it seems like a workable solution until proper support
118//! in `rustdoc` arrives, at which point we may rejoice and abandon this crate to the annals
119//! of history.
120//!
121//! # Motivation
122//!
123//! A picture is worth a thousand words. This oft quoted adage is no less true for technical
124//! documentation. A carefully crafted diagram lets a new user immediately
125//! grasp the high-level architecture of a complex library. Illustrations of geometric conventions
126//! can vastly reduce confusion among users of scientific libraries. Despite the central role
127//! of images in technical documentation, embedding images in Rust documentation in a way that
128//! portably works correctly across local installations and [docs.rs](https://docs.rs) has been a
129//! [longstanding issue of rustdoc][rustdoc-issue].
130//!
131//! This crate represents a carefully crafted solution based on procedural macros that works
132//! around the current limitations of `rustdoc` and enables a practically workable approach to
133//! embedding images in a portable manner.
134//!
135//! # Acknowledgements
136//!
137//! As an inexperienced proc macro hacker, I would not have managed to arrive at this
138//! solution without the help of several individuals on the Rust Programming Language Community
139//! Discord server, most notably:
140//!
141//! - Yandros [(github.com/danielhenrymantilla)](https://github.com/danielhenrymantilla)
142//! - Nemo157 [(github.com/Nemo157)](https://github.com/Nemo157)
143//!
144//! [showcase]: https://crates.io/crates/embed-doc-image-showcase
145//! [showcase-docs]: https://docs.rs/embed-doc-image-showcase
146//! [showcase-source]: https://github.com/Andlon/embed-doc-image/tree/master/embed-doc-image-showcase
147//! [rustdoc-issue]: https://github.com/rust-lang/rust/issues/32104
148//! [issue-tracker]: https://github.com/Andlon/embed-doc-image/issues
149//! [reddit-comment]: https://www.reddit.com/r/rust/comments/5ljshj/diagrams_in_documentation/dbwg96q?utm_source=share&utm_medium=web2x&context=3
150//!
151
152use base64::Engine;
153use proc_macro::TokenStream;
154use quote::quote;
155use std::fs::read;
156use std::path::{Path, PathBuf};
157use syn::parse;
158use syn::parse::{Parse, ParseStream};
159
160#[derive(Debug)]
161struct ImageDescription {
162    label: String,
163    path: PathBuf,
164}
165
166impl Parse for ImageDescription {
167    fn parse(input: ParseStream) -> parse::Result<Self> {
168        let label = input.parse::<syn::LitStr>()?;
169        input.parse::<syn::Token![,]>()?;
170        let path = input.parse::<syn::LitStr>()?;
171        Ok(ImageDescription {
172            label: label.value(),
173            path: PathBuf::from(path.value()),
174        })
175    }
176}
177
178fn encode_base64_image_from_path(path: &Path) -> String {
179    let bytes = read(path).unwrap_or_else(|_| panic!("Failed to load image at {}", path.display()));
180    base64::engine::general_purpose::STANDARD.encode(bytes)
181}
182
183fn determine_mime_type(extension: &str) -> String {
184    let extension = extension.to_ascii_lowercase();
185
186    // TODO: Consider using the mime_guess crate? The below list does seem kinda exhaustive for
187    // doc purposes though?
188
189    // Matches taken haphazardly from
190    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
191    match extension.as_str() {
192        "jpg" | "jpeg" => "image/jpeg",
193        "png" => "image/png",
194        "bmp" => "image/bmp",
195        "svg" => "image/svg+xml",
196        "gif" => "image/gif",
197        "tif" | "tiff" => "image/tiff",
198        "webp" => "image/webp",
199        "ico" => "image/vnd.microsoft.icon",
200        _ => panic!("Unrecognized image extension, unable to infer correct MIME type"),
201    }
202    .to_string()
203}
204
205fn produce_doc_string_for_image(image_desc: &ImageDescription) -> String {
206    let root_dir = std::env::var("CARGO_MANIFEST_DIR")
207        .expect("Failed to retrieve value of CARGO_MANOFEST_DIR.");
208    let root_dir = Path::new(&root_dir);
209    let encoded = encode_base64_image_from_path(&root_dir.join(&image_desc.path));
210    let ext = image_desc.path.extension().unwrap_or_else(|| {
211        panic!(
212            "No extension for file {}. Unable to determine MIME type.",
213            image_desc.path.display()
214        )
215    });
216    let mime = determine_mime_type(&ext.to_string_lossy());
217    let doc_string = format!(
218        " [{label}]: data:{mime};base64,{encoded}",
219        label = &image_desc.label,
220        mime = mime,
221        encoded = &encoded
222    );
223    doc_string
224}
225
226/// Produces a doc string for inclusion in Markdown documentation.
227///
228/// Please see the crate-level documentation for usage instructions.
229///
230/// # Examples
231/// ```no_run
232/// /// [ball](https://en.wikipedia.org/wiki/ball)
233/// /// ![Ball demo image][Ball demo image]
234/// #[cfg_attr(doc, doc = embed_doc_image::embed_image!("Ball demo image", "docs/ball.png"))]
235/// #[derive(Debug, Clone)]
236/// pub struct Ball {
237///     pub radius: f64,
238/// }
239/// ```
240#[proc_macro]
241pub fn embed_image(item: TokenStream) -> TokenStream {
242    let image_desc = syn::parse_macro_input!(item as ImageDescription);
243    let doc_string = produce_doc_string_for_image(&image_desc);
244
245    // Ensure that the "image table" at the end is separated from the rest of the documentation,
246    // otherwise the markdown parser will not treat them as a "lookup table" for the image data
247    let s = format!("\n \n {doc_string}");
248    let tokens = quote! {
249        #s
250    };
251    tokens.into()
252}