Skip to main content

usage/docs/markdown/
spec.rs

1use crate::docs::markdown::renderer::MarkdownRenderer;
2use crate::error::UsageErr;
3
4impl MarkdownRenderer {
5    pub fn render_spec(&self) -> Result<String, UsageErr> {
6        let all_commands = self.spec().cmd.all_subcommands();
7        let config = &self.spec().config;
8        self.render_with("spec_template.md.tera", |ctx| {
9            ctx.insert("all_commands", &all_commands);
10            ctx.insert("config", config);
11        })
12    }
13
14    pub fn render_index(&self) -> Result<String, UsageErr> {
15        let all_commands = self.spec().cmd.all_subcommands();
16        // So the index can link the settings page it sits beside. Without this the page was
17        // written and nothing pointed at it, which for a reader who starts at the index is
18        // the same as not writing it.
19        let config = &self.spec().config;
20        // The name the page will actually be written under, so the link cannot point somewhere
21        // else than the file — including when a `settings` command has taken `settings.md`.
22        self.render_with("index_template.md.tera", |ctx| {
23            // An index links separate pages even when the renderer's ordinary mode is single-file.
24            ctx.insert("multi", &false);
25            ctx.insert("all_commands", &all_commands);
26            ctx.insert("config", config);
27            ctx.insert("config_page", &self.config_page());
28        })
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use crate::docs::markdown::renderer::MarkdownRenderer;
35    use crate::test::SPEC_KITCHEN_SINK;
36    use crate::Spec;
37    use insta::assert_snapshot;
38
39    /// `--replace-pre-with-code-fences` reaches a command's long help on the single-file page.
40    ///
41    /// `MarkdownRenderer::new` used to render the whole docs model eagerly, before the builder
42    /// methods that set the options ran, and rendering marks each item done so the later pass
43    /// no-opped. The multi-page path hid this by re-deriving each page from the raw command it
44    /// is handed; the single-file page renders the stored model, so for it the flag did
45    /// nothing at all — the whole option was a no-op on `usage generate markdown` without
46    /// `--multi`.
47    #[test]
48    fn a_command_s_long_help_gets_the_rendering_options_it_was_asked_for() {
49        let src = "name \"v\"\nbin \"v\"\ncmd \"go\" help=\"Go\" long_help=\"Goes.\\n\\n    v go --now\\n\\nOk.\"\n";
50        let spec: Spec = src.parse().unwrap();
51
52        let with_fences = MarkdownRenderer::new(spec.clone())
53            .with_indented_blocks_to_code_fences(true)
54            .render_spec()
55            .unwrap();
56        assert!(
57            with_fences.contains("```\nv go --now\n```"),
58            "the option did not reach the command's help:\n{with_fences}"
59        );
60
61        // And without it the block stays indented, so the assertion above is about the option
62        // rather than about something else in the pipeline.
63        let without = MarkdownRenderer::new(spec).render_spec().unwrap();
64        assert!(!without.contains("```"), "{without}");
65        assert!(without.contains("    v go --now"), "{without}");
66    }
67
68    /// The former builder name still reaches the same option.
69    ///
70    /// It is kept, not renamed away, so an existing docs build keeps compiling; that only
71    /// helps if it keeps working, which nothing else here would notice.
72    #[test]
73    fn the_former_builder_name_still_works() {
74        let src = "name \"v\"\nbin \"v\"\ncmd \"go\" help=\"Go\" long_help=\"Goes.\\n\\n    v go --now\\n\\nOk.\"\n";
75        let spec: Spec = src.parse().unwrap();
76        let page = MarkdownRenderer::new(spec)
77            .with_replace_pre_with_code_fences(true)
78            .render_spec()
79            .unwrap();
80        assert!(page.contains("```\nv go --now\n```"), "{page}");
81    }
82
83    /// An option set after a render still applies, because the builders drop the rendered model.
84    #[test]
85    fn an_option_set_after_a_render_still_takes_effect() {
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
89        let ctx = MarkdownRenderer::new(spec);
90        assert!(!ctx.render_spec().unwrap().contains("```"));
91        let ctx = ctx.with_indented_blocks_to_code_fences(true);
92        assert!(
93            ctx.render_spec().unwrap().contains("```\nv go --now\n```"),
94            "a render before the builder froze the model"
95        );
96    }
97
98    #[test]
99    fn test_render_markdown_spec() {
100        let ctx = MarkdownRenderer::new(SPEC_KITCHEN_SINK.clone());
101        assert_snapshot!(ctx.render_spec().unwrap(), @"
102        # `mycli`
103
104        - **Usage:** `mycli [FLAGS] <ARGS>… <SUBCOMMAND>`
105
106        ## Arguments
107        - **`<arg1>`** — arg1 description
108        - **`[arg2]`** — arg2 description
109
110          **Choices:** `choice1`, `choice2`, `choice3`
111
112          **Default:** `default value`
113        - **`<arg3>`** — arg3 long description
114        - **`<argrest>…`**
115        - **`[with-default]`**
116
117          **Default:** `default value`
118
119        ## Flags
120        - **`--flag1`** — flag1 description
121        - **`--flag2`** — flag2 long description
122
123          includes a code block:
124
125              $ echo hello world
126              hello world
127
128              more code
129
130          Examples:
131
132              # run with no arguments to use the interactive selector
133              $ mise use
134
135              # set the current version of node to 20.x in mise.toml of current directory
136              # will write the fuzzy version (e.g.: 20)
137
138          some docs
139
140              $ echo hello world
141              hello world
142        - **`--flag3`** — flag3 description
143        - **`--with-default`**
144
145          **Default:** `default value`
146        - **`--shell <shell>`**
147
148          **Choices:** `bash`, `zsh`, `fish`
149
150        ## `mycli plugin`
151
152        - **Usage:** `mycli plugin <SUBCOMMAND>`
153        - **Source code:** [`src/cli/plugin.rs`](https://github.com/jdx/mise/blob/main/src/cli/plugin.rs)
154
155        ## `mycli plugin install`
156
157        - **Usage:** `mycli plugin install [FLAGS] <plugin> <version>`
158        - **Source code:** [`src/cli/plugin/install.rs`](https://github.com/jdx/mise/blob/main/src/cli/plugin/install.rs)
159
160        install a plugin
161
162        ### Arguments
163        - **`<plugin>`**
164        - **`<version>`**
165
166        ### Flags
167        - **`-g --global`**
168        - **`-d --dir <dir>`**
169        - **`-f --force`**
170        ");
171    }
172
173    #[test]
174    fn hidden_commands_are_omitted_from_single_file_markdown() {
175        let spec: Spec = r#"
176            name "mycli"
177            bin "mycli"
178            cmd "visible" help="A documented command"
179            cmd "setup" hide=#true help="An internal command"
180        "#
181        .parse()
182        .unwrap();
183
184        let output = MarkdownRenderer::new(spec).render_spec().unwrap();
185
186        assert!(output.contains("## `mycli visible`"), "{output}");
187        assert!(!output.contains("mycli setup"), "{output}");
188        assert!(!output.contains("An internal command"), "{output}");
189    }
190
191    #[test]
192    fn package_metadata_reaches_markdown() {
193        let spec: Spec = r#"
194            name "metadata"
195            bin "metadata"
196            author "Example Maintainers"
197            license "MIT OR Apache-2.0"
198            repository "https://example.com/tool"
199        "#
200        .parse()
201        .unwrap();
202        let output = MarkdownRenderer::new(spec).render_spec().unwrap();
203
204        assert!(
205            output.contains("**Author:** Example Maintainers"),
206            "{output}"
207        );
208        assert!(
209            output.contains("**License:** MIT OR Apache-2.0"),
210            "{output}"
211        );
212        assert!(
213            output.contains("**Repository:** https://example.com/tool"),
214            "{output}"
215        );
216    }
217}