// goctl's `--style` is a naming template, not a fixed set of cases. The template is
// `GOZERO`: the casing of the `GO` marker sets the first
// word's case, the casing of `ZERO` sets every later word's, and `` is what
// goes between them. So `gozero` names `welcometogozero`, `goZero` names
// `welcomeToGoZero`, `go_zero` names `welcome_to_go_zero`, and `Go#zero` names
// `Welcome#to#go#zero`. Everything moonctl invents a name for — the files of a
// generated tree and the handler/logic/middleware entry points inside them — is
// spelled through one of these.
///|
/// A `--style` template that is not `GOZERO`: either a
/// marker is missing (`go`, `zero`), they are the wrong way round, or one is cased
/// neither `go`/`GO`/`Go` nor `zero`/`ZERO`/`Zero` (`gOZero`, `goZEro`).
pub suberror StyleError {
BadStyle(String)
}
///|
impl Show for StyleError with fn output(self, logger) {
match self {
BadStyle(s) =>
logger.write_string(
"style \"" + s + "\" is not GOZERO",
)
}
}
///|
/// The case a marker asks its words to be spelled in.
enum Case {
Lower
Upper
Title
}
///|
/// Uppercase every ASCII letter (`String` has no `to_upper`).
fn upper_all(s : String) -> String {
let mut out = ""
for i = 0; i < s.length(); i = i + 1 {
let c = s[i].to_int()
if c >= 0x61 && c <= 0x7A {
out = out +
(match (c - 0x20).to_char() {
Some(ch) => ch.to_string()
None => s[i:i + 1].to_owned()
})
} else {
out = out + s[i:i + 1].to_owned()
}
}
out
}
///|
/// Whether `c` is an ASCII capital — the letter that starts a new word.
fn is_cap(c : UInt16) -> Bool {
c >= 'A' && c <= 'Z'
}
///|
/// The words of `name`: `_` separates them, and a capital starts a new one, so
/// `welcome_to_go_zero` and `WelcomeToGoZero` both split into four.
fn words(name : String) -> Array[String] {
let out : Array[String] = []
let mut w = ""
for i = 0; i < name.length(); i = i + 1 {
let c = name[i]
if c == '_' {
if w != "" {
out.push(w)
w = ""
}
} else if is_cap(c) && w != "" {
out.push(w)
w = name[i:i + 1].to_owned()
} else {
w = w + name[i:i + 1].to_owned()
}
}
if w != "" {
out.push(w)
}
out
}
///|
/// The case a marker is written in, or `None` when it is written in none of them.
fn case_of(marker : String) -> Case? {
let lower = marker.to_lower()
if marker == lower {
Some(Lower)
} else if marker == upper_all(lower) {
Some(Upper)
} else if marker == upper_first(lower) {
Some(Title)
} else {
None
}
}
///|
fn cased(w : String, c : Case) -> String {
match c {
Lower => w.to_lower()
Upper => upper_all(w)
Title => upper_first(w.to_lower())
}
}
///|
/// A `--style` naming template (← goctl's `--style`), ready to spell names with.
/// Build one with `Style::parse`, apply it with `Style::format`.
pub struct Style {
before : String
through : String
after : String
head : Case
tail : Case
}
///|
/// goctl's default style, `gozero`: every word lower-cased and run together.
pub fn Style::gozero() -> Style {
{ before: "", through: "", after: "", head: Lower, tail: Lower, }
}
///|
/// Read a `--style` template. It must contain `GO` and then `ZERO` — in any case,
/// with anything before, between and after them:
///
/// ```
/// Style::parse("gozero").format("welcome_to_go_zero") // welcometogozero
/// Style::parse("goZero").format("welcome_to_go_zero") // welcomeToGoZero
/// Style::parse("go_zero").format("welcome_to_go_zero") // welcome_to_go_zero
/// Style::parse("Go#zero").format("welcome_to_go_zero") // Welcome#to#go#zero
/// ```
///
/// The `GO` marker's own casing (`go`, `GO` or `Go`) says how the first word is
/// spelled, `ZERO`'s says how every later word is, and what stands between the two
/// markers is what stands between the words. A template missing a marker (`go`,
/// `zero`), holding them in the wrong order, or casing one of them any other way
/// (`gOZero`, `goZEro`) raises `StyleError`.
pub fn Style::parse(s : String) -> Style raise StyleError {
let up = upper_all(s)
let go = index_of_str(up, "GO")
let zero = index_of_str(up, "ZERO")
if go < 0 || zero < go + 2 {
raise BadStyle(s)
}
let head = match case_of(s[go:go + 2].to_owned()) {
Some(c) => c
None => raise BadStyle(s)
}
let tail = match case_of(s[zero:zero + 4].to_owned()) {
Some(c) => c
None => raise BadStyle(s)
}
{
before: s[0:go].to_owned(),
through: s[go + 2:zero].to_owned(),
after: s[zero + 4:].to_owned(),
head,
tail,
}
}
///|
/// Spell `name` in this style. `name` is split into words on `_` and before each
/// capital, the first word takes the `GO` marker's case and the rest take `ZERO`'s,
/// and they are joined with whatever stood between the markers.
pub fn Style::format(self : Style, name : String) -> String {
let ws = words(name)
let mut out = ""
for i = 0; i < ws.length(); i = i + 1 {
if i > 0 {
out = out + self.through
}
out = out + cased(ws[i], if i == 0 { self.head } else { self.tail })
}
self.before + out + self.after
}