« Маленький дебаггер для flex | Про работу в php с архивами » |
Сжатие css. Небольшая самописная утилитка
Сказка про то как я программу писал. Утилита для сжатия css-файлов + немного рассказа об регулярных выражениях.Хороший сайт – быстрый сайт. Надеюсь, никто спорить не будет? В борьбе за каждый байт и секунду одним из наиболее привычных способов ускорения (честно говоря, я просто предпочитаю выполнять gzip сжатие возвращаемого с сервера контента и ничего более) является использование css и javascript упаковщиков. В основе их работы лежит тот факт, что написанный вами красивый и удобочитаемый javascript или css-код:
/* це стиль для левой колонки сайта */
.foo{
padding: 3px;
/*
TODO: спросить у Васи чем ему не нравится красный цвет шрифта
*/
color: red;
}
И существует много, нет, не так, великое множество всевозможных утилит “уплотнителей и сжимателей” которые делают это. Найдется и множество сайтов, на которых вы, введя на специальной странице в html-форму текст вашего css или js-файла, получите на выходе сжатую версию оного.
А самое удобное - это когда тебя просят чуть-чуть подправить сайт (ну это делов-то на полчаса, не более) в котором все js и css файлы были так “улучшены”. Честно говоря, это не проблема т.к. есть и утилиты, выполняющие обратный процесс “развертывания” css и js-кода, но очевидно, что это развертывание вернет вам только форматирование, но не комментарии. Ага, а еще мне дают самые полные и точные версии (с документацией, схемами, описаниями моделей БД) на дискетке. К сожалению, часто бывает так, что в условиях постоянного аврала самая последняя (и главное хоть как-то рабочая) версия находится на хостинге и только на нем. А дискетка …? Забудьте!
Я сам (в тех случаях, когда вопрос производительности работы создаваемого сайта не критичен) предпочитаю динамическую генерацию css и js-файлов на основании полных (с комментариями, исходниками) версий. Например, так:
<script src=”js.php?files=main.js,about.js,for_ie_fixes.js”></script>
Но сегодняшняя заметка немного не о том. Пару дней назад у меня было немного свободного времени, и я решил тряхнуть стариной и написал на java небольшую утилиту, которая сжимает css-файлы. А почему тряхнул стариной? Так уже довольно давно не писал я java-кода работающего с regexp-ами – “пламенным мотором” этой программки – вспомнить довольно приятно. А результаты своего “графоманства” выложил на всеобщий обзор, не забыв снабдить комментариями. Ссылка на конечные исходники лежит в самом конце страницы, а пока поехали. Если вы не знаете, кто такие regexp-ы и ни разу не писали код на java, то вам лучше было бы посетить более специализированные ресурсы.
Возможные способы сжатия я подсмотрел на сайте ?.? (никак не могу найти ссылку, но потеря не велика, как я говорил ранее подобных веб-приложений тьма тьмущая).
Внешний вид программки я набросал в intellij idea. Конструктор форм там довольно приятный и я все чаще пользуюсь ним, а не создаю gui-приложений “ручками”.

Интерфейс довольно прост: вы должны указать тот файл css или каталог с файлами css, которые хотите обработать. Затем нужно указать в какой кодировке файлы следует читать (падающий список для “input character set”). Затем кроме указания того, куда нужно сохранить результат работы (новое имя файла или каталог, куда будут помещены файлы css после обработки) следует задать и кодировку выходных файлов.
Затем вы должны снять отметку с checkbox “skip compress (only change charset)”. Если эта опция включена, то файлы будут скопированы в новое местоположение и у них будет заменена кодировка, не более.
В java есть встроенная поддержка регулярных выражений со времен jdk 1.4. Это где-то лет пять, начиная с 2002 г. Поддержкой regexp-ов занимается пакет java.util.regex. В состав его входят классы: Pattern и Matcher. Первый из них содержит код, который получает при создании объекта Pattern код регулярного выражения, компилирует его, и если вы где-то ошиблись, то генерирует исключение PatternSyntaxException:
try{
Pattern p = Pattern.compile("xyz");
}catch (PatternSyntaxException e){
// что то поломалось
}
public boolean matches(String regex)
if (cssFileContent.matches(‘vasyano petrovno’)){
// да этот файл написал Вася
}
Опции сжатия делятся на следующие группы:
1.Обработка пробелов и табуляторов:

Первое правило в данной группе – это (replace multiply spaces just one space) замена нескольких подряд идущих пробелов одним.
Давайте разберем, как это сделать.
По правде говоря, в составе класса String есть функция replace. Но ее отличие в том, что мы должны явно указать ту строку, которую хотим заменить на другую. Например, так:
s = “vasyano petrovno is cool user”.replace (‘cool’, ‘bad’);
* Заменить в строке два пробела на один * Повторить предыдущее действие до тех пор, пока в строке не останется двойных пробелов.Но делать мы так не будем, ведь это громоздко и неудобочитаемо, в любом случае для более сложных задач обойтись без regexp-ов не возможно (здесь и везде далее, в переменной sin хранится исходная строка и в нее помещается результат преобразования). Для почти всех следующих задач поиска и замены мы будем использовать фунциую replaceAll. У нее два параметра: что искать (регулярное выражение) и на что заменить (здесь тоже есть специальные “значки”, но об этом позже).
sin = sin.replaceAll("\\s{2,}", " ");
При написании регулярных выражений советую использовать встроенный в idea plugin “Regexp”.

Следующая опция сжатия: “Remove Space Around Chars ;:{},”. Откровенно говоря, я был в небольшом сомнении относительно полезности данного режима, т.к. в уголках памяти “билась” мысль, что по стандарту html наличие пробелов по краям фигурных скобок является обязательным. Но, проверив быстренько работу получающихся файлов в ie, я решил, что, наверное, ошибся, искать нужную статью было откровенно лень.
Итак, для этого режима я использовал следующий regexp. Обратите внимание на то, что перед открывающей фигурной скобкой я поставил два обратных слэша.
//Remove space around chars ;:{},
sin = sin.replaceAll(" \\{", "{");
sin = sin.replaceAll(" }", "}");
sin = sin.replaceAll("\\{ ", "{");
sin = sin.replaceAll("} ", "}");
sin = sin.replaceAll(" ,", ",");
sin = sin.replaceAll(", ", ",");
sin = sin.replaceAll(" :", ":");
sin = sin.replaceAll(": ", ":");
sin = sin.replaceAll(" ;", ";");
sin = sin.replaceAll("; ", ";");
<table class="table-mediawiki" String x = "(\\{|}|,|:|;)";>
<tr>
<td sin = sin.replaceAll(x+" ", "$1");
sin = sin.replaceAll(" "+x, "$1");
Третий шаг преобразования: “Leave Spaces Between Selectors”. Это значит, что если в исходном css-файле найдется следующий фрагмент:
body,td,p,input { /*здесь между селекторами body, td, p, input НЕТ ПРОБЕЛОВ*/
body, td, p, input{/* а теперь пробелы есть*/
sin = sin.replaceAll("(?is)(,)(?!\\s)(?!\")", ", ");
Итак я ищу в строке запятую, после которой не должен идти пробел (на отрицание указывает наличие перед \\s специального модификатора (?!) – “не должно быть”). Затем идет не кавычка. Найденное такое совпадение должно быть заменено на запятую и пробел.
Следующий режим сжатия css: “Leave Spaces Between Properties”. Это значит, что если в исходном файле встречается такая комбинация символов:
p {font-face: Arial;font-size: 10px;font-weight: bold;}
p {font-face:Arial; font-size:10px; font-weight:bold}
sin = sin.replaceAll("(?is);(?!\\s)", "; ");
sin = sin.replaceAll("(?is);\\s+}", "}");
Следующее правило css-сжатия: “Remove Tab”. Здесь все очевидно, просто находим символы \t и удаляем их.
sin = sin.replaceAll("\t", "");

Первое правило в этой группе: “Leave Lines as they are”. Без комментариев, мы просто ничего не делаем.
Второй вариант “Replace nultiply empty lines just one empty line” значит, что надо найти несколько подряд идущих символов перехода на новую строку и заменить их одним, например, так:
sin = sin.replaceAll("(\n){2,}", "\n");
sin = sin.replaceAll("\n", "");

Первый вариант снова самый простой “don’t strip any comments” – комментарии должны остаться без изменений.
Второй режим “strip all comments” требует того, чтобы мы удалили все комментарии из файла, делаем:
sin = sin.replaceAll("(?is)/\\*.*?\\*/", "");
/* vasya is cool */
P {color: red;}
/* lena is cool */
Третий режим работы “Strip Comments at least X chars long” значит что нужно удалить лишь те комментарии, длина которых превосходит X символов (X – вводится в текстовое поле). Честно говоря, тут я написать regexp не смог, точнее смог, но на ряде тестов он провалился. Это ситуации, когда идет несколько подряд расположенных очень коротких комментариев по 2, 3 буквы. Так что мне пришлось сделать так:
//Replace multiple empty lines with just one empty line
//первая строка тривиальна – в переменную pi помещается значение
//предельной длины комментария из конфига.
Integer pi = Integer.parseInt(compset.getProperty(VALUE_FOR_STRIP_COMMENTS_AT_LEAST_CHARS_LONG));
StringBuffer sb = null;
// sb – буфер, куда будет накапливаться результат преобразования
Pattern p = null;
Matcher m = null;
try {
// компилируем regexp, который ищет все комментарии без учета их длины
p = Pattern.compile("(?is)/\\*.*?\\*/");
m = p.matcher(sin);
// создаем объект Matcher, с помощью которого затем будет организован
// цикл перебора всех найденных (подошедших под шаблон) строк-коментариев.
sb = new StringBuffer();
while (m.find()) {
// функция find ищет в исходной строке очередной подошедший для regexp-а фрагмент,
// но как только таких совпадений больше нет, то цикл будет прекращен
try {
String gr_0 = m.group(0);
// все группы (части regexp-а заключенные в круглые скобки) могут быть доступны
// с помощью функции group(номер_группы).
// Есть особый номер группы – 0 – эта группа захватывает абсолютно весь текст строки,
// который проассоциировался с регулярным выражением
if (gr_0.length() - 4 <= pi)
// проверяем, что если длина этого комментария за вычетом четырех символов
//(два знака “*” и два знака “/”) все же не смогла превзойти предельную то в буфер sb помещается комментарий
m.appendReplacement(sb, gr_0);
//else
// иначе комментарий удалется
// m.appendReplacement(sb, "");
} catch (Exception e) {
e.printStackTrace();
}
}// end of -- while --
// завершаем обработку хвоста исходной строки – после последнего совпадения с regexp
m.appendTail(sb);
} catch (Exception e) {
e.printStackTrace();
}
sin = sb.toString();

Первое правило “compress color codes where possible”. Этот режим сжатия основан на том, что если значение некоторого css-свойства задающего цвет равно, например:
#FF0088 , #FF0000 , #FC0080 , #FFFFFF
#F08 , #F00 , #FC0080 , #FFF
sin = sin.replaceAll("([A-Fa-f0-9])\\1([A-Fa-f0-9])\\2([A-Fa-f0-9])\\3", "$1$2$3 ");
Следующее правило сжатия: “One Command Per Line”. В том случае если в исходной строке встретится нечто вроде:
p {
font-face: Arial;
font-size: 10px;
font-weight: bold;
}
p {
font-face:Arial;font-size:10px;font-weight:bold
}
StringBuffer sb = null;
// буфер куда будет помещен результат преобразования
Pattern p = null;
Matcher m = null;
try {
// в этом шаблоне я использую следующую запись:
// найти “начало строки” (за это отвечает запись “^” и для того, чтобы она корректно работала
// мне пришлось включить режим “?m” - multiline). Возможно идет несколько знаков табуляции за
// которым должены идти любые символы (возможно css-свойство состоит из нескольких селекторов),
// затем символ “{”, снова любые символы и, наконец, “}”.
p = Pattern.compile("(?ims)^[\\t ]*(.*?)(\\n?)\\{(.*?)(\\n*)\\}");
m = p.matcher(sin);
sb = new StringBuffer();
while (m.find()) {
try {
String gr_1 = m.group(1);
String gr_3 = m.group(3);
// найденные фрагменты добавляются к sb, но предварительно
// я избавляюсь от знаков перехода на новую строку
m.appendReplacement(sb, gr_1.replace("\n", "") + "{" + gr_3.replace("\n", " ") + "}\n");
} catch (Exception e) {
e.printStackTrace();
}
}//end -- while --
m.appendTail(sb);
} catch (Exception e) {
e.printStackTrace();
}
sin = sb.toString();
« Маленький дебаггер для flex | Про работу в php с архивами » |