-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 = <<<>/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).