///|
fn valid_attribute_char(char : Char) -> Bool {
  char.is_ascii_alphabetic() ||
  char.is_ascii_digit() ||
  char == '-' ||
  char == '_' ||
  char == '.'
}

///|
/// Returns whether `name` follows Git's attribute-name grammar.
pub fn is_valid_attribute_name(name : String) -> Bool {
  guard name.length() > 0 else { false }
  guard name.get_char(0) is Some(first) else { false }
  if first == '-' || !valid_attribute_char(first) {
    return false
  }
  for char in name {
    if !valid_attribute_char(char) {
      return false
    }
  }
  true
}

///|
/// Parses one attribute token such as `text`, `-diff`, `eol=lf`, or `!merge`.
pub fn parse_assignment(token : String) -> Assignment raise AttributeError {
  let trimmed = token.trim().to_owned()
  guard trimmed.length() > 0 else { raise InvalidAssignment(token) }
  let (name, state) = if trimmed.has_prefix("-") {
    (trimmed.unsafe_substring(start=1, end=trimmed.length()), Unset)
  } else if trimmed.has_prefix("!") {
    (trimmed.unsafe_substring(start=1, end=trimmed.length()), Unspecified)
  } else {
    match trimmed.split_once("=") {
      Some((name, value)) => (name.to_owned(), Value(value.to_owned()))
      None => (trimmed, Set)
    }
  }
  guard is_valid_attribute_name(name) else { raise InvalidAttributeName(name) }
  { name, state, }
}

///|
/// Converts an assignment back to its canonical token form.
pub fn Assignment::to_token(self : Assignment) -> String {
  match self.state {
    Set => self.name
    Unset => "-\{self.name}"
    Unspecified => "!\{self.name}"
    Value(value) => "\{self.name}=\{value}"
  }
}