///|
fn is_ascii_digit_code_unit(code : UInt16) -> Bool {
  code >= '0' && code <= '9'
}

///|
fn skip_ascii_spaces(text : String, start : Int) -> Int {
  for index = start; index < text.length(); index = index + 1 {
    if text[index] != ' ' {
      break index
    }
  } nobreak {
    text.length()
  }
}

///|
fn size_scan_candidate(text : String) -> Bool {
  // Mirrors KaTeX's anchored regex for every intermediate token prefix.
  let len = text.length()
  let mut index = 0
  if index < len && (text[index] == '+' || text[index] == '-') {
    index = index + 1
  }
  index = skip_ascii_spaces(text, index)
  guard index < len else { true }
  if is_ascii_digit_code_unit(text[index]) {
    while index < len && is_ascii_digit_code_unit(text[index]) {
      index = index + 1
    }
    if index < len && text[index] == '.' {
      index = index + 1
      while index < len && is_ascii_digit_code_unit(text[index]) {
        index = index + 1
      }
    }
  } else if text[index] == '.' {
    index = index + 1
    while index < len && is_ascii_digit_code_unit(text[index]) {
      index = index + 1
    }
  } else {
    return false
  }
  index = skip_ascii_spaces(text, index)
  let unit_start = index
  while index < len && text[index] >= 'a' && text[index] <= 'z' {
    index = index + 1
  }
  guard index - unit_start <= 2 else { false }
  index = skip_ascii_spaces(text, index)
  index == len
}

///|
fn Parser::parse_size_regex_group(self : Parser) -> Token raise ParseFailure {
  let first_token = self.fetch()
  let mut last_token = first_token
  let builder = StringBuilder()
  for ;; {
    let token = self.fetch()
    guard token.text != "EOF" else { break }
    let candidate = builder.to_string() + token.text
    guard size_scan_candidate(candidate) else { break }
    builder.write_string(token.text)
    last_token = token
    self.consume()
  }
  let text = builder.to_string()
  guard !text.is_empty() else {
    raise InvalidArgument(
      message="Invalid size: '" + first_token.text + "'",
      loc=first_token.loc,
    )
  }
  first_token.range(last_token, text)
}

///|
fn parse_decimal(text : String) -> Double raise ParseFailure {
  @string.from_str(text) catch {
    _ =>
      raise InternalInvariant(
        message="Validated size number failed Double conversion: " + text,
      )
  }
}

///|
fn parse_size_measurement(text : String) -> Measurement? raise ParseFailure {
  // KaTeX's final measurement regex is intentionally unanchored.
  let len = text.length()
  for start in 0.. 0 || digits_after_dot > 0 else { continue }
    index = skip_ascii_spaces(text, index)
    guard index + 2 <= len else { continue }
    let first = text[index]
    let second = text[index + 1]
    guard first >= 'a' && first <= 'z' && second >= 'a' && second <= 'z' else {
      continue
    }
    let unit = text.unsafe_substring(start=index, end=index + 2)
    return Some({ number: parse_decimal(number_builder.to_string()), unit })
  }
  None
}

///|
fn valid_size_unit(unit : String) -> Bool {
  match unit {
    "pt"
    | "mm"
    | "cm"
    | "in"
    | "bp"
    | "pc"
    | "dd"
    | "cc"
    | "nd"
    | "nc"
    | "sp"
    | "px"
    | "ex"
    | "em"
    | "mu" => true
    _ => false
  }
}

///|
fn Parser::parse_size_group(
  self : Parser,
  optional : Bool,
) -> ParseNode? raise ParseFailure {
  self.gullet.consume_spaces()
  let parsed = if !optional && self.gullet.future().text != "{" {
    Some(self.parse_size_regex_group())
  } else {
    self.parse_string_group(optional)
  }
  match parsed {
    None => None
    Some(token) => {
      let mut text = token.text
      let is_blank = !optional && text.is_empty()
      if is_blank {
        text = "0pt"
      }
      guard parse_size_measurement(text) is Some(value) else {
        raise InvalidArgument(
          message="Invalid size: '" + text + "'",
          loc=token.loc,
        )
      }
      guard valid_size_unit(value.unit) else {
        raise InvalidArgument(
          message="Invalid unit: '" + value.unit + "'",
          loc=token.loc,
        )
      }
      Some(Size(mode=self.mode, value~, is_blank~))
    }
  }
}