1
2use crate::common::{get_canonical_name, get_alias_string, indent, pluralize};
3
4use std::fmt::{self, Write};
5
6use clap::builder::PossibleValue;
7
8
9#[non_exhaustive]
10pub struct AsciiDocOptions {
11 title: Option<String>,
12 show_table_of_contents: bool,
13 show_aliases: bool,
14}
15
16impl AsciiDocOptions {
17 pub fn new() -> Self {
18 return Self {
19 title: None,
20 show_table_of_contents: true,
21 show_aliases: true,
22 };
23 }
24
25 pub fn title(mut self, title: String) -> Self {
27 self.title = Some(title);
28
29 return self;
30 }
31
32 pub fn show_table_of_contents(mut self, show: bool) -> Self {
34 self.show_table_of_contents = show;
35
36 return self;
37 }
38
39 pub fn show_aliases(mut self, show: bool) -> Self {
41 self.show_aliases = show;
42
43 return self;
44 }
45}
46
47impl Default for AsciiDocOptions {
48 fn default() -> Self {
49 return Self::new();
50 }
51}
52
53pub fn help_asciidoc<C: clap::CommandFactory>() -> String {
59 let command = C::command();
60
61 help_asciidoc_command(&command)
62}
63
64pub fn help_asciidoc_custom<C: clap::CommandFactory>(
66 options: &AsciiDocOptions,
67) -> String {
68 let command = C::command();
69
70 return help_asciidoc_command_custom(&command, options);
71}
72
73pub fn help_asciidoc_command(command: &clap::Command) -> String {
75 return help_asciidoc_command_custom(command, &Default::default());
76}
77
78pub fn help_asciidoc_command_custom(
80 command: &clap::Command,
81 options: &AsciiDocOptions,
82) -> String {
83 let mut buffer = String::with_capacity(100);
84
85 write_help_asciidoc(&mut buffer, &command, options);
86
87 buffer
88}
89
90pub fn print_help_asciidoc<C: clap::CommandFactory>() {
98 let command = C::command();
99
100 let mut buffer = String::with_capacity(100);
101
102 write_help_asciidoc(&mut buffer, &command, &Default::default());
103
104 println!("{}", buffer);
105}
106
107fn write_help_asciidoc(
108 buffer: &mut String,
109 command: &clap::Command,
110 options: &AsciiDocOptions,
111) {
112 let title_name = get_canonical_name(command);
117
118 let title = match options.title {
119 Some(ref title) => title.to_owned(),
120 None => format!("Command-Line Help for `{title_name}`"),
121 };
122 writeln!(buffer, "= {title}\n",).unwrap();
123
124 writeln!(
125 buffer,
126 "This document contains the help content for the `{}` command-line program.\n", title_name
127 ).unwrap();
128
129 if options.show_table_of_contents {
138 writeln!(buffer, "*Command Overview:*\n").unwrap();
139
140 build_table_of_contents_asciidoc(buffer, Vec::new(), command, 0)
141 .unwrap();
142
143 write!(buffer, "\n").unwrap();
144 }
145
146 build_command_asciidoc(buffer, Vec::new(), command, 0, options).unwrap();
151}
152
153fn build_command_asciidoc(
154 buffer: &mut String,
155 parent_command_path: Vec<String>,
157 command: &clap::Command,
158 depth: usize,
159 options: &AsciiDocOptions,
160) -> std::fmt::Result {
161 if command.is_hide_set() {
164 return Ok(());
165 }
166
167 let title_name = get_canonical_name(command);
168
169 let command_path = {
171 let mut command_path = parent_command_path.clone();
172 command_path.push(title_name);
173 command_path
174 };
175
176 writeln!(buffer, "[[{}]]", command_path.join("-"))?;
190 writeln!(buffer, "== `{}`\n", command_path.join(" "))?;
191
192 if let Some(long_about) = command.get_long_about() {
193 writeln!(buffer, "{}\n", long_about)?;
194 } else if let Some(about) = command.get_about() {
195 writeln!(buffer, "{}\n", about)?;
196 }
197
198 if let Some(help) = command.get_before_long_help() {
199 writeln!(buffer, "{}\n", help)?;
200 } else if let Some(help) = command.get_before_help() {
201 writeln!(buffer, "{}\n", help)?;
202 }
203
204 writeln!(
205 buffer,
206 "*Usage:* `{}{}`\n",
207 if parent_command_path.is_empty() {
208 String::new()
209 } else {
210 let mut s = parent_command_path.join(" ");
211 s.push_str(" ");
212 s
213 },
214 command
215 .clone()
216 .render_usage()
217 .to_string()
218 .replace("Usage: ", "")
219 )?;
220
221 if options.show_aliases {
222 let aliases = command.get_visible_aliases().collect::<Vec<&str>>();
223 if let Some(aliases_str) = get_alias_string(&aliases) {
224 writeln!(
225 buffer,
226 "*{}:* {aliases_str}\n",
227 pluralize(aliases.len(), "Command Alias", "Command Aliases")
228 )?;
229 }
230 }
231
232 if let Some(help) = command.get_after_long_help() {
233 writeln!(buffer, "{}\n", help)?;
234 } else if let Some(help) = command.get_after_help() {
235 writeln!(buffer, "{}\n", help)?;
236 }
237
238 if command.get_subcommands().next().is_some() {
243 writeln!(buffer, "=== *Subcommands:*\n")?;
244
245 for subcommand in command.get_subcommands() {
246 if subcommand.is_hide_set() {
247 continue;
248 }
249
250 let title_name = get_canonical_name(subcommand);
251
252 let about = match subcommand.get_about() {
253 Some(about) => about.to_string(),
254 None => String::new(),
255 };
256
257 writeln!(buffer, "* `{title_name}` — {about}",)?;
258 }
259
260 write!(buffer, "\n")?;
261 }
262
263 if command.get_positionals().next().is_some() {
268 writeln!(buffer, "=== *Arguments:*\n")?;
269
270 for pos_arg in command.get_positionals() {
271 write_arg_asciidoc(buffer, pos_arg)?;
272 }
273
274 write!(buffer, "\n")?;
275 }
276
277 let non_pos: Vec<_> = command
282 .get_arguments()
283 .filter(|arg| !arg.is_positional() && !arg.is_hide_set())
284 .collect();
285
286 if !non_pos.is_empty() {
287 writeln!(buffer, "=== *Options:*\n")?;
288
289 for arg in non_pos {
290 write_arg_asciidoc(buffer, arg)?;
291 }
292
293 write!(buffer, "\n")?;
294 }
295
296 write!(buffer, "\n\n")?;
303
304 for subcommand in command.get_subcommands() {
305 build_command_asciidoc(
306 buffer,
307 command_path.clone(),
308 subcommand,
309 depth + 1,
310 options,
311 )?;
312 }
313
314 Ok(())
315}
316
317fn write_arg_asciidoc(buffer: &mut String, arg: &clap::Arg) -> fmt::Result {
318 write!(buffer, "* ")?;
320
321 let value_name: String = match arg.get_value_names() {
322 Some([name, ..]) => name.as_str().to_owned(),
324 Some([]) => unreachable!(
325 "clap Arg::get_value_names() returned Some(..) of empty list"
326 ),
327 None => arg.get_id().to_string().to_ascii_uppercase(),
328 };
329
330 match (arg.get_short(), arg.get_long()) {
331 (Some(short), Some(long)) => {
332 if arg.get_action().takes_values() {
333 write!(buffer, "`-{short}`, `--{long} <{value_name}>`")?
334 } else {
335 write!(buffer, "`-{short}`, `--{long}`")?
336 }
337 },
338 (Some(short), None) => {
339 if arg.get_action().takes_values() {
340 write!(buffer, "`-{short} <{value_name}>`")?
341 } else {
342 write!(buffer, "`-{short}`")?
343 }
344 },
345 (None, Some(long)) => {
346 if arg.get_action().takes_values() {
347 write!(buffer, "`--{} <{value_name}>`", long)?
348 } else {
349 write!(buffer, "`--{}`", long)?
350 }
351 },
352 (None, None) => {
353 debug_assert!(arg.is_positional(), "unexpected non-positional Arg with neither short nor long name: {arg:?}");
354
355 write!(buffer, "`<{value_name}>`",)?;
356 },
357 }
358
359 if let Some(aliases) = arg.get_visible_aliases().as_deref() {
360 if let Some(aliases_str) = get_alias_string(aliases) {
361 write!(
362 buffer,
363 " [{}: {aliases_str}]",
364 pluralize(aliases.len(), "alias", "aliases")
365 )?;
366 }
367 }
368
369 if let Some(help) = arg.get_long_help() {
370 buffer.push_str(&indent(&help.to_string(), " — ", " "))
372 } else if let Some(short_help) = arg.get_help() {
373 writeln!(buffer, " — {short_help}")?;
374 } else {
375 writeln!(buffer)?;
376 }
377
378 if !arg.get_default_values().is_empty() {
383 let default_values: String = arg
384 .get_default_values()
385 .iter()
386 .map(|value| format!("`{}`", value.to_string_lossy()))
387 .collect::<Vec<String>>()
388 .join(", ");
389
390 if arg.get_default_values().len() > 1 {
391 writeln!(buffer, "+\nDefault values: {default_values}")?;
393 } else {
394 writeln!(buffer, "+\nDefault value: {default_values}")?;
396 }
397 }
398
399 let possible_values: Vec<PossibleValue> = arg
404 .get_possible_values()
405 .into_iter()
406 .filter(|pv| !pv.is_hide_set())
407 .collect();
408
409 if !possible_values.is_empty()
412 && !matches!(arg.get_action(), clap::ArgAction::SetTrue)
413 {
414 let any_have_help: bool =
415 possible_values.iter().any(|pv| pv.get_help().is_some());
416
417 if any_have_help {
418 let text: String = possible_values
430 .iter()
431 .map(|pv| match pv.get_help() {
432 Some(help) => {
433 format!(" - `{}`:\n {}\n", pv.get_name(), help)
434 },
435 None => format!(" - `{}`\n", pv.get_name()),
436 })
437 .collect::<Vec<String>>()
438 .join("");
439
440 writeln!(buffer, "+\nPossible values:\n+\n{text}")?;
441 } else {
442 let text: String = possible_values
445 .iter()
446 .map(|pv| format!("`{}`", pv.get_name()))
448 .collect::<Vec<String>>()
449 .join(", ");
450
451 writeln!(buffer, "+\nPossible values: {text}\n")?;
452 }
453 }
454
455 Ok(())
456}
457
458fn build_table_of_contents_asciidoc(
459 buffer: &mut String,
460 parent_command_path: Vec<String>,
462 command: &clap::Command,
463 depth: usize,
464) -> std::fmt::Result {
465 if command.is_hide_set() {
468 return Ok(());
469 }
470
471 let title_name = get_canonical_name(command);
472
473 let command_path = {
475 let mut command_path = parent_command_path;
476 command_path.push(title_name);
477 command_path
478 };
479
480 writeln!(
481 buffer,
482 "* <<{},`{}`>>",
483 command_path.join("-"),
484 command_path.join(" "),
485 )?;
486
487 for subcommand in command.get_subcommands() {
492 build_table_of_contents_asciidoc(
493 buffer,
494 command_path.clone(),
495 subcommand,
496 depth + 1,
497 )?;
498 }
499
500 Ok(())
501}