Skip to main content

src/svg_path@transform@parse.erl

-module(svg_path@transform@parse).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/svg_path/transform/parse.gleam").
-export([attribute/1]).
-export_type([error/0, token/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(
    " SVG transform attribute parser.\n"
    "\n"
    " This module parses SVG transform lists such as\n"
    " `translate(10 20)rotate(30)scale(2)`. Commas are accepted where SVG allows\n"
    " separators, and adjacent signed numbers such as `translate(10-20)` are\n"
    " handled.\n"
).

-type error() :: expected_close |
    expected_open |
    expected_transform |
    {invalid_argument_count, binary(), integer()} |
    {invalid_number, binary()} |
    {unexpected_token, binary()} |
    {unknown_transform, binary()}.

-type token() :: close | {name, binary()} | {number, float()} | open.

-file("src/svg_path/transform/parse.gleam", 173).
-spec skew_y_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
skew_y_transform(Name, Arguments) ->
    case Arguments of
        [Degrees] ->
            {ok, svg_path@transform:skew_y(Degrees)};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 163).
-spec skew_x_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
skew_x_transform(Name, Arguments) ->
    case Arguments of
        [Degrees] ->
            {ok, svg_path@transform:skew_x(Degrees)};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 142).
-spec rotate_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
rotate_transform(Name, Arguments) ->
    case Arguments of
        [Degrees] ->
            {ok, svg_path@transform:rotate(Degrees)};

        [Degrees@1, Cx, Cy] ->
            Move_to_origin = svg_path@transform:translate(+0.0 - Cx, +0.0 - Cy),
            Rotate = svg_path@transform:rotate(Degrees@1),
            Move_back = svg_path@transform:translate(Cx, Cy),
            {ok,
                begin
                    _pipe = Move_to_origin,
                    _pipe@1 = svg_path@transform:chain(_pipe, Rotate),
                    svg_path@transform:chain(_pipe@1, Move_back)
                end};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 131).
-spec scale_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
scale_transform(Name, Arguments) ->
    case Arguments of
        [Factor] ->
            {ok, svg_path@transform:scale(Factor)};

        [X, Y] ->
            {ok, svg_path@transform:scale_xy(X, Y)};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 120).
-spec translate_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
translate_transform(Name, Arguments) ->
    case Arguments of
        [X] ->
            {ok, svg_path@transform:translate(X, +0.0)};

        [X@1, Y] ->
            {ok, svg_path@transform:translate(X@1, Y)};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 110).
-spec matrix_transform(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
matrix_transform(Name, Arguments) ->
    case Arguments of
        [A, B, C, D, E, F] ->
            {ok, svg_path@transform:matrix(A, B, C, D, E, F)};

        _ ->
            {error, {invalid_argument_count, Name, erlang:length(Arguments)}}
    end.

-file("src/svg_path/transform/parse.gleam", 95).
-spec transform_from_arguments(binary(), list(float())) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
transform_from_arguments(Name, Arguments) ->
    case Name of
        <<"matrix"/utf8>> ->
            matrix_transform(Name, Arguments);

        <<"translate"/utf8>> ->
            translate_transform(Name, Arguments);

        <<"scale"/utf8>> ->
            scale_transform(Name, Arguments);

        <<"rotate"/utf8>> ->
            rotate_transform(Name, Arguments);

        <<"skewX"/utf8>> ->
            skew_x_transform(Name, Arguments);

        <<"skewY"/utf8>> ->
            skew_y_transform(Name, Arguments);

        _ ->
            {error, {unknown_transform, Name}}
    end.

-file("src/svg_path/transform/parse.gleam", 82).
-spec take_arguments(list(token()), list(float())) -> {ok,
        {list(float()), list(token())}} |
    {error, error()}.
take_arguments(Tokens, Arguments) ->
    case Tokens of
        [] ->
            {error, expected_close};

        [close | Rest] ->
            {ok, {lists:reverse(Arguments), Rest}};

        [{number, Number} | Rest@1] ->
            take_arguments(Rest@1, [Number | Arguments]);

        [{name, Name} | _] ->
            {error, {unexpected_token, Name}};

        [open | _] ->
            {error, {unexpected_token, <<"("/utf8>>}}
    end.

-file("src/svg_path/transform/parse.gleam", 55).
-spec parse_transforms(list(token()), svg_path@transform:matrix()) -> {ok,
        svg_path@transform:matrix()} |
    {error, error()}.
parse_transforms(Tokens, Accumulated) ->
    case Tokens of
        [] ->
            {ok, Accumulated};

        [{name, Name}, open | Rest] ->
            case take_arguments(Rest, []) of
                {error, Error} ->
                    {error, Error};

                {ok, {Arguments, Rest@1}} ->
                    case transform_from_arguments(Name, Arguments) of
                        {error, Error@1} ->
                            {error, Error@1};

                        {ok, Next} ->
                            parse_transforms(
                                Rest@1,
                                svg_path@transform:chain(Next, Accumulated)
                            )
                    end
            end;

        [{name, _} | _] ->
            {error, expected_open};

        [open | _] ->
            {error, expected_transform};

        [close | _] ->
            {error, expected_transform};

        [{number, _} | _] ->
            {error, expected_transform}
    end.

-file("src/svg_path/transform/parse.gleam", 429).
-spec strip_leading_plus(binary()) -> binary().
strip_leading_plus(Raw) ->
    case gleam_stdlib:string_starts_with(Raw, <<"+"/utf8>>) of
        true ->
            gleam@string:drop_start(Raw, 1);

        false ->
            Raw
    end.

-file("src/svg_path/transform/parse.gleam", 397).
-spec normalize_decimal(binary()) -> binary().
normalize_decimal(Raw) ->
    case gleam_stdlib:string_starts_with(Raw, <<"."/utf8>>) of
        true ->
            <<"0"/utf8, Raw/binary>>;

        false ->
            case gleam_stdlib:string_starts_with(Raw, <<"+."/utf8>>) of
                true ->
                    <<"0"/utf8, (gleam@string:drop_start(Raw, 1))/binary>>;

                false ->
                    case gleam_stdlib:string_starts_with(Raw, <<"-."/utf8>>) of
                        true ->
                            <<"-0"/utf8,
                                (gleam@string:drop_start(Raw, 1))/binary>>;

                        false ->
                            strip_leading_plus(Raw)
                    end
            end
    end.

-file("src/svg_path/transform/parse.gleam", 383).
-spec parse_decimal_number(binary()) -> {ok, float()} | {error, nil}.
parse_decimal_number(Raw) ->
    Raw@1 = normalize_decimal(Raw),
    case gleam_stdlib:parse_float(Raw@1) of
        {ok, Number} ->
            {ok, Number};

        {error, _} ->
            case gleam_stdlib:parse_int(Raw@1) of
                {ok, Number@1} ->
                    {ok, erlang:float(Number@1)};

                {error, _} ->
                    {error, nil}
            end
    end.

-file("src/svg_path/transform/parse.gleam", 436).
-spec power_of_ten(integer()) -> float().
power_of_ten(Exponent) ->
    case Exponent of
        0 ->
            1.0;

        _ when Exponent > 0 ->
            10.0 * power_of_ten(Exponent - 1);

        _ ->
            power_of_ten(Exponent + 1) / 10.0
    end.

-file("src/svg_path/transform/parse.gleam", 414).
-spec parse_exponent_number(binary(), binary()) -> {ok, float()} | {error, nil}.
parse_exponent_number(Mantissa, Exponent) ->
    case parse_decimal_number(Mantissa) of
        {error, _} ->
            {error, nil};

        {ok, Mantissa@1} ->
            case begin
                _pipe = Exponent,
                _pipe@1 = strip_leading_plus(_pipe),
                gleam_stdlib:parse_int(_pipe@1)
            end of
                {error, _} ->
                    {error, nil};

                {ok, Exponent@1} ->
                    {ok, Mantissa@1 * power_of_ten(Exponent@1)}
            end
    end.

-file("src/svg_path/transform/parse.gleam", 371).
-spec parse_number(binary()) -> {ok, float()} | {error, nil}.
parse_number(Raw) ->
    case gleam@string:split_once(Raw, <<"e"/utf8>>) of
        {ok, {Mantissa, Exponent}} ->
            parse_exponent_number(Mantissa, Exponent);

        {error, _} ->
            case gleam@string:split_once(Raw, <<"E"/utf8>>) of
                {ok, {Mantissa@1, Exponent@1}} ->
                    parse_exponent_number(Mantissa@1, Exponent@1);

                {error, _} ->
                    parse_decimal_number(Raw)
            end
    end.

-file("src/svg_path/transform/parse.gleam", 367).
-spec is_digit(binary()) -> boolean().
is_digit(Grapheme) ->
    gleam@list:contains(
        [<<"0"/utf8>>,
            <<"1"/utf8>>,
            <<"2"/utf8>>,
            <<"3"/utf8>>,
            <<"4"/utf8>>,
            <<"5"/utf8>>,
            <<"6"/utf8>>,
            <<"7"/utf8>>,
            <<"8"/utf8>>,
            <<"9"/utf8>>],
        Grapheme
    ).

-file("src/svg_path/transform/parse.gleam", 247).
-spec read_number(list(binary()), list(binary()), boolean()) -> {binary(),
    list(binary())}.
read_number(Graphemes, Number, Previous_was_exponent) ->
    case Graphemes of
        [] ->
            {gleam@string:join(lists:reverse(Number), <<""/utf8>>), []};

        [Grapheme | Rest] ->
            case is_digit(Grapheme) orelse (Grapheme =:= <<"."/utf8>>) of
                true ->
                    read_number(Rest, [Grapheme | Number], false);

                false ->
                    case (Grapheme =:= <<"e"/utf8>>) orelse (Grapheme =:= <<"E"/utf8>>) of
                        true ->
                            read_number(Rest, [Grapheme | Number], true);

                        false ->
                            case (Previous_was_exponent orelse gleam@list:is_empty(
                                Number
                            ))
                            andalso ((Grapheme =:= <<"+"/utf8>>) orelse (Grapheme
                            =:= <<"-"/utf8>>)) of
                                true ->
                                    read_number(
                                        Rest,
                                        [Grapheme | Number],
                                        false
                                    );

                                false ->
                                    {gleam@string:join(
                                            lists:reverse(Number),
                                            <<""/utf8>>
                                        ),
                                        Graphemes}
                            end
                    end
            end
    end.

-file("src/svg_path/transform/parse.gleam", 363).
-spec is_number_start(binary()) -> boolean().
is_number_start(Grapheme) ->
    ((is_digit(Grapheme) orelse (Grapheme =:= <<"+"/utf8>>)) orelse (Grapheme
    =:= <<"-"/utf8>>))
    orelse (Grapheme =:= <<"."/utf8>>).

-file("src/svg_path/transform/parse.gleam", 303).
-spec is_ascii_letter(binary()) -> boolean().
is_ascii_letter(Grapheme) ->
    gleam@list:contains(
        [<<"A"/utf8>>,
            <<"B"/utf8>>,
            <<"C"/utf8>>,
            <<"D"/utf8>>,
            <<"E"/utf8>>,
            <<"F"/utf8>>,
            <<"G"/utf8>>,
            <<"H"/utf8>>,
            <<"I"/utf8>>,
            <<"J"/utf8>>,
            <<"K"/utf8>>,
            <<"L"/utf8>>,
            <<"M"/utf8>>,
            <<"N"/utf8>>,
            <<"O"/utf8>>,
            <<"P"/utf8>>,
            <<"Q"/utf8>>,
            <<"R"/utf8>>,
            <<"S"/utf8>>,
            <<"T"/utf8>>,
            <<"U"/utf8>>,
            <<"V"/utf8>>,
            <<"W"/utf8>>,
            <<"X"/utf8>>,
            <<"Y"/utf8>>,
            <<"Z"/utf8>>,
            <<"a"/utf8>>,
            <<"b"/utf8>>,
            <<"c"/utf8>>,
            <<"d"/utf8>>,
            <<"e"/utf8>>,
            <<"f"/utf8>>,
            <<"g"/utf8>>,
            <<"h"/utf8>>,
            <<"i"/utf8>>,
            <<"j"/utf8>>,
            <<"k"/utf8>>,
            <<"l"/utf8>>,
            <<"m"/utf8>>,
            <<"n"/utf8>>,
            <<"o"/utf8>>,
            <<"p"/utf8>>,
            <<"q"/utf8>>,
            <<"r"/utf8>>,
            <<"s"/utf8>>,
            <<"t"/utf8>>,
            <<"u"/utf8>>,
            <<"v"/utf8>>,
            <<"w"/utf8>>,
            <<"x"/utf8>>,
            <<"y"/utf8>>,
            <<"z"/utf8>>],
        Grapheme
    ).

-file("src/svg_path/transform/parse.gleam", 299).
-spec is_name_part(binary()) -> boolean().
is_name_part(Grapheme) ->
    is_ascii_letter(Grapheme).

-file("src/svg_path/transform/parse.gleam", 232).
-spec read_name(list(binary()), list(binary())) -> {binary(), list(binary())}.
read_name(Graphemes, Name) ->
    case Graphemes of
        [] ->
            {gleam@string:join(lists:reverse(Name), <<""/utf8>>), []};

        [Grapheme | Rest] ->
            case is_name_part(Grapheme) of
                true ->
                    read_name(Rest, [Grapheme | Name]);

                false ->
                    {gleam@string:join(lists:reverse(Name), <<""/utf8>>),
                        Graphemes}
            end
    end.

-file("src/svg_path/transform/parse.gleam", 295).
-spec is_name_start(binary()) -> boolean().
is_name_start(Grapheme) ->
    is_ascii_letter(Grapheme).

-file("src/svg_path/transform/parse.gleam", 287).
-spec is_separator(binary()) -> boolean().
is_separator(Grapheme) ->
    ((((Grapheme =:= <<" "/utf8>>) orelse (Grapheme =:= <<"\n"/utf8>>)) orelse (Grapheme
    =:= <<"\r"/utf8>>))
    orelse (Grapheme =:= <<"\t"/utf8>>))
    orelse (Grapheme =:= <<","/utf8>>).

-file("src/svg_path/transform/parse.gleam", 189).
-spec tokenize_loop(list(binary()), list(token())) -> {ok, list(token())} |
    {error, error()}.
tokenize_loop(Graphemes, Tokens) ->
    case Graphemes of
        [] ->
            {ok, lists:reverse(Tokens)};

        [Grapheme | Rest] ->
            case is_separator(Grapheme) of
                true ->
                    tokenize_loop(Rest, Tokens);

                false ->
                    case Grapheme of
                        <<"("/utf8>> ->
                            tokenize_loop(Rest, [open | Tokens]);

                        <<")"/utf8>> ->
                            tokenize_loop(Rest, [close | Tokens]);

                        _ ->
                            case is_name_start(Grapheme) of
                                true ->
                                    {Name, Rest@1} = read_name(Graphemes, []),
                                    tokenize_loop(
                                        Rest@1,
                                        [{name, Name} | Tokens]
                                    );

                                false ->
                                    case is_number_start(Grapheme) of
                                        true ->
                                            {Raw, Rest@2} = read_number(
                                                Graphemes,
                                                [],
                                                false
                                            ),
                                            case parse_number(Raw) of
                                                {ok, Number} ->
                                                    tokenize_loop(
                                                        Rest@2,
                                                        [{number, Number} |
                                                            Tokens]
                                                    );

                                                {error, _} ->
                                                    {error,
                                                        {invalid_number, Raw}}
                                            end;

                                        false ->
                                            {error,
                                                {unexpected_token, Grapheme}}
                                    end
                            end
                    end
            end
    end.

-file("src/svg_path/transform/parse.gleam", 183).
-spec tokenize(binary()) -> {ok, list(token())} | {error, error()}.
tokenize(Input) ->
    _pipe = Input,
    _pipe@1 = gleam@string:to_graphemes(_pipe),
    tokenize_loop(_pipe@1, []).

-file("src/svg_path/transform/parse.gleam", 48).
?DOC(
    " Parse an SVG transform attribute into a matrix.\n"
    "\n"
    " Empty strings parse as the identity matrix.\n"
).
-spec attribute(binary()) -> {ok, svg_path@transform:matrix()} |
    {error, error()}.
attribute(Input) ->
    case tokenize(Input) of
        {error, Error} ->
            {error, Error};

        {ok, Tokens} ->
            parse_transforms(Tokens, svg_path@transform:identity())
    end.