Current section
Files
Jump to
Current section
Files
src/glugify@slugger.erl
-module(glugify@slugger).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glugify/slugger.gleam").
-export([new/0, slug/2, slug_with/3]).
-export_type([slugger/0]).
-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(
" A stateful slugger that guarantees unique slugs across a batch of\n"
" inputs — the building block for tables of contents, static site\n"
" generators and CMS imports, where two posts titled \"Hello\" must not\n"
" share a URL.\n"
"\n"
" The state is an immutable value threaded through each call, so it\n"
" works naturally in folds and works identically on the Erlang and\n"
" JavaScript targets:\n"
"\n"
" ```gleam\n"
" import glugify/slugger\n"
"\n"
" let s = slugger.new()\n"
" let #(s, a) = slugger.slug(s, \"Hello World\")\n"
" let #(s, b) = slugger.slug(s, \"Hello World\")\n"
" let #(_, c) = slugger.slug(s, \"Hello World\")\n"
" // a -> \"hello-world\"\n"
" // b -> \"hello-world-1\"\n"
" // c -> \"hello-world-2\"\n"
" ```\n"
).
-opaque slugger() :: {slugger,
gleam@set:set(binary()),
gleam@dict:dict(binary(), integer())}.
-file("src/glugify/slugger.gleam", 37).
?DOC(" Creates a fresh slugger with no slugs taken.\n").
-spec new() -> slugger().
new() ->
{slugger, gleam@set:new(), maps:new()}.
-file("src/glugify/slugger.gleam", 105).
-spec find_free(slugger(), binary(), integer()) -> {slugger(), binary()}.
find_free(Slugger, Base, N) ->
Candidate = <<<<Base/binary, "-"/utf8>>/binary,
(erlang:integer_to_binary(N))/binary>>,
case gleam@set:contains(erlang:element(2, Slugger), Candidate) of
true ->
find_free(Slugger, Base, N + 1);
false ->
{{slugger,
gleam@set:insert(erlang:element(2, Slugger), Candidate),
gleam@dict:insert(erlang:element(3, Slugger), Base, N + 1)},
Candidate}
end.
-file("src/glugify/slugger.gleam", 85).
-spec unique(slugger(), binary()) -> {slugger(), binary()}.
unique(Slugger, Base) ->
case Base of
<<""/utf8>> ->
{Slugger, <<""/utf8>>};
_ ->
case gleam@set:contains(erlang:element(2, Slugger), Base) of
false ->
{{slugger,
gleam@set:insert(erlang:element(2, Slugger), Base),
erlang:element(3, Slugger)},
Base};
true ->
Next = case gleam_stdlib:map_get(
erlang:element(3, Slugger),
Base
) of
{ok, Count} ->
Count;
{error, nil} ->
1
end,
find_free(Slugger, Base, Next)
end
end.
-file("src/glugify/slugger.gleam", 60).
?DOC(
" Slugifies `text` with the default configuration and makes the result\n"
" unique against everything this slugger has produced before, by\n"
" appending `-1`, `-2`, ... to duplicates.\n"
"\n"
" Unlike github-slugger, a suffixed slug never collides with a slug\n"
" produced from genuinely suffixed input: `\"foo\"`, `\"foo\"`, `\"foo-1\"`\n"
" yields `\"foo\"`, `\"foo-1\"`, `\"foo-1-1\"`.\n"
"\n"
" Empty results (e.g. from symbol-only input) bypass uniqueness and are\n"
" returned as-is.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" let #(slugger, first) = slug(new(), \"My Post\")\n"
" let #(_, second) = slug(slugger, \"My Post\")\n"
" // first -> \"my-post\"\n"
" // second -> \"my-post-1\"\n"
" ```\n"
).
-spec slug(slugger(), binary()) -> {slugger(), binary()}.
slug(Slugger, Text) ->
Base = glugify:slugify(Text),
unique(Slugger, Base).
-file("src/glugify/slugger.gleam", 76).
?DOC(
" Like `slug`, but slugifies with a custom configuration.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" import glugify/config\n"
"\n"
" let cfg = config.default() |> config.with_separator(\"_\")\n"
" slug_with(new(), \"My Post\", cfg)\n"
" // -> Ok(#(slugger, \"my_post\"))\n"
" ```\n"
).
-spec slug_with(slugger(), binary(), glugify@config:config()) -> {ok,
{slugger(), binary()}} |
{error, glugify@errors:slugify_error()}.
slug_with(Slugger, Text, Config) ->
_pipe = glugify:slugify_with(Text, Config),
gleam@result:map(_pipe, fun(_capture) -> unique(Slugger, _capture) end).