Skip to main content

src/svg_path/parse.gleam

//// SVG path data parser.
////
//// This module parses the `d` attribute syntax used by SVG paths. It supports
//// comma and whitespace separators, compact signed numbers, relative commands,
//// implicit repeated commands, smooth curves, and arcs.

import gleam/float
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import svg_path

/// Errors returned while parsing SVG path data.
pub type Error {
  /// A parsed path was internally invalid according to the core path model.
  Core(svg_path.Error)

  /// An arc flag was not `0` or `1`.
  ExpectedArcFlag

  /// A command letter was expected.
  ExpectedCommand

  /// Path data must begin with a move command.
  ExpectedMove

  /// A numeric argument was expected.
  ExpectedNumber

  /// A numeric token could not be parsed as a float.
  InvalidNumber(String)

  /// The command letter is not supported by this library.
  UnsupportedCommand(String)
}

type Token {
  Command(String)
  Number(Float)
}

type State {
  State(
    subpaths: List(svg_path.Subpath),
    subpath: svg_path.Subpath,
    current: svg_path.Point,
    has_current: Bool,
    active: Bool,
    last_cubic_control: Option(svg_path.Point),
    last_quadratic_control: Option(svg_path.Point),
  )
}

/// Parse an SVG path data string into a `Path`.
///
/// Empty strings parse as an empty path. Move-only subpaths are preserved as
/// empty subpaths with start points. Closepath commands mark subpaths as
/// closed, inserting a straight line back to the subpath start when needed.
pub fn path(input: String) -> Result(svg_path.Path, Error) {
  case tokenize(input) {
    Error(error) -> Error(error)
    Ok(tokens) -> parse_tokens(tokens, initial_state())
  }
}

fn initial_state() -> State {
  State(
    subpaths: [],
    subpath: svg_path.empty_subpath(at: svg_path.point(0.0, 0.0)),
    current: svg_path.point(0.0, 0.0),
    has_current: False,
    active: False,
    last_cubic_control: None,
    last_quadratic_control: None,
  )
}

fn parse_tokens(
  tokens: List(Token),
  state: State,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [] -> finish(state)
    [Command(command), ..rest] -> parse_command(command, rest, state)
    [Number(_), ..] -> Error(ExpectedCommand)
  }
}

fn parse_command(
  command: String,
  tokens: List(Token),
  state: State,
) -> Result(svg_path.Path, Error) {
  case command {
    "M" -> parse_move(tokens, state, relative: False)
    "m" -> parse_move(tokens, state, relative: True)
    "L" -> parse_line(tokens, state, relative: False)
    "l" -> parse_line(tokens, state, relative: True)
    "Q" -> parse_quadratic_bezier(tokens, state, relative: False)
    "q" -> parse_quadratic_bezier(tokens, state, relative: True)
    "T" -> parse_smooth_quadratic_bezier(tokens, state, relative: False)
    "t" -> parse_smooth_quadratic_bezier(tokens, state, relative: True)
    "C" -> parse_cubic_bezier(tokens, state, relative: False)
    "c" -> parse_cubic_bezier(tokens, state, relative: True)
    "S" -> parse_smooth_cubic_bezier(tokens, state, relative: False)
    "s" -> parse_smooth_cubic_bezier(tokens, state, relative: True)
    "A" -> parse_arc(tokens, state, relative: False)
    "a" -> parse_arc(tokens, state, relative: True)
    "H" -> parse_horizontal(tokens, state, relative: False)
    "h" -> parse_horizontal(tokens, state, relative: True)
    "V" -> parse_vertical(tokens, state, relative: False)
    "v" -> parse_vertical(tokens, state, relative: True)
    "Z" | "z" -> parse_close(tokens, state)
    _ -> Error(UnsupportedCommand(command))
  }
}

fn parse_move(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case take_pair(tokens) {
    Error(error) -> Error(error)
    Ok(#(x, y, rest)) -> {
      case finish_active_subpath(state) {
        Error(error) -> Error(error)
        Ok(state) -> {
          let base = case relative && state.has_current {
            True -> state.current
            False -> svg_path.point(0.0, 0.0)
          }
          let target = offset(base, x, y)
          let state =
            State(
              ..state,
              subpath: svg_path.empty_subpath(at: target),
              current: target,
              has_current: True,
              active: True,
              last_cubic_control: None,
              last_quadratic_control: None,
            )

          parse_implicit_lines(rest, state, relative)
        }
      }
    }
  }
}

fn parse_implicit_lines(
  tokens: List(Token),
  state: State,
  relative: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_pair(tokens) {
        Error(error) -> Error(error)
        Ok(#(x, y, rest)) -> {
          case append_line_to(state, target_point(state, x, y, relative)) {
            Error(error) -> Error(error)
            Ok(state) -> parse_implicit_lines(rest, state, relative)
          }
        }
      }
    }
    _ -> parse_tokens(tokens, state)
  }
}

fn parse_line(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) -> parse_line_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_line_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_pair(tokens) {
        Error(error) -> Error(error)
        Ok(#(x, y, rest)) -> {
          case append_line_to(state, target_point(state, x, y, relative)) {
            Error(error) -> Error(error)
            Ok(state) ->
              parse_line_loop(rest, state, relative, parsed_any: True)
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_horizontal(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) -> parse_horizontal_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_horizontal_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(x), ..rest] -> {
      let target = case relative {
        True -> offset(state.current, x, 0.0)
        False -> svg_path.point(x, state.current.y)
      }

      case append_line_to(state, target) {
        Error(error) -> Error(error)
        Ok(state) ->
          parse_horizontal_loop(rest, state, relative, parsed_any: True)
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_vertical(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) -> parse_vertical_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_vertical_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(y), ..rest] -> {
      let target = case relative {
        True -> offset(state.current, 0.0, y)
        False -> svg_path.point(state.current.x, y)
      }

      case append_line_to(state, target) {
        Error(error) -> Error(error)
        Ok(state) ->
          parse_vertical_loop(rest, state, relative, parsed_any: True)
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_quadratic_bezier(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) ->
      parse_quadratic_bezier_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_quadratic_bezier_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_quadratic_bezier(tokens) {
        Error(error) -> Error(error)
        Ok(#(control_x, control_y, end_x, end_y, rest)) -> {
          let control = target_point(state, control_x, control_y, relative)
          let end = target_point(state, end_x, end_y, relative)
          let segment =
            svg_path.QuadraticBezier(start: state.current, control:, end:)

          case append_segment(state, segment, end) {
            Error(error) -> Error(error)
            Ok(state) -> {
              let state = remember_quadratic_control(state, control)
              parse_quadratic_bezier_loop(
                rest,
                state,
                relative,
                parsed_any: True,
              )
            }
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_smooth_quadratic_bezier(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) ->
      parse_smooth_quadratic_bezier_loop(
        tokens,
        state,
        relative,
        parsed_any: False,
      )
  }
}

fn parse_smooth_quadratic_bezier_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_pair(tokens) {
        Error(error) -> Error(error)
        Ok(#(end_x, end_y, rest)) -> {
          let control = reflected_quadratic_control(state)
          let end = target_point(state, end_x, end_y, relative)
          let segment =
            svg_path.QuadraticBezier(start: state.current, control:, end:)

          case append_segment(state, segment, end) {
            Error(error) -> Error(error)
            Ok(state) -> {
              let state = remember_quadratic_control(state, control)
              parse_smooth_quadratic_bezier_loop(
                rest,
                state,
                relative,
                parsed_any: True,
              )
            }
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_cubic_bezier(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) ->
      parse_cubic_bezier_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_cubic_bezier_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_cubic_bezier(tokens) {
        Error(error) -> Error(error)
        Ok(#(control1_x, control1_y, control2_x, control2_y, end_x, end_y, rest)) -> {
          let control1 = target_point(state, control1_x, control1_y, relative)
          let control2 = target_point(state, control2_x, control2_y, relative)
          let end = target_point(state, end_x, end_y, relative)
          let segment =
            svg_path.CubicBezier(
              start: state.current,
              control1:,
              control2:,
              end:,
            )

          case append_segment(state, segment, end) {
            Error(error) -> Error(error)
            Ok(state) -> {
              let state = remember_cubic_control(state, control2)
              parse_cubic_bezier_loop(rest, state, relative, parsed_any: True)
            }
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_smooth_cubic_bezier(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) ->
      parse_smooth_cubic_bezier_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_smooth_cubic_bezier_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_smooth_cubic_bezier(tokens) {
        Error(error) -> Error(error)
        Ok(#(control2_x, control2_y, end_x, end_y, rest)) -> {
          let control1 = reflected_cubic_control(state)
          let control2 = target_point(state, control2_x, control2_y, relative)
          let end = target_point(state, end_x, end_y, relative)
          let segment =
            svg_path.CubicBezier(
              start: state.current,
              control1:,
              control2:,
              end:,
            )

          case append_segment(state, segment, end) {
            Error(error) -> Error(error)
            Ok(state) -> {
              let state = remember_cubic_control(state, control2)
              parse_smooth_cubic_bezier_loop(
                rest,
                state,
                relative,
                parsed_any: True,
              )
            }
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_arc(
  tokens: List(Token),
  state: State,
  relative relative: Bool,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) -> parse_arc_loop(tokens, state, relative, parsed_any: False)
  }
}

fn parse_arc_loop(
  tokens: List(Token),
  state: State,
  relative: Bool,
  parsed_any parsed_any: Bool,
) -> Result(svg_path.Path, Error) {
  case tokens {
    [Number(_), ..] -> {
      case take_arc(tokens) {
        Error(error) -> Error(error)
        Ok(#(
          radius_x,
          radius_y,
          x_axis_rotation,
          large_arc,
          sweep,
          end_x,
          end_y,
          rest,
        )) -> {
          let end = target_point(state, end_x, end_y, relative)
          let segment =
            svg_path.Arc(
              start: state.current,
              radius: svg_path.point(radius_x, radius_y),
              x_axis_rotation:,
              large_arc:,
              sweep:,
              end:,
            )

          case append_segment(state, segment, end) {
            Error(error) -> Error(error)
            Ok(state) -> parse_arc_loop(rest, state, relative, parsed_any: True)
          }
        }
      }
    }
    _ -> {
      case parsed_any {
        True -> parse_tokens(tokens, state)
        False -> Error(ExpectedNumber)
      }
    }
  }
}

fn parse_close(
  tokens: List(Token),
  state: State,
) -> Result(svg_path.Path, Error) {
  case ensure_active(state) {
    Error(error) -> Error(error)
    Ok(Nil) -> {
      let assert Ok(start) = svg_path.start(state.subpath)
      case
        svg_path.set_closed_with(
          state.subpath,
          closed: True,
          policy: svg_path.Bridge,
        )
      {
        Error(error) -> Error(Core(error))
        Ok(subpath) -> {
          parse_tokens(
            tokens,
            State(
              subpaths: [subpath, ..state.subpaths],
              subpath: svg_path.empty_subpath(at: start),
              current: start,
              has_current: True,
              active: False,
              last_cubic_control: None,
              last_quadratic_control: None,
            ),
          )
        }
      }
    }
  }
}

fn finish(state: State) -> Result(svg_path.Path, Error) {
  case finish_active_subpath(state) {
    Error(error) -> Error(error)
    Ok(state) -> Ok(svg_path.Path(list.reverse(state.subpaths)))
  }
}

fn finish_active_subpath(state: State) -> Result(State, Error) {
  case state.active {
    False -> Ok(state)
    True ->
      Ok(
        State(
          ..state,
          subpaths: [state.subpath, ..state.subpaths],
          subpath: svg_path.empty_subpath(at: state.current),
          active: False,
        ),
      )
  }
}

fn append_line_to(
  state: State,
  target: svg_path.Point,
) -> Result(State, Error) {
  append_segment(
    state,
    svg_path.Line(start: state.current, end: target),
    target,
  )
}

fn append_segment(
  state: State,
  segment: svg_path.Segment,
  end: svg_path.Point,
) -> Result(State, Error) {
  case svg_path.append_segment(state.subpath, segment) {
    Error(error) -> Error(Core(error))
    Ok(subpath) -> {
      Ok(
        State(..state, subpath: subpath, current: end, active: True)
        |> clear_curve_controls,
      )
    }
  }
}

fn clear_curve_controls(state: State) -> State {
  State(..state, last_cubic_control: None, last_quadratic_control: None)
}

fn remember_cubic_control(state: State, control: svg_path.Point) -> State {
  State(
    ..state,
    last_cubic_control: Some(control),
    last_quadratic_control: None,
  )
}

fn remember_quadratic_control(state: State, control: svg_path.Point) -> State {
  State(
    ..state,
    last_cubic_control: None,
    last_quadratic_control: Some(control),
  )
}

fn reflected_cubic_control(state: State) -> svg_path.Point {
  case state.last_cubic_control {
    Some(control) -> reflect(control, around: state.current)
    None -> state.current
  }
}

fn reflected_quadratic_control(state: State) -> svg_path.Point {
  case state.last_quadratic_control {
    Some(control) -> reflect(control, around: state.current)
    None -> state.current
  }
}

fn reflect(
  point: svg_path.Point,
  around origin: svg_path.Point,
) -> svg_path.Point {
  svg_path.point(origin.x *. 2.0 -. point.x, origin.y *. 2.0 -. point.y)
}

fn ensure_active(state: State) -> Result(Nil, Error) {
  case state.active && state.has_current {
    True -> Ok(Nil)
    False -> Error(ExpectedMove)
  }
}

fn target_point(
  state: State,
  x: Float,
  y: Float,
  relative: Bool,
) -> svg_path.Point {
  case relative {
    True -> offset(state.current, x, y)
    False -> svg_path.point(x, y)
  }
}

fn offset(point: svg_path.Point, x: Float, y: Float) -> svg_path.Point {
  svg_path.point(point.x +. x, point.y +. y)
}

fn take_pair(
  tokens: List(Token),
) -> Result(#(Float, Float, List(Token)), Error) {
  case tokens {
    [Number(x), Number(y), ..rest] -> Ok(#(x, y, rest))
    _ -> Error(ExpectedNumber)
  }
}

fn take_quadratic_bezier(
  tokens: List(Token),
) -> Result(#(Float, Float, Float, Float, List(Token)), Error) {
  case tokens {
    [Number(control_x), Number(control_y), Number(end_x), Number(end_y), ..rest] -> {
      Ok(#(control_x, control_y, end_x, end_y, rest))
    }
    _ -> Error(ExpectedNumber)
  }
}

fn take_cubic_bezier(
  tokens: List(Token),
) -> Result(#(Float, Float, Float, Float, Float, Float, List(Token)), Error) {
  case tokens {
    [
      Number(control1_x),
      Number(control1_y),
      Number(control2_x),
      Number(control2_y),
      Number(end_x),
      Number(end_y),
      ..rest
    ] -> {
      Ok(#(control1_x, control1_y, control2_x, control2_y, end_x, end_y, rest))
    }
    _ -> Error(ExpectedNumber)
  }
}

fn take_smooth_cubic_bezier(
  tokens: List(Token),
) -> Result(#(Float, Float, Float, Float, List(Token)), Error) {
  case tokens {
    [
      Number(control2_x),
      Number(control2_y),
      Number(end_x),
      Number(end_y),
      ..rest
    ] -> {
      Ok(#(control2_x, control2_y, end_x, end_y, rest))
    }
    _ -> Error(ExpectedNumber)
  }
}

fn take_arc(
  tokens: List(Token),
) -> Result(
  #(Float, Float, Float, Bool, Bool, Float, Float, List(Token)),
  Error,
) {
  case tokens {
    [
      Number(radius_x),
      Number(radius_y),
      Number(x_axis_rotation),
      Number(large_arc),
      Number(sweep),
      Number(end_x),
      Number(end_y),
      ..rest
    ] -> {
      case arc_flag(large_arc) {
        Error(error) -> Error(error)
        Ok(large_arc) -> {
          case arc_flag(sweep) {
            Error(error) -> Error(error)
            Ok(sweep) -> {
              Ok(#(
                radius_x,
                radius_y,
                x_axis_rotation,
                large_arc,
                sweep,
                end_x,
                end_y,
                rest,
              ))
            }
          }
        }
      }
    }
    _ -> Error(ExpectedNumber)
  }
}

fn arc_flag(value: Float) -> Result(Bool, Error) {
  case value {
    0.0 -> Ok(False)
    1.0 -> Ok(True)
    _ -> Error(ExpectedArcFlag)
  }
}

fn tokenize(input: String) -> Result(List(Token), Error) {
  input
  |> string.to_graphemes
  |> tokenize_loop([])
}

fn tokenize_loop(
  graphemes: List(String),
  tokens: List(Token),
) -> Result(List(Token), Error) {
  case graphemes {
    [] -> Ok(list.reverse(tokens))
    [grapheme, ..rest] -> {
      case is_separator(grapheme) {
        True -> tokenize_loop(rest, tokens)
        False -> {
          case is_command(grapheme) {
            True -> tokenize_loop(rest, [Command(grapheme), ..tokens])
            False -> {
              case is_number_start(grapheme) {
                True -> {
                  let #(raw, rest) =
                    read_number(graphemes, [], previous_was_exponent: False)

                  case parse_number(raw) {
                    Ok(number) ->
                      tokenize_loop(rest, [Number(number), ..tokens])
                    Error(_) -> Error(InvalidNumber(raw))
                  }
                }
                False -> Error(UnsupportedCommand(grapheme))
              }
            }
          }
        }
      }
    }
  }
}

fn read_number(
  graphemes: List(String),
  number: List(String),
  previous_was_exponent previous_was_exponent: Bool,
) -> #(String, List(String)) {
  case graphemes {
    [] -> #(string.join(list.reverse(number), ""), [])
    [grapheme, ..rest] -> {
      case is_digit(grapheme) || grapheme == "." {
        True ->
          read_number(rest, [grapheme, ..number], previous_was_exponent: False)
        False -> {
          case grapheme == "e" || grapheme == "E" {
            True ->
              read_number(
                rest,
                [grapheme, ..number],
                previous_was_exponent: True,
              )
            False -> {
              case
                { previous_was_exponent || list.is_empty(number) }
                && { grapheme == "+" || grapheme == "-" }
              {
                True ->
                  read_number(
                    rest,
                    [grapheme, ..number],
                    previous_was_exponent: False,
                  )
                False -> #(string.join(list.reverse(number), ""), graphemes)
              }
            }
          }
        }
      }
    }
  }
}

fn is_separator(grapheme: String) -> Bool {
  grapheme == " "
  || grapheme == "\n"
  || grapheme == "\r"
  || grapheme == "\t"
  || grapheme == ","
}

fn is_command(grapheme: String) -> Bool {
  list.contains(
    [
      "M",
      "m",
      "L",
      "l",
      "Q",
      "q",
      "T",
      "t",
      "C",
      "c",
      "S",
      "s",
      "A",
      "a",
      "H",
      "h",
      "V",
      "v",
      "Z",
      "z",
    ],
    grapheme,
  )
}

fn is_number_start(grapheme: String) -> Bool {
  is_digit(grapheme) || grapheme == "+" || grapheme == "-" || grapheme == "."
}

fn is_digit(grapheme: String) -> Bool {
  list.contains(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], grapheme)
}

fn parse_number(raw: String) -> Result(Float, Nil) {
  case string.split_once(raw, on: "e") {
    Ok(#(mantissa, exponent)) -> parse_exponent_number(mantissa, exponent)
    Error(_) -> {
      case string.split_once(raw, on: "E") {
        Ok(#(mantissa, exponent)) -> parse_exponent_number(mantissa, exponent)
        Error(_) -> parse_decimal_number(raw)
      }
    }
  }
}

fn parse_decimal_number(raw: String) -> Result(Float, Nil) {
  let raw = strip_leading_plus(raw)

  case float.parse(raw) {
    Ok(number) -> Ok(number)
    Error(_) -> {
      case int.parse(raw) {
        Ok(number) -> Ok(int.to_float(number))
        Error(_) -> Error(Nil)
      }
    }
  }
}

fn parse_exponent_number(
  mantissa: String,
  exponent: String,
) -> Result(Float, Nil) {
  case parse_decimal_number(mantissa) {
    Error(_) -> Error(Nil)
    Ok(mantissa) -> {
      case exponent |> strip_leading_plus |> int.parse {
        Error(_) -> Error(Nil)
        Ok(exponent) -> Ok(mantissa *. power_of_ten(exponent))
      }
    }
  }
}

fn strip_leading_plus(raw: String) -> String {
  case string.starts_with(raw, "+") {
    True -> string.drop_start(raw, up_to: 1)
    False -> raw
  }
}

fn power_of_ten(exponent: Int) -> Float {
  case exponent {
    0 -> 1.0
    _ if exponent > 0 -> 10.0 *. power_of_ten(exponent - 1)
    _ -> power_of_ten(exponent + 1) /. 10.0
  }
}