///|
/// Models the extracted help text. Section matching is case-insensitive.
pub struct Document {
text : String
usage : String
program : String
priv option_sections : Array[String]
}
///|
/// Whitespace splitter used by both usage and option declarations.
fn words(text : String) -> Array[String] {
let out = []
let buf = StringBuilder()
for c in text {
if c.is_whitespace() {
if !buf.is_empty() {
out.push(buf.to_string())
buf.reset()
}
} else {
buf.write_char(c)
}
}
if !buf.is_empty() {
out.push(buf.to_string())
}
out
}
///|
/// Mirrors docopt's section boundaries without a regex dependency.
fn sections(name : String, text : String) -> Array[String] {
let result = []
let mut active : String? = None
for line in text.split("\n") {
let raw = line.to_owned()
let lower = raw.to_lower()
match lower.find(name) {
Some(start) => {
if active is Some(previous) {
result.push(previous)
}
active = Some(raw[start:].to_owned())
}
None =>
if raw.length() > 0 && raw[0].to_int() > 32 {
if active is Some(previous) {
result.push(previous)
}
active = None
} else if active is Some(previous) {
active = Some(previous + "\n" + raw)
}
}
}
if active is Some(previous) {
result.push(previous)
}
result
}
///|
/// Require exactly one Usage section and a program token.
pub fn Document::new(text : String) -> Document raise DocoptError {
guard text.length() <= 1048576 else {
raise Limit("document exceeds hard safety ceiling")
}
let usages = sections("usage:", text)
guard usages.length() == 1 else {
raise Language("expected exactly one Usage: section")
}
let tokens = words(usages[0][6:].to_owned())
guard tokens.length() > 0 else {
raise Language("Usage: has no program name")
}
{
text,
usage: usages[0],
program: tokens[0],
option_sections: sections("options:", text),
}
}
///|
/// Returns copies so callers cannot alter later compilation.
pub fn Document::options_sections(self : Document) -> Array[String] {
self.option_sections.copy()
}