views:

48

answers:

1

I'm trying to use the simple_html_dom php class to create a find and replace function that looks for keywords and replace them by a link to a definition of the keyword, with the keyword as link text.

How can i find and replace "Dexia" with <a href="info.php?tag=dexia">Dexia</a> using this class, inside a string such as <div><p>The CEO of the Dexia bank has just decided to retire.</p></div> ?

+1  A: 

That's somewhat tricky, but you could do it this way:

$html = <<< HTML
<div><p>The CEO of the Dexia bank <em>has</em> just decided to retire.</p></div>
HTML;

I've added an emphasis element just to illustrate that it works with inline elements too.

Setup

$dom = new DOMDocument;
$dom->formatOutput = TRUE;
$dom->loadXML($html);
$xpath = new DOMXPath($dom);
$nodes = $xpath->query('//text()[contains(., "Dexia")]');

The interesting thing above is the XPath of course. It queries the loaded DOM for all DOMText nodes containing the needle "Dexia". The result is DOMNodeList (as usual).

The replacement

foreach($nodes as $node) {
    $link     = '<a href="info.php?tag=dexia">Dexia</a>';
    $replaced = str_replace('Dexia', $link, $node->wholeText);
    $newNode  = $dom->createDocumentFragment();
    $newNode->appendXML($replaced);
    $node->parentNode->replaceChild($newNode, $node);
}
echo $dom->saveXML($dom->documentElement);

The found $node will contain the string The CEO of the Dexia bank for wholeText, despite it being inside the P element. That is because the $node has a sibling DOMElement with the emphasis after bank. I am creating the link as a string instead of a node and replace all occurences of "Dexia" (regardless of word boundary - that would be a good call for Regex) in the wholeText with it. Then I create a DocumentFragment from the resulting string and replace the DOMText node with it.

W3C vs PHP

Using DocumentFragement::applyXML() is a non-standard approach, because the method is not part of the W3C DOM Specs.

If you would want to do the replacement with the standard API, you'd first have to create the A Element as a new DOMElement. Then you'd have to find the offset of "Dexia" in the nodeValue of the DOMText and split the DOMText Node into two nodes at that position. Remove Dexia from the returned sibling and insert the Link Element, before the second one. Repeat this procedure with the sibling node until no more Dexia strings are found in the node. Here is how to do it for one occurence of Dexia:

foreach($nodes as $node) {
    $link = $dom->createElement('a', 'Dexia');
    $link->setAttribute('href', 'info.php?tag=dexia');
    $offset  = strpos($node->nodeValue, 'Dexia');
    $newNode = $node->splitText($offset);
    $newNode->deleteData(0, strlen('Dexia'));
    $node->parentNode->insertBefore($link, $newNode);
}

And finally the output

<div>
  <p>The CEO of the <a href="info.php?tag=dexia">Dexia</a> bank <em>has</em> just decided to retire.</p>
</div>
Gordon
DocumentFragment has been in DOM Core since DOM 1 - http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html and in DOM Level 2 Core http://www.w3.org/TR/DOM-Level-2-Core/core.html and in DOM Level 3 Core http://www.w3.org/TR/DOM-Level-3-Core/core.html
nathan
@nathan sorry, my fault. I was refering to the applyXML method. Fixed.
Gordon
@Gordon no need for apologies :)
nathan
@gordon: that's very interesting. I simplified my question as i expected something simpler; in reality i have around 500 keywords. Does this approach scales correctly? I'd have to have a double foreach loop i guess.
pixeline
@pixeline well, it's tricky in the beginning but only because DOM is quite verbose and you have to think in nodes instead of text. I understand why people want to do it with Regex, but it really ain't that difficult once you got into it. I mean, it's just 15 lines of code. Put it into a proper Service class and you got a cool reusable tool. I don't know how it performs though, so you would have to benchmark it yourself and see if it is acceptable for you.
Gordon
It works quite well (i had to change to loadHTML as the html i'm passing comes from a CMS and might not be well-formed.)
pixeline