Skip to main content

src/magic_string@codec.erl

-module(magic_string@codec).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/magic_string/codec.gleam").
-export([encode_vlq/1, decode_vlq/1, decode_mappings/1, generate_mappings/1, to_json/1, url_comment/2]).
-export_type([source_map/0, map_mode/0, segment/0, delta_state/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(
    " Source Map v3 model and base64 VLQ codec.\n"
    "\n"
    " `generate_mappings` turns a flat `Segment` list into the v3 `mappings`\n"
    " string; `decode_mappings` is its inverse. `to_json` serializes the whole\n"
    " `SourceMap`. See https://tc39.es/ecma426/ for the format.\n"
).

-type source_map() :: {source_map,
        integer(),
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        list(binary()),
        list(gleam@option:option(binary())),
        list(binary()),
        binary()}.

-type map_mode() :: {external, binary()} | inline | hidden.

-type segment() :: {segment,
        integer(),
        integer(),
        integer(),
        integer(),
        integer()}.

-type delta_state() :: {delta_state, integer(), integer(), integer()}.

-file("src/magic_string/codec.gleam", 84).
-spec base64_char(integer()) -> binary().
base64_char(N) ->
    gleam@string:slice(
        <<"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"/utf8>>,
        N,
        1
    ).

-file("src/magic_string/codec.gleam", 74).
-spec encode_vlq_loop(integer(), binary()) -> binary().
encode_vlq_loop(Vlq, Acc) ->
    Digit = erlang:'band'(Vlq, 31),
    Rest = erlang:'bsr'(Vlq, 5),
    case Rest > 0 of
        true ->
            encode_vlq_loop(
                Rest,
                <<Acc/binary, (base64_char(erlang:'bor'(Digit, 32)))/binary>>
            );

        false ->
            <<Acc/binary, (base64_char(Digit))/binary>>
    end.

-file("src/magic_string/codec.gleam", 66).
?DOC(
    " Encode a single signed integer as a base64 VLQ string.\n"
    "\n"
    " Sign is folded into the low bit (`value << 1`, or `(-value << 1) | 1` for\n"
    " negatives), then the magnitude is split into 5-bit groups emitted\n"
    " low-group-first. Every group except the last carries the continuation bit\n"
    " (32); each resulting 6-bit value indexes into `base64_alphabet`.\n"
).
-spec encode_vlq(integer()) -> binary().
encode_vlq(Value) ->
    Vlq = case Value < 0 of
        true ->
            erlang:'bor'(erlang:'bsl'(- Value, 1), 1);

        false ->
            erlang:'bsl'(Value, 1)
    end,
    encode_vlq_loop(Vlq, <<""/utf8>>).

-file("src/magic_string/codec.gleam", 132).
?DOC(" Undo the sign-in-low-bit folding `encode_vlq` applies.\n").
-spec from_vlq(integer()) -> integer().
from_vlq(Value) ->
    Magnitude = erlang:'bsr'(Value, 1),
    case erlang:'band'(Value, 1) of
        0 ->
            Magnitude;

        _ ->
            - Magnitude
    end.

-file("src/magic_string/codec.gleam", 159).
-spec char_code(binary()) -> integer().
char_code(Char) ->
    _pipe = Char,
    _pipe@1 = gleam@string:to_utf_codepoints(_pipe),
    _pipe@2 = gleam@list:first(_pipe@1),
    _pipe@3 = gleam@result:map(_pipe@2, fun gleam_stdlib:identity/1),
    gleam@result:unwrap(_pipe@3, 0).

-file("src/magic_string/codec.gleam", 141).
?DOC(" Position of a base64 character in the Source Map v3 alphabet.\n").
-spec base64_value(binary()) -> {ok, integer()} | {error, binary()}.
base64_value(Char) ->
    case char_code(Char) of
        Code when (Code >= 65) andalso (Code =< 90) ->
            {ok, Code - 65};

        Code@1 when (Code@1 >= 97) andalso (Code@1 =< 122) ->
            {ok, (Code@1 - 97) + 26};

        Code@2 when (Code@2 >= 48) andalso (Code@2 =< 57) ->
            {ok, (Code@2 - 48) + 52};

        43 ->
            {ok, 62};

        47 ->
            {ok, 63};

        Code@3 ->
            {error,
                <<<<<<<<"invalid base64 VLQ character '"/utf8, Char/binary>>/binary,
                            "' (codepoint "/utf8>>/binary,
                        (erlang:integer_to_binary(Code@3))/binary>>/binary,
                    ")"/utf8>>}
    end.

-file("src/magic_string/codec.gleam", 98).
-spec decode_vlq_loop(list(binary()), integer(), integer(), list(integer())) -> {ok,
        list(integer())} |
    {error, binary()}.
decode_vlq_loop(Chars, Partial, Shift, Acc) ->
    case Chars of
        [] ->
            case Shift of
                0 ->
                    {ok, lists:reverse(Acc)};

                _ ->
                    {error,
                        <<<<"truncated VLQ: continuation bit set on final digit (shift "/utf8,
                                (erlang:integer_to_binary(Shift))/binary>>/binary,
                            ")"/utf8>>}
            end;

        [C | Rest] ->
            case base64_value(C) of
                {error, Msg} ->
                    {error, Msg};

                {ok, Digit} ->
                    Chunk = erlang:'band'(Digit, 31),
                    Partial@1 = Partial + erlang:'bsl'(Chunk, Shift),
                    case erlang:'band'(Digit, 32) of
                        0 ->
                            decode_vlq_loop(
                                Rest,
                                0,
                                0,
                                [from_vlq(Partial@1) | Acc]
                            );

                        _ ->
                            decode_vlq_loop(Rest, Partial@1, Shift + 5, Acc)
                    end
            end
    end.

-file("src/magic_string/codec.gleam", 94).
?DOC(
    " Decode a single base64-VLQ field string back to its list of signed\n"
    " integers. This is the exact inverse of concatenating `encode_vlq` calls:\n"
    " `decode_vlq(encode_vlq(a) <> encode_vlq(b)) == [a, b]`.\n"
    "\n"
    " Returns `Error` when `field` contains a character outside the base64\n"
    " alphabet, or ends mid-value (a continuation bit with no following digit).\n"
).
-spec decode_vlq(binary()) -> {ok, list(integer())} | {error, binary()}.
decode_vlq(Field) ->
    decode_vlq_loop(gleam@string:to_graphemes(Field), 0, 0, []).

-file("src/magic_string/codec.gleam", 192).
-spec decode_line(binary(), integer(), {list(segment()), delta_state()}) -> {ok,
        {list(segment()), delta_state()}} |
    {error, binary()}.
decode_line(Line, Gen_line, Acc) ->
    case Line of
        <<""/utf8>> ->
            {ok, Acc};

        _ ->
            _pipe = gleam@string:split(Line, <<","/utf8>>),
            _pipe@2 = gleam@list:try_fold(
                _pipe,
                {erlang:element(1, Acc), erlang:element(2, Acc), 0},
                fun(State, Field) ->
                    {Segs, Delta, Prev_gen_col} = State,
                    gleam@result:'try'(
                        begin
                            _pipe@1 = decode_vlq(Field),
                            gleam@result:map_error(
                                _pipe@1,
                                fun(Err) ->
                                    <<<<<<"line "/utf8,
                                                (erlang:integer_to_binary(
                                                    Gen_line
                                                ))/binary>>/binary,
                                            ": "/utf8>>/binary,
                                        Err/binary>>
                                end
                            )
                        end,
                        fun(Values) -> case Values of
                                [Gc, Si, Ol, Oc | _] ->
                                    Gen_col = Prev_gen_col + Gc,
                                    Source_idx = erlang:element(2, Delta) + Si,
                                    Orig_line = erlang:element(3, Delta) + Ol,
                                    Orig_col = erlang:element(4, Delta) + Oc,
                                    Seg = {segment,
                                        Gen_line,
                                        Gen_col,
                                        Source_idx,
                                        Orig_line,
                                        Orig_col},
                                    {ok,
                                        {[Seg | Segs],
                                            {delta_state,
                                                Source_idx,
                                                Orig_line,
                                                Orig_col},
                                            Gen_col}};

                                [Gc@1] ->
                                    {ok, {Segs, Delta, Prev_gen_col + Gc@1}};

                                Other ->
                                    {error,
                                        <<<<<<<<"line "/utf8,
                                                        (erlang:integer_to_binary(
                                                            Gen_line
                                                        ))/binary>>/binary,
                                                    ": segment has "/utf8>>/binary,
                                                (erlang:integer_to_binary(
                                                    erlang:length(Other)
                                                ))/binary>>/binary,
                                            " fields; expected 1, 4, or 5"/utf8>>}
                            end end
                    )
                end
            ),
            gleam@result:map(
                _pipe@2,
                fun(State@1) ->
                    {erlang:element(1, State@1), erlang:element(2, State@1)}
                end
            )
    end.

-file("src/magic_string/codec.gleam", 177).
?DOC(
    " Decode a Source Map v3 `mappings` string back to a flat `Segment` list.\n"
    " This is the inverse of `generate_mappings`:\n"
    " `decode_mappings(generate_mappings(segs))` returns the same segments\n"
    " (in `(gen_line, gen_col)` order).\n"
    "\n"
    " Only 4- and 5-field segments (those carrying a source pointer) are\n"
    " returned; 1-field segments (generated column only, no source) are skipped\n"
    " since `Segment` requires a source position. The optional 5th `name` field\n"
    " is decoded for delta-tracking but discarded (this codec does not model\n"
    " names).\n"
).
-spec decode_mappings(binary()) -> {ok, list(segment())} | {error, binary()}.
decode_mappings(Mappings) ->
    Lines = gleam@string:split(Mappings, <<";"/utf8>>),
    gleam@result:map(
        gleam@list:try_fold(
            gleam@list:index_map(Lines, fun(Line, Idx) -> {Idx, Line} end),
            {[], {delta_state, 0, 0, 0}},
            fun(Acc, Entry) ->
                {Gen_line, Line@1} = Entry,
                decode_line(Line@1, Gen_line, Acc)
            end
        ),
        fun(_use0) ->
            {Segs_rev, _} = _use0,
            lists:reverse(Segs_rev)
        end
    ).

-file("src/magic_string/codec.gleam", 288).
?DOC(
    " Encode one output line's segments, threading the cross-map delta state in\n"
    " and out. `gen_col` resets to 0 at the start of every line.\n"
).
-spec emit_line(list(segment()), delta_state()) -> {binary(), delta_state()}.
emit_line(Segs, Delta) ->
    {Parts_rev, _, Next_delta} = gleam@list:fold(
        Segs,
        {[], 0, Delta},
        fun(Acc, Seg) ->
            {Parts, Prev_gen_col, D} = Acc,
            Field = erlang:list_to_binary(
                [encode_vlq(erlang:element(3, Seg) - Prev_gen_col),
                    encode_vlq(erlang:element(4, Seg) - erlang:element(2, D)),
                    encode_vlq(erlang:element(5, Seg) - erlang:element(3, D)),
                    encode_vlq(erlang:element(6, Seg) - erlang:element(4, D))]
            ),
            Next = {delta_state,
                erlang:element(4, Seg),
                erlang:element(5, Seg),
                erlang:element(6, Seg)},
            {[Field | Parts], erlang:element(3, Seg), Next}
        end
    ),
    {begin
            _pipe = Parts_rev,
            _pipe@1 = lists:reverse(_pipe),
            gleam@string:join(_pipe@1, <<","/utf8>>)
        end,
        Next_delta}.

-file("src/magic_string/codec.gleam", 305).
-spec compare_segment(segment(), segment()) -> gleam@order:order().
compare_segment(A, B) ->
    case gleam@int:compare(erlang:element(2, A), erlang:element(2, B)) of
        eq ->
            gleam@int:compare(erlang:element(3, A), erlang:element(3, B));

        Other ->
            Other
    end.

-file("src/magic_string/codec.gleam", 254).
?DOC(
    " Build the Source Map v3 `mappings` string from a flat segment list.\n"
    "\n"
    " Output lines are `;`-separated and segments within a line are\n"
    " `,`-separated. Each segment is the VLQ of four deltas:\n"
    " `[gen_col, source_idx, orig_line, orig_col]`. `gen_col` is delta'd within\n"
    " the line (reset to 0 per line); the other three are delta'd across the\n"
    " whole map. Inserted text contributes no segment (so a line with only\n"
    " inserted output is empty). `hires` is false: one segment per chunk.\n"
).
-spec generate_mappings(list(segment())) -> binary().
generate_mappings(Segments) ->
    case Segments of
        [] ->
            <<""/utf8>>;

        _ ->
            Sorted = gleam@list:sort(Segments, fun compare_segment/2),
            Max_line = gleam@list:fold(
                Sorted,
                0,
                fun(Acc, S) -> case erlang:element(2, S) > Acc of
                        true ->
                            erlang:element(2, S);

                        false ->
                            Acc
                    end end
            ),
            By_line = gleam@list:group(
                Sorted,
                fun(S@1) -> erlang:element(2, S@1) end
            ),
            {Lines_rev, _} = gleam@int:range(
                0,
                Max_line + 1,
                {[], {delta_state, 0, 0, 0}},
                fun(Acc@1, Line) ->
                    {Lines, Delta} = Acc@1,
                    Segs = begin
                        _pipe = gleam_stdlib:map_get(By_line, Line),
                        _pipe@1 = gleam@result:unwrap(_pipe, []),
                        gleam@list:sort(
                            _pipe@1,
                            fun(A, B) ->
                                gleam@int:compare(
                                    erlang:element(3, A),
                                    erlang:element(3, B)
                                )
                            end
                        )
                    end,
                    {Line_str, Next_delta} = emit_line(Segs, Delta),
                    {[Line_str | Lines], Next_delta}
                end
            ),
            _pipe@2 = Lines_rev,
            _pipe@3 = lists:reverse(_pipe@2),
            gleam@string:join(_pipe@3, <<";"/utf8>>)
    end.

-file("src/magic_string/codec.gleam", 343).
-spec json_string(binary()) -> binary().
json_string(Value) ->
    Escaped = begin
        _pipe = Value,
        _pipe@1 = gleam@string:replace(_pipe, <<"\\"/utf8>>, <<"\\\\"/utf8>>),
        _pipe@2 = gleam@string:replace(_pipe@1, <<"\""/utf8>>, <<"\\\""/utf8>>),
        _pipe@3 = gleam@string:replace(_pipe@2, <<"\n"/utf8>>, <<"\\n"/utf8>>),
        _pipe@4 = gleam@string:replace(_pipe@3, <<"\r"/utf8>>, <<"\\r"/utf8>>),
        gleam@string:replace(_pipe@4, <<"\t"/utf8>>, <<"\\t"/utf8>>)
    end,
    <<<<"\""/utf8, Escaped/binary>>/binary, "\""/utf8>>.

-file("src/magic_string/codec.gleam", 332).
-spec json_array(list(binary())) -> binary().
json_array(Items) ->
    <<<<"["/utf8, (gleam@string:join(Items, <<","/utf8>>))/binary>>/binary,
        "]"/utf8>>.

-file("src/magic_string/codec.gleam", 336).
-spec json_nullable(gleam@option:option(binary())) -> binary().
json_nullable(Value) ->
    case Value of
        {some, S} ->
            json_string(S);

        none ->
            <<"null"/utf8>>
    end.

-file("src/magic_string/codec.gleam", 314).
?DOC(
    " Serialize a `SourceMap` to a JSON string. `file` and `sourceRoot` are\n"
    " omitted when `None`; `sourcesContent` entries are `null` when `None`.\n"
).
-spec to_json(source_map()) -> binary().
to_json(Map) ->
    Fields = begin
        _pipe = [{some,
                <<"\"version\":"/utf8,
                    (erlang:integer_to_binary(erlang:element(2, Map)))/binary>>},
            gleam@option:map(
                erlang:element(3, Map),
                fun(F) -> <<"\"file\":"/utf8, (json_string(F))/binary>> end
            ),
            gleam@option:map(
                erlang:element(4, Map),
                fun(R) ->
                    <<"\"sourceRoot\":"/utf8, (json_string(R))/binary>>
                end
            ),
            {some,
                <<"\"sources\":"/utf8,
                    (json_array(
                        gleam@list:map(
                            erlang:element(5, Map),
                            fun json_string/1
                        )
                    ))/binary>>},
            {some,
                <<"\"sourcesContent\":"/utf8,
                    (json_array(
                        gleam@list:map(
                            erlang:element(6, Map),
                            fun json_nullable/1
                        )
                    ))/binary>>},
            {some,
                <<"\"names\":"/utf8,
                    (json_array(
                        gleam@list:map(
                            erlang:element(7, Map),
                            fun json_string/1
                        )
                    ))/binary>>},
            {some,
                <<"\"mappings\":"/utf8,
                    (json_string(erlang:element(8, Map)))/binary>>}],
        gleam@option:values(_pipe)
    end,
    <<<<"{"/utf8, (gleam@string:join(Fields, <<","/utf8>>))/binary>>/binary,
        "}"/utf8>>.

-file("src/magic_string/codec.gleam", 358).
?DOC(
    " Produce the `sourceMappingURL` comment for the given map and mode.\n"
    "\n"
    " `External` points at a sibling `.map` URL, `Inline` embeds the whole map\n"
    " as a base64 `data:` URI, and `Hidden` emits nothing.\n"
).
-spec url_comment(source_map(), map_mode()) -> binary().
url_comment(Map, Mode) ->
    case Mode of
        {external, Url} ->
            <<"//# sourceMappingURL="/utf8, Url/binary>>;

        hidden ->
            <<""/utf8>>;

        inline ->
            B64 = gleam_stdlib:base64_encode(
                gleam_stdlib:identity(to_json(Map)),
                true
            ),
            <<"//# sourceMappingURL=data:application/json;charset=utf-8;base64,"/utf8,
                B64/binary>>
    end.