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}