Вы не должны делать это с регулярными выражениями - по крайней мере, не только с регулярными выражениями.Вместо этого используйте правильный HTML-анализатор DOM, такой как PHP DOM-библиотека .Затем вы можете перебирать узлы, проверять, является ли это текстовым узлом, выполнять поиск по регулярному выражению и заменять текстовый узел соответствующим образом.
Что-то вроде этого должно сделать это:
$pattern = "~((?:http|https|ftp)://(?:\S*?\.\S*?))(?=\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)~i";
$doc = new DOMDocument();
$doc->loadHTML($str);
// for every element in the document
foreach ($doc->getElementsByTagName('*') as $elem) {
// for every child node in each element
foreach ($elem->childNodes as $node) {
if ($node->nodeType === XML_TEXT_NODE) {
// split the text content to get an array of 1+2*n elements for n URLs in it
$parts = preg_split($pattern, $node->nodeValue, -1, PREG_SPLIT_DELIM_CAPTURE);
$n = count($parts);
if ($n > 1) {
$parentNode = $node->parentNode;
// insert for each pair of non-URL/URL parts one DOMText and DOMElement node before the original DOMText node
for ($i=1; $i<$n; $i+=2) {
$a = $doc->createElement('a');
$a->setAttribute('href', $parts[$i]);
$a->setAttribute('target', '_blank');
$a->appendChild($doc->createTextNode($parts[$i]));
$parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
$parentNode->insertBefore($a, $node);
}
// insert the last part before the original DOMText node
$parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
// remove the original DOMText node
$node->parentNode->removeChild($node);
}
}
}
}
Ok,поскольку DOMNodeList *s из getElementsByTagName
и childNodes
являются live , каждое изменение в DOMотражается в этом списке, и поэтому вы не можете использовать foreach
, который также будет перебирать вновь добавленные узлы.Вместо этого вам нужно вместо этого использовать циклы for
и отслеживать добавленные элементы для увеличения указателей индекса и, в лучшем случае, предварительно рассчитанных границ массива соответствующим образом.
Но так как это довольно сложно в такой сложнойалгоритм (вам понадобится один указатель индекса и граница массива для каждого из трех циклов for
), использование рекурсивного алгоритма более удобно:
function mapOntoTextNodes(DOMNode $node, $callback) {
if ($node->nodeType === XML_TEXT_NODE) {
return $callback($node);
}
for ($i=0, $n=count($node->childNodes); $i<$n; ++$i) {
$nodesChanged = 0;
switch ($node->childNodes->item($i)->nodeType) {
case XML_ELEMENT_NODE:
$nodesChanged = mapOntoTextNodes($node->childNodes->item($i), $callback);
break;
case XML_TEXT_NODE:
$nodesChanged = $callback($node->childNodes->item($i));
break;
}
if ($nodesChanged !== 0) {
$n += $nodesChanged;
$i += $nodesChanged;
}
}
}
function foo(DOMText $node) {
$pattern = "~((?:http|https|ftp)://(?:\S*?\.\S*?))(?=\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)~i";
$parts = preg_split($pattern, $node->nodeValue, -1, PREG_SPLIT_DELIM_CAPTURE);
$n = count($parts);
if ($n > 1) {
$parentNode = $node->parentNode;
$doc = $node->ownerDocument;
for ($i=1; $i<$n; $i+=2) {
$a = $doc->createElement('a');
$a->setAttribute('href', $parts[$i]);
$a->setAttribute('target', '_blank');
$a->appendChild($doc->createTextNode($parts[$i]));
$parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
$parentNode->insertBefore($a, $node);
}
$parentNode->insertBefore($doc->createTextNode($parts[$i-1]), $node);
$parentNode->removeChild($node);
}
return $n-1;
}
$str = '<div>sometext http://www.somedomain.com/index.html sometext <img src="http//domain.com/image.jpg"> sometext sometext</div>';
$doc = new DOMDocument();
$doc->loadHTML($str);
$elems = $doc->getElementsByTagName('body');
mapOntoTextNodes($elems->item(0), 'foo');
Здесь mapOntoTextNodes
используется для отображения данного обратного вызоваФункция на каждом DOMText узле в документе DOM.Вы можете передать весь узел DOMDocument или только конкретный DOMNode (в данном случае только узел BODY
).
Функция foo
затем используется для поиска и замены простых URL-адресов в содержимом узла DOMText путем разбиения строки содержимого на non-URL / URL частей с использованием preg_split
при захвате использованного разделителя, в результате чего получается массив из 1 + 2 · n элементов.Затем части не-URL заменяются новыми узлами DOMText , а части URL заменяются новыми элементами A
, которые затем вставляются перед источником DOMText узел, который затем удаляется в конце.Поскольку этот mapOntoTextNodes
идет рекурсивно, достаточно просто вызвать эту функцию на определенном DOMNode .