{% else-1 %}
Веб-разработчикам часто приходится просматривать исходный HTML-код. Как правило, он оказывается довольно скверно оформлен, а потому трудно читаем. И хотя современные средства, такие как firebug, позволяют обойти эту проблему, иметь выровненный по вложенности и избавленный от каскадов пустых строк HTML-код всё же бывает полезно. В настоящей статье предлагается простое средство для автоматического выравнивания HTML-кода, реализованное на языке PHP.

Пример выравнивания можно увидеть, например, открыв исходный код любой страницы потрала webew.ru. Принципы выравнивания такие:

каждый тег переносится на новую строку, и перед ним добавляется необходимое количество отступов
если тег открывающий, то перед каждой строкой текста, идущего за ним, добавляется дополнительный отступ
если тег закрывающий, то перед ним и перед текстом, следующим за ним, добавляется на один отступ меньше
одиночный тег просто переносится на новую строку, не меняя текущее количество отступов
пустые строки (не содержащие символов, кроме пробельных) удаляются
переводы строки внутри самих тегов (внутри <...>* заменяются на пробелы
теги <textarea> и <pre> обрабатываются отдельно, т.к. содержимое этих тегов нельзя вообще никак изменять1 (оно, кстати, может порядком испортить вид результирующего HTML-кода, но с этим придется смириться)
тег <script> тоже стоит особняком от других: внутри него нельзя изменять содержимое строковых javascript-переменных; для простоты и ясности он обрабатыается аналогично <textarea> и <pre>
Есть возможность указать символ или последовательность символов, формирующую отступ, а также набор тегов, которые не следует обрабатывать (например, инлайновые теги, такие как <a>, <span> и др.)

                        
Код выполнен в виде одного небольшого класса2, выровненный HTML можно получить при помощи нескольких строк:

$align = new alignedXHTML;
$align->SPACER = " "; // устанавливаем отступ по два пробела
$align->SKIPTAGS = array('a', 'img', 'span', 'sup'); // эти теги трогать не будем
// допустим, ранее сформированный HTML-код хранится в переменной $html
$aligned_html = $align->parse($html); // получили выровненный HTML
Класс будет правильно работать при соблюдении следующих условий:

количество открывающих тегов равно количеству закрывающих (т.е. код корректный)
одиночный тег всегда имеет слэш прямо перед закрывающей скобкой (<br/> или <br />, но не<br>)
Последнее требование является частью стандарта XHTML, поэтому в итоге класс получил название alignedXHTML.

Основным недостатком рассматриваемого способа является его сравнительно низкое быстродействие: обработка 100 Кб HTML-кода на сервере, обслуживающем webew.ru, занимает порядка 0.05 сек. Видимо, упирается здесь всё в низкую производительность PHP (сам код подвергался достаточно тщательной оптимизации), и тут уж ничего не поделаешь3.

Далее приводится непосредственно сам код.

class alignedXHTML
{
public $SPACER;
public $OFFSET;
public $SKIPTAGS;
function parse($xhtml)
{
if (is_null($this->SPACER)) {$this->SPACER = " "; }
if (is_null($this->OFFSET)) {$this->OFFSET = 0; }
if (is_null($this->SKIPTAGS)) {
$this->SKIPTAGS = array('a', 'span', 'img', 'sup', 'sub');
}
/*
Теги <textarea>, <pre> и <script> - особенные, и с ними
придется попотеть.
Нужно защитить содержимое этих тегов от вмешательства
при выравнивании: убрать на время, во-первых, переводы строки,
во-вторых, HTML-теги, которые могут встретиться внутри
строковых переменных в скриптах
(точнее, не сами теги, а открывающие и закрывающие скобки).
*/
$xhtml = str_replace(array("x01", "x02", "x03"), '', $xhtml);
$xhtml = preg_replace_callback(
'/
(<(textarea|script|pre)(?:[^>"']*|"[^"]*"|'[^']*')*>)
(.*?)
(</2>)
/six',
// модификатор 's' не забываем: точка тут должна совпадать
// и с символом новой строки; модификатор 'x' позволяет добавлять в шаблон
// необрабатываемые пробелы и переводы строки, чтобы он лучше читался
create_function(
'$matches',
'$tagbody = $matches[3];
$tagbody = str_replace(" ", "x01", $tagbody);
$tagbody = str_replace("<", "x02", $tagbody);
$tagbody = str_replace(">", "x03", $tagbody);
return $matches[1] . $tagbody . $matches[4];'
),
$xhtml);

// регулярное выражение для HTML-тега
// (модификатор s не нужен, т.к. точки в выражении нет)
$tagpattern = '/<(/?)(w+)(?:[^>"']*|"[^"]*"|'[^']*')*>/';

// убираем переводы строки внутри тегов (заменяем на пробелы)
$xhtml = preg_replace_callback(
$tagpattern,
create_function(
'$matches',
'return str_replace(" ", " ", $matches[0]);'
),
$xhtml);

// теперь обрабатыавем XHTML-код по одной строке
// (PHP это не умеет, поэтому пришлось вручную)
$start = 0;
$final_xhtml = '';
do
{
$end = strpos($xhtml, " ", $start);
$line = ($end !== FALSE)
? substr($xhtml, $start, $end - ($start - 1) )
: substr($xhtml, $start);
$line = ltrim($line); // убираем ведущие пробелы, чтоб не мешали выравнивать
$final_xhtml .=
str_repeat($this->SPACER, $this->OFFSET) .
preg_replace_callback(
$tagpattern,
array($this, 'alignXHTMLtags'),
$line);
$start = $end + 1;
}
while ($end !== FALSE);

// убираем пустые строки
$final_xhtml = preg_replace('/ s*(?= )/m', '', $final_xhtml);

// возвращаем обратно содержимое <textarea>, <pre> и <script>
$final_xhtml = str_replace("x01", " ", $final_xhtml);
$final_xhtml = str_replace("x02", "<", $final_xhtml);
$final_xhtml = str_replace("x03", ">", $final_xhtml);

return $final_xhtml;
}

function alignXHTMLtags($matches)
{
$tag = $matches[0];
$tagname = $matches[2];
if (in_array($tagname, $this->SKIPTAGS)) return $tag;
$opening = FALSE;
if ($matches[1]) { $this->OFFSET -= 1; } // тег является закрывающим
elseif (substr($tag, -2, 1) == '/') { ; } // тег является одиночным
else { $opening = TRUE; } // если тег не является ни одиночным, ни закрывающим, значит, он открывающий
if ($tagname == 'textarea' OR $tagname == 'pre' OR $tagname == 'script')
{ // эти теги вообще не трогаем, просто перенесем их
// полностью (со всем содержимым) на новую строку
if ($opening) { $replacement = " " . $tag; }
else $replacement = $tag . " ";
}
else
{
$replacement = " "
. str_repeat($this->SPACER, $this->OFFSET) . $tag . " "
. str_repeat($this->SPACER, $this->OFFSET + 1);
}
if ($opening) { $this->OFFSET += 1; }
return $replacement;
}
}
1. Это касается случая, когда внутри тегов <textarea> имеются переводы строки. Если при обработке кода никак их не защитить, то в содержимое тега добавятся символы, используемые для выравнивания кода, что приведет к искажению данных (по этой же причине нельзя и просто отмахнуться от переводов строки, заменив их на пробелы — для этого все манипуляции с непечатаемыми символами).
С тегами <pre> и <script> ситуация еще сложнее: в их содержимом помимо переводов строки могут встретиться еще и HTML-теги, которые будут выравниваться наряду с остальным HTML-кодом, если это специально не предотвратить (например, заменив скобки тегов на что-нибудь на время выравнивания, а потом вернуть их обратно).
Эта проблема также решается с помощью непечатаемых символов.?

2. Писать через классы пришлось из-за того, что в callback-функцию, вызываемую в preg_replace_callback(), невозможно нормальным способом передать никакие аргументы, кроме собственно массива совпадений, и уж тем более никак нельзя получить их обратно. ?

3. Из этих 0.05 сек 60% составляет непосредственно выравнивание тегов, 35% — замена переводов строки на пробелы внутри тегов и около 5% — удаление пустых строк.
Если в коде встречаются <textarea>, <pre> и <script>, время работы возрастает еще на несколько процентов.
Работу кода можно ускорить почти в три раза, если использовать для внутренности тегов выражения /<(/?)(w+)(?:[^>"']*|"[^"]*"|'[^']*')*>/ более «грязное» — /<(/?)(w+)[^>]*>/. Дополнительный набор альтернатив требуется для случая, когда внутри одинарных или двойных кавычек в теге встретится закрывающая скоба (например,<img alt="3 > 2 - верно!">). В реальной жизни так практически никогда не бывает, и те, кто готов на это положиться, могут использовать второе выражение вместо первого.
0 16 0
Без комментариев...