Skip to content

Commit b451948

Browse files
committed
Refactor implementation to use DOMDocument instead of SimpleXml
1 parent 232ab1f commit b451948

File tree

3 files changed

+78
-135
lines changed

3 files changed

+78
-135
lines changed

src/PhpWord/TemplateProcessor.php

Lines changed: 66 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -318,33 +318,19 @@ public function cloneRow($search, $numberOfClones)
318318
*
319319
* @return string|null
320320
*/
321-
public function cloneBlock($blockname, $clones = 1, $replace = true)
321+
public function cloneBlock($blockname, $clones = 1)
322322
{
323-
$xmlBlock = null;
324-
325-
$matches = $this->findBlocks($blockname);
326-
327-
foreach ($matches as $match) {
328-
if (isset($match[1])) {
329-
$xmlBlock = $match[1];
330-
331-
$cloned = array();
332-
333-
for ($i = 1; $i <= $clones; $i++) {
334-
$cloned[] = preg_replace('/\${(.*?)}/', '${$1_' . $i . '}', $xmlBlock);
335-
}
336-
337-
if ($replace) {
338-
$this->tempDocumentMainPart = str_replace(
339-
$match[0],
340-
implode('', $cloned),
341-
$this->tempDocumentMainPart
342-
);
323+
$dom = \DOMDocument::loadXML($this->tempDocumentMainPart);
324+
$nodeSets = $this->findBlocks($blockname, $dom, 'inner');
325+
foreach ($nodeSets as $nodeSet) {
326+
for ($i = 1; $i < $clones; $i++ ) {
327+
foreach ($nodeSet as $node) {
328+
$nodeSet[0]->parentNode->insertBefore($node->cloneNode(true), $nodeSet[0]);
343329
}
344330
}
345331
}
346-
347-
return $xmlBlock;
332+
$this->deleteNodeSets($this->findBlocks($blockname, $dom, 'outer'));
333+
$this->tempDocumentMainPart = $dom->saveXML();
348334
}
349335

350336
/**
@@ -355,17 +341,19 @@ public function cloneBlock($blockname, $clones = 1, $replace = true)
355341
*/
356342
public function replaceBlock($blockname, $replacement)
357343
{
358-
$matches = $this->findBlocks($blockname);
359-
360-
foreach ($matches as $match) {
361-
if (isset($match[1])) {
362-
$this->tempDocumentMainPart = str_replace(
363-
$match[0],
364-
$replacement,
365-
$this->tempDocumentMainPart
366-
);
367-
}
368-
}
344+
$dom = \DOMDocument::loadXML($this->tempDocumentMainPart);
345+
$nodeSets = $this->findBlocks($blockname, $dom);
346+
foreach ($nodeSets as $nodeSet) {
347+
$newNode = $dom->createElement('t:marker');
348+
$nodeSet[0]->parentNode->insertBefore($newNode, $nodeSet[0]);
349+
}
350+
$this->deleteNodeSets($nodeSets);
351+
$xml = $dom->saveXML();
352+
$this->tempDocumentMainPart = str_replace(
353+
'<t:marker/>',
354+
$replacement,
355+
$xml
356+
);
369357
}
370358

371359
/**
@@ -375,7 +363,17 @@ public function replaceBlock($blockname, $replacement)
375363
*/
376364
public function deleteBlock($blockname)
377365
{
378-
$this->replaceBlock($blockname, '');
366+
$dom = \DOMDocument::loadXML($this->tempDocumentMainPart);
367+
$this->deleteNodeSets($this->findBlocks($blockname, $dom));
368+
$this->tempDocumentMainPart = $dom->saveXML();
369+
}
370+
371+
private function deleteNodeSets($nodeSets) {
372+
foreach ($nodeSets as $nodeSet) {
373+
foreach ($nodeSet as $node) {
374+
$node->parentNode->removeChild($node);
375+
}
376+
}
379377
}
380378

381379
/**
@@ -572,107 +570,45 @@ protected function getSlice($startPosition, $endPosition = 0)
572570
return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
573571
}
574572

575-
private function findBlocks($blockname)
573+
private function findBlocks($blockname, $domDoc, $type = 'complete')
576574
{
577-
// Parse the XML
578-
$xml = new \SimpleXMLElement($this->tempDocumentMainPart);
579-
580-
// Find the starting and ending tags
581-
$startNode = false;
582-
$endNode = false;
583-
$state = 'outside';
584-
$pairs = array();
585-
foreach ($xml->xpath('//w:t') as $node) {
586-
if (strpos($node, '${' . $blockname . '}') !== false) {
587-
$startNode = $node;
588-
$state = 'inside';
589-
continue;
590-
}
591-
592-
if ($state === 'inside' && strpos($node, '${/' . $blockname . '}') !== false) {
593-
$endNode = $node;
594-
$pairs[] = array($startNode, $endNode);
595-
$startNode = false;
596-
$endNode = false;
597-
$state = 'outside';
598-
}
599-
}
600-
601-
// Make sure we found the tags
602-
if (count($pairs) === 0) {
603-
return null;
604-
}
605-
606-
$result = array();
607-
foreach ($pairs as $pair) {
608-
$result[] = $this->findEnclosing($pair[0], $pair[1], $xml);
575+
$domXpath = new \DOMXpath($domDoc);
576+
$max = $domXpath->query('//w:p[contains(., "${'.$blockname.'}")]')->length;
577+
$nodeLists = array();
578+
for ($i = 1; $i <= $max; $i++) {
579+
$query = join(' | ', self::getQueryByType($type));
580+
581+
$data = array(
582+
'BLOCKNAME' => $blockname,
583+
'INDEX' => $i
584+
);
585+
$findFromTo = str_replace(array_keys($data), array_values($data), $query);
586+
$nodelist = $domXpath->query($findFromTo);
587+
$nodeLists[] = $nodelist;
609588
}
610-
611-
return $result;
589+
return $nodeLists;
612590
}
613591

614-
private static function getParentByName($node, $name)
592+
private static function getQueryByType($type)
615593
{
616-
while ($node->getName() !== $name) {
617-
// $node = $node->parent();
618-
$node = $node->xpath('..')[0];
594+
$parts = array(
595+
'//w:p[contains(., "${BLOCKNAME}")][INDEX]',
596+
// https://stackoverflow.com/questions/3428104/selecting-siblings-between-two-nodes-using-xpath
597+
'//w:p[contains(., "${BLOCKNAME}")][INDEX]/
598+
following-sibling::w:p[contains(., "${/BLOCKNAME}")][1]/
599+
preceding-sibling::w:p[
600+
preceding-sibling::w:p[contains(., "${BLOCKNAME}")][INDEX]
601+
]',
602+
'//w:p[contains(., "${/BLOCKNAME}")][INDEX]'
603+
);
604+
switch ($type) {
605+
case 'complete':
606+
return $parts;
607+
case 'inner':
608+
return array($parts[1]);
609+
case 'outer':
610+
return array($parts[0], $parts[2]);
619611
}
620-
621-
return $node;
622612
}
623613

624-
private function findEnclosing($startNode, $endNode, $xml)
625-
{
626-
// Find the parent <w:p> nodes for startNode & endNode
627-
$startNode = self::getParentByName($startNode, 'p');
628-
$endNode = self::getParentByName($endNode, 'p');
629-
630-
/*
631-
* NOTE: Because SimpleXML reduces empty tags to "self-closing" tags.
632-
* We need to replace the original XML with the version of XML as
633-
* SimpleXML sees it. The following example should show the issue
634-
* we are facing.
635-
*
636-
* This is the XML that my document contained orginally.
637-
*
638-
* ```xml
639-
* <w:p>
640-
* <w:pPr>
641-
* <w:pStyle w:val="TextBody"/>
642-
* <w:rPr></w:rPr>
643-
* </w:pPr>
644-
* <w:r>
645-
* <w:rPr></w:rPr>
646-
* <w:t>${CLONEME}</w:t>
647-
* </w:r>
648-
* </w:p>
649-
* ```
650-
*
651-
* This is the XML that SimpleXML returns from asXml().
652-
*
653-
* ```xml
654-
* <w:p>
655-
* <w:pPr>
656-
* <w:pStyle w:val="TextBody"/>
657-
* <w:rPr/>
658-
* </w:pPr>
659-
* <w:r>
660-
* <w:rPr/>
661-
* <w:t>${CLONEME}</w:t>
662-
* </w:r>
663-
* </w:p>
664-
* ```
665-
*/
666-
667-
$this->tempDocumentMainPart = $xml->asXml();
668-
669-
// Find the xml in between the tags
670-
preg_match(
671-
'/' . preg_quote($startNode->asXml(), '/') . '(.*?)' . preg_quote($endNode->asXml(), '/') . '/is',
672-
$this->tempDocumentMainPart,
673-
$matches
674-
);
675-
676-
return $matches;
677-
}
678614
}

tests/PhpWord/TemplateProcessorTest.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,21 @@ public function testCloneDeleteBlock()
210210
{
211211
$templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-delete-block.docx');
212212

213-
$this->assertEquals(
214-
array('DELETEME', '/DELETEME', 'CLONEME', '/CLONEME'),
215-
$templateProcessor->getVariables()
216-
);
217-
218213
$docName = 'clone-delete-block-result.docx';
219214
$templateProcessor->cloneBlock('CLONEME', 3);
215+
$templateProcessor->cloneBlock('CLONEMEONCE');
220216
$templateProcessor->deleteBlock('DELETEME');
217+
$templateProcessor->replaceBlock('REPLACEME', '
218+
<w:p>
219+
<w:pPr>
220+
<w:pStyle w:val="Normal"/>
221+
<w:rPr/>
222+
</w:pPr>
223+
<w:r>
224+
<w:rPr/>
225+
<w:t>You have been replaced!</w:t>
226+
</w:r>
227+
</w:p>');
221228
$templateProcessor->saveAs($docName);
222229
$docFound = file_exists($docName);
223230
unlink($docName);
-1.42 KB
Binary file not shown.

0 commit comments

Comments
 (0)