%% @author Marc Worrell %% @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(<>), [ 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(<>), [ $\n, A | Acc ]); minify(<<$\n, JS/binary>>, Acc) -> minify(next(JS), [ $\n | Acc ]); minify(<>, 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(<>, Acc) -> minify(next(JS), [ C | Acc ]). string(Q, <>, Acc) -> {JS, [ Q | Acc ]}; string(Q, <<$\\, C, JS/binary>>, Acc) -> string(Q, JS, [ C, $\\ | Acc ]); string(Q, <>, 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(<>, 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(<>, 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(<> = A) when C >= 32 -> A; next(<<_, A/binary>>) -> <<" ", A/binary>>. -spec skip_to_eol(binary()) -> binary(). skip_to_eol(<<>>) -> <<>>; skip_to_eol(<> = 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(<>, Acc) -> copy_comment(A, [ C | Acc ]).