usage/docs/markdown/
spec.rs1use crate::docs::markdown::renderer::MarkdownRenderer;
2use crate::error::UsageErr;
3
4fn default_command_name(spec: &crate::Spec) -> Option<String> {
5 spec.default_subcommand
6 .as_deref()
7 .and_then(|name| spec.cmd.find_subcommand(name))
8 .filter(|cmd| !cmd.hide)
9 .map(|cmd| cmd.name.clone())
10}
11
12impl MarkdownRenderer {
13 pub fn render_spec(&self) -> Result<String, UsageErr> {
14 let all_commands = self.spec().cmd.all_subcommands();
15 let config = &self.spec().config;
16 let default_command = default_command_name(self.raw());
17 self.render_with("spec_template.md.tera", |ctx| {
18 ctx.insert("all_commands", &all_commands);
19 ctx.insert("config", config);
20 ctx.insert("default_command", &default_command);
21 })
22 }
23
24 pub fn render_index(&self) -> Result<String, UsageErr> {
25 let all_commands = self.spec().cmd.all_subcommands();
26 let config = &self.spec().config;
30 let default_command = default_command_name(self.raw());
31 self.render_with("index_template.md.tera", |ctx| {
34 ctx.insert("multi", &false);
36 ctx.insert("all_commands", &all_commands);
37 ctx.insert("config", config);
38 ctx.insert("config_page", &self.config_page());
39 ctx.insert("default_command", &default_command);
40 })
41 }
42}
43
44#[cfg(test)]
45mod tests {
46 use crate::Spec;
47 use crate::docs::markdown::renderer::MarkdownRenderer;
48 use crate::test::SPEC_KITCHEN_SINK;
49 use insta::assert_snapshot;
50
51 #[test]
60 fn a_command_s_long_help_gets_the_rendering_options_it_was_asked_for() {
61 let src = "name \"v\"\nbin \"v\"\ncmd \"go\" help=\"Go\" long_help=\"Goes.\\n\\n v go --now\\n\\nOk.\"\n";
62 let spec: Spec = src.parse().unwrap();
63
64 let with_fences = MarkdownRenderer::new(spec.clone())
65 .with_indented_blocks_to_code_fences(true)
66 .render_spec()
67 .unwrap();
68 assert!(
69 with_fences.contains("```\nv go --now\n```"),
70 "the option did not reach the command's help:\n{with_fences}"
71 );
72
73 let without = MarkdownRenderer::new(spec).render_spec().unwrap();
76 assert!(!without.contains("```"), "{without}");
77 assert!(without.contains(" v go --now"), "{without}");
78 }
79
80 #[test]
85 fn the_former_builder_name_still_works() {
86 let src = "name \"v\"\nbin \"v\"\ncmd \"go\" help=\"Go\" long_help=\"Goes.\\n\\n v go --now\\n\\nOk.\"\n";
87 let spec: Spec = src.parse().unwrap();
88 let page = MarkdownRenderer::new(spec)
89 .with_replace_pre_with_code_fences(true)
90 .render_spec()
91 .unwrap();
92 assert!(page.contains("```\nv go --now\n```"), "{page}");
93 }
94
95 #[test]
97 fn an_option_set_after_a_render_still_takes_effect() {
98 let src = "name \"v\"\nbin \"v\"\ncmd \"go\" help=\"Go\" long_help=\"Goes.\\n\\n v go --now\\n\\nOk.\"\n";
99 let spec: Spec = src.parse().unwrap();
100
101 let ctx = MarkdownRenderer::new(spec);
102 assert!(!ctx.render_spec().unwrap().contains("```"));
103 let ctx = ctx.with_indented_blocks_to_code_fences(true);
104 assert!(
105 ctx.render_spec().unwrap().contains("```\nv go --now\n```"),
106 "a render before the builder froze the model"
107 );
108 }
109
110 #[test]
111 fn test_render_markdown_spec() {
112 let ctx = MarkdownRenderer::new(SPEC_KITCHEN_SINK.clone());
113 assert_snapshot!(ctx.render_spec().unwrap(), @"
114 # `mycli`
115
116 - **Usage:** `mycli [FLAGS] <ARGS>… [SUBCOMMAND]`
117
118 ## Arguments
119 - **`<arg1>`** — arg1 description
120 - **`[arg2]`** — arg2 description
121
122 **Choices:** `choice1`, `choice2`, `choice3`
123
124 **Default:** `default value`
125 - **`<arg3>`** — arg3 long description
126 - **`<argrest>…`**
127 - **`[with-default]`**
128
129 **Default:** `default value`
130
131 ## Flags
132 - **`--flag1`** — flag1 description
133 - **`--flag2`** — flag2 long description
134
135 includes a code block:
136
137 $ echo hello world
138 hello world
139
140 more code
141
142 Examples:
143
144 # run with no arguments to use the interactive selector
145 $ mise use
146
147 # set the current version of node to 20.x in mise.toml of current directory
148 # will write the fuzzy version (e.g.: 20)
149
150 some docs
151
152 $ echo hello world
153 hello world
154 - **`--flag3`** — flag3 description
155 - **`--with-default`**
156
157 **Default:** `default value`
158 - **`--shell <shell>`**
159
160 **Choices:** `bash`, `zsh`, `fish`
161
162 ## `mycli plugin`
163
164 - **Usage:** `mycli plugin [SUBCOMMAND]`
165 - **Source code:** [`src/cli/plugin.rs`](https://github.com/jdx/mise/blob/main/src/cli/plugin.rs)
166
167 ## `mycli plugin install`
168
169 - **Usage:** `mycli plugin install [FLAGS] <plugin> <version>`
170 - **Source code:** [`src/cli/plugin/install.rs`](https://github.com/jdx/mise/blob/main/src/cli/plugin/install.rs)
171
172 install a plugin
173
174 ### Arguments
175 - **`<plugin>`**
176 - **`<version>`**
177
178 ### Flags
179 - **`-g --global`**
180 - **`-d --dir <dir>`**
181 - **`-f --force`**
182 ");
183 }
184
185 #[test]
186 fn hidden_commands_are_omitted_from_single_file_markdown() {
187 let spec: Spec = r#"
188 name "mycli"
189 bin "mycli"
190 cmd "visible" help="A documented command"
191 cmd "setup" hide=#true help="An internal command"
192 "#
193 .parse()
194 .unwrap();
195
196 let output = MarkdownRenderer::new(spec).render_spec().unwrap();
197
198 assert!(output.contains("## `mycli visible`"), "{output}");
199 assert!(!output.contains("mycli setup"), "{output}");
200 assert!(!output.contains("An internal command"), "{output}");
201 }
202
203 #[test]
204 fn package_metadata_reaches_markdown() {
205 let spec: Spec = r#"
206 name "metadata"
207 bin "metadata"
208 author "Example Maintainers"
209 license "MIT OR Apache-2.0"
210 repository "https://example.com/tool"
211 "#
212 .parse()
213 .unwrap();
214 let output = MarkdownRenderer::new(spec).render_spec().unwrap();
215
216 assert!(
217 output.contains("**Author:** Example Maintainers"),
218 "{output}"
219 );
220 assert!(
221 output.contains("**License:** MIT OR Apache-2.0"),
222 "{output}"
223 );
224 assert!(
225 output.contains("**Repository:** https://example.com/tool"),
226 "{output}"
227 );
228 }
229}