1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DocComment {
10 pub summary: String,
12 pub description: Option<String>,
14 pub examples: Vec<String>,
16 pub sections: Vec<DocSection>,
18}
19
20impl DocComment {
21 pub fn empty() -> Self {
23 Self {
24 summary: String::new(),
25 description: None,
26 examples: Vec::new(),
27 sections: Vec::new(),
28 }
29 }
30
31 pub fn summary(text: impl Into<String>) -> Self {
33 Self {
34 summary: text.into(),
35 description: None,
36 examples: Vec::new(),
37 sections: Vec::new(),
38 }
39 }
40
41 pub fn parse(doc: &str) -> Self {
43 let lines: Vec<&str> = doc.lines().collect();
44 let mut summary = String::new();
45 let mut description = String::new();
46 let mut examples = Vec::new();
47 let mut sections = Vec::new();
48 let mut current_section: Option<(String, String)> = None;
49 let mut in_code_block = false;
50 let mut code_block_content = String::new();
51 let mut parsing_summary = true;
52
53 for line in lines {
54 let trimmed = line.trim();
55
56 if trimmed.starts_with("```") {
58 if in_code_block {
59 in_code_block = false;
61 if let Some((ref section_name, _)) = current_section
62 && (section_name == "Examples" || section_name == "Example")
63 {
64 examples.push(code_block_content.clone());
65 }
66 code_block_content.clear();
67 } else {
68 in_code_block = true;
70 }
71 continue;
72 }
73
74 if in_code_block {
75 if !code_block_content.is_empty() {
76 code_block_content.push('\n');
77 }
78 code_block_content.push_str(line);
79 continue;
80 }
81
82 if let Some(stripped) = trimmed.strip_prefix("# ") {
84 if let Some((name, content)) = current_section.take()
86 && name != "Examples"
87 && name != "Example"
88 {
89 sections.push(DocSection {
90 name,
91 content: content.trim().to_string(),
92 });
93 }
94 current_section = Some((stripped.to_string(), String::new()));
95 parsing_summary = false;
96 continue;
97 }
98
99 if trimmed.is_empty() && parsing_summary && !summary.is_empty() {
101 parsing_summary = false;
102 continue;
103 }
104
105 if let Some((_, ref mut content)) = current_section {
107 if !content.is_empty() {
108 content.push('\n');
109 }
110 content.push_str(trimmed);
111 } else if parsing_summary {
112 if !summary.is_empty() {
113 summary.push(' ');
114 }
115 summary.push_str(trimmed);
116 } else {
117 if !description.is_empty() {
118 description.push('\n');
119 }
120 description.push_str(trimmed);
121 }
122 }
123
124 if let Some((name, content)) = current_section
126 && name != "Examples"
127 && name != "Example"
128 {
129 sections.push(DocSection {
130 name,
131 content: content.trim().to_string(),
132 });
133 }
134
135 Self {
136 summary: summary.trim().to_string(),
137 description: if description.is_empty() {
138 None
139 } else {
140 Some(description.trim().to_string())
141 },
142 examples,
143 sections,
144 }
145 }
146
147 pub fn to_jsdoc(&self) -> String {
149 let mut lines = Vec::new();
150 lines.push("/**".to_string());
151
152 if !self.summary.is_empty() {
154 lines.push(format!(" * {}", self.summary));
155 }
156
157 if let Some(ref desc) = self.description {
159 lines.push(" *".to_string());
160 for line in desc.lines() {
161 lines.push(format!(" * {}", line));
162 }
163 }
164
165 for example in &self.examples {
167 lines.push(" *".to_string());
168 lines.push(" * @example".to_string());
169 lines.push(" * ```typescript".to_string());
170 for line in example.lines() {
171 lines.push(format!(" * {}", line));
172 }
173 lines.push(" * ```".to_string());
174 }
175
176 for section in &self.sections {
178 lines.push(" *".to_string());
179 lines.push(format!(" * @{}", section.name.to_lowercase()));
180 for line in section.content.lines() {
181 lines.push(format!(" * {}", line));
182 }
183 }
184
185 lines.push(" */".to_string());
186 lines.join("\n")
187 }
188
189 pub fn to_inline_jsdoc(&self) -> String {
191 if self.summary.is_empty() {
192 String::new()
193 } else {
194 format!("/** {} */", self.summary)
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct DocSection {
202 pub name: String,
204 pub content: String,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_parse_simple() {
214 let doc = "This is a simple description.";
215 let parsed = DocComment::parse(doc);
216 assert_eq!(parsed.summary, "This is a simple description.");
217 }
218
219 #[test]
220 fn test_parse_with_example() {
221 let doc = r#"User information struct
222
223# Examples
224
225```
226let user = User::new("Alice");
227```
228"#;
229 let parsed = DocComment::parse(doc);
230 assert_eq!(parsed.summary, "User information struct");
231 assert_eq!(parsed.examples.len(), 1);
232 }
233
234 #[test]
235 fn test_to_jsdoc() {
236 let doc = DocComment {
237 summary: "A user object".to_string(),
238 description: None,
239 examples: vec!["const user = new User();".to_string()],
240 sections: vec![],
241 };
242 let jsdoc = doc.to_jsdoc();
243 assert!(jsdoc.contains("A user object"));
244 assert!(jsdoc.contains("@example"));
245 }
246}
247
248#[cfg(test)]
250#[path = "docs_tests.rs"]
251mod docs_additional_tests;