PHP – автоматическое закрытие всех открытых HTML тегов
Все знают, что в административной панели сайта хорошо править контент страниц в визуальном редакторе, который имеет название WYSIWYG. Вот и наш редактор как-то странно себя начал вести, перестал закрывать HTML теги (не все, а те, которые ему хочется). Т.к. JS программист в это время был в отпуске нужно было срочно исправить проблему. Пришлось вставлять костыль в виде скрипта на PHP, которые будет автоматом закрыть все незакрытые теги.
Например корявый код:
<a href="http://artkiev.com">Разработка интернет-магазинов</a> <div><br />Лучшие цены <div><div> на </div> раскрутку <b>и оптимизацию <p>сайтов</b>
После обработки получаем:
<a href="http://artkiev.com">Разработка интернет-магазинов</a> <div><br />Лучшие цены <div><div> на </div> раскрутку <b>и оптимизацию <p>сайтов</b></div></div></p>
Функция:
<? function close_tags($content) { $position = 0; $open_tags = array(); //теги для игнорирования $ignored_tags = array('br', 'hr', 'img'); while (($position = strpos($content, '<', $position)) !== FALSE) { //забираем все теги из контента if (preg_match("|^<(/?)([a-z\d]+)\b[^>]*>|i", substr($content, $position), $match)) { $tag = strtolower($match[2]); //игнорируем все одиночные теги if (in_array($tag, $ignored_tags) == FALSE) { //тег открыт if (isset($match[1]) AND $match[1] == '') { if (isset($open_tags[$tag])) $open_tags[$tag]++; else $open_tags[$tag] = 1; } //тег закрыт if (isset($match[1]) AND $match[1] == '/') { if (isset($open_tags[$tag])) $open_tags[$tag]--; } } $position += strlen($match[0]); } else $position++; } //закрываем все теги foreach ($open_tags as $tag => $count_not_closed) { $content .= str_repeat("</{$tag}>", $count_not_closed); } return $content; } ?>
Пример использования:
$text = close_tags($_POST['body_page']);
Другие публикации:
Написать комментарий через:
- Локальный блог
Ваш отзыв
Отзывов (2) на «PHP – автоматическое закрытие всех открытых HTML тегов»

Хм.. Странно, но почему-то нигде не нашёл функции, которая открывает неоткрытые теги.. Пришлось модифицировать эту. Ловите, кому надо:
function open_close_tags($content){
$position=0;
$open_tags=array();
$ignored_tags=array(‘br’, ‘hr’, ‘img’);//теги для игнорирования
while(($position=strpos($content, ‘<', $position)) !== false){
if(preg_match("|^]*>|i”, substr($content, $position), $match)){//забираем все теги из контента
$tag=strtolower($match[2]);
if(!in_array($tag, $ignored_tags)){//игнорируем все одиночные теги
if(!isset($open_tags[$tag])){
$open_tags[$tag]=0;
}
if(isset($match[1]) AND $match[1] == ”){//тег открыт
$open_tags[$tag]++;
}
if(isset($match[1]) AND $match[1] == ‘/’)//тег закрыт
{
$open_tags[$tag]–;
}
}
$position += strlen($match[0]);
}else{
$position++;
}
}
foreach($open_tags as $tag => $count){
if($count>0){
$content.=str_repeat(“”, $count);//закрываем незакрытые
}
if($count<0){
$content=str_repeat("”, -$count).$content;//открываем неоткрытые
}
}
return $content;
}
Классная функция, лучшая из тех что нашёл, tidy для этой задачи уж больно сложен в установке, другие библиотеки, вроде HTMLPurifier уж слишком громоздкие.
Жаль что она закрывает теги не совсем верно – не по порядку, в итоге блочный элемент перекроет строчный и может поехать вёрстка.
Также не хватает открытия не открытых тегов, например если есть но нет его открытия, также может поехать вёрстка…
Для больших текстов оказывается очень медленной из-за работы через str_pos с отступом, большоего числа нарезок текста и частого вызова регулярки.
Скорость работы можно существенно ускорить, если немного заменить логику работы.
Немного доработал, возможно кому-то функция окажется полезной:
// Функция закрывает все не закрытые HTML теги
function close_tags($content) {
// Открытые теги
$open_tags = array();
// Закрытые теги
$closed_tags = array();
// Битые теги (нет закрывающего символа >)
$broken_tags = array();
//теги для игнорирования
$ignored_tags = array(‘br’ => 1, ‘hr’ => 1, ‘img’ => 1);
// Массив преобразовынных тегов из верхнего регистра в нижний
$cleaned_tags = array();
// Ищем сразу все теги в контенте
preg_match_all(“|?)|ius”, $content, $tag_matches, PREG_OFFSET_CAPTURE);
// Если найдены теги
if(!empty($tag_matches)) {
// Проходим по всем тегам, которые мы нашли
foreach ($tag_matches[2] as $k => $v) {
// Если мы ещё не преобразовывали данный тег к нижнему регистру
if(!isset($cleaned_tags[$tag_matches[2][$k][0]])) {
// Уменьшаем тег
$cleaned_tags[$tag_matches[2][$k][0]] = mb_strtolower($tag_matches[2][$k][0]);
}
// Используем уменьшенную версию тега из кэша, для ускорения работы функции
$tag = $cleaned_tags[$tag_matches[2][$k][0]];
// Игнорируем все одиночные теги
if(!isset($ignored_tags[$tag])) {
// Если регулярка нашла тег
if (isset($tag_matches[1][$k][0])) {
// Если нет закрывающего тега, значит он содержит ошибку и его нужно полностью удалить из контента.
if($tag_matches[3][$k][0] == ”) {
// Дописываем задачу на удаление тега из контента (Странно что мультибайт строки не поддерживаются опцией PREG_OFFSET_CAPTURE TODO проверить в ближайшем будущем не пофиксили ли такое поведение разработчики)
$broken_tags[] = array($tag_matches[0][$k][0], mb_strlen(substr($content, 0, $tag_matches[0][$k][1])) , mb_strlen($tag_matches[0][$k][0]));
// Если у тега всего хватает и он похож на валидный TODO сделать проверку на открытие и закрытие кавычек у атрибутов внутри тега
} else {
//тег открывается
if($tag_matches[1][$k][0] == ”) {
$open_tags[] = $tag;
// тег закрывается
} else {
// По умолчанию считаем, что среди уже найденных открытых тегов не удалось найти тот, который закрывается текущим
$is_found = 0;
// Если есть открытые теги
if(!empty($open_tags)) {
// Получаем последний открытый тег
$last = end($open_tags);
// Если открытый тег был иным, надо понять какой тег закрыли…
if($tag !== $last) {
// Определяем количество текущих открытых тегов
$c = count($open_tags);
// Проходим по всем открытым тегам в обратном порядке
for($i=$c-2; $i >= 0; $i–) {
// Если текущий тег тот же что и закрываемый
if($open_tags[$i] == $tag) {
// Удаляем открытый тег из массива
unset($open_tags[$i]);
// Проставляем ключи для массива заново, чтобы открытые теги шли по порядку 1,2,3…
$open_tags = array_values($open_tags);
// Запоминаем, что тег нашёлся, не нужно будет его дописывать перед контентом
$is_found = 1;
break;
}
}
unset($c, $i);
// Если закрывается последний открытый тег
} else {
// Убираем информацию о том, что тег открывался
array_pop($open_tags);
// Запоминаем, что тег нашёлся, не нужно будет его дописывать перед контентом
$is_found = 1;
}
}
// Если не удалось найти тег, который был закрыт, похоже что он не был открыт ранее, нужно будет дописать его в начале контента
if(!$is_found) {
$closed_tags[] = ”;
}
unset($is_found);
}
}
}
}
}
unset($k, $v);
}
// Если есть битые теги
if(!empty($broken_tags)) {
// Определяем количество битых тегов
$c = count($broken_tags);
// Проходим по всем битым тегам в обратном порядке
for($i=$c-1; $i >= 0; $i–) {
// Удаляем из контента битый тег, соединяя 2 части контента до битого тега и после битого тега
$content = mb_substr($content, 0, $broken_tags[$i][1]) . mb_substr($content, $broken_tags[$i][1] + $broken_tags[$i][2]);
}
unset($c, $i);
}
unset($broken_tags);
// Если есть не открытые теги
if(!empty($closed_tags)) {
// Теги нужно будет склеивать в обратном порядке, поэтому переворачиваем массив
$closed_tags = array_reverse($closed_tags);
// Дописываем открывающие теги в начале контента
$content = implode(”, $closed_tags) . $content;
}
unset($closed_tags);
// Если есть не закрытые теги
if(!empty($open_tags)) {
// Определяем количество текущих открытых тегов
$c = count($open_tags);
// Проходим по всем открытым тегам в обратном порядке
for($i=$c-1; $i >= 0; $i–) {
// Дописываем закрывающий тег на каждый открытый
$content .= ”;
}
unset($c, $i);
}
unset($open_tags);
return $content;
}
Тесты:
before:<b>тест
after:<b>тест</b>
before:<b>тест<span test=’
after:<b>тест</b>
before:<b>тест<table></table <span test=’
after:<b>тест<table></table></b>
before:<td></td></tr></table <span test=’
after:<tr><td></td></tr>
before:<b>тест<table><tr><table><tr><td></td></tr></table <span test=’
after:<b>тест<table><tr><table><tr><td></td></tr></table></tr></table></b>
before:<B>тест<tabLe><TR><tableattr=’
><tr><td></td></tr></table <span test=’</div>
after:<div><B>тест<tabLe><TR><tableattr=’
><tr><td></td></tr></div></table></tr></table></b>
before:Интере<b>сный<div> тек<ст
after:Интере<b>сный<div> тек<ст</div></b>
before:Интере<b>сный<div> тек<dd attr=’<div>
after:Интере<b>сный<div> тек<div></div></div></b>