gen_mdbook_summary/
lib.rs1use std::{
2 collections::HashSet,
3 path::{Path, PathBuf},
4};
5
6use anyhow::{Context, bail};
7use log::info;
8
9fn escape_path(path: &str) -> String {
12 path.replace(' ', "%20")
14}
15
16#[derive(Debug)]
17pub struct SummaryItem {
18 name: String,
19 path: PathBuf, absolute_path: PathBuf, introduction: Option<String>,
22 chapters: Vec<SummaryItem>,
23}
24
25impl SummaryItem {
26 pub fn new(
27 dir: &str,
28 ignore: &Ignore,
29 base_dir: &Path,
30 output_file: Option<&Path>,
31 ) -> anyhow::Result<Self> {
32 info!("try to create SummaryItem from {}", dir);
33 let mut chapters = Vec::new();
34 let absolute_path = Path::new(dir).canonicalize()?;
35
36 let relative_path = absolute_path
38 .strip_prefix(base_dir)
39 .map(|p| p.to_path_buf())
40 .unwrap_or_else(|_| absolute_path.clone());
41
42 let relative_path = if relative_path.as_os_str().is_empty() {
44 PathBuf::from(".")
45 } else {
46 relative_path
47 };
48
49 let dir = absolute_path.display().to_string();
50 let meta = std::fs::metadata(&dir)?;
51 let name = Self::item_name_from_path_str(&dir)?.to_string();
52
53 if meta.is_file() {
54 return Ok(Self {
55 name,
56 path: relative_path,
57 absolute_path,
58 introduction: None,
59 chapters,
60 });
61 }
62
63 if !meta.is_dir() {
65 bail!("{} is neither a file nor a directory", dir);
66 }
67 info!("{dir}");
68 let mut introduction: Option<String> = None;
70 let names = vec!["README.md", "readme.md", "README", "readme"];
71 for readme_name in names {
73 let path = format!("{}/{}", dir, readme_name);
74 if Path::new(&path).exists() && !ignore.is_ignore(&path) {
75 let intro_absolute = Path::new(&path).canonicalize()?;
77 let intro_relative = intro_absolute
78 .strip_prefix(base_dir)
79 .map(|p| p.to_path_buf())
80 .unwrap_or_else(|_| intro_absolute.clone());
81
82 let intro_relative = if intro_relative.as_os_str().is_empty() {
84 PathBuf::from(".")
85 } else {
86 intro_relative
87 };
88
89 introduction = Some(intro_relative.to_string_lossy().to_string());
90 break;
91 }
92 }
93
94 for entry in std::fs::read_dir(&dir)? {
95 let entry = entry?;
96 let path = entry.path();
97 let path_str = &path.display().to_string();
98 info!("{}", path_str);
99
100 if let Some(output) = output_file {
102 let output_absolute = if output.is_absolute() {
104 output.to_path_buf()
105 } else {
106 std::env::current_dir().unwrap().join(output)
107 };
108
109 let entry_absolute = if path.is_absolute() {
111 path.to_path_buf()
112 } else {
113 std::env::current_dir().unwrap().join(&path)
114 };
115
116 if entry_absolute == output_absolute {
117 info!("path_str {} is output file, skip", path_str);
118 continue;
119 }
120 }
121
122 if ignore.is_ignore(path_str) {
123 info!("ignore {}", path_str);
124 continue;
125 }
126 if path_str.ends_with("readme.md") || path_str.ends_with("README.md") {
127 info!("path_str {} is readme.md,skip", path_str);
128 continue;
129 }
130 chapters.push(Self::new(path_str, ignore, base_dir, output_file)?);
131 }
132 let path = relative_path;
133 Ok(Self {
134 name,
135 path,
136 absolute_path,
137 introduction,
138 chapters,
139 })
140 }
141
142 pub fn sort(&mut self) {
143 self.chapters.sort_by(|a, b| a.name.cmp(&b.name));
144 for chapter in self.chapters.iter_mut() {
145 chapter.sort();
146 }
147 }
148
149 pub fn gen_summary(&self) -> anyhow::Result<String> {
150 let mut summary = String::new();
151 summary.push_str("# Summary\n\n");
152 if self.absolute_path.is_dir() {
153 for chapter in &self.chapters {
154 summary.push('\n');
155 summary.push_str(&chapter.item(0)?);
156 }
157 } else {
158 let path_str = self
159 .path
160 .to_str()
161 .with_context(|| format!("[{}:{}]", file!(), line!(),))?;
162 summary.push_str(&format!("- [{}]({})", self.name, path_str));
163 }
164 Ok(summary)
165 }
166
167 pub fn item(&self, depth: usize) -> anyhow::Result<String> {
168 let mut item = String::new();
169 for _ in 0..depth {
170 item.push('\t');
171 }
172 let path_str = self
173 .path
174 .to_str()
175 .with_context(|| format!("[{}:{}]", file!(), line!(),))?;
176
177 let encoded_path = escape_path(path_str);
179
180 if self.absolute_path.is_dir() {
181 if let Some(introduction) = &self.introduction {
182 let encoded_intro = escape_path(introduction);
183 item.push_str(format!("- [{}]({})", &self.name, encoded_intro).as_str());
184 } else {
185 item.push_str(format!("- [{}]()", &self.name).as_str());
186 }
187 for chapter in self.chapters.iter() {
188 item.push('\n');
189 item.push_str(&chapter.item(depth + 1)?);
190 }
191 } else {
192 item.push_str(&format!("- [{}]({})", &self.name, encoded_path));
193 }
194 info!("{item}");
195 Ok(item)
196 }
197
198 fn item_name_from_path_str(path_name: &str) -> anyhow::Result<String> {
199 let path = Path::new(path_name);
201 let name = path
202 .file_name()
203 .and_then(|n| n.to_str())
204 .ok_or(anyhow::anyhow!(
205 "[{}:{}:{}]invalid name: {}",
206 file!(),
207 line!(),
208 column!(),
209 path_name
210 ))?
211 .trim();
212
213 let name = if path.is_dir() {
215 name.to_string()
216 } else {
217 let name: Vec<&str> = name.split(".").collect();
218 name[..name.len() - 1].join(".")
219 };
220
221 Ok(name)
222 }
223}
224
225#[derive(Debug)]
226pub struct Ignore {
227 unignored: HashSet<String>,
228}
229
230impl Ignore {
231 pub fn new(dir: &str, ignore_file: &str) -> anyhow::Result<Self> {
232 use ignore::WalkBuilder;
233 let mut unignored = HashSet::new();
234 for result in WalkBuilder::new(dir)
235 .add_custom_ignore_filename(ignore_file)
236 .build()
237 {
238 let result = result?;
239 let path = result.path().canonicalize()?;
240 let path = path
241 .to_str()
242 .with_context(|| format!("[{}:{}]", file!(), line!(),))?;
243 unignored.insert(path.to_string());
244 }
245 Ok(Self { unignored })
246 }
247
248 pub fn is_ignore(&self, path: &str) -> bool {
249 !self.unignored.contains(path)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::SummaryItem;
256
257 #[test]
258 fn t_item_name() {
259 let temp_dir = std::env::temp_dir();
261 let temp_dir = temp_dir.join("test_item_name");
262 std::fs::create_dir_all(&temp_dir).unwrap();
263 std::env::set_current_dir(&temp_dir).unwrap();
264
265 fn test(p: &str, is_dir: bool, expect: &str) {
266 if is_dir {
267 std::fs::create_dir_all(p).unwrap();
268 }
269 let name = SummaryItem::item_name_from_path_str(p).unwrap();
270 if is_dir {
271 std::fs::remove_dir_all(p).unwrap();
272 }
273 assert_eq!(name, expect);
274 }
275
276 test("README.md", false, "README");
277 test("One.AA.md", false, "One.AA");
278 test("blog/Pro/readme.md", false, "readme");
279 }
280}