1#![doc = env!("CARGO_PKG_DESCRIPTION")]
2#![doc = ""]
3use 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#[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}