1mod frontmatter;
2mod page;
3mod post;
4
5pub use page::Page;
6pub use post::{ContentType, Post};
7
8#[cfg(test)]
10pub use frontmatter::Frontmatter;
11
12use crate::config::PathsConfig;
13use anyhow::Result;
14use ignore::WalkBuilder;
15use log::{debug, trace};
16use std::collections::HashMap;
17use std::path::Path;
18use walkdir::WalkDir;
19
20#[derive(Debug)]
22pub struct Section {
23 pub name: String,
24 pub posts: Vec<Post>,
25}
26
27#[derive(Debug)]
29pub struct Content {
30 pub home: Option<Page>,
31 pub sections: HashMap<String, Section>,
32}
33
34pub fn discover_content(paths: &PathsConfig, base_dir: Option<&Path>) -> Result<Content> {
37 debug!("Discovering content from {:?}", paths.content);
38 let content_path = Path::new(&paths.content);
39 let content_dir = if let Some(base) = base_dir {
40 if content_path.is_absolute() {
41 content_path.to_path_buf()
42 } else {
43 base.join(content_path)
44 }
45 } else {
46 content_path.to_path_buf()
47 };
48 trace!("Content directory resolved to: {:?}", content_dir);
49
50 let mut excluded: Vec<&str> = vec![&paths.styles, &paths.static_files, &paths.templates];
52 excluded.extend(paths.exclude.iter().map(|s| s.as_str()));
53 trace!("Excluded directories: {:?}", excluded);
54
55 let home_path = content_dir.join(&paths.home);
57 let home = if home_path.exists() {
58 trace!("Loading home page from {:?}", home_path);
59 Some(Page::from_file(&home_path)?)
60 } else {
61 trace!("No home page found at {:?}", home_path);
62 None
63 };
64
65 let mut sections = HashMap::new();
67
68 let section_paths: Vec<_> = if paths.respect_gitignore {
70 WalkBuilder::new(content_dir)
71 .max_depth(Some(1))
72 .hidden(false) .build()
74 .filter_map(|e| e.ok())
75 .filter(|e| e.depth() == 1 && e.path().is_dir())
76 .map(|e| e.into_path())
77 .collect()
78 } else {
79 WalkDir::new(content_dir)
80 .min_depth(1)
81 .max_depth(1)
82 .into_iter()
83 .filter_map(|e| e.ok())
84 .filter(|e| e.path().is_dir())
85 .map(|e| e.into_path())
86 .collect()
87 };
88
89 for path in section_paths {
91 process_section(&path, &excluded, &mut sections, paths)?;
92 }
93
94 debug!(
95 "Content discovery complete: {} sections found",
96 sections.len()
97 );
98 Ok(Content { home, sections })
99}
100
101fn process_section(
103 path: &Path,
104 excluded: &[&str],
105 sections: &mut HashMap<String, Section>,
106 paths: &PathsConfig,
107) -> Result<()> {
108 let section_name = path
109 .file_name()
110 .and_then(|n| n.to_str())
111 .unwrap_or("")
112 .to_string();
113
114 if excluded
116 .iter()
117 .any(|ex| section_name == *ex || path.ends_with(ex))
118 {
119 trace!("Skipping excluded section: {}", section_name);
120 return Ok(());
121 }
122
123 trace!("Processing section: {}", section_name);
124
125 let post_paths: Vec<_> = if paths.respect_gitignore {
127 WalkBuilder::new(path)
128 .max_depth(Some(1))
129 .hidden(false)
130 .build()
131 .filter_map(|e| e.ok())
132 .filter(|e| {
133 e.depth() == 1
134 && e.path()
135 .extension()
136 .is_some_and(|ext| ext == "md" || ext == "html" || ext == "htm")
137 })
138 .map(|e| e.into_path())
139 .collect()
140 } else {
141 WalkDir::new(path)
142 .min_depth(1)
143 .max_depth(1)
144 .into_iter()
145 .filter_map(|e| e.ok())
146 .filter(|e| {
147 e.path()
148 .extension()
149 .is_some_and(|ext| ext == "md" || ext == "html" || ext == "htm")
150 })
151 .map(|e| e.into_path())
152 .collect()
153 };
154
155 let mut posts = Vec::new();
157 for post_path in post_paths {
158 let post = Post::from_file_with_section(&post_path, §ion_name)?;
159 if !post.frontmatter.draft.unwrap_or(false) {
160 posts.push(post);
161 }
162 }
163
164 posts.sort_by(|a, b| b.frontmatter.date.cmp(&a.frontmatter.date));
166
167 if !posts.is_empty() {
168 debug!("Section '{}': {} posts loaded", section_name, posts.len());
169 sections.insert(
170 section_name.clone(),
171 Section {
172 name: section_name,
173 posts,
174 },
175 );
176 } else {
177 trace!("Section '{}': no posts found", section_name);
178 }
179
180 Ok(())
181}