//// Percent Encodes strings according to RFC 3986.
////
//// See the following links for reference:
//// - <https://tools.ietf.org/html/rfc3986>
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 {
<<byte:bytes-size(1), rest:bytes>> ->
do_encode_bytes(rest, ["%" <> bit_array.base16_encode(byte), ..acc])
<<_:bits>> -> acc |> list.reverse() |> string.join("")
}
}