1use crate::discover::ParsedFile;
4use crate::extract::extract_doc_content;
5use proc_macro2::TokenStream;
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use syncdoc_core::parse::{
10 EnumSig, EnumVariant, ImplBlockSig, ModuleContent, ModuleItem, ModuleSig, StructField,
11 StructSig, TraitSig,
12};
13use unsynn::*;
14
15mod expected;
16pub use expected::find_expected_doc_paths;
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct DocExtraction {
21 pub markdown_path: PathBuf,
23 pub content: String,
25 pub source_location: String,
27}
28
29impl DocExtraction {
30 pub fn new(markdown_path: PathBuf, mut content: String, source_location: String) -> Self {
32 if !content.ends_with('\n') {
33 content.push('\n');
34 }
35 Self {
36 markdown_path,
37 content,
38 source_location,
39 }
40 }
41}
42
43#[derive(Debug, Default)]
45pub struct WriteReport {
46 pub files_written: usize,
47 pub files_skipped: usize,
48 pub errors: Vec<String>,
49}
50
51pub fn extract_all_docs(parsed: &ParsedFile, docs_root: &str) -> Vec<DocExtraction> {
56 let mut extractions = Vec::new();
57
58 let module_path = syncdoc_core::path_utils::extract_module_path(&parsed.path.to_string_lossy());
60
61 if let Some(inner_doc) = crate::extract::extract_inner_doc_content(&parsed.content.inner_attrs)
63 {
64 let file_stem = parsed
66 .path
67 .file_stem()
68 .and_then(|s| s.to_str())
69 .unwrap_or("module");
70
71 let path = if module_path.is_empty() {
72 format!("{}/{}.md", docs_root, file_stem)
73 } else {
74 format!("{}/{}.md", docs_root, module_path)
75 };
76
77 extractions.push(DocExtraction::new(
78 PathBuf::from(path),
79 inner_doc,
80 format!("{}:1", parsed.path.display()),
81 ));
82 }
83
84 let mut context = Vec::new();
86 if !module_path.is_empty() {
87 context.push(module_path);
88 }
89
90 for item_delimited in &parsed.content.items.0 {
91 let item = &item_delimited.value;
92 extractions.extend(extract_item_docs(
93 item,
94 context.clone(),
95 docs_root,
96 &parsed.path,
97 ));
98 }
99
100 extractions
101}
102
103fn extract_item_docs(
105 item: &ModuleItem,
106 context: Vec<String>,
107 base_path: &str,
108 source_file: &Path,
109) -> Vec<DocExtraction> {
110 let mut extractions = Vec::new();
111
112 match item {
113 ModuleItem::Function(func_sig) => {
114 if let Some(content) = extract_doc_content(&func_sig.attributes) {
115 let path = build_path(base_path, &context, &func_sig.name.to_string());
116 let location = format!(
117 "{}:{}",
118 source_file.display(),
119 func_sig.name.span().start().line
120 );
121 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
122 }
123 }
124
125 ModuleItem::ImplBlock(impl_block) => {
126 extractions.extend(extract_impl_docs(
127 impl_block,
128 context,
129 base_path,
130 source_file,
131 ));
132 }
133
134 ModuleItem::Module(module) => {
135 extractions.extend(extract_module_docs(module, context, base_path, source_file));
136 }
137
138 ModuleItem::Trait(trait_def) => {
139 extractions.extend(extract_trait_docs(
140 trait_def,
141 context,
142 base_path,
143 source_file,
144 ));
145 }
146
147 ModuleItem::Enum(enum_sig) => {
148 extractions.extend(extract_enum_docs(enum_sig, context, base_path, source_file));
149 }
150
151 ModuleItem::Struct(struct_sig) => {
152 extractions.extend(extract_struct_docs(
153 struct_sig,
154 context,
155 base_path,
156 source_file,
157 ));
158 }
159
160 ModuleItem::TypeAlias(type_alias) => {
161 if let Some(content) = extract_doc_content(&type_alias.attributes) {
162 let path = build_path(base_path, &context, &type_alias.name.to_string());
163 let location = format!(
164 "{}:{}",
165 source_file.display(),
166 type_alias.name.span().start().line
167 );
168 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
169 }
170 }
171
172 ModuleItem::Const(const_sig) => {
173 if let Some(content) = extract_doc_content(&const_sig.attributes) {
174 let path = build_path(base_path, &context, &const_sig.name.to_string());
175 let location = format!(
176 "{}:{}",
177 source_file.display(),
178 const_sig.name.span().start().line
179 );
180 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
181 }
182 }
183
184 ModuleItem::Static(static_sig) => {
185 if let Some(content) = extract_doc_content(&static_sig.attributes) {
186 let path = build_path(base_path, &context, &static_sig.name.to_string());
187 let location = format!(
188 "{}:{}",
189 source_file.display(),
190 static_sig.name.span().start().line
191 );
192 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
193 }
194 }
195
196 ModuleItem::Other(_) => {}
198 }
199
200 extractions
201}
202
203fn extract_impl_docs(
205 impl_block: &ImplBlockSig,
206 context: Vec<String>,
207 base_path: &str,
208 source_file: &Path,
209) -> Vec<DocExtraction> {
210 let mut extractions = Vec::new();
211
212 let impl_context = if let Some(for_trait) = &impl_block.for_trait {
216 let trait_name = if let Some(first) = impl_block.target_type.0.first() {
219 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
220 ident.to_string()
221 } else {
222 "Unknown".to_string()
223 }
224 } else {
225 "Unknown".to_string()
226 };
227
228 let type_name = if let Some(first) = for_trait.second.0.first() {
230 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
231 ident.to_string()
232 } else {
233 "Unknown".to_string()
234 }
235 } else {
236 "Unknown".to_string()
237 };
238
239 vec![type_name, trait_name]
241 } else {
242 let type_name = if let Some(first) = impl_block.target_type.0.first() {
244 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
245 ident.to_string()
246 } else {
247 "Unknown".to_string()
248 }
249 } else {
250 "Unknown".to_string()
251 };
252 vec![type_name]
253 };
254
255 let mut new_context = context;
256 new_context.extend(impl_context);
257
258 let body_stream = extract_brace_content(&impl_block.body);
260 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
261 for item_delimited in &content.items.0 {
262 extractions.extend(extract_item_docs(
263 &item_delimited.value,
264 new_context.clone(),
265 base_path,
266 source_file,
267 ));
268 }
269 }
270
271 extractions
272}
273
274fn extract_module_docs(
276 module: &ModuleSig,
277 context: Vec<String>,
278 base_path: &str,
279 source_file: &Path,
280) -> Vec<DocExtraction> {
281 let mut extractions = Vec::new();
282
283 if let Some(content) = extract_doc_content(&module.attributes) {
285 let path = build_path(base_path, &context, &module.name.to_string());
286 let location = format!(
287 "{}:{}",
288 source_file.display(),
289 module.name.span().start().line
290 );
291 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
292 }
293
294 let mut new_context = context;
296 new_context.push(module.name.to_string());
297
298 let body_stream = extract_brace_content(&module.body);
300 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
301 for item_delimited in &content.items.0 {
302 extractions.extend(extract_item_docs(
303 &item_delimited.value,
304 new_context.clone(),
305 base_path,
306 source_file,
307 ));
308 }
309 }
310
311 extractions
312}
313
314fn extract_trait_docs(
316 trait_def: &TraitSig,
317 context: Vec<String>,
318 base_path: &str,
319 source_file: &Path,
320) -> Vec<DocExtraction> {
321 let mut extractions = Vec::new();
322
323 if let Some(content) = extract_doc_content(&trait_def.attributes) {
325 let path = build_path(base_path, &context, &trait_def.name.to_string());
326 let location = format!(
327 "{}:{}",
328 source_file.display(),
329 trait_def.name.span().start().line
330 );
331 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
332 }
333
334 let mut new_context = context;
336 new_context.push(trait_def.name.to_string());
337
338 let body_stream = extract_brace_content(&trait_def.body);
340 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
341 for item_delimited in &content.items.0 {
342 extractions.extend(extract_item_docs(
343 &item_delimited.value,
344 new_context.clone(),
345 base_path,
346 source_file,
347 ));
348 }
349 }
350
351 extractions
352}
353
354fn extract_enum_docs(
356 enum_sig: &EnumSig,
357 context: Vec<String>,
358 base_path: &str,
359 source_file: &Path,
360) -> Vec<DocExtraction> {
361 let mut extractions = Vec::new();
362 let enum_name = enum_sig.name.to_string();
363
364 if let Some(content) = extract_doc_content(&enum_sig.attributes) {
366 let path = build_path(base_path, &context, &enum_name);
367 let location = format!(
368 "{}:{}",
369 source_file.display(),
370 enum_sig.name.span().start().line
371 );
372 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
373 }
374
375 let body_stream = extract_brace_content(&enum_sig.body);
377 if let Ok(variants) = body_stream
378 .into_token_iter()
379 .parse::<CommaDelimitedVec<EnumVariant>>()
380 {
381 for variant_delimited in &variants.0 {
382 let variant = &variant_delimited.value;
383 if let Some(content) = extract_doc_content(&variant.attributes) {
384 let path = build_path(
385 base_path,
386 &context,
387 &format!("{}/{}", enum_name, variant.name),
388 );
389 extractions.push(DocExtraction::new(
390 PathBuf::from(path),
391 content,
392 format!(
393 "{}:{}",
394 source_file.display(),
395 variant.name.span().start().line
396 ),
397 ));
398 }
399 }
400 }
401
402 extractions
403}
404
405fn extract_struct_docs(
407 struct_sig: &StructSig,
408 context: Vec<String>,
409 base_path: &str,
410 source_file: &Path,
411) -> Vec<DocExtraction> {
412 let mut extractions = Vec::new();
413 let struct_name = struct_sig.name.to_string();
414
415 if let Some(content) = extract_doc_content(&struct_sig.attributes) {
417 let path = build_path(base_path, &context, &struct_name);
418 let location = format!(
419 "{}:{}",
420 source_file.display(),
421 struct_sig.name.span().start().line
422 );
423 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
424 }
425
426 if let syncdoc_core::parse::StructBody::Named(brace_group) = &struct_sig.body {
428 let body_stream = extract_brace_content(brace_group);
429
430 if let Ok(fields) = body_stream
431 .into_token_iter()
432 .parse::<CommaDelimitedVec<StructField>>()
433 {
434 for field_delimited in &fields.0 {
435 let field = &field_delimited.value;
436 if let Some(content) = extract_doc_content(&field.attributes) {
437 let path = build_path(
438 base_path,
439 &context,
440 &format!("{}/{}", struct_name, field.name),
441 );
442 extractions.push(DocExtraction::new(
443 PathBuf::from(path),
444 content,
445 format!(
446 "{}:{}",
447 source_file.display(),
448 field.name.span().start().line
449 ),
450 ));
451 }
452 }
453 }
454 }
455
456 extractions
457}
458
459pub fn write_extractions(
464 extractions: &[DocExtraction],
465 dry_run: bool,
466) -> std::io::Result<WriteReport> {
467 let mut report = WriteReport::default();
468
469 let mut dirs: HashMap<PathBuf, Vec<&DocExtraction>> = HashMap::new();
471 for extraction in extractions {
472 if let Some(parent) = extraction.markdown_path.parent() {
473 dirs.entry(parent.to_path_buf())
474 .or_default()
475 .push(extraction);
476 }
477 }
478
479 for dir in dirs.keys() {
481 if !dry_run {
482 if let Err(e) = fs::create_dir_all(dir) {
483 report.errors.push(format!(
484 "Failed to create directory {}: {}",
485 dir.display(),
486 e
487 ));
488 continue;
489 }
490 }
491 }
492
493 for extraction in extractions {
495 if dry_run {
496 println!("Would write: {}", extraction.markdown_path.display());
497 report.files_written += 1;
498 } else {
499 match fs::write(&extraction.markdown_path, &extraction.content) {
500 Ok(_) => {
501 report.files_written += 1;
502 }
503 Err(e) => {
504 report.errors.push(format!(
505 "Failed to write {}: {}",
506 extraction.markdown_path.display(),
507 e
508 ));
509 report.files_skipped += 1;
510 }
511 }
512 }
513 }
514
515 Ok(report)
516}
517
518fn build_path(base_path: &str, context: &[String], item_name: &str) -> String {
521 let mut parts = vec![base_path.to_string()];
522 parts.extend(context.iter().cloned());
523 parts.push(format!("{}.md", item_name));
524 parts.join("/")
525}
526
527fn extract_brace_content(brace_group: &BraceGroup) -> TokenStream {
528 let mut ts = TokenStream::new();
529 unsynn::ToTokens::to_tokens(brace_group, &mut ts);
530 if let Some(proc_macro2::TokenTree::Group(g)) = ts.into_iter().next() {
531 g.stream()
532 } else {
533 TokenStream::new()
534 }
535}
536
537#[cfg(test)]
538mod tests;