Skip to main content

no_incode_comments/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream};
4use syn::punctuated::Punctuated;
5use syn::token::Comma;
6use syn::{parse_macro_input, Expr, Item, ItemFn, ItemStruct, Lit, Meta};
7
8use std::collections::HashMap;
9use std::fs;
10
11/// A proc macro that imports documentation from an external markdown file.
12///
13/// # Example Usage
14///
15/// ```rust
16/// use external_doc::external_doc;
17///
18/// #[external_doc(path = "docs/my_function.md", key = "Function")]
19/// pub fn my_function() {
20///     // Function implementation
21/// }
22/// ```
23///
24/// In the Markdown file "docs/my_function.md":
25///
26/// ```markdown
27/// # Function
28/// This is documentation for my_function.
29/// It will be used as the documentation for the function.
30///
31/// # Another Section
32/// This section would not be pulled in by the above example.
33/// ```
34// Define our own structure for parsing the attribute arguments
35struct ExternalDocArgs {
36    args: Punctuated<Meta, Comma>,
37}
38
39impl Parse for ExternalDocArgs {
40    fn parse(input: ParseStream) -> syn::Result<Self> {
41        Ok(ExternalDocArgs {
42            args: Punctuated::parse_terminated(input)?,
43        })
44    }
45}
46
47#[proc_macro_attribute]
48pub fn external_doc(attr: TokenStream, item: TokenStream) -> TokenStream {
49    let args = parse_macro_input!(attr as ExternalDocArgs).args;
50    let input = parse_macro_input!(item as Item);
51
52    let mut doc_path = None;
53    let mut doc_key = None;
54
55    for arg in args {
56        match arg {
57            Meta::NameValue(nv) if nv.path.is_ident("path") => {
58                if let Expr::Lit(expr_lit) = nv.value {
59                    if let Lit::Str(lit_str) = expr_lit.lit {
60                        doc_path = Some(lit_str.value());
61                    }
62                }
63            }
64            Meta::NameValue(nv) if nv.path.is_ident("key") => {
65                if let Expr::Lit(expr_lit) = nv.value {
66                    if let Lit::Str(lit_str) = expr_lit.lit {
67                        doc_key = Some(lit_str.value());
68                    }
69                }
70            }
71            _ => {}
72        }
73    }
74
75    let doc_path = doc_path.expect("Must specify Markdown path");
76    let doc_key = doc_key.expect("Must specify key");
77
78    let markdown = fs::read_to_string(&doc_path);
79
80    let doc_comment = if !markdown.is_err() {
81        let mut docs_map = HashMap::new();
82        let mut current_key = String::new();
83        let mut current_lines = Vec::new();
84
85        for line in markdown.unwrap().lines() {
86            if let Some(stripped) = line.strip_prefix("# ") {
87                if !current_key.is_empty() {
88                    docs_map.insert(current_key.clone(), current_lines.join("\n"));
89                }
90                current_key = stripped.trim().to_string();
91                current_lines = Vec::new();
92            } else if !current_key.is_empty() {
93                current_lines.push(line.trim_end().to_string());
94            }
95        }
96        if !current_key.is_empty() {
97            docs_map.insert(current_key.clone(), current_lines.join("\n"));
98        }
99
100        docs_map
101            .get(&doc_key)
102            .map(String::as_str)
103            .unwrap_or("No documentation found for item.")
104            .to_string()
105    } else {
106        "No documentation found for item.".to_string()
107    };
108
109    let doc_lines: Vec<_> = doc_comment
110        .lines()
111        .map(|line| quote! { #[doc = #line] })
112        .collect();
113
114    let output = match input {
115        Item::Fn(item_fn) => {
116            quote! {
117                #(#doc_lines)*
118                #item_fn
119            }
120        }
121        Item::Struct(item_struct) => {
122            quote! {
123                #(#doc_lines)*
124                #item_struct
125            }
126        }
127        _ => panic!("#[external_doc] can only be applied to functions or structs"),
128    };
129
130    output.into()
131}