///|
fn Parser::parse_string_group(
  self : Parser,
  optional : Bool,
) -> Token? raise ParseFailure {
  match self.gullet.scan_argument(optional) {
    None => None
    Some(arg_token) => {
      let builder = StringBuilder()
      for ;; {
        let token = self.fetch()
        if token.text == "EOF" {
          self.consume()
          arg_token.text = builder.to_string()
          break Some(arg_token)
        }
        builder.write_string(token.text)
        self.consume()
      }
    }
  }
}

///|
fn normalized_color(text : String) -> String? {
  if text.length() > 0 && text[0] == '#' {
    let digits = text.unsafe_substring(start=1, end=text.length())
    if (
        digits.length() == 3 ||
        digits.length() == 4 ||
        digits.length() == 6 ||
        digits.length() == 8
      ) &&
      digits.all(Char::is_ascii_hexdigit) {
      Some(text)
    } else {
      None
    }
  } else if text.length() == 6 && text.all(Char::is_ascii_hexdigit) {
    Some("#\{text}")
  } else if !text.is_empty() && text.all(Char::is_ascii_alphabetic) {
    Some(text)
  } else {
    None
  }
}

///|
fn Parser::parse_color_group(
  self : Parser,
  optional : Bool,
) -> ParseNode? raise ParseFailure {
  self
  .parse_string_group(optional)
  .map(token => {
    guard normalized_color(token.text) is Some(color) else {
      raise InvalidArgument(
        message="Invalid color: '\{token.text}'",
        loc=token.loc,
      )
    }
    ColorToken(mode=self.mode, color~)
  })
}