Skip to main content

wowlab_docgen_cli/infra/tree/
doc.rs

1// #t(file: rust_alloc_in_loop) doc scanning allocs per scanned line by design
2
3use 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}