Skip to main content

usage/docs/markdown/
spec.rs

1use 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        // So the index can link the settings page it sits beside. Without this the page was
27        // written and nothing pointed at it, which for a reader who starts at the index is
28        // the same as not writing it.
29        let config = &self.spec().config;
30        let default_command = default_command_name(self.raw());
31        // The name the page will actually be written under, so the link cannot point somewhere
32        // else than the file — including when a `settings` command has taken `settings.md`.
33        self.render_with("index_template.md.tera", |ctx| {
34            // An index links separate pages even when the renderer's ordinary mode is single-file.
35            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    /// `--replace-pre-with-code-fences` reaches a command's long help on the single-file page.
52    ///
53    /// `MarkdownRenderer::new` used to render the whole docs model eagerly, before the builder
54    /// methods that set the options ran, and rendering marks each item done so the later pass
55    /// no-opped. The multi-page path hid this by re-deriving each page from the raw command it
56    /// is handed; the single-file page renders the stored model, so for it the flag did
57    /// nothing at all — the whole option was a no-op on `usage generate markdown` without
58    /// `--multi`.
59    #[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        // And without it the block stays indented, so the assertion above is about the option
74        // rather than about something else in the pipeline.
75        let without = MarkdownRenderer::new(spec).render_spec().unwrap();
76        assert!(!without.contains("```"), "{without}");
77        assert!(without.contains("    v go --now"), "{without}");
78    }
79
80    /// The former builder name still reaches the same option.
81    ///
82    /// It is kept, not renamed away, so an existing docs build keeps compiling; that only
83    /// helps if it keeps working, which nothing else here would notice.
84    #[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    /// An option set after a render still applies, because the builders drop the rendered model.
96    #[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}