Минификатор JavaScript на PHP: регулярка или деление
JavaScript ♡
Минификатор кажется задачей на пять минут: выкинул пробелы и комментарии, готово. А потом выясняется, что в JavaScript один и тот же слэш умеет быть делением, началом регулярки и началом комментария, причём в зависимости от настроения. Пробелы и переносы строк тоже иногда единственное, что не даёт коду развалиться. Знакомьтесь: «JavaScript, любящий сюрпризы».
Что умеет мой минификатор
В портале есть инструмент, который сжимает JavaScript. Настоящего парсера в нём нет, только сканер, бегущий по символам. Он убирает комментарии и лишние пробелы, а переменные не переименовывает.
Это осознанное решение, оно даже записано в комментарии к классу JsMinifier. Результат получается жирнее, чем у terser или esbuild, зато безопасный. Мощное сжатие требует разбирать язык целиком, а мне для этого инструмента такой цирк не нужен.
Ловушка первая: строки и шаблоны
Всё, что внутри кавычек, трогать нельзя: пробелы в строке это данные, а не украшения. Поэтому, встретив кавычку, сканер читает строку целиком вместе с экранированием и ничего в ней не правит.
Отдельное развлечение шаблонные строки в обратных кавычках. В них бывают вставки в фигурных скобках, внутри вставок свои строки, а иногда и вложенные шаблоны, привет, матрёшка. Для этого есть отдельная функция со счётчиком вложенности, код внутри вставок остаётся нетронутым.
Ловушка вторая: слэш, он же тот коллега на трёх проектах
Самое интересное место. Встретив слэш, надо понять, кто перед нами: деление или начало регулярки. Ошибёшься, примешь деление за регулярку, и сканер «съест» полфайла в поисках закрывающего слэша. Решение принимается по последнему символу уже записанного результата:
private static function regexAllowed(string $out): bool
{
if ($out === '') {
return true;
}
$last = $out[strlen($out) - 1];
if (str_contains('(,=:[!&|?{};+-*%<>~^', $last)) {
return true;
}
if ($last === '}') {
return true;
}
if (ctype_alpha($last)) {
return (bool) preg_match(self::KEYWORDS_BEFORE_REGEX, substr($out, -12));
}
return false;
}
Логика такая. После скобки, запятой, знака равенства или оператора дальше начинается выражение, значит, слэш открывает регулярку. После слова решают ключевые слова: после return или typeof регулярка возможна, после обычной переменной нет. Если до конца строки закрывающий слэш так и не нашёлся, сканер честно сдаётся и считает слэш делением.
Упрощение, о котором лучше знать
После закрывающей фигурной скобки я считаю, что дальше может идти регулярка. Для конца блока это верно, для конца объектного литерала нет. Код от такой ошибки не ломается: когда сканер не уверен, он оставляет текст как есть. Просто иногда сжимает хуже, чем мог бы. Принцип «работает, не трогай» в действии.
Ловушка третья: перенос строки не пустое место
В JavaScript точка с запятой иногда подставляется сама, ориентируясь на перенос строки (это называется ASI, привет ещё одному любителю «я сам всё сделаю»). Поэтому склеишь две строки в одну, и смысл программы может поплыть. Я удаляю перенос только там, где это точно безопасно: после символа, на котором выражение не может закончиться, или перед символом, который обязательно продолжает выражение.
private static function newlineIsSafeToDrop(string $last, string $next): bool
{
return str_contains('{([,;=*%&|^?:<>', $last)
|| str_contains(')]}.,;?:=*%&|^<>([', $next);
}
Во всех остальных случаях перенос остаётся. Поэтому на выходе не одна бесконечная строка, а несколько.
Где пробел всё-таки обязателен
- Между двумя словами или идентификаторами, иначе «return x» слипнется в «returnx» и привет.
- Между двумя плюсами или двумя минусами: «a + +b» и «a++b» это совсем разные программы.
- После регулярки перед словом, чтобы её флаги не склеились со следующим кодом.
- Между слэшем и слэшем или звёздочкой, иначе внезапно получится комментарий.
- Между цифрой и точкой, чтобы «1 .toString()» не превратилось в число с дробной частью.
Что получается в итоге
Комментарии с восклицательным знаком, например лицензии, сохраняются. Переменные не переименовываются и мёртвый код не выкидывается, поэтому файл выходит больше, чем у terser. Самые свежие конструкции, которым нужен настоящий разбор, минификатор не понимает и потихоньку кряхтит.
Зато весь код занимает чуть больше двухсот строк, и главное его свойство одно: если сканер сомневается, он ничего не трогает. По тому же принципу живёт и типограф на этом портале. Скромность украшает.
Комментарии
Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡
Делитесь
своими мыслями
— мне очень
это важно ♡