-module(url_join). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/url_join.gleam"). -export([join/1]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Join URL path segments and normalize the result.\n" "\n" " A simple and clean library for combining URL segments into a properly\n" " formatted URL. Handles protocols, slashes, query parameters, and hash\n" " fragments automatically.\n" "\n" " ## Example\n" "\n" " ```gleam\n" " import url_join\n" "\n" " let url = url_join.join([\n" " \"https://example.com\",\n" " \"api\",\n" " \"v1\",\n" " \"users\",\n" " \"?page=1\",\n" " ])\n" " // -> \"https://example.com/api/v1/users?page=1\"\n" " ```\n" ). -file("src/url_join.gleam", 96). -spec only_slashes_or_empty(binary()) -> boolean(). only_slashes_or_empty(S) -> gleam@string:replace(S, <<"/"/utf8>>, <<""/utf8>>) =:= <<""/utf8>>. -file("src/url_join.gleam", 88). -spec is_plain_protocol(binary()) -> boolean(). is_plain_protocol(S) -> case gleam@string:split_once(S, <<":"/utf8>>) of {ok, {Before, After}} -> not gleam_stdlib:contains_string(Before, <<"/"/utf8>>) andalso only_slashes_or_empty( After ); {error, _} -> false end. -file("src/url_join.gleam", 129). -spec drop_leading_slashes(binary()) -> binary(). drop_leading_slashes(S) -> case gleam_stdlib:string_starts_with(S, <<"/"/utf8>>) of true -> drop_leading_slashes(gleam@string:drop_start(S, 1)); false -> S end. -file("src/url_join.gleam", 104). -spec normalize_protocol(binary()) -> binary(). normalize_protocol(S) -> Is_ipv6 = gleam_stdlib:string_starts_with(S, <<"["/utf8>>) andalso gleam_stdlib:contains_string( S, <<"]"/utf8>> ), Has_full_protocol = gleam_stdlib:contains_string(S, <<"://"/utf8>>), case S of <<"file:///"/utf8, _/binary>> -> S; <<"file:"/utf8, Rest/binary>> -> <<"file:///"/utf8, (drop_leading_slashes(Rest))/binary>>; _ -> case Is_ipv6 orelse Has_full_protocol of true -> S; false -> case gleam@string:split_once(S, <<":"/utf8>>) of {ok, {Protocol, Rest@1}} -> <<<>/binary, (drop_leading_slashes(Rest@1))/binary>>; {error, _} -> S end end end. -file("src/url_join.gleam", 76). -spec merge_leading_parts(binary(), list(binary())) -> list(binary()). merge_leading_parts(First, Rest) -> case Rest of [] -> [normalize_protocol(First)]; [Next | Tail] -> case (First =:= <<"/"/utf8>>) orelse is_plain_protocol(First) of true -> merge_leading_parts(<>, Tail); false -> [normalize_protocol(First) | Rest] end end. -file("src/url_join.gleam", 136). -spec drop_trailing_slashes(binary()) -> binary(). drop_trailing_slashes(S) -> case gleam_stdlib:string_ends_with(S, <<"/"/utf8>>) of true -> drop_trailing_slashes(gleam@string:drop_end(S, 1)); false -> S end. -file("src/url_join.gleam", 143). -spec collapse_trailing_slashes(binary()) -> binary(). collapse_trailing_slashes(S) -> Trimmed = drop_trailing_slashes(S), case gleam_stdlib:string_ends_with(S, <<"/"/utf8>>) andalso (Trimmed /= <<""/utf8>>) of true -> <>; false -> Trimmed end. -file("src/url_join.gleam", 156). -spec process_parts(list(binary())) -> list(binary()). process_parts(Parts) -> Len = erlang:length(Parts), _pipe = Parts, _pipe@1 = gleam@list:index_map( _pipe, fun(Part, Index) -> Is_first = Index =:= 0, Is_last = Index =:= (Len - 1), No_leading = case Is_first of true -> Part; false -> drop_leading_slashes(Part) end, case Is_last of true -> collapse_trailing_slashes(No_leading); false -> drop_trailing_slashes(No_leading) end end ), gleam@list:filter(_pipe@1, fun(P) -> P /= <<""/utf8>> end). -file("src/url_join.gleam", 183). -spec join_two_parts(binary(), binary()) -> binary(). join_two_parts(Prev, Part) -> case gleam_stdlib:string_ends_with(Prev, <<"?"/utf8>>) orelse gleam_stdlib:string_ends_with( Prev, <<"#"/utf8>> ) of true -> <>; false -> <<<>/binary, Part/binary>> end. -file("src/url_join.gleam", 176). -spec join_parts(list(binary())) -> binary(). join_parts(Parts) -> case Parts of [] -> <<""/utf8>>; [First | Rest] -> gleam@list:fold(Rest, First, fun join_two_parts/2) end. -file("src/url_join.gleam", 223). -spec split_query_parts(binary()) -> list(binary()). split_query_parts(S) -> _pipe = S, _pipe@1 = gleam@string:replace(_pipe, <<"?"/utf8>>, <<"&"/utf8>>), _pipe@2 = gleam@string:split(_pipe@1, <<"&"/utf8>>), gleam@list:filter(_pipe@2, fun(X) -> X /= <<""/utf8>> end). -file("src/url_join.gleam", 215). -spec build_query(binary()) -> binary(). build_query(S) -> case split_query_parts(S) of [] -> S; [Path] -> Path; [Path@1 | Params] -> <<<>/binary, (gleam@string:join(Params, <<"&"/utf8>>))/binary>> end. -file("src/url_join.gleam", 202). -spec normalize_query_params(binary()) -> binary(). normalize_query_params(S) -> case gleam@string:split_once(S, <<"#"/utf8>>) of {ok, {Before_hash, After_hash}} -> Query = build_query(Before_hash), case After_hash of <<""/utf8>> -> Query; _ -> <<<>/binary, After_hash/binary>> end; {error, _} -> build_query(S) end. -file("src/url_join.gleam", 194). -spec normalize_query(binary()) -> binary(). normalize_query(S) -> _pipe = S, _pipe@1 = gleam@string:replace(_pipe, <<"/?"/utf8>>, <<"?"/utf8>>), _pipe@2 = gleam@string:replace(_pipe@1, <<"/#"/utf8>>, <<"#"/utf8>>), _pipe@3 = gleam@string:replace(_pipe@2, <<"/&"/utf8>>, <<"&"/utf8>>), normalize_query_params(_pipe@3). -file("src/url_join.gleam", 60). -spec normalize(list(binary())) -> binary(). normalize(Segments) -> case Segments of [] -> <<""/utf8>>; [First | Rest] -> _pipe = First, _pipe@1 = merge_leading_parts(_pipe, Rest), _pipe@2 = process_parts(_pipe@1), _pipe@3 = join_parts(_pipe@2), normalize_query(_pipe@3) end. -file("src/url_join.gleam", 54). ?DOC( " Join URL path segments and normalize the result.\n" "\n" " This function takes a list of URL segments and combines them into a single,\n" " properly normalized URL. It handles protocols, slashes, query strings, and\n" " hash fragments intelligently.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " join([\"http://www.google.com\", \"a\", \"/b/cd\", \"?foo=123\"])\n" " // -> \"http://www.google.com/a/b/cd?foo=123\"\n" "\n" " join([\"https://example.com/\", \"/api/\", \"users\"])\n" " // -> \"https://example.com/api/users\"\n" "\n" " join([\"a\", \"b\", \"c\"])\n" " // -> \"a/b/c\"\n" " ```\n" "\n" " ## Behaviour\n" "\n" " - Filters out empty segments\n" " - Merges a leading bare protocol (e.g. `\"http:\"`) with the next segment\n" " - Merges a leading `\"/\"` with the next segment \n" " - Normalises protocols to `protocol://` (and `file:///` for file)\n" " - Trims and collapses slashes between segments\n" " - Does not add a slash before `?`, `#`, or `&`\n" " - Normalises query string (multiple `?`/`&` become a single `?` then `&`)\n" ). -spec join(list(binary())) -> binary(). join(Parts) -> _pipe = Parts, _pipe@1 = gleam@list:filter(_pipe, fun(Part) -> Part /= <<""/utf8>> end), normalize(_pipe@1).