Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to move inner content with DOMDocumentFragment?

I have a horrible algorithm, to "remove a node", moving its inner content to its parent node (see below)... But I think is possible to develop a better algorithm, using DOMDocumentFragment (and not using saveXML/loadXML).

The algorithm below was inspired by renameNode().

 /**
  * Move the content of the $from node to its parent node.
  * Conditions: parent not a document root, $from not a text node.  
  * @param DOMElement $from to be removed, preserving its contents.
  * @return true if changed, false if not.
  */
 function moveInner($from) {
     $to = $from->parentNode;
     if ($from->nodeType==1 && $to->parentNode->nodeType==1) {     
        // Scans $from, and record information:
        $lst = array(); // to avoid "scan bugs" of DomNodeList iterator
        foreach ($to->childNodes as $e)
           $lst[] =  array($e);
        for($i=0; $i<count($lst); $i++)
          if ($lst[$i][0]->nodeType==1 && $from->isSameNode($lst[$i][0])) {  
            $lst[$i][1] = array();
            foreach ($lst[$i][0]->childNodes as $e)
                $lst[$i][1][] = $e;
          }

        // Build $newTo (rebuilds the parent node):
        $newTo = $from->ownerDocument->createElement($to->nodeName);
        foreach ($to->attributes as $a) {
        $newTo->setAttribute($a->nodeName, $a->nodeValue);
        }
        foreach ($lst as $r) {
        if (count($r)==1)
            $newTo->appendChild($r[0]);
        else foreach ($r[1] as $e)
            $newTo->appendChild($e);
        }

        // Replaces it:
        $to->parentNode->replaceChild($newTo, $to);
        return true;

    } else
        return false;
 }

Example

INPUT

<html id="root">
<p id="p1"><i>Title</i></p>
<p id="p2"><b id="b1">Rosangela<sup>1</sup>, Maria<sup>2</sup></b>, 
           <b>Eduardo<sup>4</sup></b>
</p>
</html>

OUTPUT of moveInner($dom->getElementById('p1'))

... <p id="p1">Title</p> ...

OUTPUT of moveInner($dom->getElementById('b1'))

... <p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>, 
        <b>Eduardo<sup>4</sup></b>
    </p> ...

There are no changes in moveInner($dom->getElementById('root')), or moveInner($dom->getElementById('p1')) after first use.

PS: is like a "TRIM TAG" function.

like image 702
Peter Krauss Avatar asked Sep 17 '13 12:09

Peter Krauss


1 Answers

As you move inside the same document this actually is by far not so much hassle. The code you've posted alone had already many places that could be optimized just on it's own, for example to turn a childNodes NodeList into an array just use iterator_to_array:

$children = iterator_to_array($from->childNodes);

Also you should use more speaking variable names, there is no problem in having longer names. It just makes the code more readable and leaves the room to view the more important stuff faster:

/**
 * Move the content of the $from node to its parent node.
 *
 * @param DOMElement $from to be removed, preserving its contents.
 * @return DOMElement the element removed (w/o it's former children)
 * @throws InvalidArgumentException in case there is no parent element
 */
function moveInner(DOMElement $from)
{
    if (!$from->parentNode instanceof DOMElement) {
        throw new InvalidArgumentException(
            'DOMElement does not have a parent DOMElement node.'
        );
    }

    /** @var DOMNode[] $children */
    $children = iterator_to_array($from->childNodes);
    foreach ($children as $child) {
        $from->parentNode->insertBefore($child, $from);
    }

    return $from->parentNode->removeChild($from);
}

It just works. If you insert the same element into another place in DOMDocument, the element is moved, not duplicated.

If you want to duplicate (so to preserve the child, not move it), you can use the child nodes as prototypes and just clone them. As this function returns the element that is removed, it the contains the copy.

First the example w/o clone, just the function as above:

$removed = moveInner($doc->getElementById('b1'));

echo $doc->saveHTML(), "\nRemoved: ", $doc->saveHTML($removed);

Output:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html id="root"><body><p id="p1"><i>Title <b>2</b></i></p>
<p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>,
           <b>Eduardo<sup>4</sup></b>
</p>
</body></html>

Removed: <b id="b1"></b>

Then second the modified function, the change is just adding clone in the following line:

        $from->parentNode->insertBefore(clone $child, $from);
                                        #####

Output:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html id="root"><body><p id="p1"><i>Title <b>2</b></i></p>
<p id="p2">Rosangela<sup>1</sup>, Maria<sup>2</sup>,
           <b>Eduardo<sup>4</sup></b>
</p>
</body></html>

Removed: <b id="b1">Rosangela<sup>1</sup>, Maria<sup>2</sup></b>

I hope this is helpful and matches your needs. You really had very much code there, probably a bit misleaded by the replace node scenario which is different. Also in that scenario I was patching some different error code that is not always the best base to change into good code.

This btw. reminded me about a question where clone was also very helpful which I just answered today:

  • Appending an Element multiple times in DOMDocument
like image 136
hakre Avatar answered Sep 28 '22 08:09

hakre