1use proc_macro2::TokenStream;
4use quote::quote;
5use unsynn::*;
6
7use crate::parse::{FnSig, SyncDocArg, SyncDocInner};
8use crate::path_utils::make_manifest_relative_path;
9
10pub fn syncdoc_impl(
11 args: TokenStream,
12 item: TokenStream,
13) -> core::result::Result<TokenStream, TokenStream> {
14 let syncdoc_args = match parse_syncdoc_args(&mut args.to_token_iter()) {
16 Ok(args) => args,
17 Err(e) => {
18 return Ok(quote! {
20 compile_error!(#e);
21 #item
22 });
23 }
24 };
25
26 let mut item_iter = item.to_token_iter();
28 let func = match parse_simple_function(&mut item_iter) {
29 Ok(func) => func,
30 Err(e) => {
31 return Ok(quote! {
32 compile_error!(#e);
33 #item
34 });
35 }
36 };
37
38 Ok(generate_documented_function(syncdoc_args, func))
39}
40
41pub fn module_doc_impl(args: TokenStream) -> core::result::Result<TokenStream, TokenStream> {
46 let call_site = proc_macro2::Span::call_site();
47 let source_file = call_site
48 .local_file()
49 .ok_or_else(|| {
50 let error = "Could not determine source file location";
51 quote! { compile_error!(#error) }
52 })?
53 .to_string_lossy()
54 .to_string();
55
56 let base_path = if args.is_empty() {
58 crate::config::get_docs_path(&source_file).map_err(|e| {
60 let error = format!("Failed to get docs path from config: {}", e);
61 quote! { compile_error!(#error) }
62 })?
63 } else {
64 let mut args_iter = args.into_token_iter();
66 match parse_syncdoc_args(&mut args_iter) {
67 Ok(parsed_args) => parsed_args.base_path,
68 Err(e) => {
69 let error = format!("Failed to parse module_doc args: {}", e);
70 return Err(quote! { compile_error!(#error) });
71 }
72 }
73 };
74
75 let module_path = crate::path_utils::extract_module_path(&source_file);
77 let doc_path = if module_path.is_empty() {
78 let file_stem = std::path::Path::new(&source_file)
80 .file_stem()
81 .and_then(|s| s.to_str())
82 .unwrap_or("module");
83 format!("{}/{}.md", base_path, file_stem)
84 } else {
85 format!("{}/{}.md", base_path, module_path)
86 };
87
88 let local_file = call_site.local_file().ok_or_else(|| {
90 let error = "Could not find local file";
91 quote! { compile_error!(#error) }
92 })?;
93 let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
94
95 Ok(quote! {
97 include_str!(#rel_doc_path)
98 })
99}
100
101#[derive(Debug)]
102struct SyncDocArgs {
103 base_path: String,
104 name: Option<String>,
105 cfg_attr: Option<String>,
106}
107
108struct SimpleFunction {
109 attrs: Vec<TokenStream>,
110 vis: Option<TokenStream>,
111 const_kw: Option<TokenStream>,
112 async_kw: Option<TokenStream>,
113 unsafe_kw: Option<TokenStream>,
114 extern_kw: Option<TokenStream>,
115 fn_name: proc_macro2::Ident,
116 generics: Option<TokenStream>,
117 params: TokenStream,
118 ret_type: Option<TokenStream>,
119 where_clause: Option<TokenStream>,
120 body: TokenStream,
121}
122
123fn parse_syncdoc_args(input: &mut TokenIter) -> core::result::Result<SyncDocArgs, String> {
124 match input.parse::<SyncDocInner>() {
125 Ok(parsed) => {
126 let mut args = SyncDocArgs {
127 base_path: String::new(),
128 name: None,
129 cfg_attr: None,
130 };
131
132 if let Some(arg_list) = parsed.args {
133 for arg in arg_list.0 {
134 match arg.value {
135 SyncDocArg::Path(path_arg) => {
136 args.base_path = path_arg.value.as_str().to_string();
137 }
138 SyncDocArg::Name(name_arg) => {
139 args.name = Some(name_arg.value.as_str().to_string());
140 }
141 SyncDocArg::CfgAttr(cfg_attr_arg) => {
142 args.cfg_attr = Some(cfg_attr_arg.value.as_str().to_string());
143 }
144 }
145 }
146 }
147
148 if args.base_path.is_empty() || args.cfg_attr.is_none() {
149 if args.base_path.is_empty() {
151 let call_site = proc_macro2::Span::call_site();
153 let source_file = call_site
154 .local_file()
155 .ok_or("Could not determine source file location")?
156 .to_string_lossy()
157 .to_string();
158
159 let base_path = crate::config::get_docs_path(&source_file)
160 .map_err(|e| format!("Failed to get docs path from config: {}", e))?;
161
162 let module_path = crate::path_utils::extract_module_path(&source_file);
164 args.base_path = if module_path.is_empty() {
165 base_path
166 } else {
167 format!("{}/{}", base_path, module_path)
168 };
169 }
170
171 if args.cfg_attr.is_none() {
173 let call_site = proc_macro2::Span::call_site();
174 if let Some(source_path) = call_site.local_file() {
175 let source_file = source_path.to_string_lossy().to_string();
176 if let Ok(cfg) = crate::config::get_cfg_attr(&source_file) {
177 args.cfg_attr = cfg;
178 }
179 }
180 }
181 }
182
183 Ok(args)
184 }
185 Err(e) => Err(format!("Failed to parse syncdoc args: {}", e)),
186 }
187}
188
189fn parse_simple_function(input: &mut TokenIter) -> core::result::Result<SimpleFunction, String> {
190 match input.parse::<FnSig>() {
191 Ok(parsed) => {
192 let attrs = if let Some(attr_list) = parsed.attributes {
194 attr_list
195 .0
196 .into_iter()
197 .map(|attr| {
198 let mut tokens = TokenStream::new();
199 unsynn::ToTokens::to_tokens(&attr, &mut tokens);
200 tokens
201 })
202 .collect()
203 } else {
204 Vec::new()
205 };
206
207 let vis = parsed.visibility.map(|v| {
209 let mut tokens = TokenStream::new();
210 quote::ToTokens::to_tokens(&v, &mut tokens);
211 tokens
212 });
213
214 let const_kw = parsed.const_kw.map(|k| {
216 let mut tokens = TokenStream::new();
217 unsynn::ToTokens::to_tokens(&k, &mut tokens);
218 tokens
219 });
220
221 let async_kw = parsed.async_kw.map(|k| {
223 let mut tokens = TokenStream::new();
224 unsynn::ToTokens::to_tokens(&k, &mut tokens);
225 tokens
226 });
227
228 let unsafe_kw = parsed.unsafe_kw.map(|k| {
230 let mut tokens = TokenStream::new();
231 unsynn::ToTokens::to_tokens(&k, &mut tokens);
232 tokens
233 });
234
235 let extern_kw = parsed.extern_kw.map(|k| {
237 let mut tokens = TokenStream::new();
238 unsynn::ToTokens::to_tokens(&k, &mut tokens);
239 tokens
240 });
241
242 let fn_name = parsed.name;
243
244 let generics = parsed.generics.map(|g| {
245 let mut tokens = TokenStream::new();
246 unsynn::ToTokens::to_tokens(&g, &mut tokens);
247 tokens
248 });
249
250 let mut params = TokenStream::new();
251 unsynn::ToTokens::to_tokens(&parsed.params, &mut params);
252
253 let ret_type = parsed.return_type.map(|rt| {
254 let mut tokens = TokenStream::new();
255 unsynn::ToTokens::to_tokens(&rt, &mut tokens);
256 tokens
257 });
258
259 let where_clause = parsed.where_clause.map(|wc| {
260 let mut tokens = TokenStream::new();
261 unsynn::ToTokens::to_tokens(&wc, &mut tokens);
262 tokens
263 });
264
265 let mut body = TokenStream::new();
266 unsynn::ToTokens::to_tokens(&parsed.body, &mut body);
267
268 Ok(SimpleFunction {
269 attrs,
270 vis,
271 const_kw,
272 async_kw,
273 unsafe_kw,
274 extern_kw,
275 fn_name,
276 generics,
277 params,
278 ret_type,
279 where_clause,
280 body,
281 })
282 }
283 Err(e) => Err(format!("Failed to parse function: {}", e)),
284 }
285}
286
287fn generate_documented_function(args: SyncDocArgs, func: SimpleFunction) -> TokenStream {
288 let SimpleFunction {
289 attrs,
290 vis,
291 const_kw,
292 async_kw,
293 unsafe_kw,
294 extern_kw,
295 fn_name,
296 generics,
297 params,
298 ret_type,
299 where_clause,
300 body,
301 } = func;
302
303 let doc_file_name = args.name.unwrap_or_else(|| fn_name.to_string());
305 let doc_path = if args.base_path.ends_with(".md") {
306 args.base_path
308 } else {
309 format!("{}/{}.md", args.base_path, doc_file_name)
311 };
312
313 let call_site = proc_macro2::Span::call_site();
315 let local_file = call_site.local_file().expect("Could not find local file");
316 let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
317
318 let vis_tokens = vis.unwrap_or_default();
320 let const_tokens = const_kw.unwrap_or_default();
321 let async_tokens = async_kw.unwrap_or_default();
322 let unsafe_tokens = unsafe_kw.unwrap_or_default();
323 let extern_tokens = extern_kw.unwrap_or_default();
324 let generics_tokens = generics.unwrap_or_default();
325 let ret_tokens = ret_type.unwrap_or_default();
326 let where_tokens = where_clause.unwrap_or_default();
327
328 let doc_attr = if let Some(cfg_value) = args.cfg_attr {
330 let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
331 quote! { #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))] }
332 } else {
333 quote! { #[doc = include_str!(#rel_doc_path)] }
334 };
335
336 quote! {
337 #(#attrs)*
338 #doc_attr
339 #vis_tokens #const_tokens #async_tokens #unsafe_tokens #extern_tokens fn #fn_name #generics_tokens #params #ret_tokens #where_tokens #body
340 }
341}
342
343pub fn inject_doc_attr(
345 doc_path: String,
346 cfg_attr: Option<String>,
347 item: TokenStream,
348) -> TokenStream {
349 let call_site = proc_macro2::Span::call_site();
351 let local_file = call_site.local_file().expect("Could not find local file");
352 let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
353 if let Some(cfg_value) = cfg_attr {
354 let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
355 quote! {
356 #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))]
357 #item
358 }
359 } else {
360 quote! {
361 #[doc = include_str!(#rel_doc_path)]
362 #item
363 }
364 }
365}