Search code examples
phpxmlnestedattributeslibreoffice

PHP How To Insert Nested Elements Libreoffice Style


Libreoffice stores Writer document content in an XML formatted file. In PHP I would like to insert text with a different formatting into a text paragraph. Unfortunately, Libreoffice does that with a nested element inside the text of another element. Here's a simplified example:

<text:p text:style-name="P1">

   the quick brown
        <text:span text:style-name="T1"> fox jumps over</text:span>      
   the lazy dog

</text:p>

I have found no SimpleXML or XML DOM function in PHP that lets me insert a new element inside the text of another element as is required here. Am I overlooking something here?


Solution

  • SimpleXML does not do well with mixed child nodes but in DOM it is not difficult, just a little verbose. Keep in mind that in DOM anything is a node, not just the elements. So what you're trying to do is to replace a single text node with three new nodes - A text node, the new element node and another text node.

    $xmlns = [
      'text' => 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'
    ];
    
    $xml = <<<'XML'
    <text:p 
       text:style-name="P1" 
       xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0">
       the quick brown fox jumps over the lazy dog
    </text:p>
    XML;
    
    $document = new DOMDocument();
    $document->loadXML($xml);
    $xpath = new DOMXpath($document);
    
    $searchFor = 'fox jumps over';
    
    // iterate over text nodes containing the search string
    $expression = '//text:p//text()[contains(., "'.$searchFor.'")]';
    foreach ($xpath->evaluate($expression) as $textNode) {
        // split the text content at the search string and capture any part
        $parts = preg_split(
            '(('.preg_quote($searchFor).'))', 
            $textNode->textContent, 
            -1, 
            PREG_SPLIT_DELIM_CAPTURE
        );
        // here should be at least two parts
        if (count($parts) < 2) {
            continue;
        }
        // fragments allow to treat several nodes like one
        $fragment = $document->createDocumentFragment();
        foreach ($parts as $part) {
            // matched the text
            if ($part === $searchFor) {
                // create the new span
                $fragment->appendChild(
                    $span = $document->createElementNS($xmlns['text'], 'text:span')
                );
                $span->setAttributeNS($xmlns['text'], 'text:style-name', 'T1');
                $span->appendChild($document->createTextNode($part));
            } else {
                // add the part as a new text node
                $fragment->appendChild($document->createTextNode($part));
            }   
        }
        // replace the text node with the fragment
        $textNode->parentNode->replaceChild($fragment, $textNode);
    }
    
    echo $document->saveXML();