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_replace_pre_with_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 test_render_markdown_groups_by_heading() {
158 let spec: Spec = r#"
159bin "mycli"
160flag "--verbose" help="Verbose output"
161flag "--filter <pattern>" help="Only matching" help_heading="Filtering"
162flag "--hidden-one" help="Not shown" help_heading="Filtering" hide=#true
163arg "<file>" help="The file"
164arg "<mode>" help="How to run" help_heading="Behaviour"
165"#
166 .parse()
167 .unwrap();
168 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
169
170 assert_snapshot!(ctx.render_cmd(&spec.cmd).unwrap(), @"
173 # `mycli`
174
175 - **Usage:** `mycli [--verbose] [--filter <pattern>] <file> <mode>`
176
177 ## Arguments
178 - **`<file>`** — The file
179
180 ## Behaviour
181 - **`<mode>`** — How to run
182
183 ## Flags
184 - **`--verbose`** — Verbose output
185
186 ## Filtering
187 - **`--filter <pattern>`** — Only matching
188 ");
189 }
190
191 #[test]
192 fn test_render_markdown_groups_global_flags_too() {
193 let spec: Spec = r#"
196bin "mycli"
197flag "--verbose" help="Verbose output" global=#true
198flag "--filter <pattern>" help="Only matching" help_heading="Filtering" global=#true
199flag "--local-one" help="Not global"
200cmd "sub" help="a subcommand"
201"#
202 .parse()
203 .unwrap();
204 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
205
206 assert_snapshot!(ctx.render_cmd(&spec.cmd).unwrap(), @"
207 # `mycli`
208
209 - **Usage:** `mycli [FLAGS] <SUBCOMMAND>`
210
211 ## Global Flags
212 - **`--verbose`** — Verbose output
213
214 ## Filtering
215 - **`--filter <pattern>`** — Only matching
216
217 ## Flags
218 - **`--local-one`** — Not global
219
220 ## Subcommands
221
222 - [`mycli sub`](/sub.md)
223 ");
224 }
225
226 #[test]
227 fn generated_reference_separates_visible_flag_aliases() {
228 let spec: Spec = r#"
229bin "mycli"
230flag "-t -f --tail --follow" help="Follow output"
231"#
232 .parse()
233 .unwrap();
234 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
235 let rendered = ctx.render_cmd(&spec.cmd).unwrap();
236 assert!(rendered.contains("- **`-t --tail`**"), "{rendered}");
237 assert!(
238 rendered.contains("**Aliases:** `-f`, `--follow`"),
239 "{rendered}"
240 );
241 assert!(
242 !rendered.contains("**`-t -f --tail --follow`**"),
243 "{rendered}"
244 );
245 }
246
247 #[test]
248 fn test_render_markdown_cmd_outputs_and_exit_codes() {
249 let spec: Spec = r#"
250name "ex"
251bin "ex"
252exit_code 0 "success"
253exit_code 130 "interrupted | terminated"
254cmd "check" help="Check the project" {
255 flag "--format <FMT>" help="Output format"
256 output "human" default=#true help="A table"
257 output "jsonl" framing="jsonl" help="One event per line"
258 select "--format"
259 exit_code 1 "a check failed"
260}
261cmd "version" help="Show the version"
262 "#
263 .parse()
264 .unwrap();
265 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
266 let rendered = spec
267 .cmd
268 .subcommands
269 .values()
270 .map(|cmd| ctx.render_cmd(cmd).unwrap())
271 .collect::<Vec<_>>()
272 .join("\n\n");
273
274 assert!(rendered.contains("## Output Formats"), "{rendered}");
279 assert!(rendered.contains("- **`human`** (default)"), "{rendered}");
280 assert!(
281 rendered.contains("**Select:** `--format jsonl`"),
282 "{rendered}"
283 );
284 assert!(
285 rendered.contains("one document per line, read as it arrives"),
286 "{rendered}"
287 );
288 assert!(rendered.contains("| `1` | a check failed |"), "{rendered}");
289 assert!(
290 rendered.contains(r"| `130` | interrupted \| terminated |"),
291 "{rendered}"
292 );
293
294 let version = ctx.render_cmd(&spec.cmd.subcommands["version"]).unwrap();
295 assert!(!version.contains("## Output Formats"), "{version}");
296 assert!(version.contains("| `0` | success |"), "{version}");
297 }
298
299 #[test]
300 fn a_command_with_no_outputs_renders_no_output_section() {
301 let spec: Spec = r#"
302name "ex"
303bin "ex"
304cmd "ls" help="List things"
305 "#
306 .parse()
307 .unwrap();
308 let ctx = MarkdownRenderer::new(spec.clone()).with_multi(true);
309 let rendered = ctx.render_cmd(&spec.cmd.subcommands["ls"]).unwrap();
310 assert!(!rendered.contains("## Output Formats"), "{rendered}");
311 assert!(!rendered.contains("## Exit Status"), "{rendered}");
312 }
313
314 #[test]
315 fn compact_output_formats_collapse_only_long_catalogs() {
316 let short: Spec = r#"
317name "short"
318output "text"
319output "json" framing="json"
320 "#
321 .parse()
322 .unwrap();
323 let short = MarkdownRenderer::new(short).render_spec().unwrap();
324 assert!(short.contains("## Output Formats"), "{short}");
325 assert!(!short.contains("<details>"), "{short}");
326
327 let boundary: Spec = r#"
328name "boundary"
329output "text"
330output "json" framing="json"
331output "jsonl" framing="jsonl"
332output "xml"
333output "yaml"
334 "#
335 .parse()
336 .unwrap();
337 let boundary = MarkdownRenderer::new(boundary).render_spec().unwrap();
338 assert!(!boundary.contains("<details>"), "{boundary}");
339
340 let long: Spec = r#"
341name "long"
342output "text"
343output "json" framing="json"
344output "jsonl" framing="jsonl"
345output "xml"
346output "yaml"
347output "csv"
348 "#
349 .parse()
350 .unwrap();
351 let compact = MarkdownRenderer::new(long.clone()).render_spec().unwrap();
352 assert!(
353 compact.contains("<summary>6 available formats</summary>"),
354 "{compact}"
355 );
356 assert!(compact.contains("- **`csv`**"), "{compact}");
357
358 let detailed = MarkdownRenderer::new(long)
359 .with_theme(MarkdownTheme::Detailed)
360 .render_spec()
361 .unwrap();
362 assert!(detailed.contains("## Output Formats"), "{detailed}");
363 assert!(!detailed.contains("<details>"), "{detailed}");
364 }
365}