1use std::{collections::VecDeque, mem, str::FromStr};
2
3const PREFIX: &str = "## ";
4
5fn discard_doc(lines: &mut VecDeque<&str>) {
6 while let Some(line) = lines.front() {
7 if !line.starts_with(PREFIX) && *line != "##" {
8 return;
9 }
10 lines.pop_front().unwrap();
11 }
12}
13
14fn discard_non_doc(lines: &mut VecDeque<&str>) {
15 while let Some(line) = lines.front() {
16 if line.starts_with(PREFIX) {
17 return;
18 }
19 lines.pop_front().unwrap();
20 }
21}
22
23#[derive(Debug)]
24pub struct Document {
25 modules: Vec<Module>,
26}
27
28impl FromStr for Document {
29 type Err = &'static str;
30
31 fn from_str(s: &str) -> Result<Self, Self::Err> {
32 let mut lines = s.lines().collect::<VecDeque<_>>();
33 Ok(Self {
34 modules: Module::take_all(&mut lines),
35 })
36 }
37}
38
39impl Document {
40 fn push_targets_help(s: &mut String, targets: &[Target]) {
41 let Some(width) = targets.iter().map(|t| t.name.len()).max() else {
42 return;
43 };
44 for target in targets {
45 s.push_str(&format!(
46 "{:>width$}: {}\n",
47 target.name,
48 target.summary,
49 width = width + 1
50 ));
51 }
52 }
53
54 pub fn help(&self) -> String {
56 let Self { modules } = self;
57 let mut s = String::new();
58 let mut first = true;
59 for module in modules {
60 if !module.targets.is_empty() {
61 if !mem::replace(&mut first, false) {
62 s.push('\n');
63 }
64 s.push_str(&module.name);
65 s.push_str(":\n");
66 Self::push_targets_help(&mut s, &module.targets);
67 }
68 for submodule in &module.submodules {
69 if !submodule.targets.is_empty() {
70 if !mem::replace(&mut first, false) {
71 s.push('\n');
72 }
73 s.push_str(&submodule.name);
74 s.push_str(":\n");
75 Self::push_targets_help(&mut s, &submodule.targets);
76 }
77 }
78 }
79 s
80 }
81}
82
83#[derive(Debug)]
84struct Module {
85 name: String,
86 targets: Vec<Target>,
87 submodules: Vec<Submodule>,
88}
89
90impl Module {
91 fn take_all(lines: &mut VecDeque<&str>) -> Vec<Self> {
92 let mut modules = Vec::new();
93 while let Some(module) = Self::take_one(lines) {
94 modules.push(module);
95 }
96 modules
97 }
98
99 fn take_one(lines: &mut VecDeque<&str>) -> Option<Self> {
100 discard_non_doc(lines);
101 let name = lines.front()?.strip_prefix(PREFIX)?;
102 let underscore = lines.get(1)?.strip_prefix(PREFIX)?;
103 if !underscore.chars().all(|c| c == '=') {
104 debug_assert_eq!(underscore.len(), name.len());
105 return None;
106 }
107 discard_doc(lines);
108
109 let targets = Target::take_all(lines);
110 let submodules = Submodule::take_all(lines);
111
112 Some(Self {
113 name: name.to_string(),
114 targets,
115 submodules,
116 })
117 }
118}
119
120#[derive(Debug)]
121struct Submodule {
122 name: String,
123 targets: Vec<Target>,
124}
125
126impl Submodule {
127 fn take_all(lines: &mut VecDeque<&str>) -> Vec<Self> {
128 let mut submodules = Vec::new();
129 while let Some(submodule) = Self::take_one(lines) {
130 submodules.push(submodule);
131 }
132 submodules
133 }
134
135 fn take_one(lines: &mut VecDeque<&str>) -> Option<Self> {
136 discard_non_doc(lines);
137 let name = lines.front()?.strip_prefix(PREFIX)?;
138 let underscore = lines.get(1)?.strip_prefix(PREFIX)?;
139 if !underscore.chars().all(|c| c == '-') {
140 debug_assert_eq!(underscore.len(), name.len());
141 return None;
142 }
143 discard_doc(lines);
144
145 let targets = Target::take_all(lines);
146
147 Some(Self {
148 name: name.to_string(),
149 targets,
150 })
151 }
152}
153
154#[derive(Debug)]
155struct Target {
156 name: String,
157 summary: String,
158}
159
160impl Target {
161 fn title_case(s: &str) -> String {
162 let s = s.to_string();
163 let mut chars = s.chars().collect::<Vec<_>>();
164 if let Some(c) = chars.get_mut(0) {
165 c.make_ascii_uppercase();
166 }
167 chars.into_iter().collect()
168 }
169
170 fn take_one(lines: &mut VecDeque<&str>) -> Option<Self> {
171 discard_non_doc(lines);
172 let summary = lines.front()?.strip_prefix(PREFIX)?;
173 if let Some(underscore) = lines.get(1).and_then(|s| s.strip_prefix(PREFIX)) {
174 if (underscore.chars().all(|c| c == '=') || underscore.chars().all(|c| c == '-'))
175 && !underscore.is_empty()
176 {
177 return None;
178 }
179 }
180 discard_doc(lines);
181 let name = lines.front()?.split_once(':')?.0.to_string();
182
183 Some(Target {
184 summary: if summary == "_" {
185 Self::title_case(&name).replace('_', " ")
186 } else {
187 summary.to_string()
188 },
189 name,
190 })
191 }
192
193 fn take_all(lines: &mut VecDeque<&str>) -> Vec<Self> {
194 let mut targets = Vec::new();
195 while let Some(target) = Target::take_one(lines) {
196 targets.push(target);
197 }
198 targets
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use crate::Document;
205
206 #[test]
207 fn output_is_correct_for_this_project() {
208 let expected = r#"Verbs:
209 help: Print help message
210
211Checks:
212 check_all: Run all checks
213 check_format: Check format
214 check_lint: Check lint
215 check_docs: Check that documentation can be built
216 check_tests: Check that unit tests pass
217
218Fixes:
219 fix_format: Fix format
220 fix_lint: Fix lint
221"#;
222 let doc: Document = std::fs::read_to_string("Makefile")
223 .unwrap()
224 .parse()
225 .unwrap();
226 let actual = doc.help();
227 assert_eq!(actual, expected);
228 }
229}