Skip to main content

src/gg_cn.erl

-module(gg_cn).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gg_cn.gleam").
-export([new/0, default/0, tw_join/1, tw_merge/2, cn/2]).
-export_type([merger/0, class_value/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(
    " gg_cn — a pure-Gleam `tailwind-merge`.\n"
    "\n"
    " Ported from [cnfast](https://github.com/aidenybai/cnfast)'s logic engine\n"
    " (itself a faithful reimplementation of `tailwind-merge` v4), this resolves\n"
    " conflicting Tailwind utility classes by keeping the last one per conflict\n"
    " group: `tw_merge(\"px-2 px-4\") == \"px-4\"`.\n"
    "\n"
    " This is the engine behind `gg_base_ui/helpers/cn` (which `gg_ui` and its\n"
    " components use): a real `clsx + tailwind-merge`, for any markup that mixes\n"
    " raw Tailwind utilities and needs conflict resolution. It is pure Gleam (no\n"
    " FFI), so it compiles and behaves identically on JS and the BEAM — which is\n"
    " why it can back gg_ui's `cn` without bringing Elixir `tails` into the build.\n"
    "\n"
    " ## Usage\n"
    "\n"
    " ```gleam\n"
    " import gg_cn\n"
    "\n"
    " let merge = gg_cn.new()\n"
    " merge |> gg_cn.tw_merge(\"px-2 py-1 px-4\")   // \"py-1 px-4\"\n"
    " ```\n"
    "\n"
    " `new()` builds the class trie once (it is moderately expensive); reuse the\n"
    " returned `Merger` across calls rather than rebuilding it per merge. For\n"
    " render-time callers, prefer [`default`](#default) — a process-global `Merger`\n"
    " built once and reused (what `gg_base_ui/helpers/cn` uses).\n"
).

-opaque merger() :: {merger, gg_cn@internal@merge:engine()}.

-type class_value() :: {class, binary()} |
    {'when', boolean(), binary()} |
    {group, list(class_value())}.

-file("src/gg_cn.gleam", 44).
?DOC(" Build a `Merger` from the baked-in Tailwind v4 configuration.\n").
-spec new() -> merger().
new() ->
    Regexes = gg_cn@internal@validators:compile(),
    Cfg = gg_cn@internal@config:default_config(Regexes),
    {merger, gg_cn@internal@merge:new(Cfg)}.

-file("src/gg_cn.gleam", 59).
?DOC(
    " A process-global default `Merger`, built **once** on first use and reused\n"
    " forever after. Backed by `global_value` (persistent_term on the BEAM, a\n"
    " singleton object on JS), so the expensive trie + regex build happens a single\n"
    " time per runtime — the right thing for render-time callers (e.g. gg_ui's\n"
    " `cn`) that shouldn't thread a `Merger` around or rebuild it per call.\n"
    "\n"
    " This memoizes only the *engine* (the trie). It does not yet cache per-input\n"
    " merge *results*; that LRU is a separate, optional layer (see the package\n"
    " README / gg_ui follow-up).\n"
).
-spec default() -> merger().
default() ->
    global_value:create_with_unique_name(
        <<"gg_cn.default_merger"/utf8>>,
        fun new/0
    ).

-file("src/gg_cn.gleam", 83).
-spec resolve_value(class_value()) -> binary().
resolve_value(Value) ->
    case Value of
        {class, Class} ->
            Class;

        {'when', true, Class@1} ->
            Class@1;

        {'when', false, _} ->
            <<""/utf8>>;

        {group, Values} ->
            tw_join(Values)
    end.

-file("src/gg_cn.gleam", 76).
?DOC(
    " Join class values into one space-separated string, dropping falsy/empty\n"
    " parts — the `twJoin`/`clsx` step, without conflict resolution.\n"
).
-spec tw_join(list(class_value())) -> binary().
tw_join(Values) ->
    _pipe = Values,
    _pipe@1 = gleam@list:map(_pipe, fun resolve_value/1),
    _pipe@2 = gleam@list:filter(_pipe@1, fun(Part) -> Part /= <<""/utf8>> end),
    gleam@string:join(_pipe@2, <<" "/utf8>>).

-file("src/gg_cn.gleam", 99).
?DOC(
    " Merge an already-joined, space-separated class string, resolving conflicts.\n"
    "\n"
    " The result is memoized by input string (JS only; the BEAM recomputes — see\n"
    " `internal/cache`). Output depends solely on the input (single baked config),\n"
    " so the cache never changes results, only speed.\n"
).
-spec tw_merge(merger(), binary()) -> binary().
tw_merge(Merger, Class_list) ->
    gg_cn@internal@cache:cached(
        Class_list,
        fun() ->
            gg_cn@internal@merge:merge_class_list(
                erlang:element(2, Merger),
                Class_list
            )
        end
    ).

-file("src/gg_cn.gleam", 107).
?DOC(
    " `clsx` + `twMerge` in one — the shadcn `cn` helper. Joins the class values,\n"
    " then resolves conflicts.\n"
).
-spec cn(merger(), list(class_value())) -> binary().
cn(Merger, Values) ->
    tw_merge(Merger, tw_join(Values)).