Если открыть исходный код сайта, созданного на MODX с использованием pdoTools и Fenom, иногда можно увидеть огромное количество пустых строк, пробелов и отступов.
В браузере страница при этом выглядит нормально, но HTML получается примерно таким:
<div class="author">
<span>Иванов</span>
<span>Иван</span>
<span>Иванович</span>
</div>
Code language: HTML, XML (xml)На работу сайта такие пробелы обычно практически не влияют. Но когда шаблонов и вложенных чанков становится много, исходный HTML начинает выглядеть довольно неряшливо.
Особенно это заметно на крупных сайтах, где один шаблон собирается из десятков Fenom-чанков.
Возникает естественное желание всё это почистить.
В pdoTools для этого есть очень удобная настройка:
{"strip": true}
Code language: JSON / JSON with Comments (json)Она действительно делает HTML намного аккуратнее.
Но именно здесь нас ждала неожиданная проблема.
Зачем вообще понадобился strip
Fenom позволяет писать шаблоны в удобном для человека виде:
<div class="author">
{if $author}
<span>{$author.lastName}</span>
<span>{$author.firstName}</span>
<span>{$author.middleName}</span>
{/if}
</div>
Code language: HTML, XML (xml)Такой код легко читать и редактировать.
Однако переносы строк и отступы из шаблона частично попадают и в итоговый HTML.
Кроме того, когда используются:
- вложенные чанки;
{if};{foreach};{set};- блоки Fenom;
- вызовы сниппетов;
пустых строк становится ещё больше.
В результате исходный код страницы может содержать десятки пустых строк между элементами.
С точки зрения браузера это обычно не проблема.
Но хотелось получить более компактный HTML на production.
Поэтому в системной настройке pdoTools:
pdotools_fenom_options
мы включили:
{"strip": true}
Code language: JSON / JSON with Comments (json)И на первый взгляд результат оказался отличным.
HTML стал компактным.
Но затем клиент заметил странную ошибку
Через некоторое время обнаружилась проблема.
Например, в шаблоне было:
<span>Иванов</span>
<span>Иван</span>
<span>Иванович</span>
Code language: HTML, XML (xml)До включения strip браузер отображал:
Иванов Иван Иванович
А после включения:
ИвановИванИванович
То есть слова внезапно склеились.
На первый взгляд это выглядит странно: ведь внутри каждого <span> текст остался на месте.
Проблема находится между тегами.
Откуда между <span> вообще берётся пробел
Возьмём обычный HTML:
<span>Иванов</span>
<span>Иван</span>
Code language: HTML, XML (xml)Между:
</span>
Code language: HTML, XML (xml)и:
<span>
Code language: HTML, XML (xml)есть перенос строки.
Для HTML это текстовый whitespace.
Браузер отображает его как обычный пробел.
Поэтому визуально получается:
Иванов Иван
Если записать теги подряд:
<span>Иванов</span><span>Иван</span>
Code language: HTML, XML (xml)то между ними больше нет никакого текстового символа.
И браузер совершенно правильно показывает:
ИвановИван
Именно это в нашем случае происходило после strip.
Почему strip нельзя было просто оставить
Fenom выполнял очистку шаблона и превращал конструкцию:
<span>Иванов</span>
<span>Иван</span>
Code language: HTML, XML (xml)фактически в:
<span>Иванов</span><span>Иван</span>
Code language: HTML, XML (xml)Для многих HTML-элементов это совершенно безопасно.
Например:
</div>
<div>
Code language: HTML, XML (xml)можно спокойно превратить в:
</div><div>
Code language: HTML, XML (xml)Потому что <div> — блочный элемент, и наличие текстового пробела между двумя div визуально ничего не меняет.
Но с inline-элементами всё иначе.
Например:
<span>
<strong>
<a>
<em>
<small>
<time>
Code language: HTML, XML (xml)они могут находиться прямо внутри предложения.
Поэтому пробел между ними иногда является частью текста, а не просто форматированием HTML.
Почему нельзя было просто исправить все статьи
Самый очевидный вариант решения:
<span>Иванов</span> <span>Иван</span> <span>Иванович</span>
Code language: HTML, XML (xml)  — это HTML-код обычного пробела.
Даже после удаления переносов получится:
<span>Иванов</span> <span>Иван</span> <span>Иванович</span>
Code language: HTML, XML (xml)и браузер покажет:
Иванов Ивано Иванович
Технически решение работает.
Практически — нет.
На существующем сайте уже было большое количество:
- страниц;
- статей;
- чанков;
- шаблонов;
- старого HTML;
- контента, созданного через редактор.
И невозможно вручную искать по всему сайту конструкции вроде:
</span> <span>
Code language: HTML, XML (xml)и заменять пробелы на:
 
Code language: HTML, XML (xml)Кроме того, такие изменения пришлось бы помнить при создании каждого нового материала.
Это плохая архитектура.
Шаблоны должны оставаться естественными:
<span>Иванов</span>
<span>Иван</span>
Code language: HTML, XML (xml)а проблема должна решаться централизованно.
Почему нельзя было настроить сам strip
Следующая идея — оставить:
{"strip": true}
Code language: JSON / JSON with Comments (json)но изменить его поведение.
Например:
Удаляй пробелы между
div,section,article, но сохраняй их междуspan.
К сожалению, стандартный механизм strip Fenom не предоставляет такой тонкой настройки.
Он либо включён, либо выключен.
Поэтому мы решили:
- оставить стандартный
strip; - перед его выполнением найти потенциально опасные пробелы;
- заменить их на HTML-сущность
 .
Для этого удобно использовать событие pdoTools:
pdoToolsOnFenomInit
Что такое pdoToolsOnFenomInit
pdoTools использует Fenom для обработки шаблонов.
Перед тем как Fenom начнёт компилировать шаблоны, pdoTools позволяет подключиться к его экземпляру через событие:
pdoToolsOnFenomInit
На этом этапе можно добавить собственный Fenom filter.
Схема получается примерно такой:
Fenom-шаблон
↓
наш filter
↓
защита важных пробелов
↓
стандартный strip
↓
компиляция
↓
готовый HTML
Главная идея состоит в том, что мы не пытаемся переписывать уже готовый HTML.
Мы вмешиваемся до выполнения стандартного strip.
Какие пробелы нужно защищать
Нас интересует конструкция:
</span>
<span>
Code language: HTML, XML (xml)или:
</strong> <span>
Code language: HTML, XML (xml)или:
</a>
<strong>
Code language: HTML, XML (xml)То есть:
закрывающий inline-тег
+
один или несколько пробельных символов
+
открывающий inline-тег
Например:
</span>
<span>
Code language: HTML, XML (xml)Нужно превратить в:
</span> <span>
Code language: HTML, XML (xml)После этого стандартный strip уже не сможет удалить пробел, потому что перед ним находится не настоящий whitespace, а HTML-сущность:
 
Code language: HTML, XML (xml)В браузере она снова превратится в обычный пробел.
Какие HTML-теги считаем inline
Для нашего фильтра использовался список:
$inlineTags = [
'a',
'abbr',
'b',
'bdi',
'bdo',
'cite',
'code',
'del',
'dfn',
'em',
'i',
'ins',
'kbd',
'mark',
'q',
's',
'samp',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'var',
];
Code language: PHP (php)То есть фильтр не пытается защищать вообще любые два соседних HTML-тега.
Например:
</div>
<div>
Code language: HTML, XML (xml)нас не интересует.
А вот:
</span>
<span>
Code language: HTML, XML (xml)интересует.
Именно это делает обработку намного безопаснее.
Как работает регулярное выражение
Основная часть решения выглядит так:
preg_replace(
'~(</(?:' . $inlineTags . ')\s*>)\s+(<(?:' . $inlineTags . ')\b)~iu',
'$1 $2',
$text
);
Code language: PHP (php)На первый взгляд выражение выглядит пугающе, но идея очень простая.
Оно ищет:
</inline-tag>
Code language: HTML, XML (xml)затем:
пробелы / табы / переносы строк
и затем:
<inline-tag
Например:
</span>
<span>
Code language: HTML, XML (xml)И превращает это в:
</span> <span>
Code language: HTML, XML (xml)При этом содержимое самих тегов не изменяется.
Что получается после обработки
Исходный шаблон:
<div class="author">
<span>Иванов</span>
<span>Иван</span>
<span>Иванович</span>
</div>
Code language: HTML, XML (xml)После нашего фильтра:
<div class="author">
<span>Иванов</span> <span>Иван</span> <span>Иванович</span>
</div>
Code language: HTML, XML (xml)После стандартного strip:
<div class="author"><span>Иванов</span> <span>Иван</span> <span>Иванович</span></div>
Code language: HTML, XML (xml)А пользователь в браузере видит:
Иванов Иван Иванович
То есть мы получили сразу оба преимущества:
HTML компактный, но текст не склеивается.
Почему не стоит просто удалять всё через регулярное выражение
Иногда для минификации HTML советуют использовать что-нибудь вроде:
$html = preg_replace(
'/>\s+</',
'><',
$html
);
Code language: PHP (php)На первый взгляд это кажется идеальным решением.
Например:
<div>
<p>Text</p>
</div>
Code language: HTML, XML (xml)превращается в:
<div><p>Text</p></div>
Code language: HTML, XML (xml)Но проблема с inline-тегами никуда не исчезает.
Вот это:
<span>Hello</span>
<span>world</span>
Code language: HTML, XML (xml)тоже превратится в:
<span>Hello</span><span>world</span>
Code language: HTML, XML (xml)и браузер покажет:
Helloworld
Поэтому минификация HTML — немного более сложная задача, чем просто:
«Удалить всё между
>и<».
Whitespace в HTML иногда является частью содержимого страницы.
Почему решение через плагин удобнее
В результате не пришлось менять:
- старые статьи;
- существующие чанки;
- шаблоны;
- содержимое TinyMCE;
- HTML внутри ресурсов.
Редактор по-прежнему может писать нормальный HTML:
<span>Первое слово</span>
<span>Второе слово</span>
Code language: HTML, XML (xml)а система сама превращает безопасный пробел в:
 
Code language: HTML, XML (xml)перед выполнением strip.
Особенно удобно, что решение работает централизованно.
Если на сайте сотни или тысячи материалов, достаточно изменить поведение Fenom один раз.
Где создать плагин в MODX
В MODX Manager создаём обычный Plugin.
Например:
FenomSafeStrip
В системных событиях плагина включаем:
pdoToolsOnFenomInit
При этом системная настройка:
pdotools_fenom_options
может оставаться:
{"strip": true}
Code language: JSON / JSON with Comments (json)Наш plugin будет защищать важные пробелы до того, как Fenom выполнит strip.
После создания или изменения плагина желательно очистить кеш MODX.
Полный код плагина
Ниже полный вариант плагина, который использовался для решения проблемы.
<?php
/**
* FenomSafeStrip
*
* MODX / pdoTools / Fenom
*
* Событие:
* pdoToolsOnFenomInit
*
* Позволяет использовать:
*
* pdotools_fenom_options = {"strip":true}
*
* и при этом не склеивать текст между соседними
* inline HTML-элементами:
*
* <span>Иванов</span>
* <span>Иван</span>
*
* превращается перед strip в:
*
* <span>Иванов</span> <span>Иван</span>
*/
/**
* Получаем экземпляр Fenom.
*/
if (
!isset($fenom)
|| !is_object($fenom)
) {
return;
}
/**
* Inline HTML-теги, между которыми whitespace
* может быть частью отображаемого текста.
*/
$inlineTags = implode('|', [
'a',
'abbr',
'b',
'bdi',
'bdo',
'cite',
'code',
'del',
'dfn',
'em',
'i',
'ins',
'kbd',
'mark',
'q',
's',
'samp',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'var',
]);
/**
* Добавляем pre-filter для Fenom.
*
* Он выполняется до стандартного AUTO_STRIP.
*/
$fenom->addFilter(
static function ($template, $text) use ($inlineTags) {
/**
* Ищем:
*
* </inline-tag>
* whitespace
* <inline-tag>
*
* Например:
*
* </span>
* <span>
*
* и заменяем whitespace на HTML-пробел:
*
* </span> <span>
*
* После этого strip уже не сможет удалить
* этот пробел.
*/
$result = preg_replace(
'~(</(?:'
. $inlineTags
. ')\s*>)\s+(<(?:'
. $inlineTags
. ')\b)~iu',
'$1 $2',
$text
);
/**
* В случае ошибки preg_replace
* возвращаем исходный шаблон.
*/
return $result ?? $text;
}
);
Code language: HTML, XML (xml)Что проверить после установки
Для начала достаточно создать тест:
<p>
<span>Иванов</span>
<span>Иван</span>
<span>Иванович</span>
</p>
Code language: HTML, XML (xml)При:
{"strip": true}
Code language: JSON / JSON with Comments (json)в исходном HTML желательно увидеть примерно:
<p><span>Иванов</span> <span>Иван</span> <span>Иванович</span></p>
Code language: HTML, XML (xml)А визуально на странице:
Иванов Иван Иванович
Также стоит проверить комбинации:
<strong>Очень</strong>
<span>важный</span>
<em>текст</em>
Code language: HTML, XML (xml)и:
<a href="#">Первая</a>
<a href="#">Вторая</a>
Code language: HTML, XML (xml)При этом обычные блочные элементы должны продолжить нормально очищаться:
</div>
<div>
Code language: HTML, XML (xml)Важный момент
Этот plugin не является универсальным HTML-minifier.
Он решает конкретную проблему:
позволяет использовать Fenom
strip, сохраняя пробелы между соседними inline-элементами там, где обычныйstripмог бы изменить отображаемый текст.
И именно ограниченность решения здесь является преимуществом.
Чем агрессивнее мы пытаемся «оптимизировать» HTML регулярными выражениями, тем больше вероятность случайно изменить содержимое страницы.
Вывод
Иногда оптимизация, которая кажется совершенно безопасной, неожиданно затрагивает сам контент.
Настройка:
{"strip": true}
Code language: JSON / JSON with Comments (json)отлично очищает HTML Fenom от лишних переносов и отступов.
Но HTML-whitespace — это не всегда просто мусор.
В конструкции:
<span>Иванов</span>
<span>Иван</span>
Code language: HTML, XML (xml)перенос строки одновременно является пробелом между словами.
Поэтому глобальное удаление whitespace привело к:
ИвановИван
Вместо изменения сотен существующих страниц мы перенесли решение на уровень шаблонизатора:
исходный Fenom
↓
pdoToolsOnFenomInit
↓
защищаем whitespace между inline-тегами
↓
 
↓
Fenom strip
↓
компактный HTML
↓
текст остаётся читаемым
Code language: HTML, XML (xml)В результате удалось сохранить преимущества strip, не переписывая существующие материалы и не заставляя редакторов вручную вставлять   между тегами.
Это хороший пример общего принципа при работе с большими сайтами: если одна и та же проблема встречается в сотнях страниц, лучше исправить правило обработки один раз, чем исправлять сотни страниц вручную.
