pub const SECTIONS: [&str; 6] = ["about", "usage", "commands", "args", "flags", "after_help"];
pub fn is_set(template: &str) -> bool {
!template.trim().is_empty()
}
pub fn check(template: &str) -> Result<(), String> {
let mut rest = template;
while let Some(at) = rest.find("{{") {
let after = &rest[at + 2..];
let Some(end) = after.find("}}") else {
return Err(format!(
"help_template has a `{{{{` with no `}}}}` after it; the sections are {}",
SECTIONS.join(", ")
));
};
let name = after[..end].trim();
if !SECTIONS.contains(&name) {
return Err(format!(
"help_template names no section \"{name}\"; a page is assembled from {} — \
reorder, omit or wrap those, and note that clap's `{{options}}` is \
`{{{{flags}}}}` here and its `{{positionals}}` is `{{{{args}}}}`",
SECTIONS.join(", ")
));
}
rest = &after[end + 2..];
}
Ok(())
}
pub fn substitute(template: &str, section: impl Fn(&str) -> Option<String>) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(at) = rest.find("{{") {
out.push_str(&rest[..at]);
let after = &rest[at + 2..];
let Some(end) = after.find("}}") else {
out.push_str(&rest[at..]);
return collapse_blank_runs(&out);
};
match section(after[..end].trim()) {
Some(text) => out.push_str(&text),
None => out.push_str(&rest[at..at + 2 + end + 2]),
}
rest = &after[end + 2..];
}
out.push_str(rest);
collapse_blank_runs(&out)
}
fn collapse_blank_runs(page: &str) -> String {
let mut out = String::with_capacity(page.len());
let mut blank = false;
for line in page.split('\n') {
if line.trim().is_empty() {
blank = !out.is_empty();
continue;
}
if !out.is_empty() {
out.push('\n');
if blank {
out.push('\n');
}
}
blank = false;
out.push_str(line);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitespace_alone_is_not_a_layout() {
assert!(!is_set(""));
assert!(!is_set(" \n\t"));
assert!(is_set("{{usage}}"));
assert!(check("").is_ok());
}
#[test]
fn a_placeholder_naming_no_section_is_refused_by_name() {
let err = check("{{about}}{{options}}").expect_err("no section is called options");
assert!(err.contains("\"options\""), "{err}");
assert!(err.contains("`{{flags}}`"), "{err}");
assert!(check("{{ about }} {{usage}}").is_ok());
assert!(check("no placeholders at all").is_ok());
assert!(check("{{usage").is_err());
}
#[test]
fn substitution_takes_only_the_names_it_is_given() {
let filled = substitute("[{{usage}}]{{ nope }}", |name| {
(name == "usage").then(|| "Usage: ex".to_string())
});
assert_eq!(filled, "[Usage: ex]{{ nope }}");
}
#[test]
fn a_section_that_came_out_empty_leaves_no_gap_behind() {
let template = "{{usage}}\n\n{{args}}\n\n{{flags}}";
let full = substitute(template, |name| {
Some(match name {
"usage" => "Usage: ex".to_string(),
"args" => "Arguments:\n <file>".to_string(),
_ => "Flags:\n --force".to_string(),
})
});
assert_eq!(
full,
"Usage: ex\n\nArguments:\n <file>\n\nFlags:\n --force"
);
let no_args = substitute(template, |name| {
Some(match name {
"usage" => "Usage: ex".to_string(),
"args" => String::new(),
_ => "Flags:\n --force".to_string(),
})
});
assert_eq!(no_args, "Usage: ex\n\nFlags:\n --force");
}
#[test]
fn a_sections_own_indentation_survives_the_collapsing() {
let page = substitute(" {{flags}}", |_| {
Some("Flags:\n --force Do it anyway".to_string())
});
assert_eq!(page, " Flags:\n --force Do it anyway");
}
}