embed_doc_image/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//! # Showcase
9//!
10//! See the [showcase documentation][showcase-docs] for an example with embedded images.
11//!
12//! Please also check out the [source code][showcase-source] for [the showcase crate][showcase]
13//! for a fleshed out example.
14//!
15//! # Motivation
16//!
17//! A picture is worth a thousand words. This oft quoted adage is no less true for technical
18//! documentation. A carefully crafted diagram lets a new user immediately
19//! grasp the high-level architecture of a complex library. Illustrations of geometric conventions
20//! can vastly reduce confusion among users of scientific libraries. Despite the central role
21//! of images in technical documentation, embedding images in Rust documentation in a way that
22//! portably works correctly across local installations and [docs.rs](https://docs.rs) has been a
23//! [longstanding issue of rustdoc][rustdoc-issue].
24//!
25//! This crate represents a carefully crafted solution based on procedural macros that works
26//! around the current limitations of `rustdoc` and enables a practically workable approach to
27//! embedding images in a portable manner.
28//!
29//! # How to embed images in documentation
30//!
31//! First, you'll need to depend on this crate. In `cargo.toml`:
32//!
33//! ```toml
34//! [dependencies]
35//! // Replace x.x with the latest version
36//! embed-doc-image = "x.x"
37//! ```
38//!
39//! What the next step is depends on whether you want to embed images into *inner attribute
40//! documentation* or *outer attribute documentation*. Inner attribute documentation is usually
41//! used to document crate-level or module-level documentation, and typically starts each line with
42//! `//!`. Outer attribute docs are used for most other forms of documentation, such as function
43//! and struct documentation. Outer attribute documentation typically starts each line with `///`.
44//!
45//! In both cases all image paths are relative to the **crate root**.
46//!
47//! ## Embedding images in outer attribute documentation
48//!
49//! Outer attribute documentation is typically used for documenting functions, structs, traits,
50//! macros and so on. Let's consider documenting a function and embedding an image into its
51//! documentation:
52//!
53//! ```rust
54//! // Import the attribute macro
55//! use embed_doc_image::embed_doc_image;
56//!
57//! /// Foos the bar.
58//! ///
59//! /// Let's drop an image below this text.
60//! ///
61//! /// ![Alt text goes here][myimagelabel]
62//! ///
63//! /// And another one.
64//! ///
65//! /// ![A Foobaring][foobaring]
66//! ///
67//! /// We can include any number of images in the above fashion. The important part is that
68//! /// you match the label ("myimagelabel" or "foobaring" in this case) with the label in the
69//! /// below attribute macro.
70//! // Paths are always relative to the **crate root**
71//! #[embed_doc_image("myimagelabel", "images/foo.png")]
72//! #[embed_doc_image("foobaring", "assets/foobaring.jpg")]
73//! fn foobar() {}
74//! ```
75//!
76//! And that's it! If you run `cargo doc`, you should hopefully be able to see your images
77//! in the documentation for `foobar`, and it should also work on `docs.rs` without trouble.
78//!
79//! ## Embedding images in inner attribute documentation
80//!
81//! The ability for macros to do *anything* with *inner attributes* is very limited. In fact,
82//! before Rust 1.54 (which at the time of writing has not yet been released),
83//! it is for all intents and purposes non-existent. This also means that we can not directly
84//! use our approach to embed images in documentation for Rust < 1.54. However, we can make our
85//! code compile with Rust < 1.54 and instead inject a prominent message that some images are
86//! missing.
87//! `docs.rs`, which always uses a nightly compiler, will be able to show the images. We'll
88//! also locally be able to properly embed the images as long as we're using Rust >= 1.54
89//! (or nightly). Here's how you can embed images in crate-level or module-level documentation:
90//!
91//! ```rust
92//! //! My awesome crate for fast foobaring in latent space.
93//! //!
94//! // Important: note the blank line of documentation on each side of the image lookup table.
95//! // The "image lookup table" can be placed anywhere, but we place it here together with the
96//! // warning if the `doc-images` feature is not enabled.
97//! #![cfg_attr(feature = "doc-images",
98//! cfg_attr(all(),
99//! doc = ::embed_doc_image::embed_image!("myimagelabel", "images/foo.png"),
100//! doc = ::embed_doc_image::embed_image!("foobaring", "assets/foobaring.png")))]
101//! #![cfg_attr(
102//! not(feature = "doc-images"),
103//! doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \
104//! to enable."
105//! )]
106//! //!
107//! //! Let's use our images:
108//! //! ![Alt text goes here][myimagelabel] ![A Foobaring][foobaring]
109//! ```
110//!
111//! Sadly there is currently no way to detect Rust versions in `cfg_attr`. Therefore we must
112//! rely on a feature flag for toggling proper image embedding. We'll need the following in our
113//! `Cargo.toml`:
114//!
115//! ```toml
116//! [features]
117//! doc-images = []
118//!
119//! [package.metadata.docs.rs]
120//! # docs.rs uses a nightly compiler, so by instructing it to use our `doc-images` feature we
121//! # ensure that it will render any images that we may have in inner attribute documentation.
122//! features = ["doc-images"]
123//! ```
124//!
125//! Let's summarize:
126//!
127//! - `docs.rs` will correctly render our documentation with images.
128//! - Locally:
129//! - for Rust >= 1.54 with `--features doc-images`, the local documentation will
130//! correctly render images.
131//! - for Rust < 1.54: the local documentation will be missing some images, and will
132//! contain a warning with instructions on how to enable proper image embedding.
133//! - we can also use e.g. `cargo +nightly doc --features doc-images` to produce correct
134//! documentation with a nightly compiler.
135//!
136//!
137//! # How it works
138//!
139//! The crux of the issue is that `rustdoc` does not have a mechanism for tracking locally stored
140//! images referenced by documentation and carry them over to the final documentation. Therefore
141//! currently images on `docs.rs` can only be included if you host the image somewhere on the
142//! internet and include the image with its URL. However, this has a number of issues:
143//!
144//! - You need to host the image, which incurs considerable additional effort on the part of
145//! crate authors.
146//! - The image is only available for as long as the image is hosted.
147//! - Images in local documentation will not work without internet access.
148//! - Images are not *versioned*, unless carefully done so manually by the crate author. That is,
149//! the author must carefully provide *all* versions of the image across all versions of the
150//! crate with a consistent naming convention in order to ensure that documentation of
151//! older versions of the crate display the image consistent with that particular version.
152//!
153//! The solution employed by this crate is based on a remark made in an old
154//! [reddit comment from 2017][reddit-comment]. In short, Rustdoc allows images to be provided
155//! inline in the Markdown as `base64` encoded binary blobs in the following way:
156//!
157//! ```rust
158//! ![Alt text][myimagelabel]
159//!
160//! [myimagelabel]: data:image/png;base64,BaSe64EnCoDeDdAtA
161//! ```
162//!
163//! Basically we can use the "reference" feature of Markdown links/images to provide the URL
164//! of the image in a different location than the image itself, but instead of providing an URL
165//! we can directly provide the binary data of the image in the Markdown documentation.
166//!
167//! However, doing this manually with images would terribly clutter the documentation, which
168//! seems less than ideal. Instead, we do this programmatically. The macros available in this
169//! crate essentially follow this idea:
170//!
171//! - Take a label and image path relative to the crate root as input.
172//! - Determine the MIME type (based on extension) and `base64` encoding of the image.
173//! - Produce an appropriate doc string and inject it into the Markdown documentation for the
174//! crate/function/struct/etc.
175//!
176//! Clearly, this is still quite hacky, but it seems like a workable solution until proper support
177//! in `rustdoc` arrives, at which point we may rejoice and abandon this crate to the annals
178//! of history.
179//!
180//! # Acknowledgements
181//!
182//! As an inexperienced proc macro hacker, I would not have managed to arrive at this
183//! solution without the help of several individuals on the Rust Programming Language Community
184//! Discord server, most notably:
185//!
186//! - Yandros [(github.com/danielhenrymantilla)](https://github.com/danielhenrymantilla)
187//! - Nemo157 [(github.com/Nemo157)](https://github.com/Nemo157)
188//!
189//! [showcase]: https://crates.io/crates/embed-doc-image-showcase
190//! [showcase-docs]: https://docs.rs/embed-doc-image-showcase
191//! [showcase-source]: https://github.com/Andlon/embed-doc-image/tree/master/embed-doc-image-showcase
192//! [rustdoc-issue]: https://github.com/rust-lang/rust/issues/32104
193//! [issue-tracker]: https://github.com/Andlon/embed-doc-image/issues
194//! [reddit-comment]: https://www.reddit.com/r/rust/comments/5ljshj/diagrams_in_documentation/dbwg96q?utm_source=share&utm_medium=web2x&context=3
195//!
196//!
197
198use proc_macro::TokenStream;
199use quote::{quote, ToTokens};
200use std::fs::read;
201use std::path::{Path, PathBuf};
202use syn::parse;
203use syn::parse::{Parse, ParseStream};
204use syn::{
205 Item, ItemConst, ItemEnum, ItemExternCrate, ItemFn, ItemForeignMod, ItemImpl, ItemMacro,
206 ItemMacro2, ItemMod, ItemStatic, ItemStruct, ItemTrait, ItemTraitAlias, ItemType, ItemUnion,
207 ItemUse,
208};
209
210#[derive(Debug)]
211struct ImageDescription {
212 label: String,
213 path: PathBuf,
214}
215
216impl Parse for ImageDescription {
217 fn parse(input: ParseStream) -> parse::Result<Self> {
218 let label = input.parse::<syn::LitStr>()?;
219 input.parse::<syn::Token![,]>()?;
220 let path = input.parse::<syn::LitStr>()?;
221 Ok(ImageDescription {
222 label: label.value(),
223 path: PathBuf::from(path.value()),
224 })
225 }
226}
227
228fn encode_base64_image_from_path(path: &Path) -> String {
229 let bytes = read(path).unwrap_or_else(|_| panic!("Failed to load image at {}", path.display()));
230 base64::encode(bytes)
231}
232
233fn determine_mime_type(extension: &str) -> String {
234 let extension = extension.to_ascii_lowercase();
235
236 // TODO: Consider using the mime_guess crate? The below list does seem kinda exhaustive for
237 // doc purposes though?
238
239 // Matches taken haphazardly from
240 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
241 match extension.as_str() {
242 "jpg" | "jpeg" => "image/jpeg",
243 "png" => "image/png",
244 "bmp" => "image/bmp",
245 "svg" => "image/svg+xml",
246 "gif" => "image/gif",
247 "tif" | "tiff" => "image/tiff",
248 "webp" => "image/webp",
249 "ico" => "image/vnd.microsoft.icon",
250 _ => panic!("Unrecognized image extension, unable to infer correct MIME type"),
251 }
252 .to_string()
253}
254
255fn produce_doc_string_for_image(image_desc: &ImageDescription) -> String {
256 let root_dir = std::env::var("CARGO_MANIFEST_DIR")
257 .expect("Failed to retrieve value of CARGO_MANOFEST_DIR.");
258 let root_dir = Path::new(&root_dir);
259 let encoded = encode_base64_image_from_path(&root_dir.join(&image_desc.path));
260 let ext = image_desc.path.extension().unwrap_or_else(|| {
261 panic!(
262 "No extension for file {}. Unable to determine MIME type.",
263 image_desc.path.display()
264 )
265 });
266 let mime = determine_mime_type(&ext.to_string_lossy());
267 let doc_string = format!(
268 " [{label}]: data:{mime};base64,{encoded}",
269 label = &image_desc.label,
270 mime = mime,
271 encoded = &encoded
272 );
273 doc_string
274}
275
276/// Produces a doc string for inclusion in Markdown documentation.
277///
278/// Please see the crate-level documentation for usage instructions.
279#[proc_macro]
280pub fn embed_image(item: TokenStream) -> TokenStream {
281 let image_desc = syn::parse_macro_input!(item as ImageDescription);
282 let doc_string = produce_doc_string_for_image(&image_desc);
283
284 // Ensure that the "image table" at the end is separated from the rest of the documentation,
285 // otherwise the markdown parser will not treat them as a "lookup table" for the image data
286 let s = format!("\n \n {}", doc_string);
287 let tokens = quote! {
288 #s
289 };
290 tokens.into()
291}
292
293/// Produces a doc string for inclusion in Markdown documentation.
294///
295/// Please see the crate-level documentation for usage instructions.
296#[proc_macro_attribute]
297pub fn embed_doc_image(attr: TokenStream, item: TokenStream) -> TokenStream {
298 let image_desc = syn::parse_macro_input!(attr as ImageDescription);
299 let doc_string = produce_doc_string_for_image(&image_desc);
300
301 // Then inject a doc string that "resolves" the image reference and supplies the
302 // base64-encoded data inline
303 let mut input: syn::Item = syn::parse_macro_input!(item);
304 match input {
305 Item::Const(ItemConst { ref mut attrs, .. })
306 | Item::Enum(ItemEnum { ref mut attrs, .. })
307 | Item::ExternCrate(ItemExternCrate { ref mut attrs, .. })
308 | Item::Fn(ItemFn { ref mut attrs, .. })
309 | Item::ForeignMod(ItemForeignMod { ref mut attrs, .. })
310 | Item::Impl(ItemImpl { ref mut attrs, .. })
311 | Item::Macro(ItemMacro { ref mut attrs, .. })
312 | Item::Macro2(ItemMacro2 { ref mut attrs, .. })
313 | Item::Mod(ItemMod { ref mut attrs, .. })
314 | Item::Static(ItemStatic { ref mut attrs, .. })
315 | Item::Struct(ItemStruct { ref mut attrs, .. })
316 | Item::Trait(ItemTrait { ref mut attrs, .. })
317 | Item::TraitAlias(ItemTraitAlias { ref mut attrs, .. })
318 | Item::Type(ItemType { ref mut attrs, .. })
319 | Item::Union(ItemUnion { ref mut attrs, .. })
320 | Item::Use(ItemUse { ref mut attrs, .. }) => {
321 let str = doc_string;
322 // Insert an empty doc line to ensure that we get a blank line between the
323 // docs and the "bibliography" containing the actual image data.
324 // Otherwise the markdown parser will mess up our output.
325 attrs.push(syn::parse_quote! {
326 #[doc = ""]
327 });
328 attrs.push(syn::parse_quote! {
329 #[doc = #str]
330 });
331 input.into_token_stream()
332 }
333 _ => syn::Error::new_spanned(
334 input,
335 "Unsupported item. Cannot apply attribute to the given item.",
336 )
337 .to_compile_error(),
338 }
339 .into()
340}