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