1use crate::ast::{Program, Statement, StmtKind, SubSigParam};
10
11pub fn generate_markdown(filename: &str, source: &str, program: &Program) -> String {
17 let source_lines: Vec<&str> = source.lines().collect();
18 let module_title = derive_module_title(filename, program);
19
20 let mut out = String::new();
21 out.push_str("# Module: ");
22 out.push_str(&module_title);
23 out.push_str("\n\n");
24
25 if let Some(header) = leading_module_doc(&source_lines) {
28 out.push_str(&header);
29 out.push_str("\n\n");
30 }
31
32 let mut subs: Vec<&Statement> = Vec::new();
34 let mut structs: Vec<&Statement> = Vec::new();
35 let mut enums: Vec<&Statement> = Vec::new();
36 let mut classes: Vec<&Statement> = Vec::new();
37 let mut traits: Vec<&Statement> = Vec::new();
38 let mut consts: Vec<&Statement> = Vec::new();
39 let mut packages: Vec<&Statement> = Vec::new();
40
41 for stmt in &program.statements {
42 match &stmt.kind {
43 StmtKind::SubDecl { .. } => subs.push(stmt),
44 StmtKind::StructDecl { .. } => structs.push(stmt),
45 StmtKind::EnumDecl { .. } => enums.push(stmt),
46 StmtKind::ClassDecl { .. } => classes.push(stmt),
47 StmtKind::TraitDecl { .. } => traits.push(stmt),
48 StmtKind::Use { module, .. } if module == "constant" => consts.push(stmt),
49 StmtKind::Package { .. } => packages.push(stmt),
50 _ => {}
51 }
52 }
53
54 if !packages.is_empty() {
55 out.push_str("## Packages\n\n");
56 for stmt in &packages {
57 if let StmtKind::Package { name } = &stmt.kind {
58 let doc = extract_doc_above(&source_lines, stmt.line);
59 out.push_str(&format!("### `package {}`\n\n", name));
60 if !doc.is_empty() {
61 out.push_str(&doc);
62 out.push_str("\n\n");
63 }
64 }
65 }
66 }
67
68 if !consts.is_empty() {
69 out.push_str("## Constants\n\n");
70 for stmt in &consts {
71 if let StmtKind::Use { imports, .. } = &stmt.kind {
72 let doc = extract_doc_above(&source_lines, stmt.line);
73 for name in extract_constant_names(imports) {
74 out.push_str(&format!("### `{}`\n\n", name));
75 if !doc.is_empty() {
76 out.push_str(&doc);
77 out.push_str("\n\n");
78 }
79 }
80 }
81 }
82 }
83
84 if !traits.is_empty() {
85 out.push_str("## Traits\n\n");
86 for stmt in &traits {
87 if let StmtKind::TraitDecl { def } = &stmt.kind {
88 let doc = extract_doc_above(&source_lines, stmt.line);
89 out.push_str(&format!("### `trait {}`\n\n", def.name));
90 if !doc.is_empty() {
91 out.push_str(&doc);
92 out.push_str("\n\n");
93 }
94 }
95 }
96 }
97
98 if !structs.is_empty() {
99 out.push_str("## Structs\n\n");
100 for stmt in &structs {
101 if let StmtKind::StructDecl { def } = &stmt.kind {
102 let doc = extract_doc_above(&source_lines, stmt.line);
103 out.push_str(&format!("### `struct {}`\n\n", def.name));
104 if !doc.is_empty() {
105 out.push_str(&doc);
106 out.push_str("\n\n");
107 }
108 if !def.fields.is_empty() {
109 out.push_str("Fields:\n");
110 for f in &def.fields {
111 out.push_str(&format!("- `{}`\n", f.name));
112 }
113 out.push('\n');
114 }
115 }
116 }
117 }
118
119 if !enums.is_empty() {
120 out.push_str("## Enums\n\n");
121 for stmt in &enums {
122 if let StmtKind::EnumDecl { def } = &stmt.kind {
123 let doc = extract_doc_above(&source_lines, stmt.line);
124 out.push_str(&format!("### `enum {}`\n\n", def.name));
125 if !doc.is_empty() {
126 out.push_str(&doc);
127 out.push_str("\n\n");
128 }
129 if !def.variants.is_empty() {
130 out.push_str("Variants:\n");
131 for v in &def.variants {
132 out.push_str(&format!("- `{}`\n", v.name));
133 }
134 out.push('\n');
135 }
136 }
137 }
138 }
139
140 if !classes.is_empty() {
141 out.push_str("## Classes\n\n");
142 for stmt in &classes {
143 if let StmtKind::ClassDecl { def } = &stmt.kind {
144 let doc = extract_doc_above(&source_lines, stmt.line);
145 out.push_str(&format!("### `class {}`\n\n", def.name));
146 if !doc.is_empty() {
147 out.push_str(&doc);
148 out.push_str("\n\n");
149 }
150 if !def.fields.is_empty() {
151 out.push_str("Fields:\n");
152 for f in &def.fields {
153 out.push_str(&format!("- `{}`\n", f.name));
154 }
155 out.push('\n');
156 }
157 }
158 }
159 }
160
161 if !subs.is_empty() {
162 out.push_str("## Subroutines\n\n");
163 for stmt in &subs {
164 if let StmtKind::SubDecl { name, params, .. } = &stmt.kind {
165 let doc = extract_doc_above(&source_lines, stmt.line);
166 let sig = format_sub_signature(name, params);
167 out.push_str(&format!("### `fn {}`\n\n", sig));
168 if !doc.is_empty() {
169 out.push_str(&doc);
170 out.push_str("\n\n");
171 }
172 }
173 }
174 }
175
176 out
177}
178
179fn derive_module_title(filename: &str, program: &Program) -> String {
182 for stmt in &program.statements {
183 if let StmtKind::Package { name } = &stmt.kind {
184 return name.clone();
185 }
186 }
187 std::path::Path::new(filename)
188 .file_stem()
189 .and_then(|s| s.to_str())
190 .unwrap_or(filename)
191 .to_string()
192}
193
194fn extract_doc_above(source_lines: &[&str], decl_line_1based: usize) -> String {
199 if decl_line_1based < 2 {
200 return String::new();
201 }
202 let mut collected: Vec<String> = Vec::new();
203 let mut i = decl_line_1based.saturating_sub(2); loop {
205 let line = source_lines.get(i).copied().unwrap_or("");
206 let trimmed = line.trim_start();
207 if let Some(rest) = trimmed.strip_prefix("## ") {
208 collected.push(rest.to_string());
209 } else if trimmed == "##" {
210 collected.push(String::new());
211 } else {
212 break;
213 }
214 if i == 0 {
215 break;
216 }
217 i -= 1;
218 }
219 collected.reverse();
220 collected.join("\n")
221}
222
223fn leading_module_doc(source_lines: &[&str]) -> Option<String> {
226 let mut collected: Vec<String> = Vec::new();
227 let mut i = 0usize;
228 if let Some(line) = source_lines.first() {
230 if line.starts_with("#!") {
231 i = 1;
232 }
233 }
234 while i < source_lines.len() {
235 let line = source_lines[i];
236 let trimmed = line.trim_start();
237 if let Some(rest) = trimmed.strip_prefix("## ") {
238 collected.push(rest.to_string());
239 } else if trimmed == "##" {
240 collected.push(String::new());
241 } else if trimmed.is_empty() {
242 if !collected.is_empty() {
244 break;
245 }
246 } else {
247 break;
248 }
249 i += 1;
250 }
251 if collected.is_empty() {
252 None
253 } else {
254 Some(collected.join("\n"))
255 }
256}
257
258fn format_sub_signature(name: &str, params: &[SubSigParam]) -> String {
262 if params.is_empty() {
263 return name.to_string();
264 }
265 let parts: Vec<String> = params
266 .iter()
267 .map(|p| match p {
268 SubSigParam::Scalar(n, _, _) => format!("${}", n),
269 SubSigParam::Array(n, _) => format!("@{}", n),
270 SubSigParam::Hash(n, _) => format!("%{}", n),
271 SubSigParam::ArrayDestruct(_) => "[…]".to_string(),
272 SubSigParam::HashDestruct(_) => "{…}".to_string(),
273 })
274 .collect();
275 format!("{}({})", name, parts.join(", "))
276}
277
278fn extract_constant_names(imports: &[crate::ast::Expr]) -> Vec<String> {
281 let mut names: Vec<String> = Vec::new();
282 for imp in imports {
283 match &imp.kind {
284 crate::ast::ExprKind::List(items) => {
285 let mut i = 0;
286 while i + 1 < items.len() {
287 if let Some(n) = constant_name_of(&items[i]) {
288 names.push(n);
289 }
290 i += 2;
291 }
292 }
293 crate::ast::ExprKind::HashRef(pairs) => {
294 for (k, _) in pairs {
295 if let Some(n) = constant_name_of(k) {
296 names.push(n);
297 }
298 }
299 }
300 _ => {
301 if let Some(n) = constant_name_of(imp) {
302 names.push(n);
303 }
304 }
305 }
306 }
307 names
308}
309
310fn constant_name_of(e: &crate::ast::Expr) -> Option<String> {
311 match &e.kind {
312 crate::ast::ExprKind::String(s) => Some(s.clone()),
313 crate::ast::ExprKind::Bareword(s) => Some(s.clone()),
314 _ => None,
315 }
316}