rsticle-rustdoc 0.1.1

Proc macro to convert specially marked up source files into rust documentation
Documentation
#![doc = env!("CARGO_PKG_DESCRIPTION")]
#![doc = ""]
//! This is the documentation macro. If you want to use the functionality as a library, see
//! [`rsticle`].
//!
//! See the [Readme] for a general overview.
//!
//!   [`rsticle`]: https://docs.rs/rsticle
//!   [Readme]: https://codeberg.org/wldmr/rsticle/src/branch/main/README.md
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use rsticle::{SLASH, convert_str};
use std::{borrow::Cow, path::PathBuf};

struct Error {
    msg: Cow<'static, str>,
    span: Span,
}

impl Error {
    fn new(msg: impl Into<Cow<'static, str>>, span: Span) -> Self {
        Self {
            msg: msg.into(),
            span,
        }
    }
}

/// Include an annotated example file within the rustdoc of your library.
///
/// ```rust
/// # use rsticle_rustdoc::include_as_doc;
/// #![doc = include_as_doc!("examples/example.rs")]
/// ```
#[proc_macro]
pub fn include_as_doc(args: TokenStream) -> TokenStream {
    macro_main(args).unwrap_or_else(compile_error)
}

fn macro_main(args: TokenStream) -> Result<TokenStream, Error> {
    let mut input = args.into_iter();
    let Some(literal) = input.next() else {
        return Err(Error::new(
            "expected filename, found empty parameter list",
            Span::call_site(),
        ));
    };
    let arg_span = literal.span();
    let TokenTree::Literal(literal) = literal else {
        return Err(Error::new(
            format!("expected literal, found \"{literal}\""),
            arg_span,
        ));
    };

    let arg_literal = literal.to_string();
    let Some(path) = arg_literal
        .strip_prefix('"')
        .and_then(|it| it.strip_suffix('"'))
    else {
        return Err(Error::new(
            format!("Expected path to file, got {arg_literal}"),
            arg_span,
        ));
    };

    let path = PathBuf::from(path);
    if path.extension().is_none_or(|it| it != "rs") {
        return Err(Error::new("Path must point to an .rs file", arg_span));
    }
    let path = if path.is_absolute() {
        path
    } else {
        let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
            .map(PathBuf::from)
            .ok_or_else(|| {
                Error::new(
                    "the CARGO_MANIFEST_DIR environment variable is not set",
                    Span::call_site(),
                )
            })?;
        manifest_dir.join(path)
    };

    let source = std::fs::read_to_string(&path)
        .map_err(|e| Error::new(format!("Could not read file {path:?}: {e}"), arg_span))?;

    let markdown = convert_str(&SLASH, &source)
        .map_err(|e| Error::new(format!("Conversion failed: {e}"), arg_span))?;

    let markdown = proc_macro::Literal::string(&markdown);
    Ok(TokenTree::Literal(markdown).into())
}

fn compile_error(error: Error) -> TokenStream {
    let compile_error: TokenTree = Ident::new("compile_error", error.span).into();
    let exclaim: TokenTree = Punct::new('!', Spacing::Joint).into();
    let msg: TokenTree = Literal::string(&error.msg).into();

    let parens: TokenTree =
        Group::new(Delimiter::Parenthesis, TokenStream::from_iter([msg])).into();

    TokenStream::from_iter([compile_error, exclaim, parens])
}