├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------