├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── embed-doc-image-showcase ├── Cargo.toml ├── README.md ├── images │ ├── corro.svg │ ├── dancing-ferris-tiny.gif │ ├── rustacean-flat-gesture-tiny.png │ └── rustacean-orig-noshadow-tiny.png └── src │ └── lib.rs └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## [0.1.4] - 2021-05-26 7 | ### Fixed 8 | - Wrong URLs in Cargo.toml. 9 | 10 | ## [0.1.3] - 2021-05-26 11 | ### Fixed 12 | - Improve docs. 13 | 14 | ## [0.1.2] - 2021-05-26 15 | ### Fixed 16 | - Improve docs. 17 | 18 | ## [0.1.1] - 2021-05-26 19 | ### Fixed 20 | - Fix wrong slugs in Cargo.toml. 21 | 22 | ## [0.1] - 2021-05-26 23 | 24 | Initial release. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embed-doc-image" 3 | version = "0.1.4" 4 | authors = ["Andreas Longva"] 5 | edition = "2018" 6 | description = "Embed images in Rust documentation" 7 | license = "MIT" 8 | documentation = "https://docs.rs/embed-doc-image" 9 | homepage = "https://github.com/Andlon/embed-doc-image" 10 | repository = "https://github.com/Andlon/embed-doc-image" 11 | categories = ["development-tools", "rust-patterns"] 12 | keywords = ["documentation", "rustdoc"] 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = { version="1.0.72", default-features=false, features = ["full", "parsing", "proc-macro", "printing"] } 19 | quote = "1.0.9" 20 | proc-macro2 = "1.0.27" 21 | base64 = "0.13" 22 | 23 | [workspace] 24 | members = [ "embed-doc-image-showcase" ] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andreas Borgen Longva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # embed-doc-image 2 | 3 | `embed-doc-image` is proc macro crate that facilitates the embedding of images in Rust documentation. 4 | 5 | Please see the [documentation](https://docs.rs/embed-doc-image) for motivation, usage instructions and more. 6 | 7 | ## Contributing 8 | I'm happy to accept contributions in the form of pull requests, bug reports or feature requests. 9 | 10 | For major contributions I'd prefer that you first open an issue so that we can 11 | discuss whether the proposed changes are appropriate. 12 | 13 | ## Acknowledgements 14 | 15 | As an inexperienced proc macro hacker, I would not have managed to arrive at this 16 | solution without the help of several individuals on the Rust Programming Language Community 17 | Discord server, most notably: 18 | 19 | - Yandros [(github.com/danielhenrymantilla)](https://github.com/danielhenrymantilla) 20 | - Nemo157 [(github.com/Nemo157)](https://github.com/Nemo157) 21 | 22 | ## License 23 | 24 | This crate is licensed under the MIT license. See `LICENSE` for details. -------------------------------------------------------------------------------- /embed-doc-image-showcase/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "embed-doc-image-showcase" 3 | version = "0.1.2" 4 | authors = ["Andreas Longva"] 5 | edition = "2018" 6 | readme = "README.md" 7 | license = "MIT" 8 | description = "Showcase for the embed-doc-images crate" 9 | documentation = "https://docs.rs/embed-doc-image-showcase" 10 | 11 | [package.metadata.docs.rs] 12 | # docs.rs uses a nightly compiler, so by instructing it to use our `doc-images` feature we ensure that it will render 13 | # any images that we may have in our crate-level documentation. 14 | features = ["doc-images"] 15 | 16 | [features] 17 | # This is a necessary workaround so that we can embed images in crate-level documentation for Rust >= 1.54, 18 | # while at the same time have the code working (without images in crate-level documentation) for older compilers 19 | doc-images = [] 20 | 21 | [dependencies] 22 | embed-doc-image = { version = "0.1", path = ".." } -------------------------------------------------------------------------------- /embed-doc-image-showcase/README.md: -------------------------------------------------------------------------------- 1 | # embed-doc-image-showcase 2 | 3 | This crate is a showcase for the [embed-doc-image][embed-doc-image-crates.io] crate. 4 | 5 | Please see the [documentation on docs.rs][showcase-docs] to see it in action. 6 | 7 | [embed-doc-image-crates.io]: https://crates.io/crates/embed-doc-image 8 | [showcase-docs]: https://docs.rs/embed-doc-image-showcase -------------------------------------------------------------------------------- /embed-doc-image-showcase/images/corro.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 49 | 52 | 55 | 60 | 65 | 70 | 75 | 76 | 79 | 83 | 88 | 89 | 92 | 96 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /embed-doc-image-showcase/images/dancing-ferris-tiny.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andlon/embed-doc-image/9d640640b374c90ac18091ec6ac291b4ef8d3c66/embed-doc-image-showcase/images/dancing-ferris-tiny.gif -------------------------------------------------------------------------------- /embed-doc-image-showcase/images/rustacean-flat-gesture-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andlon/embed-doc-image/9d640640b374c90ac18091ec6ac291b4ef8d3c66/embed-doc-image-showcase/images/rustacean-flat-gesture-tiny.png -------------------------------------------------------------------------------- /embed-doc-image-showcase/images/rustacean-orig-noshadow-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andlon/embed-doc-image/9d640640b374c90ac18091ec6ac291b4ef8d3c66/embed-doc-image-showcase/images/rustacean-orig-noshadow-tiny.png -------------------------------------------------------------------------------- /embed-doc-image-showcase/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Showcase for `embed-doc-image`. 2 | //! 3 | // Important: note the blank line of documentation on each side of the image lookup table. 4 | // The "image lookup table" can be placed anywhere, but we place it here together with the 5 | // warning if the `doc-images` feature is not enabled. 6 | #![cfg_attr(feature = "doc-images", 7 | cfg_attr(all(), 8 | doc = ::embed_doc_image::embed_image!("ferris", "images/rustacean-orig-noshadow-tiny.png"), 9 | doc = ::embed_doc_image::embed_image!("ferris-gesture", "images/rustacean-flat-gesture-tiny.png"), 10 | doc = ::embed_doc_image::embed_image!("dancing-ferris", "images/dancing-ferris-tiny.gif"), 11 | doc = ::embed_doc_image::embed_image!("corro", "images/corro.svg")))] 12 | #![cfg_attr( 13 | not(feature = "doc-images"), 14 | doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \ 15 | to enable." 16 | )] 17 | //! 18 | //! This crate contains no functionality, it is merely a demonstration of how to use 19 | //! [embed-doc-image](https://crates.io/crates/embed-doc-image) to embed images local to the 20 | //! repository that work across both [docs.rs](https://docs.rs) and 21 | //! local documentation. The motivation for this crate is 22 | //! [rustdoc's inability to include local images](https://github.com/rust-lang/rust/issues/32104) 23 | //! in a way that consistently works across local copies of the repository and `docs.rs`. 24 | //! 25 | //! See [the documentation](https://docs.rs/embed-doc-image) for more information. 26 | //! In addition, you are encouraged to browse the source code for this showcase crate to see a 27 | //! fleshed out example of how the solution works. 28 | //! 29 | //! In addition to serving as a showcase, this crate is used to verify that the solution indeed 30 | //! works across both local installations and `docs.rs`. 31 | //! This is necessary because a proc macro crate cannot use its own macros in its own documentation. 32 | //! 33 | //! `embed-doc-image` should work across the usual web-supported file types 34 | //! (jpg, png, svg, gif, bmp). If you find that it does not work with your files, please 35 | //! file an issue. 36 | //! 37 | //! The below Ferris images are courtesy of [rustacean.net](https://rustacean.net). 38 | //! 39 | //! ![Original Ferris][ferris] 40 | //! 41 | //! ![Ferris making gesture][ferris-gesture] 42 | //! 43 | //! ![Corro][corro] 44 | //! 45 | //! ![Dancing Ferris][dancing-ferris] 46 | //! 47 | use embed_doc_image::embed_doc_image; 48 | 49 | /// Test that images render in function docs. 50 | /// 51 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 52 | /// 53 | /// Some more docs. 54 | /// 55 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 56 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 57 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 58 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 59 | #[embed_doc_image("corro", "images/corro.svg")] 60 | pub fn function_docs_work() {} 61 | 62 | /// Test that images render in module docs. 63 | /// 64 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 65 | /// 66 | /// Some more docs. 67 | /// 68 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 69 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 70 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 71 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 72 | #[embed_doc_image("corro", "images/corro.svg")] 73 | pub mod module_docs_work {} 74 | 75 | /// Test that images render in macro docs. 76 | /// 77 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 78 | /// 79 | /// Some more docs. 80 | /// 81 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 82 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 83 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 84 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 85 | #[embed_doc_image("corro", "images/corro.svg")] 86 | #[macro_export] 87 | macro_rules! macro_docs_work { 88 | () => {}; 89 | } 90 | 91 | /// Test that images render in struct docs. 92 | /// 93 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 94 | /// 95 | /// Some more docs. 96 | /// 97 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 98 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 99 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 100 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 101 | #[embed_doc_image("corro", "images/corro.svg")] 102 | pub struct StructDocsWork {} 103 | 104 | /// Test that images render in trait docs. 105 | /// 106 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 107 | /// 108 | /// Some more docs. 109 | /// 110 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 111 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 112 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 113 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 114 | #[embed_doc_image("corro", "images/corro.svg")] 115 | pub trait TraitDocsWork {} 116 | 117 | /// Test that images render in type docs. 118 | /// 119 | /// ![Original Ferris][ferris] ![Ferris makes gesture][ferris-gesture] 120 | /// 121 | /// Some more docs. 122 | /// 123 | /// ![Corro][corro] ![Dancing Ferris][dancing-ferris] 124 | #[embed_doc_image("ferris", "images/rustacean-orig-noshadow-tiny.png")] 125 | #[embed_doc_image("ferris-gesture", "images/rustacean-flat-gesture-tiny.png")] 126 | #[embed_doc_image("dancing-ferris", "images/dancing-ferris-tiny.gif")] 127 | #[embed_doc_image("corro", "images/corro.svg")] 128 | pub type TypeAliasDocsWork = f64; 129 | -------------------------------------------------------------------------------- /src/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 | 198 | use proc_macro::TokenStream; 199 | use quote::{quote, ToTokens}; 200 | use std::fs::read; 201 | use std::path::{Path, PathBuf}; 202 | use syn::parse; 203 | use syn::parse::{Parse, ParseStream}; 204 | use 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)] 211 | struct ImageDescription { 212 | label: String, 213 | path: PathBuf, 214 | } 215 | 216 | impl Parse for ImageDescription { 217 | fn parse(input: ParseStream) -> parse::Result { 218 | let label = input.parse::()?; 219 | input.parse::()?; 220 | let path = input.parse::()?; 221 | Ok(ImageDescription { 222 | label: label.value(), 223 | path: PathBuf::from(path.value()), 224 | }) 225 | } 226 | } 227 | 228 | fn 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 | 233 | fn 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 | 255 | fn 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] 280 | pub 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] 297 | pub 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 | } 341 | --------------------------------------------------------------------------------