1+ use itertools:: Itertools ;
12use std:: borrow:: Cow ;
23
3- use itertools :: Itertools ;
4+ use clap :: { Arg , Command , CommandFactory } ;
45
5- pub fn render ( mut command : clap:: Command ) -> String {
6+ use crate :: args:: Args ;
7+
8+ /// Renders a Markdown reference page for `pdu`'s CLI.
9+ ///
10+ /// The output includes:
11+ /// - A `# Usage` section with a `sh` code block
12+ /// - A `## Arguments` section listing positional arguments
13+ /// - A `## Options` section with a subsection per option flag
14+ /// - A `## Examples` section parsed from `after_long_help`
15+ pub fn render_usage_md ( ) -> String {
16+ let mut command: Command = Args :: command ( ) ;
617 let mut out = String :: new ( ) ;
718
819 // Usage section
9- let usage = command. render_usage ( ) . to_string ( ) ;
10- if let Some ( rest) = usage. strip_prefix ( "Usage: " ) {
11- out. push_str ( "# Usage\n \n ```sh\n " ) ;
12- out. push_str ( rest. trim ( ) ) ;
13- out. push_str ( "\n ```\n \n " ) ;
14- }
20+ let usage_str = command. render_usage ( ) . to_string ( ) ;
21+ let usage_cmd = usage_str
22+ . trim ( )
23+ . splitn ( 2 , ':' )
24+ . nth ( 1 )
25+ . map ( str:: trim)
26+ . unwrap_or ( "" ) ;
27+ out. push_str ( "# Usage\n \n ```sh\n " ) ;
28+ out. push_str ( usage_cmd) ;
29+ out. push_str ( "\n ```\n \n " ) ;
1530
1631 // Arguments section – positional, non-hidden args
17- let positional_args: Vec < clap:: Arg > = command
18- . get_arguments ( )
19- . filter ( |a| a. is_positional ( ) && !a. is_hide_set ( ) && !a. is_hide_long_help_set ( ) )
20- . cloned ( )
21- . collect ( ) ;
22- if !positional_args. is_empty ( ) {
23- out. push_str ( "## Arguments\n \n " ) ;
24- for arg in & positional_args {
25- render_argument ( arg, & mut out) ;
32+ let mut arguments_heading_written = false ;
33+ for arg in command. get_arguments ( ) {
34+ if !arg. is_positional ( ) || arg. is_hide_set ( ) || arg. is_hide_long_help_set ( ) {
35+ continue ;
36+ }
37+ if !arguments_heading_written {
38+ arguments_heading_written = true ;
39+ out. push_str ( "## Arguments\n \n " ) ;
2640 }
41+ render_argument ( arg, & mut out) ;
42+ }
43+ if arguments_heading_written {
2744 out. push ( '\n' ) ;
2845 }
2946
3047 // Options section – non-positional, non-hidden args
31- let option_args: Vec < clap:: Arg > = command
32- . get_arguments ( )
33- . filter ( |a| !a. is_positional ( ) && !a. is_hide_set ( ) && !a. is_hide_long_help_set ( ) )
34- . cloned ( )
35- . collect ( ) ;
36- if !option_args. is_empty ( ) {
37- out. push_str ( "## Options\n \n " ) ;
38- for arg in & option_args {
39- render_option ( arg, & mut out) ;
48+ let mut options_heading_written = false ;
49+ for arg in command. get_arguments ( ) {
50+ if arg. is_positional ( ) || arg. is_hide_set ( ) || arg. is_hide_long_help_set ( ) {
51+ continue ;
4052 }
53+ if !options_heading_written {
54+ options_heading_written = true ;
55+ out. push_str ( "## Options\n \n " ) ;
56+ }
57+ render_option ( arg, & mut out) ;
4158 }
4259
4360 // Examples section – parse from after_long_help text
4461 if let Some ( after_help) = command. get_after_long_help ( ) {
4562 let text = after_help. to_string ( ) ;
46- let lines: Vec < & str > = text. lines ( ) . collect ( ) ;
47- if let Some ( pos) = lines. iter ( ) . position ( |l| l. trim ( ) == "Examples:" ) {
63+ let mut lines_iter = text. lines ( ) ;
64+ let mut has_examples = false ;
65+ for line in lines_iter. by_ref ( ) {
66+ if line. trim ( ) == "Examples:" {
67+ has_examples = true ;
68+ break ;
69+ }
70+ }
71+ if has_examples {
4872 out. push_str ( "## Examples\n \n " ) ;
49- render_examples_section ( & lines [ pos + 1 .. ] , & mut out) ;
73+ render_examples_section ( lines_iter , & mut out) ;
5074 }
5175 }
5276
5377 out
5478}
5579
56- fn render_argument ( arg : & clap :: Arg , out : & mut String ) {
80+ fn render_argument ( arg : & Arg , out : & mut String ) {
5781 let name = arg
5882 . get_value_names ( )
5983 . and_then ( |names| names. first ( ) )
@@ -73,7 +97,7 @@ fn render_argument(arg: &clap::Arg, out: &mut String) {
7397 out. push_str ( & format ! ( "* `{display_name}`: {desc}\n " ) ) ;
7498}
7599
76- fn render_option ( arg : & clap :: Arg , out : & mut String ) {
100+ fn render_option ( arg : & Arg , out : & mut String ) {
77101 let primary_long = arg. get_long ( ) . expect ( "option must have a long flag" ) ;
78102 let primary_name = format ! ( "--{primary_long}" ) ;
79103
@@ -115,7 +139,7 @@ fn render_option(arg: &clap::Arg, out: &mut String) {
115139
116140 // Default values – skip "false" (clap's implicit default for boolean flags)
117141 let default_values: Vec < _ > = if arg. is_hide_default_value_set ( ) {
118- vec ! [ ]
142+ Vec :: new ( )
119143 } else {
120144 arg. get_default_values ( )
121145 . iter ( )
@@ -125,7 +149,7 @@ fn render_option(arg: &clap::Arg, out: &mut String) {
125149
126150 // Possible values (choices)
127151 let possible_values: Vec < _ > = if arg. is_hide_possible_values_set ( ) {
128- vec ! [ ]
152+ Vec :: new ( )
129153 } else {
130154 arg. get_possible_values ( )
131155 . into_iter ( )
@@ -141,7 +165,10 @@ fn render_option(arg: &clap::Arg, out: &mut String) {
141165 out. push_str ( & format ! ( "* _Aliases:_ {aliases_str}.\n " ) ) ;
142166 }
143167 if !default_values. is_empty ( ) {
144- let default_str = default_values. iter ( ) . map ( |v| v. to_string_lossy ( ) ) . join ( ", " ) ;
168+ let default_str = default_values
169+ . iter ( )
170+ . map ( |v| v. to_string_lossy ( ) )
171+ . join ( ", " ) ;
145172 out. push_str ( & format ! ( "* _Default:_ `{default_str}`.\n " ) ) ;
146173 }
147174 if !possible_values. is_empty ( ) {
@@ -171,7 +198,7 @@ fn render_option(arg: &clap::Arg, out: &mut String) {
171198}
172199
173200/// Returns the help text for an argument: `get_help()` with `get_long_help()` appended if set.
174- fn get_help_text ( arg : & clap :: Arg ) -> String {
201+ fn get_help_text ( arg : & Arg ) -> String {
175202 let mut parts: Vec < String > = Vec :: new ( ) ;
176203 if let Some ( h) = arg. get_help ( ) {
177204 parts. push ( h. to_string ( ) ) ;
@@ -182,33 +209,21 @@ fn get_help_text(arg: &clap::Arg) -> String {
182209 parts. join ( "\n " )
183210}
184211
185- fn render_examples_section ( lines : & [ & str ] , out : & mut String ) {
186- let mut i = 0 ;
187- while i < lines. len ( ) {
188- let trimmed = lines [ i ] . trim ( ) ;
212+ fn render_examples_section < ' a > ( lines : impl Iterator < Item = & ' a str > , out : & mut String ) {
213+ let mut current_title : Option < & ' a str > = None ;
214+ for line in lines {
215+ let trimmed = line . trim ( ) ;
189216 if trimmed. is_empty ( ) {
190- i += 1 ;
191217 continue ;
192218 }
193- // A line starting with `$ ` is a bare command (no preceding description).
194219 if let Some ( cmd) = trimmed. strip_prefix ( '$' ) . map ( str:: trim) {
195- out. push_str ( & format ! ( "### `{cmd}`\n \n ```sh\n {cmd}\n ```\n \n " ) ) ;
196- i += 1 ;
197- } else {
198- // Description line — the very next non-empty line should be `$ <cmd>`.
199- let desc = trimmed;
200- i += 1 ;
201- while i < lines. len ( ) && lines[ i] . trim ( ) . is_empty ( ) {
202- i += 1 ;
203- }
204- if i < lines. len ( ) {
205- if let Some ( cmd) = lines[ i] . trim ( ) . strip_prefix ( '$' ) . map ( str:: trim) {
206- out. push_str ( & format ! ( "### {desc}\n \n ```sh\n {cmd}\n ```\n \n " ) ) ;
207- i += 1 ;
208- continue ;
209- }
220+ if let Some ( title) = current_title. take ( ) {
221+ out. push_str ( & format ! ( "### {title}\n \n ```sh\n {cmd}\n ```\n \n " ) ) ;
222+ } else {
223+ out. push_str ( & format ! ( "### `{cmd}`\n \n ```sh\n {cmd}\n ```\n \n " ) ) ;
210224 }
211- out. push_str ( & format ! ( "### {desc}\n \n " ) ) ;
225+ } else {
226+ current_title = Some ( trimmed) ;
212227 }
213228 }
214229}
0 commit comments