diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/gleam/uri.gleam | 135 | ||||
-rw-r--r-- | src/gleam_stdlib.erl | 71 | ||||
-rw-r--r-- | src/gleam_stdlib.js | 16 |
3 files changed, 152 insertions, 70 deletions
diff --git a/src/gleam/uri.gleam b/src/gleam/uri.gleam index 5482478..6a931e4 100644 --- a/src/gleam/uri.gleam +++ b/src/gleam/uri.gleam @@ -7,11 +7,8 @@ //// Query encoding (Form encoding) is defined in the w3c specification. //// https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data -if erlang { - import gleam/dynamic.{Dynamic} -} - import gleam/function +import gleam/string_builder.{StringBuilder} import gleam/int import gleam/list import gleam/map @@ -216,72 +213,80 @@ if javascript { "../gleam_stdlib.js" "parse_query" } -if erlang { - type Encoding { - Utf8 - } +/// Encodes a list of key value pairs as a URI query string. +/// +/// The opposite operation is `uri.parse_query`. +/// +/// ## Examples +/// +/// ``` +/// > query_to_string([#("a", "1"), #("b", "2")]) +/// +/// "a=1&b=2" +/// ``` +/// +pub fn query_to_string(query: List(#(String, String))) -> String { + query + |> list.map(query_pair) + |> list.intersperse(string_builder.from_string("&")) + |> string_builder.concat + |> string_builder.to_string +} - type ErlQueryToStringOption { - Encoding(Encoding) - } +fn query_pair(pair: #(String, String)) -> StringBuilder { + string_builder.from_strings([ + percent_encode(pair.0), + "=", + percent_encode(pair.1), + ]) +} - external fn erl_query_to_string( - List(#(String, String)), - List(ErlQueryToStringOption), - ) -> Dynamic = - "uri_string" "compose_query" +/// Encodes a string into a percent encoded representation. +/// +/// ## Examples +/// +/// ``` +/// > percent_encode("100% great") +/// +/// "100%25%20great" +/// ``` +/// +pub fn percent_encode(value: String) -> String { + do_percent_encode(value) +} - /// Encodes a list of key value pairs as a URI query string. - /// - /// The opposite operation is `uri.parse_query`. - /// - /// ## Examples - /// - /// ``` - /// > query_to_string([#("a", "1"), #("b", "2")]) - /// - /// "a=1&b=2" - /// ``` - /// - pub fn query_to_string(query: List(#(String, String))) -> String { - query - |> erl_query_to_string([Encoding(Utf8)]) - |> dynamic.string - |> result.unwrap("") - } +if erlang { + external fn do_percent_encode(String) -> String = + "gleam_stdlib" "percent_encode" +} - /// Encodes a string into a percent encoded representation. - /// Note that this encodes space as +. - /// - /// ## Examples - /// - /// ``` - /// > percent_encode("100% great") - /// - /// "100%25+great" - /// ``` - /// - pub fn percent_encode(value: String) -> String { - query_to_string([#("k", value)]) - |> string.replace(each: "k=", with: "") - } +if javascript { + external fn do_percent_encode(String) -> String = + "../gleam_stdlib.js" "percent_encode" +} - /// Decodes a percent encoded string. - /// - /// ## Examples - /// - /// ``` - /// > percent_decode("100%25+great") - /// - /// Ok("100% great") - /// ``` - /// - pub fn percent_decode(value: String) -> Result(String, Nil) { - string.concat(["k=", value]) - |> parse_query - |> result.then(list.head) - |> result.map(pair.second) - } +/// Decodes a percent encoded string. +/// +/// ## Examples +/// +/// ``` +/// > percent_decode("100%25+great") +/// +/// Ok("100% great") +/// ``` +/// +pub fn percent_decode(value: String) -> Result(String, Nil) { + do_percent_decode(value) +} + +if erlang { + external fn do_percent_decode(String) -> Result(String, Nil) = + "gleam_stdlib" "percent_decode" +} + +if javascript { + external fn do_percent_decode(String) -> Result(String, Nil) = + "../gleam_stdlib.js" "percent_decode" } fn do_remove_dot_segments( diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index f8f6851..9429f71 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -9,9 +9,22 @@ string_ends_with/2, string_pad/4, decode_tuple2/1, decode_tuple3/1, decode_tuple4/1, decode_tuple5/1, decode_tuple6/1, decode_map/1, bit_string_int_to_u32/1, bit_string_int_from_u32/1, decode_result/1, - bit_string_slice/3, decode_bit_string/1, compile_regex/2, - regex_check/2, regex_split/2, regex_scan/2, base_decode64/1, - parse_query/1, bit_string_concat/1]). + bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2, + percent_encode/1, percent_decode/1, regex_check/2, regex_split/2, + base_decode64/1, parse_query/1, bit_string_concat/1]). + +%% Taken from OTP's uri_string module +-define(DEC2HEX(X), + if ((X) >= 0) andalso ((X) =< 9) -> (X) + $0; + ((X) >= 10) andalso ((X) =< 15) -> (X) + $A - 10 + end). + +%% Taken from OTP's uri_string module +-define(HEX2DEC(X), + if ((X) >= $0) andalso ((X) =< $9) -> (X) - $0; + ((X) >= $A) andalso ((X) =< $F) -> (X) - $A + 10; + ((X) >= $a) andalso ((X) =< $f) -> (X) - $a + 10 + end). should_equal(Actual, Expected) -> ?assertEqual(Expected, Actual), @@ -245,3 +258,55 @@ parse_query(Query) -> end, Pairs), {ok, Pairs1} end. + +percent_encode(B) -> percent_encode(B, <<>>). +percent_encode(<<>>, Acc) -> + Acc; +percent_encode(<<H,T/binary>>, Acc) -> + case percent_ok(H) of + true -> + percent_encode(T, <<Acc/binary,H>>); + false -> + <<A:4,B:4>> = <<H>>, + percent_encode(T, <<Acc/binary,$%,(?DEC2HEX(A)),(?DEC2HEX(B))>>) + end. + +percent_decode(Cs) -> percent_decode(Cs, <<>>). +percent_decode(<<$%, C0, C1, Cs/binary>>, Acc) -> + case is_hex_digit(C0) andalso is_hex_digit(C1) of + true -> + B = ?HEX2DEC(C0)*16+?HEX2DEC(C1), + percent_decode(Cs, <<Acc/binary, B>>); + false -> + {error, nil} + end; +percent_decode(<<C,Cs/binary>>, Acc) -> + percent_decode(Cs, <<Acc/binary, C>>); +percent_decode(<<>>, Acc) -> + check_utf8(Acc). + +percent_ok($!) -> true; +percent_ok($$) -> true; +percent_ok($') -> true; +percent_ok($() -> true; +percent_ok($)) -> true; +percent_ok($*) -> true; +percent_ok($+) -> true; +percent_ok($-) -> true; +percent_ok($.) -> true; +percent_ok($_) -> true; +percent_ok($~) -> true; +percent_ok(C) when $0 =< C, C =< $9 -> true; +percent_ok(C) when $A =< C, C =< $Z -> true; +percent_ok(C) when $a =< C, C =< $z -> true; +percent_ok(_) -> false. + +is_hex_digit(C) -> + ($0 =< C andalso C =< $9) orelse ($a =< C andalso C =< $f) orelse ($A =< C andalso C =< $F). + +check_utf8(Cs) -> + case unicode:characters_to_list(Cs) of + {incomplete, _, _} -> {error, nil}; + {error, _, _} -> {error, nil}; + _ -> {ok, Cs} + end. diff --git a/src/gleam_stdlib.js b/src/gleam_stdlib.js index 082e661..3658142 100644 --- a/src/gleam_stdlib.js +++ b/src/gleam_stdlib.js @@ -353,17 +353,29 @@ export function map_insert(key, value, map) { return map.insert(key, value); } -function decode_query_component(string) { +function unsafe_percent_decode(string) { return decodeURIComponent((string || "").replace("+", " ")); } +export function percent_decode(string) { + try { + return new Ok(unsafe_percent_decode(string)); + } catch (error) { + return new Error(Nil); + } +} + +export function percent_encode(string) { + return encodeURIComponent(string); +} + export function parse_query(query) { try { let pairs = []; for (let section of query.split("&")) { let [key, value] = section.split("="); if (!key) continue; - pairs.push([decode_query_component(key), decode_query_component(value)]); + pairs.push([unsafe_percent_decode(key), unsafe_percent_decode(value)]); } return new Ok(List.fromArray(pairs)); } catch (error) { |