ox_content_docs/
generator.rs1use std::path::Path;
4
5use crate::config::DocsConfig;
6use crate::extractor::{DocExtractor, DocItem, ExtractResult};
7
8use thiserror::Error;
9
10pub type GenerateResult<T> = Result<T, GenerateError>;
12
13#[derive(Debug, Error)]
15pub enum GenerateError {
16 #[error("IO error: {0}")]
18 Io(#[from] std::io::Error),
19
20 #[error("Extraction error: {0}")]
22 Extract(#[from] crate::extractor::ExtractError),
23
24 #[error("Template error: {0}")]
26 Template(String),
27}
28
29pub struct DocsGenerator {
31 config: DocsConfig,
32 extractor: DocExtractor,
33}
34
35impl DocsGenerator {
36 #[must_use]
38 pub fn new(config: DocsConfig) -> Self {
39 let extractor = DocExtractor::with_private(config.document_private);
40 Self { config, extractor }
41 }
42
43 #[must_use]
45 pub fn config(&self) -> &DocsConfig {
46 &self.config
47 }
48
49 pub fn generate(&self) -> GenerateResult<()> {
51 let items = self.extract_all()?;
52 self.render(&items)?;
53 Ok(())
54 }
55
56 pub fn extract_all(&self) -> ExtractResult<Vec<DocItem>> {
58 let mut all_items = Vec::new();
59
60 for src_dir in &self.config.src_dirs {
61 let items = self.extract_dir(Path::new(src_dir))?;
62 all_items.extend(items);
63 }
64
65 Ok(all_items)
66 }
67
68 fn extract_dir(&self, dir: &Path) -> ExtractResult<Vec<DocItem>> {
70 let mut items = Vec::new();
71
72 if !dir.is_dir() {
73 return Ok(items);
74 }
75
76 for entry in std::fs::read_dir(dir)? {
77 let entry = entry?;
78 let path = entry.path();
79
80 if path.is_dir() {
81 items.extend(self.extract_dir(&path)?);
82 } else if self.should_include(&path) {
83 if let Ok(file_items) = self.extractor.extract_file(&path) {
84 items.extend(file_items);
85 }
86 }
87 }
88
89 Ok(items)
90 }
91
92 fn should_include(&self, path: &Path) -> bool {
94 let path_str = path.to_string_lossy();
95
96 for pattern in &self.config.exclude {
98 if glob_match(pattern, &path_str) {
99 return false;
100 }
101 }
102
103 for pattern in &self.config.include {
105 if glob_match(pattern, &path_str) {
106 return true;
107 }
108 }
109
110 false
111 }
112
113 fn render(&self, items: &[DocItem]) -> GenerateResult<()> {
115 let out_dir = Path::new(&self.config.out_dir);
116 std::fs::create_dir_all(out_dir)?;
117
118 if self.config.json {
119 let json = serde_json::to_string_pretty(items)
120 .map_err(|e| GenerateError::Template(e.to_string()))?;
121 std::fs::write(out_dir.join("docs.json"), json)?;
122 }
123
124 Ok(())
128 }
129}
130
131fn glob_match(pattern: &str, path: &str) -> bool {
133 if pattern.contains("**") {
136 let parts: Vec<&str> = pattern.split("**").collect();
137 if parts.len() == 2 {
138 let prefix = parts[0].trim_end_matches('/');
139 let suffix = parts[1].trim_start_matches('/');
140
141 if !suffix.is_empty() {
143 if let Some(ext) = suffix.strip_prefix('*') {
145 return path.ends_with(ext);
146 }
147 return path.ends_with(suffix);
148 }
149 if !prefix.is_empty() && !path.starts_with(prefix) {
150 return false;
151 }
152 return true;
153 }
154 }
155
156 if pattern.contains('*') {
157 let parts: Vec<&str> = pattern.split('*').collect();
158 if parts.len() == 2 {
159 return path.starts_with(parts[0]) && path.ends_with(parts[1]);
160 }
161 }
162
163 pattern == path
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_glob_match() {
172 assert!(glob_match("**/*.ts", "src/foo/bar.ts"));
174 assert!(glob_match("**/*.ts", "bar.ts"));
175 assert!(!glob_match("**/*.ts", "bar.js"));
176 assert!(glob_match("*.ts", "foo.ts"));
178 assert!(glob_match("*.ts", "src/foo.ts"));
181 assert!(glob_match("foo.ts", "foo.ts"));
183 assert!(!glob_match("foo.ts", "bar.ts"));
184 }
185}