syncdoc_core/
doc_injector.rs

1//! Procedural macro attributes for automatically injecting documentation from files.
2
3use proc_macro2::TokenStream;
4use quote::quote;
5use unsynn::*;
6
7use crate::parse::{SyncDocArg, SyncDocInner};
8use crate::path_utils::make_manifest_relative_path;
9
10/// Injects a doc attribute without parsing the item structure
11pub fn omnidoc_impl(doc_path: String, cfg_attr: Option<String>, item: TokenStream) -> TokenStream {
12    // Get the call site's file path if there might be config we could use there
13    let call_site = proc_macro2::Span::call_site();
14    let local_file = call_site.local_file().expect("Could not find local file");
15    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
16    if let Some(cfg_value) = cfg_attr {
17        let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
18        quote! {
19            #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))]
20            #item
21        }
22    } else {
23        quote! {
24            #[doc = include_str!(#rel_doc_path)]
25            #item
26        }
27    }
28}
29
30/// Implementation for the module_doc!() macro
31///
32/// Generates an include_str!() call with the automatically resolved path
33/// to the module's markdown documentation file.
34pub fn module_doc_impl(args: TokenStream) -> core::result::Result<TokenStream, TokenStream> {
35    let call_site = proc_macro2::Span::call_site();
36    let source_file = call_site
37        .local_file()
38        .ok_or_else(|| {
39            let error = "Could not determine source file location";
40            quote! { compile_error!(#error) }
41        })?
42        .to_string_lossy()
43        .to_string();
44
45    // Parse the arguments to get base_path if provided
46    let base_path = if args.is_empty() {
47        // No args provided, get from config
48        crate::config::get_docs_path(&source_file).map_err(|e| {
49            let error = format!("Failed to get docs path from config: {}", e);
50            quote! { compile_error!(#error) }
51        })?
52    } else {
53        // Parse args to extract path
54        let mut args_iter = args.into_token_iter();
55        match parse_syncdoc_args(&mut args_iter) {
56            Ok(parsed_args) => parsed_args.base_path,
57            Err(e) => {
58                let error = format!("Failed to parse module_doc args: {}", e);
59                return Err(quote! { compile_error!(#error) });
60            }
61        }
62    };
63
64    // Extract module path and construct full doc path
65    let module_path = crate::path_utils::extract_module_path(&source_file);
66    let doc_path = if module_path.is_empty() {
67        // For lib.rs or main.rs, use the file stem
68        let file_stem = std::path::Path::new(&source_file)
69            .file_stem()
70            .and_then(|s| s.to_str())
71            .unwrap_or("module");
72        format!("{}/{}.md", base_path, file_stem)
73    } else {
74        format!("{}/{}.md", base_path, module_path)
75    };
76
77    // Make path relative to call site
78    let local_file = call_site.local_file().ok_or_else(|| {
79        let error = "Could not find local file";
80        quote! { compile_error!(#error) }
81    })?;
82    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
83
84    // Generate include_str!() call
85    Ok(quote! {
86        include_str!(#rel_doc_path)
87    })
88}
89
90#[derive(Debug)]
91struct SyncDocArgs {
92    base_path: String,
93    name: Option<String>,
94    cfg_attr: Option<String>,
95}
96
97fn parse_syncdoc_args(input: &mut TokenIter) -> core::result::Result<SyncDocArgs, String> {
98    match input.parse::<SyncDocInner>() {
99        Ok(parsed) => {
100            let mut args = SyncDocArgs {
101                base_path: String::new(),
102                name: None,
103                cfg_attr: None,
104            };
105
106            if let Some(arg_list) = parsed.args {
107                for arg in arg_list.0 {
108                    match arg.value {
109                        SyncDocArg::Path(path_arg) => {
110                            args.base_path = path_arg.value.as_str().to_string();
111                        }
112                        SyncDocArg::Name(name_arg) => {
113                            args.name = Some(name_arg.value.as_str().to_string());
114                        }
115                        SyncDocArg::CfgAttr(cfg_attr_arg) => {
116                            args.cfg_attr = Some(cfg_attr_arg.value.as_str().to_string());
117                        }
118                    }
119                }
120            }
121
122            if args.base_path.is_empty() || args.cfg_attr.is_none() {
123                // If macro path and TOML docs-path both unset, we don't know where to find the docs
124                if args.base_path.is_empty() {
125                    // Get the call site's file path if there might be config we could use there
126                    let call_site = proc_macro2::Span::call_site();
127                    let source_file = call_site
128                        .local_file()
129                        .ok_or("Could not determine source file location")?
130                        .to_string_lossy()
131                        .to_string();
132
133                    let base_path = crate::config::get_docs_path(&source_file)
134                        .map_err(|e| format!("Failed to get docs path from config: {}", e))?;
135
136                    // Extract module path and prepend to base_path
137                    let module_path = crate::path_utils::extract_module_path(&source_file);
138                    args.base_path = if module_path.is_empty() {
139                        base_path
140                    } else {
141                        format!("{}/{}", base_path, module_path)
142                    };
143                }
144
145                // We don't error on unconfigured cfg_attr, it's optional
146                if args.cfg_attr.is_none() {
147                    let call_site = proc_macro2::Span::call_site();
148                    if let Some(source_path) = call_site.local_file() {
149                        let source_file = source_path.to_string_lossy().to_string();
150                        if let Ok(cfg) = crate::config::get_cfg_attr(&source_file) {
151                            args.cfg_attr = cfg;
152                        }
153                    }
154                }
155            }
156
157            Ok(args)
158        }
159        Err(e) => Err(format!("Failed to parse syncdoc args: {}", e)),
160    }
161}