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