///|
/// Parsing for the `animation` / `transition` properties (shorthands and
/// longhands). Values are comma-separated lists; commas inside function
/// arguments (e.g. `cubic-bezier(...)`, `steps(...)`, `linear(...)`) are not
/// separators, so splitting is done at paren depth 0 on the token stream.

///|
/// Split a token view into comma-separated segments at paren depth 0.
fn split_top_level_commas(
  tokens : ArrayView[@token.Token],
) -> Array[ArrayView[@token.Token]] {
  let segments : Array[ArrayView[@token.Token]] = []
  let mut depth = 0
  let mut start = 0
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Function(_) | @token.Token::LeftParen => depth = depth + 1
      @token.Token::RightParen => if depth > 0 { depth = depth - 1 }
      @token.Token::Comma => if depth == 0 {
        segments.push(tokens[start:i])
        start = i + 1
      }
      _ => ()
    }
  }
  segments.push(tokens[start:])
  segments
}

///|
/// Reconstruct CSS source text for a token slice (used to feed sub-parsers such
/// as `parse_easing`). Only the token kinds that appear in animation/transition
/// values are handled.
fn reconstruct_css(tokens : ArrayView[@token.Token]) -> String {
  let sb = StringBuilder::new()
  for i = 0; i < tokens.length(); i = i + 1 {
    match tokens[i] {
      @token.Token::Ident(s) => sb.write_string(s)
      @token.Token::Function(name) => {
        sb.write_string(name)
        sb.write_char('(')
      }
      @token.Token::Number(v, _) => sb.write_string(format_number(v))
      @token.Token::Dimension(v, unit) => {
        sb.write_string(format_number(v))
        sb.write_string(unit)
      }
      @token.Token::Percentage(v) => {
        sb.write_string(format_number(v))
        sb.write_char('%')
      }
      @token.Token::LeftParen => sb.write_char('(')
      @token.Token::RightParen => sb.write_char(')')
      @token.Token::Comma => sb.write_char(',')
      @token.Token::Whitespace => sb.write_char(' ')
      @token.Token::Delim(c) => sb.write_char(c)
      @token.Token::Colon => sb.write_char(':')
      _ => ()
    }
  }
  sb.to_string()
}

///|
/// Format a Double for CSS reconstruction, dropping a trailing `.0` for
/// integers (so `4.0` becomes `4`, matching the original source closely enough
/// for the sub-parsers).
fn format_number(v : Double) -> String {
  let i = v.to_int()
  if i.to_double() == v {
    i.to_string()
  } else {
    v.to_string()
  }
}

///|
/// Parse a single `