usage/docs/markdown/
config.rs1use crate::docs::markdown::renderer::MarkdownRenderer;
2use crate::error::UsageErr;
3
4impl MarkdownRenderer {
5 pub fn render_config(&self) -> Result<String, UsageErr> {
10 let config = &self.spec().config;
11 if config.is_empty() {
12 return Ok(String::new());
13 }
14 self.render_with("config_template.md.tera", |ctx| {
15 ctx.insert("config", config)
16 })
17 }
18}
19
20#[cfg(test)]
21mod tests {
22 use crate::docs::markdown::renderer::MarkdownRenderer;
23 use insta::assert_snapshot;
24
25 fn rendered(src: &str) -> String {
26 let spec: crate::Spec = src.parse().unwrap();
27 MarkdownRenderer::new(spec)
28 .with_indented_blocks_to_code_fences(true)
29 .render_config()
30 .unwrap()
31 }
32
33 #[test]
34 fn a_cli_with_no_settings_renders_nothing() {
35 assert_eq!(rendered("name \"ex\"\nbin \"ex\"\n"), "");
36 }
37
38 #[test]
39 fn explicit_optionality_and_aliases_are_documented() {
40 let page = rendered(
41 r#"
42name "ex"
43bin "ex"
44config {
45 prop "jobs" type="uint" optional=#false {
46 alias "parallelism" "threads"
47 }
48}
49"#,
50 );
51 assert!(
52 page.contains("**Aliases:** `parallelism`, `threads`"),
53 "{page}"
54 );
55 assert!(page.contains("**Optional:** false"), "{page}");
56 }
57
58 #[test]
59 fn the_facts_list_starts_on_its_own_line_whatever_its_first_item_is() {
60 let page = rendered(
65 r##"
66name "ex"
67bin "ex"
68config {
69 prop "untyped" default=1 help="No declared type"
70 prop "gone" default=2 deprecated="Use untyped." help="Also no declared type"
71}
72"##,
73 );
74 assert!(
75 page.contains("## `untyped`\n\n- **Default:** `1`"),
76 "the list item is against the heading:\n{page:?}"
77 );
78 assert!(
79 page.contains(":::\n\n- **Default:** `2`"),
80 "the list item is against the admonition:\n{page:?}"
81 );
82 }
83
84 #[test]
85 fn the_settings_section_sits_under_the_title_on_the_single_file_page() {
86 let spec: crate::Spec = r##"
91name "ex"
92bin "ex"
93config {
94 prop "jobs" type="uint" help="How many"
95}
96cmd "run" help="Run"
97"##
98 .parse()
99 .unwrap();
100 let page = MarkdownRenderer::new(spec).render_spec().unwrap();
101 assert!(
102 page.contains("\n\n## Configuration\n"),
103 "the heading is glued to the line above it, or at the wrong level:\n{page}"
104 );
105 assert!(page.contains("### `jobs`"), "{page}");
107 }
108
109 #[test]
110 fn a_setting_s_long_help_gets_the_rendering_options_it_was_asked_for() {
111 let src = r##"
117name "ex"
118bin "ex"
119config {
120 prop "shell" help="Which shell" {
121 long_help "Run it like this:\n\n ex --shell bash\n"
122 }
123}
124"##;
125 let spec: crate::Spec = src.parse().unwrap();
126 let with_fences = MarkdownRenderer::new(spec.clone())
127 .with_indented_blocks_to_code_fences(true)
128 .render_config()
129 .unwrap();
130 assert!(
131 with_fences.contains("```"),
132 "the option did not reach the setting's help:\n{with_fences}"
133 );
134 let without = MarkdownRenderer::new(spec.clone()).render_config().unwrap();
137 assert!(!without.contains("```"), "{without}");
138 assert!(without.contains(" ex --shell bash"), "{without}");
139
140 let whole = MarkdownRenderer::new(spec)
142 .with_indented_blocks_to_code_fences(true)
143 .render_spec()
144 .unwrap();
145 assert!(
146 whole.contains("```"),
147 "the option did not reach the settings section of the whole-spec page:\n{whole}"
148 );
149 }
150
151 #[test]
152 fn the_index_links_the_settings_page_beside_it() {
153 let with_settings: crate::Spec = r##"
156name "ex"
157bin "ex"
158config {
159 prop "jobs" type="uint"
160}
161cmd "run" help="Run"
162"##
163 .parse()
164 .unwrap();
165 let renderer = MarkdownRenderer::new(with_settings);
166 let index = renderer.render_index().unwrap();
167 assert!(
168 index.contains(&format!("[Settings](/{})", renderer.config_page())),
169 "{index}"
170 );
171
172 let collides: crate::Spec = r##"
176name "ex"
177bin "ex"
178config {
179 prop "jobs" type="uint"
180}
181cmd "settings" help="Manage settings"
182"##
183 .parse()
184 .unwrap();
185 let renderer = MarkdownRenderer::new(collides);
186 assert_eq!(renderer.config_page(), "configuration.md");
187 assert_eq!(renderer.config_page_collision(), None);
188
189 let clash: crate::Spec = r##"
191name "ex"
192bin "ex"
193config {
194 prop "jobs" type="uint"
195}
196cmd "configuration" help="Somebody really did this"
197"##
198 .parse()
199 .unwrap();
200 assert_eq!(
201 MarkdownRenderer::new(clash).config_page_collision(),
202 Some("configuration")
203 );
204
205 let without: crate::Spec = "name \"ex\"\nbin \"ex\"\ncmd \"run\" help=\"Run\"\n"
208 .parse()
209 .unwrap();
210 let renderer = MarkdownRenderer::new(without);
211 let index = renderer.render_index().unwrap();
212 assert!(
216 !index.contains(&format!("[Settings](/{}", renderer.config_page())),
217 "{index}"
218 );
219 }
220
221 #[test]
222 fn a_block_of_only_files_reaches_every_output() {
223 let src = r##"
229name "ex"
230bin "ex"
231config {
232 file "/etc/ex/config.toml" scope="system"
233 file "ex.toml" findup=#true
234}
235"##;
236 let spec: crate::Spec = src.parse().unwrap();
237 let renderer = MarkdownRenderer::new(spec);
238 let page = renderer.render_config().unwrap();
239 assert!(page.contains("ex.toml"), "{page}");
240 let whole = renderer.render_spec().unwrap();
241 assert!(
242 whole.contains("ex.toml"),
243 "the single-file page dropped the file chain:\n{whole}"
244 );
245 }
246
247 #[test]
248 fn every_part_of_a_prop_reaches_the_page() {
249 assert_snapshot!(rendered(
250 r##"
251name "hk"
252bin "hk"
253config {
254 source "git" name="git config" doc_hint="git config `{key}`"
255 file "~/.config/hk/config.toml" scope="global"
256 file "hk.toml" findup=#true
257 prop "jobs" type="uint" default=0 default_note="0 = auto-detect" \
258 help="Number of parallel jobs" since="1.0.0" help_heading="Performance" {
259 cli "--jobs" "-j"
260 env "HK_JOBS" "HK_JOB"
261 source "git" "hk.jobs"
262 example "hk check --jobs 4"
263 }
264 prop "exclude" type="list<string>" merge="union" help="Patterns to skip" {
265 default "target" "node_modules"
266 env "HK_EXCLUDE"
267 }
268 prop "stash" type="string" help="How to stash" {
269 choices {
270 choice "git" help="Use `git stash`"
271 choice "none" help="No stashing"
272 }
273 }
274 prop "trusted" type="bool" scope="global" help="Trust the config"
275 prop "old" deprecated="Use jobs instead." deprecated_remove_at="2027.12.0" help="Old"
276 prop "secret" hide=#true help="Not for the page"
277}
278"##
279 ));
280 }
281}