gear_mesh_core/
docs.rs

1//! ドキュメントコメント処理
2//!
3//! Rustのdocコメントを解析し、JSDoc形式に変換します。
4
5use serde::{Deserialize, Serialize};
6
7/// ドキュメントコメント
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DocComment {
10    /// メインの説明文
11    pub summary: String,
12    /// 詳細な説明
13    pub description: Option<String>,
14    /// @example セクション
15    pub examples: Vec<String>,
16    /// その他のセクション
17    pub sections: Vec<DocSection>,
18}
19
20impl DocComment {
21    /// 空のドキュメントコメント
22    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    /// 単純なサマリーのみのドキュメント
32    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    /// Rustのdocコメントからパース
42    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            // コードブロックの処理
57            if trimmed.starts_with("```") {
58                if in_code_block {
59                    // コードブロック終了
60                    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                    // コードブロック開始
69                    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            // セクションヘッダの処理
83            if let Some(stripped) = trimmed.strip_prefix("# ") {
84                // 前のセクションを保存
85                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            // 空行はサマリーの終わり
100            if trimmed.is_empty() && parsing_summary && !summary.is_empty() {
101                parsing_summary = false;
102                continue;
103            }
104
105            // 内容の追加
106            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        // 最後のセクションを保存
125        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    /// JSDoc形式に変換
148    pub fn to_jsdoc(&self) -> String {
149        let mut lines = Vec::new();
150        lines.push("/**".to_string());
151
152        // サマリー
153        if !self.summary.is_empty() {
154            lines.push(format!(" * {}", self.summary));
155        }
156
157        // 説明
158        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        // サンプル
166        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        // その他のセクション
177        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    /// 単一行JSDocコメント
190    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/// ドキュメントセクション
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct DocSection {
202    /// セクション名 (e.g., "Arguments", "Returns")
203    pub name: String,
204    /// セクション内容
205    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// 追加テストをインクルード
249#[cfg(test)]
250#[path = "docs_tests.rs"]
251mod docs_additional_tests;