rsticle_rustdoc/
lib.rs

1#![doc = env!("CARGO_PKG_DESCRIPTION")]
2#![doc = ""]
3//! This is the documentation macro. If you want to use the functionality as a library, see
4//! [`rsticle`].
5//!
6//! See the [Readme] for a general overview.
7//!
8//!   [`rsticle`]: https://docs.rs/rsticle
9//!   [Readme]: https://codeberg.org/wldmr/rsticle/src/branch/main/README.md
10use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
11use rsticle::{SLASH, convert_str};
12use std::{borrow::Cow, path::PathBuf};
13
14struct Error {
15    msg: Cow<'static, str>,
16    span: Span,
17}
18
19impl Error {
20    fn new(msg: impl Into<Cow<'static, str>>, span: Span) -> Self {
21        Self {
22            msg: msg.into(),
23            span,
24        }
25    }
26}
27
28/// Include an annotated example file within the rustdoc of your library.
29///
30/// ```rust
31/// # use rsticle_rustdoc::include_as_doc;
32/// #![doc = include_as_doc!("examples/example.rs")]
33/// ```
34#[proc_macro]
35pub fn include_as_doc(args: TokenStream) -> TokenStream {
36    macro_main(args).unwrap_or_else(compile_error)
37}
38
39fn macro_main(args: TokenStream) -> Result<TokenStream, Error> {
40    let mut input = args.into_iter();
41    let Some(literal) = input.next() else {
42        return Err(Error::new(
43            "expected filename, found empty parameter list",
44            Span::call_site(),
45        ));
46    };
47    let arg_span = literal.span();
48    let TokenTree::Literal(literal) = literal else {
49        return Err(Error::new(
50            format!("expected literal, found \"{literal}\""),
51            arg_span,
52        ));
53    };
54
55    let arg_literal = literal.to_string();
56    let Some(path) = arg_literal
57        .strip_prefix('"')
58        .and_then(|it| it.strip_suffix('"'))
59    else {
60        return Err(Error::new(
61            format!("Expected path to file, got {arg_literal}"),
62            arg_span,
63        ));
64    };
65
66    let path = PathBuf::from(path);
67    if path.extension().is_none_or(|it| it != "rs") {
68        return Err(Error::new("Path must point to an .rs file", arg_span));
69    }
70    let path = if path.is_absolute() {
71        path
72    } else {
73        let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
74            .map(PathBuf::from)
75            .ok_or_else(|| {
76                Error::new(
77                    "the CARGO_MANIFEST_DIR environment variable is not set",
78                    Span::call_site(),
79                )
80            })?;
81        manifest_dir.join(path)
82    };
83
84    let source = std::fs::read_to_string(&path)
85        .map_err(|e| Error::new(format!("Could not read file {path:?}: {e}"), arg_span))?;
86
87    let markdown = convert_str(&SLASH, &source)
88        .map_err(|e| Error::new(format!("Conversion failed: {e}"), arg_span))?;
89
90    let markdown = proc_macro::Literal::string(&markdown);
91    Ok(TokenTree::Literal(markdown).into())
92}
93
94fn compile_error(error: Error) -> TokenStream {
95    let compile_error: TokenTree = Ident::new("compile_error", error.span).into();
96    let exclaim: TokenTree = Punct::new('!', Spacing::Joint).into();
97    let msg: TokenTree = Literal::string(&error.msg).into();
98
99    let parens: TokenTree =
100        Group::new(Delimiter::Parenthesis, TokenStream::from_iter([msg])).into();
101
102    TokenStream::from_iter([compile_error, exclaim, parens])
103}