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
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 type_name = if let Some(first) = impl_block.target_type.0.first() {
211 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
212 ident.to_string()
213 } else {
214 "Unknown".to_string()
215 }
216 } else {
217 "Unknown".to_string()
218 };
219
220 let mut new_context = context;
221 new_context.push(type_name);
222
223 let body_stream = extract_brace_content(&impl_block.body);
225 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
226 for item_delimited in &content.items.0 {
227 extractions.extend(extract_item_docs(
228 &item_delimited.value,
229 new_context.clone(),
230 base_path,
231 source_file,
232 ));
233 }
234 }
235
236 extractions
237}
238
239fn extract_module_docs(
241 module: &ModuleSig,
242 context: Vec<String>,
243 base_path: &str,
244 source_file: &Path,
245) -> Vec<DocExtraction> {
246 let mut extractions = Vec::new();
247
248 if let Some(content) = extract_doc_content(&module.attributes) {
250 let path = build_path(base_path, &context, &module.name.to_string());
251 let location = format!(
252 "{}:{}",
253 source_file.display(),
254 module.name.span().start().line
255 );
256 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
257 }
258
259 let mut new_context = context;
261 new_context.push(module.name.to_string());
262
263 let body_stream = extract_brace_content(&module.body);
265 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
266 for item_delimited in &content.items.0 {
267 extractions.extend(extract_item_docs(
268 &item_delimited.value,
269 new_context.clone(),
270 base_path,
271 source_file,
272 ));
273 }
274 }
275
276 extractions
277}
278
279fn extract_trait_docs(
281 trait_def: &TraitSig,
282 context: Vec<String>,
283 base_path: &str,
284 source_file: &Path,
285) -> Vec<DocExtraction> {
286 let mut extractions = Vec::new();
287
288 if let Some(content) = extract_doc_content(&trait_def.attributes) {
290 let path = build_path(base_path, &context, &trait_def.name.to_string());
291 let location = format!(
292 "{}:{}",
293 source_file.display(),
294 trait_def.name.span().start().line
295 );
296 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
297 }
298
299 let mut new_context = context;
301 new_context.push(trait_def.name.to_string());
302
303 let body_stream = extract_brace_content(&trait_def.body);
305 if let Ok(content) = body_stream.into_token_iter().parse::<ModuleContent>() {
306 for item_delimited in &content.items.0 {
307 extractions.extend(extract_item_docs(
308 &item_delimited.value,
309 new_context.clone(),
310 base_path,
311 source_file,
312 ));
313 }
314 }
315
316 extractions
317}
318
319fn extract_enum_docs(
321 enum_sig: &EnumSig,
322 context: Vec<String>,
323 base_path: &str,
324 source_file: &Path,
325) -> Vec<DocExtraction> {
326 let mut extractions = Vec::new();
327 let enum_name = enum_sig.name.to_string();
328
329 if let Some(content) = extract_doc_content(&enum_sig.attributes) {
331 let path = build_path(base_path, &context, &enum_name);
332 let location = format!(
333 "{}:{}",
334 source_file.display(),
335 enum_sig.name.span().start().line
336 );
337 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
338 }
339
340 let body_stream = extract_brace_content(&enum_sig.body);
342 if let Ok(variants) = body_stream
343 .into_token_iter()
344 .parse::<CommaDelimitedVec<EnumVariant>>()
345 {
346 for variant_delimited in &variants.0 {
347 let variant = &variant_delimited.value;
348 if let Some(content) = extract_doc_content(&variant.attributes) {
349 let path = build_path(
350 base_path,
351 &context,
352 &format!("{}/{}", enum_name, variant.name),
353 );
354 extractions.push(DocExtraction::new(
355 PathBuf::from(path),
356 content,
357 format!(
358 "{}:{}",
359 source_file.display(),
360 variant.name.span().start().line
361 ),
362 ));
363 }
364 }
365 }
366
367 extractions
368}
369
370fn extract_struct_docs(
372 struct_sig: &StructSig,
373 context: Vec<String>,
374 base_path: &str,
375 source_file: &Path,
376) -> Vec<DocExtraction> {
377 let mut extractions = Vec::new();
378 let struct_name = struct_sig.name.to_string();
379
380 if let Some(content) = extract_doc_content(&struct_sig.attributes) {
382 let path = build_path(base_path, &context, &struct_name);
383 let location = format!(
384 "{}:{}",
385 source_file.display(),
386 struct_sig.name.span().start().line
387 );
388 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
389 }
390
391 if let syncdoc_core::parse::StructBody::Named(brace_group) = &struct_sig.body {
393 let body_stream = extract_brace_content(brace_group);
394
395 if let Ok(fields) = body_stream
396 .into_token_iter()
397 .parse::<CommaDelimitedVec<StructField>>()
398 {
399 for field_delimited in &fields.0 {
400 let field = &field_delimited.value;
401 if let Some(content) = extract_doc_content(&field.attributes) {
402 let path = build_path(
403 base_path,
404 &context,
405 &format!("{}/{}", struct_name, field.name),
406 );
407 extractions.push(DocExtraction::new(
408 PathBuf::from(path),
409 content,
410 format!(
411 "{}:{}",
412 source_file.display(),
413 field.name.span().start().line
414 ),
415 ));
416 }
417 }
418 }
419 }
420
421 extractions
422}
423
424pub fn write_extractions(
429 extractions: &[DocExtraction],
430 dry_run: bool,
431) -> std::io::Result<WriteReport> {
432 let mut report = WriteReport::default();
433
434 let mut dirs: HashMap<PathBuf, Vec<&DocExtraction>> = HashMap::new();
436 for extraction in extractions {
437 if let Some(parent) = extraction.markdown_path.parent() {
438 dirs.entry(parent.to_path_buf())
439 .or_default()
440 .push(extraction);
441 }
442 }
443
444 for dir in dirs.keys() {
446 if !dry_run {
447 if let Err(e) = fs::create_dir_all(dir) {
448 report.errors.push(format!(
449 "Failed to create directory {}: {}",
450 dir.display(),
451 e
452 ));
453 continue;
454 }
455 }
456 }
457
458 for extraction in extractions {
460 if dry_run {
461 println!("Would write: {}", extraction.markdown_path.display());
462 report.files_written += 1;
463 } else {
464 match fs::write(&extraction.markdown_path, &extraction.content) {
465 Ok(_) => {
466 report.files_written += 1;
467 }
468 Err(e) => {
469 report.errors.push(format!(
470 "Failed to write {}: {}",
471 extraction.markdown_path.display(),
472 e
473 ));
474 report.files_skipped += 1;
475 }
476 }
477 }
478 }
479
480 Ok(report)
481}
482
483fn build_path(base_path: &str, context: &[String], item_name: &str) -> String {
486 let mut parts = vec![base_path.to_string()];
487 parts.extend(context.iter().cloned());
488 parts.push(format!("{}.md", item_name));
489 parts.join("/")
490}
491
492fn extract_brace_content(brace_group: &BraceGroup) -> TokenStream {
493 let mut ts = TokenStream::new();
494 unsynn::ToTokens::to_tokens(brace_group, &mut ts);
495 if let Some(proc_macro2::TokenTree::Group(g)) = ts.into_iter().next() {
496 g.stream()
497 } else {
498 TokenStream::new()
499 }
500}
501
502#[cfg(test)]
503mod tests;