Как я уже писал в комментарии, сначала нужно найти текстовое смещение, где делать вырез.
Прежде всего я устанавливаю DOMDocument
, содержащий фрагмент HTML, а затем выбираю тело, которое представляет его в 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);
}
}
}
}
Надеюсь, это полезно.