wowlab_docgen_cli/infra/tree/
doc.rs1use wowlab_fs::path::Path;
4
5const DOC_SCAN_LINES: usize = 10;
6
7pub(super) fn extract_file_doc(workspace: &crate::WorkspaceIndex, path: &Path) -> Option<String> {
8 let ext = path.extension()?.to_str()?;
9 let contents = workspace.contents(path)?;
10
11 match ext {
12 "rs" => extract_rust_doc(contents),
13 "ts" | "tsx" | "js" | "jsx" => extract_js_doc(contents),
14 "py" => extract_py_doc(contents),
15 _ => None,
16 }
17}
18
19fn extract_rust_doc(contents: &str) -> Option<String> {
20 for line in contents.lines().take(DOC_SCAN_LINES) {
21 let trimmed = line.trim();
22
23 if trimmed.is_empty() || trimmed.starts_with("// #t(") {
24 continue;
25 }
26
27 if let Some(doc) = trimmed.strip_prefix("//!") {
28 let doc = doc.trim();
29
30 if !doc.is_empty() {
31 return Some(doc.to_string());
32 }
33 }
34
35 break;
36 }
37
38 None
39}
40
41fn extract_js_doc(contents: &str) -> Option<String> {
42 let mut in_jsdoc = false;
43
44 for line in contents.lines().take(DOC_SCAN_LINES) {
45 let trimmed = line.trim();
46
47 if trimmed.is_empty() {
48 continue;
49 }
50
51 let result = if in_jsdoc {
52 parse_jsdoc_body(trimmed)
53 } else {
54 parse_js_first_comment(trimmed)
55 };
56
57 match result {
58 JsDocLine::Found(doc) => return Some(doc),
59 JsDocLine::End => return None,
60 JsDocLine::EnterBlock => in_jsdoc = true,
61 JsDocLine::Skip => {}
62 }
63 }
64
65 None
66}
67
68enum JsDocLine {
69 Found(String),
70 End,
71 EnterBlock,
72 Skip,
73}
74
75fn parse_jsdoc_body(trimmed: &str) -> JsDocLine {
76 if let Some(rest) = trimmed.strip_prefix('*') {
77 let rest = rest.trim();
78 let rest = rest.strip_suffix("*/").map_or(rest, str::trim);
79
80 if !rest.is_empty() && rest != "/" {
81 return JsDocLine::Found(rest.to_string());
82 }
83 }
84
85 if trimmed.contains("*/") {
86 return JsDocLine::End;
87 }
88
89 JsDocLine::Skip
90}
91
92fn parse_js_first_comment(trimmed: &str) -> JsDocLine {
93 if let Some(rest) = trimmed.strip_prefix("/**") {
94 return parse_jsdoc_opening(rest.trim());
95 }
96
97 if let Some(doc) = trimmed.strip_prefix("//") {
98 let doc = doc.trim();
99
100 if !doc.is_empty() {
101 return JsDocLine::Found(doc.to_string());
102 }
103 }
104
105 JsDocLine::End
106}
107
108fn parse_jsdoc_opening(rest: &str) -> JsDocLine {
109 if let Some(content) = rest.strip_suffix("*/") {
110 let content = content.trim();
111
112 if !content.is_empty() {
113 return JsDocLine::Found(content.to_string());
114 }
115
116 return JsDocLine::End;
117 }
118
119 if !rest.is_empty() {
120 return JsDocLine::Found(rest.to_string());
121 }
122
123 JsDocLine::EnterBlock
124}
125
126fn extract_py_doc(contents: &str) -> Option<String> {
127 for line in contents.lines().take(DOC_SCAN_LINES) {
128 let trimmed = line.trim();
129
130 if trimmed.is_empty() || trimmed.starts_with('#') {
131 continue;
132 }
133
134 for fence in ["\"\"\"", "'''"] {
135 if trimmed.starts_with(fence) {
136 return py_fence_doc(trimmed, fence);
137 }
138 }
139
140 break;
141 }
142
143 None
144}
145
146fn py_fence_doc(trimmed: &str, fence: &str) -> Option<String> {
147 let rest = trimmed.strip_prefix(fence).unwrap_or(trimmed);
148
149 if let Some(content) = rest.strip_suffix(fence) {
150 let content = content.trim();
151
152 if !content.is_empty() {
153 return Some(content.to_string());
154 }
155
156 return None;
157 }
158
159 let rest = rest.trim();
160
161 if !rest.is_empty() {
162 return Some(rest.to_string());
163 }
164
165 None
166}