doc_item/
lib.rs

1//! Attributes for item-level documentation customization.
2//!
3//! This crate provides attributes for adding various features to items when they are documented by
4//! [`rustdoc`](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html). This includes defining
5//! item-info docboxes, annotating an item's minimum version, and marking an item to be displayed as
6//! semi-transparent on module lists.
7//!
8//! This allows for enhanced documentation, similar to what is done in the standard library with the
9//! [`staged_api`](https://doc.rust-lang.org/beta/unstable-book/language-features/staged-api.html)
10//! feature and what is available on nightly with the
11//! [`doc_cfg`](https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html) feature.
12//! However, this crate provides even more customization, allowing for use of custom CSS classes and
13//! text within docboxes.
14//!
15//! ## Usage
16//!
17//! ### Defining an Experimental API
18//! Marking an item as experimental (similar to what is done in the standard library through the
19//! [`#[unstable]`](https://rustc-dev-guide.rust-lang.org/stability.html#unstable) attribute) can be
20//! done as follows:
21//!
22//! ```
23//! /// This is an experimental API.
24//! ///
25//! /// The docbox will indicate the function is experimental. It will also appear semi-transparent on
26//! /// module lists.
27//! #[doc_item::docbox(content="<span class='emoji'>🔬</span> This is an experimental API.", class="unstable")]
28//! #[doc_item::short_docbox(content="Experimental", class="unstable")]
29//! #[doc_item::semi_transparent]
30//! pub fn foo() {}
31//! ```
32//!
33//! ### Creating Custom-Styled Docboxes
34//! You can create your own custom styles to customize the display of docboxes. Define your item's
35//! docbox as follows:
36//!
37//! ```
38//! /// An item with a custom docbox.
39//! ///
40//! /// The docbox will be a different color.
41//! #[doc_item::docbox(content="A custom docbox", class="custom")]
42//! #[doc_item::short_docbox(content="Custom", class="custom")]
43//! pub fn foo() {}
44//! ```
45//!
46//! Next, create a style definition in a separate HTML file.
47//! ```html
48//! <style>
49//!     .custom {
50//!         background: #c4ffd7;
51//!         border-color: #7bdba1;
52//!     }
53//! </style>
54//! ```
55//!
56//! Finally, include the HTML file's contents in your documentation:
57//!
58//! ```bash
59//! $ RUSTDOCFLAGS="--html-in-header custom.html" cargo doc --no-deps --open
60//! ```
61//!
62//! And instruct [docs.rs](https://docs.rs/) to include the HTML file's contents as well by adding to your `Cargo.toml`:
63//!
64//! ```toml
65//! [package.metadata.docs.rs]
66//! rustdoc-args = [ "--html-in-header", "custom.html" ]
67//! ```
68
69#![warn(
70    clippy::cargo,
71    clippy::nursery,
72    clippy::pedantic,
73    unused_qualifications
74)]
75#![allow(clippy::default_trait_access, clippy::missing_panics_doc)]
76
77extern crate proc_macro;
78
79use darling::FromMeta;
80use proc_macro::{token_stream, TokenStream};
81use std::str::FromStr;
82use syn::{parse_macro_input, AttributeArgs};
83
84#[derive(FromMeta)]
85struct BoxArgs {
86    #[darling(default)]
87    content: String,
88    #[darling(default)]
89    class: String,
90}
91
92#[derive(FromMeta)]
93struct SinceArgs {
94    #[darling(default)]
95    content: String,
96}
97
98fn insert_after_attributes(
99    result: &mut TokenStream,
100    value: TokenStream,
101    mut item_iter: token_stream::IntoIter,
102) {
103    while let Some(token) = item_iter.next() {
104        if token.to_string() == "#" {
105            Extend::extend::<TokenStream>(result, token.into());
106            Extend::extend::<TokenStream>(result, item_iter.next().unwrap().into());
107        } else {
108            result.extend(value);
109            Extend::extend::<TokenStream>(result, token.into());
110            Extend::extend::<TokenStream>(result, item_iter.collect());
111            return;
112        }
113    }
114    // Catch-all, just in case there are no tokens after the attributes.
115    result.extend(value);
116}
117
118fn prepend_to_doc(result: &mut TokenStream, value: &str, item_iter: &mut token_stream::IntoIter) {
119    while let Some(token) = item_iter.next() {
120        if token.to_string() == "#" {
121            let attribute = item_iter.next().unwrap().to_string();
122            if attribute.starts_with("[doc =") {
123                Extend::extend::<TokenStream>(result, token.into());
124                let mut old_doc = attribute
125                    .splitn(2, '\"')
126                    .skip(1)
127                    .collect::<String>()
128                    .trim_start()
129                    .trim_end_matches("\"]")
130                    .to_owned();
131                if !old_doc.starts_with('<') {
132                    old_doc = format!("<p>{}</p>", old_doc);
133                }
134                Extend::extend::<TokenStream>(
135                    result,
136                    TokenStream::from_str(&format!("[doc = \"{}{}\"]", value, old_doc)).unwrap(),
137                );
138                return;
139            }
140            Extend::extend::<TokenStream>(result, token.into());
141            Extend::extend::<TokenStream>(result, TokenStream::from_str(&attribute).unwrap());
142        } else {
143            // There are no more attributes, and therefore no more docs.
144            result.extend(TokenStream::from_str(&format!("#[doc = \"{}\"]", value)).unwrap());
145            Extend::extend::<TokenStream>(result, token.into());
146            return;
147        }
148    }
149}
150
151/// Adds a docbox to the item's item-info.
152///
153/// A docbox is defined to be a box below the item's definition within documentation, alerting the
154/// user to important information about the item. A common use case is to alert about an
155/// experimental item. This can be done as follows:
156///
157/// ```
158/// #[doc_item::docbox(content="This API is experimental", class="unstable")]
159/// pub fn foo() {}
160/// ```
161///
162/// # Custom Styles
163///
164/// The docbox can be styled using the `class` parameter. The class corresponds to a CSS class in
165/// the generated HTML. In the above example, `"unstable"` was used, as it is already a predefined
166/// class by rustdoc. Other predefined classes include `"portability"` and `"deprecated"`. If
167/// different style is desired, a custom class can be provided using the `--html-in-header` rustdoc
168/// flag.
169///
170/// Provide a custom class like this:
171///
172/// ```
173/// #[doc_item::docbox(content="A custom docbox", class="custom")]
174/// pub fn foo() {}
175/// ```
176///
177/// Define the custom class in a separate file, potentially named `custom.html`.
178///
179/// ```html
180/// <style>
181///     .custom {
182///         background: #f5ffd6;
183///         border-color: #b9ff00;
184///     }
185/// </style>
186/// ```
187///
188/// And finally build the documentation with the custom docbox class.
189///
190/// ```bash
191/// $ RUSTDOCFLAGS="--html-in-header custom.html" cargo doc --no-deps --open
192/// ```
193///
194/// # Multiple Docboxes
195/// Multiple docbox attributes may be used on a single item. When generating the documentation,
196/// `doc_item` will insert the docboxes in the *reverse* order that they are provided in. For
197/// example:
198///
199/// ```
200/// #[doc_item::docbox(content="This box will display second", class="unstable")]
201/// #[doc_item::docbox(content="This box will display first", class="portability")]
202/// pub fn foo() {}
203/// ```
204///
205/// will result in the `"portability"` docbox being displayed above the `"unstable"` docbox.
206#[proc_macro_attribute]
207pub fn docbox(attr: TokenStream, item: TokenStream) -> TokenStream {
208    let box_args = match BoxArgs::from_list(&parse_macro_input!(attr as AttributeArgs)) {
209        Ok(args) => args,
210        Err(err) => {
211            return err.write_errors().into();
212        }
213    };
214
215    let mut result = TokenStream::new();
216
217    // Insert the box after all other attributes.
218    insert_after_attributes(
219        &mut result,
220        TokenStream::from_str(&format!(
221            "#[doc = \"\n <div class='item-info'><div class='stab {}'>{}</div></div><script>var box = document.currentScript.previousElementSibling;if(box.parentElement.classList.contains('docblock-short')){{box.remove();}}else if(box.parentElement.parentElement.classList.contains('top-doc')){{box.parentElement.parentElement.before(box);}}else{{box.parentElement.before(box);}}document.currentScript.remove();</script>\"]",
222            box_args.class,
223            box_args.content
224        ))
225        .unwrap(),
226        item.into_iter()
227    );
228
229    result
230}
231
232/// Adds a short docbox to the item in module lists.
233///
234/// A short docbox is defined to be a box immediately before the item's short documentation in
235/// module lists, alerting the user to important information about the item. A common use case is to
236/// alert about an experimental item. This can be done as follows:
237///
238/// ```
239/// #[doc_item::short_docbox(content="Experimental", class="unstable")]
240/// pub fn foo() {}
241/// ```
242///
243/// It is good practice to keep the `content` concise, as short docblocks have limited space. When
244/// used with a [`macro@docbox`] attribute, the `short_docbox`'s content should be an abbreviated form of
245/// the `docbox`'s content.
246///
247/// # Custom Styles
248///
249/// The short docbox can be styled using the `class` parameter. The class corresponds to a CSS class
250/// in the generated HTML. In the above example, `"unstable"` was used, as it is already a
251/// predefined class by rustdoc. Other predefined classes include `"portability"` and
252/// `"deprecated"`. If different style is desired, a custom class can be provided using the
253/// `--html-in-header` rustdoc flag.
254///
255/// Provide a custom class like this:
256///
257/// ```
258/// #[doc_item::short_docbox(content="Custom", class="custom")]
259/// pub fn foo() {}
260/// ```
261///
262/// Define the custom class in a separate file, potentially named `custom.html`.
263///
264/// ```html
265/// <style>
266///     .custom {
267///         background: #f5ffd6;
268///         border-color: #b9ff00;
269///     }
270/// </style>
271/// ```
272///
273/// And finally build the documentation with the custom docbox class.
274///
275/// ```bash
276/// $ RUSTDOCFLAGS="--html-in-header custom.html" cargo doc --no-deps --open
277/// ```
278///
279/// # Multiple Short Docboxes
280/// Multiple short docbox attributes may be used on a single item. When generating the
281/// documentation, `doc_item` will insert the docboxes in the *reverse* order that they are provided
282/// in. For example:
283///
284/// ```
285/// #[doc_item::short_docbox(content="Second", class="unstable")]
286/// #[doc_item::short_docbox(content="First", class="portability")]
287/// pub fn foo() {}
288/// ```
289///
290/// will result in the `"portability"` short docbox being displayed to the left of the `"unstable"`
291/// short docbox.
292#[proc_macro_attribute]
293pub fn short_docbox(attr: TokenStream, item: TokenStream) -> TokenStream {
294    let box_args = match BoxArgs::from_list(&parse_macro_input!(attr as AttributeArgs)) {
295        Ok(args) => args,
296        Err(err) => {
297            return err.write_errors().into();
298        }
299    };
300
301    let mut result = TokenStream::new();
302    let mut item_iter = item.clone().into_iter();
303
304    // Insert the short box.
305    let short_docbox = &format!(
306        "<script>document.currentScript.remove();</script><span class='stab {}'>{}</span><script>var box = document.currentScript.previousElementSibling;var classes = document.currentScript.parentElement.parentElement.getElementsByClassName('module-item');if (classes.length == 0) {{box.remove();}} else {{classes[0].append(box);}}document.currentScript.remove();</script>",
307        box_args.class, box_args.content
308    );
309    prepend_to_doc(&mut result, short_docbox, &mut item_iter);
310        
311    Extend::extend::<TokenStream>(&mut result, item_iter.collect());
312
313    result
314}
315
316/// Makes an item semi-transparent in module lists.
317///
318/// This is commonly used to denote an item that is unstable and could potentially change in the
319/// future, indicating to users that it is not very reliable.
320///
321/// To make an item semi-transparent, add this attribute before the item as follows:
322///
323/// ```
324/// #[doc_item::semi_transparent]
325/// pub fn foo() {}
326/// ```
327#[proc_macro_attribute]
328pub fn semi_transparent(_attr: TokenStream, item: TokenStream) -> TokenStream {
329    let mut result = TokenStream::new();
330    let mut item_iter = item.into_iter();
331
332    // Insert script to gray the text.
333    prepend_to_doc(
334        &mut result,
335        "<script>var module_items = document.currentScript.parentElement.parentElement.getElementsByClassName('module-item'); if(module_items.length != 0){{module_items[0].classList.add('unstable');}}document.currentScript.remove();</script>",
336        &mut item_iter
337    );
338
339    Extend::extend::<TokenStream>(&mut result, item_iter.collect());
340
341    result
342}
343
344/// Adds a minimal version to an item.
345///
346/// This is meant to indicate that an item has been available since a certain version. The value
347/// is placed to the right of the item's definition in light text.
348///
349/// The value is styled the same as the since values used in the standard library's documentation.
350///
351/// ```
352/// #[doc_item::since(content="1.2.0")]
353/// pub fn foo() {}
354/// ```
355#[proc_macro_attribute]
356pub fn since(attr: TokenStream, item: TokenStream) -> TokenStream {
357    let since_args = match SinceArgs::from_list(&parse_macro_input!(attr as AttributeArgs)) {
358        Ok(args) => args,
359        Err(err) => {
360            return err.write_errors().into();
361        }
362    };
363
364    let mut result = TokenStream::new();
365
366    insert_after_attributes(
367        &mut result,
368        TokenStream::from_str(&format!(
369            "#[doc = \" <script>document.currentScript.remove();</script><span class='since'>{}</span><script>var since=document.currentScript.previousElementSibling;if(since.parentElement.classList.contains('docblock-short')){{since.remove();}}else if(since.parentElement.parentElement.classList.contains('top-doc')){{var out_of_band = since.parentElement.parentElement.parentElement.getElementsByClassName('out-of-band')[0];out_of_band.prepend(' · ');out_of_band.prepend(since);}}else{{var rightside = since.parentElement.parentElement.getElementsByClassName('rightside')[0];rightside.prepend(' · ');rightside.prepend(since);}}document.currentScript.remove();</script>\"]",
370            since_args.content
371        ))
372        .unwrap(),
373        item.into_iter()
374    );
375
376    result
377}