Упаковщик Javascript: зачистка js-файла

версия для печати

Задача: удалить однострочные/многострочные комментарии, tab-отступы, пустые строки в js-скрипте. Так же нужно учесть, что в коде возможны экранированные back-slash и кавычки. После всего удалить переносы строк, чтобы получится весь код в одной строке. Разбить код на строки заданной длины.

Я не знаю, зачем последнее действие, но все "обфускаторы" так делают =-). Приведенный в статье скрипт я считаю упаковщиком, потому что он уменьшает вес конечного файла, а еще - мне так больше нравится :) Зачем все это нужно, я уже писал здесь. В двух словах: этот скрипт чистит js-файлы для загрузки на сервер.

Код получился большой (для статьи), поэтому разделен на две части, пояснения под кодом. Прежде чем писать статью, я трижды его переделывал, каждый раз думая, что теперь все правильно работает %). Если опять что-то не так будет, укажу здесь дату изменений (см. строку 1).

//Версия v1.4b, 22 октября 2012
$fn='/home/site/js/script.js'
$text=file_get_contents($fn);
$text=preg_replace("#\r\n|\r#","\n",$text);//Приводим концы строк к одному виду - Linux окончания

//---блок_1----
$new='';
//Ищем одно из пяти "контрольных" выражений.
while (preg_match('#.*(//|/\*|[\'"/])#Us',$text,$match,PREG_OFFSET_CAPTURE))
{
 $case=$match[1][0]; //какое именно выражение нашла функция.
 $pos=$match[1][1]+1;//позиция, следующая за найденным выражением. Значение важно только для кавычек. 
   if ($case=='//') $text=preg_replace('#\/\/.*(\n?)$#m', "$1", $text, 1);//Вырезаем один комментарий, начинающийся с //
    elseif($case=='/*') $text=preg_replace('#\/\*.*\*\/#Us', '', $text, 1);//Вырезаем один комментарий, заключенный в "/*...*/"
   else{
       $new.=$match[0][0];//Вхождение всего шаблона, описанного в условии цикла, т.е. весь текущий $text до открывающей кавычки включительно.
      //Ищем не экранированную закрывающую кавычку, одинарную или двойную, в зависимости от открывающей (значение $case).
      //Шаблон: выбрать все до кавычки включительно (не жадный поиск). Все бэк-слеши перед ней выделить отдельно.
        $ptn="#.*?(\\\*)($case)#";       
        $flag=1;
        //Гоняем цикл, пока не найдем НЕ экранированную закрывающую кавычку или пока не возникнет ошибка.
       while ($flag==1){
           $flag=preg_match($ptn, $text, $match, PREG_OFFSET_CAPTURE, $pos);
           //Если количество бэк-слешей перед кавычкой - нечетное, значит она экранированная.
          if(strlen($match[1][0])%2 == 1)
             $pos=$match[2][1]+1;      //Позиция для продолжения поиска - со следующего символа после экранированной кавычки.
            else $flag='break';          //Нашли закрывающую кавычку, больше не гонять цикл.
          $new.=$match[0][0];          //Вхождение всего шаблона. Здесь весь текст до кавычки (включительно), найденой в любом состоянии экрана.
      }
       if(!$flag) die('ERROR: Не найдена закрывающая кавычка. Прерываем зачистку.');
     $text=substr($text,$match[2][1]+1);//Остаток текста после очередной закрывающей кавычки.
    }
}
$new.=$text; //отстаток текста без каких-либо кавычек или комментариев.
$text=preg_replace("#\s+\n+#","\n",$new);//Заменяем все пробелы в конце строки и множественные переносы строк на один перенос строки (Linux).
//---блок_1. Конец----
Пояснения: блок 1

В этом блоке все основано на последних двух параметрах функции preg_match($pattern, $subject, $matches, $FLAGS, $OFFSET).
Смысл: ищем вхождение одного из "выражений": // ИЛИ /* ИЛИ ' ИЛИ " ИЛИ /. Слеши должны быть именно в такой последовательности. В итоге есть 5 вариантов развития событий:
1. однострочный комментарий - регуляркой удаляем все до конца строки (что бы там не было, все это - комментарий). Если есть перенос строки, пишем его. Переноса не будет, только если комментарий написан в конце файла. Это частный случай, но его нужно учитывать.
2. многострочный комментарий - регулярка для удаления такого коммента.
В первых двух случаях изменения происходят с исходным текстом ($text), поэтому в $new ничего не пишется, а $text укорачивается регулярками.
3. и 4. кавычка или двойная кавычка: все до открывающей кавычки (вместе с ней) пишем в $new, потом начиная с позиции +1 от нее ищем не экранированную закрывающую кавычку. Все, что найдем между кавычками (вместе с закрывающей), так же дописываем в $new. После этого отбрасываем часть исходного текста до найденой закрывающей кавычки включительно, т.к. эта часть проверена и больше не нужна в анализе. В конце всего дописываем в $new то, что осталось от исходного текста.
5. Одиночные слеши в js используются для описания регулярного выражения, типа '/pattern/gmi'. Упаковщик должен воспринимать такие выражения, как текст в кавычках, т.е. ничего внутри не меняя. Поэтому обработка такая же, как в случаях 3 и 4.

Сейчас в переменной $text - код скрипта без комментариев. Продолжение:

//Удаление форматирования
$text=preg_replace('#\t#','',$text);      //Убираем tab-отступы.
$text=preg_replace("#\n+\s+#","\n",$text);//Убираем все пробелы в начале строк.
//$text=preg_replace("#\n#",'',$text);    //Убираем переносы строк. Закомментил, т.к. ниже будем делить весь текст на куски.

//---блок_2----
$from=$offset=0;
$new='';
$len=strlen($text);
//Ищем ближайший перенос строки сразу за смещением в NN символов. NN=450. Здесь же следим за краем текста. 
//Если оставшийся кусок будет меньше, то уже ничего не ищем, будем вырезать все, что осталось.
do {
 $offset=$from+450;
  if($offset<$len) $pos=strpos($text,"\n",$offset); else $pos=$len;
   if($pos===false) $pos=$len;
 $sub=substr($text,$from,$pos-$from);    //Вырезаем очередной кусок текста
   $new.=preg_replace("#\n#",'',$sub)."\n";//Убираем внутри него переносы строк, в конце куска наоборот ставим один перенос строки.
    $from=$pos+1;
} while ($pos!=$len);
$text=substr($new,0,-1);//Уберем последний перенос строки. Штрих красоты ;)
//---блок_2. Конец----

file_put_contents($fn,$text);//Пишем итоговый текст в файл.
Пояснения: блок 2

Делим на строки по признаку переноса строки "\n". Почему именно он? Потому что этот признак гарантированно исключает деление строки в неподходящих местах, например посреди текстового значения переменной.

Есть масса вариантов поделить текст кода на строки, начиная от последовательного удаления каждого переноса (считая длину), до заполнения массива кусками текста с последующей склейкой символом "\n". Я выбрал не самый мутный и не самый долгий вариант :) Итоговые куски текста могут получиться короче заявленных NN символов, т.к. из них удаляется несколько переносов строк.

Если блок 2 исключить из скрипта, то в итоговом файле сохранится разбиение на строки. Если же нужен весь код в одной строке, то кроме исключения блока 2 нужно раскомментировать строку (4).

P.S.: Почему нельзя считать данное решение обфускацией? Как сказал в одной статье Крис Касперски "обфускация - это сложная инженерная задача". В википедии есть определение: "Обфускация (от лат. obfuscare — затенять, затемнять; и англ. obfuscate — делать неочевидным, запутанным, сбивать с толку) или запутывание кода — приведение исходного текста или исполняемого кода программы к виду, сохраняющему ее функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции". Поставленная здесь задача - всего лишь чистка исходного кода с целью уменьшения веса файла. Конечно, удаление комментариев делает понимание сложнее, но не настолько, чтоб заявлять об обфускации.

[UPD0] Раньше в этой статье я высказал идею использовать PHP-функцию php_strip_whitespace() для очистки js-скрипта. Я был уверен, что функция будет работать, почему что: синтакис javascript относительно комментариев такой же, как у PHP; экранирование символов в текстовых значениях переменных такое же, а ограничений на эти значения еще больше (в javascript нельзя записать текстовое значение в несколько строк в паре кавычек).

Я ошибался. Синтаксис регулярных выражений js не совпадает с PHP, а именно возможность описать шаблон типа 'prtn=/patten/g'. Такие описания могут привести к неправильной очистке js-кода через php_strip_whitespace(). Поэтому ее использовать нельзя.

[1oo%, EoF]


Похожие материалы: Упаковщик CSS: зачистка css-файла
Понравилась статья? Расскажите о ней друзьям:


Комментарии
Для работы модуля комментариев включите javaScript


Показать/скрыть правила
Имя
[i] [b] [u] [s] [url]
:-) ;-) :D *lol* 8-) :-* :-| :-( *cry* :o :-? *unsure* *oops* :-x *shocked* *zzz* :P *evil*

Осталось 1000 символов.
Код защиты от спама Обновить код
Каждый комментарий проходит ручную модерацию. 100% фильтрация спама.
Продвижение
Время
Метки