%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2018 Marc Worrell
%% @doc Javascript minifier. Based on jsmin.c
%% Copyright 2018 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% jsmin is Copyright (c) 2002 Douglas Crockford (www.crockford.com)
%% see https://github.com/douglascrockford/JSMin/blob/master/jsmin.c
-module(z_jsmin).
-export([
minify/1
]).
-define(is_alnum(C), (
(C >= $a andalso C =< $z) orelse
(C >= $A andalso C =< $Z) orelse
(C >= $0 andalso C =< $9) orelse
C =:= $_ orelse C =:= $\\ orelse C =:= $$ orelse C > 126)).
-define(is_pre_regexp(C), (
C =:= $( orelse C =:= $, orelse C =:= $= orelse C =:= $: orelse
C =:= $[ orelse C =:= $! orelse C =:= $& orelse C =:= $| orelse
C =:= $? orelse C =:= $+ orelse C =:= $- orelse C =:= $~ orelse
C =:= $* orelse C =:= $/ orelse C =:= ${ orelse C =:= $\n)).
-define(isspace(C), (C =:= 32 orelse C =:= $\n)).
%% @doc Minify a binary containing JavaScript.
-spec minify( binary() ) -> binary().
minify( JS ) ->
minify(next(JS), []).
minify(<<>>, [ C | Acc ]) when ?isspace(C) ->
minify(<<>>, Acc);
minify(<<>>, Acc) ->
iolist_to_binary(lists:reverse(Acc));
minify(<<$\n, JS/binary>>, []) ->
minify(next(JS), []);
minify(<<32, JS/binary>>, []) ->
minify(next(JS), []);
minify(<<$\n, JS/binary>>, [ $\n | _ ] = Acc) ->
minify(next(JS), Acc);
minify(<<$\n, JS/binary>>, [ $; | _ ] = Acc) ->
minify(next(JS), Acc);
minify(<<32, JS/binary>>, [ C | _ ] = Acc) when ?isspace(C) ->
minify(next(JS), Acc);
minify(<<32, B, JS/binary>>, [ A | _ ] = Acc)
when ?is_alnum(B) andalso ?is_alnum(A) ->
minify(next(<<B, JS/binary>>), [ 32 | Acc ]);
minify(<<32, $-, JS/binary>>, [ $- | _ ] = Acc) ->
minify(next(<<$-, JS/binary>>), [ 32 | Acc ]);
minify(<<32, $+, JS/binary>>, [ $+ | _ ] = Acc) ->
minify(next(<<$+, JS/binary>>), [ 32 | Acc ]);
minify(<<32, JS/binary>>, Acc) ->
minify(next(JS), Acc);
minify(<<$\n, B, JS/binary>>, [ A | Acc ])
when (?is_alnum(B) orelse B =:= ${ orelse B =:= $[ orelse B =:= $( orelse B =:= $+ orelse B =:= $-) andalso
(?is_alnum(A) orelse A =:= $} orelse A =:= $] orelse A =:= $) orelse B =:= $+ orelse B =:= $- orelse B =:= $" orelse B =:= $' orelse B =:= $`) ->
minify(next(<<B, JS/binary>>), [ $\n, A | Acc ]);
minify(<<$\n, JS/binary>>, Acc) ->
minify(next(JS), [ $\n | Acc ]);
minify(<<Q, JS/binary>>, Acc) when Q =:= $'; Q =:= $"; Q =:= $` ->
{JS1, Acc1} = string(Q, JS, [ Q | Acc]),
minify(next(JS1), Acc1);
minify(<<"/*!", JS/binary>>, Acc) ->
{JS1, Acc1} = copy_comment(JS, [ $!, $*, $/ | Acc ]),
minify(next(JS1), Acc1);
minify(<<$/, JS/binary>>, [ C | _] = Acc) when ?is_pre_regexp(C) ->
Acc1 = case C of
$/ -> [ $/, 32 | Acc ];
$* -> [ $/, 32 | Acc ];
_ -> [ $/ | Acc ]
end,
{JS1, Acc2} = regexp(JS, Acc1),
minify(next(JS1), Acc2);
minify(<<C, JS/binary>>, Acc) ->
minify(next(JS), [ C | Acc ]).
string(Q, <<Q, JS/binary>>, Acc) ->
{JS, [ Q | Acc ]};
string(Q, <<$\\, C, JS/binary>>, Acc) ->
string(Q, JS, [ C, $\\ | Acc ]);
string(Q, <<C, JS/binary>>, Acc) ->
string(Q, JS, [ C | Acc ]);
string(_Q, <<>>, _Acc) ->
throw('Unterminated string').
regexp(<<$[, JS/binary>>, Acc) ->
{JS1, Acc1} = regexp_set(JS, [ $[ | Acc ]),
regexp(JS1, Acc1);
regexp(<<$/, C, _/binary>>, _Acc) when C =:= $/; C =:= $* ->
throw('Unterminated regexp');
regexp(<<$/, JS/binary>>, Acc) ->
{JS, [ $/ | Acc ]};
regexp(<<$\\, C, JS/binary>>, Acc) ->
regexp(JS, [ C, $\\ | Acc ]);
regexp(<<>>, _Acc) ->
throw('Unterminated regexp');
regexp(<<C, JS/binary>>, Acc) ->
regexp(JS, [ C | Acc ]).
regexp_set(<<$], JS/binary>>, Acc) ->
{JS, [ $] | Acc ]};
regexp_set(<<$\\, C, JS/binary>>, Acc) ->
regexp_set(JS, [ C, $\\ | Acc ]);
regexp_set(<<C, JS/binary>>, Acc) ->
regexp_set(JS, [ C | Acc ]);
regexp_set(<<>>, _Acc) ->
throw('Unterminated set in regexp').
-spec next(binary()) -> binary().
next(<<>>) -> <<>>;
next(<<"//", A/binary>>) -> next(skip_to_eol(A));
next(<<"/*!", _/binary>> = A) -> A;
next(<<"/*", A/binary>>) -> next(skip_comment(A));
next(<<$\r, A/binary>>) -> <<$\n, A/binary>>;
next(<<$\n, _/binary>> = A) -> A;
next(<<C, _/binary>> = A) when C >= 32 -> A;
next(<<_, A/binary>>) -> <<" ", A/binary>>.
-spec skip_to_eol(binary()) -> binary().
skip_to_eol(<<>>) -> <<>>;
skip_to_eol(<<C, _/binary>> = A) when C =< $\n -> A;
skip_to_eol(<<_, A/binary>>) -> skip_to_eol(A).
-spec skip_comment(binary()) -> binary().
skip_comment(<<>>) -> throw('Unterminated comment');
skip_comment(<<"*/", A/binary>>) -> <<" ", A/binary>>;
skip_comment(<<_, A/binary>>) -> skip_comment(A).
-spec copy_comment(binary(), list()) -> {binary(), list()}.
copy_comment(<<>>, _Acc) -> throw('Unterminated comment');
copy_comment(<<"*/", A/binary>>, Acc) -> {A, [ $/, $* | Acc ]};
copy_comment(<<C, A/binary>>, Acc) -> copy_comment(A, [ C | Acc ]).