usage/docs/markdown/
cmd.rs1use crate::docs::markdown::renderer::MarkdownRenderer;
2use crate::docs::models::SpecCommand;
3use crate::error::UsageErr;
4
5impl MarkdownRenderer {
6 pub fn render_cmd(&self, cmd: &crate::SpecCommand) -> Result<String, UsageErr> {
7 let mut cmd = SpecCommand::from(cmd);
8 if let Some(folded) = self.folded(&cmd.full_cmd) {
13 cmd.outputs = folded.outputs.clone();
14 cmd.exit_codes = folded.exit_codes.clone();
15 }
16 cmd.render_md(self);
17 self.render_with("cmd_template.md.tera", |ctx| ctx.insert("cmd", &cmd))
18 }
19
20 fn folded(&self, path: &[String]) -> Option<&SpecCommand> {
22 let mut cmd = &self.spec().cmd;
23 for name in path {
24 cmd = cmd.subcommands.get(name)?;
25 }
26 Some(cmd)
27 }
28}
29
30#[cfg(test)]
31mod tests {
32 use crate::docs::markdown::renderer::{MarkdownRenderer, MarkdownTheme};
33 use crate::test::SPEC_KITCHEN_SINK;
34 use crate::Spec;
35 use insta::assert_snapshot;
36
37 #[test]
38 fn test_render_markdown_cmd() {
39 let ctx = MarkdownRenderer::new(SPEC_KITCHEN_SINK.clone())
40 .with_multi(true)
41 .with_indented_blocks_to_code_fences(true);
42 assert_snapshot!(ctx.render_cmd(&SPEC_KITCHEN_SINK.cmd).unwrap(), @"
43 # `mycli`
44
45 - **Usage:** `mycli [FLAGS] <ARGS>… [SUBCOMMAND]`
46
47 ## Arguments
48 - **`<arg1>`** — arg1 description
49 - **`[arg2]`** — arg2 description
50
51 **Choices:** `choice1`, `choice2`, `choice3`
52
53 **Default:** `default value`
54 - **`<arg3>`** — arg3 long description
55 - **`<argrest>…`**
56 - **`[with-default]`**
57
58 **Default:** `default value`
59
60 ## Flags
61 - **`--flag1`** — flag1 description
62 - **`--flag2`** — flag2 long description
63
64 includes a code block:
65
66 ```
67 $ echo hello world
68 hello world
69
70 more code
71 ```
72
73 Examples:
74
75 ```
76 # run with no arguments to use the interactive selector
77 $ mise use
78
79 # set the current version of node to 20.x in mise.toml of current directory
80 # will write the fuzzy version (e.g.: 20)
81 ```
82
83 some docs
84
85 ```
86 $ echo hello world
87 hello world
88 ```
89 - **`--flag3`** — flag3 description
90 - **`--with-default`**
91
92 **Default:** `default value`
93 - **`--shell <shell>`**
94
95 **Choices:** `bash`, `zsh`, `fish`
96
97 ## Subcommands
98
99 - [`mycli plugin [SUBCOMMAND]`](/plugin.md)
100 ");
101 }
102
103 #[test]
104 fn test_render_markdown_cmd_effect() {
105 let spec: Spec = r#"
106name "mise"
107bin "mise"
108cmd "ls" effect="read" help="List installed tools"
109cmd "use" effect="write" help="Install a tool"
110cmd "uninstall" effect="destructive" help="Remove a tool"
111cmd "version" help="Show the version"
112 "#
113 .parse()
114 .unwrap();
115 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
116 let rendered = spec
117 .cmd
118 .subcommands
119 .values()
120 .map(|cmd| ctx.render_cmd(cmd).unwrap())
121 .collect::<Vec<_>>()
122 .join("\n\n");
123
124 assert_snapshot!(rendered, @"
127 # `mise ls`
128
129 - **Usage:** `mise ls`
130 - **Effect:** read-only
131
132 List installed tools
133
134 # `mise use`
135
136 - **Usage:** `mise use`
137 - **Effect:** modifies state
138
139 Install a tool
140
141 # `mise uninstall`
142
143 - **Usage:** `mise uninstall`
144 - **Effect:** destructive — may delete or irreversibly overwrite
145
146 Remove a tool
147
148 # `mise version`
149
150 - **Usage:** `mise version`
151
152 Show the version
153 ");
154 }
155
156 #[test]
157 fn clause_fields_are_documented_like_the_arguments_users_type() {
158 let spec: Spec = r#"
159bin "mycli"
160cmd "use" {
161 clause tools {
162 flag "--postinstall <command>" help="Run after installation"
163 arg <tool> help="Tool to install"
164 }
165}
166 "#
167 .parse()
168 .unwrap();
169 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
170 let rendered = ctx
171 .render_cmd(spec.cmd.subcommands.get("use").unwrap())
172 .unwrap();
173
174 assert!(rendered.contains("## Arguments"), "{rendered}");
175 assert!(
176 rendered.contains("**`<tool>`** — Tool to install"),
177 "{rendered}"
178 );
179 assert!(rendered.contains("## Flags"), "{rendered}");
180 assert!(
181 rendered.contains("**`--postinstall <command>`** — Run after installation"),
182 "{rendered}"
183 );
184 }
185
186 #[test]
187 fn test_render_markdown_groups_by_heading() {
188 let spec: Spec = r#"
189bin "mycli"
190flag "--verbose" help="Verbose output"
191flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
192flag "--hidden-one" help="Not shown" help_heading="Filtering" hide=#true
193arg "<file>" help="The file"
194arg "<mode>" help="How to run" help_heading="Behaviour"
195"#
196 .parse()
197 .unwrap();
198 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
199
200 assert_snapshot!(ctx.render_cmd(&spec.cmd).unwrap(), @"
203 # `mycli`
204
205 - **Usage:** `mycli [--verbose] [--filter <pattern>] <file> <mode>`
206
207 ## Arguments
208 - **`<file>`** — The file
209
210 ## Behaviour
211 - **`<mode>`** — How to run
212
213 ## Flags
214 - **`--verbose`** — Verbose output
215
216 ## Filtering
217 - **`--filter <pattern>`** — Only matching
218 ");
219 }
220
221 #[test]
222 fn test_render_markdown_groups_global_flags_too() {
223 let spec: Spec = r#"
226bin "mycli"
227flag "--verbose" help="Verbose output" global=#true
228flag "--filter <pattern>" help="Only matching" help_heading="Filtering" global=#true
229flag "--local-one" help="Not global"
230cmd "sub" help="a subcommand"
231"#
232 .parse()
233 .unwrap();
234 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
235
236 assert_snapshot!(ctx.render_cmd(&spec.cmd).unwrap(), @"
237 # `mycli`
238
239 - **Usage:** `mycli [FLAGS] [SUBCOMMAND]`
240
241 ## Global Flags
242 - **`--verbose`** — Verbose output
243
244 ## Filtering
245 - **`--filter <pattern>`** — Only matching
246
247 ## Flags
248 - **`--local-one`** — Not global
249
250 ## Subcommands
251
252 - [`mycli sub`](/sub.md)
253 ");
254 }
255
256 #[test]
257 fn generated_reference_separates_visible_flag_aliases() {
258 let spec: Spec = r#"
259bin "mycli"
260flag "-t -f --tail --follow" help="Follow output"
261"#
262 .parse()
263 .unwrap();
264 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
265 let rendered = ctx.render_cmd(&spec.cmd).unwrap();
266 assert!(rendered.contains("- **`-t --tail`**"), "{rendered}");
267 assert!(
268 rendered.contains("**Aliases:** `-f`, `--follow`"),
269 "{rendered}"
270 );
271 assert!(
272 !rendered.contains("**`-t -f --tail --follow`**"),
273 "{rendered}"
274 );
275 }
276
277 #[test]
278 fn test_render_markdown_cmd_outputs_and_exit_codes() {
279 let spec: Spec = r#"
280name "ex"
281bin "ex"
282exit_code 0 "success"
283exit_code 130 "interrupted | terminated"
284cmd "check" help="Check the project" {
285 flag "--format <FMT>" help="Output format"
286 output "human" default=#true help="A table"
287 output "jsonl" framing="jsonl" help="One event per line"
288 select "--format"
289 exit_code 1 "a check failed"
290}
291cmd "version" help="Show the version"
292 "#
293 .parse()
294 .unwrap();
295 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
296 let rendered = spec
297 .cmd
298 .subcommands
299 .values()
300 .map(|cmd| ctx.render_cmd(cmd).unwrap())
301 .collect::<Vec<_>>()
302 .join("\n\n");
303
304 assert!(rendered.contains("## Output Formats"), "{rendered}");
309 assert!(rendered.contains("- **`human`** (default)"), "{rendered}");
310 assert!(
311 rendered.contains("**Select:** `--format jsonl`"),
312 "{rendered}"
313 );
314 assert!(
315 rendered.contains("one document per line, read as it arrives"),
316 "{rendered}"
317 );
318 assert!(rendered.contains("| `1` | a check failed |"), "{rendered}");
319 assert!(
320 rendered.contains(r"| `130` | interrupted \| terminated |"),
321 "{rendered}"
322 );
323
324 let version = ctx.render_cmd(&spec.cmd.subcommands["version"]).unwrap();
325 assert!(!version.contains("## Output Formats"), "{version}");
326 assert!(version.contains("| `0` | success |"), "{version}");
327 }
328
329 #[test]
330 fn a_command_with_no_outputs_renders_no_output_section() {
331 let spec: Spec = r#"
332name "ex"
333bin "ex"
334cmd "ls" help="List things"
335 "#
336 .parse()
337 .unwrap();
338 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
339 let rendered = ctx.render_cmd(&spec.cmd.subcommands["ls"]).unwrap();
340 assert!(!rendered.contains("## Output Formats"), "{rendered}");
341 assert!(!rendered.contains("## Exit Status"), "{rendered}");
342 }
343
344 #[test]
345 fn compact_output_formats_collapse_only_long_catalogs() {
346 let short: Spec = r#"
347name "short"
348output "text"
349output "json" framing="json"
350 "#
351 .parse()
352 .unwrap();
353 let short = MarkdownRenderer::new(short).render_spec().unwrap();
354 assert!(short.contains("## Output Formats"), "{short}");
355 assert!(!short.contains("<details>"), "{short}");
356
357 let boundary: Spec = r#"
358name "boundary"
359output "text"
360output "json" framing="json"
361output "jsonl" framing="jsonl"
362output "xml"
363output "yaml"
364 "#
365 .parse()
366 .unwrap();
367 let boundary = MarkdownRenderer::new(boundary).render_spec().unwrap();
368 assert!(!boundary.contains("<details>"), "{boundary}");
369
370 let long: Spec = r#"
371name "long"
372output "text"
373output "json" framing="json"
374output "jsonl" framing="jsonl"
375output "xml"
376output "yaml"
377output "csv"
378 "#
379 .parse()
380 .unwrap();
381 let compact = MarkdownRenderer::new(long.clone()).render_spec().unwrap();
382 assert!(
383 compact.contains("<summary>6 available formats</summary>"),
384 "{compact}"
385 );
386 assert!(compact.contains("- **`csv`**"), "{compact}");
387
388 let detailed = MarkdownRenderer::new(long)
389 .with_theme(MarkdownTheme::Detailed)
390 .render_spec()
391 .unwrap();
392 assert!(detailed.contains("## Output Formats"), "{detailed}");
393 assert!(!detailed.contains("<details>"), "{detailed}");
394 }
395}