//// Percent Encodes strings according to RFC 3986. //// //// See the following links for reference: //// - import gleam/bit_array import gleam/bool import gleam/list import gleam/order import gleam/pair import gleam/result import gleam/string import internal/encoder/encoding /// Estimates the percent encoded size of a string in bytes, taking /// maximum size of a line into account. pub fn estimate_encoded_size( of string: String, maximum_size maximum_size: Int, ) -> Result(Int, encoding.EncoderError) { string |> string.split("") |> list.try_fold(0, fn(size, character) { let character_size = string.byte_size(character) case should_encode_character(character) { True -> Ok(size + { 3 * character_size }) False -> Ok(size + character_size) } |> result.try(fn(size) { case size <= maximum_size { True -> Ok(size) False -> Error(encoding.MaximumSizeExceeded(maximum_size)) } }) }) } /// Percent encode a string, taking the maximum line size into /// account and pretending to start the first line at the passed /// start position. pub fn encode_string( encode string: String, start position: Int, maximum_size maximum_size: Int, ) -> Result(List(String), encoding.EncoderError) { string |> string.split("") |> do_encode_string(position, maximum_size) } fn do_encode_string( characters: List(String), count: Int, maximum_size: Int, ) -> Result(List(String), encoding.EncoderError) { characters |> list.try_fold(#(count, []), fn(accumulator, character) { let #(used_size, lines) = accumulator let encoded_character = encode_character(character) let encoded_character_size = string.byte_size(encoded_character) case lines { [] -> Ok(#(encoded_character_size, [encoded_character])) [first_line, ..other_lines] if used_size + encoded_character_size <= maximum_size -> Ok( #(used_size + encoded_character_size, [ first_line <> encoded_character, ..other_lines ]), ) _otherwise -> Error(encoding.MaximumSizeExceeded(maximum_size)) } }) |> result.map(pair.second) } fn should_encode_character(character: String) -> Bool { { string.compare(character, "0") == order.Lt || string.compare(character, "9") == order.Gt } && { string.compare(character, "A") == order.Lt || string.compare(character, "Z") == order.Gt } && { string.compare(character, "a") == order.Lt || string.compare(character, "z") == order.Gt } && !string.contains("-_.~", character) } fn encode_character(character: String) -> String { use <- bool.guard( when: !should_encode_character(character), return: character, ) do_encode_bytes(bit_array.from_string(character), []) } fn do_encode_bytes(bytes: BitArray, acc: List(String)) -> String { case bytes { <> -> do_encode_bytes(rest, ["%" <> bit_array.base16_encode(byte), ..acc]) <<_:bits>> -> acc |> list.reverse() |> string.join("") } }