import gleam/bit_array import gleam/dict.{type Dict} import gleam/int import gleam/list import gleam/result import gleam/string pub type DecompressError { EInvalidInput } type DecodeType { Char(#(BitArray, BitArray, Dict(Int, BitArray))) Index(#(Int, BitArray)) EOF } /// Compress a string to a BitArray /// ## Example /// ```gleam /// let compressed = compress_to_uint8(string: "Hello World") /// ``` /// pub fn compress_to_uint8(string: String) -> BitArray { compress(string) } /// Decompress an lz-string BitArray back to the original UTF16 string /// ## Example /// ```gleam /// let assert Ok(decompressed) = decompress_from_uint8(bits: <<5, 132, 178, 0>>) /// ``` /// pub fn decompress_from_uint8(bits: BitArray) -> Result(String, DecompressError) { decompress(bits) } /// Compress a string into an ASCII base64 encoded representation /// ## Example /// ```gleam /// let compressed = compress_to_base64(string: "Hello World") /// ``` /// pub fn compress_to_base64(string: String) -> String { string |> compress |> bit_array.base64_encode(True) } /// Decompress an lz-string base64 string back to the original UTF16 string /// ## Example /// ```gleam /// let assert Ok(decompressed) = decompress_from_base64(base64_string: "BYUwNmD2A0AECWsCGBbZtDUzkAA=") /// ``` /// pub fn decompress_from_base64( base64_string: String, ) -> Result(String, DecompressError) { "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" |> string.to_graphemes() |> list.index_map(fn(x, i) { #(x, i) }) |> dict.from_list() |> decode_base64(string.to_graphemes(base64_string), _, <<>>) |> result.try(decompress) } /// Compress a string into ASCII representing the original string with a few changes to make it URI safe /// ## Example /// ```gleam /// let compressed = compress_to_encoded_uri(string: "Hello World") /// ``` /// pub fn compress_to_encoded_uri(string: String) -> String { string |> compress_to_base64 |> string.replace("/", "-") |> string.replace("=", "$") } /// Decompress an lz-string URI string back to the original UTF16 string /// ## Example /// ```gleam /// let assert Ok(decompressed) = decompress_from_encoded_uri(uri_string: "BYUwNmD2A0AECWsCGBbZtDUzkAA$") /// ``` /// pub fn decompress_from_encoded_uri( uri_string: String, ) -> Result(String, DecompressError) { uri_string |> string.replace("-", "/") |> string.replace("$", "=") |> decompress_from_base64 } fn decode_base64( string_list: List(String), key_dict: Dict(String, Int), bitstring: BitArray, ) { case string_list { [char, ..rest] -> { case dict.get(key_dict, char) { Ok(num) -> { decode_base64(rest, key_dict, <>:bits>>) } _ -> Error(EInvalidInput) } } [] -> Ok(bitstring) } } fn compress(string: String) { case string { "" -> <<"":utf8>> _ -> { let bitstring = string.to_utf_codepoints(string) |> compress_string("", _, dict.new(), <<>>) let padding_bits = 16 - { { bitstring |> bit_size(0) } % 16 } <> } } } fn compress_string( w: String, str: List(UtfCodepoint), dict: Dict(String, #(Int, Bool)), final_str: BitArray, ) -> BitArray { case str { [] -> { let size = find_bits(dict.size(dict) + 2) let #(_dict, output) = w_output(w, dict, False) <>):bits>> } [c, ..rest] -> { let char_just_added = !dict.has_key(dict, string.from_utf_codepoints([c])) let dict = case char_just_added { True -> { dict.insert(dict, string.from_utf_codepoints([c]), #( dict.size(dict) + 3, True, )) } False -> { dict } } let wc = w <> string.from_utf_codepoints([c]) case dict.has_key(dict, wc) { True -> { compress_string(wc, rest, dict, final_str) } False -> { let #(dict, output) = w_output(w, dict, char_just_added) let dict = dict.insert(dict, wc, #(dict.size(dict) + 3, False)) compress_string(string.from_utf_codepoints([c]), rest, dict, << final_str:bits, output:bits, >>) } } } } } fn w_output(w: String, dict: Dict(String, #(Int, Bool)), char_just_added: Bool) { // This should never be reached if w isn't already in the dict let assert Ok(var) = dict.get(dict, w) case var { #(index, True) -> { let dict = dict.insert(dict, w, #(index, False)) let marker_size = find_bits(index) let assert <> = <> let char_val = string.utf_codepoint_to_int(char_codepoint) let #(size_marker, char_size) = { case find_bits(char_val) { index if index <= 8 -> #(0, 8) _ -> #(1, 16) } } let size_marker_bits = reverse(<>) let char_bits = reverse(<>) #(dict, <>) } #(index, False) -> { let map_size = dict.size(dict) + 2 let map_size = case char_just_added { True -> map_size - 1 False -> map_size } let size = find_bits(map_size) #(dict, reverse(<>)) } } } fn decompress(bstring) -> Result(String, DecompressError) { case bstring { <<>> -> Ok("") _ -> { result.try(decode_next_segment(bstring, dict.new()), fn(return) { case return { Char(char) -> { result.try( decompress_string(char.0, char.1, char.2, <<>>), fn(string) { to_utf16(string, "") }, ) } _ -> Error(EInvalidInput) } }) } } } fn decompress_string( w: BitArray, str: BitArray, dict: Dict(Int, BitArray), final_str: BitArray, ) -> Result(BitArray, DecompressError) { result.try(decode_next_segment(str, dict), fn(return) { case return { Char(char) -> { let dict = dict.insert( char.2, dict.size(char.2) + 3, bit_array.append(w, char.0), ) decompress_string(char.0, char.1, dict, <>) } Index(seq) -> { let c = case dict.get(dict, seq.0) { Ok(value) -> Ok(value) Error(Nil) -> { case { dict.size(dict) + 3 } == seq.0 { True -> Ok(bit_array.append(w, <>)) False -> Error(EInvalidInput) } } } result.try(c, fn(c) { let dict = dict.insert( dict, dict.size(dict) + 3, bit_array.append(w, <>), ) decompress_string(c, seq.1, dict, <>) }) } EOF -> { Ok(<>) } } }) } fn decode_next_segment(bitstring, dict) -> Result(DecodeType, DecompressError) { let size = { dict.size(dict) + 3 } |> find_bits case bitstring { <> -> { let assert <> = reverse(<>) case dict_entry { 0 -> { case rest { <> -> { let assert <> = reverse(<>) let assert Ok(codepoint) = string.utf_codepoint(c) let char = <> let dict = dict.insert(dict, dict.size(dict) + 3, char) Ok(Char(#(char, rest, dict))) } _ -> Error(EInvalidInput) } } 1 -> { case rest { <> -> { let assert <> = reverse(<>) let char = <> let dict = dict.insert(dict, dict.size(dict) + 3, char) Ok(Char(#(char, rest, dict))) } _ -> Error(EInvalidInput) } } 2 -> { Ok(EOF) } index -> { Ok(Index(#(index, rest))) } } } _ -> Error(EInvalidInput) } } // HELPERS fn to_utf16(bitstring: BitArray, string: String) { case bitstring { <<>> -> Ok(string) <> -> { case bytes { surrogate if surrogate >= 0xD800 && surrogate <= 0xDFFF -> { //check if high or low surrogate case surrogate { high if high >= 0xD800 && high <= 0xDBFF -> { case rest { <> -> { // Convert surrogates to codepoint - https://www.unicode.org/versions/Unicode3.0.0/ch03.pdf let codepoint = { high - 0xD800 } * 0x400 + { low - 0xDC00 } + 0x10000 let assert Ok(codepoint) = string.utf_codepoint(codepoint) to_utf16( rest, string <> string.from_utf_codepoints([codepoint]), ) } _ -> Error(EInvalidInput) } } _ -> Error(EInvalidInput) } } other -> { let assert Ok(str) = case other { 65_534 -> { bit_array.to_string(<<239, 191, 190>>) } 65_535 -> { bit_array.to_string(<<239, 191, 191>>) } any -> { let assert Ok(codepoint) = string.utf_codepoint(any) Ok(string.from_utf_codepoints([codepoint])) } } to_utf16(rest, string <> str) } } } _ -> { //impossible to reach Error(EInvalidInput) } } } fn find_bits(num: Int) { num |> int.to_base2 |> string.length } fn reverse(bitstring: BitArray) { case bitstring { <<>> -> { <<>> } _ -> { let assert <> = bitstring bit_array.append(reverse(rest), value) } } } fn bit_size(bits: BitArray, size: Int) { case bits { <<_:bits-size(1), rest:bits>> -> bit_size(rest, size + 1) _ -> size } }