Минификатор JavaScript на PHP: регулярка или деление

0

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);
}

Во всех остальных случаях перенос остаётся. Поэтому на выходе не одна бесконечная строка, а несколько.

Где пробел всё-таки обязателен

  1. Между двумя словами или идентификаторами, иначе «return x» слипнется в «returnx» и привет.
  2. Между двумя плюсами или двумя минусами: «a + +b» и «a++b» это совсем разные программы.
  3. После регулярки перед словом, чтобы её флаги не склеились со следующим кодом.
  4. Между слэшем и слэшем или звёздочкой, иначе внезапно получится комментарий.
  5. Между цифрой и точкой, чтобы «1 .toString()» не превратилось в число с дробной частью.

Что получается в итоге

Комментарии с восклицательным знаком, например лицензии, сохраняются. Переменные не переименовываются и мёртвый код не выкидывается, поэтому файл выходит больше, чем у terser. Самые свежие конструкции, которым нужен настоящий разбор, минификатор не понимает и потихоньку кряхтит.

Зато весь код занимает чуть больше двухсот строк, и главное его свойство одно: если сканер сомневается, он ничего не трогает. По тому же принципу живёт и типограф на этом портале. Скромность украшает.

Комментарии

Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡

Пока никто не написал.
Станьте первым! ♡