Wordwrap / Cut Text в строке HTML

вот что я хочу сделать: у меня есть строка, содержащая HTML-теги, и я хочу ее отрезать, используя функцию wordwrap, исключая теги HTML.

Я застрял :

public function textWrap($string, $width) { $dom = new DOMDocument(); $dom->loadHTML($string); foreach ($dom->getElementsByTagName('*') as $elem) { foreach ($elem->childNodes as $node) { if ($node->nodeType === XML_TEXT_NODE) { $text = trim($node->nodeValue); $length = mb_strlen($text); $width -= $length; if($width <= 0) { // Here, I would like to delete all next nodes // and cut the current nodeValue and finally return the string } } } } } 

Я не уверен, что сейчас делаю это правильно. Надеюсь, это ясно …

РЕДАКТИРОВАТЬ :

Вот пример. У меня есть этот текст

  <p> <span class="Underline"><span class="Bold">Test to be cut</span></span> </p><p>Some text</p> 

Предположим, я хочу сократить его на 6-м символе, я хотел бы вернуть это:

 <p> <span class="Underline"><span class="Bold">Test to</span></span> </p> 

Как я писал в комментарии, вам сначала нужно найти текстовое смещение, где нужно сделать разрез.

Прежде всего, я устанавливаю DOMDocument содержащий фрагмент HTML, а затем DOMDocument тело, которое представляет его в DOM:

 $htmlFragment = <<<HTML <p> <span class="Underline"><span class="Bold">Test to be cut</span></span> </p><p>Some text </p> HTML; $dom = new DOMDocument(); $dom->loadHTML($htmlFragment); $parent = $dom->getElementsByTagName('body')->item(0); if (!$parent) { throw new Exception('Parent element not found.'); } 

Затем я использую класс TextRange чтобы найти место, где нужно сделать разрез, и я использую TextRange чтобы на самом деле сделать разрез и найти DOMNode который должен стать последним узлом фрагмента:

 $range = new TextRange($parent); // find position where to cut the HTML textual represenation // by looking for a word or the at least matching whitespace // with a regular expression. $width = 17; $pattern = sprintf('~^.{0,%d}(?<=\S)(?=\s)|^.{0,%1$d}(?=\s)~su', $width); $r = preg_match($pattern, $range, $matches); if (FALSE === $r) {  throw new Exception('Wordcut regex failed.'); } if (!$r) { throw new Exception(sprintf('Text "%s" is not cut-able (should not happen).', $range)); } 

Это регулярное выражение находит смещение, где можно отрезать вещи в текстовом представлении, доступном для $range . Шаблон регулярных выражений вдохновлен еще одним ответом, который обсуждает его более подробно и был слегка изменен для соответствия этим потребностям.

 // chop-off the textnodes to make a cut in DOM possible $range->split($matches[0]); $nodes = $range->getNodes(); $cutPosition = end($nodes); 

Поскольку возможно, что нечего резать (например, body станет пустым), мне нужно иметь дело с этим частным случаем. В противном случае – как отмечено в комментарии – все следующие узлы необходимо удалить:

 // obtain list of elements to remove with xpath if (FALSE === $cutPosition) { // if there is no node, delete all parent children $cutPosition = $parent; $xpath = 'child::node()'; } else { $xpath = 'following::node()'; } 

Остальное прямо: запросите xpath, удалите узлы и выведите результат:

 // execute xpath $xp = new DOMXPath($dom); $remove = $xp->query($xpath, $cutPosition); if (!$remove) { throw new Exception('XPath query failed to obtain elements to remove'); } // remove nodes foreach($remove as $node) { $node->parentNode->removeChild($node); } // inner HTML (PHP >= 5.3.6) foreach($parent->childNodes as $node) { echo $dom->saveHTML($node); } 

Полный пример кода доступен в кодексе viper, вкл. класс TextRange . У кодекада есть ошибка, так что результат не является правильным (связанный: порядок результата запроса XPath ). Фактический вывод следующий:

 <p> <span class="Underline"><span class="Bold">Test to</span></span></p> 

Поэтому будьте осторожны, у вас есть текущая версия libxml (как правило, случай), а выход foreach в конце использует функцию PHP saveHTML доступную с этим параметром с PHP 5.3.6. Если у вас нет этой версии PHP, возьмите альтернативу, как описано в разделе Как получить содержимое xml узла в виде строки? или аналогичный вопрос.

Когда вы внимательно смотрите в моем примере кода, вы можете заметить, что длина разреза довольно велика ( $width = 17; ). Это связано с тем, что перед текстом есть много пробельных символов. Это может быть изменено путем принятия регулярного выражения капли любое количество пробелов в нем и / или путем обрезки TextRange первую очередь. Второй вариант требует больше функциональности, я написал что-то быстрое, которое можно использовать после создания начального диапазона:

 ... $range = new TextRange($parent); $trimmer = new TextRangeTrimmer($range); $trimmer->trim(); ... 

Это позволит удалить ненужные пробелы слева и справа внутри вашего фрагмента HTML. Код TextRangeTrimmer следующий:

 class TextRangeTrimmer { /** * @var TextRange */ private $range; /** * @var array */ private $charlist; public function __construct(TextRange $range, Array $charlist = NULL) { $this->range = $range; $this->setCharlist($charlist); } /** * @param array $charlist list of UTF-8 encoded characters * @throws InvalidArgumentException */ public function setCharlist(Array $charlist = NULL) { if (NULL === $charlist) $charlist = str_split(" \t\n\r\0\x0B") ; $list = array(); foreach($charlist as $char) { if (!is_string($char)) { throw new InvalidArgumentException('Not an Array of strings.'); } if (strlen($char)) { $list[] = $char; } } $this->charlist = array_flip($list); } /** * @return array characters */ public function getCharlist() { return array_keys($this->charlist); } public function trim() { if (!$this->charlist) return; $this->ltrim(); $this->rtrim(); } /** * number of consecutive charcters of $charlist from $start to $direction * * @param array $charlist * @param int $start offset * @param int $direction 1: forward, -1: backward * @throws InvalidArgumentException */ private function lengthOfCharacterSequence(Array $charlist, $start, $direction = 1) { $start = (int) $start; $direction = max(-1, min(1, $direction)); if (!$direction) throw new InvalidArgumentException('Direction must be 1 or -1.'); $count = 0; for(;$char = $this->range->getCharacter($start), $char !== ''; $start += $direction, $count++) if (!isset($charlist[$char])) break; return $count; } public function ltrim() { $count = $this->lengthOfCharacterSequence($this->charlist, 0); if ($count) { $remainder = $this->range->split($count); foreach($this->range->getNodes() as $textNode) { $textNode->parentNode->removeChild($textNode); } $this->range->setNodes($remainder->getNodes()); } } public function rtrim() { $count = $this->lengthOfCharacterSequence($this->charlist, -1, -1); if ($count) { $chop = $this->range->split(-$count); foreach($chop->getNodes() as $textNode) { $textNode->parentNode->removeChild($textNode); } } } } 

Надеюсь, это полезно.

Если использование разбора DOM не является целью, и вам нужен только транкат HTML – посмотрите на функцию cot_string_truncate в этом Gist . Это взято из Cotonti CMF.

Он обрабатывает обычный текст или HTML. Вы можете установить длину и выбрать способ трансакции текста – точные символы по пределу или по ближайшей границе слова.

Он надлежащим образом обрабатывает объекты HTML и символы серийного пространства как один (как он просматривается в браузере) – так что ваш пример должен хорошо работать:

 $test_str = "<p> <span class=\"Underline\"><span class=\"Bold\">Test to be cut</span></span> </p><p>Some text</p>"; echo cot_string_truncate($test_str, 8); 

Результат:

 <p> <span class="Underline"><span class="Bold">Test to</span></span></p>